From 8c025bcf3fdfb68fb8b1e5fab4b7deb0f94453f6 Mon Sep 17 00:00:00 2001 From: John Bowler Date: Sun, 20 Sep 2015 20:40:52 -0700 Subject: [PATCH] Simplified API sRGB gamma handling correction If a gamma encoded file that has a gamma not matching that of sRGB is passed to the simplified API the previous code simply interpreted it as a power law encoding. However old Mac files had a power law correction of 1.45 built in to the encoding, even though the display devices were consistent with sRGB. Assuming a power law encoding results in substantial differences in the interpretation of low 8-bit values; below 10. For example an Apple '5' which is equivalent to an sRGB '17' ends up as the value '8'. This patch provides some measure of correction for this by making the gamma correction done within the simplified API assume that any encoded data is encoded relative to an sRGB-like transfer function; the data is corrected back to the PNG-nominal 2.2 value then decoded to linear (if required) using the sRGB transfer function. This reduces the errors reported by pngstest for such files (colormapped ones) but still leaves the issue with files where the standard libpng code does the gamma decoding. To cope with the latter cases the patch also includes a new pngstest-errors which allows the result, however this is still a work-in-progress; a better solution is possible. Signed-off-by: John Bowler --- contrib/libtests/pngstest-errors.h | 18 ++-- pngread.c | 132 +++++++++++++++++++++++------ pngrtran.c | 3 - 3 files changed, 117 insertions(+), 36 deletions(-) diff --git a/contrib/libtests/pngstest-errors.h b/contrib/libtests/pngstest-errors.h index b818fd497..070d88bff 100644 --- a/contrib/libtests/pngstest-errors.h +++ b/contrib/libtests/pngstest-errors.h @@ -32,7 +32,7 @@ static png_uint_16 gpc_error[16/*in*/][16/*out*/][4/*a*/] = { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } }, { /* input: sRGB-rgb+alpha */ - { 0, 4, 13, 0 }, { 0, 15, 13, 0 }, { 0, 19, 1, 0 }, { 0, 0, 0, 0 }, + { 0, 14, 13, 0 }, { 0, 15, 13, 0 }, { 0, 19, 1, 0 }, { 0, 0, 0, 0 }, { 0, 832, 764, 0 }, { 0, 832, 764, 0 }, { 0, 897, 788, 0 }, { 0, 897, 788, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } @@ -52,7 +52,7 @@ static png_uint_16 gpc_error[16/*in*/][16/*out*/][4/*a*/] = { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } }, { /* input: linear-rgb+alpha */ - { 0, 126, 143, 0 }, { 0, 9, 7, 0 }, { 0, 74, 9, 0 }, { 0, 17, 9, 0 }, + { 0, 126, 143, 0 }, { 0, 11, 7, 0 }, { 0, 74, 9, 0 }, { 0, 17, 9, 0 }, { 0, 4, 4, 0 }, { 0, 5, 4, 0 }, { 0, 0, 0, 0 }, { 0, 1, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } @@ -73,7 +73,7 @@ static png_uint_16 gpc_error[16/*in*/][16/*out*/][4/*a*/] = { 0, 0, 460, 0 }, { 0, 0, 460, 0 }, { 0, 0, 263, 0 }, { 0, 0, 263, 0 } }, { /* input: color-mapped-sRGB-rgb+alpha */ { 0, 6, 8, 0 }, { 0, 7, 8, 0 }, { 0, 75, 8, 0 }, { 0, 9, 8, 0 }, - { 0, 585, 427, 0 }, { 0, 585, 427, 0 }, { 0, 717, 462, 0 }, { 0, 717, 462, 0 }, + { 0, 585, 427, 0 }, { 0, 585, 427, 0 }, { 0, 717, 584, 0 }, { 0, 717, 584, 0 }, { 0, 1, 1, 0 }, { 0, 1, 1, 0 }, { 0, 1, 0, 0 }, { 0, 0, 0, 0 }, { 0, 13323, 460, 0 }, { 0, 427, 460, 0 }, { 0, 16480, 263, 0 }, { 0, 243, 263, 0 } }, { /* input: color-mapped-linear-gray */ @@ -115,7 +115,7 @@ static png_uint_16 gpc_error_via_linear[16][4/*out*/][4] = }, { /* input: linear-rgb */ { 0, 0, 1, 0 }, { 0, 0, 1, 0 }, { 0, 0, 1, 0 }, { 0, 0, 1, 0 } }, { /* input: linear-rgb+alpha */ - { 0, 1, 1, 0 }, { 0, 8, 1, 0 }, { 0, 1, 1, 0 }, { 0, 1, 1, 0 } + { 0, 1, 1, 0 }, { 0, 9, 1, 0 }, { 0, 1, 1, 0 }, { 0, 1, 1, 0 } }, { /* input: color-mapped-sRGB-gray */ { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } }, { /* input: color-mapped-sRGB-gray+alpha */ @@ -123,7 +123,7 @@ static png_uint_16 gpc_error_via_linear[16][4/*out*/][4] = }, { /* input: color-mapped-sRGB-rgb */ { 0, 0, 13, 0 }, { 0, 0, 13, 0 }, { 0, 0, 14, 0 }, { 0, 0, 14, 0 } }, { /* input: color-mapped-sRGB-rgb+alpha */ - { 0, 4, 8, 0 }, { 0, 9, 8, 0 }, { 0, 9, 3, 0 }, { 0, 32, 3, 0 } + { 0, 4, 8, 0 }, { 0, 9, 8, 0 }, { 0, 9, 9, 0 }, { 0, 32, 9, 0 } }, { /* input: color-mapped-linear-gray */ { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } }, { /* input: color-mapped-linear-gray+alpha */ @@ -138,7 +138,7 @@ static png_uint_16 gpc_error_to_colormap[8/*i*/][8/*o*/][4] = { { /* input: sRGB-gray */ { 0, 0, 9, 0 }, { 0, 0, 9, 0 }, { 0, 0, 9, 0 }, { 0, 0, 9, 0 }, - { 0, 0, 560, 0 }, { 0, 0, 560, 0 }, { 0, 0, 560, 0 }, { 0, 0, 560, 0 } + { 0, 0, 742, 0 }, { 0, 0, 742, 0 }, { 0, 0, 742, 0 }, { 0, 0, 742, 0 } }, { /* input: sRGB-gray+alpha */ { 0, 19, 2, 0 }, { 0, 255, 2, 25 }, { 0, 88, 2, 0 }, { 0, 255, 2, 25 }, { 0, 1012, 745, 0 }, { 0, 16026, 745, 6425 }, { 0, 1012, 745, 0 }, { 0, 16026, 745, 6425 } @@ -147,19 +147,19 @@ static png_uint_16 gpc_error_to_colormap[8/*i*/][8/*o*/][4] = { 0, 0, 962, 0 }, { 0, 0, 962, 0 }, { 0, 0, 13677, 0 }, { 0, 0, 13677, 0 } }, { /* input: sRGB-rgb+alpha */ { 0, 63, 77, 0 }, { 0, 255, 19, 25 }, { 0, 220, 25, 0 }, { 0, 255, 25, 67 }, - { 0, 17534, 18491, 0 }, { 0, 15614, 2824, 6425 }, { 0, 14019, 13677, 0 }, { 0, 48573, 13677, 17219 } + { 0, 17534, 18491, 0 }, { 0, 15998, 2824, 6425 }, { 0, 14103, 13677, 0 }, { 0, 50115, 13677, 17219 } }, { /* input: linear-gray */ { 0, 0, 73, 0 }, { 0, 0, 73, 0 }, { 0, 0, 73, 0 }, { 0, 0, 73, 0 }, { 0, 0, 18817, 0 }, { 0, 0, 18817, 0 }, { 0, 0, 18817, 0 }, { 0, 0, 18817, 0 } }, { /* input: linear-gray+alpha */ { 0, 74, 74, 0 }, { 0, 255, 74, 25 }, { 0, 97, 74, 0 }, { 0, 255, 74, 25 }, - { 0, 18919, 18907, 0 }, { 0, 24549, 18907, 6552 }, { 0, 18919, 18907, 0 }, { 0, 24549, 18907, 6552 } + { 0, 18919, 18907, 0 }, { 0, 24549, 18907, 6553 }, { 0, 18919, 18907, 0 }, { 0, 24549, 18907, 6553 } }, { /* input: linear-rgb */ { 0, 0, 73, 0 }, { 0, 0, 73, 0 }, { 0, 0, 98, 0 }, { 0, 0, 98, 0 }, { 0, 0, 18664, 0 }, { 0, 0, 18664, 0 }, { 0, 0, 24998, 0 }, { 0, 0, 24998, 0 } }, { /* input: linear-rgb+alpha */ { 0, 181, 196, 0 }, { 0, 255, 61, 25 }, { 206, 187, 98, 0 }, { 0, 255, 98, 67 }, - { 0, 18141, 18137, 0 }, { 0, 17494, 17504, 6553 }, { 0, 24979, 24992, 0 }, { 0, 46509, 24992, 17347 } + { 0, 18141, 18137, 0 }, { 0, 17494, 17504, 6553 }, { 0, 24979, 24992, 0 }, { 0, 49172, 24992, 17347 } } }; /* END MACHINE GENERATED */ diff --git a/pngread.c b/pngread.c index 5295bbcaa..5e9ca7066 100644 --- a/pngread.c +++ b/pngread.c @@ -756,7 +756,7 @@ png_read_destroy(png_structrp png_ptr) if (png_ptr->zstream.state != NULL) { int ret = inflateEnd(&png_ptr->zstream); - + if (ret != Z_OK) { png_zstream_error(png_ptr, ret); @@ -1081,7 +1081,7 @@ typedef struct png_voidp first_row; ptrdiff_t row_bytes; /* step between rows */ int file_encoding; /* E_ values above */ - png_fixed_point gamma_to_linear; /* For P_FILE, reciprocal of gamma */ + png_fixed_point file_to_sRGB; /* Cached correction factor */ int colormap_processing; /* PNG_CMAP_ values above */ } png_image_read_control; @@ -1445,20 +1445,15 @@ png_image_skip_unused_chunks(png_structrp png_ptr) static void set_file_encoding(png_image_read_control *display) { + /* First test for an encoding close to linear: */ if (png_need_gamma_correction(display->image->opaque->png_ptr, 0/*PNG gamma*/, 0/*not sRGB*/)) { png_fixed_point g = display->image->opaque->png_ptr->colorspace.gamma; + /* Now look for one close to sRGB: */ if (png_gamma_not_sRGB(g)) - { display->file_encoding = P_FILE; - /* Record the reciprocal of 'g', the colorspace gamma. If this - * overflows just store FP_1. - */ - if (!png_muldiv(&display->gamma_to_linear, PNG_FP_1, PNG_FP_1, g)) - display->gamma_to_linear = PNG_FP_1; - } else display->file_encoding = P_sRGB; @@ -1468,6 +1463,96 @@ set_file_encoding(png_image_read_control *display) display->file_encoding = P_LINEAR8; } +/* For colormap entries we end up doing the gamma correction here and the + * following routines are provided to separate out the code. In all cases the + * input value is in the range 0..255 and is encoded P_FILE with the gamma value + * stored in the png_struct colorspace. + */ +static void +init_correct(png_const_structrp png_ptr, png_fixed_point *correct) +{ + /* Record the convertion necessary to get from the encoding values to + * sRGB. If this overflows just store FP_1. + * + * NOTE: this code used to store, and use, a convertion factor to + * linear then use the sRGB encoding tables to get back to sRGB, but + * this smashes the low values; the ones which fall in the linear part + * of the sRGB transfer function. + * + * The new version of this code assumes an encoding which is neither + * linear nor sRGB is a power law transform of the sRGB curve, not + * linear values. This is somewhat at odds with a precise reading of + * the PNG spec, but given that we are trying to produce sRGB values + * here it is most likely to be correct. + */ + affirm(png_ptr->colorspace.gamma > 0); + + if (!png_muldiv(correct, PNG_GAMMA_sRGB_INVERSE, PNG_FP_1, + png_ptr->colorspace.gamma)) + *correct = PNG_FP_1; +} + +static png_uint_32 +convert_to_sRGB(png_image_read_control *display, png_uint_32 value) +{ + /* Converts an 8-bit value from P_FILE to P_sRGB */ + png_const_structrp png_ptr = display->image->opaque->png_ptr; + + affirm(value <= 255U); + + if (display->file_to_sRGB == 0) + init_correct(png_ptr, &display->file_to_sRGB); + + /* Now simply apply this correction factor and scale back to 8 bits. */ + if (display->file_to_sRGB != PNG_FP_1) + value = PNG_DIV257( + png_gamma_16bit_correct(png_ptr, value*257U, display->file_to_sRGB)); + + return value; +} + +static png_uint_32 +convert_to_linear(png_image_read_control *display, png_uint_32 value) +{ + /* Converts an 8-bit value from P_FILE to 16-bit P_LINEAR */ + png_const_structrp png_ptr = display->image->opaque->png_ptr; + + affirm(value <= 255U); + + if (display->file_to_sRGB == 0) + init_correct(png_ptr, &display->file_to_sRGB); + + /* Use this correct to get a 16-bit sRGB value: */ + value *= 257U; + + if (display->file_to_sRGB != PNG_FP_1) + value = png_gamma_16bit_correct(png_ptr, value, display->file_to_sRGB); + + /* Now convert this back to linear, using the correct transfer function. */ + if (value <= 2650U /* 65535 * 0.04045 */) + { + /* We want to divide a 12-bit number by 12.92, do this by scaling to 32 + * bits then dividing by 2^24, with rounding: + */ + value = (value * 1298546U + 649273U) >> 24; + } + + else + { + /* Calculate for v in the range 0.04045..1.0 calculate: + * + * ((v + 0.055)/1.055)^2.4 + * + * the gamma correction function needs a 16-bit value: + */ + value *= 62119U; + value += 223904831U+32768U; /* cannot overflow; test with 65535 */ + value = png_gamma_16bit_correct(png_ptr, value >> 16, 240000); + } + + return value; +} + static unsigned int decode_gamma(png_image_read_control *display, png_uint_32 value, int encoding) { @@ -1483,8 +1568,7 @@ decode_gamma(png_image_read_control *display, png_uint_32 value, int encoding) switch (encoding) { case P_FILE: - value = png_gamma_16bit_correct(display->image->opaque->png_ptr, - value*257, display->gamma_to_linear); + value = convert_to_linear(display, value); break; case P_sRGB: @@ -1567,31 +1651,26 @@ png_create_colormap_entry(png_image_read_control *display, if (display->file_encoding == P_NOTSET) set_file_encoding(display); - /* Note that the cached value may be P_FILE too, but if it is then the - * gamma_to_linear member has been set. - */ + /* Note that the cached value may be P_FILE too. */ encoding = display->file_encoding; } if (encoding == P_FILE) { - png_fixed_point g = display->gamma_to_linear; - - red = png_gamma_16bit_correct(png_ptr, red*257, g); - green = png_gamma_16bit_correct(png_ptr, green*257, g); - blue = png_gamma_16bit_correct(png_ptr, blue*257, g); - if (convert_to_Y != 0 || output_encoding == P_LINEAR) { + red = convert_to_linear(display, red); + green = convert_to_linear(display, green); + blue = convert_to_linear(display, blue); alpha *= 257; encoding = P_LINEAR; } else { - red = PNG_sRGB_FROM_LINEAR(png_ptr, red * 255); - green = PNG_sRGB_FROM_LINEAR(png_ptr, green * 255); - blue = PNG_sRGB_FROM_LINEAR(png_ptr, blue * 255); + red = convert_to_sRGB(display, red); + green = convert_to_sRGB(display, green); + blue = convert_to_sRGB(display, blue); encoding = P_sRGB; } } @@ -1960,7 +2039,12 @@ png_image_read_colormap(png_voidp argument) else png_ptr->colorspace.gamma = PNG_GAMMA_sRGB_INVERSE; - png_ptr->colorspace.flags |= PNG_COLORSPACE_HAVE_GAMMA; + /* Make sure libpng doesn't ignore the setting: */ + if (png_ptr->colorspace.flags & PNG_COLORSPACE_INVALID) + png_ptr->colorspace.flags = PNG_COLORSPACE_HAVE_GAMMA; + + else + png_ptr->colorspace.flags |= PNG_COLORSPACE_HAVE_GAMMA; } /* Decide what to do based on the PNG color type of the input data. The diff --git a/pngrtran.c b/pngrtran.c index 98ef61569..df317d2fc 100644 --- a/pngrtran.c +++ b/pngrtran.c @@ -4926,10 +4926,7 @@ png_do_background_alpha_GA(png_transformp *transform, png_transform_controlp tc) case 65535U: /* opaque */ if (copy) - { memcpy(dp, sp, 4U); - UNTESTED - } break; }