From 88912466105d731ff7b73af76cb4cd707876e518 Mon Sep 17 00:00:00 2001 From: Kim Woelders Date: Sat, 15 Jan 2022 19:25:19 +0100 Subject: [PATCH] PNG loader: Add multiframe support --- src/modules/loaders/loader_png.c | 286 ++++++++++++++++++++++++++++++- 1 file changed, 285 insertions(+), 1 deletion(-) diff --git a/src/modules/loaders/loader_png.c b/src/modules/loaders/loader_png.c index bd6777b..a395972 100644 --- a/src/modules/loaders/loader_png.c +++ b/src/modules/loaders/loader_png.c @@ -3,6 +3,7 @@ #include #include #include +#include #define DBG_PFX "LDR-png" @@ -11,11 +12,85 @@ #define _PNG_MIN_SIZE 60 /* Min. PNG file size (8 + 3*12 + 13 (+3) */ #define _PNG_SIG_SIZE 8 /* Signature size */ +#define T(a,b,c,d) ((a << 0) | (b << 8) | (c << 16) | (d << 24)) + +#define PNG_TYPE_IHDR T('I', 'H', 'D', 'R') +#define PNG_TYPE_acTL T('a', 'c', 'T', 'L') +#define PNG_TYPE_fcTL T('f', 'c', 'T', 'L') +#define PNG_TYPE_fdAT T('f', 'd', 'A', 'T') +#define PNG_TYPE_IDAT T('I', 'D', 'A', 'T') +#define PNG_TYPE_IEND T('I', 'E', 'N', 'D') + +#define APNG_DISPOSE_OP_NONE 0 +#define APNG_DISPOSE_OP_BACKGROUND 1 +#define APNG_DISPOSE_OP_PREVIOUS 2 + +#define APNG_BLEND_OP_SOURCE 0 +#define APNG_BLEND_OP_OVER 1 + +typedef struct { + uint32_t len; + union { + uint32_t type; + char name[4]; + }; +} png_chunk_hdr_t; + +/* IHDR */ +typedef struct { + uint32_t w; // Width + uint32_t h; // Height + uint8_t depth; // Bit depth (1, 2, 4, 8, or 16) + uint8_t color; // Color type (0, 2, 3, 4, or 6) + uint8_t comp; // Compression method (0) + uint8_t filt; // filter method (0) + uint8_t interl; // interlace method (0 "no interlace" or 1 "Adam7 interlace") +} png_ihdr_t; + +#define _PNG_IHDR_SIZE (4 + 4 + 13 + 4) + +/* acTL */ +typedef struct { + uint32_t num_frames; // Number of frames + uint32_t num_plays; // Number of times to loop this APNG. 0 indicates infinite looping. +} png_actl_t; + +/* fcTL */ +typedef struct { + uint32_t frame; // -- // Sequence number of the animation chunk, starting from 0 + uint32_t w; // -- // Width of the following frame + uint32_t h; // -- // Height of the following frame + uint32_t x; // -- // X position at which to render the following frame + uint32_t y; // -- // Y position at which to render the following frame + uint16_t delay_num; // Frame delay fraction numerator + uint16_t delay_den; // Frame delay fraction denominator + uint8_t dispose_op; // Type of frame area disposal to be done after rendering this frame + uint8_t blend_op; // Type of frame area rendering for this frame +} png_fctl_t; + +/* fdAT */ +typedef struct { + uint32_t frame; // -- // Sequence number of the animation chunk, starting from 0 + uint8_t data[]; +} png_fdat_t; + +typedef struct { + png_chunk_hdr_t hdr; + union { + png_ihdr_t ihdr; + png_actl_t actl; + png_fctl_t fctl; + png_fdat_t fdat; + }; + uint32_t crc; // Misplaced - just indication +} png_chunk_t; + typedef struct { ImlibImage *im; char load_data; char rc; + const png_chunk_t *pch_fctl; // Placed here to avoid clobber warning char interlace; } ctx_t; @@ -59,6 +134,7 @@ info_callback(png_struct * png_ptr, png_info * info_ptr) rc = LOAD_BADIMAGE; /* Format accepted */ + /* Semi-redundant fetch/check */ png_get_IHDR(png_ptr, info_ptr, &w32, &h32, &bit_depth, &color_type, &interlace_type, NULL, NULL); @@ -84,6 +160,7 @@ info_callback(png_struct * png_ptr, png_info * info_ptr) QUIT_WITH_RC(LOAD_SUCCESS); /* Load data */ + ctx->interlace = interlace_type; /* Prep for transformations... ultimately we want ARGB */ /* expand palette -> RGB if necessary */ @@ -216,6 +293,13 @@ load2(ImlibImage * im, int load_data) png_structp png_ptr = NULL; png_infop info_ptr = NULL; ctx_t ctx = { 0 }; + int ic; + unsigned char *fptr; + const png_chunk_t *chunk; + const png_fctl_t *pfctl; + unsigned int len, val; + int w, h, frame, save_fdat; + png_chunk_t cbuf; /* read header */ rc = LOAD_FAIL; @@ -253,7 +337,207 @@ load2(ImlibImage * im, int load_data) png_set_progressive_read_fn(png_ptr, &ctx, info_callback, row_callback, NULL); - png_process_data(png_ptr, info_ptr, fdata, im->fsize); + if (im->frame_num <= 0) + goto scan_done; + + /* Animation info requested. Look it up to find the frame's + * w,h which we need for making a "fake" IHDR in next pass. */ + + frame = 0; /* Frame number */ + ctx.pch_fctl = NULL; /* Ponter to requested frame fcTL chunk */ + fptr = (unsigned char *)fdata + _PNG_SIG_SIZE; + + for (ic = 0;; ic++, fptr += 8 + len + 4) + { + chunk = (const png_chunk_t *)fptr; + len = htonl(chunk->hdr.len); + D("Scan %3d: %06lx: %6d: %.4s: ", ic, + fptr - (unsigned char *)fdata, len, chunk->hdr.name); + if (fptr + len - (unsigned char *)fdata > im->fsize) + break; + + switch (chunk->hdr.type) + { + case PNG_TYPE_IDAT: + D("\n"); + if (im->frame_count == 0) + goto scan_done; /* No acTL before IDAT - Regular PNG */ + break; + + case PNG_TYPE_acTL: +#define P (&chunk->actl) + im->frame_count = htonl(P->num_frames); + D("num_frames=%d num_plays=%d\n", im->frame_count, + htonl(P->num_plays)); + if (im->frame_num > im->frame_count) + QUIT_WITH_RC(LOAD_BADFRAME); + break; +#undef P + + case PNG_TYPE_fcTL: +#define P (&chunk->fctl) + frame++; + D("frame=%d(%d) x,y=%d,%d wxh=%dx%d delay=%d/%d disp=%d blend=%d\n", // + frame, htonl(P->frame), + htonl(P->x), htonl(P->y), htonl(P->w), htonl(P->h), + htons(P->delay_num), htons(P->delay_den), + P->dispose_op, P->blend_op); + if (im->frame_num != frame) + break; + ctx.pch_fctl = chunk; /* Remember fcTL location */ +#if IMLIB2_DEBUG + break; /* Show all frames */ +#else + goto scan_done; +#endif +#undef P + + case PNG_TYPE_IEND: + D("\n"); + goto scan_check; + + default: + D("\n"); + break; + } + } + + scan_check: + if (!ctx.pch_fctl) + goto quit; /* Requested frame not found */ + + scan_done: + /* Now feed data into libpng to extract requested frame */ + + save_fdat = 0; + + /* At this point we start "progressive" PNG data processing */ + fptr = fdata; + png_process_data(png_ptr, info_ptr, fptr, _PNG_SIG_SIZE); + + fptr = (unsigned char *)fdata + _PNG_SIG_SIZE; + + for (ic = 0;; ic++, fptr += 8 + len + 4) + { + chunk = (const png_chunk_t *)fptr; + len = htonl(chunk->hdr.len); + D("Chunk %3d: %06lx: %6d: %.4s: ", ic, + fptr - (unsigned char *)fdata, len, chunk->hdr.name); + if (fptr + len - (unsigned char *)fdata > im->fsize) + break; + + switch (chunk->hdr.type) + { + case PNG_TYPE_IHDR: +#define P (&chunk->ihdr) + w = htonl(P->w); + h = htonl(P->h); + if (!ctx.pch_fctl) + { + D("WxH=%dx%d depth=%d color=%d comp=%d filt=%d interlace=%d\n", // + w, h, P->depth, P->color, P->comp, P->filt, P->interl); + break; /* Process actual IHDR chunk */ + } + + /* Deal with frame animation info */ + pfctl = &ctx.pch_fctl->fctl; +#if 0 + im->w = htonl(pfctl->w); + im->h = htonl(pfctl->h); +#endif + im->canvas_w = w; + im->canvas_h = h; + im->frame_x = htonl(pfctl->x); + im->frame_y = htonl(pfctl->y); + if (pfctl->dispose_op == APNG_DISPOSE_OP_BACKGROUND) + im->frame_flags |= FF_FRAME_DISPOSE_CLEAR; + else if (pfctl->dispose_op == APNG_DISPOSE_OP_PREVIOUS) + im->frame_flags |= FF_FRAME_DISPOSE_PREV; + if (pfctl->blend_op != APNG_BLEND_OP_SOURCE) + im->frame_flags |= FF_FRAME_BLEND; + val = htons(pfctl->delay_den); + im->frame_delay = + val > 0 ? 1000 * htons(pfctl->delay_num) / val : 100; + + D("WxH=%dx%d(%dx%d) X,Y=%d,%d depth=%d color=%d comp=%d filt=%d interlace=%d disp=%d blend=%d delay=%d/%d\n", // + htonl(pfctl->w), htonl(pfctl->h), + im->canvas_w, im->canvas_h, im->frame_x, im->frame_y, + P->depth, P->color, P->comp, P->filt, P->interl, + pfctl->dispose_op, pfctl->blend_op, + pfctl->delay_num, pfctl->delay_den); + + if (im->frame_num <= 1) + break; /* Process actual IHDR chunk */ + + /* Process fake IHDR for frame */ + memcpy(&cbuf, fptr, len + 12); + cbuf.ihdr.w = pfctl->w; + cbuf.ihdr.h = pfctl->h; + png_set_crc_action(png_ptr, PNG_CRC_QUIET_USE, PNG_CRC_QUIET_USE); + png_process_data(png_ptr, info_ptr, (void *)&cbuf, len + 12); + png_set_crc_action(png_ptr, PNG_CRC_DEFAULT, PNG_CRC_DEFAULT); + continue; +#undef P + + case PNG_TYPE_IDAT: + D("\n"); + /* Needed chunks should now be read */ + /* Note - Just before starting to process data chunks libpng will + * call info_callback() */ + if (im->frame_num <= 1) + break; /* Process actual IDAT chunk */ + /* Jump to the record after the frame's fcTL, will typically be + * the frame's first fdAT chunk */ + fptr = (unsigned char *)ctx.pch_fctl; + len = htonl(ctx.pch_fctl->hdr.len); + save_fdat = 1; /* Save fdAT's as of now (until next fcTL) */ + continue; + + case PNG_TYPE_acTL: +#define P (&chunk->actl) + if (im->frame_count > 1) + im->frame_flags |= FF_IMAGE_ANIMATED; + D("num_frames=%d num_plays=%d\n", + im->frame_count, htonl(P->num_plays)); + continue; +#undef P + + case PNG_TYPE_fcTL: + D("\n"); + if (save_fdat) + goto done; /* First fcTL after frame's fdAT's - done */ + continue; + + case PNG_TYPE_fdAT: +#define P (&chunk->fdat) + D("\n"); + if (im->frame_num <= 1) + continue; + if (!save_fdat) + continue; + /* Process fake IDAT frame data */ + cbuf.hdr.len = htonl(len - 4); + memcpy(cbuf.hdr.name, "IDAT", 4); + png_process_data(png_ptr, info_ptr, (void *)&cbuf, 8); + + png_set_crc_action(png_ptr, PNG_CRC_QUIET_USE, PNG_CRC_QUIET_USE); + png_process_data(png_ptr, info_ptr, (void *)P->data, len + 4 - 4); + png_set_crc_action(png_ptr, PNG_CRC_DEFAULT, PNG_CRC_DEFAULT); + continue; +#undef P + + default: + D("\n"); + break; + } + + png_process_data(png_ptr, info_ptr, fptr, len + 12); + + if (chunk->hdr.type == PNG_TYPE_IEND) + break; + } + + done: rc = ctx.rc; if (rc <= 0)