1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the plugins of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:LGPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
24 | ** |
25 | ** GNU General Public License Usage |
26 | ** Alternatively, this file may be used under the terms of the GNU |
27 | ** General Public License version 2.0 or (at your option) the GNU General |
28 | ** Public license version 3 or any later version approved by the KDE Free |
29 | ** Qt Foundation. The licenses are as published by the Free Software |
30 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
31 | ** included in the packaging of this file. Please review the following |
32 | ** information to ensure the GNU General Public License requirements will |
33 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
34 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
35 | ** |
36 | ** $QT_END_LICENSE$ |
37 | ** |
38 | ****************************************************************************/ |
39 | |
40 | #include "qjpeghandler_p.h" |
41 | |
42 | #include <qbuffer.h> |
43 | #include <qcolorspace.h> |
44 | #include <qcolortransform.h> |
45 | #include <qdebug.h> |
46 | #include <qimage.h> |
47 | #include <qlist.h> |
48 | #include <qloggingcategory.h> |
49 | #include <qmath.h> |
50 | #include <qvariant.h> |
51 | #include <private/qicc_p.h> |
52 | #include <private/qsimd_p.h> |
53 | #include <private/qimage_p.h> // for qt_getImageText |
54 | |
55 | #include <stdio.h> // jpeglib needs this to be pre-included |
56 | #include <setjmp.h> |
57 | |
58 | #ifdef FAR |
59 | #undef FAR |
60 | #endif |
61 | |
62 | // including jpeglib.h seems to be a little messy |
63 | extern "C" { |
64 | // jpeglib.h->jmorecfg.h tries to typedef int boolean; but this conflicts with |
65 | // some Windows headers that may or may not have been included |
66 | #ifdef HAVE_BOOLEAN |
67 | # undef HAVE_BOOLEAN |
68 | #endif |
69 | #define boolean jboolean |
70 | |
71 | #define XMD_H // shut JPEGlib up |
72 | #include <jpeglib.h> |
73 | #ifdef const |
74 | # undef const // remove crazy C hackery in jconfig.h |
75 | #endif |
76 | } |
77 | |
78 | QT_BEGIN_NAMESPACE |
79 | |
80 | Q_LOGGING_CATEGORY(lcJpeg, "qt.gui.imageio.jpeg" ) |
81 | |
82 | QT_WARNING_DISABLE_GCC("-Wclobbered" ) |
83 | |
84 | Q_GUI_EXPORT void QT_FASTCALL qt_convert_rgb888_to_rgb32(quint32 *dst, const uchar *src, int len); |
85 | typedef void (QT_FASTCALL *Rgb888ToRgb32Converter)(quint32 *dst, const uchar *src, int len); |
86 | |
87 | struct my_error_mgr : public jpeg_error_mgr { |
88 | jmp_buf setjmp_buffer; |
89 | }; |
90 | |
91 | extern "C" { |
92 | |
93 | static void my_error_exit (j_common_ptr cinfo) |
94 | { |
95 | my_error_mgr* myerr = (my_error_mgr*) cinfo->err; |
96 | char buffer[JMSG_LENGTH_MAX]; |
97 | (*cinfo->err->format_message)(cinfo, buffer); |
98 | qCWarning(lcJpeg, "%s" , buffer); |
99 | longjmp(myerr->setjmp_buffer, 1); |
100 | } |
101 | |
102 | static void my_output_message(j_common_ptr cinfo) |
103 | { |
104 | char buffer[JMSG_LENGTH_MAX]; |
105 | (*cinfo->err->format_message)(cinfo, buffer); |
106 | qCWarning(lcJpeg,"%s" , buffer); |
107 | } |
108 | |
109 | } |
110 | |
111 | |
112 | static const int max_buf = 4096; |
113 | |
114 | struct my_jpeg_source_mgr : public jpeg_source_mgr { |
115 | // Nothing dynamic - cannot rely on destruction over longjump |
116 | QIODevice *device; |
117 | JOCTET buffer[max_buf]; |
118 | const QBuffer *memDevice; |
119 | |
120 | public: |
121 | my_jpeg_source_mgr(QIODevice *device); |
122 | }; |
123 | |
124 | extern "C" { |
125 | |
126 | static void qt_init_source(j_decompress_ptr) |
127 | { |
128 | } |
129 | |
130 | static boolean qt_fill_input_buffer(j_decompress_ptr cinfo) |
131 | { |
132 | my_jpeg_source_mgr* src = (my_jpeg_source_mgr*)cinfo->src; |
133 | qint64 num_read = 0; |
134 | if (src->memDevice) { |
135 | src->next_input_byte = (const JOCTET *)(src->memDevice->data().constData() + src->memDevice->pos()); |
136 | num_read = src->memDevice->data().size() - src->memDevice->pos(); |
137 | src->device->seek(src->memDevice->data().size()); |
138 | } else { |
139 | src->next_input_byte = src->buffer; |
140 | num_read = src->device->read((char*)src->buffer, max_buf); |
141 | } |
142 | if (num_read <= 0) { |
143 | // Insert a fake EOI marker - as per jpeglib recommendation |
144 | src->next_input_byte = src->buffer; |
145 | src->buffer[0] = (JOCTET) 0xFF; |
146 | src->buffer[1] = (JOCTET) JPEG_EOI; |
147 | src->bytes_in_buffer = 2; |
148 | } else { |
149 | src->bytes_in_buffer = num_read; |
150 | } |
151 | return TRUE; |
152 | } |
153 | |
154 | static void qt_skip_input_data(j_decompress_ptr cinfo, long num_bytes) |
155 | { |
156 | my_jpeg_source_mgr* src = (my_jpeg_source_mgr*)cinfo->src; |
157 | |
158 | // `dumb' implementation from jpeglib |
159 | |
160 | /* Just a dumb implementation for now. Could use fseek() except |
161 | * it doesn't work on pipes. Not clear that being smart is worth |
162 | * any trouble anyway --- large skips are infrequent. |
163 | */ |
164 | if (num_bytes > 0) { |
165 | while (num_bytes > (long) src->bytes_in_buffer) { // Should not happen in case of memDevice |
166 | num_bytes -= (long) src->bytes_in_buffer; |
167 | (void) qt_fill_input_buffer(cinfo); |
168 | /* note we assume that qt_fill_input_buffer will never return false, |
169 | * so suspension need not be handled. |
170 | */ |
171 | } |
172 | src->next_input_byte += (size_t) num_bytes; |
173 | src->bytes_in_buffer -= (size_t) num_bytes; |
174 | } |
175 | } |
176 | |
177 | static void qt_term_source(j_decompress_ptr cinfo) |
178 | { |
179 | my_jpeg_source_mgr* src = (my_jpeg_source_mgr*)cinfo->src; |
180 | if (!src->device->isSequential()) |
181 | src->device->seek(src->device->pos() - src->bytes_in_buffer); |
182 | } |
183 | |
184 | } |
185 | |
186 | inline my_jpeg_source_mgr::my_jpeg_source_mgr(QIODevice *device) |
187 | { |
188 | jpeg_source_mgr::init_source = qt_init_source; |
189 | jpeg_source_mgr::fill_input_buffer = qt_fill_input_buffer; |
190 | jpeg_source_mgr::skip_input_data = qt_skip_input_data; |
191 | jpeg_source_mgr::resync_to_restart = jpeg_resync_to_restart; |
192 | jpeg_source_mgr::term_source = qt_term_source; |
193 | this->device = device; |
194 | memDevice = qobject_cast<QBuffer *>(device); |
195 | bytes_in_buffer = 0; |
196 | next_input_byte = buffer; |
197 | } |
198 | |
199 | |
200 | inline static bool read_jpeg_size(int &w, int &h, j_decompress_ptr cinfo) |
201 | { |
202 | (void) jpeg_calc_output_dimensions(cinfo); |
203 | |
204 | w = cinfo->output_width; |
205 | h = cinfo->output_height; |
206 | return true; |
207 | } |
208 | |
209 | #define HIGH_QUALITY_THRESHOLD 50 |
210 | |
211 | inline static bool read_jpeg_format(QImage::Format &format, j_decompress_ptr cinfo) |
212 | { |
213 | |
214 | bool result = true; |
215 | switch (cinfo->output_components) { |
216 | case 1: |
217 | format = QImage::Format_Grayscale8; |
218 | break; |
219 | case 3: |
220 | case 4: |
221 | format = QImage::Format_RGB32; |
222 | break; |
223 | default: |
224 | result = false; |
225 | break; |
226 | } |
227 | cinfo->output_scanline = cinfo->output_height; |
228 | return result; |
229 | } |
230 | |
231 | static bool ensureValidImage(QImage *dest, struct jpeg_decompress_struct *info, |
232 | const QSize& size) |
233 | { |
234 | QImage::Format format; |
235 | switch (info->output_components) { |
236 | case 1: |
237 | format = QImage::Format_Grayscale8; |
238 | break; |
239 | case 3: |
240 | case 4: |
241 | format = QImage::Format_RGB32; |
242 | break; |
243 | default: |
244 | return false; // unsupported format |
245 | } |
246 | |
247 | return QImageIOHandler::allocateImage(size, format, dest); |
248 | } |
249 | |
250 | static bool read_jpeg_image(QImage *outImage, |
251 | QSize scaledSize, QRect scaledClipRect, |
252 | QRect clipRect, int quality, |
253 | Rgb888ToRgb32Converter converter, |
254 | j_decompress_ptr info, struct my_error_mgr* err ) |
255 | { |
256 | if (!setjmp(err->setjmp_buffer)) { |
257 | // -1 means default quality. |
258 | if (quality < 0) |
259 | quality = 75; |
260 | |
261 | // If possible, merge the scaledClipRect into either scaledSize |
262 | // or clipRect to avoid doing a separate scaled clipping pass. |
263 | // Best results are achieved by clipping before scaling, not after. |
264 | if (!scaledClipRect.isEmpty()) { |
265 | if (scaledSize.isEmpty() && clipRect.isEmpty()) { |
266 | // No clipping or scaling before final clip. |
267 | clipRect = scaledClipRect; |
268 | scaledClipRect = QRect(); |
269 | } else if (scaledSize.isEmpty()) { |
270 | // Clipping, but no scaling: combine the clip regions. |
271 | scaledClipRect.translate(clipRect.topLeft()); |
272 | clipRect = scaledClipRect.intersected(clipRect); |
273 | scaledClipRect = QRect(); |
274 | } else if (clipRect.isEmpty()) { |
275 | // No clipping, but scaling: if we can map back to an |
276 | // integer pixel boundary, then clip before scaling. |
277 | if ((info->image_width % scaledSize.width()) == 0 && |
278 | (info->image_height % scaledSize.height()) == 0) { |
279 | int x = scaledClipRect.x() * info->image_width / |
280 | scaledSize.width(); |
281 | int y = scaledClipRect.y() * info->image_height / |
282 | scaledSize.height(); |
283 | int width = (scaledClipRect.right() + 1) * |
284 | info->image_width / scaledSize.width() - x; |
285 | int height = (scaledClipRect.bottom() + 1) * |
286 | info->image_height / scaledSize.height() - y; |
287 | clipRect = QRect(x, y, width, height); |
288 | scaledSize = scaledClipRect.size(); |
289 | scaledClipRect = QRect(); |
290 | } |
291 | } else { |
292 | // Clipping and scaling: too difficult to figure out, |
293 | // and not a likely use case, so do it the long way. |
294 | } |
295 | } |
296 | |
297 | // Determine the scale factor to pass to libjpeg for quick downscaling. |
298 | if (!scaledSize.isEmpty() && info->image_width && info->image_height) { |
299 | if (clipRect.isEmpty()) { |
300 | double f = qMin(double(info->image_width) / scaledSize.width(), |
301 | double(info->image_height) / scaledSize.height()); |
302 | |
303 | // libjpeg supports M/8 scaling with M=[1,16]. All downscaling factors |
304 | // are a speed improvement, but upscaling during decode is slower. |
305 | info->scale_num = qBound(1, qCeil(8/f), 8); |
306 | info->scale_denom = 8; |
307 | } else { |
308 | info->scale_denom = qMin(clipRect.width() / scaledSize.width(), |
309 | clipRect.height() / scaledSize.height()); |
310 | |
311 | // Only scale by powers of two when clipping so we can |
312 | // keep the exact pixel boundaries |
313 | if (info->scale_denom < 2) |
314 | info->scale_denom = 1; |
315 | else if (info->scale_denom < 4) |
316 | info->scale_denom = 2; |
317 | else if (info->scale_denom < 8) |
318 | info->scale_denom = 4; |
319 | else |
320 | info->scale_denom = 8; |
321 | info->scale_num = 1; |
322 | |
323 | // Correct the scale factor so that we clip accurately. |
324 | // It is recommended that the clip rectangle be aligned |
325 | // on an 8-pixel boundary for best performance. |
326 | while (info->scale_denom > 1 && |
327 | ((clipRect.x() % info->scale_denom) != 0 || |
328 | (clipRect.y() % info->scale_denom) != 0 || |
329 | (clipRect.width() % info->scale_denom) != 0 || |
330 | (clipRect.height() % info->scale_denom) != 0)) { |
331 | info->scale_denom /= 2; |
332 | } |
333 | } |
334 | } |
335 | |
336 | // If high quality not required, use fast decompression |
337 | if( quality < HIGH_QUALITY_THRESHOLD ) { |
338 | info->dct_method = JDCT_IFAST; |
339 | info->do_fancy_upsampling = FALSE; |
340 | } |
341 | |
342 | (void) jpeg_calc_output_dimensions(info); |
343 | |
344 | // Determine the clip region to extract. |
345 | QRect imageRect(0, 0, info->output_width, info->output_height); |
346 | QRect clip; |
347 | if (clipRect.isEmpty()) { |
348 | clip = imageRect; |
349 | } else if (info->scale_denom == info->scale_num) { |
350 | clip = clipRect.intersected(imageRect); |
351 | } else { |
352 | // The scale factor was corrected above to ensure that |
353 | // we don't miss pixels when we scale the clip rectangle. |
354 | clip = QRect(clipRect.x() / int(info->scale_denom), |
355 | clipRect.y() / int(info->scale_denom), |
356 | clipRect.width() / int(info->scale_denom), |
357 | clipRect.height() / int(info->scale_denom)); |
358 | clip = clip.intersected(imageRect); |
359 | } |
360 | |
361 | // Allocate memory for the clipped QImage. |
362 | if (!ensureValidImage(outImage, info, clip.size())) |
363 | longjmp(err->setjmp_buffer, 1); |
364 | |
365 | // Avoid memcpy() overhead if grayscale with no clipping. |
366 | bool quickGray = (info->output_components == 1 && |
367 | clip == imageRect); |
368 | if (!quickGray) { |
369 | // Ask the jpeg library to allocate a temporary row. |
370 | // The library will automatically delete it for us later. |
371 | // The libjpeg docs say we should do this before calling |
372 | // jpeg_start_decompress(). We can't use "new" here |
373 | // because we are inside the setjmp() block and an error |
374 | // in the jpeg input stream would cause a memory leak. |
375 | JSAMPARRAY rows = (info->mem->alloc_sarray) |
376 | ((j_common_ptr)info, JPOOL_IMAGE, |
377 | info->output_width * info->output_components, 1); |
378 | |
379 | (void) jpeg_start_decompress(info); |
380 | |
381 | while (info->output_scanline < info->output_height) { |
382 | int y = int(info->output_scanline) - clip.y(); |
383 | if (y >= clip.height()) |
384 | break; // We've read the entire clip region, so abort. |
385 | |
386 | (void) jpeg_read_scanlines(info, rows, 1); |
387 | |
388 | if (y < 0) |
389 | continue; // Haven't reached the starting line yet. |
390 | |
391 | if (info->output_components == 3) { |
392 | uchar *in = rows[0] + clip.x() * 3; |
393 | QRgb *out = (QRgb*)outImage->scanLine(y); |
394 | converter(out, in, clip.width()); |
395 | } else if (info->out_color_space == JCS_CMYK) { |
396 | // Convert CMYK->RGB. |
397 | uchar *in = rows[0] + clip.x() * 4; |
398 | QRgb *out = (QRgb*)outImage->scanLine(y); |
399 | for (int i = 0; i < clip.width(); ++i) { |
400 | int k = in[3]; |
401 | *out++ = qRgb(k * in[0] / 255, k * in[1] / 255, |
402 | k * in[2] / 255); |
403 | in += 4; |
404 | } |
405 | } else if (info->output_components == 1) { |
406 | // Grayscale. |
407 | memcpy(outImage->scanLine(y), |
408 | rows[0] + clip.x(), clip.width()); |
409 | } |
410 | } |
411 | } else { |
412 | // Load unclipped grayscale data directly into the QImage. |
413 | (void) jpeg_start_decompress(info); |
414 | while (info->output_scanline < info->output_height) { |
415 | uchar *row = outImage->scanLine(info->output_scanline); |
416 | (void) jpeg_read_scanlines(info, &row, 1); |
417 | } |
418 | } |
419 | |
420 | if (info->output_scanline == info->output_height) |
421 | (void) jpeg_finish_decompress(info); |
422 | |
423 | if (info->density_unit == 1) { |
424 | outImage->setDotsPerMeterX(int(100. * info->X_density / 2.54)); |
425 | outImage->setDotsPerMeterY(int(100. * info->Y_density / 2.54)); |
426 | } else if (info->density_unit == 2) { |
427 | outImage->setDotsPerMeterX(int(100. * info->X_density)); |
428 | outImage->setDotsPerMeterY(int(100. * info->Y_density)); |
429 | } |
430 | |
431 | if (scaledSize.isValid() && scaledSize != clip.size()) { |
432 | *outImage = outImage->scaled(scaledSize, Qt::IgnoreAspectRatio, quality >= HIGH_QUALITY_THRESHOLD ? Qt::SmoothTransformation : Qt::FastTransformation); |
433 | } |
434 | |
435 | if (!scaledClipRect.isEmpty()) |
436 | *outImage = outImage->copy(scaledClipRect); |
437 | return !outImage->isNull(); |
438 | } |
439 | else |
440 | return false; |
441 | } |
442 | |
443 | struct my_jpeg_destination_mgr : public jpeg_destination_mgr { |
444 | // Nothing dynamic - cannot rely on destruction over longjump |
445 | QIODevice *device; |
446 | JOCTET buffer[max_buf]; |
447 | |
448 | public: |
449 | my_jpeg_destination_mgr(QIODevice *); |
450 | }; |
451 | |
452 | |
453 | extern "C" { |
454 | |
455 | static void qt_init_destination(j_compress_ptr) |
456 | { |
457 | } |
458 | |
459 | static boolean qt_empty_output_buffer(j_compress_ptr cinfo) |
460 | { |
461 | my_jpeg_destination_mgr* dest = (my_jpeg_destination_mgr*)cinfo->dest; |
462 | |
463 | int written = dest->device->write((char*)dest->buffer, max_buf); |
464 | if (written == -1) |
465 | (*cinfo->err->error_exit)((j_common_ptr)cinfo); |
466 | |
467 | dest->next_output_byte = dest->buffer; |
468 | dest->free_in_buffer = max_buf; |
469 | |
470 | return TRUE; |
471 | } |
472 | |
473 | static void qt_term_destination(j_compress_ptr cinfo) |
474 | { |
475 | my_jpeg_destination_mgr* dest = (my_jpeg_destination_mgr*)cinfo->dest; |
476 | qint64 n = max_buf - dest->free_in_buffer; |
477 | |
478 | qint64 written = dest->device->write((char*)dest->buffer, n); |
479 | if (written == -1) |
480 | (*cinfo->err->error_exit)((j_common_ptr)cinfo); |
481 | } |
482 | |
483 | } |
484 | |
485 | inline my_jpeg_destination_mgr::my_jpeg_destination_mgr(QIODevice *device) |
486 | { |
487 | jpeg_destination_mgr::init_destination = qt_init_destination; |
488 | jpeg_destination_mgr::empty_output_buffer = qt_empty_output_buffer; |
489 | jpeg_destination_mgr::term_destination = qt_term_destination; |
490 | this->device = device; |
491 | next_output_byte = buffer; |
492 | free_in_buffer = max_buf; |
493 | } |
494 | |
495 | static constexpr int maxMarkerSize = 65533; |
496 | |
497 | static inline void set_text(const QImage &image, j_compress_ptr cinfo, const QString &description) |
498 | { |
499 | const QMap<QString, QString> text = qt_getImageText(image, description); |
500 | for (auto it = text.begin(), end = text.end(); it != end; ++it) { |
501 | QByteArray = it.key().toUtf8(); |
502 | if (!comment.isEmpty()) |
503 | comment += ": " ; |
504 | comment += it.value().toUtf8(); |
505 | if (comment.length() > maxMarkerSize) |
506 | comment.truncate(maxMarkerSize); |
507 | jpeg_write_marker(cinfo, JPEG_COM, (const JOCTET *)comment.constData(), comment.size()); |
508 | } |
509 | } |
510 | |
511 | static inline void write_icc_profile(const QImage &image, j_compress_ptr cinfo) |
512 | { |
513 | const QByteArray iccProfile = image.colorSpace().iccProfile(); |
514 | if (iccProfile.isEmpty()) |
515 | return; |
516 | |
517 | const QByteArray iccSignature("ICC_PROFILE" , 12); |
518 | constexpr int maxIccMarkerSize = maxMarkerSize - (12 + 2); |
519 | int index = 0; |
520 | const int markers = (iccProfile.size() + (maxIccMarkerSize - 1)) / maxIccMarkerSize; |
521 | Q_ASSERT(markers < 256); |
522 | for (int marker = 1; marker <= markers; ++marker) { |
523 | const int len = qMin(iccProfile.size() - index, maxIccMarkerSize); |
524 | const QByteArray block = iccSignature |
525 | + QByteArray(1, char(marker)) + QByteArray(1, char(markers)) |
526 | + iccProfile.mid(index, len); |
527 | jpeg_write_marker(cinfo, JPEG_APP0 + 2, reinterpret_cast<const JOCTET *>(block.constData()), block.size()); |
528 | index += len; |
529 | } |
530 | } |
531 | |
532 | static bool do_write_jpeg_image(struct jpeg_compress_struct &cinfo, |
533 | JSAMPROW *row_pointer, |
534 | const QImage &image, |
535 | QIODevice *device, |
536 | int sourceQuality, |
537 | const QString &description, |
538 | bool optimize, |
539 | bool progressive) |
540 | { |
541 | bool success = false; |
542 | const QList<QRgb> cmap = image.colorTable(); |
543 | |
544 | if (image.format() == QImage::Format_Invalid || image.format() == QImage::Format_Alpha8) |
545 | return false; |
546 | |
547 | struct my_jpeg_destination_mgr *iod_dest = new my_jpeg_destination_mgr(device); |
548 | struct my_error_mgr jerr; |
549 | |
550 | cinfo.err = jpeg_std_error(&jerr); |
551 | jerr.error_exit = my_error_exit; |
552 | jerr.output_message = my_output_message; |
553 | |
554 | if (!setjmp(jerr.setjmp_buffer)) { |
555 | // WARNING: |
556 | // this if loop is inside a setjmp/longjmp branch |
557 | // do not create C++ temporaries here because the destructor may never be called |
558 | // if you allocate memory, make sure that you can free it (row_pointer[0]) |
559 | jpeg_create_compress(&cinfo); |
560 | |
561 | cinfo.dest = iod_dest; |
562 | |
563 | cinfo.image_width = image.width(); |
564 | cinfo.image_height = image.height(); |
565 | |
566 | bool gray = false; |
567 | switch (image.format()) { |
568 | case QImage::Format_Mono: |
569 | case QImage::Format_MonoLSB: |
570 | case QImage::Format_Indexed8: |
571 | gray = true; |
572 | for (int i = image.colorCount(); gray && i; i--) { |
573 | gray = gray & qIsGray(cmap[i-1]); |
574 | } |
575 | cinfo.input_components = gray ? 1 : 3; |
576 | cinfo.in_color_space = gray ? JCS_GRAYSCALE : JCS_RGB; |
577 | break; |
578 | case QImage::Format_Grayscale8: |
579 | gray = true; |
580 | cinfo.input_components = 1; |
581 | cinfo.in_color_space = JCS_GRAYSCALE; |
582 | break; |
583 | default: |
584 | cinfo.input_components = 3; |
585 | cinfo.in_color_space = JCS_RGB; |
586 | } |
587 | |
588 | jpeg_set_defaults(&cinfo); |
589 | |
590 | qreal diffInch = qAbs(image.dotsPerMeterX()*2.54/100. - qRound(image.dotsPerMeterX()*2.54/100.)) |
591 | + qAbs(image.dotsPerMeterY()*2.54/100. - qRound(image.dotsPerMeterY()*2.54/100.)); |
592 | qreal diffCm = (qAbs(image.dotsPerMeterX()/100. - qRound(image.dotsPerMeterX()/100.)) |
593 | + qAbs(image.dotsPerMeterY()/100. - qRound(image.dotsPerMeterY()/100.)))*2.54; |
594 | if (diffInch < diffCm) { |
595 | cinfo.density_unit = 1; // dots/inch |
596 | cinfo.X_density = qRound(image.dotsPerMeterX()*2.54/100.); |
597 | cinfo.Y_density = qRound(image.dotsPerMeterY()*2.54/100.); |
598 | } else { |
599 | cinfo.density_unit = 2; // dots/cm |
600 | cinfo.X_density = (image.dotsPerMeterX()+50) / 100; |
601 | cinfo.Y_density = (image.dotsPerMeterY()+50) / 100; |
602 | } |
603 | |
604 | if (optimize) |
605 | cinfo.optimize_coding = true; |
606 | |
607 | if (progressive) |
608 | jpeg_simple_progression(&cinfo); |
609 | |
610 | int quality = sourceQuality >= 0 ? qMin(int(sourceQuality),100) : 75; |
611 | jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */); |
612 | jpeg_start_compress(&cinfo, TRUE); |
613 | |
614 | set_text(image, &cinfo, description); |
615 | if (cinfo.in_color_space == JCS_RGB) |
616 | write_icc_profile(image, &cinfo); |
617 | |
618 | row_pointer[0] = new uchar[cinfo.image_width*cinfo.input_components]; |
619 | int w = cinfo.image_width; |
620 | while (cinfo.next_scanline < cinfo.image_height) { |
621 | uchar *row = row_pointer[0]; |
622 | switch (image.format()) { |
623 | case QImage::Format_Mono: |
624 | case QImage::Format_MonoLSB: |
625 | if (gray) { |
626 | const uchar* data = image.constScanLine(cinfo.next_scanline); |
627 | if (image.format() == QImage::Format_MonoLSB) { |
628 | for (int i=0; i<w; i++) { |
629 | bool bit = !!(*(data + (i >> 3)) & (1 << (i & 7))); |
630 | row[i] = qRed(cmap[bit]); |
631 | } |
632 | } else { |
633 | for (int i=0; i<w; i++) { |
634 | bool bit = !!(*(data + (i >> 3)) & (1 << (7 -(i & 7)))); |
635 | row[i] = qRed(cmap[bit]); |
636 | } |
637 | } |
638 | } else { |
639 | const uchar* data = image.constScanLine(cinfo.next_scanline); |
640 | if (image.format() == QImage::Format_MonoLSB) { |
641 | for (int i=0; i<w; i++) { |
642 | bool bit = !!(*(data + (i >> 3)) & (1 << (i & 7))); |
643 | *row++ = qRed(cmap[bit]); |
644 | *row++ = qGreen(cmap[bit]); |
645 | *row++ = qBlue(cmap[bit]); |
646 | } |
647 | } else { |
648 | for (int i=0; i<w; i++) { |
649 | bool bit = !!(*(data + (i >> 3)) & (1 << (7 -(i & 7)))); |
650 | *row++ = qRed(cmap[bit]); |
651 | *row++ = qGreen(cmap[bit]); |
652 | *row++ = qBlue(cmap[bit]); |
653 | } |
654 | } |
655 | } |
656 | break; |
657 | case QImage::Format_Indexed8: |
658 | if (gray) { |
659 | const uchar* pix = image.constScanLine(cinfo.next_scanline); |
660 | for (int i=0; i<w; i++) { |
661 | *row = qRed(cmap[*pix]); |
662 | ++row; ++pix; |
663 | } |
664 | } else { |
665 | const uchar* pix = image.constScanLine(cinfo.next_scanline); |
666 | for (int i=0; i<w; i++) { |
667 | *row++ = qRed(cmap[*pix]); |
668 | *row++ = qGreen(cmap[*pix]); |
669 | *row++ = qBlue(cmap[*pix]); |
670 | ++pix; |
671 | } |
672 | } |
673 | break; |
674 | case QImage::Format_Grayscale8: |
675 | memcpy(row, image.constScanLine(cinfo.next_scanline), w); |
676 | break; |
677 | case QImage::Format_RGB888: |
678 | memcpy(row, image.constScanLine(cinfo.next_scanline), w * 3); |
679 | break; |
680 | case QImage::Format_RGB32: |
681 | case QImage::Format_ARGB32: |
682 | case QImage::Format_ARGB32_Premultiplied: |
683 | { |
684 | const QRgb* rgb = (const QRgb*)image.constScanLine(cinfo.next_scanline); |
685 | for (int i=0; i<w; i++) { |
686 | *row++ = qRed(*rgb); |
687 | *row++ = qGreen(*rgb); |
688 | *row++ = qBlue(*rgb); |
689 | ++rgb; |
690 | } |
691 | } |
692 | break; |
693 | default: |
694 | { |
695 | // (Testing shows that this way is actually faster than converting to RGB888 + memcpy) |
696 | QImage rowImg = image.copy(0, cinfo.next_scanline, w, 1).convertToFormat(QImage::Format_RGB32); |
697 | const QRgb* rgb = (const QRgb*)rowImg.constScanLine(0); |
698 | for (int i=0; i<w; i++) { |
699 | *row++ = qRed(*rgb); |
700 | *row++ = qGreen(*rgb); |
701 | *row++ = qBlue(*rgb); |
702 | ++rgb; |
703 | } |
704 | } |
705 | break; |
706 | } |
707 | jpeg_write_scanlines(&cinfo, row_pointer, 1); |
708 | } |
709 | |
710 | jpeg_finish_compress(&cinfo); |
711 | jpeg_destroy_compress(&cinfo); |
712 | success = true; |
713 | } else { |
714 | jpeg_destroy_compress(&cinfo); |
715 | success = false; |
716 | } |
717 | |
718 | delete iod_dest; |
719 | return success; |
720 | } |
721 | |
722 | static bool write_jpeg_image(const QImage &image, |
723 | QIODevice *device, |
724 | int sourceQuality, |
725 | const QString &description, |
726 | bool optimize, |
727 | bool progressive) |
728 | { |
729 | // protect these objects from the setjmp/longjmp pair inside |
730 | // do_write_jpeg_image (by making them non-local). |
731 | struct jpeg_compress_struct cinfo; |
732 | JSAMPROW row_pointer[1]; |
733 | row_pointer[0] = nullptr; |
734 | |
735 | const bool success = do_write_jpeg_image(cinfo, row_pointer, |
736 | image, device, |
737 | sourceQuality, description, |
738 | optimize, progressive); |
739 | |
740 | delete [] row_pointer[0]; |
741 | return success; |
742 | } |
743 | |
744 | class QJpegHandlerPrivate |
745 | { |
746 | public: |
747 | enum State { |
748 | Ready, |
749 | ReadHeader, |
750 | ReadingEnd, |
751 | Error |
752 | }; |
753 | |
754 | QJpegHandlerPrivate(QJpegHandler *qq) |
755 | : quality(75), transformation(QImageIOHandler::TransformationNone), iod_src(nullptr), |
756 | rgb888ToRgb32ConverterPtr(qt_convert_rgb888_to_rgb32), state(Ready), optimize(false), progressive(false), q(qq) |
757 | {} |
758 | |
759 | ~QJpegHandlerPrivate() |
760 | { |
761 | if(iod_src) |
762 | { |
763 | jpeg_destroy_decompress(&info); |
764 | delete iod_src; |
765 | iod_src = nullptr; |
766 | } |
767 | } |
768 | |
769 | bool readJpegHeader(QIODevice*); |
770 | bool read(QImage *image); |
771 | |
772 | int quality; |
773 | QImageIOHandler::Transformations transformation; |
774 | QVariant size; |
775 | QImage::Format format; |
776 | QSize scaledSize; |
777 | QRect scaledClipRect; |
778 | QRect clipRect; |
779 | QString description; |
780 | QStringList readTexts; |
781 | QByteArray iccProfile; |
782 | |
783 | struct jpeg_decompress_struct info; |
784 | struct my_jpeg_source_mgr * iod_src; |
785 | struct my_error_mgr err; |
786 | |
787 | Rgb888ToRgb32Converter rgb888ToRgb32ConverterPtr; |
788 | |
789 | State state; |
790 | |
791 | bool optimize; |
792 | bool progressive; |
793 | |
794 | QJpegHandler *q; |
795 | }; |
796 | |
797 | static bool (QDataStream &stream) |
798 | { |
799 | char prefix[6]; |
800 | if (stream.readRawData(prefix, sizeof(prefix)) != sizeof(prefix)) |
801 | return false; |
802 | static const char exifMagic[6] = {'E', 'x', 'i', 'f', 0, 0}; |
803 | return memcmp(prefix, exifMagic, 6) == 0; |
804 | } |
805 | |
806 | /* |
807 | * Returns -1 on error |
808 | * Returns 0 if no Exif orientation was found |
809 | * Returns 1 orientation is horizontal (normal) |
810 | * Returns 2 mirror horizontal |
811 | * Returns 3 rotate 180 |
812 | * Returns 4 mirror vertical |
813 | * Returns 5 mirror horizontal and rotate 270 CCW |
814 | * Returns 6 rotate 90 CW |
815 | * Returns 7 mirror horizontal and rotate 90 CW |
816 | * Returns 8 rotate 270 CW |
817 | */ |
818 | static int getExifOrientation(QByteArray &exifData) |
819 | { |
820 | // Current EXIF version (2.3) says there can be at most 5 IFDs, |
821 | // byte we allow for 10 so we're able to deal with future extensions. |
822 | const int maxIfdCount = 10; |
823 | |
824 | QDataStream stream(&exifData, QIODevice::ReadOnly); |
825 | |
826 | if (!readExifHeader(stream)) |
827 | return -1; |
828 | |
829 | quint16 val; |
830 | quint32 offset; |
831 | const qint64 = 6; // the EXIF header has a constant size |
832 | Q_ASSERT(headerStart == stream.device()->pos()); |
833 | |
834 | // read byte order marker |
835 | stream >> val; |
836 | if (val == 0x4949) // 'II' == Intel |
837 | stream.setByteOrder(QDataStream::LittleEndian); |
838 | else if (val == 0x4d4d) // 'MM' == Motorola |
839 | stream.setByteOrder(QDataStream::BigEndian); |
840 | else |
841 | return -1; // unknown byte order |
842 | |
843 | // confirm byte order |
844 | stream >> val; |
845 | if (val != 0x2a) |
846 | return -1; |
847 | |
848 | stream >> offset; |
849 | |
850 | // read IFD |
851 | for (int n = 0; n < maxIfdCount; ++n) { |
852 | quint16 numEntries; |
853 | |
854 | const qint64 bytesToSkip = offset - (stream.device()->pos() - headerStart); |
855 | if (bytesToSkip < 0 || (offset + headerStart >= exifData.size())) { |
856 | // disallow going backwards, though it's permitted in the spec |
857 | return -1; |
858 | } else if (bytesToSkip != 0) { |
859 | // seek to the IFD |
860 | if (!stream.device()->seek(offset + headerStart)) |
861 | return -1; |
862 | } |
863 | |
864 | stream >> numEntries; |
865 | |
866 | for (; numEntries > 0 && stream.status() == QDataStream::Ok; --numEntries) { |
867 | quint16 tag; |
868 | quint16 type; |
869 | quint32 components; |
870 | quint16 value; |
871 | quint16 dummy; |
872 | |
873 | stream >> tag >> type >> components >> value >> dummy; |
874 | if (tag == 0x0112) { // Tag Exif.Image.Orientation |
875 | if (components != 1) |
876 | return -1; |
877 | if (type != 3) // we are expecting it to be an unsigned short |
878 | return -1; |
879 | if (value < 1 || value > 8) // check for valid range |
880 | return -1; |
881 | |
882 | // It is possible to include the orientation multiple times. |
883 | // Right now the first value is returned. |
884 | return value; |
885 | } |
886 | } |
887 | |
888 | // read offset to next IFD |
889 | stream >> offset; |
890 | if (stream.status() != QDataStream::Ok) |
891 | return -1; |
892 | if (offset == 0) // this is the last IFD |
893 | return 0; // No Exif orientation was found |
894 | } |
895 | |
896 | // too many IFDs |
897 | return -1; |
898 | } |
899 | |
900 | static QImageIOHandler::Transformations exif2Qt(int exifOrientation) |
901 | { |
902 | switch (exifOrientation) { |
903 | case 1: // normal |
904 | return QImageIOHandler::TransformationNone; |
905 | case 2: // mirror horizontal |
906 | return QImageIOHandler::TransformationMirror; |
907 | case 3: // rotate 180 |
908 | return QImageIOHandler::TransformationRotate180; |
909 | case 4: // mirror vertical |
910 | return QImageIOHandler::TransformationFlip; |
911 | case 5: // mirror horizontal and rotate 270 CW |
912 | return QImageIOHandler::TransformationFlipAndRotate90; |
913 | case 6: // rotate 90 CW |
914 | return QImageIOHandler::TransformationRotate90; |
915 | case 7: // mirror horizontal and rotate 90 CW |
916 | return QImageIOHandler::TransformationMirrorAndRotate90; |
917 | case 8: // rotate 270 CW |
918 | return QImageIOHandler::TransformationRotate270; |
919 | } |
920 | qCWarning(lcJpeg, "Invalid EXIF orientation" ); |
921 | return QImageIOHandler::TransformationNone; |
922 | } |
923 | |
924 | /*! |
925 | \internal |
926 | */ |
927 | bool QJpegHandlerPrivate::readJpegHeader(QIODevice *device) |
928 | { |
929 | if(state == Ready) |
930 | { |
931 | state = Error; |
932 | iod_src = new my_jpeg_source_mgr(device); |
933 | |
934 | info.err = jpeg_std_error(&err); |
935 | err.error_exit = my_error_exit; |
936 | err.output_message = my_output_message; |
937 | |
938 | jpeg_create_decompress(&info); |
939 | info.src = iod_src; |
940 | |
941 | if (!setjmp(err.setjmp_buffer)) { |
942 | jpeg_save_markers(&info, JPEG_COM, 0xFFFF); |
943 | jpeg_save_markers(&info, JPEG_APP0 + 1, 0xFFFF); // Exif uses APP1 marker |
944 | jpeg_save_markers(&info, JPEG_APP0 + 2, 0xFFFF); // ICC uses APP2 marker |
945 | |
946 | (void) jpeg_read_header(&info, TRUE); |
947 | |
948 | int width = 0; |
949 | int height = 0; |
950 | read_jpeg_size(width, height, &info); |
951 | size = QSize(width, height); |
952 | |
953 | format = QImage::Format_Invalid; |
954 | read_jpeg_format(format, &info); |
955 | |
956 | QByteArray exifData; |
957 | |
958 | for (jpeg_saved_marker_ptr marker = info.marker_list; marker != nullptr; marker = marker->next) { |
959 | if (marker->marker == JPEG_COM) { |
960 | #ifndef QT_NO_IMAGEIO_TEXT_LOADING |
961 | QString key, value; |
962 | QString s = QString::fromUtf8((const char *)marker->data, marker->data_length); |
963 | int index = s.indexOf(QLatin1String(": " )); |
964 | if (index == -1 || s.indexOf(QLatin1Char(' ')) < index) { |
965 | key = QLatin1String("Description" ); |
966 | value = s; |
967 | } else { |
968 | key = s.left(index); |
969 | value = s.mid(index + 2); |
970 | } |
971 | if (!description.isEmpty()) |
972 | description += QLatin1String("\n\n" ); |
973 | description += key + QLatin1String(": " ) + value.simplified(); |
974 | readTexts.append(key); |
975 | readTexts.append(value); |
976 | #endif |
977 | } else if (marker->marker == JPEG_APP0 + 1) { |
978 | exifData.append((const char*)marker->data, marker->data_length); |
979 | } else if (marker->marker == JPEG_APP0 + 2) { |
980 | if (marker->data_length > 128 + 4 + 14 && strcmp((const char *)marker->data, "ICC_PROFILE" ) == 0) { |
981 | iccProfile.append((const char*)marker->data + 14, marker->data_length - 14); |
982 | } |
983 | } |
984 | } |
985 | |
986 | if (!exifData.isEmpty()) { |
987 | // Exif data present |
988 | int exifOrientation = getExifOrientation(exifData); |
989 | if (exifOrientation > 0) |
990 | transformation = exif2Qt(exifOrientation); |
991 | } |
992 | |
993 | state = ReadHeader; |
994 | return true; |
995 | } |
996 | else |
997 | { |
998 | return false; |
999 | } |
1000 | } |
1001 | else if(state == Error) |
1002 | return false; |
1003 | return true; |
1004 | } |
1005 | |
1006 | bool QJpegHandlerPrivate::read(QImage *image) |
1007 | { |
1008 | if(state == Ready) |
1009 | readJpegHeader(q->device()); |
1010 | |
1011 | if(state == ReadHeader) |
1012 | { |
1013 | bool success = read_jpeg_image(image, scaledSize, scaledClipRect, clipRect, quality, rgb888ToRgb32ConverterPtr, &info, &err); |
1014 | if (success) { |
1015 | for (int i = 0; i < readTexts.size()-1; i+=2) |
1016 | image->setText(readTexts.at(i), readTexts.at(i+1)); |
1017 | |
1018 | if (!iccProfile.isEmpty()) |
1019 | image->setColorSpace(QColorSpace::fromIccProfile(iccProfile)); |
1020 | |
1021 | state = ReadingEnd; |
1022 | return true; |
1023 | } |
1024 | |
1025 | state = Error; |
1026 | } |
1027 | |
1028 | return false; |
1029 | } |
1030 | |
1031 | Q_GUI_EXPORT void QT_FASTCALL qt_convert_rgb888_to_rgb32_neon(quint32 *dst, const uchar *src, int len); |
1032 | Q_GUI_EXPORT void QT_FASTCALL qt_convert_rgb888_to_rgb32_ssse3(quint32 *dst, const uchar *src, int len); |
1033 | extern "C" void qt_convert_rgb888_to_rgb32_mips_dspr2_asm(quint32 *dst, const uchar *src, int len); |
1034 | |
1035 | QJpegHandler::QJpegHandler() |
1036 | : d(new QJpegHandlerPrivate(this)) |
1037 | { |
1038 | #if defined(__ARM_NEON__) |
1039 | // from qimage_neon.cpp |
1040 | if (qCpuHasFeature(NEON)) |
1041 | d->rgb888ToRgb32ConverterPtr = qt_convert_rgb888_to_rgb32_neon; |
1042 | #endif |
1043 | |
1044 | #if defined(QT_COMPILER_SUPPORTS_SSSE3) |
1045 | // from qimage_ssse3.cpps |
1046 | if (qCpuHasFeature(SSSE3)) { |
1047 | d->rgb888ToRgb32ConverterPtr = qt_convert_rgb888_to_rgb32_ssse3; |
1048 | } |
1049 | #endif // QT_COMPILER_SUPPORTS_SSSE3 |
1050 | #if defined(QT_COMPILER_SUPPORTS_MIPS_DSPR2) |
1051 | if (qCpuHasFeature(DSPR2)) { |
1052 | d->rgb888ToRgb32ConverterPtr = qt_convert_rgb888_to_rgb32_mips_dspr2_asm; |
1053 | } |
1054 | #endif // QT_COMPILER_SUPPORTS_DSPR2 |
1055 | } |
1056 | |
1057 | QJpegHandler::~QJpegHandler() |
1058 | { |
1059 | delete d; |
1060 | } |
1061 | |
1062 | bool QJpegHandler::canRead() const |
1063 | { |
1064 | if(d->state == QJpegHandlerPrivate::Ready && !canRead(device())) |
1065 | return false; |
1066 | |
1067 | if (d->state != QJpegHandlerPrivate::Error && d->state != QJpegHandlerPrivate::ReadingEnd) { |
1068 | setFormat("jpeg" ); |
1069 | return true; |
1070 | } |
1071 | |
1072 | return false; |
1073 | } |
1074 | |
1075 | bool QJpegHandler::canRead(QIODevice *device) |
1076 | { |
1077 | if (!device) { |
1078 | qCWarning(lcJpeg, "QJpegHandler::canRead() called with no device" ); |
1079 | return false; |
1080 | } |
1081 | |
1082 | char buffer[2]; |
1083 | if (device->peek(buffer, 2) != 2) |
1084 | return false; |
1085 | return uchar(buffer[0]) == 0xff && uchar(buffer[1]) == 0xd8; |
1086 | } |
1087 | |
1088 | bool QJpegHandler::read(QImage *image) |
1089 | { |
1090 | if (!canRead()) |
1091 | return false; |
1092 | return d->read(image); |
1093 | } |
1094 | |
1095 | extern void qt_imageTransform(QImage &src, QImageIOHandler::Transformations orient); |
1096 | |
1097 | bool QJpegHandler::write(const QImage &image) |
1098 | { |
1099 | if (d->transformation != QImageIOHandler::TransformationNone) { |
1100 | // We don't support writing EXIF headers so apply the transform to the data. |
1101 | QImage img = image; |
1102 | qt_imageTransform(img, d->transformation); |
1103 | return write_jpeg_image(img, device(), d->quality, d->description, d->optimize, d->progressive); |
1104 | } |
1105 | return write_jpeg_image(image, device(), d->quality, d->description, d->optimize, d->progressive); |
1106 | } |
1107 | |
1108 | bool QJpegHandler::supportsOption(ImageOption option) const |
1109 | { |
1110 | return option == Quality |
1111 | || option == ScaledSize |
1112 | || option == ScaledClipRect |
1113 | || option == ClipRect |
1114 | || option == Description |
1115 | || option == Size |
1116 | || option == ImageFormat |
1117 | || option == OptimizedWrite |
1118 | || option == ProgressiveScanWrite |
1119 | || option == ImageTransformation; |
1120 | } |
1121 | |
1122 | QVariant QJpegHandler::option(ImageOption option) const |
1123 | { |
1124 | switch(option) { |
1125 | case Quality: |
1126 | return d->quality; |
1127 | case ScaledSize: |
1128 | return d->scaledSize; |
1129 | case ScaledClipRect: |
1130 | return d->scaledClipRect; |
1131 | case ClipRect: |
1132 | return d->clipRect; |
1133 | case Description: |
1134 | d->readJpegHeader(device()); |
1135 | return d->description; |
1136 | case Size: |
1137 | d->readJpegHeader(device()); |
1138 | return d->size; |
1139 | case ImageFormat: |
1140 | d->readJpegHeader(device()); |
1141 | return d->format; |
1142 | case OptimizedWrite: |
1143 | return d->optimize; |
1144 | case ProgressiveScanWrite: |
1145 | return d->progressive; |
1146 | case ImageTransformation: |
1147 | d->readJpegHeader(device()); |
1148 | return int(d->transformation); |
1149 | default: |
1150 | break; |
1151 | } |
1152 | |
1153 | return QVariant(); |
1154 | } |
1155 | |
1156 | void QJpegHandler::setOption(ImageOption option, const QVariant &value) |
1157 | { |
1158 | switch(option) { |
1159 | case Quality: |
1160 | d->quality = value.toInt(); |
1161 | break; |
1162 | case ScaledSize: |
1163 | d->scaledSize = value.toSize(); |
1164 | break; |
1165 | case ScaledClipRect: |
1166 | d->scaledClipRect = value.toRect(); |
1167 | break; |
1168 | case ClipRect: |
1169 | d->clipRect = value.toRect(); |
1170 | break; |
1171 | case Description: |
1172 | d->description = value.toString(); |
1173 | break; |
1174 | case OptimizedWrite: |
1175 | d->optimize = value.toBool(); |
1176 | break; |
1177 | case ProgressiveScanWrite: |
1178 | d->progressive = value.toBool(); |
1179 | break; |
1180 | case ImageTransformation: { |
1181 | int transformation = value.toInt(); |
1182 | if (transformation > 0 && transformation < 8) |
1183 | d->transformation = QImageIOHandler::Transformations(transformation); |
1184 | } |
1185 | default: |
1186 | break; |
1187 | } |
1188 | } |
1189 | |
1190 | QT_END_NAMESPACE |
1191 | |