diff --git a/src/formats/png.c b/src/formats/png.c index 5d97fef..dcbaf4f 100644 --- a/src/formats/png.c +++ b/src/formats/png.c @@ -28,16 +28,356 @@ static void png_reader(png_structp png, png_bytep buffer, size_t size) } } +// read image to allocated buffer +static bool do_read_png_image(png_structp png, uint32_t* out, uint32_t width, + uint32_t height) +{ + png_bytepp rows; + + rows = png_malloc(png, height * sizeof(png_bytep)); + if (!rows) { + return false; + } + + for (uint32_t i = 0; i < height; ++i) { + rows[i] = (png_bytep)&out[i * width]; + } + + if (setjmp(png_jmpbuf(png))) { + png_free(png, rows); + return false; + } + + png_read_image(png, rows); + + png_free(png, rows); + + return true; +} + +// get a single png frame +static bool decode_png_frame(struct image* ctx, png_structp png, png_infop info) +{ + struct image_frame* frame; + uint32_t width, height; + + width = png_get_image_width(png, info); + height = png_get_image_height(png, info); + + frame = image_create_frame(ctx, width, height); + if (!frame) { + png_destroy_read_struct(&png, &info, NULL); + return false; + } + + if (do_read_png_image(png, frame->data, width, height)) { + return true; + } else { + image_free_frames(ctx); + return false; + } + + return true; +} + +#ifdef PNG_APNG_SUPPORTED +/** + * Copy frame area of output buffer + * @param dst frame buffer + * @param dw frame buffer width + * @param dh frame buffer height + * @param src png output buffer + * @param sw output buffer width + * @param xo output buffer offset by x + * @param yo output buffer offset by y + */ +static inline void copy_frame(uint32_t* dst, uint32_t dw, uint32_t dh, + uint32_t* src, uint32_t sw, uint32_t xo, + uint32_t yo) +{ + for (uint32_t i = 0; i < dh; ++i) { + memcpy(dst + (i * dw), src + (yo + i) * sw + xo, dw * sizeof(*dst)); + } +} + +/** + * Paste frame area to output buffer + * @param dst png output buffer + * @param dw output buffer width + * @param src frame buffer + * @param sw frame buffer wudth + * @param xo output buffer offset by x + * @param yo output buffer offset by y + */ +static inline void paste_frame(uint32_t* dst, uint32_t dw, uint32_t* src, + uint32_t sw, uint32_t sh, uint32_t xo, + uint32_t yo) +{ + for (uint32_t i = 0; i < sh; ++i) { + memcpy(dst + (yo + i) * dw + xo, src + (i * sw), sw * sizeof(*src)); + } +} + +/** + * Clear frame area of output buffer + * @param dst output buffer + * @param w output buffer width + * @param aw clearing area width + * @param ah clearing area height + * @param xo output buffer offset by x + * @param yo output buffer offset by y + */ +static inline void clear_area(uint32_t* dst, uint32_t w, uint32_t aw, + uint32_t ah, uint32_t xo, uint32_t yo) +{ + for (uint32_t i = 0; i < ah; ++i) { + memset(dst + (yo + i) * w + xo, 0, aw * sizeof(*dst)); + } +} + +/** + * Composite two buffers based on its alpha + * @param dst output buffer + * @param dw output buffer width + * @param src frame buffer + * @param sh frame buffer height + * @param sw frame buffer width + * @param xo output buffer offset by x + * @param yo output buffer offset by y + */ +static inline void blend_over(uint32_t* dst, uint32_t dw, uint32_t* src, + uint32_t sw, uint32_t sh, uint32_t xo, + uint32_t yo) +{ + for (uint32_t y = 0; y < sh; ++y) { + for (uint32_t x = 0; x < sw; ++x) { + uint32_t fg_pixel = *(src + y * sw + x); + + uint8_t fg_alpha = fg_pixel >> 24; + + if (fg_alpha == 255) { + *(dst + (yo + y) * dw + xo + x) = fg_pixel; + continue; + } + + if (fg_alpha != 0) { + uint32_t bg_pixel = *(dst + (yo + y) * dw + xo + x); + uint32_t bg_alpha = bg_pixel >> 24; + + if (bg_alpha != 0) { + *(dst + (yo + y) * dw + xo + x) = ARGB_ALPHA_BLEND( + bg_alpha, fg_alpha, bg_pixel, fg_pixel); + } else { + *(dst + (yo + y) * dw + xo + x) = fg_pixel; + } + } + } + } +} + +// Delete all allocated frames except first one and reduce allocated memory +static void delete_frames_keep_default(struct image* ctx, uint32_t last_frame) +{ + struct image_frame* tmp; + + ctx->num_frames = 1; + for (uint32_t i = 1; i <= last_frame; ++i) { + free(ctx->frames[i].data); + } + + tmp = realloc(ctx->frames, sizeof(*ctx->frames)); + + if (tmp) { + ctx->frames = tmp; + } +} + +// Get png frames sequence or default png image +static bool decode_png_frames(struct image* ctx, png_structp png, + png_infop info) +{ + uint32_t* frame = NULL; + uint32_t* output_buffer = NULL; + uint32_t* tmp = NULL; + uint8_t dispose_op, blend_op; + uint16_t delay_num, delay_den; + uint32_t num_frames; + uint32_t width, height; + uint32_t x_offset, y_offset, sub_width, sub_height; + bool first_frame_hidden; + size_t buffer_size; + + width = png_get_image_width(png, info); + height = png_get_image_height(png, info); + first_frame_hidden = png_get_first_frame_is_hidden(png, info); + num_frames = png_get_num_frames(png, info); + buffer_size = width * height * sizeof(*frame); + + output_buffer = malloc(buffer_size); + if (!output_buffer) { + image_print_error(ctx, "not enough memory"); + return false; + } + + if (!image_create_frames(ctx, num_frames)) { + free(output_buffer); + return false; + } + + // buffer must be black and transparent at start + memset(output_buffer, 0, buffer_size); + + for (uint32_t frame_num = 0; frame_num < num_frames; frame_num++) { + if (!image_frame_allocate(&ctx->frames[frame_num], width, height)) { + goto fail; + } + + if (setjmp(png_jmpbuf(png))) { + if (frame_num > 0) { + delete_frames_keep_default(ctx, frame_num); + goto done; + } else { + goto fail; + } + } + + if (png_get_valid(png, info, PNG_INFO_acTL)) { + png_read_frame_head(png, info); + } + + if (png_get_valid(png, info, PNG_INFO_fcTL)) { + png_get_next_frame_fcTL(png, info, &sub_width, &sub_height, + &x_offset, &y_offset, &delay_num, + &delay_den, &dispose_op, &blend_op); + } else { + sub_width = width; + sub_height = height; + x_offset = y_offset = 0; + delay_num = delay_den = 100; + dispose_op = PNG_DISPOSE_OP_BACKGROUND; + blend_op = PNG_BLEND_OP_SOURCE; + } + + if (sub_width + x_offset > width || sub_height + y_offset > height) { + image_print_error(ctx, "malformed png"); + goto fail; + } + + frame = malloc(sub_height * sub_width * sizeof(*frame)); + if (!frame) { + image_print_error(ctx, "not enough memory for frame buffer"); + goto fail; + } + + if (!do_read_png_image(png, frame, sub_width, sub_height)) { + free(frame); + if (frame_num > 0) { + delete_frames_keep_default(ctx, frame_num); + goto done; + } else { + goto fail; + } + } + + if (dispose_op == PNG_DISPOSE_OP_PREVIOUS) { + if (frame_num > 0) { + tmp = malloc(sub_width * sub_height * sizeof(*tmp)); + if (tmp) { + copy_frame(tmp, sub_width, sub_height, output_buffer, width, + x_offset, y_offset); + } + } else { + dispose_op = PNG_DISPOSE_OP_BACKGROUND; + } + } + + switch (blend_op) { + case PNG_BLEND_OP_SOURCE: + paste_frame(output_buffer, width, frame, sub_width, sub_height, + x_offset, y_offset); + break; + case PNG_BLEND_OP_OVER: + blend_over(output_buffer, width, frame, sub_width, sub_height, + x_offset, y_offset); + break; + default: + image_print_error(ctx, "unsupported blend type"); + break; + } + + switch (dispose_op) { + case PNG_DISPOSE_OP_PREVIOUS: + memcpy(ctx->frames[frame_num].data, output_buffer, buffer_size); + + if (tmp) { + paste_frame(output_buffer, width, tmp, sub_width, + sub_height, x_offset, y_offset); + free(tmp); + tmp = NULL; + } + break; + case PNG_DISPOSE_OP_BACKGROUND: + memcpy(ctx->frames[frame_num].data, output_buffer, buffer_size); + + clear_area(output_buffer, width, sub_width, sub_height, + x_offset, y_offset); + break; + case PNG_DISPOSE_OP_NONE: + memcpy(ctx->frames[frame_num].data, output_buffer, buffer_size); + break; + default: + image_print_error(ctx, "unsupported disposition type"); + break; + } + + if (delay_num == 0) { + ctx->frames[frame_num].duration = 0; + } else { + if (delay_den == 0) { + delay_den = 100; + } + + ctx->frames[frame_num].duration = + (size_t)((double)delay_num * 1000.0f / (double)delay_den); + } + + free(frame); + } + + if (first_frame_hidden) { + struct image_frame* tmp; + ctx->num_frames -= 1; + free(ctx->frames[0].data); + memmove(&ctx->frames[0], &ctx->frames[1], + ctx->num_frames * sizeof(*ctx->frames)); + + tmp = realloc(ctx->frames, ctx->num_frames * sizeof(*ctx->frames)); + + if (tmp) { + ctx->frames = tmp; + } + } + +done: + free(output_buffer); + return true; + +fail: + free(output_buffer); + image_free_frames(ctx); + return false; +} +#endif + // PNG loader implementation enum loader_status decode_png(struct image* ctx, const uint8_t* data, size_t size) { png_struct* png = NULL; png_info* info = NULL; - png_bytep* lines = NULL; - size_t width, height; png_byte color_type, bit_depth; - struct image_frame* frame; + png_byte interlace; + int ret; struct mem_reader reader = { .data = data, @@ -66,8 +406,6 @@ enum loader_status decode_png(struct image* ctx, const uint8_t* data, // setup error handling if (setjmp(png_jmpbuf(png))) { png_destroy_read_struct(&png, &info, NULL); - free(lines); - image_free_frames(ctx); image_print_error(ctx, "failed to decode png"); return ldr_fmterror; } @@ -75,10 +413,9 @@ enum loader_status decode_png(struct image* ctx, const uint8_t* data, // get general image info png_set_read_fn(png, &reader, &png_reader); png_read_info(png, info); - width = png_get_image_width(png, info); - height = png_get_image_height(png, info); color_type = png_get_color_type(png, info); bit_depth = png_get_bit_depth(png, info); + interlace = png_get_interlace_type(png, info); // setup decoder if (color_type == PNG_COLOR_TYPE_PALETTE) { @@ -102,33 +439,37 @@ enum loader_status decode_png(struct image* ctx, const uint8_t* data, png_set_packswap(png); png_set_bgr(png); - frame = image_create_frame(ctx, width, height); - if (!frame) { - png_destroy_read_struct(&png, &info, NULL); - return ldr_fmterror; + if (interlace != PNG_INTERLACE_NONE) + png_set_interlace_handling(png); + + png_set_expand(png); + + png_read_update_info(png, info); + +#ifdef PNG_APNG_SUPPORTED + if (png_get_valid(png, info, PNG_INFO_acTL)) { + if (png_get_num_frames(png, info) > 1) { + ret = decode_png_frames(ctx, png, info); + } else { + ret = decode_png_frame(ctx, png, info); + } + } else { + ret = decode_png_frame(ctx, png, info); } +#else + ret = decode_png_frame(ctx, png, info); +#endif - // prepare list of pointers to image lines - lines = malloc(height * sizeof(png_bytep)); - if (!lines) { - image_print_error(ctx, "not enough memory"); + if (!ret) { png_destroy_read_struct(&png, &info, NULL); - image_free_frames(ctx); return ldr_fmterror; } - for (size_t i = 0; i < height; ++i) { - lines[i] = (png_bytep)&frame->data[frame->width * i]; - } - - // read image - png_read_image(png, lines); image_set_format(ctx, "PNG %dbit", bit_depth * 4); ctx->alpha = true; // free resources png_destroy_read_struct(&png, &info, NULL); - free(lines); return ldr_success; }