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