1// Copyright (C) 2013 Samuel Gaist <samuel.gaist@edeltech.ch>
2// Copyright (C) 2016 The Qt Company Ltd.
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4
5#include "private/qpnghandler_p.h"
6
7#ifndef QT_NO_IMAGEFORMAT_PNG
8#include <qcoreapplication.h>
9#include <qdebug.h>
10#include <qiodevice.h>
11#include <qimage.h>
12#include <qloggingcategory.h>
13#include <qvariant.h>
14
15#include <private/qimage_p.h> // for qt_getImageText
16
17#include <qcolorspace.h>
18#include <private/qcolorspace_p.h>
19
20#include <png.h>
21#include <pngconf.h>
22
23#if PNG_LIBPNG_VER >= 10400 && PNG_LIBPNG_VER <= 10502 \
24 && defined(PNG_PEDANTIC_WARNINGS_SUPPORTED)
25/*
26 Versions 1.4.0 to 1.5.2 of libpng declare png_longjmp_ptr to
27 have a noreturn attribute if PNG_PEDANTIC_WARNINGS_SUPPORTED
28 is enabled, but most declarations of longjmp in the wild do
29 not add this attribute. This causes problems when the png_jmpbuf
30 macro expands to calling png_set_longjmp_fn with a mismatched
31 longjmp, as compilers such as Clang will treat this as an error.
32
33 To work around this we override the png_jmpbuf macro to cast
34 longjmp to a png_longjmp_ptr.
35*/
36# undef png_jmpbuf
37# ifdef PNG_SETJMP_SUPPORTED
38# define png_jmpbuf(png_ptr) \
39 (*png_set_longjmp_fn((png_ptr), (png_longjmp_ptr)longjmp, sizeof(jmp_buf)))
40# else
41# define png_jmpbuf(png_ptr) \
42 (LIBPNG_WAS_COMPILED_WITH__PNG_NO_SETJMP)
43# endif
44#endif
45
46QT_BEGIN_NAMESPACE
47
48using namespace Qt::StringLiterals;
49
50Q_DECLARE_LOGGING_CATEGORY(lcImageIo)
51
52// avoid going through QImage::scanLine() which calls detach
53#define FAST_SCAN_LINE(data, bpl, y) (data + (y) * bpl)
54
55/*
56 All PNG files load to the minimal QImage equivalent.
57
58 All QImage formats output to reasonably efficient PNG equivalents.
59*/
60
61class QPngHandlerPrivate
62{
63public:
64 enum State {
65 Ready,
66 ReadHeader,
67 ReadingEnd,
68 Error
69 };
70 // Defines the order of how the various ways of setting colorspace overrides each other:
71 enum ColorSpaceState {
72 Undefined = 0,
73 GammaChrm = 1, // gAMA+cHRM chunks
74 Srgb = 2, // sRGB chunk
75 Icc = 3 // iCCP chunk
76 };
77
78 QPngHandlerPrivate(QPngHandler *qq)
79 : gamma(0.0), fileGamma(0.0), quality(50), compression(50), colorSpaceState(Undefined),
80 png_ptr(nullptr), info_ptr(nullptr), end_info(nullptr), row_pointers(nullptr), state(Ready), q(qq)
81 { }
82
83 float gamma;
84 float fileGamma;
85 int quality; // quality is used for backward compatibility, maps to compression
86 int compression;
87 QString description;
88 QStringList readTexts;
89 QColorSpace colorSpace;
90 ColorSpaceState colorSpaceState;
91
92 png_struct *png_ptr;
93 png_info *info_ptr;
94 png_info *end_info;
95 png_byte **row_pointers;
96
97 bool readPngHeader();
98 bool readPngImage(QImage *image);
99 void readPngTexts(png_info *info);
100
101 QImage::Format readImageFormat();
102
103 State state;
104
105 QPngHandler *q;
106};
107
108
109class QPNGImageWriter {
110public:
111 explicit QPNGImageWriter(QIODevice*);
112 ~QPNGImageWriter();
113
114 enum DisposalMethod { Unspecified, NoDisposal, RestoreBackground, RestoreImage };
115 void setDisposalMethod(DisposalMethod);
116 void setLooping(int loops=0); // 0 == infinity
117 void setFrameDelay(int msecs);
118 void setGamma(float);
119
120 bool writeImage(const QImage& img, int x, int y);
121 bool writeImage(const QImage& img, int compression_in, const QString &description, int x, int y);
122 bool writeImage(const QImage& img)
123 { return writeImage(img, x: 0, y: 0); }
124 bool writeImage(const QImage& img, int compression, const QString &description)
125 { return writeImage(img, compression_in: compression, description, x: 0, y: 0); }
126
127 QIODevice* device() { return dev; }
128
129private:
130 QIODevice* dev;
131 int frames_written;
132 DisposalMethod disposal;
133 int looping;
134 int ms_delay;
135 float gamma;
136};
137
138extern "C" {
139static
140void iod_read_fn(png_structp png_ptr, png_bytep data, png_size_t length)
141{
142 QPngHandlerPrivate *d = (QPngHandlerPrivate *)png_get_io_ptr(png_ptr);
143 QIODevice *in = d->q->device();
144
145 if (d->state == QPngHandlerPrivate::ReadingEnd && !in->isSequential() && in->size() > 0 && (in->size() - in->pos()) < 4 && length == 4) {
146 // Workaround for certain malformed PNGs that lack the final crc bytes
147 uchar endcrc[4] = { 0xae, 0x42, 0x60, 0x82 };
148 memcpy(dest: data, src: endcrc, n: 4);
149 in->seek(pos: in->size());
150 return;
151 }
152
153 while (length) {
154 int nr = in->read(data: (char*)data, maxlen: length);
155 if (nr <= 0) {
156 png_error(png_ptr, error_message: "Read Error");
157 return;
158 }
159 length -= nr;
160 }
161}
162
163
164static
165void qpiw_write_fn(png_structp png_ptr, png_bytep data, png_size_t length)
166{
167 QPNGImageWriter* qpiw = (QPNGImageWriter*)png_get_io_ptr(png_ptr);
168 QIODevice* out = qpiw->device();
169
170 uint nr = out->write(data: (char*)data, len: length);
171 if (nr != length) {
172 png_error(png_ptr, error_message: "Write Error");
173 return;
174 }
175}
176
177
178static
179void qpiw_flush_fn(png_structp /* png_ptr */)
180{
181}
182
183}
184
185static
186bool setup_qt(QImage& image, png_structp png_ptr, png_infop info_ptr)
187{
188 png_uint_32 width = 0;
189 png_uint_32 height = 0;
190 int bit_depth = 0;
191 int color_type = 0;
192 png_bytep trans_alpha = nullptr;
193 png_color_16p trans_color_p = nullptr;
194 int num_trans;
195 png_colorp palette = nullptr;
196 int num_palette;
197 png_get_IHDR(png_ptr, info_ptr, width: &width, height: &height, bit_depth: &bit_depth, color_type: &color_type, interlace_method: nullptr, compression_method: nullptr, filter_method: nullptr);
198 QSize size(width, height);
199 png_set_interlace_handling(png_ptr);
200
201 if (color_type == PNG_COLOR_TYPE_GRAY) {
202 // Black & White or grayscale
203 if (bit_depth == 1 && png_get_channels(png_ptr, info_ptr) == 1) {
204 png_set_invert_mono(png_ptr);
205 png_read_update_info(png_ptr, info_ptr);
206 if (!QImageIOHandler::allocateImage(size, format: QImage::Format_Mono, image: &image))
207 return false;
208 image.setColorCount(2);
209 image.setColor(i: 1, c: qRgb(r: 0,g: 0,b: 0));
210 image.setColor(i: 0, c: qRgb(r: 255,g: 255,b: 255));
211 if (png_get_tRNS(png_ptr, info_ptr, trans_alpha: &trans_alpha, num_trans: &num_trans, trans_color: &trans_color_p) && trans_color_p) {
212 const int g = trans_color_p->gray;
213 // the image has white in the first position of the color table,
214 // black in the second. g is 0 for black, 1 for white.
215 if (g == 0)
216 image.setColor(i: 1, c: qRgba(r: 0, g: 0, b: 0, a: 0));
217 else if (g == 1)
218 image.setColor(i: 0, c: qRgba(r: 255, g: 255, b: 255, a: 0));
219 }
220 } else if (bit_depth == 16
221 && png_get_channels(png_ptr, info_ptr) == 1
222 && !png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
223 if (!QImageIOHandler::allocateImage(size, format: QImage::Format_Grayscale16, image: &image))
224 return false;
225 png_read_update_info(png_ptr, info_ptr);
226 if (QSysInfo::ByteOrder == QSysInfo::LittleEndian)
227 png_set_swap(png_ptr);
228 } else if (bit_depth == 16) {
229 bool hasMask = png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS);
230 if (!hasMask)
231 png_set_filler(png_ptr, filler: 0xffff, PNG_FILLER_AFTER);
232 else
233 png_set_expand(png_ptr);
234 png_set_gray_to_rgb(png_ptr);
235 QImage::Format format = hasMask ? QImage::Format_RGBA64 : QImage::Format_RGBX64;
236 if (!QImageIOHandler::allocateImage(size, format, image: &image))
237 return false;
238 png_read_update_info(png_ptr, info_ptr);
239 if (QSysInfo::ByteOrder == QSysInfo::LittleEndian)
240 png_set_swap(png_ptr);
241 } else if (bit_depth == 8 && !png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
242 png_set_expand(png_ptr);
243 if (!QImageIOHandler::allocateImage(size, format: QImage::Format_Grayscale8, image: &image))
244 return false;
245 png_read_update_info(png_ptr, info_ptr);
246 } else {
247 if (bit_depth < 8)
248 png_set_packing(png_ptr);
249 int ncols = bit_depth < 8 ? 1 << bit_depth : 256;
250 png_read_update_info(png_ptr, info_ptr);
251 if (!QImageIOHandler::allocateImage(size, format: QImage::Format_Indexed8, image: &image))
252 return false;
253 image.setColorCount(ncols);
254 for (int i=0; i<ncols; i++) {
255 int c = i*255/(ncols-1);
256 image.setColor(i, c: qRgba(r: c,g: c,b: c,a: 0xff));
257 }
258 if (png_get_tRNS(png_ptr, info_ptr, trans_alpha: &trans_alpha, num_trans: &num_trans, trans_color: &trans_color_p) && trans_color_p) {
259 const int g = trans_color_p->gray;
260 if (g < ncols) {
261 image.setColor(i: g, c: 0);
262 }
263 }
264 }
265 } else if (color_type == PNG_COLOR_TYPE_PALETTE
266 && png_get_PLTE(png_ptr, info_ptr, palette: &palette, num_palette: &num_palette)
267 && num_palette <= 256)
268 {
269 // 1-bit and 8-bit color
270 if (bit_depth != 1)
271 png_set_packing(png_ptr);
272 png_read_update_info(png_ptr, info_ptr);
273 png_get_IHDR(png_ptr, info_ptr, width: &width, height: &height, bit_depth: &bit_depth, color_type: &color_type, interlace_method: nullptr, compression_method: nullptr, filter_method: nullptr);
274 size = QSize(width, height);
275 QImage::Format format = bit_depth == 1 ? QImage::Format_Mono : QImage::Format_Indexed8;
276 if (!QImageIOHandler::allocateImage(size, format, image: &image))
277 return false;
278 png_get_PLTE(png_ptr, info_ptr, palette: &palette, num_palette: &num_palette);
279 image.setColorCount((format == QImage::Format_Mono) ? 2 : num_palette);
280 int i = 0;
281 if (png_get_tRNS(png_ptr, info_ptr, trans_alpha: &trans_alpha, num_trans: &num_trans, trans_color: &trans_color_p) && trans_alpha) {
282 while (i < num_trans) {
283 image.setColor(i, c: qRgba(
284 r: palette[i].red,
285 g: palette[i].green,
286 b: palette[i].blue,
287 a: trans_alpha[i]
288 )
289 );
290 i++;
291 }
292 }
293 while (i < num_palette) {
294 image.setColor(i, c: qRgba(
295 r: palette[i].red,
296 g: palette[i].green,
297 b: palette[i].blue,
298 a: 0xff
299 )
300 );
301 i++;
302 }
303 // Qt==ARGB==Big(ARGB)==Little(BGRA)
304 if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) {
305 png_set_bgr(png_ptr);
306 }
307 } else if (bit_depth == 16 && !(color_type & PNG_COLOR_MASK_PALETTE)) {
308 QImage::Format format = QImage::Format_RGBA64;
309 if (!(color_type & PNG_COLOR_MASK_ALPHA) && !png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
310 png_set_filler(png_ptr, filler: 0xffff, PNG_FILLER_AFTER);
311 format = QImage::Format_RGBX64;
312 }
313 if (!(color_type & PNG_COLOR_MASK_COLOR))
314 png_set_gray_to_rgb(png_ptr);
315 if (!QImageIOHandler::allocateImage(size, format, image: &image))
316 return false;
317 png_read_update_info(png_ptr, info_ptr);
318 if (QSysInfo::ByteOrder == QSysInfo::LittleEndian)
319 png_set_swap(png_ptr);
320 } else {
321 // 32-bit
322 if (bit_depth == 16)
323 png_set_strip_16(png_ptr);
324
325 png_set_expand(png_ptr);
326
327 if (color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
328 png_set_gray_to_rgb(png_ptr);
329
330 QImage::Format format = QImage::Format_ARGB32;
331 // Only add filler if no alpha, or we can get 5 channel data.
332 if (!(color_type & PNG_COLOR_MASK_ALPHA)
333 && !png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
334 png_set_filler(png_ptr, filler: 0xff, flags: QSysInfo::ByteOrder == QSysInfo::BigEndian ?
335 PNG_FILLER_BEFORE : PNG_FILLER_AFTER);
336 // We want 4 bytes, but it isn't an alpha channel
337 format = QImage::Format_RGB32;
338 }
339 if (!QImageIOHandler::allocateImage(size, format, image: &image))
340 return false;
341
342 if (QSysInfo::ByteOrder == QSysInfo::BigEndian)
343 png_set_swap_alpha(png_ptr);
344
345 // Qt==ARGB==Big(ARGB)==Little(BGRA)
346 if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) {
347 png_set_bgr(png_ptr);
348 }
349
350 png_read_update_info(png_ptr, info_ptr);
351 }
352 return true;
353}
354
355extern "C" {
356static void qt_png_warning(png_structp /*png_ptr*/, png_const_charp message)
357{
358 qCInfo(lcImageIo, "libpng warning: %s", message);
359}
360
361}
362
363
364void QPngHandlerPrivate::readPngTexts(png_info *info)
365{
366#ifndef QT_NO_IMAGEIO_TEXT_LOADING
367 png_textp text_ptr;
368 int num_text=0;
369 png_get_text(png_ptr, info_ptr: info, text_ptr: &text_ptr, num_text: &num_text);
370
371 while (num_text--) {
372 QString key, value;
373 key = QString::fromLatin1(ba: text_ptr->key);
374#if defined(PNG_iTXt_SUPPORTED)
375 if (text_ptr->itxt_length) {
376 value = QString::fromUtf8(utf8: text_ptr->text, size: int(text_ptr->itxt_length));
377 } else
378#endif
379 {
380 value = QString::fromLatin1(str: text_ptr->text, size: int(text_ptr->text_length));
381 }
382 if (!description.isEmpty())
383 description += "\n\n"_L1;
384 description += key + ": "_L1 + value.simplified();
385 readTexts.append(t: key);
386 readTexts.append(t: value);
387 text_ptr++;
388 }
389#else
390 Q_UNUSED(info);
391#endif
392}
393
394
395bool QPngHandlerPrivate::readPngHeader()
396{
397 state = Error;
398 png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,error_ptr: nullptr,error_fn: nullptr,warn_fn: nullptr);
399 if (!png_ptr)
400 return false;
401
402 png_set_error_fn(png_ptr, error_ptr: nullptr, error_fn: nullptr, warning_fn: qt_png_warning);
403
404#if defined(PNG_SET_OPTION_SUPPORTED) && defined(PNG_MAXIMUM_INFLATE_WINDOW)
405 // Trade off a little bit of memory for better compatibility with existing images
406 // Ref. "invalid distance too far back" explanation in libpng-manual.txt
407 png_set_option(png_ptr, PNG_MAXIMUM_INFLATE_WINDOW, PNG_OPTION_ON);
408#endif
409
410 info_ptr = png_create_info_struct(png_ptr);
411 if (!info_ptr) {
412 png_destroy_read_struct(png_ptr_ptr: &png_ptr, info_ptr_ptr: nullptr, end_info_ptr_ptr: nullptr);
413 png_ptr = nullptr;
414 return false;
415 }
416
417 end_info = png_create_info_struct(png_ptr);
418 if (!end_info) {
419 png_destroy_read_struct(png_ptr_ptr: &png_ptr, info_ptr_ptr: &info_ptr, end_info_ptr_ptr: nullptr);
420 png_ptr = nullptr;
421 return false;
422 }
423
424 if (setjmp(png_jmpbuf(png_ptr))) {
425 png_destroy_read_struct(png_ptr_ptr: &png_ptr, info_ptr_ptr: &info_ptr, end_info_ptr_ptr: &end_info);
426 png_ptr = nullptr;
427 return false;
428 }
429
430 png_set_read_fn(png_ptr, io_ptr: this, read_data_fn: iod_read_fn);
431 png_read_info(png_ptr, info_ptr);
432
433 readPngTexts(info: info_ptr);
434
435#ifdef PNG_iCCP_SUPPORTED
436 if (png_get_valid(png_ptr, info_ptr, PNG_INFO_iCCP)) {
437 png_charp name = nullptr;
438 int compressionType = 0;
439#if (PNG_LIBPNG_VER < 10500)
440 png_charp profileData = nullptr;
441#else
442 png_bytep profileData = nullptr;
443#endif
444 png_uint_32 profLen;
445 png_get_iCCP(png_ptr, info_ptr, name: &name, compression_type: &compressionType, profile: &profileData, proflen: &profLen);
446 Q_UNUSED(name);
447 Q_UNUSED(compressionType);
448 if (profLen > 0) {
449 colorSpace = QColorSpace::fromIccProfile(iccProfile: QByteArray((const char *)profileData, profLen));
450 QColorSpacePrivate *csD = QColorSpacePrivate::get(colorSpace);
451 if (csD->description.isEmpty())
452 csD->description = QString::fromLatin1(ba: (const char *)name);
453 colorSpaceState = Icc;
454 }
455 }
456#endif
457 if (colorSpaceState <= Srgb && png_get_valid(png_ptr, info_ptr, PNG_INFO_sRGB)) {
458 int rendering_intent = -1;
459 png_get_sRGB(png_ptr, info_ptr, file_srgb_intent: &rendering_intent);
460 // We don't actually care about the rendering_intent, just that it is valid
461 if (rendering_intent >= 0 && rendering_intent <= 3) {
462 colorSpace = QColorSpace::SRgb;
463 colorSpaceState = Srgb;
464 }
465 }
466 if (colorSpaceState <= GammaChrm && png_get_valid(png_ptr, info_ptr, PNG_INFO_gAMA)) {
467 double file_gamma = 0.0;
468 png_get_gAMA(png_ptr, info_ptr, file_gamma: &file_gamma);
469 fileGamma = file_gamma;
470 if (fileGamma > 0.0f) {
471 QColorSpacePrimaries primaries;
472 if (png_get_valid(png_ptr, info_ptr, PNG_INFO_cHRM)) {
473 double white_x, white_y, red_x, red_y;
474 double green_x, green_y, blue_x, blue_y;
475 png_get_cHRM(png_ptr, info_ptr,
476 white_x: &white_x, white_y: &white_y, red_x: &red_x, red_y: &red_y,
477 green_x: &green_x, green_y: &green_y, blue_x: &blue_x, blue_y: &blue_y);
478 primaries.whitePoint = QPointF(white_x, white_y);
479 primaries.redPoint = QPointF(red_x, red_y);
480 primaries.greenPoint = QPointF(green_x, green_y);
481 primaries.bluePoint = QPointF(blue_x, blue_y);
482 }
483 if (primaries.areValid()) {
484 colorSpace = QColorSpace(primaries.whitePoint, primaries.redPoint, primaries.greenPoint, primaries.bluePoint,
485 QColorSpace::TransferFunction::Gamma, 1.0f / fileGamma);
486 } else {
487 colorSpace = QColorSpace(QColorSpace::Primaries::SRgb,
488 QColorSpace::TransferFunction::Gamma, 1.0f / fileGamma);
489 }
490 colorSpaceState = GammaChrm;
491 }
492 }
493
494 state = ReadHeader;
495 return true;
496}
497
498bool QPngHandlerPrivate::readPngImage(QImage *outImage)
499{
500 if (state == Error)
501 return false;
502
503 if (state == Ready && !readPngHeader()) {
504 state = Error;
505 return false;
506 }
507
508 row_pointers = nullptr;
509 if (setjmp(png_jmpbuf(png_ptr))) {
510 png_destroy_read_struct(png_ptr_ptr: &png_ptr, info_ptr_ptr: &info_ptr, end_info_ptr_ptr: &end_info);
511 png_ptr = nullptr;
512 delete[] row_pointers;
513 state = Error;
514 return false;
515 }
516
517 if (gamma != 0.0 && fileGamma != 0.0) {
518 // This configuration forces gamma correction and
519 // thus changes the output colorspace
520 png_set_gamma(png_ptr, screen_gamma: 1.0f / gamma, override_file_gamma: fileGamma);
521 colorSpace.setTransferFunction(transferFunction: QColorSpace::TransferFunction::Gamma, gamma: 1.0f / gamma);
522 colorSpaceState = GammaChrm;
523 }
524
525 if (!setup_qt(image&: *outImage, png_ptr, info_ptr)) {
526 png_destroy_read_struct(png_ptr_ptr: &png_ptr, info_ptr_ptr: &info_ptr, end_info_ptr_ptr: &end_info);
527 png_ptr = nullptr;
528 delete[] row_pointers;
529 state = Error;
530 return false;
531 }
532
533 png_uint_32 width = 0;
534 png_uint_32 height = 0;
535 png_int_32 offset_x = 0;
536 png_int_32 offset_y = 0;
537
538 int bit_depth = 0;
539 int color_type = 0;
540 int unit_type = PNG_OFFSET_PIXEL;
541 png_get_IHDR(png_ptr, info_ptr, width: &width, height: &height, bit_depth: &bit_depth, color_type: &color_type, interlace_method: nullptr, compression_method: nullptr, filter_method: nullptr);
542 png_get_oFFs(png_ptr, info_ptr, offset_x: &offset_x, offset_y: &offset_y, unit_type: &unit_type);
543 uchar *data = outImage->bits();
544 qsizetype bpl = outImage->bytesPerLine();
545 row_pointers = new png_bytep[height];
546
547 for (uint y = 0; y < height; y++)
548 row_pointers[y] = data + y * bpl;
549
550 png_read_image(png_ptr, image: row_pointers);
551
552 outImage->setDotsPerMeterX(png_get_x_pixels_per_meter(png_ptr,info_ptr));
553 outImage->setDotsPerMeterY(png_get_y_pixels_per_meter(png_ptr,info_ptr));
554
555 if (unit_type == PNG_OFFSET_PIXEL)
556 outImage->setOffset(QPoint(offset_x, offset_y));
557
558 // sanity check palette entries
559 if (color_type == PNG_COLOR_TYPE_PALETTE && outImage->format() == QImage::Format_Indexed8) {
560 int color_table_size = outImage->colorCount();
561 for (int y=0; y<(int)height; ++y) {
562 uchar *p = FAST_SCAN_LINE(data, bpl, y);
563 uchar *end = p + width;
564 while (p < end) {
565 if (*p >= color_table_size)
566 *p = 0;
567 ++p;
568 }
569 }
570 }
571
572 state = ReadingEnd;
573 png_read_end(png_ptr, info_ptr: end_info);
574
575 readPngTexts(info: end_info);
576 for (int i = 0; i < readTexts.size()-1; i+=2)
577 outImage->setText(key: readTexts.at(i), value: readTexts.at(i: i+1));
578
579 png_destroy_read_struct(png_ptr_ptr: &png_ptr, info_ptr_ptr: &info_ptr, end_info_ptr_ptr: &end_info);
580 png_ptr = nullptr;
581 delete[] row_pointers;
582 row_pointers = nullptr;
583 state = Ready;
584
585 if (colorSpaceState > Undefined && colorSpace.isValid())
586 outImage->setColorSpace(colorSpace);
587
588 return true;
589}
590
591QImage::Format QPngHandlerPrivate::readImageFormat()
592{
593 QImage::Format format = QImage::Format_Invalid;
594 png_uint_32 width = 0, height = 0;
595 int bit_depth = 0, color_type = 0;
596 png_colorp palette;
597 int num_palette;
598 png_get_IHDR(png_ptr, info_ptr, width: &width, height: &height, bit_depth: &bit_depth, color_type: &color_type, interlace_method: nullptr, compression_method: nullptr, filter_method: nullptr);
599 if (color_type == PNG_COLOR_TYPE_GRAY) {
600 // Black & White or grayscale
601 if (bit_depth == 1 && png_get_channels(png_ptr, info_ptr) == 1) {
602 format = QImage::Format_Mono;
603 } else if (bit_depth == 16) {
604 format = png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) ? QImage::Format_RGBA64 : QImage::Format_Grayscale16;
605 } else if (bit_depth == 8 && !png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
606 format = QImage::Format_Grayscale8;
607 } else {
608 format = QImage::Format_Indexed8;
609 }
610 } else if (color_type == PNG_COLOR_TYPE_PALETTE
611 && png_get_PLTE(png_ptr, info_ptr, palette: &palette, num_palette: &num_palette)
612 && num_palette <= 256)
613 {
614 // 1-bit and 8-bit color
615 format = bit_depth == 1 ? QImage::Format_Mono : QImage::Format_Indexed8;
616 } else if (bit_depth == 16 && !(color_type & PNG_COLOR_MASK_PALETTE)) {
617 format = QImage::Format_RGBA64;
618 if (!(color_type & PNG_COLOR_MASK_ALPHA) && !png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
619 format = QImage::Format_RGBX64;
620 } else {
621 // 32-bit
622 format = QImage::Format_ARGB32;
623 // Only add filler if no alpha, or we can get 5 channel data.
624 if (!(color_type & PNG_COLOR_MASK_ALPHA)
625 && !png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
626 // We want 4 bytes, but it isn't an alpha channel
627 format = QImage::Format_RGB32;
628 }
629 }
630
631 return format;
632}
633
634QPNGImageWriter::QPNGImageWriter(QIODevice* iod) :
635 dev(iod),
636 frames_written(0),
637 disposal(Unspecified),
638 looping(-1),
639 ms_delay(-1),
640 gamma(0.0)
641{
642}
643
644QPNGImageWriter::~QPNGImageWriter()
645{
646}
647
648void QPNGImageWriter::setDisposalMethod(DisposalMethod dm)
649{
650 disposal = dm;
651}
652
653void QPNGImageWriter::setLooping(int loops)
654{
655 looping = loops;
656}
657
658void QPNGImageWriter::setFrameDelay(int msecs)
659{
660 ms_delay = msecs;
661}
662
663void QPNGImageWriter::setGamma(float g)
664{
665 gamma = g;
666}
667
668static void set_text(const QImage &image, png_structp png_ptr, png_infop info_ptr,
669 const QString &description)
670{
671 const QMap<QString, QString> text = qt_getImageText(image, description);
672
673 if (text.isEmpty())
674 return;
675
676 png_textp text_ptr = new png_text[text.size()];
677 memset(s: text_ptr, c: 0, n: text.size() * sizeof(png_text));
678
679 QMap<QString, QString>::ConstIterator it = text.constBegin();
680 int i = 0;
681 while (it != text.constEnd()) {
682 text_ptr[i].key = qstrdup(QStringView{it.key()}.left(n: 79).toLatin1().constData());
683 bool noCompress = (it.value().size() < 40);
684
685#ifdef PNG_iTXt_SUPPORTED
686 bool needsItxt = false;
687 for (QChar c : it.value()) {
688 uchar ch = c.cell();
689 if (c.row() || (ch < 0x20 && ch != '\n') || (ch > 0x7e && ch < 0xa0)) {
690 needsItxt = true;
691 break;
692 }
693 }
694
695 if (needsItxt) {
696 text_ptr[i].compression = noCompress ? PNG_ITXT_COMPRESSION_NONE : PNG_ITXT_COMPRESSION_zTXt;
697 QByteArray value = it.value().toUtf8();
698 text_ptr[i].text = qstrdup(value.constData());
699 text_ptr[i].itxt_length = value.size();
700 text_ptr[i].lang = const_cast<char*>("UTF-8");
701 text_ptr[i].lang_key = qstrdup(it.key().toUtf8().constData());
702 }
703 else
704#endif
705 {
706 text_ptr[i].compression = noCompress ? PNG_TEXT_COMPRESSION_NONE : PNG_TEXT_COMPRESSION_zTXt;
707 QByteArray value = it.value().toLatin1();
708 text_ptr[i].text = qstrdup(value.constData());
709 text_ptr[i].text_length = value.size();
710 }
711 ++i;
712 ++it;
713 }
714
715 png_set_text(png_ptr, info_ptr, text_ptr, num_text: i);
716 for (i = 0; i < text.size(); ++i) {
717 delete [] text_ptr[i].key;
718 delete [] text_ptr[i].text;
719#ifdef PNG_iTXt_SUPPORTED
720 delete [] text_ptr[i].lang_key;
721#endif
722 }
723 delete [] text_ptr;
724}
725
726bool QPNGImageWriter::writeImage(const QImage& image, int off_x, int off_y)
727{
728 return writeImage(img: image, compression_in: -1, description: QString(), x: off_x, y: off_y);
729}
730
731bool QPNGImageWriter::writeImage(const QImage& image, int compression_in, const QString &description,
732 int off_x_in, int off_y_in)
733{
734 QPoint offset = image.offset();
735 int off_x = off_x_in + offset.x();
736 int off_y = off_y_in + offset.y();
737
738 png_structp png_ptr;
739 png_infop info_ptr;
740
741 png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING,error_ptr: nullptr,error_fn: nullptr,warn_fn: nullptr);
742 if (!png_ptr) {
743 return false;
744 }
745
746 png_set_error_fn(png_ptr, error_ptr: nullptr, error_fn: nullptr, warning_fn: qt_png_warning);
747#ifdef PNG_BENIGN_ERRORS_SUPPORTED
748 png_set_benign_errors(png_ptr, allowed: 1);
749#endif
750
751 info_ptr = png_create_info_struct(png_ptr);
752 if (!info_ptr) {
753 png_destroy_write_struct(png_ptr_ptr: &png_ptr, info_ptr_ptr: nullptr);
754 return false;
755 }
756
757 if (setjmp(png_jmpbuf(png_ptr))) {
758 png_destroy_write_struct(png_ptr_ptr: &png_ptr, info_ptr_ptr: &info_ptr);
759 return false;
760 }
761
762 int compression = compression_in;
763 if (compression >= 0) {
764 if (compression > 9) {
765 qCWarning(lcImageIo, "PNG: Compression %d out of range", compression);
766 compression = 9;
767 }
768 png_set_compression_level(png_ptr, level: compression);
769 }
770
771 png_set_write_fn(png_ptr, io_ptr: (void*)this, write_data_fn: qpiw_write_fn, output_flush_fn: qpiw_flush_fn);
772
773
774 int color_type = 0;
775 if (image.format() <= QImage::Format_Indexed8) {
776 if (image.isGrayscale())
777 color_type = PNG_COLOR_TYPE_GRAY;
778 else
779 color_type = PNG_COLOR_TYPE_PALETTE;
780 }
781 else if (image.format() == QImage::Format_Grayscale8
782 || image.format() == QImage::Format_Grayscale16)
783 color_type = PNG_COLOR_TYPE_GRAY;
784 else if (image.hasAlphaChannel())
785 color_type = PNG_COLOR_TYPE_RGB_ALPHA;
786 else
787 color_type = PNG_COLOR_TYPE_RGB;
788
789 int bpc = 0;
790 switch (image.format()) {
791 case QImage::Format_Mono:
792 case QImage::Format_MonoLSB:
793 bpc = 1;
794 break;
795 case QImage::Format_RGBX64:
796 case QImage::Format_RGBA64:
797 case QImage::Format_RGBA64_Premultiplied:
798 case QImage::Format_Grayscale16:
799 bpc = 16;
800 break;
801 default:
802 bpc = 8;
803 break;
804 }
805
806 png_set_IHDR(png_ptr, info_ptr, width: image.width(), height: image.height(),
807 bit_depth: bpc, // per channel
808 color_type, interlace_method: 0, compression_method: 0, filter_method: 0); // sets #channels
809
810#ifdef PNG_iCCP_SUPPORTED
811 QColorSpace cs = image.colorSpace();
812 // Support the old gamma making it override transferfunction (if possible)
813 if (cs.isValid() && gamma != 0.0 && !qFuzzyCompare(p1: cs.gamma(), p2: 1.0f / gamma))
814 cs = cs.withTransferFunction(transferFunction: QColorSpace::TransferFunction::Gamma, gamma: 1.0f / gamma);
815 QByteArray iccProfile = cs.iccProfile();
816 if (!iccProfile.isEmpty()) {
817 QByteArray iccProfileName = cs.description().toLatin1();
818 if (iccProfileName.isEmpty())
819 iccProfileName = QByteArrayLiteral("Custom");
820 png_set_iCCP(png_ptr, info_ptr,
821 #if PNG_LIBPNG_VER < 10500
822 iccProfileName.data(), PNG_COMPRESSION_TYPE_BASE, iccProfile.data(),
823 #else
824 name: iccProfileName.constData(), PNG_COMPRESSION_TYPE_BASE,
825 profile: (png_const_bytep)iccProfile.constData(),
826 #endif
827 proflen: iccProfile.size());
828 } else
829#endif
830 if (gamma != 0.0) {
831 png_set_gAMA(png_ptr, info_ptr, file_gamma: 1.0/gamma);
832 }
833
834 if (image.format() == QImage::Format_MonoLSB)
835 png_set_packswap(png_ptr);
836
837 if (color_type == PNG_COLOR_TYPE_PALETTE) {
838 // Paletted
839 int num_palette = qMin(a: 256, b: image.colorCount());
840 png_color palette[256];
841 png_byte trans[256];
842 int num_trans = 0;
843 for (int i=0; i<num_palette; i++) {
844 QRgb rgba=image.color(i);
845 palette[i].red = qRed(rgb: rgba);
846 palette[i].green = qGreen(rgb: rgba);
847 palette[i].blue = qBlue(rgb: rgba);
848 trans[i] = qAlpha(rgb: rgba);
849 if (trans[i] < 255) {
850 num_trans = i+1;
851 }
852 }
853 png_set_PLTE(png_ptr, info_ptr, palette, num_palette);
854
855 if (num_trans) {
856 png_set_tRNS(png_ptr, info_ptr, trans_alpha: trans, num_trans, trans_color: nullptr);
857 }
858 }
859
860 // Swap ARGB to RGBA (normal PNG format) before saving on
861 // BigEndian machines
862 if (QSysInfo::ByteOrder == QSysInfo::BigEndian) {
863 switch (image.format()) {
864 case QImage::Format_RGBX8888:
865 case QImage::Format_RGBA8888:
866 case QImage::Format_RGBX64:
867 case QImage::Format_RGBA64:
868 case QImage::Format_RGBA64_Premultiplied:
869 break;
870 default:
871 png_set_swap_alpha(png_ptr);
872 }
873 }
874
875 // Qt==ARGB==Big(ARGB)==Little(BGRA). But RGB888 is RGB regardless
876 if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) {
877 switch (image.format()) {
878 case QImage::Format_RGB888:
879 case QImage::Format_RGBX8888:
880 case QImage::Format_RGBA8888:
881 case QImage::Format_RGBX64:
882 case QImage::Format_RGBA64:
883 case QImage::Format_RGBA64_Premultiplied:
884 break;
885 default:
886 png_set_bgr(png_ptr);
887 }
888 }
889
890 if (off_x || off_y) {
891 png_set_oFFs(png_ptr, info_ptr, offset_x: off_x, offset_y: off_y, PNG_OFFSET_PIXEL);
892 }
893
894 if (frames_written > 0)
895 png_set_sig_bytes(png_ptr, num_bytes: 8);
896
897 if (image.dotsPerMeterX() > 0 || image.dotsPerMeterY() > 0) {
898 png_set_pHYs(png_ptr, info_ptr,
899 res_x: image.dotsPerMeterX(), res_y: image.dotsPerMeterY(),
900 PNG_RESOLUTION_METER);
901 }
902
903 set_text(image, png_ptr, info_ptr, description);
904
905 png_write_info(png_ptr, info_ptr);
906
907 if (image.depth() != 1)
908 png_set_packing(png_ptr);
909
910 if (color_type == PNG_COLOR_TYPE_RGB) {
911 switch (image.format()) {
912 case QImage::Format_RGB888:
913 case QImage::Format_BGR888:
914 break;
915 case QImage::Format_RGBX8888:
916 case QImage::Format_RGBX64:
917 png_set_filler(png_ptr, filler: 0, PNG_FILLER_AFTER);
918 break;
919 default:
920 png_set_filler(png_ptr, filler: 0,
921 flags: QSysInfo::ByteOrder == QSysInfo::BigEndian ?
922 PNG_FILLER_BEFORE : PNG_FILLER_AFTER);
923 }
924 }
925
926 if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) {
927 switch (image.format()) {
928 case QImage::Format_RGBX64:
929 case QImage::Format_RGBA64:
930 case QImage::Format_RGBA64_Premultiplied:
931 case QImage::Format_Grayscale16:
932 png_set_swap(png_ptr);
933 break;
934 default:
935 break;
936 }
937 }
938
939 if (looping >= 0 && frames_written == 0) {
940 uchar data[13] = "NETSCAPE2.0";
941 // 0123456789aBC
942 data[0xB] = looping%0x100;
943 data[0xC] = looping/0x100;
944 png_write_chunk(png_ptr, chunk_name: const_cast<png_bytep>((const png_byte *)"gIFx"), data, length: 13);
945 }
946 if (ms_delay >= 0 || disposal!=Unspecified) {
947 uchar data[4];
948 data[0] = disposal;
949 data[1] = 0;
950 data[2] = (ms_delay/10)/0x100; // hundredths
951 data[3] = (ms_delay/10)%0x100;
952 png_write_chunk(png_ptr, chunk_name: const_cast<png_bytep>((const png_byte *)"gIFg"), data, length: 4);
953 }
954
955 int height = image.height();
956 int width = image.width();
957 switch (image.format()) {
958 case QImage::Format_Mono:
959 case QImage::Format_MonoLSB:
960 case QImage::Format_Indexed8:
961 case QImage::Format_Grayscale8:
962 case QImage::Format_Grayscale16:
963 case QImage::Format_RGB32:
964 case QImage::Format_ARGB32:
965 case QImage::Format_RGB888:
966 case QImage::Format_BGR888:
967 case QImage::Format_RGBX8888:
968 case QImage::Format_RGBA8888:
969 case QImage::Format_RGBX64:
970 case QImage::Format_RGBA64:
971 {
972 png_bytep* row_pointers = new png_bytep[height];
973 for (int y=0; y<height; y++)
974 row_pointers[y] = const_cast<png_bytep>(image.constScanLine(y));
975 png_write_image(png_ptr, image: row_pointers);
976 delete [] row_pointers;
977 }
978 break;
979 case QImage::Format_RGBA64_Premultiplied:
980 {
981 QImage row;
982 png_bytep row_pointers[1];
983 for (int y=0; y<height; y++) {
984 row = image.copy(x: 0, y, w: width, h: 1).convertToFormat(f: QImage::Format_RGBA64);
985 row_pointers[0] = const_cast<png_bytep>(row.constScanLine(0));
986 png_write_rows(png_ptr, row: row_pointers, num_rows: 1);
987 }
988 }
989 break;
990 default:
991 {
992 QImage::Format fmt = image.hasAlphaChannel() ? QImage::Format_ARGB32 : QImage::Format_RGB32;
993 QImage row;
994 png_bytep row_pointers[1];
995 for (int y=0; y<height; y++) {
996 row = image.copy(x: 0, y, w: width, h: 1).convertToFormat(f: fmt);
997 row_pointers[0] = const_cast<png_bytep>(row.constScanLine(0));
998 png_write_rows(png_ptr, row: row_pointers, num_rows: 1);
999 }
1000 }
1001 break;
1002 }
1003
1004 png_write_end(png_ptr, info_ptr);
1005 frames_written++;
1006
1007 png_destroy_write_struct(png_ptr_ptr: &png_ptr, info_ptr_ptr: &info_ptr);
1008
1009 return true;
1010}
1011
1012static bool write_png_image(const QImage &image, QIODevice *device,
1013 int compression, int quality, float gamma, const QString &description)
1014{
1015 // quality is used for backward compatibility, maps to compression
1016
1017 QPNGImageWriter writer(device);
1018 if (compression >= 0)
1019 compression = qMin(a: compression, b: 100);
1020 else if (quality >= 0)
1021 compression = 100 - qMin(a: quality, b: 100);
1022
1023 if (compression >= 0)
1024 compression = (compression * 9) / 91; // map [0,100] -> [0,9]
1025
1026 writer.setGamma(gamma);
1027 return writer.writeImage(img: image, compression, description);
1028}
1029
1030QPngHandler::QPngHandler()
1031 : d(new QPngHandlerPrivate(this))
1032{
1033}
1034
1035QPngHandler::~QPngHandler()
1036{
1037 if (d->png_ptr)
1038 png_destroy_read_struct(png_ptr_ptr: &d->png_ptr, info_ptr_ptr: &d->info_ptr, end_info_ptr_ptr: &d->end_info);
1039 delete d;
1040}
1041
1042bool QPngHandler::canRead() const
1043{
1044 if (d->state == QPngHandlerPrivate::Ready && !canRead(device: device()))
1045 return false;
1046
1047 if (d->state != QPngHandlerPrivate::Error) {
1048 setFormat("png");
1049 return true;
1050 }
1051
1052 return false;
1053}
1054
1055bool QPngHandler::canRead(QIODevice *device)
1056{
1057 if (!device) {
1058 qCWarning(lcImageIo, "QPngHandler::canRead() called with no device");
1059 return false;
1060 }
1061
1062 return device->peek(maxlen: 8) == "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A";
1063}
1064
1065bool QPngHandler::read(QImage *image)
1066{
1067 if (!canRead())
1068 return false;
1069 return d->readPngImage(outImage: image);
1070}
1071
1072bool QPngHandler::write(const QImage &image)
1073{
1074 return write_png_image(image, device: device(), compression: d->compression, quality: d->quality, gamma: d->gamma, description: d->description);
1075}
1076
1077bool QPngHandler::supportsOption(ImageOption option) const
1078{
1079 return option == Gamma
1080 || option == Description
1081 || option == ImageFormat
1082 || option == Quality
1083 || option == CompressionRatio
1084 || option == Size;
1085}
1086
1087QVariant QPngHandler::option(ImageOption option) const
1088{
1089 if (d->state == QPngHandlerPrivate::Error)
1090 return QVariant();
1091 if (d->state == QPngHandlerPrivate::Ready && !d->readPngHeader())
1092 return QVariant();
1093
1094 if (option == Gamma)
1095 return d->gamma == 0.0 ? d->fileGamma : d->gamma;
1096 else if (option == Quality)
1097 return d->quality;
1098 else if (option == CompressionRatio)
1099 return d->compression;
1100 else if (option == Description)
1101 return d->description;
1102 else if (option == Size)
1103 return QSize(png_get_image_width(png_ptr: d->png_ptr, info_ptr: d->info_ptr),
1104 png_get_image_height(png_ptr: d->png_ptr, info_ptr: d->info_ptr));
1105 else if (option == ImageFormat)
1106 return d->readImageFormat();
1107 return QVariant();
1108}
1109
1110void QPngHandler::setOption(ImageOption option, const QVariant &value)
1111{
1112 if (option == Gamma)
1113 d->gamma = value.toFloat();
1114 else if (option == Quality)
1115 d->quality = value.toInt();
1116 else if (option == CompressionRatio)
1117 d->compression = value.toInt();
1118 else if (option == Description)
1119 d->description = value.toString();
1120}
1121
1122QT_END_NAMESPACE
1123
1124#endif // QT_NO_IMAGEFORMAT_PNG
1125

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

source code of qtbase/src/gui/image/qpnghandler.cpp