1 // Copyright 2008-present Contributors to the OpenImageIO project.
2 // SPDX-License-Identifier: BSD-3-Clause
3 // https://github.com/OpenImageIO/oiio/blob/master/LICENSE.md
4 
5 #include <cmath>
6 #include <cstdio>
7 #include <cstdlib>
8 #include <ctime>
9 #include <iostream>
10 
11 #include "png_pvt.h"
12 
13 
14 OIIO_PLUGIN_NAMESPACE_BEGIN
15 
16 
17 class PNGOutput final : public ImageOutput {
18 public:
19     PNGOutput();
20     virtual ~PNGOutput();
format_name(void) const21     virtual const char* format_name(void) const override { return "png"; }
supports(string_view feature) const22     virtual int supports(string_view feature) const override
23     {
24         return (feature == "alpha" || feature == "ioproxy");
25     }
26     virtual bool open(const std::string& name, const ImageSpec& spec,
27                       OpenMode mode = Create) override;
28     virtual bool close() override;
29     virtual bool write_scanline(int y, int z, TypeDesc format, const void* data,
30                                 stride_t xstride) override;
31     virtual bool write_tile(int x, int y, int z, TypeDesc format,
32                             const void* data, stride_t xstride,
33                             stride_t ystride, stride_t zstride) override;
set_ioproxy(Filesystem::IOProxy * ioproxy)34     virtual bool set_ioproxy(Filesystem::IOProxy* ioproxy) override
35     {
36         m_io = ioproxy;
37         return true;
38     }
39 
40 private:
41     std::string m_filename;  ///< Stash the filename
42     png_structp m_png;       ///< PNG read structure pointer
43     png_infop m_info;        ///< PNG image info structure pointer
44     unsigned int m_dither;
45     int m_color_type;      ///< PNG color model type
46     bool m_convert_alpha;  ///< Do we deassociate alpha?
47     float m_gamma;         ///< Gamma to use for alpha conversion
48     std::vector<unsigned char> m_scratch;
49     std::vector<png_text> m_pngtext;
50     std::vector<unsigned char> m_tilebuffer;
51     std::unique_ptr<Filesystem::IOProxy> m_local_io;
52     Filesystem::IOProxy* m_io = nullptr;
53     bool m_err                = false;
54 
55     // Initialize private members to pre-opened state
init(void)56     void init(void)
57     {
58         m_png           = NULL;
59         m_info          = NULL;
60         m_convert_alpha = true;
61         m_gamma         = 1.0;
62         m_pngtext.clear();
63         m_local_io.reset();
64         m_io  = nullptr;
65         m_err = false;
66     }
67 
68     // Add a parameter to the output
69     bool put_parameter(const std::string& name, TypeDesc type,
70                        const void* data);
71 
72     // Callback for PNG that writes via an IOProxy instead of writing
73     // to a file.
PngWriteCallback(png_structp png_ptr,png_bytep data,png_size_t length)74     static void PngWriteCallback(png_structp png_ptr, png_bytep data,
75                                  png_size_t length)
76     {
77         PNGOutput* pngoutput = (PNGOutput*)png_get_io_ptr(png_ptr);
78         OIIO_DASSERT(pngoutput);
79         size_t bytes = pngoutput->m_io->write(data, length);
80         if (bytes != length) {
81             pngoutput->errorf("Write error");
82             pngoutput->m_err = true;
83         }
84     }
85 
PngFlushCallback(png_structp png_ptr)86     static void PngFlushCallback(png_structp png_ptr)
87     {
88         PNGOutput* pngoutput = (PNGOutput*)png_get_io_ptr(png_ptr);
89         OIIO_DASSERT(pngoutput);
90         pngoutput->m_io->flush();
91     }
92 };
93 
94 
95 
96 // Obligatory material to make this a recognizeable imageio plugin:
97 OIIO_PLUGIN_EXPORTS_BEGIN
98 
99 OIIO_EXPORT ImageOutput*
png_output_imageio_create()100 png_output_imageio_create()
101 {
102     return new PNGOutput;
103 }
104 
105 // OIIO_EXPORT int png_imageio_version = OIIO_PLUGIN_VERSION;   // it's in pnginput.cpp
106 
107 OIIO_EXPORT const char* png_output_extensions[] = { "png", nullptr };
108 
109 OIIO_PLUGIN_EXPORTS_END
110 
111 
112 
PNGOutput()113 PNGOutput::PNGOutput() { init(); }
114 
115 
116 
~PNGOutput()117 PNGOutput::~PNGOutput()
118 {
119     // Close, if not already done.
120     close();
121 }
122 
123 
124 
125 bool
open(const std::string & name,const ImageSpec & userspec,OpenMode mode)126 PNGOutput::open(const std::string& name, const ImageSpec& userspec,
127                 OpenMode mode)
128 {
129     if (mode != Create) {
130         errorf("%s does not support subimages or MIP levels", format_name());
131         return false;
132     }
133 
134     m_spec = userspec;  // Stash the spec
135 
136     // If not uint8 or uint16, default to uint8
137     if (m_spec.format != TypeDesc::UINT8 && m_spec.format != TypeDesc::UINT16)
138         m_spec.set_format(TypeDesc::UINT8);
139 
140     // See if we were requested to write to a memory buffer, and if so,
141     // extract the pointer.
142     auto ioparam = m_spec.find_attribute("oiio:ioproxy", TypeDesc::PTR);
143     if (ioparam)
144         m_io = ioparam->get<Filesystem::IOProxy*>();
145     if (!m_io) {
146         // If no proxy was supplied, create a file writer
147         m_io = new Filesystem::IOFile(name, Filesystem::IOProxy::Mode::Write);
148         m_local_io.reset(m_io);
149     }
150     if (!m_io || m_io->mode() != Filesystem::IOProxy::Mode::Write) {
151         errorf("Could not open \"%s\"", name);
152         return false;
153     }
154 
155     std::string s = PNG_pvt::create_write_struct(m_png, m_info, m_color_type,
156                                                  m_spec, this);
157     if (s.length()) {
158         close();
159         errorf("%s", s);
160         return false;
161     }
162 
163     png_set_write_fn(m_png, this, PngWriteCallback, PngFlushCallback);
164 
165     png_set_compression_level(
166         m_png, std::max(std::min(m_spec.get_int_attribute(
167                                      "png:compressionLevel",
168                                      6 /* medium speed vs size tradeoff */),
169                                  Z_BEST_COMPRESSION),
170                         Z_NO_COMPRESSION));
171     std::string compression = m_spec.get_string_attribute("compression");
172     if (compression.empty()) {
173         png_set_compression_strategy(m_png, Z_DEFAULT_STRATEGY);
174     } else if (Strutil::iequals(compression, "default")) {
175         png_set_compression_strategy(m_png, Z_DEFAULT_STRATEGY);
176     } else if (Strutil::iequals(compression, "filtered")) {
177         png_set_compression_strategy(m_png, Z_FILTERED);
178     } else if (Strutil::iequals(compression, "huffman")) {
179         png_set_compression_strategy(m_png, Z_HUFFMAN_ONLY);
180     } else if (Strutil::iequals(compression, "rle")) {
181         png_set_compression_strategy(m_png, Z_RLE);
182     } else if (Strutil::iequals(compression, "fixed")) {
183         png_set_compression_strategy(m_png, Z_FIXED);
184     } else {
185         png_set_compression_strategy(m_png, Z_DEFAULT_STRATEGY);
186     }
187 
188     png_set_filter(m_png, 0,
189                    spec().get_int_attribute("png:filter", PNG_NO_FILTERS));
190     // https://www.w3.org/TR/PNG-Encoders.html#E.Filter-selection
191     // https://www.w3.org/TR/PNG-Rationale.html#R.Filtering
192     // The official advice is to PNG_NO_FILTER for palette or < 8 bpp
193     // images, but we and one of the others may be fine for >= 8 bit
194     // greyscale or color images (they aren't very prescriptive, noting that
195     // different flters may be better for different images.
196     // We have found the tradeoff complex, in fact as seen in
197     // https://github.com/OpenImageIO/oiio/issues/2645
198     // where we showed that across several images, 8 (PNG_FILTER_NONE --
199     // don't ask me how that's different from PNG_NO_FILTERS) had the
200     // fastest performance, but also made the largest files. I had trouble
201     // finding a filter choice that for "ordinary" images consistently
202     // performed better than the default on both time and resulting file
203     // size. So for now, we are keeping the default 0 (PNG_NO_FILTERS).
204 
205 #if defined(PNG_SKIP_sRGB_CHECK_PROFILE) && defined(PNG_SET_OPTION_SUPPORTED)
206     // 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
208     // right for sRGB. This call disables that behavior, which tends to have
209     // many false positives. Some references to discussion about this:
210     //    https://github.com/kornelski/pngquant/issues/190
211     //    https://sourceforge.net/p/png-mng/mailman/message/32003609/
212     //    https://bugzilla.gnome.org/show_bug.cgi?id=721135
213     png_set_option(m_png, PNG_SKIP_sRGB_CHECK_PROFILE, PNG_OPTION_ON);
214 #endif
215 
216     PNG_pvt::write_info(m_png, m_info, m_color_type, m_spec, m_pngtext,
217                         m_convert_alpha, m_gamma);
218 
219     m_dither = (m_spec.format == TypeDesc::UINT8)
220                    ? m_spec.get_int_attribute("oiio:dither", 0)
221                    : 0;
222 
223     m_convert_alpha = m_spec.alpha_channel != -1
224                       && !m_spec.get_int_attribute("oiio:UnassociatedAlpha", 0);
225 
226     // If user asked for tiles -- which this format doesn't support, emulate
227     // it by buffering the whole image.
228     if (m_spec.tile_width && m_spec.tile_height)
229         m_tilebuffer.resize(m_spec.image_bytes());
230 
231     return true;
232 }
233 
234 
235 
236 bool
close()237 PNGOutput::close()
238 {
239     if (!m_io) {  // already closed
240         init();
241         return true;
242     }
243 
244     bool ok = true;
245     if (m_spec.tile_width) {
246         // Handle tile emulation -- output the buffered pixels
247         OIIO_ASSERT(m_tilebuffer.size());
248         ok &= write_scanlines(m_spec.y, m_spec.y + m_spec.height, 0,
249                               m_spec.format, &m_tilebuffer[0]);
250         std::vector<unsigned char>().swap(m_tilebuffer);
251     }
252 
253     if (m_png) {
254         PNG_pvt::finish_image(m_png, m_info);
255     }
256 
257     init();  // re-initialize
258     return ok;
259 }
260 
261 
262 
263 template<class T>
264 static void
deassociateAlpha(T * data,int size,int channels,int alpha_channel,float gamma)265 deassociateAlpha(T* data, int size, int channels, int alpha_channel,
266                  float gamma)
267 {
268     unsigned int max = std::numeric_limits<T>::max();
269     if (gamma == 1) {
270         for (int x = 0; x < size; ++x, data += channels)
271             if (data[alpha_channel])
272                 for (int c = 0; c < channels; c++)
273                     if (c != alpha_channel) {
274                         unsigned int f = data[c];
275                         f              = (f * max) / data[alpha_channel];
276                         data[c]        = (T)std::min(max, f);
277                     }
278     } else {
279         for (int x = 0; x < size; ++x, data += channels)
280             if (data[alpha_channel]) {
281                 // See associateAlpha() for an explanation.
282                 float alpha_deassociate = pow((float)max / data[alpha_channel],
283                                               gamma);
284                 for (int c = 0; c < channels; c++)
285                     if (c != alpha_channel)
286                         data[c] = static_cast<T>(std::min(
287                             max, (unsigned int)(data[c] * alpha_deassociate)));
288             }
289     }
290 }
291 
292 
293 
294 bool
write_scanline(int y,int z,TypeDesc format,const void * data,stride_t xstride)295 PNGOutput::write_scanline(int y, int z, TypeDesc format, const void* data,
296                           stride_t xstride)
297 {
298     y -= m_spec.y;
299     m_spec.auto_stride(xstride, format, spec().nchannels);
300     const void* origdata = data;
301     data = to_native_scanline(format, data, xstride, m_scratch, m_dither, y, z);
302     if (data == origdata) {
303         m_scratch.assign((unsigned char*)data,
304                          (unsigned char*)data + m_spec.scanline_bytes());
305         data = &m_scratch[0];
306     }
307 
308     // PNG specifically dictates unassociated (un-"premultiplied") alpha
309     if (m_convert_alpha) {
310         if (m_spec.format == TypeDesc::UINT16)
311             deassociateAlpha((unsigned short*)data, m_spec.width,
312                              m_spec.nchannels, m_spec.alpha_channel, m_gamma);
313         else
314             deassociateAlpha((unsigned char*)data, m_spec.width,
315                              m_spec.nchannels, m_spec.alpha_channel, m_gamma);
316     }
317 
318     // PNG is always big endian
319     if (littleendian() && m_spec.format == TypeDesc::UINT16)
320         swap_endian((unsigned short*)data, m_spec.width * m_spec.nchannels);
321 
322     if (!PNG_pvt::write_row(m_png, (png_byte*)data)) {
323         errorf("PNG library error");
324         return false;
325     }
326 
327     return true;
328 }
329 
330 
331 
332 bool
write_tile(int x,int y,int z,TypeDesc format,const void * data,stride_t xstride,stride_t ystride,stride_t zstride)333 PNGOutput::write_tile(int x, int y, int z, TypeDesc format, const void* data,
334                       stride_t xstride, stride_t ystride, stride_t zstride)
335 {
336     // Emulate tiles by buffering the whole image
337     return copy_tile_to_image_buffer(x, y, z, format, data, xstride, ystride,
338                                      zstride, &m_tilebuffer[0]);
339 }
340 
341 
342 OIIO_PLUGIN_NAMESPACE_END
343