Skip to content

Commit 3663310

Browse files
lgritzbrechtvl
andauthored
fix(png): alpha premultiplication adjustment and attribute (AcademySoftwareFoundation#4585)
See discussion AcademySoftwareFoundation#4054 and PR AcademySoftwareFoundation#4315 In PR 4315, we fixed PNG read/write to do the required premultiplication in linear space (that is, linearize, multiply by alpha, then go back to the sRGB or gamma space). Which we really believe is "correct." Except that maybe there are a ton of incorrectly made PNG files out there (maybe most of them?) where partial alpha pixels had their premultiplication occur on the sRGB or gamma values directly. In this patch, we partly revert, allowing both potential behaviors, controlled by an attribute "png:linear_premult", which instructs the PNG driver to do any premultiplication in linear space if it's set to nonzero. It can be set globally (via `OIIO::attribute()`), as well as set/overridden on any individual file by setting the attribute in the configuration hints for an ImageInput or in the spec for an ImageOutput. As presented in this patch, we're setting the default to 0, meaning that by default we are reverting back to the old behavior of doing the premultiply of partial alpha pixels on the direct values intead of in a linear space. Applications or sites that are confident that any PNG files they encounter are "correct" can set the attribute to do the multiplication linearly. I'm not 100% confident about the default, and so am very happy to entertain arguments for keeping the default set to do the multiplication in linear space. I had to change an internal spin mutex to a recursive mutex in order to address a latent misdesign that was made symptomatic by this change. Basically, asking for an attribute inside an ImageInput::init could make a deadlock because certain attribute queries (that catalogue the list of plugins) might instantiate those plugins, thus causing their init functions to run, leading to recursive dependence on the mutex that guards attribute queries. --------- Signed-off-by: Larry Gritz <[email protected]> Co-authored-by: Brecht Van Lommel <[email protected]>
1 parent e964a6a commit 3663310

File tree

10 files changed

+171
-29
lines changed

10 files changed

+171
-29
lines changed

src/doc/builtinplugins.rst

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1754,6 +1754,12 @@ attributes are supported:
17541754
- ptr
17551755
- Pointer to a ``Filesystem::IOProxy`` that will handle the I/O, for
17561756
example by reading from memory rather than the file system.
1757+
* - ``png:linear_premult``
1758+
- int
1759+
- If nonzero, will convert or gamma-encoded values to linear color
1760+
space for any premultiplication-by-alpha step done by the PNG reader.
1761+
If zero (the default), any needed premultiplication will happen directly
1762+
to the encoded values.
17571763

17581764
**Configuration settings for PNG output**
17591765

@@ -1797,13 +1803,31 @@ control aspects of the writing itself:
17971803
to have larger PNG files on disk, you may want to use that value for
17981804
this attribute.
17991805

1806+
* - ``png:linear_premult``
1807+
- int
1808+
- If nonzero, will convert sRGB or gamma-encoded values to linear color
1809+
space for any unpremultiplication-by-alpha step done by the PNG writer.
1810+
If zero (the default), any needed unpremultiplication will happen
1811+
directly to the encoded sRGB or gamma-corrected values.
1812+
18001813
**Custom I/O Overrides**
18011814

18021815
PNG input and output both support the "custom I/O" feature via the special
18031816
``"oiio:ioproxy"`` attributes (see Sections :ref:`sec-imageoutput-ioproxy`
18041817
and :ref:`sec-imageinput-ioproxy`) as well as the `set_ioproxy()` methods.
18051818

1806-
1819+
**Note on premultiplication**
1820+
1821+
PNG files encoded as sRGB or gamma-corrected values that also have alpha
1822+
should (in theory) have any premultiplication performed in a linear space
1823+
(that is, the color should first be linearized, then premultiplied by alpha,
1824+
then converted back to the nonlinear form). However, many existing PNG files
1825+
are apparently encoded with the assumption that any premultiplication will be
1826+
performed directly on the encoded values, so that is the default behavior for
1827+
OpenImageIO's PNG reader and writer will. If you want to force the reader or
1828+
writer to linearize the values for premultiplication, you can set either the
1829+
reader/writer configuration hint or the global OIIO attribute
1830+
``png:linear_premult`` to 1.
18071831

18081832
**Limitations**
18091833

src/include/OpenImageIO/imageio.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2908,6 +2908,14 @@ OIIO_API std::string geterror(bool clear = true);
29082908
/// and only set ImageDescription if the parsing fails. Otherwise, always
29092909
/// set ImageDescription to the first comment block. Default is 1.
29102910
///
2911+
/// - `int png:linear_premult` (0)
2912+
///
2913+
/// If nonzero, will convert perform any necessary premultiplication by
2914+
/// alpha steps needed of the PNG reader/writer in a linear color space.
2915+
/// If zero (the default), any needed premultiplication will happen
2916+
/// directly on the values, even if they are sRGB or gamma-corrected.
2917+
/// For more information, please see OpenImageIO's documentation on the
2918+
/// built-in PNG format support.
29112919
///
29122920
/// - `int limits:channels` (1024)
29132921
///

src/include/imageio_pvt.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ extern OIIO_UTIL_API int oiio_print_uncaught_errors;
4343
extern int oiio_log_times;
4444
extern int openexr_core;
4545
extern int jpeg_com_attributes;
46+
extern int png_linear_premult;
4647
extern int limit_channels;
4748
extern int limit_imagesize_MB;
4849
extern int imagebuf_print_uncaught_errors;

src/libOpenImageIO/imageio.cpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ atomic_int oiio_try_all_readers(1);
4949
// Should we use "Exr core C library"?
5050
int openexr_core(OIIO_OPENEXR_CORE_DEFAULT);
5151
int jpeg_com_attributes(1);
52+
int png_linear_premult(0);
5253
int tiff_half(0);
5354
int tiff_multithread(1);
5455
int dds_bc5normal(0);
@@ -72,7 +73,7 @@ using namespace pvt;
7273

7374
namespace {
7475
// Hidden global OIIO data.
75-
static spin_mutex attrib_mutex;
76+
static std::recursive_mutex attrib_mutex;
7677
static const int maxthreads = 512; // reasonable maximum for sanity check
7778

7879
class TimingLog {
@@ -347,7 +348,7 @@ attribute(string_view name, TypeDesc type, const void* val)
347348
}
348349

349350
// Things below here need to buarded by the attrib_mutex
350-
spin_lock lock(attrib_mutex);
351+
std::lock_guard lock(attrib_mutex);
351352
if (name == "read_chunk" && type == TypeInt) {
352353
oiio_read_chunk = *(const int*)val;
353354
return true;
@@ -372,6 +373,10 @@ attribute(string_view name, TypeDesc type, const void* val)
372373
jpeg_com_attributes = *(const int*)val;
373374
return true;
374375
}
376+
if (name == "png:linear_premult" && type == TypeInt) {
377+
png_linear_premult = *(const int*)val;
378+
return true;
379+
}
375380
if (name == "tiff:half" && type == TypeInt) {
376381
tiff_half = *(const int*)val;
377382
return true;
@@ -460,7 +465,7 @@ getattribute(string_view name, TypeDesc type, void* val)
460465
}
461466

462467
// Things below here need to buarded by the attrib_mutex
463-
spin_lock lock(attrib_mutex);
468+
std::lock_guard lock(attrib_mutex);
464469
if (name == "read_chunk" && type == TypeInt) {
465470
*(int*)val = oiio_read_chunk;
466471
return true;
@@ -551,6 +556,10 @@ getattribute(string_view name, TypeDesc type, void* val)
551556
*(int*)val = jpeg_com_attributes;
552557
return true;
553558
}
559+
if (name == "png:linear_premult" && type == TypeInt) {
560+
*(int*)val = png_linear_premult;
561+
return true;
562+
}
554563
if (name == "tiff:half" && type == TypeInt) {
555564
*(int*)val = tiff_half;
556565
return true;

src/png.imageio/pnginput.cpp

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class PNGInput final : public ImageInput {
4545
Imath::Color3f m_bg; ///< Background color
4646
int m_next_scanline;
4747
bool m_keep_unassociated_alpha; ///< Do not convert unassociated alpha
48+
bool m_linear_premult; ///< Do premult for sRGB images in linear
4849
bool m_srgb = false; ///< It's an sRGB image (not gamma)
4950
bool m_err = false;
5051
float m_gamma = 1.0f;
@@ -60,9 +61,10 @@ class PNGInput final : public ImageInput {
6061
m_buf.clear();
6162
m_next_scanline = 0;
6263
m_keep_unassociated_alpha = false;
63-
m_srgb = false;
64-
m_err = false;
65-
m_gamma = 1.0;
64+
m_linear_premult = OIIO::get_int_attribute("png:linear_premult");
65+
m_srgb = false;
66+
m_err = false;
67+
m_gamma = 1.0;
6668
m_config.reset();
6769
ioproxy_clear();
6870
}
@@ -88,8 +90,8 @@ class PNGInput final : public ImageInput {
8890
}
8991

9092
template<class T>
91-
static void associateAlpha(T* data, int size, int channels,
92-
int alpha_channel, bool srgb, float gamma);
93+
void associateAlpha(T* data, int size, int channels, int alpha_channel,
94+
bool srgb, float gamma);
9395
};
9496

9597

@@ -189,6 +191,9 @@ PNGInput::open(const std::string& name, ImageSpec& newspec,
189191
// Check 'config' for any special requests
190192
if (config.get_int_attribute("oiio:UnassociatedAlpha", 0) == 1)
191193
m_keep_unassociated_alpha = true;
194+
m_linear_premult = config.get_int_attribute("png:linear_premult",
195+
OIIO::get_int_attribute(
196+
"png:linear_premult"));
192197
ioproxy_retrieve_from_config(config);
193198
m_config.reset(new ImageSpec(config)); // save config spec
194199
return open(name, newspec);
@@ -229,7 +234,8 @@ PNGInput::associateAlpha(T* data, int size, int channels, int alpha_channel,
229234
{
230235
// We need to transform to linear space, associate the alpha, and then
231236
// transform back.
232-
if (srgb) {
237+
if (srgb && m_linear_premult) {
238+
// sRGB with request to do premult in linear space
233239
for (int x = 0; x < size; ++x, data += channels) {
234240
DataArrayProxy<T, float> val(data);
235241
float alpha = val[alpha_channel];
@@ -242,25 +248,29 @@ PNGInput::associateAlpha(T* data, int size, int channels, int alpha_channel,
242248
}
243249
}
244250
}
245-
} else if (gamma == 1.0f) {
251+
} else if (gamma != 1.0f && m_linear_premult) {
252+
// Gamma correction with request to do premult in linear space
253+
float inv_gamma = 1.0f / gamma;
246254
for (int x = 0; x < size; ++x, data += channels) {
247255
DataArrayProxy<T, float> val(data);
248256
float alpha = val[alpha_channel];
249257
if (alpha != 1.0f) {
250258
for (int c = 0; c < channels; c++)
251259
if (c != alpha_channel)
252-
data[c] = data[c] * alpha;
260+
val[c] = powf((powf(val[c], gamma)) * alpha, inv_gamma);
253261
}
254262
}
255-
} else { // With gamma correction
256-
float inv_gamma = 1.0f / gamma;
263+
} else {
264+
// Do the premult directly on the values. This is correct for the
265+
// "gamma=1" case, and is also commonly what is needed for many sRGB
266+
// images (even though it's technically wrong in that case).
257267
for (int x = 0; x < size; ++x, data += channels) {
258268
DataArrayProxy<T, float> val(data);
259269
float alpha = val[alpha_channel];
260270
if (alpha != 1.0f) {
261271
for (int c = 0; c < channels; c++)
262272
if (c != alpha_channel)
263-
val[c] = powf((powf(val[c], gamma)) * alpha, inv_gamma);
273+
val[c] = val[c] * alpha;
264274
}
265275
}
266276
}

src/png.imageio/pngoutput.cpp

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class PNGOutput final : public ImageOutput {
4747
int m_color_type; ///< PNG color model type
4848
bool m_convert_alpha; ///< Do we deassociate alpha?
4949
bool m_need_swap; ///< Do we need to swap bytes?
50+
bool m_linear_premult; ///< Do premult for sRGB images in linear
5051
bool m_srgb = false; ///< It's an sRGB image (not gamma)
5152
float m_gamma = 1.0f; ///< Gamma to use for alpha conversion
5253
std::vector<unsigned char> m_scratch;
@@ -57,13 +58,14 @@ class PNGOutput final : public ImageOutput {
5758
// Initialize private members to pre-opened state
5859
void init(void)
5960
{
60-
m_png = NULL;
61-
m_info = NULL;
62-
m_convert_alpha = true;
63-
m_need_swap = false;
64-
m_srgb = false;
65-
m_err = false;
66-
m_gamma = 1.0;
61+
m_png = NULL;
62+
m_info = NULL;
63+
m_convert_alpha = true;
64+
m_need_swap = false;
65+
m_linear_premult = false;
66+
m_srgb = false;
67+
m_err = false;
68+
m_gamma = 1.0;
6769
m_pngtext.clear();
6870
ioproxy_clear();
6971
}
@@ -187,6 +189,10 @@ PNGOutput::open(const std::string& name, const ImageSpec& userspec,
187189

188190
m_need_swap = (m_spec.format == TypeDesc::UINT16 && littleendian());
189191

192+
m_linear_premult = m_spec.get_int_attribute("png:linear_premult",
193+
OIIO::get_int_attribute(
194+
"png:linear_premult"));
195+
190196
png_set_filter(m_png, 0,
191197
spec().get_int_attribute("png:filter", PNG_NO_FILTERS));
192198
// https://www.w3.org/TR/PNG-Encoders.html#E.Filter-selection
@@ -277,7 +283,8 @@ void
277283
PNGOutput::deassociateAlpha(T* data, size_t npixels, int channels,
278284
int alpha_channel, bool srgb, float gamma)
279285
{
280-
if (srgb) {
286+
if (srgb && m_linear_premult) {
287+
// sRGB with request to do unpremult in linear space
281288
for (size_t x = 0; x < npixels; ++x, data += channels) {
282289
DataArrayProxy<T, float> val(data);
283290
float alpha = val[alpha_channel];
@@ -290,27 +297,31 @@ PNGOutput::deassociateAlpha(T* data, size_t npixels, int channels,
290297
}
291298
}
292299
}
293-
} else if (gamma == 1) {
300+
} else if (gamma != 1.0f && m_linear_premult) {
301+
// Gamma correction with request to do unpremult in linear space
294302
for (size_t x = 0; x < npixels; ++x, data += channels) {
295303
DataArrayProxy<T, float> val(data);
296304
float alpha = val[alpha_channel];
297305
if (alpha != 0.0f && alpha != 1.0f) {
306+
// See associateAlpha() for an explanation.
307+
float alpha_deassociate = pow(1.0f / val[alpha_channel], gamma);
298308
for (int c = 0; c < channels; c++) {
299309
if (c != alpha_channel)
300-
val[c] = data[c] / alpha;
310+
val[c] = val[c] * alpha_deassociate;
301311
}
302312
}
303313
}
304314
} else {
315+
// Do the unpremult directly on the values. This is correct for the
316+
// "gamma=1" case, and is also commonly what is needed for many sRGB
317+
// images (even though it's technically wrong in that case).
305318
for (size_t x = 0; x < npixels; ++x, data += channels) {
306319
DataArrayProxy<T, float> val(data);
307320
float alpha = val[alpha_channel];
308321
if (alpha != 0.0f && alpha != 1.0f) {
309-
// See associateAlpha() for an explanation.
310-
float alpha_deassociate = pow(1.0f / val[alpha_channel], gamma);
311322
for (int c = 0; c < channels; c++) {
312323
if (c != alpha_channel)
313-
val[c] = val[c] * alpha_deassociate;
324+
val[c] = data[c] / alpha;
314325
}
315326
}
316327
}

testsuite/png/ref/out-libpng15.txt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ exif.png : 64 x 64, 3 channel, uint8 png
2727
SHA-1: 7CB41FEA50720B48BE0C145E1473982B23E9AB77
2828
channel list: R, G, B
2929
oiio:ColorSpace: "sRGB"
30+
alphagamma:
3031
1 x 1, 4 channel, float png
3132
channel list: R, G, B, A
3233
ResolutionUnit: "inch"
@@ -46,6 +47,43 @@ exif.png : 64 x 64, 3 channel, uint8 png
4647
Constant: Yes
4748
Constant Color: 186.00 186.00 186.00 127.00 (of 255)
4849
Monochrome: No
50+
gimp_gradient:
51+
256 x 256, 4 channel, float png
52+
channel list: R, G, B, A
53+
Comment: "Created with GIMP"
54+
DateTime: "2025:01:05 04:44:59"
55+
ICCProfile: 0, 0, 2, 160, 108, 99, 109, 115, 4, 64, 0, 0, 109, 110, 116, 114, ... [672 x uint8]
56+
ResolutionUnit: "inch"
57+
XResolution: 300
58+
YResolution: 300
59+
ICCProfile:attributes: "Reflective, Glossy, Positive, Color"
60+
ICCProfile:cmm_type: 1818455411
61+
ICCProfile:color_space: "RGB"
62+
ICCProfile:copyright: "Public Domain"
63+
ICCProfile:creation_date: "2025:01:05 04:34:16"
64+
ICCProfile:creator_signature: "6c636d73"
65+
ICCProfile:device_class: "Display device profile"
66+
ICCProfile:device_manufacturer_description: "GIMP"
67+
ICCProfile:device_model_description: "sRGB"
68+
ICCProfile:flags: "Not Embedded, Independent"
69+
ICCProfile:manufacturer: "0"
70+
ICCProfile:model: "0"
71+
ICCProfile:platform_signature: "Apple Computer, Inc."
72+
ICCProfile:profile_connection_space: "XYZ"
73+
ICCProfile:profile_description: "GIMP built-in sRGB"
74+
ICCProfile:profile_size: 672
75+
ICCProfile:profile_version: "4.4.0"
76+
ICCProfile:rendering_intent: "Perceptual"
77+
oiio:ColorSpace: "sRGB"
78+
Stats Min: 0 0 0 0 (of 255)
79+
Stats Max: 255 255 0 255 (of 255)
80+
Stats Avg: 142.37 105.72 0.00 154.72 (of 255)
81+
Stats StdDev: 79.19 98.91 0.00 87.39 (of 255)
82+
Stats NanCount: 0 0 0 0
83+
Stats InfCount: 0 0 0 0
84+
Stats FiniteCount: 65536 65536 65536 65536
85+
Constant: No
86+
Monochrome: No
4987
smallalpha.png : 1 x 1, 4 channel, uint8 png
5088
Pixel (0, 0): 240 108 119 1 (0.94117653 0.42352945 0.4666667 0.003921569)
5189
Comparing "test16.png" and "ref/test16.png"

0 commit comments

Comments
 (0)