/* Test presumed libpng packing bug (not a bug!) */ /* Last edited on 2024-12-21 11:45:39 by stolfi */ #include #include #include #include #include #include int main (int argc, char **argv); void do_test(char* image_name, int unpack, uint32_t cols, uint32_t rows, uint32_t chns, uint32_t pnm_maxval); /* Tries to read the image file "in/{image_name}.png", with palette and alpha expansion. If {unpack} is 1, resquests unpacking of small sample values with {png_set+packing}. Expects the image to have {cols} columns and {rows} rows of pixels, each with {chns} samples. Expects all samples in each channel {c} to be in the range {0..2^n-1} where {n} is the true bit size of that channel, as specified in the PNG header and the {sBIT} chunk. Also writes the samples to a file "out/{image_name}-{unpack}.pgm" (if the PNG file was grayscale) or "out/{image_name}-{unpack}.ppm" (if it was color), in ascii PGM/PPM format. If the PGM image has an alpha channel, writes that out as a separate file "out/{image_name}-{unpack}-alpha.pgm". These images will have {maxval = pnm_maxval}, whatever the range of samples in the input file. */ FILE *open_input_png_file(char *dir, char *image_name); /* Opens the file "{dir}/{image_name}.png" for reading. */ FILE *open_output_pnm_file ( char *dir, char *image_name, int unpack, char *tag, uint32_t cols, uint32_t rows, uint32_t chns, uint32_t maxval ); /* Open the file "{dir}/{image_name}-{unpack}{tag}.{ext}" for writing, and writes the proper ascii PGM or PPM header. The extension {ext} is "pgm" if {chns} is 1, "ppm" if {chns} is 3. The {maxval} is written to the file header. */ void dump_info(FILE *wr, char *label, png_structp pr, png_infop pi); /* Writes to {wr} various data taken from the PNG structures {pr,pi}. */ int main (int argc, char **argv) { /* Try reading test images without and with {png_set_packing}: */ for (int unpack = 0; unpack <= 1; unpack++) { do_test("BASI0G01", unpack, 32, 32, 1, 1); do_test("BASI0G02", unpack, 32, 32, 1, 3); do_test("BASI0G04", unpack, 32, 32, 1, 15); do_test("BASI3P02", unpack, 32, 32, 3, 1); do_test("BASI3P04", unpack, 32, 32, 3, 15); do_test("S04I3P01", unpack, 4, 4, 3, 15); do_test("S05I3P02", unpack, 5, 5, 3, 15); do_test("S06I3P02", unpack, 6, 6, 3, 15); } return 0; } void do_test(char* image_name, int unpack, uint32_t cols, uint32_t rows, uint32_t chns, uint32_t pnm_maxval) { fprintf(stderr, "=== %s unpack = %d =============================================\n", image_name, unpack); int has_alpha = (chns == 2) || (chns == 4); /* Open PNG image file "{image_name}.png" for input: */ FILE *rd = open_input_png_file("in", image_name); /* Open ascii PGM/PPM file "{image_name}.pgm" or "{image_name}.ppm" for output and write the header: */ FILE *wrs = open_output_pnm_file("out", image_name, unpack, "", cols, rows, (chns - has_alpha), pnm_maxval); /* If has alpha, open ascii PGM file "{image_name}-alpha.pgm" for output and write the header: */ FILE *wra = NULL; if (has_alpha) { wra = open_output_pnm_file("out", image_name, unpack, "-alpha", cols, rows, 1, pnm_maxval); } /* Allocate the reader structures: */ png_structp pr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); assert(pr != NULL); png_infop pi = png_create_info_struct(pr); assert(pi != NULL); png_infop pe = png_create_info_struct(pr); assert(pe != NULL); /* Tell {libpng} to abort in case of errors: */ if (setjmp(png_jmpbuf(pr))) { assert(0); } /* Attach the file handle to the PNG reader, tell it we already read the sig: */ png_init_io(pr, rd); png_set_sig_bytes(pr, 0); /* We did not read any part of the signature. */ /* Read the PNG file header: */ png_read_info(pr, pi); dump_info(stderr, "before transforms", pr, pi); /* Get image dimensions: */ png_uint_32 file_cols = png_get_image_width(pr, pi); assert(file_cols == cols); png_uint_32 file_rows = png_get_image_height(pr, pi); assert(file_rows == rows); /* Get color type before transformations: */ uint8_t orig_color_type = png_get_color_type(pr, pi); /* Get channel count before transformations: */ png_uint_32 orig_chns = png_get_channels(pr, pi); if (orig_color_type == PNG_COLOR_TYPE_PALETTE) { assert(orig_chns == 1); } else { assert(orig_chns == chns); } /* Get number of bits per sample before all transformations: */ uint8_t orig_bits = png_get_bit_depth(pr, pi); /* Bits per sample in file. */ assert((orig_bits >= 1) && (orig_bits <= 16)); /* Bits per sample in actual color data (not in palette index) before unpacking: */ uint8_t orig_smp_bits = (orig_color_type == PNG_COLOR_TYPE_PALETTE ? 8 : orig_bits); /* -- INPUT TRANSFORMATION REQUESTS -------------------------------- */ if (unpack) { /* Request expansion of 1,2,4-bit samples to one byte: */ /* This should leave each sample in the LOWER {bits} bits of each byte. */ if ((orig_bits < 8) && (orig_color_type != PNG_COLOR_TYPE_PALETTE)) { fprintf(stderr, "requesting unpacking of %u-bit samples\n", orig_bits); png_set_packing(pr); } else { fprintf(stderr, "unpacking not needed for this PNG file type\n"); } } /* Request conversion of palette colors to RGB or RGBA: */ if (orig_color_type == PNG_COLOR_TYPE_PALETTE) { fprintf(stderr, "requesting expansion of %u-bit palette indices to true RGB\n", orig_bits); png_set_palette_to_rgb(pr); } /* If palette with a transparent color, request conversion to RGBA: */ if (png_get_valid(pr, pi, PNG_INFO_tRNS)) { fprintf(stderr, "requesting conversion of 'tRNS' chunk to alpha channel\n"); png_set_tRNS_to_alpha(pr); } /* If there is an 'sBIT' chunk, request that the original bit sizes per channel be preserved: */ png_color_8p sBITP; int has_sBIT = png_get_sBIT(pr, pi, &sBITP); if (has_sBIT) { fprintf(stderr, "requesting samples at true bit sizes:"); fprintf(stderr, " Y%d R%d G%d B%d A%d\n", sBITP->gray, sBITP->red, sBITP->green, sBITP->blue, sBITP->alpha); png_set_shift(pr, sBITP); } /* Request de-interlacing: */ (void)png_set_interlace_handling(pr); /* ------------------------------------------------------------------ */ /* Update the derived info fields: */ png_read_update_info(pr, pi); dump_info(stderr, "after transform requests", pr, pi); /* Get channel count after transformations: */ png_uint_32 final_chns = png_get_channels(pr, pi); assert(final_chns == chns); /* Get number of bits per pixel after transformations: */ uint8_t final_smp_bits = png_get_bit_depth(pr, pi); /* Bits per sample in transformed image. */ if (unpack || (orig_color_type == PNG_COLOR_TYPE_PALETTE)) { /* Ensure that {png_set_packing} worked: */ assert((final_smp_bits == 8) || (final_smp_bits == 16)); } /* Get color type after transformations, ensure that palette was expanded: */ uint8_t final_color_type = png_get_color_type(pr, pi); if (orig_color_type == PNG_COLOR_TYPE_PALETTE) { /* Final color type must be RGB or RGBA, and samples must be 8 bits: */ assert((final_color_type == PNG_COLOR_TYPE_RGB) | (final_color_type == PNG_COLOR_TYPE_RGB_ALPHA)); assert(final_smp_bits == 8); } /* Consistency of channel count and color space, and set {buse[0..chns-1]}: */ uint8_t buse[chns]; /* Used bits per channel. */ char *ctype; if (final_color_type == PNG_COLOR_TYPE_GRAY) { assert(chns == 1); buse[0] = (has_sBIT ? sBITP->gray : orig_smp_bits); ctype = "GRAY"; } else if (final_color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { assert(chns == 2); buse[0] = (has_sBIT ? sBITP->gray : orig_smp_bits); buse[1] = (has_sBIT ? sBITP->alpha : orig_smp_bits); ctype = "GRAY+ALPHA"; } else if (final_color_type == PNG_COLOR_TYPE_RGB) { assert(chns == 3); buse[0] = (has_sBIT ? sBITP->red : orig_smp_bits); buse[1] = (has_sBIT ? sBITP->green : orig_smp_bits); buse[2] = (has_sBIT ? sBITP->blue : orig_smp_bits); ctype = "RGB"; } else if (final_color_type == PNG_COLOR_TYPE_RGB_ALPHA) { assert(chns == 4); buse[0] = (has_sBIT ? sBITP->red : orig_smp_bits); buse[1] = (has_sBIT ? sBITP->green : orig_smp_bits); buse[2] = (has_sBIT ? sBITP->blue : orig_smp_bits); buse[3] = (has_sBIT ? sBITP->alpha : orig_smp_bits); ctype = "RGB+ALPHA"; } else if (final_color_type == PNG_COLOR_TYPE_PALETTE) { fprintf(stderr, "** libpng failed to expand the palette\n"); assert(0); } else { assert(0); } fprintf(stderr, "PNG file: color type = %s channels = %u", ctype, chns); fprintf(stderr, " bits per channel ="); for (uint32_t chn = 0; chn < chns; chn++) { fprintf(stderr, " %u", buse[chn]); } fprintf(stderr, " size = %u x %u\n", cols, rows); /* Validate used bit counts, compute {final_maxval[0..chns]}: */ uint32_t final_maxval[chns]; for (uint32_t chn = 0; chn < chns; chn++) { assert ((buse[chn] > 0) && (buse[chn] <= orig_smp_bits)); final_maxval[chn] = (1u << buse[chn]) - 1u; assert(final_maxval[chn] <= pnm_maxval); /* So that the PGM/PPM file is valid. */ } fprintf(stderr, "expected max sample value per channel ="); for (uint32_t chn = 0; chn < chns; chn++) { fprintf(stderr, " %u", final_maxval[chn]); } fprintf(stderr, "\n"); /* Compute how many bytes per sample are needed for {png_read_row}. */ assert(sizeof(png_byte) == 1); uint32_t samples_per_row = (uint32_t)(cols*chns); /* Samples per row in PNG file and image. */ uint32_t bytes_per_png_row; /* Bytes per row for {png_write_row} */ if (final_smp_bits >= 8) { assert((final_smp_bits == 8) || (final_smp_bits == 16)); uint32_t png_bytes_per_sample = final_smp_bits/8; /* Bytes per sample after transformations. */ bytes_per_png_row = samples_per_row * png_bytes_per_sample; } else { assert((final_smp_bits == 1) || (final_smp_bits == 2) || (final_smp_bits == 4)); /* Assume that bits are packed without filling between pixels. */ bytes_per_png_row = (samples_per_row * final_smp_bits + 7)/8; } fprintf(stderr, "expected bytes per row = %u", bytes_per_png_row); fprintf(stderr, " png_get_rowbytes() = %u\n", (uint32_t)png_get_rowbytes(pr, pi)); assert(bytes_per_png_row == (uint32_t)png_get_rowbytes(pr, pi)); /* Allocate the image data: */ png_bytep *png_dataP = malloc(rows*sizeof(png_bytep)); assert(png_dataP != NULL); for (uint32_t row = 0; row < rows; row++) { png_dataP[row] = malloc(bytes_per_png_row); assert(png_dataP[row] != NULL); } /* Read the image samples: */ png_read_image(pr, png_dataP); /* Read and write out the pixels, row by row: */ for (uint32_t row = 0; row < rows; row++) { /* Process one row of the image: */ png_bytep png_rowP = png_dataP[row]; uint32_t smpix = 0; /* Sample index in row. */ for (uint32_t col = 0; col < cols; col++) { for (uint32_t chn = 0; chn < chns; chn++) { /* Get sample {smp} from PNG row buffer: */ uint32_t smp; if (final_smp_bits == 1) { /* Samples are packed 8 to a byte: */ uint32_t ibyte = smpix/8; /* Byte index. */ uint32_t ibit = 7u - (smpix%8); /* Bit index in byte. */ png_bytep p = png_rowP + ibyte; smp = ((*p) >> ibit) & 1u; } else if (final_smp_bits == 2) { /* Samples are packed 4 to a byte: */ uint32_t ibyte = smpix/4; /* Byte index. */ uint32_t ibit = 6u - 2*(smpix%4); /* Low bit index in byte. */ png_bytep p = png_rowP + ibyte; smp = ((*p) >> ibit) & 3u; } else if (final_smp_bits == 4) { /* Samples are packed 2 to a byte: */ uint32_t ibyte = smpix/2; /* Byte index. */ uint32_t ibit = 4u - 4*(smpix%2); /* Low bit index in byte. */ png_bytep p = png_rowP + ibyte; smp = ((*p) >> ibit) & 15u; } else if (final_smp_bits == 8) { /* Samples are 1 per byte: */ png_bytep p = png_rowP + smpix; smp = (*p); } else if (final_smp_bits == 16) { /* Samples are 2 bytes: */ uint32_t ibyte = 2*smpix; /* Byte index. */ png_bytep p = png_rowP + ibyte; smp = ((*p) << 8) | (*(p+1)); } else { /* Invalid bit size: */ assert(0); } /* Consistency of samplevalue: */ if (smp > final_maxval[chn]) { fprintf(stderr, "invalid sample"); fprintf(stderr, " col = %d row = %d chn = %d", col, row, chn); fprintf(stderr, " smp = %u max = %u", smp, final_maxval[chn]); /* Try to get the upper and lower {buse[chn]} bits: */ uint8_t bsz = buse[chn]; fprintf(stderr, " smp(lo) = %u", smp & ((1u << bsz) - 1u)); fprintf(stderr, " smp(hi) = %u", smp >> (final_smp_bits - bsz)); /* Try to downscale {smp} from {2^final_smp_bits-1} to {final_maxval[chn]}: */ uint32_t full_maxval = (1u << final_smp_bits) - 1u; fprintf(stderr, " smp(sc) = %u", (smp * final_maxval[chn])/full_maxval); fprintf(stderr, "\n"); } /* Write sample to PGM/PPM file(s): */ if (has_alpha && (chn == chns-1)) { fprintf(wra, " %u", smp); } else { fprintf(wrs, " %u", smp); } smpix++; } } fprintf(wrs, "\n"); if (has_alpha) { fprintf(wra, "\n"); } } /* Read any post-image chunks just in case: */ png_read_end(pr, pe); /* Close files: */ fclose(rd); fclose(wrs); if (has_alpha) { fclose(wra); } /* Free storage: */ for (uint32_t row = 0; row < rows; row++) { free(png_dataP[row]); } free(png_dataP); png_destroy_read_struct(&pr, &pi, &pe); fprintf(stderr, "=====================================================================\n\n"); } void dump_info(FILE *wr, char *label, png_structp pr, png_infop pi) { fprintf(wr, "------------------------------------------------------------\n"); fprintf(wr, "PNG file info - %s\n", label); fprintf(wr, "image_width = %u\n", (uint32_t)png_get_image_width(pr, pi)); fprintf(wr, "image_height = %u\n", (uint32_t)png_get_image_height(pr, pi)); fprintf(wr, "channels = %u\n", (uint32_t)png_get_channels(pr, pi)); fprintf(wr, "bit_depth = %d\n", png_get_bit_depth (pr, pi)); /* fprintf(wr, "usr_bit_depth = %d\n", (uint32_t)pr->usr_bit_depth; */ fprintf(wr, "color_type = %d\n", png_get_color_type(pr, pi)); fprintf(wr, "interlace_type = %d\n", png_get_interlace_type(pr, pi)); fprintf(wr, "compression_type = %d\n", png_get_compression_type(pr, pi)); fprintf(wr, "filter_type = %d\n", png_get_filter_type(pr, pi)); png_color_8p sBITP; if (png_get_sBIT(pr, pi, &sBITP)) { fprintf ( wr, "sBIT = %16p = ( %u %u %u %u %u )\n", (void *)sBITP, sBITP->gray, sBITP->red, sBITP->green, sBITP->blue, sBITP->alpha ); } else { fprintf(wr, "sBIT not specified\n"); } int has_tRNS = png_get_valid(pr, pi, PNG_INFO_tRNS); fprintf(wr, "has tRNS = %d\n", has_tRNS); double gamma; if (png_get_gAMA(pr, pi, &gamma)) { fprintf(wr, "gAMA = %25.16e\n", gamma); } else { fprintf(wr, "gAMA not specified\n"); } fprintf(wr, "channels = %d\n", png_get_channels(pr, pi)); fprintf(wr, "rowbytes = %u\n", (uint32_t)png_get_rowbytes(pr, pi)); png_bytep sg = (png_bytep)png_get_signature(pr, pi); if (sg != NULL) { fprintf ( wr, "signature = %16p = %02x %02x %02x %02x %02x %02x %02x %02x\n", (void *)sg, sg[0], sg[1], sg[2], sg[3], sg[4], sg[5], sg[6], sg[7] ); } else { fprintf(wr, "png_get_signature failed\n"); } fprintf(wr, "------------------------------------------------------------\n"); } FILE *open_input_png_file(char *dir, char *image_name) { char *fname = jsprintf("%s/%s.png", dir, image_name); fprintf(stderr, "opening input file %s\n", fname); FILE *rd = fopen(fname, "r"); free(fname); return rd; } FILE *open_output_pnm_file ( char *dir, char *image_name, int unpack, char *tag, uint32_t cols, uint32_t rows, uint32_t chns, uint32_t maxval ) { assert((chns == 1) || (chns == 3)); int gray = (chns == 1); char *ext = (gray ? "pgm" : "ppm"); char *magic = (gray ? "P2" : "P3"); char *fname = jsprintf("%s/%s-%d%s.%s", dir, image_name, unpack, tag, ext); fprintf(stderr, "opening output file %s\n", fname); FILE *wr = fopen(fname, "w"); free(fname); fprintf(wr, "%s\n", magic); fprintf(wr, "%u %u\n", cols, rows); fprintf(wr, "%u\n", maxval); fflush(wr); return wr; }