#include "spngt_common.h" #include "test_spng.h" #include "test_png.h" #include static int n_test_cases, actual_count; static struct spngt_test_case test_cases[100]; static int extended_tests(FILE *file, int fmt); const char* fmt_str(int fmt) { switch(fmt) { case SPNG_FMT_RGBA8: return "RGBA8"; case SPNG_FMT_RGBA16: return "RGBA16"; case SPNG_FMT_RGB8: return "RGB8"; case SPNG_FMT_GA8: return "GA8"; case SPNG_FMT_GA16: return "GA16"; case SPNG_FMT_G8: return "G8"; case SPNG_FMT_PNG: return "PNG"; case SPNG_FMT_RAW: return "RAW"; case SPNGT_FMT_VIPS: return "VIPS"; default: return " "; } } static void print_test_args(struct spngt_test_case *test_case) { char *type; if(test_case->test_flags & SPNGT_ENCODE_ROUNDTRIP) type = "Encode"; else type = "Decode"; printf("%s and compare %s", type, fmt_str(test_case->fmt)); char pad_str[] = " "; pad_str[sizeof(pad_str) - strlen(fmt_str(test_case->fmt))] = '\0'; printf(",%sFLAGS: ", pad_str); if(!test_case->flags && !test_case->test_flags) printf("(NONE)"); if(test_case->flags & SPNG_DECODE_TRNS) printf("TRNS "); if(test_case->flags & SPNG_DECODE_GAMMA) printf("GAMMA "); if(test_case->test_flags & SPNGT_COMPARE_CHUNKS) printf("COMPARE_CHUNKS "); if(test_case->test_flags & SPNGT_EXTENDED_TESTS) printf("EXTENDED "); if(test_case->test_flags & SPNGT_SKIP) printf("[SKIPPED]"); printf("\n"); fflush(stdout); } static void add_test_case(int fmt, int flags, int test_flags) { int n = n_test_cases; test_cases[n].fmt = fmt; test_cases[n].flags = flags; test_cases[n_test_cases++].test_flags = test_flags; if( !(test_flags & SPNGT_SKIP) ) actual_count++; } /* Returns 1 on different images with, allows a small difference if gamma corrected */ static int compare_images(const struct spng_ihdr *ihdr, int fmt, int flags, const unsigned char *img_spng, const unsigned char *img_png, size_t img_size) { uint32_t w, h; uint32_t x, y; uint32_t max_diff=0, diff_div = 50; /* allow up to ~2% difference for each channel */ uint8_t have_alpha = 1; uint8_t alpha_mismatch = 0; uint8_t pixel_diff = 0; uint8_t bytes_per_pixel = 4; /* SPNG_FMT_RGBA8 */ const uint8_t samples_per_byte = 8 / ihdr->bit_depth; const uint8_t mask = (uint16_t)(1 << ihdr->bit_depth) - 1; const uint8_t initial_shift = 8 - ihdr->bit_depth; uint8_t shift_amount = initial_shift; size_t row_width = img_size / ihdr->height; size_t px_ofs = 0; unsigned channels; uint32_t red_diff = 0, green_diff = 0, blue_diff = 0, sample_diff = 0; uint16_t spng_red = 0, spng_green = 0, spng_blue = 0, spng_alpha = 0, spng_sample = 0; uint16_t png_red = 0, png_green = 0, png_blue = 0, png_alpha = 0, png_sample = 0; w = ihdr->width; h = ihdr->height; if(fmt & (SPNG_FMT_PNG | SPNG_FMT_RAW)) { if(ihdr->color_type == SPNG_COLOR_TYPE_TRUECOLOR) { if(ihdr->bit_depth == 8) fmt = SPNG_FMT_RGB8; else fmt = SPNG_FMT_RGB16; } else if(ihdr->color_type == SPNG_COLOR_TYPE_TRUECOLOR_ALPHA) { if(ihdr->bit_depth == 8) fmt = SPNG_FMT_RGBA8; else fmt = SPNG_FMT_RGBA16; } else {/* gray 1,2,4,8,16 bits, indexed 1,2,4,8 or gray alpha 8,16 */ channels = 1; /* grayscale or indexed color */ if(ihdr->color_type == SPNG_COLOR_TYPE_GRAYSCALE_ALPHA) channels = 2; else have_alpha = 0; if(ihdr->bit_depth < 8) bytes_per_pixel = 1; else bytes_per_pixel = channels * (ihdr->bit_depth / 8); if(ihdr->color_type == SPNG_COLOR_TYPE_INDEXED) flags &= ~SPNG_DECODE_GAMMA; } } if(fmt == SPNG_FMT_RGBA8) { bytes_per_pixel = 4; max_diff = 256 / diff_div; } else if(fmt == SPNG_FMT_RGBA16) { bytes_per_pixel = 8; max_diff = 65536 / diff_div; } else if(fmt == SPNG_FMT_RGB8) { bytes_per_pixel = 3; have_alpha = 0; max_diff = 256 / diff_div; } else if(fmt == SPNG_FMT_RGB16) { bytes_per_pixel = 6; have_alpha = 0; max_diff = 65536 / diff_div; } else if(fmt == SPNG_FMT_GA8) { bytes_per_pixel = 2; have_alpha = 1; max_diff = 256 / diff_div; } else if(fmt == SPNG_FMT_G8) { bytes_per_pixel = 1; have_alpha = 0; max_diff = 256 / diff_div; } for(y=0; y < h; y++) { for(x=0; x < w; x++) { if(fmt & (SPNG_FMT_PNG | SPNG_FMT_RAW) && ihdr->bit_depth < 8) px_ofs = (y * row_width) + x / samples_per_byte; else px_ofs = (x + (y * w)) * bytes_per_pixel; if(fmt & (SPNG_FMT_RGBA16 | SPNG_FMT_RGB16)) { uint16_t s_red, s_green, s_blue, s_alpha; uint16_t p_red, p_green, p_blue, p_alpha; memcpy(&s_red, img_spng + px_ofs, 2); memcpy(&s_green, img_spng + px_ofs + 2, 2); memcpy(&s_blue, img_spng + px_ofs + 4, 2); memcpy(&p_red, img_png + px_ofs, 2); memcpy(&p_green, img_png + px_ofs + 2, 2); memcpy(&p_blue, img_png + px_ofs + 4, 2); if(have_alpha) { memcpy(&s_alpha, img_spng + px_ofs + 6, 2); memcpy(&p_alpha, img_png + px_ofs + 6, 2); spng_alpha = s_alpha; png_alpha = p_alpha; } spng_red = s_red; spng_green = s_green; spng_blue = s_blue; png_red = p_red; png_green = p_green; png_blue = p_blue; } else if(fmt & (SPNG_FMT_RGBA8 | SPNG_FMT_RGB8)) { uint8_t s_red, s_green, s_blue, s_alpha; uint8_t p_red, p_green, p_blue, p_alpha; memcpy(&s_red, img_spng + px_ofs, 1); memcpy(&s_green, img_spng + px_ofs + 1, 1); memcpy(&s_blue, img_spng + px_ofs + 2, 1); memcpy(&p_red, img_png + px_ofs, 1); memcpy(&p_green, img_png + px_ofs + 1, 1); memcpy(&p_blue, img_png + px_ofs + 2, 1); if(have_alpha) { memcpy(&s_alpha, img_spng + px_ofs + 3, 1); memcpy(&p_alpha, img_png + px_ofs + 3, 1); spng_alpha = s_alpha; png_alpha = p_alpha; } spng_red = s_red; spng_green = s_green; spng_blue = s_blue; png_red = p_red; png_green = p_green; png_blue = p_blue; } else if(fmt & (SPNG_FMT_PNG | SPNG_FMT_RAW | SPNG_FMT_G8 | SPNG_FMT_GA8 | SPNG_FMT_GA16)) { if(ihdr->bit_depth <= 8) /* gray 1-8, gray-alpha 8, indexed 1-8 */ { uint8_t s_alpha, s_sample; uint8_t p_alpha, p_sample; memcpy(&s_sample, img_spng + px_ofs, 1); memcpy(&p_sample, img_png + px_ofs, 1); if(shift_amount > 7) shift_amount = initial_shift; s_sample = (s_sample >> shift_amount) & mask; p_sample = (p_sample >> shift_amount) & mask; shift_amount -= ihdr->bit_depth; spng_sample = s_sample; png_sample = p_sample; if(have_alpha) { memcpy(&s_alpha, img_spng + px_ofs + 1, 1); memcpy(&p_alpha, img_png + px_ofs + 1, 1); spng_alpha = s_alpha; png_alpha = p_alpha; } } else /* gray 16, gray-alpha 16 */ { uint16_t s_alpha, s_sample; uint16_t p_alpha, p_sample; memcpy(&s_sample, img_spng + px_ofs, 2); memcpy(&p_sample, img_png + px_ofs, 2); spng_sample = s_sample; png_sample = p_sample; if(have_alpha) { memcpy(&s_alpha, img_spng + px_ofs + 2, 2); memcpy(&p_alpha, img_png + px_ofs + 2, 2); spng_alpha = s_alpha; png_alpha = p_alpha; } } } if(spng_red != png_red || spng_green != png_green || spng_blue != png_blue || spng_sample != png_sample) { if(flags & SPNG_DECODE_GAMMA) { red_diff = abs(spng_red - png_red); green_diff = abs(spng_green - png_green); blue_diff = abs(spng_blue - png_blue); sample_diff = abs(spng_sample - png_sample); if(red_diff > max_diff || green_diff > max_diff || blue_diff > max_diff) { printf("invalid gamma correction at x: %" PRIu32 " y: %" PRIu32 ", " "spng: %" PRIu16 " %" PRIu16 " %" PRIu16 " " "png: %" PRIu16 " %" PRIu16 " %" PRIu16 "\n", x, y, spng_red, spng_green, spng_blue, png_red, png_green, png_blue); pixel_diff = 1; } else if(sample_diff > max_diff) { printf("invalid gamma correction at x: %" PRIu32 " y: %" PRIu32 ", " "spng: %" PRIu16 " png: %" PRIu16 "\n", x, y, spng_sample, png_sample); pixel_diff = 1; } } else { if(spng_sample != png_sample) { char *issue_str = ""; if(ihdr->color_type == SPNG_COLOR_TYPE_INDEXED) issue_str = "index mismatch"; else issue_str = "grayscale difference"; printf("%s at x: %u y: %u spng: %u png: %u\n", issue_str, x, y, spng_sample, png_sample); } else { printf("color difference at x: %" PRIu32 " y: %" PRIu32 ", " "spng: %" PRIu16 " %" PRIu16 " %" PRIu16 " " "png: %" PRIu16 " %" PRIu16 " %" PRIu16 "\n", x, y, spng_red, spng_green, spng_blue, png_red, png_green, png_blue); } pixel_diff = 1; } } if(have_alpha && spng_alpha != png_alpha) { printf("alpha mismatch at x:%" PRIu32 " y:%" PRIu32 ", " "spng: %" PRIu16 " png: %" PRIu16 "\n", x, y, spng_alpha, png_alpha); alpha_mismatch = 1; } } } if(alpha_mismatch || pixel_diff) return 1; return 0; } static void print_chunks(spngt_chunk_bitfield chunks) { spngt_chunk_bitfield none = { 0 }; if(!memcmp(&none, &chunks, sizeof(spngt_chunk_bitfield))) printf(" (none)"); if(chunks.plte) printf(" PLTE"); if(chunks.trns) printf(" tRNS"); if(chunks.chrm) printf(" cHRM"); if(chunks.gama) printf(" gAMA"); if(chunks.iccp) printf(" iCCP"); if(chunks.sbit) printf(" sBIT"); if(chunks.srgb) printf(" sRGB"); if(chunks.text) printf(" tEXt"); if(chunks.ztxt) printf(" zTXt"); if(chunks.itxt) printf(" iTXt"); if(chunks.bkgd) printf(" bKGD"); if(chunks.hist) printf(" hIST"); if(chunks.phys) printf(" pHYs"); if(chunks.splt) printf(" sPLT"); if(chunks.time) printf(" tIME"); if(chunks.offs) printf(" oFFs"); if(chunks.exif) printf(" eXIF"); if(chunks.unknown) printf(" (unknown)"); } static void free_chunks(spngt_chunk_data *spng) { free(spng->splt); free(spng->text); free(spng->chunks); spng->splt = NULL; spng->text = NULL; spng->chunks = NULL; } static int get_chunks(spng_ctx *ctx, spngt_chunk_data *spng) { int ret = spng_get_ihdr(ctx, &spng->ihdr); if(!ret) spng->have.ihdr = 1; else return ret; ret = spng_get_plte(ctx, &spng->plte); if(!ret) { spng->n_plte_entries = spng->plte.n_entries; spng->have.plte = 1; } else if(ret == SPNG_ECHUNKAVAIL) ret = 0; else return ret; if(!spng_get_trns(ctx, &spng->trns)) spng->have.trns = 1; if(!spng_get_chrm(ctx, &spng->chrm)) spng->have.chrm = 1; if(spng->have.chrm) spng_get_chrm_int(ctx, &spng->chrm_int); if(!spng_get_gama(ctx, &spng->gamma)) spng->have.gama = 1; if(spng->have.gama) spng_get_gama_int(ctx, &spng->gamma_int); if(!spng_get_iccp(ctx, &spng->iccp)) spng->have.iccp = 1; if(!spng_get_sbit(ctx, &spng->sbit)) spng->have.sbit = 1; if(!spng_get_srgb(ctx, &spng->srgb_rendering_intent)) spng->have.srgb = 1; if(!spng_get_text(ctx, NULL, &spng->n_text)) spng->have.text = 1; if(!spng_get_bkgd(ctx, &spng->bkgd)) spng->have.bkgd = 1; if(!spng_get_hist(ctx, &spng->hist)) spng->have.hist = 1; if(!spng_get_phys(ctx, &spng->phys)) spng->have.phys = 1; if(!spng_get_splt(ctx, NULL, &spng->n_splt)) spng->have.splt = 1; if(!spng_get_time(ctx, &spng->time)) spng->have.time = 1; if(!spng_get_offs(ctx, &spng->offs)) spng->have.offs = 1; if(!spng_get_exif(ctx, &spng->exif)) spng->have.exif = 1; if(!spng_get_unknown_chunks(ctx, NULL, &spng->n_unknown_chunks)) spng->have.unknown = 1; if(spng->have.text) { spng->text = malloc(spng->n_text * sizeof(struct spng_text)); if(!spng->text) return 2; spng_get_text(ctx, spng->text, &spng->n_text); } if(spng->have.splt) { spng->splt = malloc(spng->n_splt * sizeof(struct spng_splt)); if(!spng->splt) { free_chunks(spng); return 2; } spng_get_splt(ctx, spng->splt, &spng->n_splt); } if(spng->have.unknown) { spng->chunks = malloc(spng->n_unknown_chunks * sizeof(struct spng_unknown_chunk)); if(!spng->chunks) { free_chunks(spng); return 2; } spng_get_unknown_chunks(ctx, spng->chunks, &spng->n_unknown_chunks); } return ret; } static int set_chunks(spng_ctx *dst, spngt_chunk_data *spng) { int ret = 0; ret = spng_set_ihdr(dst, &spng->ihdr); if(ret) { printf("spng_set_ihdr() error: %s\n", spng_strerror(ret)); return ret; } if(spng->have.plte) spng_set_plte(dst, &spng->plte); if(spng->have.trns) spng_set_trns(dst, &spng->trns); if(spng->have.chrm) spng_set_chrm_int(dst, &spng->chrm_int); if(spng->have.gama) spng_set_gama_int(dst, spng->gamma_int); if(spng->have.iccp) spng_set_iccp(dst, &spng->iccp); if(spng->have.sbit) spng_set_sbit(dst, &spng->sbit); if(spng->have.srgb) spng_set_srgb(dst, spng->srgb_rendering_intent); if(spng->have.text) spng_set_text(dst, spng->text, spng->n_text); if(spng->have.bkgd) spng_set_bkgd(dst, &spng->bkgd); if(spng->have.hist) spng_set_hist(dst, &spng->hist); if(spng->have.phys) spng_set_phys(dst, &spng->phys); if(spng->have.splt) spng_set_splt(dst, spng->splt, spng->n_splt); if(spng->have.time) spng_set_time(dst, &spng->time); if(spng->have.offs) spng_set_offs(dst, &spng->offs); if(spng->have.exif) spng_set_exif(dst, &spng->exif); if(spng->have.unknown) spng_set_unknown_chunks(dst, spng->chunks, spng->n_unknown_chunks); return ret; } static int compare_chunks(spng_ctx *ctx, png_infop info_ptr, png_structp png_ptr, int after_idat) { uint32_t i; enum spng_errno ret = 0; spngt_chunk_data spng = {0}; spngt_chunk_data png = {0}; ret = get_chunks(ctx, &spng); if(ret) goto cleanup; png.have.ihdr = 1; if(png_get_valid(png_ptr, info_ptr, PNG_INFO_PLTE)) png.have.plte = 1; if(png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) png.have.trns = 1; if(png_get_valid(png_ptr, info_ptr, PNG_INFO_cHRM)) png.have.chrm = 1; if(png_get_valid(png_ptr, info_ptr, PNG_INFO_gAMA)) png.have.gama = 1; if(png_get_valid(png_ptr, info_ptr, PNG_INFO_iCCP)) png.have.iccp = 1; if(png_get_valid(png_ptr, info_ptr, PNG_INFO_sBIT)) png.have.sbit = 1; if(png_get_valid(png_ptr, info_ptr, PNG_INFO_sRGB)) png.have.srgb = 1; if(png_get_valid(png_ptr, info_ptr, PNG_INFO_bKGD)) png.have.bkgd = 1; if(png_get_valid(png_ptr, info_ptr, PNG_INFO_hIST)) png.have.hist = 1; if(png_get_valid(png_ptr, info_ptr, PNG_INFO_pHYs)) png.have.phys = 1; if(png_get_valid(png_ptr, info_ptr, PNG_INFO_tIME)) png.have.time = 1; if(png_get_valid(png_ptr, info_ptr, PNG_INFO_oFFs)) png.have.offs = 1; if(png_get_valid(png_ptr, info_ptr, PNG_INFO_eXIf)) png.have.exif = 1; png.have.unknown = spng.have.unknown; int png_num_palette; png_color *png_palette; if(png_get_PLTE(png_ptr, info_ptr, &png_palette, &png_num_palette) == PNG_INFO_PLTE) png.n_plte_entries = png_num_palette; png_text *png_text; png.n_text = png_get_text(png_ptr, info_ptr, &png_text, NULL); if(png.n_text) png.have.text = 1; png_sPLT_t *png_splt; png.n_splt = png_get_sPLT(png_ptr, info_ptr, &png_splt); if(png.n_splt) png.have.splt = 1; png_unknown_chunk *png_chunks; png.n_unknown_chunks = png_get_unknown_chunks(png_ptr, info_ptr, &png_chunks); if(png.n_unknown_chunks) png.have.unknown = 1; const char *pos = after_idat ? " after IDAT" : "before IDAT"; printf("[%s] spng chunks: ", pos); print_chunks(spng.have); printf("\n"); printf("[%s] libpng chunks:", pos); print_chunks(png.have); printf("\n"); if(memcmp(&spng.have, &png.have, sizeof(spngt_chunk_bitfield))) { printf("[%s] ERROR: metadata mismatch!\n", pos); return 1; } /* NOTE: libpng changes or corrupts chunk data once it's past the IDAT stream, some checks are not done because of this. */ uint32_t png_width, png_height; int png_bit_depth = 0, png_color_type = 0, png_interlace_method = 0, png_compression_method = 0, png_filter_method = 0; png_get_IHDR(png_ptr, info_ptr, &png_width, &png_height, &png_bit_depth, &png_color_type, &png_interlace_method, &png_compression_method, &png_filter_method); if(spng.ihdr.width != png_width || spng.ihdr.height != png_height || spng.ihdr.bit_depth != png_bit_depth || spng.ihdr.color_type != png_color_type || spng.ihdr.interlace_method != png_interlace_method || spng.ihdr.compression_method != png_compression_method || spng.ihdr.filter_method != png_filter_method) { if(!after_idat) { printf("IHDR data not identical\n"); ret = 1; goto cleanup; } } if(spng.plte.n_entries != png.n_plte_entries) { printf("different number of palette entries (%u, %u)\n", spng.plte.n_entries, png.n_plte_entries); ret = 1; } else { for(i=0; i < spng.plte.n_entries; i++) { if(spng.plte.entries[i].red != png_palette[i].red || spng.plte.entries[i].green != png_palette[i].green || spng.plte.entries[i].blue != png_palette[i].blue) { printf("palette entry %d not identical\n", i); ret = 1; } } } if(spng.have.trns) { png_byte *png_trans_alpha; int png_num_trans; png_color_16 *png_trans_color; png_get_tRNS(png_ptr, info_ptr, &png_trans_alpha, &png_num_trans, &png_trans_color); if(spng.ihdr.color_type == SPNG_COLOR_TYPE_GRAYSCALE) { if(spng.trns.gray != png_trans_color->gray) { printf("tRNS gray sample is not identical\n"); ret = 1; } } else if(spng.ihdr.color_type == SPNG_COLOR_TYPE_TRUECOLOR) { if(spng.trns.red != png_trans_color->red || spng.trns.green != png_trans_color->green || spng.trns.blue != png_trans_color->blue) { printf("tRNS truecolor samples not identical\n"); ret = 1; } } else if(spng.ihdr.color_type == SPNG_COLOR_TYPE_INDEXED) { if(spng.trns.n_type3_entries == png_num_trans) { for(i=0; i < spng.trns.n_type3_entries; i++) { if(spng.trns.type3_alpha[i] != png_trans_alpha[i]) { printf("tRNS alpha entry %d not identical\n", i); ret = 1; } } } else { if(!after_idat) { printf("different number of tRNS alpha entries\n"); ret = 1; } } } } if(spng.have.chrm) { png_fixed_point png_fwhite_x, png_fwhite_y, png_fred_x, png_fred_y, png_fgreen_x, png_fgreen_y, png_fblue_x, png_fblue_y; png_get_cHRM_fixed(png_ptr, info_ptr, &png_fwhite_x, &png_fwhite_y, &png_fred_x, &png_fred_y, &png_fgreen_x, &png_fgreen_y, &png_fblue_x, &png_fblue_y); if(spng.chrm_int.white_point_x != png_fwhite_x || spng.chrm_int.white_point_y != png_fwhite_y || spng.chrm_int.red_x != png_fred_x || spng.chrm_int.red_y != png_fred_y || spng.chrm_int.green_x != png_fgreen_x || spng.chrm_int.green_y != png_fgreen_y || spng.chrm_int.blue_x != png_fblue_x || spng.chrm_int.blue_y != png_fblue_y) { printf("cHRM fixed point values are not identical\n"); ret = 1; } } if(spng.have.gama) { png_fixed_point png_fgamna; png_get_gAMA_fixed(png_ptr, info_ptr, &png_fgamna); if(spng.gamma_int != png_fgamna) { printf("gamma values not identical\n"); ret = 1; } } if(spng.have.iccp) { png_charp png_iccp_name; int png_iccp_compression_type; png_bytep png_iccp_profile; png_uint_32 png_iccp_proflen; png_get_iCCP(png_ptr, info_ptr, &png_iccp_name, &png_iccp_compression_type, &png_iccp_profile, &png_iccp_proflen); if(spng.iccp.profile_len == png_iccp_proflen) { if(memcmp(spng.iccp.profile, png_iccp_profile, spng.iccp.profile_len)) { printf("iccp profile data not identical\n"); ret = 1; } } else { printf("iccp profile lengths are different\n"); ret = 1; } } if(spng.have.sbit) { png_color_8p png_sig_bit; png_get_sBIT(png_ptr, info_ptr, &png_sig_bit); if(spng.ihdr.color_type == SPNG_COLOR_TYPE_GRAYSCALE && spng.sbit.grayscale_bits != png_sig_bit->gray) { printf("grayscale significant bits not identical\n"); ret = 1; } else if(spng.ihdr.color_type == SPNG_COLOR_TYPE_TRUECOLOR || spng.ihdr.color_type == SPNG_COLOR_TYPE_INDEXED) { if(spng.sbit.red_bits != png_sig_bit->red || spng.sbit.green_bits != png_sig_bit->green || spng.sbit.blue_bits != png_sig_bit->blue) { printf("rgb significant bits not identical\n"); ret = 1; } } else if(spng.ihdr.color_type == SPNG_COLOR_TYPE_GRAYSCALE_ALPHA) { if(spng.sbit.grayscale_bits != png_sig_bit->gray || spng.sbit.alpha_bits != png_sig_bit->alpha) { printf("grayscale alpha significant bits not identical\n"); ret = 1; } } else if(spng.ihdr.color_type == 6) { if(spng.sbit.red_bits != png_sig_bit->red || spng.sbit.green_bits != png_sig_bit->green || spng.sbit.blue_bits != png_sig_bit->blue || spng.sbit.alpha_bits != png_sig_bit->alpha) { printf("rgba significant bits not identical\n"); ret = 1; } } } if(spng.have.srgb) { int png_rgb_intent; png_get_sRGB(png_ptr, info_ptr, &png_rgb_intent); if(spng.srgb_rendering_intent != png_rgb_intent) { printf("sRGB rendering intent mismatch\n"); ret = 1; } } if(spng.n_text != png.n_text) { printf("text chunk count mismatch: %u(spng), %d(libpng)\n", spng.n_text, png.n_text); ret = 1; goto cleanup; } else { for(i=0; i < spng.n_text; i++) { if(strcmp(spng.text[i].keyword, png_text[i].key)) { printf("text[%d]: keyword mismatch!\nspng: %s\n\nlibpng: %s\n", i, spng.text[i].keyword, png_text[i].key); ret = 1; } if(strcmp(spng.text[i].text, png_text[i].text)) { printf("text[%d]: text mismatch!\nspng: %s\n\nlibpng: %s\n", i, spng.text[i].text, png_text[i].text); ret = 1; } if(spng.text[i].type != SPNG_ITXT) continue; if(strcmp(spng.text[i].language_tag, png_text[i].lang)) { printf("text[%d]: language tag mismatch!\nspng: %s\n\nlibpng: %s\n", i, spng.text[i].language_tag, png_text[i].lang); ret = 1; } if(strcmp(spng.text[i].translated_keyword, png_text[i].lang_key)) { printf("text[%d]: translated keyword mismatch!\nspng: %s\n\nlibpng: %s\n", i, spng.text[i].translated_keyword, png_text[i].lang_key); ret = 1; } } } if(spng.have.bkgd) { png_color_16p png_bkgd; png_get_bKGD(png_ptr, info_ptr, &png_bkgd); if(spng.ihdr.color_type == SPNG_COLOR_TYPE_GRAYSCALE || spng.ihdr.color_type == SPNG_COLOR_TYPE_GRAYSCALE_ALPHA) { if(spng.bkgd.gray != png_bkgd->gray) { printf("bKGD grayscale samples are not identical\n"); ret = 1; } } else if(spng.ihdr.color_type == SPNG_COLOR_TYPE_TRUECOLOR || spng.ihdr.color_type == SPNG_COLOR_TYPE_TRUECOLOR_ALPHA) { if(spng.bkgd.red != png_bkgd->red || spng.bkgd.green != png_bkgd->green || spng.bkgd.blue != png_bkgd->blue) { printf("bKGD rgb samples are not identical\n"); ret = 1; } } else if(spng.ihdr.color_type == SPNG_COLOR_TYPE_INDEXED) { if(spng.bkgd.plte_index != png_bkgd->index) { printf("bKGD type3 indices are not identical\n"); ret = 1; } } } if(spng.have.hist) { png_uint_16p png_hist; png_get_hIST(png_ptr, info_ptr, &png_hist); for(i=0; i < spng.plte.n_entries; i++) { if(spng.hist.frequency[i] != png_hist[i]) { printf("histogram entry %d is not identical\n", i); ret = 1; } } } if(spng.have.phys) { uint32_t png_phys_res_x, png_phys_rex_y; int png_phys_unit_type; png_get_pHYs(png_ptr, info_ptr, &png_phys_res_x, &png_phys_rex_y, &png_phys_unit_type); if(spng.phys.ppu_x != png_phys_res_x || spng.phys.ppu_y != png_phys_rex_y || spng.phys.unit_specifier != png_phys_unit_type) { printf("pHYs data not indentical\n"); ret = 1; } } if(spng.n_splt != png.n_splt) { printf("different number of suggested palettes\n"); ret = 1; } else { uint32_t j; for(j=0; j < spng.n_splt; j++) { if(strcmp(spng.splt[j].name, png_splt[j].name)) { printf("sPLT[%d]: name mismatch\n", j); ret = 1; } if(spng.splt[j].sample_depth != png_splt[j].depth) { printf("sPLT[%d]: sample depth mismatch\n", j); ret = 1; } if(spng.splt[j].n_entries != png_splt[j].nentries) { printf("sPLT[%d]: entry count mismatch\n", j); ret = 1; break; } struct spng_splt_entry entry; png_sPLT_entry png_entry; for(i=0; i < spng.splt[j].n_entries; i++) { entry = spng.splt[j].entries[i]; png_entry = png_splt[j].entries[i]; if(entry.alpha != png_entry.alpha || entry.red != png_entry.red || entry.green != png_entry.green || entry.blue != png_entry.blue || entry.frequency != png_entry.frequency) { printf("sPLT[%d]: mismatch for entry %d\n", j, i); ret = 1; } } } } if(spng.have.time) { png_time *png_time; png_get_tIME(png_ptr, info_ptr, &png_time); if(spng.time.year != png_time->year || spng.time.month != png_time->month || spng.time.day != png_time->day || spng.time.hour != png_time->hour || spng.time.minute != png_time->minute || spng.time.second != png_time->second) { printf("tIME data not identical\n"); ret = 1; } } if(spng.have.offs) { png_int_32 png_offset_x, png_offset_y; int png_offs_unit_type; png_get_oFFs(png_ptr, info_ptr, &png_offset_x, &png_offset_y, &png_offs_unit_type); if(spng.offs.x != png_offset_x || spng.offs.y != png_offset_y || spng.offs.unit_specifier != png_offs_unit_type) { printf("oFFs data not identical\n"); ret = 1; } } if(spng.have.exif) { png_byte *png_exif; png_uint_32 png_exif_length; png_get_eXIf_1(png_ptr, info_ptr, &png_exif_length, &png_exif); if(spng.exif.length == png_exif_length) { if(memcmp(spng.exif.data, png_exif, spng.exif.length)) { printf("eXIf data not identical\n"); ret = 1; } } else { printf("eXIf chunk length mismatch\n"); ret = 1; } } if(png.n_unknown_chunks != spng.n_unknown_chunks) { printf("unknown chunk count mismatch: %u(spng), %d(libpng)\n", spng.n_unknown_chunks, png.n_unknown_chunks); ret = 1; goto cleanup; } else { for(i=0; i < spng.n_unknown_chunks; i++) { if(spng.chunks[i].length != png_chunks[i].size) { printf("chunk[%d]: size mismatch %zu (spng) %zu (libpng)\n", i, spng.chunks[i].length, png_chunks[i].size); ret = 1; } if(spng.chunks[i].location != png_chunks[i].location) { printf("chunk[%d]: location mismatch\n", i); ret = 1; } } } cleanup: free_chunks(&spng); return ret; } static int decode_and_compare(spngt_test_case *spng, spngt_test_case *png) { int ret = 0; spng_ctx *ctx = NULL; size_t img_spng_size; unsigned char *img_spng = NULL; int fmt = spng->fmt, flags = spng->flags, test_flags = spng->test_flags; png_infop info_ptr = NULL; png_structp png_ptr = NULL; size_t img_png_size; unsigned char *img_png = NULL; struct spng_ihdr ihdr; ctx = init_spng(spng, &ihdr); if(ctx == NULL) { ret = 1; goto cleanup; } spng_set_crc_action(ctx, SPNG_CRC_ERROR, SPNG_CRC_ERROR); img_spng = getimage_spng(ctx, &img_spng_size, fmt, flags); if(img_spng == NULL) { printf("getimage_spng() failed\n"); ret = 1; goto cleanup; } png_ptr = init_libpng(png, &info_ptr); if(png_ptr == NULL) { ret = 1; goto cleanup; } if(test_flags & SPNGT_COMPARE_CHUNKS) { ret = compare_chunks(ctx, info_ptr, png_ptr, 0); } img_png = getimage_libpng(png_ptr, info_ptr, &img_png_size, fmt, flags); if(img_png == NULL) { printf("getimage_libpng() failed\n"); ret = 1; goto cleanup; } if(img_png_size != img_spng_size) { printf("output image size mismatch\n"); printf("spng: %lu\n png: %lu\n", (unsigned long int)img_spng_size, (unsigned long int)img_png_size); ret = 1; goto cleanup; } if(fmt == SPNGT_FMT_VIPS) {/* Get the right format for compare_images() */ fmt = SPNG_FMT_PNG; if(ihdr.color_type == SPNG_COLOR_TYPE_INDEXED) fmt = SPNG_FMT_RGB8; else if(ihdr.color_type == SPNG_COLOR_TYPE_GRAYSCALE && ihdr.bit_depth < 8) fmt = SPNG_FMT_G8; spng_get_trns_fmt(ctx, &fmt); printf("VIPS format: %s\n", fmt_str(fmt)); } if(!memcmp(img_spng, img_png, img_spng_size)) goto identical; if( !(flags & SPNG_DECODE_GAMMA) ) { printf("error: image buffers are not identical\n"); ret = 1; } ret |= compare_images(&ihdr, fmt, flags, img_spng, img_png, img_spng_size); if(!ret && !(flags & SPNG_DECODE_GAMMA)) {/* in case compare_images() has some edge case */ printf("compare_images() returned 0 but images are not identical\n"); ret = 1; goto cleanup; } identical: if(test_flags & SPNGT_COMPARE_CHUNKS) { ret |= compare_chunks(ctx, info_ptr, png_ptr, 1); } cleanup: spng_ctx_free(ctx); png_destroy_read_struct(&png_ptr, &info_ptr, NULL); free(img_spng); free(img_png); return ret; } static void dump_buffer(void *buf, size_t size, const char *opt_name) { const char *filename = opt_name ? opt_name : "dump.png"; FILE *f = fopen(filename, "wb"); if(f == NULL) goto err; size_t written = fwrite(buf, size, 1, f); if(written != 1) goto err; printf("file dumped to %s\n", filename); goto cleanup; err: printf("failed to dump file: %s\n", strerror(errno)); cleanup: if(f != NULL) fclose(f); } static int spngt_run_test(const char *filename, struct spngt_test_case *test_case) { enum spng_errno ret; size_t file_length = 0; struct spngt_test_case spng = *test_case; struct spngt_test_case libpng = *test_case; if(test_case->source.type == SPNGT_SRC_FILE) { spng.source.file = fopen(filename, "rb"); libpng.source.file = fopen(filename, "rb"); if(!spng.source.file || !libpng.source.file) { ret = 1; goto cleanup; } fseek(spng.source.file, 0, SEEK_END); file_length = ftell(spng.source.file); rewind(spng.source.file); } ret = decode_and_compare(&spng, &libpng); if(ret) goto cleanup; if(test_case->test_flags & SPNGT_ENCODE_ROUNDTRIP) { printf(" compare (reencoded, original)...\n"); if(test_case->source.type == SPNGT_SRC_FILE) { rewind(spng.source.file); rewind(libpng.source.file); } spng_ctx *src = init_spng(&spng, NULL); spng_ctx *dst = NULL; spngt_chunk_data data = {0}; size_t img_size; void *img_spng = getimage_spng(src, &img_size, test_case->fmt, test_case->flags); size_t encoded_len; char *encoded_pngbuf = NULL; if(img_spng == NULL) { ret = 1; goto encode_cleanup; } ret = get_chunks(src, &data); if(ret) goto encode_cleanup; dst = spng_ctx_new(SPNG_CTX_ENCODER); spng_set_option(dst, SPNG_ENCODE_TO_BUFFER, 1); ret = set_chunks(dst, &data); if(ret) goto encode_cleanup; ret = spng_encode_image(dst, img_spng, img_size, test_case->fmt, SPNG_ENCODE_FINALIZE); if(ret) { printf("encode error: %s\n", spng_strerror(ret)); goto encode_cleanup; } encoded_pngbuf = spng_get_png_buffer(dst, &encoded_len, &ret); if(ret) { printf("failed to get encoded PNG: %s\n", spng_strerror(ret)); goto encode_cleanup; } /* Unfortunately there's a handful of testsuite image that don't compress well with the default filter heuristic */ /* Fail the test on a 4% size increase */ /*int pct = 25; if( (encoded_len - encoded_len / pct) > file_length) { printf("Reencoded PNG exceeds maximum %d%% size increase: %zu (original: %zu)\n", 100 / pct, encoded_len, file_length); ret = 1; goto encode_cleanup; }*/ (void)file_length; spng.source.type = SPNGT_SRC_BUFFER; spng.source.buffer = encoded_pngbuf; spng.source.png_size = encoded_len; spng.test_flags |= SPNGT_COMPARE_CHUNKS; ret = decode_and_compare(&spng, &libpng); if(ret) { printf("compare error (%d))\n", ret); dump_buffer(encoded_pngbuf, encoded_len, NULL); goto encode_cleanup; } libpng.source.type = SPNGT_SRC_BUFFER; libpng.source.buffer = encoded_pngbuf; libpng.source.png_size = encoded_len; libpng.test_flags |= SPNGT_COMPARE_CHUNKS; printf(" compare (reencoded, reencoded)...\n"); ret = decode_and_compare(&spng, &libpng); if(ret) { printf("compare error (%d))\n", ret); dump_buffer(encoded_pngbuf, encoded_len, NULL); } encode_cleanup: free(img_spng); free(encoded_pngbuf); spng_ctx_free(src); spng_ctx_free(dst); free_chunks(&data); } if(test_case->test_flags & SPNGT_EXTENDED_TESTS) { rewind(spng.source.file); ret = extended_tests(spng.source.file, test_case->fmt); if(ret) printf("extended tests failed\n"); } cleanup: if(spng.source.file) fclose(spng.source.file); if(libpng.source.file) fclose(libpng.source.file); return ret; } static int get_image_info(FILE *f, struct spng_ihdr *ihdr) { spng_ctx *ctx = spng_ctx_new(0); spng_set_crc_action(ctx, SPNG_CRC_USE, SPNG_CRC_USE); spng_set_png_file(ctx, f); int ret = spng_get_ihdr(ctx, ihdr); spng_ctx_free(ctx); return ret; } int main(int argc, char **argv) { if(argc < 2) { printf("no input file\n"); return 1; } char *filename = argv[1]; if(!strcmp(filename, "info")) { unsigned int png_ver = png_access_version_number(); printf("spng header version: %u.%u.%u, library version: %s\n", SPNG_VERSION_MAJOR, SPNG_VERSION_MINOR, SPNG_VERSION_PATCH, spng_version_string()); printf("png header version: %u.%u.%u, library version: %u.%u.%u\n", PNG_LIBPNG_VER_MAJOR, PNG_LIBPNG_VER_MINOR, PNG_LIBPNG_VER_RELEASE, png_ver / 10000, png_ver / 100 % 100, png_ver % 100); return 0; } FILE *file = fopen(filename, "rb"); if(file == NULL) { printf("error opening input file %s\n", filename); return 1; } struct spng_ihdr ihdr = {0}; if(!get_image_info(file, &ihdr)) { char *clr_type_str; if(ihdr.color_type == SPNG_COLOR_TYPE_GRAYSCALE) clr_type_str = "GRAY"; else if(ihdr.color_type == SPNG_COLOR_TYPE_TRUECOLOR) clr_type_str = "RGB"; else if(ihdr.color_type == SPNG_COLOR_TYPE_INDEXED) clr_type_str = "INDEXED"; else if(ihdr.color_type == SPNG_COLOR_TYPE_GRAYSCALE_ALPHA) clr_type_str = "GRAY-ALPHA"; else clr_type_str = "RGBA"; printf("%s %" PRIu8 "-bit, %" PRIu32 "x%" PRIu32 " %s\n", clr_type_str, ihdr.bit_depth, ihdr.width, ihdr.height, ihdr.interlace_method ? "interlaced" : "non-interlaced"); } else printf("failed to get image info\n"); fclose(file); /* With libpng it's not possible to request 8/16-bit images regardless of PNG format without calling functions that alias to png_set_expand(_16), which acts as if png_set_tRNS_to_alpha() was called, as a result there are no tests where transparency is not applied */ int gamma_bug = 0; int skip_encode = 0; int fmt_limit = SPNGT_SKIP; int fmt_limit_2 = SPNGT_SKIP; /* https://github.com/randy408/libspng/issues/17 */ if(ihdr.color_type == SPNG_COLOR_TYPE_GRAYSCALE) gamma_bug = SPNGT_SKIP; /* Some output formats are limited to certain PNG formats */ if(ihdr.color_type == SPNG_COLOR_TYPE_GRAYSCALE && ihdr.bit_depth <= 8) fmt_limit = 0; if(ihdr.color_type == SPNG_COLOR_TYPE_GRAYSCALE && ihdr.bit_depth == 16) fmt_limit_2 = 0; add_test_case(SPNG_FMT_PNG, 0, SPNGT_COMPARE_CHUNKS); add_test_case(SPNG_FMT_PNG, 0, SPNGT_ENCODE_ROUNDTRIP | skip_encode); add_test_case(SPNG_FMT_RAW, 0, SPNGT_ENCODE_ROUNDTRIP | skip_encode); add_test_case(SPNG_FMT_RAW, 0, 0); add_test_case(SPNG_FMT_RGBA8, SPNG_DECODE_TRNS, 0); add_test_case(SPNG_FMT_RGBA8, SPNG_DECODE_TRNS | SPNG_DECODE_GAMMA, gamma_bug); add_test_case(SPNG_FMT_RGBA16, SPNG_DECODE_TRNS, 0); add_test_case(SPNG_FMT_RGBA16, SPNG_DECODE_TRNS | SPNG_DECODE_GAMMA, 0); add_test_case(SPNG_FMT_RGB8, 0, 0); add_test_case(SPNG_FMT_RGB8, SPNG_DECODE_GAMMA, gamma_bug); add_test_case(SPNG_FMT_G8, 0, fmt_limit); add_test_case(SPNG_FMT_GA8, 0, fmt_limit); add_test_case(SPNG_FMT_GA8, SPNG_DECODE_TRNS, fmt_limit); add_test_case(SPNG_FMT_GA16, 0, fmt_limit_2); add_test_case(SPNG_FMT_GA16, SPNG_DECODE_TRNS, fmt_limit_2); /* This tests the input->output format logic used in libvips, it emulates the behavior of their old PNG loader which uses libpng. */ add_test_case(SPNGT_FMT_VIPS, SPNG_DECODE_TRNS, 0); add_test_case(SPNG_FMT_PNG, 0, SPNGT_EXTENDED_TESTS); printf("%d test cases", n_test_cases); if(n_test_cases != actual_count) printf(" (skipping %d)", n_test_cases - actual_count); printf("\n"); int i, ret = 0; for(i=0; i < n_test_cases; i++) { print_test_args(&test_cases[i]); if(test_cases[i].test_flags & SPNGT_SKIP) continue; int e = spngt_run_test(filename, &test_cases[i]); if(!ret) ret = e; } return ret; } static int stream_write_checked(spng_ctx *ctx, void *user, void *data, size_t len) { (void)ctx; struct buf_state *state = user; if(len > state->bytes_left) return SPNG_IO_EOF; if(memcmp(data, state->data, len)) { printf("stream write does not match buffer data\n"); return SPNG_IO_ERROR; } state->data += len; state->bytes_left -= len; return 0; } /* Tests that don't fit anywhere else */ static int extended_tests(FILE *file, int fmt) { uint32_t i; enum spng_errno ret = 0; unsigned char *image = NULL; unsigned char *encoded = NULL; spng_ctx *enc = NULL; spng_ctx *dec = spng_ctx_new(0); struct spng_ihdr ihdr = {0}; struct spng_plte plte = {0}; static unsigned char chunk_data[9000]; /* NOTE: This value is compressed to 2 bits by zlib, it's not a 1:1 mapping */ int compression_level = 0; int expected_compression_level = 0; spng_set_png_file(dec, file); spng_get_ihdr(dec, &ihdr); spng_get_plte(dec, &plte); size_t image_size; image = getimage_spng(dec, &image_size, fmt, 0); enc = spng_ctx_new(SPNG_CTX_ENCODER); spng_set_option(enc, SPNG_ENCODE_TO_BUFFER, 1); spng_set_option(enc, SPNG_IMG_COMPRESSION_LEVEL, compression_level); spng_set_ihdr(enc, &ihdr); if(plte.n_entries) spng_set_plte(enc, &plte); struct spng_unknown_chunk chunk = { .location = SPNG_AFTER_IHDR, .type = "cHNK", .length = sizeof(chunk_data), .data = chunk_data }; uint8_t x = 0; for(i=0; i < chunk.length; i++) { chunk_data[i] = x; x++; } spng_set_unknown_chunks(enc, &chunk, 1); ret = spng_encode_image(enc, image, image_size, fmt, SPNG_ENCODE_FINALIZE); if(ret) { printf("encoding failed (%d): %s\n", ret, spng_strerror(ret)); goto cleanup; } size_t bytes_encoded; encoded = spng_get_png_buffer(enc, &bytes_encoded, &ret); if(!encoded) { printf("getting buffer failed (%d): %s\n", ret, spng_strerror(ret)); goto cleanup; } spng_ctx_free(enc); enc = NULL; /* Verify the image's zlib FLEVEL */ spng_ctx_free(dec); dec = spng_ctx_new(0); spng_set_png_buffer(dec, encoded, bytes_encoded); spng_decode_image(dec, NULL, 0, SPNG_FMT_PNG, SPNG_DECODE_PROGRESSIVE); ret = spng_get_option(dec, SPNG_IMG_COMPRESSION_LEVEL, &compression_level); if(ret || (compression_level != expected_compression_level) ) { if(ret) printf("error getting image compression level: %s\n", spng_strerror(ret)); else { printf("unexpected compression level (expected %d, got %d)\n", expected_compression_level, compression_level); ret = 1; } goto cleanup; } /* Reencode the same image but to a stream this time */ enc = spng_ctx_new(SPNG_CTX_ENCODER); struct buf_state state = { .data = encoded, .bytes_left = bytes_encoded }; spng_set_png_stream(enc, stream_write_checked, &state); spng_set_option(enc, SPNG_IMG_COMPRESSION_LEVEL, compression_level); spng_set_ihdr(enc, &ihdr); if(plte.n_entries) spng_set_plte(enc, &plte); spng_set_unknown_chunks(enc, &chunk, 1); ret = spng_encode_image(enc, 0, 0, fmt, SPNG_ENCODE_PROGRESSIVE | SPNG_ENCODE_FINALIZE); if(ret) { printf("progressive init failed: %s\n", spng_strerror(ret)); goto cleanup; } struct spng_row_info row_info = {0}; size_t image_width = image_size / ihdr.height; do { ret = spng_get_row_info(enc, &row_info); if(ret) break; void *ptr = image + image_width * row_info.row_num; ret = spng_encode_row(enc, ptr, image_width); }while(!ret); if(ret == SPNG_EOI) ret = 0; if(ret) { printf("reencode to stream failed (%d): %s\n", ret, spng_strerror(ret)); printf("stream offset: %zu\n", bytes_encoded - state.bytes_left); } if(spng_get_png_buffer(enc, &bytes_encoded, &ret)) { printf("this should not happen\n"); ret = 1; goto cleanup; } if(!ret) { printf("spng_get_png_buffer(): invalid return value\n"); ret = 1; goto cleanup; } else ret = 0; /* clear the (expected) error */ if(state.bytes_left) { printf("incomplete stream (%zu bytes shorter)\n", state.bytes_left); ret = 1; } cleanup: free(image); free(encoded); spng_ctx_free(dec); spng_ctx_free(enc); return ret; }