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