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