Skip to content

Commit 0b255fd

Browse files
committed
fix(png): Correctly read PNGs with partial alpha (AcademySoftwareFoundation#4315)
TL;DR: when turning unassociated alpha into associated for gamma-corrected PNG images, we got the exponent wrong for the linearization step, and when doing the same for sRGB images, we didn't linearize at all. WARNING: will change appearance of PNG files with partial alpha. Details: Ugh, two separate problems related to how we associate alpha (i.e. premultiply the colors) for PNG pixels with partial alpha. The correct thing to do is linearize the unassociated pixel value first, then associate, then go back to the nonlinear space. First problem: PNGs have three possible transfer functions: gamma correction (with a particular gamma), no gamma correction / linear, and explicitly sRGB. Guess what? We were neglecting the case of pngs tagged as sRGB and not doing the linearize/delinearize round trip for those images. But if that's not enough, also for the gamma case, we were, ugh, swapping the gamma and 1/gamma, resulting in those partial alpha pixels ending up a whole lot darker than they should have been. None of this affected most ordinary PNGs with no alpha channel, or where alpha was 1.0. It only affected "edge" or "partially transparent" pixels with 0 < alpha < 1. But it was definitely wrong before, for which I apologize and hope you'll understand why those pixels are going to change now (hopefully, always always for the better). While I was at it, I also made the color space handling a little more robust -- instead of just a straight string compare for color space names, use the ColorConfig to check `equivalent`, which should make us a lot more robust against aliases and whatnot. Fixes AcademySoftwareFoundation#4314 Closes AcademySoftwareFoundation#4054 --------- Signed-off-by: Larry Gritz <[email protected]>
1 parent 976f9f8 commit 0b255fd

File tree

8 files changed

+139
-50
lines changed

8 files changed

+139
-50
lines changed

src/ico.imageio/icooutput.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,12 +319,13 @@ ICOOutput::open(const std::string& name, const ImageSpec& userspec,
319319
// unused still, should do conversion to unassociated
320320
bool convert_alpha;
321321
float gamma;
322+
bool srgb;
322323

323324
png_init_io(m_png, m_file);
324325
png_set_compression_level(m_png, Z_BEST_COMPRESSION);
325326

326327
PNG_pvt::write_info(m_png, m_info, m_color_type, m_spec, m_pngtext,
327-
convert_alpha, gamma);
328+
convert_alpha, srgb, gamma);
328329
} else {
329330
// write DIB header
330331
ico_bitmapinfo bmi;

src/png.imageio/png_pvt.h

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <zlib.h>
99

1010
#include <OpenImageIO/Imath.h>
11+
#include <OpenImageIO/color.h>
1112
#include <OpenImageIO/dassert.h>
1213
#include <OpenImageIO/filesystem.h>
1314
#include <OpenImageIO/fmath.h>
@@ -574,7 +575,8 @@ put_parameter(png_structp& sp, png_infop& ip, const std::string& _name,
574575
///
575576
inline const std::string
576577
write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec,
577-
std::vector<png_text>& text, bool& convert_alpha, float& gamma)
578+
std::vector<png_text>& text, bool& convert_alpha, bool& srgb,
579+
float& gamma)
578580
{
579581
// Force either 16 or 8 bit integers
580582
if (spec.format == TypeDesc::UINT8 || spec.format == TypeDesc::INT8)
@@ -598,11 +600,14 @@ write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec,
598600

599601
gamma = spec.get_float_attribute("oiio:Gamma", 1.0);
600602

603+
const ColorConfig& colorconfig = ColorConfig::default_colorconfig();
601604
string_view colorspace = spec.get_string_attribute("oiio:ColorSpace");
602-
if (Strutil::iequals(colorspace, "Linear")) {
605+
if (colorconfig.equivalent(colorspace, "scene_linear")
606+
|| colorconfig.equivalent(colorspace, "linear")) {
603607
if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp)
604608
return "Could not set PNG gAMA chunk";
605609
png_set_gAMA(sp, ip, 1.0);
610+
srgb = false;
606611
} else if (Strutil::istarts_with(colorspace, "Gamma")) {
607612
Strutil::parse_word(colorspace);
608613
float g = Strutil::from_string<float>(colorspace);
@@ -611,10 +616,12 @@ write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec,
611616
if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp)
612617
return "Could not set PNG gAMA chunk";
613618
png_set_gAMA(sp, ip, 1.0f / gamma);
614-
} else if (Strutil::iequals(colorspace, "sRGB")) {
619+
srgb = false;
620+
} else if (colorconfig.equivalent(colorspace, "sRGB")) {
615621
if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp)
616622
return "Could not set PNG gAMA and cHRM chunk";
617623
png_set_sRGB_gAMA_and_cHRM(sp, ip, PNG_sRGB_INTENT_ABSOLUTE);
624+
srgb = true;
618625
}
619626

620627
// Write ICC profile, if we have anything

src/png.imageio/pnginput.cpp

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,11 @@ class PNGInput final : public ImageInput {
4444
int m_subimage; ///< What subimage are we looking at?
4545
Imath::Color3f m_bg; ///< Background color
4646
int m_next_scanline;
47-
bool m_keep_unassociated_alpha; ///< Do not convert unassociated alpha
47+
bool m_keep_unassociated_alpha; ///< Do not convert unassociated alpha
48+
bool m_srgb = false; ///< It's an sRGB image (not gamma)
49+
bool m_err = false;
50+
float m_gamma = 1.0f;
4851
std::unique_ptr<ImageSpec> m_config; // Saved copy of configuration spec
49-
bool m_err = false;
5052

5153
/// Reset everything to initial state
5254
///
@@ -58,7 +60,9 @@ class PNGInput final : public ImageInput {
5860
m_buf.clear();
5961
m_next_scanline = 0;
6062
m_keep_unassociated_alpha = false;
63+
m_srgb = false;
6164
m_err = false;
65+
m_gamma = 1.0;
6266
m_config.reset();
6367
ioproxy_clear();
6468
}
@@ -82,6 +86,10 @@ class PNGInput final : public ImageInput {
8286
png_chunk_error(png_ptr, pnginput->geterror(false).c_str());
8387
}
8488
}
89+
90+
template<class T>
91+
static void associateAlpha(T* data, int size, int channels,
92+
int alpha_channel, bool srgb, float gamma);
8593
};
8694

8795

@@ -159,6 +167,12 @@ PNGInput::open(const std::string& name, ImageSpec& newspec)
159167
return false;
160168
}
161169

170+
m_gamma = m_spec.get_float_attribute("oiio:Gamma", 1.0f);
171+
string_view colorspace = m_spec.get_string_attribute("oiio:ColorSpace",
172+
"sRGB");
173+
const ColorConfig& colorconfig(ColorConfig::default_colorconfig());
174+
m_srgb = colorconfig.equivalent(colorspace, "sRGB");
175+
162176
newspec = spec();
163177
m_next_scanline = 0;
164178

@@ -208,34 +222,45 @@ PNGInput::close()
208222

209223

210224
template<class T>
211-
static void
212-
png_associateAlpha(T* data, int size, int channels, int alpha_channel,
213-
float gamma)
225+
void
226+
PNGInput::associateAlpha(T* data, int size, int channels, int alpha_channel,
227+
bool srgb, float gamma)
214228
{
215-
T max = std::numeric_limits<T>::max();
216-
if (gamma == 1) {
217-
for (int x = 0; x < size; ++x, data += channels)
218-
for (int c = 0; c < channels; c++)
219-
if (c != alpha_channel) {
220-
unsigned int f = data[c];
221-
data[c] = (f * data[alpha_channel]) / max;
229+
// We need to transform to linear space, associate the alpha, and then
230+
// transform back.
231+
if (srgb) {
232+
for (int x = 0; x < size; ++x, data += channels) {
233+
DataArrayProxy<T, float> val(data);
234+
float alpha = val[alpha_channel];
235+
if (alpha != 0.0f && alpha != 1.0f) {
236+
for (int c = 0; c < channels; c++) {
237+
if (c != alpha_channel) {
238+
float f = sRGB_to_linear(val[c]);
239+
val[c] = linear_to_sRGB(f * alpha);
240+
}
222241
}
223-
} else { //With gamma correction
224-
float inv_max = 1.0 / max;
242+
}
243+
}
244+
} else if (gamma == 1.0f) {
225245
for (int x = 0; x < size; ++x, data += channels) {
226-
float alpha_associate = pow(data[alpha_channel] * inv_max, gamma);
227-
// We need to transform to linear space, associate the alpha, and
228-
// then transform back. That is, if D = data[c], we want
229-
//
230-
// D' = max * ( (D/max)^(1/gamma) * (alpha/max) ) ^ gamma
231-
//
232-
// This happens to simplify to something which looks like
233-
// multiplying by a nonlinear alpha:
234-
//
235-
// D' = D * (alpha/max)^gamma
236-
for (int c = 0; c < channels; c++)
237-
if (c != alpha_channel)
238-
data[c] = static_cast<T>(data[c] * alpha_associate);
246+
DataArrayProxy<T, float> val(data);
247+
float alpha = val[alpha_channel];
248+
if (alpha != 0.0f && alpha != 1.0f) {
249+
for (int c = 0; c < channels; c++)
250+
if (c != alpha_channel)
251+
data[c] = data[c] * alpha;
252+
}
253+
}
254+
} else { // With gamma correction
255+
float inv_gamma = 1.0f / gamma;
256+
for (int x = 0; x < size; ++x, data += channels) {
257+
DataArrayProxy<T, float> val(data);
258+
float alpha = val[alpha_channel];
259+
if (alpha != 0.0f && alpha != 1.0f) {
260+
for (int c = 0; c < channels; c++)
261+
if (c != alpha_channel)
262+
val[c] = powf((powf(val[c], gamma)) * alpha, inv_gamma);
263+
}
239264
}
240265
}
241266
}
@@ -295,13 +320,13 @@ PNGInput::read_native_scanline(int subimage, int miplevel, int y, int /*z*/,
295320
// PNG specifically dictates unassociated (un-"premultiplied") alpha.
296321
// Convert to associated unless we were requested not to do so.
297322
if (m_spec.alpha_channel != -1 && !m_keep_unassociated_alpha) {
298-
float gamma = m_spec.get_float_attribute("oiio:Gamma", 1.0f);
299323
if (m_spec.format == TypeDesc::UINT16)
300-
png_associateAlpha((unsigned short*)data, m_spec.width,
301-
m_spec.nchannels, m_spec.alpha_channel, gamma);
324+
associateAlpha((unsigned short*)data, m_spec.width,
325+
m_spec.nchannels, m_spec.alpha_channel, m_srgb,
326+
m_gamma);
302327
else
303-
png_associateAlpha((unsigned char*)data, m_spec.width,
304-
m_spec.nchannels, m_spec.alpha_channel, gamma);
328+
associateAlpha((unsigned char*)data, m_spec.width, m_spec.nchannels,
329+
m_spec.alpha_channel, m_srgb, m_gamma);
305330
}
306331

307332
return true;

src/png.imageio/pngoutput.cpp

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@ class PNGOutput final : public ImageOutput {
4444
png_structp m_png; ///< PNG read structure pointer
4545
png_infop m_info; ///< PNG image info structure pointer
4646
unsigned int m_dither;
47-
int m_color_type; ///< PNG color model type
48-
bool m_convert_alpha; ///< Do we deassociate alpha?
49-
bool m_need_swap; ///< Do we need to swap bytes?
50-
float m_gamma; ///< Gamma to use for alpha conversion
47+
int m_color_type; ///< PNG color model type
48+
bool m_convert_alpha; ///< Do we deassociate alpha?
49+
bool m_need_swap; ///< Do we need to swap bytes?
50+
bool m_srgb = false; ///< It's an sRGB image (not gamma)
51+
float m_gamma = 1.0f; ///< Gamma to use for alpha conversion
5152
std::vector<unsigned char> m_scratch;
5253
std::vector<png_text> m_pngtext;
5354
std::vector<unsigned char> m_tilebuffer;
@@ -60,10 +61,11 @@ class PNGOutput final : public ImageOutput {
6061
m_info = NULL;
6162
m_convert_alpha = true;
6263
m_need_swap = false;
64+
m_srgb = false;
65+
m_err = false;
6366
m_gamma = 1.0;
6467
m_pngtext.clear();
6568
ioproxy_clear();
66-
m_err = false;
6769
}
6870

6971
// Add a parameter to the output
@@ -90,7 +92,7 @@ class PNGOutput final : public ImageOutput {
9092

9193
template<class T>
9294
void deassociateAlpha(T* data, size_t npixels, int channels,
93-
int alpha_channel, float gamma);
95+
int alpha_channel, bool srgb, float gamma);
9496
};
9597

9698

@@ -204,7 +206,7 @@ PNGOutput::open(const std::string& name, const ImageSpec& userspec,
204206

205207
#if defined(PNG_SKIP_sRGB_CHECK_PROFILE) && defined(PNG_SET_OPTION_SUPPORTED)
206208
// libpng by default checks ICC profiles and are very strict, treating
207-
// it as a serious error if it doesn't match th profile it thinks is
209+
// it as a serious error if it doesn't match the profile it thinks is
208210
// right for sRGB. This call disables that behavior, which tends to have
209211
// many false positives. Some references to discussion about this:
210212
// https://github.com/kornelski/pngquant/issues/190
@@ -214,7 +216,7 @@ PNGOutput::open(const std::string& name, const ImageSpec& userspec,
214216
#endif
215217

216218
s = PNG_pvt::write_info(m_png, m_info, m_color_type, m_spec, m_pngtext,
217-
m_convert_alpha, m_gamma);
219+
m_convert_alpha, m_srgb, m_gamma);
218220

219221
if (s.length()) {
220222
close();
@@ -273,9 +275,22 @@ PNGOutput::close()
273275
template<class T>
274276
void
275277
PNGOutput::deassociateAlpha(T* data, size_t npixels, int channels,
276-
int alpha_channel, float gamma)
278+
int alpha_channel, bool srgb, float gamma)
277279
{
278-
if (gamma == 1) {
280+
if (srgb) {
281+
for (size_t x = 0; x < npixels; ++x, data += channels) {
282+
DataArrayProxy<T, float> val(data);
283+
float alpha = val[alpha_channel];
284+
if (alpha != 0.0f && alpha != 1.0f) {
285+
for (int c = 0; c < channels; c++) {
286+
if (c != alpha_channel) {
287+
float f = sRGB_to_linear(val[c]);
288+
val[c] = linear_to_sRGB(f / alpha);
289+
}
290+
}
291+
}
292+
}
293+
} else if (gamma == 1) {
279294
for (size_t x = 0; x < npixels; ++x, data += channels) {
280295
DataArrayProxy<T, float> val(data);
281296
float alpha = val[alpha_channel];
@@ -331,7 +346,7 @@ PNGOutput::write_scanline(int y, int z, TypeDesc format, const void* data,
331346
TypeFloat, AutoStride, AutoStride, AutoStride);
332347
// Deassociate alpha
333348
deassociateAlpha(floatvals, size_t(m_spec.width), m_spec.nchannels,
334-
m_spec.alpha_channel, m_gamma);
349+
m_spec.alpha_channel, m_srgb, m_gamma);
335350
data = floatvals;
336351
format = TypeFloat;
337352
xstride = size_t(m_spec.nchannels) * sizeof(float);
@@ -394,7 +409,7 @@ PNGOutput::write_scanlines(int ybegin, int yend, int z, TypeDesc format,
394409
AutoStride);
395410
// Deassociate alpha
396411
deassociateAlpha(floatvals, npixels, m_spec.nchannels,
397-
m_spec.alpha_channel, m_gamma);
412+
m_spec.alpha_channel, m_srgb, m_gamma);
398413
data = floatvals;
399414
format = TypeFloat;
400415
xstride = size_t(m_spec.nchannels) * sizeof(float);

testsuite/png/ref/out-libpng15.txt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Comparing "../oiio-images/oiio-logo-no-alpha.png" and "oiio-logo-no-alpha.png"
1212
PASS
1313
Reading ../oiio-images/oiio-logo-with-alpha.png
1414
../oiio-images/oiio-logo-with-alpha.png : 135 x 135, 4 channel, uint8 png
15-
SHA-1: 8AED04DCCE8F83B068C537DC0982A42EFBE431B6
15+
SHA-1: 9F3C517AC714A93C0FE93F8B4B40C68338504DC8
1616
channel list: R, G, B, A
1717
Comment: "Created with GIMP"
1818
DateTime: "2009:03:26 18:44:26"
@@ -27,6 +27,25 @@ exif.png : 64 x 64, 3 channel, uint8 png
2727
SHA-1: 7CB41FEA50720B48BE0C145E1473982B23E9AB77
2828
channel list: R, G, B
2929
oiio:ColorSpace: "sRGB"
30+
1 x 1, 4 channel, float png
31+
channel list: R, G, B, A
32+
ResolutionUnit: "inch"
33+
Software: "OpenImageIO 2.4.1.1dev : oiiotool -no-autopremult SLEEP_MM.png -cut 1x1+227+1211 -o kaka.png"
34+
XResolution: 299.999
35+
YResolution: 299.999
36+
Exif:ImageHistory: "oiiotool -no-autopremult SLEEP_MM.png -cut 1x1+227+1211 -o kaka.png"
37+
oiio:ColorSpace: "Gamma2.2"
38+
oiio:Gamma: 2.2
39+
Stats Min: 186 186 186 127 (of 255)
40+
Stats Max: 186 186 186 127 (of 255)
41+
Stats Avg: 186.00 186.00 186.00 127.00 (of 255)
42+
Stats StdDev: 0.00 0.00 0.00 0.00 (of 255)
43+
Stats NanCount: 0 0 0 0
44+
Stats InfCount: 0 0 0 0
45+
Stats FiniteCount: 1 1 1 1
46+
Constant: Yes
47+
Constant Color: 186.00 186.00 186.00 127.00 (of 255)
48+
Monochrome: No
3049
smallalpha.png : 1 x 1, 4 channel, uint8 png
3150
Pixel (0, 0): 240 108 119 1 (0.94117653 0.42352945 0.4666667 0.003921569)
3251
Comparing "test16.png" and "ref/test16.png"

testsuite/png/ref/out.txt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Comparing "../oiio-images/oiio-logo-no-alpha.png" and "oiio-logo-no-alpha.png"
1212
PASS
1313
Reading ../oiio-images/oiio-logo-with-alpha.png
1414
../oiio-images/oiio-logo-with-alpha.png : 135 x 135, 4 channel, uint8 png
15-
SHA-1: 8AED04DCCE8F83B068C537DC0982A42EFBE431B6
15+
SHA-1: 9F3C517AC714A93C0FE93F8B4B40C68338504DC8
1616
channel list: R, G, B, A
1717
Comment: "Created with GIMP"
1818
DateTime: "2009:03:26 18:44:26"
@@ -31,6 +31,25 @@ exif.png : 64 x 64, 3 channel, uint8 png
3131
Exif:FocalLength: 45.7 (45.7 mm)
3232
Exif:WhiteBalance: 0 (auto)
3333
oiio:ColorSpace: "sRGB"
34+
1 x 1, 4 channel, float png
35+
channel list: R, G, B, A
36+
ResolutionUnit: "inch"
37+
Software: "OpenImageIO 2.4.1.1dev : oiiotool -no-autopremult SLEEP_MM.png -cut 1x1+227+1211 -o kaka.png"
38+
XResolution: 299.999
39+
YResolution: 299.999
40+
Exif:ImageHistory: "oiiotool -no-autopremult SLEEP_MM.png -cut 1x1+227+1211 -o kaka.png"
41+
oiio:ColorSpace: "Gamma2.2"
42+
oiio:Gamma: 2.2
43+
Stats Min: 186 186 186 127 (of 255)
44+
Stats Max: 186 186 186 127 (of 255)
45+
Stats Avg: 186.00 186.00 186.00 127.00 (of 255)
46+
Stats StdDev: 0.00 0.00 0.00 0.00 (of 255)
47+
Stats NanCount: 0 0 0 0
48+
Stats InfCount: 0 0 0 0
49+
Stats FiniteCount: 1 1 1 1
50+
Constant: Yes
51+
Constant Color: 186.00 186.00 186.00 127.00 (of 255)
52+
Monochrome: No
3453
smallalpha.png : 1 x 1, 4 channel, uint8 png
3554
Pixel (0, 0): 240 108 119 1 (0.94117653 0.42352945 0.4666667 0.003921569)
3655
Comparing "test16.png" and "ref/test16.png"

testsuite/png/run.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
# regression test for 16 bit output bug
2222
command += oiiotool ("--pattern fill:topleft=1,0,0,1:topright=0,1,0,1:bottomleft=0,0,1,1:bottomright=1,1,1,1 16x16 4 -d uint16 -o test16.png")
2323

24+
# regression test for wrong gamma correction for partial alpha
25+
command += oiiotool ("src/alphagamma.png --printinfo:stats=1")
26+
2427
# Test high quality alpha deassociation using alpha value close to zero.
2528
# This example is inspired by Yafes on the Slack.
2629
command += oiiotool ("--pattern fill:color=0.00235,0.00106,0.00117,0.0025 1x1 4 -d uint8 -o smallalpha.png")

testsuite/png/src/alphagamma.png

374 Bytes
Loading

0 commit comments

Comments
 (0)