KImageFormats

jxl.cpp
1/*
2 JPEG XL (JXL) support for QImage.
3
4 SPDX-FileCopyrightText: 2021 Daniel Novomesky <dnovomesky@gmail.com>
5
6 SPDX-License-Identifier: BSD-2-Clause
7*/
8
9#include <QThread>
10#include <QtGlobal>
11
12#include "jxl_p.h"
13#include "microexif_p.h"
14#include "util_p.h"
15
16#include <jxl/encode.h>
17#include <jxl/thread_parallel_runner.h>
18
19#if JPEGXL_NUMERIC_VERSION >= JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
20#include <jxl/cms.h>
21#endif
22
23#include <string.h>
24
25// Avoid rotation on buggy Qts (see also https://bugreports.qt.io/browse/QTBUG-126575)
26#if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 7) && QT_VERSION < QT_VERSION_CHECK(6, 6, 0)) || (QT_VERSION >= QT_VERSION_CHECK(6, 7, 3))
27#ifndef JXL_QT_AUTOTRANSFORM
28#define JXL_QT_AUTOTRANSFORM
29#endif
30#endif
31
32#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
33#ifndef JXL_HDR_PRESERVATION_DISABLED
34// Define JXL_HDR_PRESERVATION_DISABLED to disable HDR preservation
35// (HDR images are saved as UINT16).
36#define JXL_HDR_PRESERVATION_DISABLED
37#endif
38#endif
39
40#ifndef JXL_DECODE_BOXES_DISABLED
41// Decode Boxes in order to read optional metadata (XMP, Exif, etc...).
42// Define JXL_DECODE_BOXES_DISABLED to disable Boxes decoding.
43// #define JXL_DECODE_BOXES_DISABLED
44#endif
45
46#define FEATURE_LEVEL_5_WIDTH 262144
47#define FEATURE_LEVEL_5_HEIGHT 262144
48#define FEATURE_LEVEL_5_PIXELS 268435456
49
50#if QT_POINTER_SIZE < 8
51#define MAX_IMAGE_WIDTH 32767
52#define MAX_IMAGE_HEIGHT 32767
53#define MAX_IMAGE_PIXELS FEATURE_LEVEL_5_PIXELS
54#else // JXL code stream level 5
55#define MAX_IMAGE_WIDTH FEATURE_LEVEL_5_WIDTH
56#define MAX_IMAGE_HEIGHT FEATURE_LEVEL_5_HEIGHT
57#define MAX_IMAGE_PIXELS FEATURE_LEVEL_5_PIXELS
58#endif
59
60QJpegXLHandler::QJpegXLHandler()
61 : m_parseState(ParseJpegXLNotParsed)
62 , m_quality(90)
63 , m_currentimage_index(0)
64 , m_previousimage_index(-1)
65 , m_transformations(QImageIOHandler::TransformationNone)
66 , m_decoder(nullptr)
67 , m_runner(nullptr)
68 , m_next_image_delay(0)
69 , m_input_image_format(QImage::Format_Invalid)
70 , m_target_image_format(QImage::Format_Invalid)
71 , m_buffer_size(0)
72{
73}
74
75QJpegXLHandler::~QJpegXLHandler()
76{
77 if (m_runner) {
78 JxlThreadParallelRunnerDestroy(m_runner);
79 }
80 if (m_decoder) {
81 JxlDecoderDestroy(m_decoder);
82 }
83}
84
85bool QJpegXLHandler::canRead() const
86{
87 if (m_parseState == ParseJpegXLNotParsed && !canRead(device())) {
88 return false;
89 }
90
91 if (m_parseState != ParseJpegXLError) {
92 setFormat("jxl");
93
94 if (m_parseState == ParseJpegXLFinished) {
95 return false;
96 }
97
98 return true;
99 }
100 return false;
101}
102
103bool QJpegXLHandler::canRead(QIODevice *device)
104{
105 if (!device) {
106 return false;
107 }
108 QByteArray header = device->peek(32);
109 if (header.size() < 12) {
110 return false;
111 }
112
113 JxlSignature signature = JxlSignatureCheck(reinterpret_cast<const uint8_t *>(header.constData()), header.size());
114 if (signature == JXL_SIG_CODESTREAM || signature == JXL_SIG_CONTAINER) {
115 return true;
116 }
117 return false;
118}
119
120bool QJpegXLHandler::ensureParsed() const
121{
122 if (m_parseState == ParseJpegXLSuccess || m_parseState == ParseJpegXLBasicInfoParsed || m_parseState == ParseJpegXLFinished) {
123 return true;
124 }
125 if (m_parseState == ParseJpegXLError) {
126 return false;
127 }
128
129 QJpegXLHandler *that = const_cast<QJpegXLHandler *>(this);
130
131 return that->ensureDecoder();
132}
133
134bool QJpegXLHandler::ensureALLCounted() const
135{
136 if (!ensureParsed()) {
137 return false;
138 }
139
140 if (m_parseState == ParseJpegXLSuccess || m_parseState == ParseJpegXLFinished) {
141 return true;
142 }
143
144 QJpegXLHandler *that = const_cast<QJpegXLHandler *>(this);
145
146 return that->countALLFrames();
147}
148
149bool QJpegXLHandler::ensureDecoder()
150{
151 if (m_decoder) {
152 return true;
153 }
154
155 m_rawData = device()->readAll();
156
157 if (m_rawData.isEmpty()) {
158 return false;
159 }
160
161 JxlSignature signature = JxlSignatureCheck(reinterpret_cast<const uint8_t *>(m_rawData.constData()), m_rawData.size());
162 if (signature != JXL_SIG_CODESTREAM && signature != JXL_SIG_CONTAINER) {
163 m_parseState = ParseJpegXLError;
164 return false;
165 }
166
167 m_decoder = JxlDecoderCreate(nullptr);
168 if (!m_decoder) {
169 qWarning("ERROR: JxlDecoderCreate failed");
170 m_parseState = ParseJpegXLError;
171 return false;
172 }
173
174#ifdef JXL_QT_AUTOTRANSFORM
175 // Let Qt handle the orientation.
176 JxlDecoderSetKeepOrientation(m_decoder, true);
177#endif
178
179 int num_worker_threads = QThread::idealThreadCount();
180 if (!m_runner && num_worker_threads >= 4) {
181 /* use half of the threads because plug-in is usually used in environment
182 * where application performs another tasks in backround (pre-load other images) */
183 num_worker_threads = num_worker_threads / 2;
184 num_worker_threads = qBound(2, num_worker_threads, 64);
185 m_runner = JxlThreadParallelRunnerCreate(nullptr, num_worker_threads);
186
187 if (JxlDecoderSetParallelRunner(m_decoder, JxlThreadParallelRunner, m_runner) != JXL_DEC_SUCCESS) {
188 qWarning("ERROR: JxlDecoderSetParallelRunner failed");
189 m_parseState = ParseJpegXLError;
190 return false;
191 }
192 }
193
194 if (JxlDecoderSetInput(m_decoder, reinterpret_cast<const uint8_t *>(m_rawData.constData()), m_rawData.size()) != JXL_DEC_SUCCESS) {
195 qWarning("ERROR: JxlDecoderSetInput failed");
196 m_parseState = ParseJpegXLError;
197 return false;
198 }
199
200 JxlDecoderCloseInput(m_decoder);
201
202 JxlDecoderStatus status = JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME);
203 if (status == JXL_DEC_ERROR) {
204 qWarning("ERROR: JxlDecoderSubscribeEvents failed");
205 m_parseState = ParseJpegXLError;
206 return false;
207 }
208
209 status = JxlDecoderProcessInput(m_decoder);
210 if (status == JXL_DEC_ERROR) {
211 qWarning("ERROR: JXL decoding failed");
212 m_parseState = ParseJpegXLError;
213 return false;
214 }
215 if (status == JXL_DEC_NEED_MORE_INPUT) {
216 qWarning("ERROR: JXL data incomplete");
217 m_parseState = ParseJpegXLError;
218 return false;
219 }
220
221 status = JxlDecoderGetBasicInfo(m_decoder, &m_basicinfo);
222 if (status != JXL_DEC_SUCCESS) {
223 qWarning("ERROR: JXL basic info not available");
224 m_parseState = ParseJpegXLError;
225 return false;
226 }
227
228 if (m_basicinfo.xsize == 0 || m_basicinfo.ysize == 0) {
229 qWarning("ERROR: JXL image has zero dimensions");
230 m_parseState = ParseJpegXLError;
231 return false;
232 }
233
234 if (m_basicinfo.xsize > MAX_IMAGE_WIDTH || m_basicinfo.ysize > MAX_IMAGE_HEIGHT) {
235 qWarning("JXL image (%dx%d) is too large", m_basicinfo.xsize, m_basicinfo.ysize);
236 m_parseState = ParseJpegXLError;
237 return false;
238 }
239
240 m_parseState = ParseJpegXLBasicInfoParsed;
241 return true;
242}
243
244bool QJpegXLHandler::countALLFrames()
245{
246 if (m_parseState != ParseJpegXLBasicInfoParsed) {
247 return false;
248 }
249
250 JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder);
251 if (status != JXL_DEC_COLOR_ENCODING) {
252 qWarning("Unexpected event %d instead of JXL_DEC_COLOR_ENCODING", status);
253 m_parseState = ParseJpegXLError;
254 return false;
255 }
256
257 bool is_gray = m_basicinfo.num_color_channels == 1 && m_basicinfo.alpha_bits == 0;
258 JxlColorEncoding color_encoding;
259 if (m_basicinfo.uses_original_profile == JXL_FALSE && m_basicinfo.have_animation == JXL_FALSE) {
260#if JPEGXL_NUMERIC_VERSION >= JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
261 const JxlCmsInterface *jxlcms = JxlGetDefaultCms();
262 if (jxlcms) {
263 status = JxlDecoderSetCms(m_decoder, *jxlcms);
264 if (status != JXL_DEC_SUCCESS) {
265 qWarning("JxlDecoderSetCms ERROR");
266 }
267 } else {
268 qWarning("No JPEG XL CMS Interface");
269 }
270#endif
271 JxlColorEncodingSetToSRGB(&color_encoding, is_gray ? JXL_TRUE : JXL_FALSE);
272 JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding);
273 }
274
275 bool loadalpha = false;
276 if (m_basicinfo.alpha_bits > 0) {
277 loadalpha = true;
278 }
279
280 m_input_pixel_format.endianness = JXL_NATIVE_ENDIAN;
281 m_input_pixel_format.align = 4;
282 m_input_pixel_format.num_channels = is_gray ? 1 : 4;
283
284 if (m_basicinfo.bits_per_sample > 8) { // high bit depth
285#ifdef JXL_HDR_PRESERVATION_DISABLED
286 bool is_fp = false;
287#else
288 bool is_fp = m_basicinfo.exponent_bits_per_sample > 0 && m_basicinfo.num_color_channels == 3;
289#endif
290
291 if (is_gray) {
292 m_input_pixel_format.data_type = JXL_TYPE_UINT16;
293 m_input_image_format = m_target_image_format = QImage::Format_Grayscale16;
294 m_buffer_size = ((size_t)m_basicinfo.ysize - 1) * (((((size_t)m_basicinfo.xsize) * 2 + 3) >> 2) << 2) + (size_t)m_basicinfo.xsize * 2;
295 } else if (m_basicinfo.bits_per_sample > 16 && is_fp) {
296 m_input_pixel_format.data_type = JXL_TYPE_FLOAT;
297 m_input_image_format = QImage::Format_RGBA32FPx4;
298 m_buffer_size = (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize * m_input_pixel_format.num_channels * 4;
299 if (loadalpha)
300 m_target_image_format = QImage::Format_RGBA32FPx4;
301 else
302 m_target_image_format = QImage::Format_RGBX32FPx4;
303 } else {
304 m_input_pixel_format.data_type = is_fp ? JXL_TYPE_FLOAT16 : JXL_TYPE_UINT16;
305 m_buffer_size = (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize * m_input_pixel_format.num_channels * 2;
306 m_input_image_format = is_fp ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBA64;
307 if (loadalpha)
308 m_target_image_format = is_fp ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBA64;
309 else
310 m_target_image_format = is_fp ? QImage::Format_RGBX16FPx4 : QImage::Format_RGBX64;
311 }
312 } else { // 8bit depth
313 m_input_pixel_format.data_type = JXL_TYPE_UINT8;
314
315 if (is_gray) {
316 m_input_image_format = m_target_image_format = QImage::Format_Grayscale8;
317 m_buffer_size = ((size_t)m_basicinfo.ysize - 1) * (((((size_t)m_basicinfo.xsize) + 3) >> 2) << 2) + (size_t)m_basicinfo.xsize;
318 } else {
319 m_input_image_format = QImage::Format_RGBA8888;
320 m_buffer_size = (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize * m_input_pixel_format.num_channels;
321 if (loadalpha) {
322 m_target_image_format = QImage::Format_ARGB32;
323 } else {
324 m_target_image_format = QImage::Format_RGB32;
325 }
326 }
327 }
328
329 status = JxlDecoderGetColorAsEncodedProfile(m_decoder,
330#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
331 &m_input_pixel_format,
332#endif
333 JXL_COLOR_PROFILE_TARGET_DATA,
334 &color_encoding);
335
336 if (status == JXL_DEC_SUCCESS && color_encoding.color_space == JXL_COLOR_SPACE_RGB && color_encoding.white_point == JXL_WHITE_POINT_D65
337 && color_encoding.primaries == JXL_PRIMARIES_SRGB && color_encoding.transfer_function == JXL_TRANSFER_FUNCTION_SRGB) {
338 m_colorspace = QColorSpace(QColorSpace::SRgb);
339 } else {
340 size_t icc_size = 0;
341 if (JxlDecoderGetICCProfileSize(m_decoder,
342#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
343 &m_input_pixel_format,
344#endif
345 JXL_COLOR_PROFILE_TARGET_DATA,
346 &icc_size)
347 == JXL_DEC_SUCCESS) {
348 if (icc_size > 0) {
349 QByteArray icc_data(icc_size, 0);
350 if (JxlDecoderGetColorAsICCProfile(m_decoder,
351#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
352 &m_input_pixel_format,
353#endif
354 JXL_COLOR_PROFILE_TARGET_DATA,
355 reinterpret_cast<uint8_t *>(icc_data.data()),
356 icc_data.size())
357 == JXL_DEC_SUCCESS) {
358 m_colorspace = QColorSpace::fromIccProfile(icc_data);
359
360 if (!m_colorspace.isValid()) {
361 qWarning("JXL image has Qt-unsupported or invalid ICC profile!");
362 }
363 } else {
364 qWarning("Failed to obtain data from JPEG XL decoder");
365 }
366 } else {
367 qWarning("Empty ICC data");
368 }
369 } else {
370 qWarning("no ICC, other color profile");
371 }
372 }
373
374 if (m_basicinfo.have_animation) { // count all frames
375 JxlFrameHeader frame_header;
376 int delay;
377
378 for (status = JxlDecoderProcessInput(m_decoder); status != JXL_DEC_SUCCESS; status = JxlDecoderProcessInput(m_decoder)) {
379 if (status != JXL_DEC_FRAME) {
380 switch (status) {
381 case JXL_DEC_ERROR:
382 qWarning("ERROR: JXL decoding failed");
383 break;
384 case JXL_DEC_NEED_MORE_INPUT:
385 qWarning("ERROR: JXL data incomplete");
386 break;
387 default:
388 qWarning("Unexpected event %d instead of JXL_DEC_FRAME", status);
389 break;
390 }
391 m_parseState = ParseJpegXLError;
392 return false;
393 }
394
395 if (JxlDecoderGetFrameHeader(m_decoder, &frame_header) != JXL_DEC_SUCCESS) {
396 qWarning("ERROR: JxlDecoderGetFrameHeader failed");
397 m_parseState = ParseJpegXLError;
398 return false;
399 }
400
401 if (m_basicinfo.animation.tps_denominator > 0 && m_basicinfo.animation.tps_numerator > 0) {
402 delay = (int)(0.5 + 1000.0 * frame_header.duration * m_basicinfo.animation.tps_denominator / m_basicinfo.animation.tps_numerator);
403 } else {
404 delay = 0;
405 }
406
407 m_framedelays.append(delay);
408
409 if (frame_header.is_last == JXL_TRUE) {
410 break;
411 }
412 }
413
414 if (m_framedelays.isEmpty()) {
415 qWarning("no frames loaded by the JXL plug-in");
416 m_parseState = ParseJpegXLError;
417 return false;
418 }
419
420 if (m_framedelays.count() == 1) {
421 qWarning("JXL file was marked as animation but it has only one frame.");
422 m_basicinfo.have_animation = JXL_FALSE;
423 }
424 } else { // static picture
425 m_framedelays.resize(1);
426 m_framedelays[0] = 0;
427 }
428
429#ifndef JXL_DECODE_BOXES_DISABLED
430 if (!decodeContainer()) {
431 return false;
432 }
433#endif
434
435 if (!rewind()) {
436 return false;
437 }
438
439 m_next_image_delay = m_framedelays[0];
440 m_parseState = ParseJpegXLSuccess;
441 return true;
442}
443
444bool QJpegXLHandler::decode_one_frame()
445{
446 JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder);
447 if (status != JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
448 qWarning("Unexpected event %d instead of JXL_DEC_NEED_IMAGE_OUT_BUFFER", status);
449 m_parseState = ParseJpegXLError;
450 return false;
451 }
452
453 m_current_image = imageAlloc(m_basicinfo.xsize, m_basicinfo.ysize, m_input_image_format);
454 if (m_current_image.isNull()) {
455 qWarning("Memory cannot be allocated");
456 m_parseState = ParseJpegXLError;
457 return false;
458 }
459
460 m_current_image.setColorSpace(m_colorspace);
461 if (!m_xmp.isEmpty()) {
462 m_current_image.setText(QStringLiteral(META_KEY_XMP_ADOBE), QString::fromUtf8(m_xmp));
463 }
464
465 if (!m_exif.isEmpty()) {
466 auto exif = MicroExif::fromByteArray(m_exif);
467 // set image resolution
468 if (exif.horizontalResolution() > 0)
469 m_current_image.setDotsPerMeterX(qRound(exif.horizontalResolution() / 25.4 * 1000));
470 if (exif.verticalResolution() > 0)
471 m_current_image.setDotsPerMeterY(qRound(exif.verticalResolution() / 25.4 * 1000));
472 // set image metadata
473 exif.toImageMetadata(m_current_image);
474 }
475
476 if (JxlDecoderSetImageOutBuffer(m_decoder, &m_input_pixel_format, m_current_image.bits(), m_buffer_size) != JXL_DEC_SUCCESS) {
477 qWarning("ERROR: JxlDecoderSetImageOutBuffer failed");
478 m_parseState = ParseJpegXLError;
479 return false;
480 }
481
482 status = JxlDecoderProcessInput(m_decoder);
483 if (status != JXL_DEC_FULL_IMAGE) {
484 qWarning("Unexpected event %d instead of JXL_DEC_FULL_IMAGE", status);
485 m_parseState = ParseJpegXLError;
486 return false;
487 }
488
489 if (m_target_image_format != m_input_image_format) {
490 m_current_image.convertTo(m_target_image_format);
491 }
492
493 m_next_image_delay = m_framedelays[m_currentimage_index];
494 m_previousimage_index = m_currentimage_index;
495
496 if (m_framedelays.count() > 1) {
497 m_currentimage_index++;
498
499 if (m_currentimage_index >= m_framedelays.count()) {
500 if (!rewind()) {
501 return false;
502 }
503
504 // all frames in animation have been read
505 m_parseState = ParseJpegXLFinished;
506 } else {
507 m_parseState = ParseJpegXLSuccess;
508 }
509 } else {
510 // the static image has been read
511 m_parseState = ParseJpegXLFinished;
512 }
513
514 return true;
515}
516
517bool QJpegXLHandler::read(QImage *image)
518{
519 if (!ensureALLCounted()) {
520 return false;
521 }
522
523 if (m_currentimage_index == m_previousimage_index) {
524 *image = m_current_image;
525 return jumpToNextImage();
526 }
527
528 if (decode_one_frame()) {
529 *image = m_current_image;
530 return true;
531 } else {
532 return false;
533 }
534}
535
536template<class T>
537void packRGBPixels(QImage &img)
538{
539 // pack pixel data
540 auto dest_pixels = reinterpret_cast<T *>(img.bits());
541 for (qint32 y = 0; y < img.height(); y++) {
542 auto src_pixels = reinterpret_cast<const T *>(img.constScanLine(y));
543 for (qint32 x = 0; x < img.width(); x++) {
544 // R
545 *dest_pixels = *src_pixels;
546 dest_pixels++;
547 src_pixels++;
548 // G
549 *dest_pixels = *src_pixels;
550 dest_pixels++;
551 src_pixels++;
552 // B
553 *dest_pixels = *src_pixels;
554 dest_pixels++;
555 src_pixels += 2; // skipalpha
556 }
557 }
558}
559
560bool QJpegXLHandler::write(const QImage &image)
561{
562 if (image.format() == QImage::Format_Invalid) {
563 qWarning("No image data to save");
564 return false;
565 }
566
567 if ((image.width() == 0) || (image.height() == 0)) {
568 qWarning("Image has zero dimension!");
569 return false;
570 }
571
572 if ((image.width() > MAX_IMAGE_WIDTH) || (image.height() > MAX_IMAGE_HEIGHT)) {
573 qWarning("Image (%dx%d) is too large to save!", image.width(), image.height());
574 return false;
575 }
576
577 size_t pixel_count = size_t(image.width()) * image.height();
578 if (MAX_IMAGE_PIXELS && pixel_count > MAX_IMAGE_PIXELS) {
579 qWarning("Image (%dx%d) will not be saved because it has more than %d megapixels!", image.width(), image.height(), MAX_IMAGE_PIXELS / 1024 / 1024);
580 return false;
581 }
582
583 int save_depth = 8; // 8 / 16 / 32
584 bool save_fp = false;
585 bool is_gray = false;
586 // depth detection
587 switch (image.format()) {
591#ifndef JXL_HDR_PRESERVATION_DISABLED
592 save_depth = 32;
593 save_fp = true;
594 break;
595#endif
599#ifndef JXL_HDR_PRESERVATION_DISABLED
600 save_depth = 16;
601 save_fp = true;
602 break;
603#endif
611 save_depth = 16;
612 break;
620#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
621 case QImage::Format_CMYK8888:
622#endif
623 save_depth = 8;
624 break;
626 save_depth = 16;
627 is_gray = true;
628 break;
633 save_depth = 8;
634 is_gray = true;
635 break;
637 save_depth = 8;
638 is_gray = image.isGrayscale();
639 break;
640 default:
641 if (image.depth() > 32) {
642 save_depth = 16;
643 } else {
644 save_depth = 8;
645 }
646 break;
647 }
648
649 JxlEncoder *encoder = JxlEncoderCreate(nullptr);
650 if (!encoder) {
651 qWarning("Failed to create Jxl encoder");
652 return false;
653 }
654
655 if (m_quality > 100) {
656 m_quality = 100;
657 } else if (m_quality < 0) {
658 m_quality = 90;
659 }
660
661 JxlEncoderUseContainer(encoder, JXL_TRUE);
662 JxlEncoderUseBoxes(encoder);
663
664 JxlBasicInfo output_info;
665 JxlEncoderInitBasicInfo(&output_info);
666 output_info.have_container = JXL_TRUE;
667
668 QByteArray iccprofile;
669 QColorSpace tmpcs = image.colorSpace();
670 if (!tmpcs.isValid() || tmpcs.primaries() != QColorSpace::Primaries::SRgb || tmpcs.transferFunction() != QColorSpace::TransferFunction::SRgb
671 || m_quality == 100) {
672 // no profile or Qt-unsupported ICC profile
673 iccprofile = tmpcs.iccProfile();
674 // note: lossless encoding requires uses_original_profile = JXL_TRUE
675 if (iccprofile.size() > 0 || m_quality == 100 || is_gray) {
676 output_info.uses_original_profile = JXL_TRUE;
677 }
678 }
679
680 // clang-format off
681 if ( (save_depth > 8 && (image.hasAlphaChannel() || output_info.uses_original_profile))
682 || (save_depth > 16)
683 || (pixel_count > FEATURE_LEVEL_5_PIXELS)
684 || (image.width() > FEATURE_LEVEL_5_WIDTH)
685 || (image.height() > FEATURE_LEVEL_5_HEIGHT)) {
686 JxlEncoderSetCodestreamLevel(encoder, 10);
687 }
688 // clang-format on
689
690 void *runner = nullptr;
691 int num_worker_threads = qBound(1, QThread::idealThreadCount(), 64);
692
693 if (num_worker_threads > 1) {
694 runner = JxlThreadParallelRunnerCreate(nullptr, num_worker_threads);
695 if (JxlEncoderSetParallelRunner(encoder, JxlThreadParallelRunner, runner) != JXL_ENC_SUCCESS) {
696 qWarning("JxlEncoderSetParallelRunner failed");
697 JxlThreadParallelRunnerDestroy(runner);
698 JxlEncoderDestroy(encoder);
699 return false;
700 }
701 }
702
703 JxlPixelFormat pixel_format;
704 QImage::Format tmpformat;
705 JxlEncoderStatus status;
706
707 pixel_format.endianness = JXL_NATIVE_ENDIAN;
708 pixel_format.align = 0;
709
710 output_info.animation.tps_numerator = 10;
711 output_info.animation.tps_denominator = 1;
712 output_info.orientation = JXL_ORIENT_IDENTITY;
713 if (m_transformations == QImageIOHandler::TransformationMirror) {
714 output_info.orientation = JXL_ORIENT_FLIP_HORIZONTAL;
715 } else if (m_transformations == QImageIOHandler::TransformationRotate180) {
716 output_info.orientation = JXL_ORIENT_ROTATE_180;
717 } else if (m_transformations == QImageIOHandler::TransformationFlip) {
718 output_info.orientation = JXL_ORIENT_FLIP_VERTICAL;
719 } else if (m_transformations == QImageIOHandler::TransformationFlipAndRotate90) {
720 output_info.orientation = JXL_ORIENT_TRANSPOSE;
721 } else if (m_transformations == QImageIOHandler::TransformationRotate90) {
722 output_info.orientation = JXL_ORIENT_ROTATE_90_CW;
723 } else if (m_transformations == QImageIOHandler::TransformationMirrorAndRotate90) {
724 output_info.orientation = JXL_ORIENT_ANTI_TRANSPOSE;
725 } else if (m_transformations == QImageIOHandler::TransformationRotate270) {
726 output_info.orientation = JXL_ORIENT_ROTATE_90_CCW;
727 }
728
729 if (save_depth > 8 && is_gray) { // 16bit depth gray
730 pixel_format.data_type = JXL_TYPE_UINT16;
731 pixel_format.align = 4;
732 output_info.num_color_channels = 1;
733 output_info.bits_per_sample = 16;
734 tmpformat = QImage::Format_Grayscale16;
735 pixel_format.num_channels = 1;
736 } else if (is_gray) { // 8bit depth gray
737 pixel_format.data_type = JXL_TYPE_UINT8;
738 pixel_format.align = 4;
739 output_info.num_color_channels = 1;
740 output_info.bits_per_sample = 8;
741 tmpformat = QImage::Format_Grayscale8;
742 pixel_format.num_channels = 1;
743 } else if (save_depth > 16) { // 32bit depth rgb
744 pixel_format.data_type = JXL_TYPE_FLOAT;
745 output_info.exponent_bits_per_sample = 8;
746 output_info.num_color_channels = 3;
747 output_info.bits_per_sample = 32;
748
749 if (image.hasAlphaChannel()) {
750 tmpformat = QImage::Format_RGBA32FPx4;
751 pixel_format.num_channels = 4;
752 output_info.alpha_bits = 32;
753 output_info.alpha_exponent_bits = 8;
754 output_info.num_extra_channels = 1;
755 } else {
756 tmpformat = QImage::Format_RGBX32FPx4;
757 pixel_format.num_channels = 3;
758 output_info.alpha_bits = 0;
759 output_info.num_extra_channels = 0;
760 }
761 } else if (save_depth > 8) { // 16bit depth rgb
762 pixel_format.data_type = save_fp ? JXL_TYPE_FLOAT16 : JXL_TYPE_UINT16;
763 output_info.exponent_bits_per_sample = save_fp ? 5 : 0;
764 output_info.num_color_channels = 3;
765 output_info.bits_per_sample = 16;
766
767 if (image.hasAlphaChannel()) {
768 tmpformat = save_fp ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBA64;
769 pixel_format.num_channels = 4;
770 output_info.alpha_bits = 16;
771 output_info.alpha_exponent_bits = save_fp ? 5 : 0;
772 output_info.num_extra_channels = 1;
773 } else {
774 tmpformat = save_fp ? QImage::Format_RGBX16FPx4 : QImage::Format_RGBX64;
775 pixel_format.num_channels = 3;
776 output_info.alpha_bits = 0;
777 output_info.num_extra_channels = 0;
778 }
779 } else { // 8bit depth rgb
780 pixel_format.data_type = JXL_TYPE_UINT8;
781 pixel_format.align = 4;
782 output_info.num_color_channels = 3;
783 output_info.bits_per_sample = 8;
784
785 if (image.hasAlphaChannel()) {
786 tmpformat = QImage::Format_RGBA8888;
787 pixel_format.num_channels = 4;
788 output_info.alpha_bits = 8;
789 output_info.num_extra_channels = 1;
790 } else {
791 tmpformat = QImage::Format_RGB888;
792 pixel_format.num_channels = 3;
793 output_info.alpha_bits = 0;
794 output_info.num_extra_channels = 0;
795 }
796 }
797
798#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
799 // TODO: add native CMYK support (libjxl supports CMYK images)
800 QImage tmpimage;
801 auto cs = image.colorSpace();
802 if (cs.isValid() && cs.colorModel() == QColorSpace::ColorModel::Cmyk && image.format() == QImage::Format_CMYK8888) {
803 tmpimage = image.convertedToColorSpace(QColorSpace(QColorSpace::SRgb), tmpformat);
804 iccprofile.clear();
805 } else {
806 tmpimage = image.convertToFormat(tmpformat);
807 }
808#else
809 QImage tmpimage = image.convertToFormat(tmpformat);
810#endif
811
812 const size_t xsize = tmpimage.width();
813 const size_t ysize = tmpimage.height();
814
815 if (xsize == 0 || ysize == 0 || tmpimage.isNull()) {
816 qWarning("Unable to allocate memory for output image");
817 if (runner) {
818 JxlThreadParallelRunnerDestroy(runner);
819 }
820 JxlEncoderDestroy(encoder);
821 return false;
822 }
823
824 output_info.xsize = tmpimage.width();
825 output_info.ysize = tmpimage.height();
826
827 status = JxlEncoderSetBasicInfo(encoder, &output_info);
828 if (status != JXL_ENC_SUCCESS) {
829 qWarning("JxlEncoderSetBasicInfo failed!");
830 if (runner) {
831 JxlThreadParallelRunnerDestroy(runner);
832 }
833 JxlEncoderDestroy(encoder);
834 return false;
835 }
836
837 auto exif_data = MicroExif::fromImage(image).toByteArray();
838 if (!exif_data.isEmpty()) {
839 exif_data = QByteArray::fromHex("00000000") + exif_data;
840 const char *box_type = "Exif";
841 status = JxlEncoderAddBox(encoder, box_type, reinterpret_cast<const uint8_t *>(exif_data.constData()), exif_data.size(), JXL_FALSE);
842 if (status != JXL_ENC_SUCCESS) {
843 qWarning("JxlEncoderAddBox failed!");
844 if (runner) {
845 JxlThreadParallelRunnerDestroy(runner);
846 }
847 JxlEncoderDestroy(encoder);
848 return false;
849 }
850 }
851 auto xmp_data = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8();
852 if (!xmp_data.isEmpty()) {
853 const char *box_type = "xml ";
854 status = JxlEncoderAddBox(encoder, box_type, reinterpret_cast<const uint8_t *>(xmp_data.constData()), xmp_data.size(), JXL_FALSE);
855 if (status != JXL_ENC_SUCCESS) {
856 qWarning("JxlEncoderAddBox failed!");
857 if (runner) {
858 JxlThreadParallelRunnerDestroy(runner);
859 }
860 JxlEncoderDestroy(encoder);
861 return false;
862 }
863 }
864 JxlEncoderCloseBoxes(encoder); // no more metadata
865
866 if (iccprofile.size() > 0) {
867 status = JxlEncoderSetICCProfile(encoder, reinterpret_cast<const uint8_t *>(iccprofile.constData()), iccprofile.size());
868 if (status != JXL_ENC_SUCCESS) {
869 qWarning("JxlEncoderSetICCProfile failed!");
870 if (runner) {
871 JxlThreadParallelRunnerDestroy(runner);
872 }
873 JxlEncoderDestroy(encoder);
874 return false;
875 }
876 } else {
877 JxlColorEncoding color_profile;
878 JxlColorEncodingSetToSRGB(&color_profile, is_gray ? JXL_TRUE : JXL_FALSE);
879
880 status = JxlEncoderSetColorEncoding(encoder, &color_profile);
881 if (status != JXL_ENC_SUCCESS) {
882 qWarning("JxlEncoderSetColorEncoding failed!");
883 if (runner) {
884 JxlThreadParallelRunnerDestroy(runner);
885 }
886 JxlEncoderDestroy(encoder);
887 return false;
888 }
889 }
890
891 JxlEncoderFrameSettings *encoder_options = JxlEncoderFrameSettingsCreate(encoder, nullptr);
892
893 JxlEncoderSetFrameDistance(encoder_options, (100.0f - m_quality) / 10.0f);
894
895 JxlEncoderSetFrameLossless(encoder_options, (m_quality == 100) ? JXL_TRUE : JXL_FALSE);
896
897 size_t buffer_size = size_t(tmpimage.bytesPerLine()) * tmpimage.height();
898 if (!image.hasAlphaChannel() && save_depth > 8 && !is_gray) { // pack pixel on tmpimage
899 buffer_size = (size_t(save_depth / 8) * pixel_format.num_channels * xsize * ysize);
900
901 // detaching image
902 tmpimage.detach();
903 if (tmpimage.isNull()) {
904 qWarning("Memory allocation error");
905 if (runner) {
906 JxlThreadParallelRunnerDestroy(runner);
907 }
908 JxlEncoderDestroy(encoder);
909 return false;
910 }
911
912 // pack pixel data
913 if (save_depth > 16 && save_fp)
914 packRGBPixels<float>(tmpimage);
915 else if (save_fp)
916 packRGBPixels<qfloat16>(tmpimage);
917 else
918 packRGBPixels<quint16>(tmpimage);
919 }
920 status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, static_cast<const void *>(tmpimage.constBits()), buffer_size);
921
922 if (status == JXL_ENC_ERROR) {
923 qWarning("JxlEncoderAddImageFrame failed!");
924 if (runner) {
925 JxlThreadParallelRunnerDestroy(runner);
926 }
927 JxlEncoderDestroy(encoder);
928 return false;
929 }
930
931 JxlEncoderCloseInput(encoder);
932
933 std::vector<uint8_t> compressed;
934 compressed.resize(4096);
935 size_t offset = 0;
936 uint8_t *next_out;
937 size_t avail_out;
938 do {
939 next_out = compressed.data() + offset;
940 avail_out = compressed.size() - offset;
941 status = JxlEncoderProcessOutput(encoder, &next_out, &avail_out);
942
943 if (status == JXL_ENC_NEED_MORE_OUTPUT) {
944 offset = next_out - compressed.data();
945 compressed.resize(compressed.size() * 2);
946 } else if (status == JXL_ENC_ERROR) {
947 qWarning("JxlEncoderProcessOutput failed!");
948 if (runner) {
949 JxlThreadParallelRunnerDestroy(runner);
950 }
951 JxlEncoderDestroy(encoder);
952 return false;
953 }
954 } while (status != JXL_ENC_SUCCESS);
955
956 if (runner) {
957 JxlThreadParallelRunnerDestroy(runner);
958 }
959 JxlEncoderDestroy(encoder);
960
961 compressed.resize(next_out - compressed.data());
962
963 if (compressed.size() > 0) {
964 qint64 write_status = device()->write(reinterpret_cast<const char *>(compressed.data()), compressed.size());
965
966 if (write_status > 0) {
967 return true;
968 } else if (write_status == -1) {
969 qWarning("Write error: %s\n", qUtf8Printable(device()->errorString()));
970 }
971 }
972
973 return false;
974}
975
976QVariant QJpegXLHandler::option(ImageOption option) const
977{
978 if (!supportsOption(option)) {
979 return QVariant();
980 }
981
982 if (option == Quality) {
983 return m_quality;
984 }
985
986 if (!ensureParsed()) {
987#ifdef JXL_QT_AUTOTRANSFORM
988 if (option == ImageTransformation) {
989 return int(m_transformations);
990 }
991#endif
992 return QVariant();
993 }
994
995 switch (option) {
996 case Size:
997 return QSize(m_basicinfo.xsize, m_basicinfo.ysize);
998 case Animation:
999 if (m_basicinfo.have_animation) {
1000 return true;
1001 } else {
1002 return false;
1003 }
1004#ifdef JXL_QT_AUTOTRANSFORM
1005 case ImageTransformation:
1006 if (m_basicinfo.orientation == JXL_ORIENT_IDENTITY) {
1008 } else if (m_basicinfo.orientation == JXL_ORIENT_FLIP_HORIZONTAL) {
1010 } else if (m_basicinfo.orientation == JXL_ORIENT_ROTATE_180) {
1012 } else if (m_basicinfo.orientation == JXL_ORIENT_FLIP_VERTICAL) {
1014 } else if (m_basicinfo.orientation == JXL_ORIENT_TRANSPOSE) {
1016 } else if (m_basicinfo.orientation == JXL_ORIENT_ROTATE_90_CW) {
1018 } else if (m_basicinfo.orientation == JXL_ORIENT_ANTI_TRANSPOSE) {
1020 } else if (m_basicinfo.orientation == JXL_ORIENT_ROTATE_90_CCW) {
1022 }
1023 break;
1024#endif
1025 default:
1026 return QVariant();
1027 }
1028
1029 return QVariant();
1030}
1031
1032void QJpegXLHandler::setOption(ImageOption option, const QVariant &value)
1033{
1034 switch (option) {
1035 case Quality:
1036 m_quality = value.toInt();
1037 if (m_quality > 100) {
1038 m_quality = 100;
1039 } else if (m_quality < 0) {
1040 m_quality = 90;
1041 }
1042 return;
1043#ifdef JXL_QT_AUTOTRANSFORM
1044 case ImageTransformation:
1045 if (auto t = value.toInt()) {
1046 if (t > 0 && t < 8)
1047 m_transformations = QImageIOHandler::Transformations(t);
1048 }
1049 break;
1050#endif
1051 default:
1052 break;
1053 }
1054 QImageIOHandler::setOption(option, value);
1055}
1056
1057bool QJpegXLHandler::supportsOption(ImageOption option) const
1058{
1059 auto supported = option == Quality || option == Size || option == Animation;
1060#ifdef JXL_QT_AUTOTRANSFORM
1061 supported = supported || option == ImageTransformation;
1062#endif
1063 return supported;
1064}
1065
1066int QJpegXLHandler::imageCount() const
1067{
1068 if (!ensureParsed()) {
1069 return 0;
1070 }
1071
1072 if (m_parseState == ParseJpegXLBasicInfoParsed) {
1073 if (!m_basicinfo.have_animation) {
1074 return 1;
1075 }
1076
1077 if (!ensureALLCounted()) {
1078 return 0;
1079 }
1080 }
1081
1082 if (!m_framedelays.isEmpty()) {
1083 return m_framedelays.count();
1084 }
1085 return 0;
1086}
1087
1088int QJpegXLHandler::currentImageNumber() const
1089{
1090 if (m_parseState == ParseJpegXLNotParsed) {
1091 return -1;
1092 }
1093
1094 if (m_parseState == ParseJpegXLError || m_parseState == ParseJpegXLBasicInfoParsed || !m_decoder) {
1095 return 0;
1096 }
1097
1098 return m_currentimage_index;
1099}
1100
1101bool QJpegXLHandler::jumpToNextImage()
1102{
1103 if (!ensureALLCounted()) {
1104 return false;
1105 }
1106
1107 if (m_framedelays.count() > 1) {
1108 m_currentimage_index++;
1109
1110 if (m_currentimage_index >= m_framedelays.count()) {
1111 if (!rewind()) {
1112 return false;
1113 }
1114 } else {
1115 JxlDecoderSkipFrames(m_decoder, 1);
1116 }
1117 }
1118
1119 m_parseState = ParseJpegXLSuccess;
1120 return true;
1121}
1122
1123bool QJpegXLHandler::jumpToImage(int imageNumber)
1124{
1125 if (!ensureALLCounted()) {
1126 return false;
1127 }
1128
1129 if (imageNumber < 0 || imageNumber >= m_framedelays.count()) {
1130 return false;
1131 }
1132
1133 if (imageNumber == m_currentimage_index) {
1134 m_parseState = ParseJpegXLSuccess;
1135 return true;
1136 }
1137
1138 if (imageNumber > m_currentimage_index) {
1139 JxlDecoderSkipFrames(m_decoder, imageNumber - m_currentimage_index);
1140 m_currentimage_index = imageNumber;
1141 m_parseState = ParseJpegXLSuccess;
1142 return true;
1143 }
1144
1145 if (!rewind()) {
1146 return false;
1147 }
1148
1149 if (imageNumber > 0) {
1150 JxlDecoderSkipFrames(m_decoder, imageNumber);
1151 }
1152 m_currentimage_index = imageNumber;
1153 m_parseState = ParseJpegXLSuccess;
1154 return true;
1155}
1156
1157int QJpegXLHandler::nextImageDelay() const
1158{
1159 if (!ensureALLCounted()) {
1160 return 0;
1161 }
1162
1163 if (m_framedelays.count() < 2) {
1164 return 0;
1165 }
1166
1167 return m_next_image_delay;
1168}
1169
1170int QJpegXLHandler::loopCount() const
1171{
1172 if (!ensureParsed()) {
1173 return 0;
1174 }
1175
1176 if (m_basicinfo.have_animation) {
1177 return (m_basicinfo.animation.num_loops > 0) ? m_basicinfo.animation.num_loops - 1 : -1;
1178 } else {
1179 return 0;
1180 }
1181}
1182
1183bool QJpegXLHandler::rewind()
1184{
1185 m_currentimage_index = 0;
1186
1187 JxlDecoderReleaseInput(m_decoder);
1188 JxlDecoderRewind(m_decoder);
1189 if (m_runner) {
1190 if (JxlDecoderSetParallelRunner(m_decoder, JxlThreadParallelRunner, m_runner) != JXL_DEC_SUCCESS) {
1191 qWarning("ERROR: JxlDecoderSetParallelRunner failed");
1192 m_parseState = ParseJpegXLError;
1193 return false;
1194 }
1195 }
1196
1197 if (JxlDecoderSetInput(m_decoder, reinterpret_cast<const uint8_t *>(m_rawData.constData()), m_rawData.size()) != JXL_DEC_SUCCESS) {
1198 qWarning("ERROR: JxlDecoderSetInput failed");
1199 m_parseState = ParseJpegXLError;
1200 return false;
1201 }
1202
1203 JxlDecoderCloseInput(m_decoder);
1204
1205 if (m_basicinfo.uses_original_profile == JXL_FALSE && m_basicinfo.have_animation == JXL_FALSE) {
1206 if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
1207 qWarning("ERROR: JxlDecoderSubscribeEvents failed");
1208 m_parseState = ParseJpegXLError;
1209 return false;
1210 }
1211
1212 JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder);
1213 if (status != JXL_DEC_COLOR_ENCODING) {
1214 qWarning("Unexpected event %d instead of JXL_DEC_COLOR_ENCODING", status);
1215 m_parseState = ParseJpegXLError;
1216 return false;
1217 }
1218
1219#if JPEGXL_NUMERIC_VERSION >= JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
1220 const JxlCmsInterface *jxlcms = JxlGetDefaultCms();
1221 if (jxlcms) {
1222 status = JxlDecoderSetCms(m_decoder, *jxlcms);
1223 if (status != JXL_DEC_SUCCESS) {
1224 qWarning("JxlDecoderSetCms ERROR");
1225 }
1226 } else {
1227 qWarning("No JPEG XL CMS Interface");
1228 }
1229#endif
1230
1231 bool is_gray = m_basicinfo.num_color_channels == 1 && m_basicinfo.alpha_bits == 0;
1232 JxlColorEncoding color_encoding;
1233 JxlColorEncodingSetToSRGB(&color_encoding, is_gray ? JXL_TRUE : JXL_FALSE);
1234 JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding);
1235 } else {
1236 if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
1237 qWarning("ERROR: JxlDecoderSubscribeEvents failed");
1238 m_parseState = ParseJpegXLError;
1239 return false;
1240 }
1241 }
1242
1243 return true;
1244}
1245
1246bool QJpegXLHandler::decodeContainer()
1247{
1248#if JPEGXL_NUMERIC_VERSION >= JPEGXL_COMPUTE_NUMERIC_VERSION(0, 11, 0)
1249 if (m_basicinfo.have_container == JXL_FALSE) {
1250 return true;
1251 }
1252
1253 const size_t len = m_rawData.size();
1254 if (len == 0) {
1255 m_parseState = ParseJpegXLError;
1256 return false;
1257 }
1258
1259 const uint8_t *buf = reinterpret_cast<const uint8_t *>(m_rawData.constData());
1260 if (JxlSignatureCheck(buf, len) != JXL_SIG_CONTAINER) {
1261 return true;
1262 }
1263
1264 JxlDecoderReleaseInput(m_decoder);
1265 JxlDecoderRewind(m_decoder);
1266
1267 if (JxlDecoderSetInput(m_decoder, buf, len) != JXL_DEC_SUCCESS) {
1268 qWarning("ERROR: JxlDecoderSetInput failed");
1269 m_parseState = ParseJpegXLError;
1270 return false;
1271 }
1272
1273 JxlDecoderCloseInput(m_decoder);
1274
1275 if (JxlDecoderSetDecompressBoxes(m_decoder, JXL_TRUE) != JXL_DEC_SUCCESS) {
1276 qWarning("WARNING: JxlDecoderSetDecompressBoxes failed");
1277 }
1278
1279 if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_BOX | JXL_DEC_BOX_COMPLETE) != JXL_DEC_SUCCESS) {
1280 qWarning("ERROR: JxlDecoderSubscribeEvents failed");
1281 m_parseState = ParseJpegXLError;
1282 return false;
1283 }
1284
1285 bool search_exif = true;
1286 bool search_xmp = true;
1287 JxlBoxType box_type;
1288
1289 QByteArray exifBox;
1290 QByteArray xmpBox;
1291
1292 while (search_exif || search_xmp) {
1293 JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder);
1294 switch (status) {
1295 case JXL_DEC_SUCCESS:
1296 search_exif = false;
1297 search_xmp = false;
1298 break;
1299 case JXL_DEC_BOX:
1300 status = JxlDecoderGetBoxType(m_decoder, box_type, JXL_TRUE);
1301 if (status != JXL_DEC_SUCCESS) {
1302 qWarning("Error in JxlDecoderGetBoxType");
1303 m_parseState = ParseJpegXLError;
1304 return false;
1305 }
1306
1307 if (box_type[0] == 'E' && box_type[1] == 'x' && box_type[2] == 'i' && box_type[3] == 'f' && search_exif) {
1308 search_exif = false;
1309 if (!extractBox(exifBox, len)) {
1310 return false;
1311 }
1312 } else if (box_type[0] == 'x' && box_type[1] == 'm' && box_type[2] == 'l' && box_type[3] == ' ' && search_xmp) {
1313 search_xmp = false;
1314 if (!extractBox(xmpBox, len)) {
1315 return false;
1316 }
1317 }
1318 break;
1319 case JXL_DEC_ERROR:
1320 qWarning("JXL Metadata decoding error");
1321 m_parseState = ParseJpegXLError;
1322 return false;
1323 break;
1324 case JXL_DEC_NEED_MORE_INPUT:
1325 qWarning("JXL metadata are probably incomplete");
1326 m_parseState = ParseJpegXLError;
1327 return false;
1328 break;
1329 default:
1330 qWarning("Unexpected event %d instead of JXL_DEC_BOX", status);
1331 m_parseState = ParseJpegXLError;
1332 return false;
1333 break;
1334 }
1335 }
1336
1337 if (xmpBox.size() > 0) {
1338 m_xmp = xmpBox;
1339 }
1340
1341 if (exifBox.size() > 4) {
1342 const char tiffHeaderBE[4] = {'M', 'M', 0, 42};
1343 const char tiffHeaderLE[4] = {'I', 'I', 42, 0};
1344 const QByteArray tiffBE = QByteArray::fromRawData(tiffHeaderBE, 4);
1345 const QByteArray tiffLE = QByteArray::fromRawData(tiffHeaderLE, 4);
1346 auto headerindexBE = exifBox.indexOf(tiffBE);
1347 auto headerindexLE = exifBox.indexOf(tiffLE);
1348
1349 if (headerindexLE != -1) {
1350 if (headerindexBE == -1) {
1351 m_exif = exifBox.mid(headerindexLE);
1352 } else {
1353 m_exif = exifBox.mid((headerindexLE <= headerindexBE) ? headerindexLE : headerindexBE);
1354 }
1355 } else if (headerindexBE != -1) {
1356 m_exif = exifBox.mid(headerindexBE);
1357 } else {
1358 qWarning("Exif box in JXL file doesn't have TIFF header");
1359 }
1360 }
1361#endif
1362 return true;
1363}
1364
1365bool QJpegXLHandler::extractBox(QByteArray &output, size_t container_size)
1366{
1367#if JPEGXL_NUMERIC_VERSION >= JPEGXL_COMPUTE_NUMERIC_VERSION(0, 11, 0)
1368 uint64_t rawboxsize = 0;
1369 JxlDecoderStatus status = JxlDecoderGetBoxSizeRaw(m_decoder, &rawboxsize);
1370 if (status != JXL_DEC_SUCCESS) {
1371 qWarning("ERROR: JxlDecoderGetBoxSizeRaw failed");
1372 m_parseState = ParseJpegXLError;
1373 return false;
1374 }
1375
1376 if (rawboxsize > container_size) {
1377 qWarning("JXL metadata box is incomplete");
1378 m_parseState = ParseJpegXLError;
1379 return false;
1380 }
1381
1382 output.resize(rawboxsize);
1383 status = JxlDecoderSetBoxBuffer(m_decoder, reinterpret_cast<uint8_t *>(output.data()), output.size());
1384 if (status != JXL_DEC_SUCCESS) {
1385 qWarning("ERROR: JxlDecoderSetBoxBuffer failed");
1386 m_parseState = ParseJpegXLError;
1387 return false;
1388 }
1389
1390 do {
1391 status = JxlDecoderProcessInput(m_decoder);
1392 if (status == JXL_DEC_BOX_NEED_MORE_OUTPUT) {
1393 size_t bytes_remains = JxlDecoderReleaseBoxBuffer(m_decoder);
1394
1395 if (output.size() > 4194304) { // approx. 4MB limit for decompressed metadata box
1396 qWarning("JXL metadata box is too large");
1397 m_parseState = ParseJpegXLError;
1398 return false;
1399 }
1400
1401 output.append(16384, '\0');
1402 size_t extension_size = 16384 + bytes_remains;
1403 uint8_t *extension_buffer = reinterpret_cast<uint8_t *>(output.data()) + (output.size() - extension_size);
1404
1405 if (JxlDecoderSetBoxBuffer(m_decoder, extension_buffer, extension_size) != JXL_DEC_SUCCESS) {
1406 qWarning("ERROR: JxlDecoderSetBoxBuffer failed after JXL_DEC_BOX_NEED_MORE_OUTPUT");
1407 m_parseState = ParseJpegXLError;
1408 return false;
1409 }
1410 }
1411 } while (status == JXL_DEC_BOX_NEED_MORE_OUTPUT);
1412
1413 if (status != JXL_DEC_BOX_COMPLETE) {
1414 qWarning("Unexpected event %d instead of JXL_DEC_BOX_COMPLETE", status);
1415 m_parseState = ParseJpegXLError;
1416 return false;
1417 }
1418
1419 size_t unused_bytes = JxlDecoderReleaseBoxBuffer(m_decoder);
1420 output.chop(unused_bytes);
1421#endif
1422 return true;
1423}
1424
1425QImageIOPlugin::Capabilities QJpegXLPlugin::capabilities(QIODevice *device, const QByteArray &format) const
1426{
1427 if (format == "jxl") {
1428 return Capabilities(CanRead | CanWrite);
1429 }
1430
1431 if (!format.isEmpty()) {
1432 return {};
1433 }
1434 if (!device->isOpen()) {
1435 return {};
1436 }
1437
1438 Capabilities cap;
1439 if (device->isReadable() && QJpegXLHandler::canRead(device)) {
1440 cap |= CanRead;
1441 }
1442
1443 if (device->isWritable()) {
1444 cap |= CanWrite;
1445 }
1446
1447 return cap;
1448}
1449
1450QImageIOHandler *QJpegXLPlugin::create(QIODevice *device, const QByteArray &format) const
1451{
1452 QImageIOHandler *handler = new QJpegXLHandler;
1453 handler->setDevice(device);
1454 handler->setFormat(format);
1455 return handler;
1456}
1457
1458#include "moc_jxl_p.cpp"
Q_SCRIPTABLE CaptureState status()
QFlags< Capability > Capabilities
QByteArray & append(QByteArrayView data)
void chop(qsizetype n)
void clear()
const char * constData() const const
char * data()
QByteArray fromHex(const QByteArray &hexEncoded)
QByteArray fromRawData(const char *data, qsizetype size)
qsizetype indexOf(QByteArrayView bv, qsizetype from) const const
bool isEmpty() const const
QByteArray mid(qsizetype pos, qsizetype len) const const
void resize(qsizetype newSize, char c)
qsizetype size() const const
QColorSpace fromIccProfile(const QByteArray &iccProfile)
QByteArray iccProfile() const const
bool isValid() const const
Primaries primaries() const const
TransferFunction transferFunction() const const
Format_Grayscale16
uchar * bits()
qsizetype bytesPerLine() const const
QColorSpace colorSpace() const const
const uchar * constBits() const const
const uchar * constScanLine(int i) const const
QImage convertToFormat(Format format, Qt::ImageConversionFlags flags) &&
QImage convertedToColorSpace(const QColorSpace &colorSpace) const const
int depth() const const
Format format() const const
bool hasAlphaChannel() const const
int height() const const
bool isGrayscale() const const
bool isNull() const const
QString text(const QString &key) const const
int width() const const
void setDevice(QIODevice *device)
void setFormat(const QByteArray &format)
virtual void setOption(ImageOption option, const QVariant &value)
typedef Capabilities
bool isOpen() const const
bool isReadable() const const
bool isWritable() const const
QByteArray peek(qint64 maxSize)
QByteArray readAll()
qint64 write(const QByteArray &data)
QString fromUtf8(QByteArrayView str)
QByteArray toUtf8() const const
int idealThreadCount()
int toInt(bool *ok) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 24 2025 11:53:42 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.