mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
FSO doesn't appear to use this any longer. And even when it was enabled the requirements made it largely useless anyway. Removing to clean up cfile a bit and so we don't have to keep maintaining it.
1064 lines
30 KiB
C++
1064 lines
30 KiB
C++
#include <cstdio>
|
|
#include <cstring>
|
|
|
|
#include "bmpman/bmpman.h"
|
|
#include "cfile/cfile.h"
|
|
#include "globalincs/pstypes.h"
|
|
#include "graphics/2d.h"
|
|
#include "pngutils/pngutils.h"
|
|
#include "utils/base64.h"
|
|
|
|
struct png_status {
|
|
CFILE* cfp = nullptr;
|
|
const char* filename = nullptr;
|
|
bool reading_header = false;
|
|
bool writing = false;
|
|
};
|
|
|
|
/*
|
|
* @brief copy/pasted from libpng
|
|
*/
|
|
static void png_scp_read_data(png_structp png_ptr, png_bytep data, png_size_t length)
|
|
{
|
|
png_size_t check;
|
|
|
|
if (png_ptr == NULL)
|
|
return;
|
|
|
|
png_status* status = reinterpret_cast<png_status*>(png_get_io_ptr(png_ptr));
|
|
|
|
/* fread() returns 0 on error, so it is OK to store this in a png_size_t
|
|
* instead of an int, which is what fread() actually returns.
|
|
*/
|
|
check = (png_size_t)cfread(data, (png_size_t)1, (int)length, status->cfp);
|
|
if (check != length)
|
|
png_error(png_ptr, "Read Error");
|
|
}
|
|
|
|
static void png_scp_write_data(png_structp png_ptr, png_bytep data, png_size_t length) {
|
|
auto status = static_cast<png_status*>(png_get_io_ptr(png_ptr));
|
|
|
|
Assertion(status != nullptr, "Invalid file pointer in PNG writing function.");
|
|
|
|
auto check = (png_size_t)cfwrite(data, (png_size_t)1, (int)length, status->cfp);
|
|
if (check != length)
|
|
png_error(png_ptr, "Write Error");
|
|
}
|
|
static void png_scp_flush(png_structp png_ptr) {
|
|
auto status = static_cast<png_status*>(png_get_io_ptr(png_ptr));
|
|
|
|
Assertion(status != nullptr, "Invalid file pointer in PNG writing function.");
|
|
|
|
cflush(status->cfp);
|
|
}
|
|
|
|
static png_voidp png_malloc_fn(png_structp, png_size_t size)
|
|
{
|
|
return vm_malloc(size);
|
|
}
|
|
|
|
static void png_free_fn(png_structp, png_voidp ptr)
|
|
{
|
|
vm_free(ptr);
|
|
}
|
|
|
|
static void png_error_fn(png_structp png_ptr, png_const_charp message)
|
|
{
|
|
png_status* status = reinterpret_cast<png_status*>(png_get_error_ptr(png_ptr));
|
|
|
|
if (status->writing) {
|
|
mprintf(("PNG error while writing %s: %s\n", status->filename, message));
|
|
} else {
|
|
mprintf(("PNG error while reading %s of %s: %s\n", status->reading_header ? "header" : "pixel data", status->filename, message));
|
|
}
|
|
|
|
longjmp(png_jmpbuf(png_ptr), 1);
|
|
}
|
|
|
|
static void png_warning_fn(png_structp png_ptr, png_const_charp message)
|
|
{
|
|
png_status* status = reinterpret_cast<png_status*>(png_get_error_ptr(png_ptr));
|
|
|
|
if (status->writing) {
|
|
nprintf(("PNG warning", "PNG warning while writing %s: %s\n", status->filename, message));
|
|
} else {
|
|
nprintf(("PNG warning", "PNG warning while reading %s of %s: %s\n", status->reading_header ? "header" : "pixel data", status->filename, message));
|
|
}
|
|
}
|
|
|
|
|
|
static int png_read_header_data(int* w, int* h, int* bpp, png_voidp status, png_error_ptr error, png_error_ptr warning, png_rw_ptr readFunc, std::function<void()> onClose)
|
|
{
|
|
png_infop info_ptr;
|
|
png_structp png_ptr;
|
|
|
|
/* Create and initialize the png_struct with the desired error handler
|
|
* functions. If you want to use the default stderr and longjump method,
|
|
* you can supply NULL for the last three parameters. We also supply the
|
|
* the compiler header file version, so that we know if the application
|
|
* was compiled with a compatible version of the library. REQUIRED
|
|
*/
|
|
png_ptr = png_create_read_struct_2(PNG_LIBPNG_VER_STRING, status, error, warning, status, png_malloc_fn, png_free_fn);
|
|
|
|
if (png_ptr == nullptr)
|
|
{
|
|
mprintf(("png_read_header: error creating read struct\n"));
|
|
onClose();
|
|
return PNG_ERROR_READING;
|
|
}
|
|
|
|
/* Allocate/initialize the memory for image information. REQUIRED. */
|
|
info_ptr = png_create_info_struct(png_ptr);
|
|
if (info_ptr == NULL)
|
|
{
|
|
mprintf(("png_read_header: error creating info struct\n"));
|
|
onClose();
|
|
png_destroy_read_struct(&png_ptr, nullptr, nullptr);
|
|
return PNG_ERROR_READING;
|
|
}
|
|
|
|
if (setjmp(png_jmpbuf(png_ptr)))
|
|
{
|
|
mprintf(("png_read_header: something went wrong\n"));
|
|
/* Free all of the memory associated with the png_ptr and info_ptr */
|
|
png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
|
|
onClose();
|
|
/* If we get here, we had a problem reading the file */
|
|
return PNG_ERROR_READING;
|
|
}
|
|
|
|
png_set_read_fn(png_ptr, status, readFunc);
|
|
|
|
png_read_info(png_ptr, info_ptr);
|
|
|
|
if (w) *w = png_get_image_width(png_ptr, info_ptr);
|
|
if (h) *h = png_get_image_height(png_ptr, info_ptr);
|
|
// this turns out to be near useless, but meh
|
|
if (bpp) {
|
|
// bit depth can also be 16 bit we tell libpng to reduce that to 8 bits so we also need to tell our caller about that
|
|
auto bits = std::min(8, (int)png_get_bit_depth(png_ptr, info_ptr));
|
|
*bpp = (png_get_channels(png_ptr, info_ptr) * bits);
|
|
}
|
|
|
|
png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
|
|
|
|
return PNG_ERROR_NONE;
|
|
}
|
|
|
|
/*
|
|
* @brief Reads header information from the PNG file into the bitmap pointer
|
|
*
|
|
* @param [in] filename name of the PNG bitmap file
|
|
* @param [out] w width of the bitmap
|
|
* @param [out] h height of the bitmap
|
|
* @param [out] bpp bits per pixel of the bitmap
|
|
* @param [out] palette
|
|
*
|
|
* @retval PNG_ERROR_NONE if successful, otherwise error code
|
|
*/
|
|
int png_read_header(const char *real_filename, CFILE *img_cfp, int *w, int *h, int *bpp, ubyte * /*palette*/)
|
|
{
|
|
char filename[MAX_FILENAME_LEN];
|
|
|
|
png_status status;
|
|
status.reading_header = true;
|
|
status.filename = real_filename;
|
|
|
|
if (img_cfp == nullptr) {
|
|
strcpy_s( filename, real_filename );
|
|
|
|
char *p = strchr( filename, '.' );
|
|
|
|
if ( p )
|
|
*p = 0;
|
|
strcat_s( filename, ".png" );
|
|
|
|
status.cfp = cfopen( filename , "rb" );
|
|
|
|
if ( !status.cfp ) {
|
|
return PNG_ERROR_READING;
|
|
}
|
|
} else {
|
|
status.cfp = img_cfp;
|
|
}
|
|
|
|
Assert( status.cfp != nullptr);
|
|
|
|
if (status.cfp == nullptr)
|
|
return PNG_ERROR_READING;
|
|
|
|
int result = png_read_header_data(w, h, bpp, &status, png_error_fn, png_warning_fn, png_scp_read_data, [&status]() {cfclose(status.cfp); });
|
|
|
|
if (img_cfp == nullptr) {
|
|
cfclose(status.cfp);
|
|
status.cfp = nullptr;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
int png_read_header(const SCP_string& b64, int* w, int* h, int* bpp, ubyte* /*palette*/)
|
|
{
|
|
struct b64_dec_buffer {
|
|
SCP_string decoded;
|
|
size_t pos;
|
|
} buffer{ base64_decode(b64), 0 };
|
|
|
|
return png_read_header_data(w, h, bpp, &buffer,
|
|
[](png_structp png_ptr, png_const_charp msg) {
|
|
mprintf(("PNG error while parsing base64 PNG: %s\n", msg));
|
|
longjmp(png_jmpbuf(png_ptr), 1);
|
|
},
|
|
[](png_structp, png_const_charp msg) {
|
|
mprintf(("PNG warning while parsing base64 PNG: %s\n", msg));
|
|
},
|
|
[](png_structp png_ptr, png_bytep datap, png_size_t length) {
|
|
b64_dec_buffer& buf = *static_cast<b64_dec_buffer*>(png_get_io_ptr(png_ptr));
|
|
|
|
for (size_t finalPos = buf.pos + length; buf.pos < finalPos; buf.pos++) {
|
|
*(datap++) = buf.decoded.at(buf.pos);
|
|
}
|
|
}, []() {});
|
|
}
|
|
|
|
/*
|
|
* Loads a PNG image
|
|
*
|
|
* @param [out] image_data allocated storage for the bitmap
|
|
* @param [in] bpp
|
|
* @param [in] dest_size
|
|
*
|
|
* @retval true if succesful, false otherwise
|
|
*/
|
|
static int png_read_bitmap_data(ubyte *image_data, int *bpp, png_voidp status, png_error_ptr error, png_error_ptr warning, png_rw_ptr readFunc, std::function<void()> onClose)
|
|
{
|
|
png_infop info_ptr;
|
|
png_structp png_ptr;
|
|
png_bytepp row_pointers;
|
|
unsigned int i;
|
|
|
|
/* Create and initialize the png_struct with the desired error handler
|
|
* functions. If you want to use the default stderr and longjump method,
|
|
* you can supply NULL for the last three parameters. We also supply the
|
|
* the compiler header file version, so that we know if the application
|
|
* was compiled with a compatible version of the library. REQUIRED
|
|
*/
|
|
png_ptr = png_create_read_struct_2(PNG_LIBPNG_VER_STRING, status, error, warning, nullptr, nullptr, nullptr);
|
|
|
|
if (png_ptr == nullptr)
|
|
{
|
|
mprintf(("png_read_bitmap: png_ptr went wrong\n"));
|
|
onClose();
|
|
return PNG_ERROR_READING;
|
|
}
|
|
|
|
/* Allocate/initialize the memory for image information. REQUIRED. */
|
|
info_ptr = png_create_info_struct(png_ptr);
|
|
if (info_ptr == nullptr)
|
|
{
|
|
mprintf(("png_read_bitmap: info_ptr went wrong\n"));
|
|
onClose();
|
|
png_destroy_read_struct(&png_ptr, nullptr, nullptr);
|
|
return PNG_ERROR_READING;
|
|
}
|
|
|
|
if (setjmp(png_jmpbuf(png_ptr)))
|
|
{
|
|
mprintf(("png_read_bitmap: something went wrong\n"));
|
|
/* Free all of the memory associated with the png_ptr and info_ptr */
|
|
png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
|
|
onClose();
|
|
/* If we get here, we had a problem reading the file */
|
|
return PNG_ERROR_READING;
|
|
}
|
|
|
|
png_set_read_fn(png_ptr, status, readFunc);
|
|
|
|
png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_BGR | PNG_TRANSFORM_EXPAND | PNG_TRANSFORM_STRIP_16, NULL);
|
|
auto len = png_get_rowbytes(png_ptr, info_ptr);
|
|
|
|
row_pointers = png_get_rows(png_ptr, info_ptr);
|
|
|
|
if(bpp)
|
|
*bpp = (ubyte)(len / png_get_image_width(png_ptr, info_ptr)) << 3;
|
|
|
|
//copy row data to image
|
|
unsigned int height = png_get_image_height(png_ptr, info_ptr);
|
|
for (i = 0; i < height; i++) {
|
|
memcpy(&image_data[i * len], row_pointers[i], len);
|
|
}
|
|
|
|
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
|
|
onClose();
|
|
|
|
return PNG_ERROR_NONE;
|
|
}
|
|
|
|
/*
|
|
* Loads a PNG image
|
|
*
|
|
* @param [in] real_filename name of the png file to load
|
|
* @param [out] image_data allocated storage for the bitmap
|
|
* @param [in] bpp
|
|
* @param [in] dest_size
|
|
* @param [in] cf_type
|
|
*
|
|
* @retval true if succesful, false otherwise
|
|
*/
|
|
int png_read_bitmap(const char* real_filename, ubyte* image_data, int* bpp, int /*dest_size*/, int cf_type)
|
|
{
|
|
char filename[MAX_FILENAME_LEN];
|
|
|
|
png_status status;
|
|
status.reading_header = false;
|
|
status.filename = real_filename;
|
|
|
|
strcpy_s(filename, real_filename);
|
|
char* p = strchr(filename, '.');
|
|
if (p) *p = 0;
|
|
strcat_s(filename, ".png");
|
|
|
|
status.cfp = cfopen(filename, "rb", cf_type);
|
|
|
|
if (status.cfp == NULL)
|
|
return PNG_ERROR_READING;
|
|
|
|
return png_read_bitmap_data(image_data, bpp, &status, png_error_fn, png_warning_fn, png_scp_read_data, [&status]() {cfclose(status.cfp); });
|
|
}
|
|
|
|
int png_read_bitmap(const SCP_string& b64, ubyte* image_data, int* bpp)
|
|
{
|
|
struct b64_dec_buffer {
|
|
SCP_string decoded;
|
|
size_t pos;
|
|
} buffer{ base64_decode(b64), 0 };
|
|
|
|
return png_read_bitmap_data(image_data, bpp, &buffer,
|
|
[](png_structp png_ptr, png_const_charp msg) {
|
|
mprintf(("PNG error while parsing base64 PNG: %s\n", msg));
|
|
longjmp(png_jmpbuf(png_ptr), 1);
|
|
},
|
|
[](png_structp, png_const_charp msg) {
|
|
mprintf(("PNG warning while parsing base64 PNG: %s\n", msg));
|
|
},
|
|
[](png_structp png_ptr, png_bytep datap, png_size_t length) {
|
|
b64_dec_buffer& buf = *static_cast<b64_dec_buffer*>(png_get_io_ptr(png_ptr));
|
|
|
|
for (size_t finalPos = std::min(buf.pos + length, buf.decoded.size()); buf.pos < finalPos; buf.pos++) {
|
|
*(datap++) = buf.decoded.at(buf.pos);
|
|
}
|
|
}, []() {});
|
|
}
|
|
|
|
static bool png_write_bitmap_data(size_t width, size_t height, bool y_flip, const uint8_t* data, png_voidp status, png_error_ptr error, png_error_ptr warning, png_rw_ptr writeFunc, png_flush_ptr flushFnc) {
|
|
auto png_ptr = png_create_write_struct_2(PNG_LIBPNG_VER_STRING, status, error, warning, status, png_malloc_fn, png_free_fn);
|
|
|
|
auto info_ptr = png_create_info_struct(png_ptr);
|
|
png_set_IHDR(png_ptr, info_ptr, (png_uint_32)width, (png_uint_32)height, 8, PNG_COLOR_TYPE_RGBA,
|
|
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
|
|
|
|
std::vector<uint8_t*> rows(height);
|
|
for (size_t y = 0; y < height; ++y) {
|
|
auto index = y_flip ? height - y - 1 : y;
|
|
|
|
rows[index] = (uint8_t*)data + y * width * 4;
|
|
}
|
|
|
|
png_set_rows(png_ptr, info_ptr, rows.data());
|
|
#ifdef PNG_WRITE_CUSTOMIZE_COMPRESSION_SUPPORTED
|
|
// According to the documentation level 6 should perform reasonably well for us
|
|
png_set_compression_level(png_ptr, 6);
|
|
#endif
|
|
png_set_write_fn(png_ptr, status, writeFunc, flushFnc);
|
|
png_write_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, nullptr);
|
|
|
|
png_destroy_write_struct(&png_ptr, &info_ptr);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool png_write_bitmap(const char* filename, size_t width, size_t height, bool y_flip, const uint8_t* data) {
|
|
png_status status;
|
|
|
|
status.writing = true;
|
|
status.filename = filename;
|
|
status.cfp = cfopen(filename, "wb");
|
|
|
|
if (!status.cfp) {
|
|
return false;
|
|
}
|
|
|
|
png_write_bitmap_data(width, height, y_flip, data, &status, png_error_fn, png_warning_fn, png_scp_write_data, png_scp_flush);
|
|
|
|
cfclose(status.cfp);
|
|
|
|
return true;
|
|
}
|
|
|
|
SCP_string png_b64_bitmap(size_t width, size_t height, bool y_flip, const uint8_t* data) {
|
|
struct b64_enc_buffer {
|
|
size_t i = 0;
|
|
std::array<png_byte, 3> buffer;
|
|
std::stringstream b64;
|
|
|
|
} buffer;
|
|
|
|
png_write_bitmap_data(width, height, y_flip, data, &buffer,
|
|
[](png_structp png_ptr, png_const_charp msg) {
|
|
mprintf(("PNG error while generating base64: %s\n", msg));
|
|
longjmp(png_jmpbuf(png_ptr), 1);
|
|
},
|
|
[](png_structp, png_const_charp msg) {
|
|
mprintf(("PNG warning while generating base64: %s\n", msg));
|
|
},
|
|
[](png_structp png_ptr, png_bytep datap, png_size_t length) {
|
|
auto& buf = *static_cast<b64_enc_buffer*>(png_get_io_ptr(png_ptr));
|
|
|
|
while (buf.i != 0 && length != 0) {
|
|
length--;
|
|
buf.buffer[buf.i] = *(datap++);
|
|
|
|
if (++buf.i >= 3) {
|
|
buf.i = 0;
|
|
base64_encode(buf.b64, buf.buffer.data(), 3);
|
|
}
|
|
}
|
|
|
|
if (length == 0)
|
|
return;
|
|
|
|
size_t rem = length % 3;
|
|
|
|
base64_encode(buf.b64, datap, (unsigned int) length - (unsigned int) rem);
|
|
datap = &datap[length - rem];
|
|
|
|
for (; buf.i < rem; buf.i++) {
|
|
buf.buffer[buf.i] = *(datap++);
|
|
}
|
|
},
|
|
[](png_structp png_ptr) {
|
|
auto& buf = *static_cast<b64_enc_buffer*>(png_get_io_ptr(png_ptr));
|
|
|
|
if (buf.i != 0) {
|
|
base64_encode(buf.b64, buf.buffer.data(), (unsigned int) buf.i);
|
|
}
|
|
});
|
|
|
|
if (buffer.i != 0)
|
|
base64_encode(buffer.b64, buffer.buffer.data(), (unsigned int)buffer.i);
|
|
|
|
return buffer.b64.str();
|
|
}
|
|
|
|
/*
|
|
* APNG related code
|
|
* Refer https://wiki.mozilla.org/APNG_Specification
|
|
*
|
|
* Original Copyright (c) 2014 Max Stepin
|
|
* maxst at users.sourceforge.net
|
|
* http://sourceforge.net/projects/apng/files/libpng/examples/
|
|
* and
|
|
* https://github.com/apngasm/apngasm/commit/92e409b8b9182b4442b0d9ab6dac0ed90c4e3ffd
|
|
* with hints gleaned from
|
|
* https://bugs.webkit.org/attachment.cgi?id=248547&action=diff
|
|
*
|
|
* zlib license
|
|
* ------------
|
|
*
|
|
* This software is provided 'as-is', without any express or implied
|
|
* warranty. In no event will the authors be held liable for any damages
|
|
* arising from the use of this software.
|
|
*
|
|
* Permission is granted to anyone to use this software for any purpose,
|
|
* including commercial applications, and to alter it and redistribute it
|
|
* freely, subject to the following restrictions:
|
|
*
|
|
* 1. The origin of this software must not be misrepresented; you must not
|
|
* claim that you wrote the original software. If you use this software
|
|
* in a product, an acknowledgment in the product documentation would be
|
|
* appreciated but is not required.
|
|
* 2. Altered source versions must be plainly marked as such, and must not be
|
|
* misrepresented as being the original software.
|
|
* 3. This notice may not be removed or altered from any source distribution.
|
|
*
|
|
*/
|
|
|
|
namespace apng {
|
|
|
|
const uint id_IHDR = 0x52444849; // PNG header
|
|
const uint id_acTL = 0x4C546361; // Animation control chunk
|
|
const uint id_fcTL = 0x4C546366; // Frame control chunk
|
|
const uint id_IDAT = 0x54414449; // first frame and/or default image
|
|
const uint id_fdAT = 0x54416466; // Frame data chunk
|
|
const uint id_IEND = 0x444E4549; // end/footer chunk
|
|
|
|
// Protect against large PNGs. See Mozilla's bug #251381 for more info.
|
|
const unsigned long cMaxPNGSize = 1000000UL;
|
|
|
|
/*
|
|
* @brief test if character is a valid png chunk type character
|
|
* @note refer to http://www.w3.org/TR/PNG/#5Chunk-layout
|
|
*/
|
|
static inline bool not_chunk(ubyte c)
|
|
{
|
|
return c < 65 || c > 122 || (c > 90 && c < 97);
|
|
}
|
|
|
|
/*
|
|
* @brief shim for libpng info/IHDR chunk callback
|
|
*/
|
|
static inline void info_callback(png_structp png_ptr, png_infop /*info_ptr*/)
|
|
{
|
|
static_cast<apng_ani*>(png_get_progressive_ptr(png_ptr))->info_callback();
|
|
}
|
|
|
|
/*
|
|
* @brief shim for libpng row callback
|
|
*/
|
|
static inline void row_callback(png_structp png_ptr, png_bytep new_row, png_uint_32 row_num, int /*pass*/)
|
|
{
|
|
static_cast<apng_ani*>(png_get_progressive_ptr(png_ptr))->row_callback(new_row, row_num);
|
|
}
|
|
|
|
/*
|
|
* @brief compose new frame from previous frame & currently-processing frames raw data
|
|
* @note assumes png data has 4 bytes
|
|
*/
|
|
void apng_ani::_compose_frame()
|
|
{
|
|
int u, v, al;
|
|
|
|
// our libpng transformations should ensure 4 bytes per pixel by this point
|
|
Assertion(bpp == 32, "apng frame composition assumes 4 bytes of data per pixel, get a coder!");
|
|
for (uint j = 0; j < _frameh; j++) {
|
|
ubyte* sp = _frame_raw.rows[j];
|
|
ubyte* dp = frame.rows[j + _y_offset] + _x_offset * 4;
|
|
|
|
if (_blend_op == 0) {
|
|
// blend operation 0 (APNG_BLEND_OP_SOURCE) means ignore destination
|
|
memcpy(dp, sp, _framew * 4);
|
|
}
|
|
else {
|
|
for (uint i = 0; i < _framew; i++, sp += 4, dp += 4) {
|
|
if (sp[3] == 255) {
|
|
// if source is not transparent at all, just copy it
|
|
memcpy(dp, sp, 4);
|
|
}
|
|
else {
|
|
if (sp[3] != 0) {
|
|
if (dp[3] != 0) {
|
|
u = sp[3] * 255;
|
|
v = (255 - sp[3]) * dp[3];
|
|
al = u + v;
|
|
dp[0] = static_cast<ubyte>((sp[0] * u + dp[0] * v) / al);
|
|
dp[1] = static_cast<ubyte>((sp[1] * u + dp[1] * v) / al);
|
|
dp[2] = static_cast<ubyte>((sp[2] * u + dp[2] * v) / al);
|
|
dp[3] = static_cast<ubyte>(al / 255);
|
|
}
|
|
else {
|
|
// destination is transparent, overwrite it
|
|
memcpy(dp, sp, 4);
|
|
}
|
|
}
|
|
// if source is transparent, ignore it
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* @brief start processing apng frame
|
|
* @note uses libpng calls, treats apng data as discrete png images
|
|
*/
|
|
int apng_ani::_processing_start()
|
|
{
|
|
static ubyte png_sig[8] = {137, 80, 78, 71, 13, 10, 26, 10};
|
|
|
|
_pngp = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
|
_infop = png_create_info_struct(_pngp);
|
|
if (_pngp == nullptr || _infop == nullptr) {
|
|
return 1;
|
|
}
|
|
|
|
if (setjmp(png_jmpbuf(_pngp))) {
|
|
png_destroy_read_struct(&_pngp, &_infop, 0);
|
|
return 1;
|
|
}
|
|
|
|
png_set_crc_action(_pngp, PNG_CRC_QUIET_USE, PNG_CRC_QUIET_USE);
|
|
png_set_progressive_read_fn(_pngp, this, apng::info_callback, apng::row_callback, nullptr);
|
|
|
|
png_process_data(_pngp, _infop, png_sig, 8);
|
|
memcpy(&_chunk_IHDR.data[8], &_chunk.data[12], 8); // use frame width & height from fcTL
|
|
png_process_data(_pngp, _infop, &_chunk_IHDR.data[0], _chunk_IHDR.size);
|
|
|
|
for (size_t i = 0; i < _info_chunks.size(); ++i) {
|
|
// this processes chunks like tRNS / PLTE / etc
|
|
png_process_data(_pngp, _infop, &_info_chunks.at(i).data[0], _info_chunks.at(i).size);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* @brief process apng data chunks
|
|
* @note uses libpng calls, treats apng data as discrete png images
|
|
*/
|
|
void apng_ani::_processing_data(ubyte* data, uint size)
|
|
{
|
|
png_process_data(_pngp, _infop, data, size);
|
|
}
|
|
|
|
/*
|
|
* @brief finish processing apng frame
|
|
* @note uses libpng calls, treats apng data as discrete png images
|
|
*/
|
|
int apng_ani::_processing_finish()
|
|
{
|
|
static ubyte end_sig[12] = {0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130};
|
|
|
|
if (_pngp == nullptr || _infop == nullptr) {
|
|
return 1;
|
|
}
|
|
|
|
if (setjmp(png_jmpbuf(_pngp))) {
|
|
png_destroy_read_struct(&_pngp, &_infop, 0);
|
|
return 1;
|
|
}
|
|
|
|
png_process_data(_pngp, _infop, end_sig, 12);
|
|
png_destroy_read_struct(&_pngp, &_infop, 0);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* @brief process IHDR data
|
|
* @note not called until the IDAT chunk starts processing
|
|
*/
|
|
void apng_ani::info_callback()
|
|
{
|
|
png_set_expand(_pngp);
|
|
png_set_strip_16(_pngp);
|
|
png_set_gray_to_rgb(_pngp); // what about 'non-colour' headanis?
|
|
png_set_add_alpha(_pngp, 0xff, PNG_FILLER_AFTER); // doesn't affect images that already have alpha channel
|
|
png_set_interlace_handling(_pngp);
|
|
png_set_bgr(_pngp);
|
|
|
|
png_read_update_info(_pngp, _infop);
|
|
}
|
|
|
|
/*
|
|
* @brief process each row of data provided by libpng
|
|
* @note callback from libpng
|
|
*
|
|
* @param [in] new_row pointer to start of next row of data
|
|
* @param [in] row_num number of row in image
|
|
*/
|
|
void apng_ani::row_callback(png_bytep new_row, png_uint_32 row_num)
|
|
{
|
|
png_progressive_combine_row(_pngp, _frame_raw.rows.at(row_num), new_row);
|
|
}
|
|
|
|
|
|
/*
|
|
* @brief manually process a single chunk of apng data
|
|
*/
|
|
void apng_ani::_process_chunk()
|
|
{
|
|
_id = _read_chunk(_chunk);
|
|
|
|
if (_id == id_acTL && !_got_IDAT && !_got_acTL) {
|
|
// animation control chunk
|
|
if (!_reading) {
|
|
return;
|
|
}
|
|
nframes = png_get_uint_32(&_chunk.data[8]);
|
|
plays = png_get_uint_32(&_chunk.data[12]);
|
|
|
|
if (!nframes || nframes > PNG_UINT_31_MAX || plays > PNG_UINT_31_MAX) {
|
|
_apng_failed("invalid apng acTL data");
|
|
}
|
|
|
|
_got_acTL = true;
|
|
_frames.reserve(nframes);
|
|
_frame_offsets.reserve(nframes+1); // extra 1 is for EOF offset
|
|
}
|
|
else if (_id == id_fcTL && (!_got_IDAT || _got_acTL)) {
|
|
// frame control chunk
|
|
if (_reading) {
|
|
uint sequence_num = png_get_uint_32(&_chunk.data[8]);
|
|
if (sequence_num != _sequence_num++) {
|
|
_apng_failed("invalid apng fcTL sequence number");
|
|
}
|
|
}
|
|
|
|
// handle frame finish in next_frame
|
|
_framew = png_get_uint_32(&_chunk.data[12]);
|
|
_frameh = png_get_uint_32(&_chunk.data[16]);
|
|
_x_offset = png_get_uint_32(&_chunk.data[20]);
|
|
_y_offset = png_get_uint_32(&_chunk.data[24]);
|
|
_delay_num = png_get_uint_16(&_chunk.data[28]);
|
|
_delay_den = png_get_uint_16(&_chunk.data[30]);
|
|
_dispose_op = _chunk.data[32];
|
|
_blend_op = _chunk.data[33];
|
|
|
|
if (_reading &&
|
|
(_framew > cMaxPNGSize || _frameh > cMaxPNGSize
|
|
|| _x_offset > cMaxPNGSize || _y_offset > cMaxPNGSize
|
|
|| _x_offset + _framew > w || _y_offset + _frameh > h
|
|
|| _dispose_op > 2 || _blend_op > 1)) {
|
|
_apng_failed("invalid apng fcTL data");
|
|
}
|
|
|
|
// according to spec...
|
|
if (current_frame == 0 && _dispose_op == 2) {
|
|
_dispose_op = 1;
|
|
}
|
|
|
|
if (_delay_den == 0) _delay_den = 100; // APNG spec
|
|
if (_delay_num == 0) _delay_num = 1; // arbitrary lower bound
|
|
float frame_delay = static_cast<float>(_delay_num)/static_cast<float>(_delay_den);
|
|
frame.delay = frame_delay;
|
|
|
|
if (_reading) {
|
|
anim_time+= frame_delay;
|
|
_frame_offsets.push_back((int)_offset);
|
|
}
|
|
else {
|
|
if (_got_IDAT && _processing_start()) {
|
|
_apng_failed("couldn't start fdat apng frame");
|
|
}
|
|
}
|
|
}
|
|
else if (_id == id_IDAT) {
|
|
_got_IDAT = true;
|
|
_processing_data(&_chunk.data[0], _chunk.size);
|
|
}
|
|
else if (_id == id_fdAT && _got_acTL) {
|
|
if (_reading) {
|
|
uint sequence_num = png_get_uint_32(&_chunk.data[8]);
|
|
if (sequence_num != _sequence_num++) {
|
|
_apng_failed("invalid apng fdAT sequence number");
|
|
}
|
|
}
|
|
|
|
if (_reading) {
|
|
return;
|
|
}
|
|
|
|
png_save_uint_32(&_chunk.data[4], _chunk.size - 16);
|
|
memcpy(&_chunk.data[8], "IDAT", 4);
|
|
_processing_data(&_chunk.data[4], _chunk.size - 4);
|
|
}
|
|
else if (_id == id_IEND) {
|
|
return;
|
|
}
|
|
else if (not_chunk(_chunk.data[4]) || not_chunk(_chunk.data[5]) ||
|
|
not_chunk(_chunk.data[6]) || not_chunk(_chunk.data[7])) {
|
|
_apng_failed("unknown chunk ID found");
|
|
}
|
|
else if (!_got_IDAT) {
|
|
_processing_data(&_chunk.data[0], _chunk.size);
|
|
_info_chunks.push_back(_chunk);
|
|
}
|
|
}
|
|
|
|
unsigned int apng_ani::_read_chunk(_chunk_s& chunk)
|
|
{
|
|
_offset = cftell(_cfp);
|
|
if (cfread(&chunk.data[0], 4, 1, _cfp) == 1) {
|
|
chunk.size = png_get_uint_32(&chunk.data[0]) + 12;
|
|
// reduce the amount of vector resizing
|
|
if (chunk.size > _max_chunk_size) {
|
|
_max_chunk_size = chunk.size;
|
|
chunk.data.resize(chunk.size);
|
|
}
|
|
if (cfread(&chunk.data[4], chunk.size-4, 1, _cfp) == 1) {
|
|
return *(uint*)(&chunk.data[4]);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* @brief preload/cache the anim frames
|
|
* @note useful for preloading without having to load the apng into bmpman slots
|
|
*/
|
|
void apng_ani::preload()
|
|
{
|
|
_reading = false;
|
|
_cache = true; // implied, otherwise preload is almost pointless
|
|
while (current_frame < nframes) {
|
|
next_frame();
|
|
}
|
|
goto_start();
|
|
}
|
|
|
|
/*
|
|
* @brief get previous apng frame
|
|
* @note due to apng format, this can only play backwards from the furthest forward frame reached
|
|
*/
|
|
void apng_ani::prev_frame()
|
|
{
|
|
if (_cache != true) {
|
|
_apng_failed("no caching, therefore can't retrieve previous frame");
|
|
}
|
|
_reading = false;
|
|
if (current_frame > 0) {
|
|
frame = _frames.at(--current_frame);
|
|
nprintf(("apng", "apng prev_frame; (%03i/%03u)\n", current_frame, static_cast<uint>(_frames.size())));
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* @brief get next apng frame
|
|
*/
|
|
void apng_ani::next_frame()
|
|
{
|
|
_reading = false;
|
|
// setup new frame (if frame doesn't already exist)
|
|
// always do if not caching
|
|
// don't do if caching & already have frame
|
|
if (_cache == false || (_frames.size() <= current_frame)) {
|
|
if (current_frame > 0) {
|
|
if (_dispose_op == 1) {
|
|
// clear previous frame region of output buffer to fully transparent black
|
|
for (uint row = 0; row < _frameh; ++row) {
|
|
memset(&frame.data.at((_y_offset+row) * _row_len + _x_offset * 4), 0, _framew * 4);
|
|
}
|
|
}
|
|
else if (_dispose_op == 2) {
|
|
// revert previous frames region of output buffer to previous contents
|
|
for (uint row = 0; row < _frameh; ++row) {
|
|
uint pos = (_y_offset+row) * _row_len + _x_offset * 4;
|
|
memcpy(&frame.data.at(pos), &_frame_next.data.at(pos), _framew * 4);
|
|
}
|
|
}
|
|
// default is to do nothing with output buffer
|
|
}
|
|
|
|
while (cftell(_cfp) < _frame_offsets.at(current_frame+1)) {
|
|
_process_chunk();
|
|
}
|
|
|
|
nprintf(("apng", "apng next_frame; new (%03i/%03u/%03i) (%u) (%u) %03u|%03u %03u|%03u (%02u) (%04f)\n",
|
|
current_frame, static_cast<uint>(_frames.size()), nframes, _dispose_op, _blend_op,
|
|
_framew, _x_offset, _frameh, _y_offset,
|
|
static_cast<uint>(_frame_offsets.size()), frame.delay));
|
|
|
|
if (_got_IDAT && _processing_finish()) {
|
|
_apng_failed("couldn't finish fdat apng frame");
|
|
}
|
|
|
|
if (_dispose_op == 2) {
|
|
// revert to previous; so save the current frame region for later
|
|
for (uint row = 0; row < _frameh; ++row) {
|
|
uint pos = (_y_offset+row) * _row_len + _x_offset * 4;
|
|
memcpy(&_frame_next.data.at(pos), &frame.data.at(pos), _framew * 4);
|
|
}
|
|
}
|
|
|
|
_compose_frame();
|
|
if (_cache == true) _frames.push_back(frame);
|
|
}
|
|
else {
|
|
if (current_frame < nframes) {
|
|
nprintf(("apng", "apng next_frame; used old (%03i/%03u)\n", current_frame, static_cast<uint>(_frames.size())));
|
|
frame = _frames.at(current_frame);
|
|
}
|
|
}
|
|
++current_frame;
|
|
}
|
|
|
|
/*
|
|
* @brief return image size in bytes
|
|
*/
|
|
size_t apng_ani::imgsize()
|
|
{
|
|
return _image_size;
|
|
}
|
|
|
|
|
|
/*
|
|
* @brief send animation to its 1st frame
|
|
*
|
|
* @note headers must be read before this function can be called
|
|
*/
|
|
void apng_ani::goto_start()
|
|
{
|
|
current_frame = 0;
|
|
if (_cache == false ) { // only required if not caching frames
|
|
if (cfseek(_cfp, _frame_offsets.at(0), CF_SEEK_SET) != 0) {
|
|
_apng_failed("couldn't seek to 1st fcTL offset");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* @brief Get info about the apng
|
|
* @note Also validates the apng & sets it up to have frames read
|
|
*
|
|
* @retval PNG_ERROR_NONE (0), otherwise will raise exception
|
|
*/
|
|
int apng_ani::load_header()
|
|
{
|
|
char filename[MAX_FILENAME_LEN];
|
|
|
|
strcpy_s(filename, _filename.c_str());
|
|
char *p = strchr( filename, '.' );
|
|
if ( p != nullptr ) *p = 0;
|
|
strcat_s( filename, ".png" );
|
|
|
|
_cfp = cfopen( filename , "rb" );
|
|
|
|
if ( _cfp == nullptr) {
|
|
_apng_failed("couldn't open filename");
|
|
}
|
|
|
|
_reading = true;
|
|
|
|
ubyte sig[8];
|
|
if (cfread(sig, 8, 1, _cfp) != 1) {
|
|
_apng_failed("cfread of png signature failed");
|
|
}
|
|
if (png_sig_cmp(sig, 0, 8) != 0) {
|
|
_apng_failed("file has invalid png signature");
|
|
}
|
|
|
|
// setup chunk sizes before use
|
|
_chunk_IHDR.data.resize(25); // fixed IHDR chunk size
|
|
_chunk.data.resize(25); // match the other sizes, maybe waste up to 13 bytes (ooooh)
|
|
|
|
_id = _read_chunk(_chunk_IHDR);
|
|
|
|
if (_id != id_IHDR || _chunk_IHDR.size != 25) {
|
|
_apng_failed("failed to read IHDR chunk");
|
|
}
|
|
|
|
w = png_get_uint_32(&_chunk_IHDR.data[8]);
|
|
h = png_get_uint_32(&_chunk_IHDR.data[12]);
|
|
_row_len = w * 4;
|
|
|
|
// setup frames & keep bm_create happy
|
|
_image_size = _row_len * h;
|
|
frame.data.reserve(_image_size); // alloc only once
|
|
frame.data.assign(_image_size, 0); // all transparent black per spec
|
|
frame.rows.resize(h);
|
|
_frame_raw.data.resize(_image_size);
|
|
_frame_raw.rows.resize(h);
|
|
_frame_next.data.resize(_image_size);
|
|
_frame_next.rows.resize(h);
|
|
for (uint i = 0; i < h; ++i) {
|
|
// everything is correctly sized above; avoid .at() error checks
|
|
frame.rows[i] = &frame.data[i * _row_len];
|
|
_frame_raw.rows[i] = &_frame_raw.data[i * _row_len];
|
|
_frame_next.rows[i] = &_frame_next.data[i * _row_len];
|
|
}
|
|
|
|
// read all data
|
|
while (!cfeof(_cfp)) {
|
|
_process_chunk();
|
|
}
|
|
|
|
// should be at EOF; attach to _frame_offsets to make next_frame code simpler
|
|
Assertion(cfeof(_cfp) != 0, "apng not at EOF, get a coder!");
|
|
_frame_offsets.push_back(cftell(_cfp));
|
|
|
|
// sanity checks
|
|
if (anim_time <= 0.0f) {
|
|
_apng_failed("animation duration <= 0.0f, bad data?");
|
|
}
|
|
|
|
if (nframes < 1) {
|
|
_apng_failed("animation didn't have any frames, is this a static png?");
|
|
}
|
|
|
|
// back to start, including reset of _cfp so it can be used for the 1st frame
|
|
_reading = false;
|
|
if (cfseek(_cfp, _frame_offsets.at(0), CF_SEEK_SET) != 0) {
|
|
_apng_failed("couldn't seek to 1st fcTL offset");
|
|
}
|
|
|
|
return PNG_ERROR_NONE;
|
|
}
|
|
|
|
/*
|
|
* @brief cleanup resources
|
|
*
|
|
*/
|
|
void apng_ani::_cleanup_resources()
|
|
{
|
|
png_destroy_read_struct(&_pngp, &_infop, nullptr);
|
|
if (_cfp != nullptr) cfclose(_cfp);
|
|
}
|
|
|
|
/*
|
|
* @brief something went badly wrong, throw an exception
|
|
*
|
|
* @param [in] msg text to display about the error
|
|
*/
|
|
void apng_ani::_apng_failed(const char* msg)
|
|
{
|
|
nframes = 0;
|
|
current_frame = 0;
|
|
plays = 0;
|
|
anim_time = 0.0f;
|
|
_cleanup_resources();
|
|
|
|
SCP_string error_msg = "(file ";
|
|
error_msg += _filename;
|
|
error_msg += ") ";
|
|
error_msg += msg;
|
|
throw ApngException(error_msg.c_str());
|
|
}
|
|
|
|
/*
|
|
* @brief ctor
|
|
*
|
|
* @note loads apng header data
|
|
*/
|
|
apng_ani::apng_ani(const char* filename, bool cache)
|
|
: w(0)
|
|
, h(0)
|
|
, bpp(32) // force all apngs to use this
|
|
, nframes(0)
|
|
, current_frame(0)
|
|
, plays(0)
|
|
, anim_time(0.0f)
|
|
, _filename(filename)
|
|
, _pngp(nullptr)
|
|
, _infop(nullptr)
|
|
, _cfp(nullptr)
|
|
, _offset(0)
|
|
, _sequence_num(0)
|
|
, _id(0)
|
|
, _row_len(0)
|
|
, _image_size(0)
|
|
, _framew(0)
|
|
, _frameh(0)
|
|
, _x_offset(0)
|
|
, _y_offset(0)
|
|
, _max_chunk_size(25) // IHDR must be 1st and it must be this big
|
|
, _delay_num(1)
|
|
, _delay_den(100)
|
|
, _dispose_op(0)
|
|
, _blend_op(0)
|
|
, _reading(true)
|
|
, _got_acTL(false)
|
|
, _got_IDAT(false)
|
|
, _cache(cache)
|
|
{
|
|
load_header();
|
|
}
|
|
|
|
apng_ani::~apng_ani()
|
|
{
|
|
_cleanup_resources();
|
|
}
|
|
|
|
} // end namespace apng
|