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/cms.h>
17#include <jxl/encode.h>
18#include <jxl/thread_parallel_runner.h>
19
20#include <string.h>
21
22// Avoid rotation on buggy Qts (see also https://bugreports.qt.io/browse/QTBUG-126575)
23#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))
24#ifndef JXL_QT_AUTOTRANSFORM
25#define JXL_QT_AUTOTRANSFORM
26#endif
27#endif
28
29#ifndef JXL_HDR_PRESERVATION_DISABLED
30// Define JXL_HDR_PRESERVATION_DISABLED to disable HDR preservation
31// (HDR images are saved as UINT16).
32// #define JXL_HDR_PRESERVATION_DISABLED
33#endif
34
35#ifndef JXL_DECODE_BOXES_DISABLED
36// Decode Boxes in order to read optional metadata (XMP, Exif, etc...).
37// Define JXL_DECODE_BOXES_DISABLED to disable Boxes decoding.
38// #define JXL_DECODE_BOXES_DISABLED
39#endif
40
41#define FEATURE_LEVEL_5_WIDTH 262144
42#define FEATURE_LEVEL_5_HEIGHT 262144
43#define FEATURE_LEVEL_5_PIXELS 268435456
44
45#if QT_POINTER_SIZE < 8
46#define MAX_IMAGE_WIDTH 32767
47#define MAX_IMAGE_HEIGHT 32767
48#define MAX_IMAGE_PIXELS FEATURE_LEVEL_5_PIXELS
49#else // JXL code stream level 5
50#define MAX_IMAGE_WIDTH FEATURE_LEVEL_5_WIDTH
51#define MAX_IMAGE_HEIGHT FEATURE_LEVEL_5_HEIGHT
52#define MAX_IMAGE_PIXELS FEATURE_LEVEL_5_PIXELS
53#endif
54
55QJpegXLHandler::QJpegXLHandler()
56 : m_parseState(ParseJpegXLNotParsed)
57 , m_quality(90)
58 , m_currentimage_index(0)
59 , m_previousimage_index(-1)
60 , m_transformations(QImageIOHandler::TransformationNone)
61 , m_decoder(nullptr)
62 , m_runner(nullptr)
63 , m_next_image_delay(0)
64 , m_isCMYK(false)
65 , m_cmyk_channel_id(0)
66 , m_alpha_channel_id(0)
67 , m_input_image_format(QImage::Format_Invalid)
68 , m_target_image_format(QImage::Format_Invalid)
69{
70}
71
72QJpegXLHandler::~QJpegXLHandler()
73{
74 if (m_runner) {
75 JxlThreadParallelRunnerDestroy(m_runner);
76 }
77 if (m_decoder) {
78 JxlDecoderDestroy(m_decoder);
79 }
80}
81
82bool QJpegXLHandler::canRead() const
83{
84 if (m_parseState == ParseJpegXLNotParsed && !canRead(device())) {
85 return false;
86 }
87
88 if (m_parseState != ParseJpegXLError) {
89 setFormat("jxl");
90
91 if (m_parseState == ParseJpegXLFinished) {
92 return false;
93 }
94
95 return true;
96 }
97 return false;
98}
99
100bool QJpegXLHandler::canRead(QIODevice *device)
101{
102 if (!device) {
103 return false;
104 }
105 QByteArray header = device->peek(32);
106 if (header.size() < 12) {
107 return false;
108 }
109
110 JxlSignature signature = JxlSignatureCheck(reinterpret_cast<const uint8_t *>(header.constData()), header.size());
111 if (signature == JXL_SIG_CODESTREAM || signature == JXL_SIG_CONTAINER) {
112 return true;
113 }
114 return false;
115}
116
117bool QJpegXLHandler::ensureParsed() const
118{
119 if (m_parseState == ParseJpegXLSuccess || m_parseState == ParseJpegXLBasicInfoParsed || m_parseState == ParseJpegXLFinished) {
120 return true;
121 }
122 if (m_parseState == ParseJpegXLError) {
123 return false;
124 }
125
126 QJpegXLHandler *that = const_cast<QJpegXLHandler *>(this);
127
128 return that->ensureDecoder();
129}
130
131bool QJpegXLHandler::ensureALLCounted() const
132{
133 if (!ensureParsed()) {
134 return false;
135 }
136
137 if (m_parseState == ParseJpegXLSuccess || m_parseState == ParseJpegXLFinished) {
138 return true;
139 }
140
141 QJpegXLHandler *that = const_cast<QJpegXLHandler *>(this);
142
143 return that->countALLFrames();
144}
145
146bool QJpegXLHandler::ensureDecoder()
147{
148 if (m_decoder) {
149 return true;
150 }
151
152 m_rawData = device()->readAll();
153
154 if (m_rawData.isEmpty()) {
155 return false;
156 }
157
158 JxlSignature signature = JxlSignatureCheck(reinterpret_cast<const uint8_t *>(m_rawData.constData()), m_rawData.size());
159 if (signature != JXL_SIG_CODESTREAM && signature != JXL_SIG_CONTAINER) {
160 m_parseState = ParseJpegXLError;
161 return false;
162 }
163
164 m_decoder = JxlDecoderCreate(nullptr);
165 if (!m_decoder) {
166 qWarning("ERROR: JxlDecoderCreate failed");
167 m_parseState = ParseJpegXLError;
168 return false;
169 }
170
171#ifdef JXL_QT_AUTOTRANSFORM
172 // Let Qt handle the orientation.
173 JxlDecoderSetKeepOrientation(m_decoder, true);
174#endif
175
176 int num_worker_threads = QThread::idealThreadCount();
177 if (!m_runner && num_worker_threads >= 4) {
178 /* use half of the threads because plug-in is usually used in environment
179 * where application performs another tasks in backround (pre-load other images) */
180 num_worker_threads = num_worker_threads / 2;
181 num_worker_threads = qBound(2, num_worker_threads, 64);
182 m_runner = JxlThreadParallelRunnerCreate(nullptr, num_worker_threads);
183
184 if (JxlDecoderSetParallelRunner(m_decoder, JxlThreadParallelRunner, m_runner) != JXL_DEC_SUCCESS) {
185 qWarning("ERROR: JxlDecoderSetParallelRunner failed");
186 m_parseState = ParseJpegXLError;
187 return false;
188 }
189 }
190
191 if (JxlDecoderSetInput(m_decoder, reinterpret_cast<const uint8_t *>(m_rawData.constData()), m_rawData.size()) != JXL_DEC_SUCCESS) {
192 qWarning("ERROR: JxlDecoderSetInput failed");
193 m_parseState = ParseJpegXLError;
194 return false;
195 }
196
197 JxlDecoderCloseInput(m_decoder);
198
199 JxlDecoderStatus status = JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME);
200 if (status == JXL_DEC_ERROR) {
201 qWarning("ERROR: JxlDecoderSubscribeEvents failed");
202 m_parseState = ParseJpegXLError;
203 return false;
204 }
205
206 status = JxlDecoderProcessInput(m_decoder);
207 if (status == JXL_DEC_ERROR) {
208 qWarning("ERROR: JXL decoding failed");
209 m_parseState = ParseJpegXLError;
210 return false;
211 }
212 if (status == JXL_DEC_NEED_MORE_INPUT) {
213 qWarning("ERROR: JXL data incomplete");
214 m_parseState = ParseJpegXLError;
215 return false;
216 }
217
218 status = JxlDecoderGetBasicInfo(m_decoder, &m_basicinfo);
219 if (status != JXL_DEC_SUCCESS) {
220 qWarning("ERROR: JXL basic info not available");
221 m_parseState = ParseJpegXLError;
222 return false;
223 }
224
225 if (m_basicinfo.xsize == 0 || m_basicinfo.ysize == 0) {
226 qWarning("ERROR: JXL image has zero dimensions");
227 m_parseState = ParseJpegXLError;
228 return false;
229 }
230
231 if (m_basicinfo.xsize > MAX_IMAGE_WIDTH || m_basicinfo.ysize > MAX_IMAGE_HEIGHT) {
232 qWarning("JXL image (%dx%d) is too large", m_basicinfo.xsize, m_basicinfo.ysize);
233 m_parseState = ParseJpegXLError;
234 return false;
235 }
236
237 m_parseState = ParseJpegXLBasicInfoParsed;
238 return true;
239}
240
241bool QJpegXLHandler::countALLFrames()
242{
243 if (m_parseState != ParseJpegXLBasicInfoParsed) {
244 return false;
245 }
246
247 JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder);
248 if (status != JXL_DEC_COLOR_ENCODING) {
249 qWarning("Unexpected event %d instead of JXL_DEC_COLOR_ENCODING", status);
250 m_parseState = ParseJpegXLError;
251 return false;
252 }
253
254 bool is_gray = m_basicinfo.num_color_channels == 1 && m_basicinfo.alpha_bits == 0;
255 JxlColorEncoding color_encoding;
256 if (m_basicinfo.uses_original_profile == JXL_FALSE && m_basicinfo.have_animation == JXL_FALSE) {
257 const JxlCmsInterface *jxlcms = JxlGetDefaultCms();
258 if (jxlcms) {
259 status = JxlDecoderSetCms(m_decoder, *jxlcms);
260 if (status != JXL_DEC_SUCCESS) {
261 qWarning("JxlDecoderSetCms ERROR");
262 }
263 } else {
264 qWarning("No JPEG XL CMS Interface");
265 }
266
267 JxlColorEncodingSetToSRGB(&color_encoding, is_gray ? JXL_TRUE : JXL_FALSE);
268 JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding);
269 }
270
271 bool loadalpha = false;
272 if (m_basicinfo.alpha_bits > 0) {
273 loadalpha = true;
274 }
275
276 m_input_pixel_format.endianness = JXL_NATIVE_ENDIAN;
277 m_input_pixel_format.align = 4;
278
279 if (m_basicinfo.bits_per_sample > 8) { // high bit depth
280#ifdef JXL_HDR_PRESERVATION_DISABLED
281 bool is_fp = false;
282#else
283 bool is_fp = m_basicinfo.exponent_bits_per_sample > 0 && m_basicinfo.num_color_channels == 3;
284#endif
285
286 m_input_pixel_format.num_channels = 4;
287
288 if (is_gray) {
289 m_input_pixel_format.num_channels = 1;
290 m_input_pixel_format.data_type = JXL_TYPE_UINT16;
291 m_input_image_format = m_target_image_format = QImage::Format_Grayscale16;
292 } else if (m_basicinfo.bits_per_sample > 16 && is_fp) {
293 m_input_pixel_format.data_type = JXL_TYPE_FLOAT;
294 m_input_image_format = QImage::Format_RGBA32FPx4;
295 if (loadalpha)
296 m_target_image_format = QImage::Format_RGBA32FPx4;
297 else
298 m_target_image_format = QImage::Format_RGBX32FPx4;
299 } else {
300 m_input_pixel_format.data_type = is_fp ? JXL_TYPE_FLOAT16 : JXL_TYPE_UINT16;
301 m_input_image_format = is_fp ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBA64;
302 if (loadalpha)
303 m_target_image_format = is_fp ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBA64;
304 else
305 m_target_image_format = is_fp ? QImage::Format_RGBX16FPx4 : QImage::Format_RGBX64;
306 }
307 } else { // 8bit depth
308 m_input_pixel_format.data_type = JXL_TYPE_UINT8;
309
310 if (is_gray) {
311 m_input_pixel_format.num_channels = 1;
312 m_input_image_format = m_target_image_format = QImage::Format_Grayscale8;
313 } else {
314 if (loadalpha) {
315 m_input_pixel_format.num_channels = 4;
316 m_input_image_format = QImage::Format_RGBA8888;
317 m_target_image_format = QImage::Format_ARGB32;
318 } else {
319 m_input_pixel_format.num_channels = 3;
320 m_input_image_format = QImage::Format_RGB888;
321 m_target_image_format = QImage::Format_RGB32;
322 }
323 }
324 }
325
326 status = JxlDecoderGetColorAsEncodedProfile(m_decoder, JXL_COLOR_PROFILE_TARGET_DATA, &color_encoding);
327
328 if (status == JXL_DEC_SUCCESS && color_encoding.color_space == JXL_COLOR_SPACE_RGB && color_encoding.white_point == JXL_WHITE_POINT_D65
329 && color_encoding.primaries == JXL_PRIMARIES_SRGB && color_encoding.transfer_function == JXL_TRANSFER_FUNCTION_SRGB) {
330 m_colorspace = QColorSpace(QColorSpace::SRgb);
331 } else {
332 size_t icc_size = 0;
333 if (JxlDecoderGetICCProfileSize(m_decoder, JXL_COLOR_PROFILE_TARGET_DATA, &icc_size) == JXL_DEC_SUCCESS) {
334 if (icc_size > 0) {
335 QByteArray icc_data(icc_size, 0);
336 if (JxlDecoderGetColorAsICCProfile(m_decoder, JXL_COLOR_PROFILE_TARGET_DATA, reinterpret_cast<uint8_t *>(icc_data.data()), icc_data.size())
337 == JXL_DEC_SUCCESS) {
338 m_colorspace = QColorSpace::fromIccProfile(icc_data);
339
340 if (!m_colorspace.isValid()) {
341 qWarning("JXL image has Qt-unsupported or invalid ICC profile!");
342 }
343 } else {
344 qWarning("Failed to obtain data from JPEG XL decoder");
345 }
346 } else {
347 qWarning("Empty ICC data");
348 }
349 } else {
350 qWarning("no ICC, other color profile");
351 }
352 }
353
354 if (m_basicinfo.have_animation) { // count all frames
355 JxlFrameHeader frame_header;
356 int delay;
357
358 for (status = JxlDecoderProcessInput(m_decoder); status != JXL_DEC_SUCCESS; status = JxlDecoderProcessInput(m_decoder)) {
359 if (status != JXL_DEC_FRAME) {
360 switch (status) {
361 case JXL_DEC_ERROR:
362 qWarning("ERROR: JXL decoding failed");
363 break;
364 case JXL_DEC_NEED_MORE_INPUT:
365 qWarning("ERROR: JXL data incomplete");
366 break;
367 default:
368 qWarning("Unexpected event %d instead of JXL_DEC_FRAME", status);
369 break;
370 }
371 m_parseState = ParseJpegXLError;
372 return false;
373 }
374
375 if (JxlDecoderGetFrameHeader(m_decoder, &frame_header) != JXL_DEC_SUCCESS) {
376 qWarning("ERROR: JxlDecoderGetFrameHeader failed");
377 m_parseState = ParseJpegXLError;
378 return false;
379 }
380
381 if (m_basicinfo.animation.tps_denominator > 0 && m_basicinfo.animation.tps_numerator > 0) {
382 delay = (int)(0.5 + 1000.0 * frame_header.duration * m_basicinfo.animation.tps_denominator / m_basicinfo.animation.tps_numerator);
383 } else {
384 delay = 0;
385 }
386
387 m_framedelays.append(delay);
388
389 if (frame_header.is_last == JXL_TRUE) {
390 break;
391 }
392 }
393
394 if (m_framedelays.isEmpty()) {
395 qWarning("no frames loaded by the JXL plug-in");
396 m_parseState = ParseJpegXLError;
397 return false;
398 }
399
400 if (m_framedelays.count() == 1) {
401 qWarning("JXL file was marked as animation but it has only one frame.");
402 m_basicinfo.have_animation = JXL_FALSE;
403 }
404 } else { // static picture
405 m_framedelays.resize(1);
406 m_framedelays[0] = 0;
407 }
408
409#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
410 // CMYK detection
411 if ((m_basicinfo.uses_original_profile == JXL_TRUE) && (m_basicinfo.num_color_channels == 3) && (m_colorspace.isValid())) {
412 bool alpha_found = false;
413 JxlExtraChannelInfo channel_info;
414 for (uint32_t index = 0; index < m_basicinfo.num_extra_channels; index++) {
415 status = JxlDecoderGetExtraChannelInfo(m_decoder, index, &channel_info);
416 if (status != JXL_DEC_SUCCESS) {
417 qWarning("JxlDecoderGetExtraChannelInfo for channel %d returned %d", index, status);
418 m_parseState = ParseJpegXLError;
419 return false;
420 }
421
422 if (channel_info.type == JXL_CHANNEL_BLACK) {
423 if (m_colorspace.colorModel() == QColorSpace::ColorModel::Cmyk) {
424 m_isCMYK = true;
425 m_cmyk_channel_id = index;
426
427 if (m_basicinfo.alpha_bits > 0) {
428 if (!alpha_found) {
429 // continue searching for alpha channel
430 for (uint32_t alpha_index = index + 1; alpha_index < m_basicinfo.num_extra_channels; alpha_index++) {
431 status = JxlDecoderGetExtraChannelInfo(m_decoder, alpha_index, &channel_info);
432 if (status != JXL_DEC_SUCCESS) {
433 qWarning("JxlDecoderGetExtraChannelInfo for channel %d returned %d", alpha_index, status);
434 m_parseState = ParseJpegXLError;
435 return false;
436 }
437
438 if (channel_info.type == JXL_CHANNEL_ALPHA) {
439 alpha_found = true;
440 m_alpha_channel_id = alpha_index;
441 break;
442 }
443 }
444
445 if (!alpha_found) {
446 qWarning("JXL BasicInfo indicate Alpha channel but it was not found");
447 m_parseState = ParseJpegXLError;
448 return false;
449 }
450 }
451 }
452 } else {
453 qWarning("JXL has BLACK channel but colorspace is not CMYK!");
454 }
455 break;
456 } else if ((channel_info.type == JXL_CHANNEL_ALPHA) && !alpha_found) {
457 alpha_found = true;
458 m_alpha_channel_id = index;
459 }
460 }
461
462 if (!m_isCMYK && (m_colorspace.colorModel() == QColorSpace::ColorModel::Cmyk)) {
463 qWarning("JXL has CMYK colorspace but BLACK channel was not found!");
464 }
465 }
466#endif
467
468#ifndef JXL_DECODE_BOXES_DISABLED
469 if (!decodeContainer()) {
470 return false;
471 }
472#endif
473
474 if (!rewind()) {
475 return false;
476 }
477
478 m_next_image_delay = m_framedelays[0];
479 m_parseState = ParseJpegXLSuccess;
480 return true;
481}
482
483bool QJpegXLHandler::decode_one_frame()
484{
485 JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder);
486 if (status != JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
487 qWarning("Unexpected event %d instead of JXL_DEC_NEED_IMAGE_OUT_BUFFER", status);
488 m_parseState = ParseJpegXLError;
489 return false;
490 }
491
492 if (m_isCMYK) { // CMYK decoding
493#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
494 uchar *pixels_cmy = nullptr;
495 uchar *pixels_black = nullptr;
496
497 JxlPixelFormat format_extra;
498
499 m_input_pixel_format.num_channels = 3;
500 m_input_pixel_format.data_type = JXL_TYPE_UINT8;
501 m_input_pixel_format.endianness = JXL_NATIVE_ENDIAN;
502 m_input_pixel_format.align = 0;
503
504 format_extra.num_channels = 1;
505 format_extra.data_type = JXL_TYPE_UINT8;
506 format_extra.endianness = JXL_NATIVE_ENDIAN;
507 format_extra.align = 0;
508
509 const size_t extra_buffer_size = size_t(m_basicinfo.xsize) * size_t(m_basicinfo.ysize);
510 const size_t cmy_buffer_size = extra_buffer_size * 3;
511
512 if (m_basicinfo.alpha_bits > 0) { // CMYK + alpha
513 QImage tmp_cmyk_image = imageAlloc(m_basicinfo.xsize, m_basicinfo.ysize, QImage::Format_CMYK8888);
514 if (tmp_cmyk_image.isNull()) {
515 qWarning("Memory cannot be allocated");
516 m_parseState = ParseJpegXLError;
517 return false;
518 }
519
520 tmp_cmyk_image.setColorSpace(m_colorspace);
521
522 uchar *pixels_alpha = reinterpret_cast<uchar *>(malloc(extra_buffer_size));
523 if (!pixels_alpha) {
524 qWarning("Memory cannot be allocated for ALPHA channel");
525 m_parseState = ParseJpegXLError;
526 return false;
527 }
528
529 pixels_cmy = reinterpret_cast<uchar *>(malloc(cmy_buffer_size));
530 if (!pixels_cmy) {
531 free(pixels_alpha);
532 pixels_alpha = nullptr;
533 qWarning("Memory cannot be allocated for CMY buffer");
534 m_parseState = ParseJpegXLError;
535 return false;
536 }
537
538 pixels_black = reinterpret_cast<uchar *>(malloc(extra_buffer_size));
539 if (!pixels_black) {
540 free(pixels_cmy);
541 pixels_cmy = nullptr;
542 free(pixels_alpha);
543 pixels_alpha = nullptr;
544 qWarning("Memory cannot be allocated for BLACK buffer");
545 m_parseState = ParseJpegXLError;
546 return false;
547 }
548
549 if (JxlDecoderSetImageOutBuffer(m_decoder, &m_input_pixel_format, pixels_cmy, cmy_buffer_size) != JXL_DEC_SUCCESS) {
550 free(pixels_black);
551 pixels_black = nullptr;
552 free(pixels_cmy);
553 pixels_cmy = nullptr;
554 free(pixels_alpha);
555 pixels_alpha = nullptr;
556 qWarning("ERROR: JxlDecoderSetImageOutBuffer failed");
557 m_parseState = ParseJpegXLError;
558 return false;
559 }
560
561 if (JxlDecoderSetExtraChannelBuffer(m_decoder, &format_extra, pixels_black, extra_buffer_size, m_cmyk_channel_id) != JXL_DEC_SUCCESS) {
562 free(pixels_black);
563 pixels_black = nullptr;
564 free(pixels_cmy);
565 pixels_cmy = nullptr;
566 free(pixels_alpha);
567 pixels_alpha = nullptr;
568 qWarning("ERROR: JxlDecoderSetExtraChannelBuffer failed");
569 m_parseState = ParseJpegXLError;
570 return false;
571 }
572
573 if (JxlDecoderSetExtraChannelBuffer(m_decoder, &format_extra, pixels_alpha, extra_buffer_size, m_alpha_channel_id) != JXL_DEC_SUCCESS) {
574 free(pixels_black);
575 pixels_black = nullptr;
576 free(pixels_cmy);
577 pixels_cmy = nullptr;
578 free(pixels_alpha);
579 pixels_alpha = nullptr;
580 qWarning("ERROR: JxlDecoderSetExtraChannelBuffer failed");
581 m_parseState = ParseJpegXLError;
582 return false;
583 }
584
585 status = JxlDecoderProcessInput(m_decoder);
586 if (status != JXL_DEC_FULL_IMAGE) {
587 free(pixels_black);
588 pixels_black = nullptr;
589 free(pixels_cmy);
590 pixels_cmy = nullptr;
591 free(pixels_alpha);
592 pixels_alpha = nullptr;
593 qWarning("Unexpected event %d instead of JXL_DEC_FULL_IMAGE", status);
594 m_parseState = ParseJpegXLError;
595 return false;
596 }
597
598 const uchar *src_CMY = pixels_cmy;
599 const uchar *src_K = pixels_black;
600 for (int y = 0; y < tmp_cmyk_image.height(); y++) {
601 uchar *write_pointer = tmp_cmyk_image.scanLine(y);
602 for (int x = 0; x < tmp_cmyk_image.width(); x++) {
603 *write_pointer = 255 - *src_CMY; // C
604 write_pointer++;
605 src_CMY++;
606 *write_pointer = 255 - *src_CMY; // M
607 write_pointer++;
608 src_CMY++;
609 *write_pointer = 255 - *src_CMY; // Y
610 write_pointer++;
611 src_CMY++;
612 *write_pointer = 255 - *src_K; // K
613 write_pointer++;
614 src_K++;
615 }
616 }
617
618 free(pixels_black);
619 pixels_black = nullptr;
620 free(pixels_cmy);
621 pixels_cmy = nullptr;
622
624 if (m_current_image.isNull()) {
625 free(pixels_alpha);
626 pixels_alpha = nullptr;
627 qWarning("ERROR: convertedToColorSpace returned empty image");
628 m_parseState = ParseJpegXLError;
629 return false;
630 }
631
632 // set alpha channel into ARGB image
633 const uchar *src_alpha = pixels_alpha;
634 for (int y = 0; y < m_current_image.height(); y++) {
635 uchar *write_pointer = m_current_image.scanLine(y);
636 for (int x = 0; x < m_current_image.width(); x++) {
637#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
638 write_pointer += 3; // skip BGR
639 *write_pointer = *src_alpha; // A
640 write_pointer++;
641 src_alpha++;
642#else
643 *write_pointer = *src_alpha;
644 write_pointer += 4; // move 4 bytes (skip RGB)
645 src_alpha++;
646#endif
647 }
648 }
649
650 free(pixels_alpha);
651 pixels_alpha = nullptr;
652 } else { // CMYK (no alpha)
653 m_current_image = imageAlloc(m_basicinfo.xsize, m_basicinfo.ysize, QImage::Format_CMYK8888);
654 if (m_current_image.isNull()) {
655 qWarning("Memory cannot be allocated");
656 m_parseState = ParseJpegXLError;
657 return false;
658 }
659
660 m_current_image.setColorSpace(m_colorspace);
661
662 pixels_cmy = reinterpret_cast<uchar *>(malloc(cmy_buffer_size));
663 if (!pixels_cmy) {
664 qWarning("Memory cannot be allocated for CMY buffer");
665 m_parseState = ParseJpegXLError;
666 return false;
667 }
668
669 pixels_black = reinterpret_cast<uchar *>(malloc(extra_buffer_size));
670 if (!pixels_black) {
671 free(pixels_cmy);
672 pixels_cmy = nullptr;
673 qWarning("Memory cannot be allocated for BLACK buffer");
674 m_parseState = ParseJpegXLError;
675 return false;
676 }
677
678 if (JxlDecoderSetImageOutBuffer(m_decoder, &m_input_pixel_format, pixels_cmy, cmy_buffer_size) != JXL_DEC_SUCCESS) {
679 free(pixels_black);
680 pixels_black = nullptr;
681 free(pixels_cmy);
682 pixels_cmy = nullptr;
683 qWarning("ERROR: JxlDecoderSetImageOutBuffer failed");
684 m_parseState = ParseJpegXLError;
685 return false;
686 }
687
688 if (JxlDecoderSetExtraChannelBuffer(m_decoder, &format_extra, pixels_black, extra_buffer_size, m_cmyk_channel_id) != JXL_DEC_SUCCESS) {
689 free(pixels_black);
690 pixels_black = nullptr;
691 free(pixels_cmy);
692 pixels_cmy = nullptr;
693 qWarning("ERROR: JxlDecoderSetExtraChannelBuffer failed");
694 m_parseState = ParseJpegXLError;
695 return false;
696 }
697
698 status = JxlDecoderProcessInput(m_decoder);
699 if (status != JXL_DEC_FULL_IMAGE) {
700 free(pixels_black);
701 pixels_black = nullptr;
702 free(pixels_cmy);
703 pixels_cmy = nullptr;
704 qWarning("Unexpected event %d instead of JXL_DEC_FULL_IMAGE", status);
705 m_parseState = ParseJpegXLError;
706 return false;
707 }
708
709 const uchar *src_CMY = pixels_cmy;
710 const uchar *src_K = pixels_black;
711 for (int y = 0; y < m_current_image.height(); y++) {
712 uchar *write_pointer = m_current_image.scanLine(y);
713 for (int x = 0; x < m_current_image.width(); x++) {
714 *write_pointer = 255 - *src_CMY; // C
715 write_pointer++;
716 src_CMY++;
717 *write_pointer = 255 - *src_CMY; // M
718 write_pointer++;
719 src_CMY++;
720 *write_pointer = 255 - *src_CMY; // Y
721 write_pointer++;
722 src_CMY++;
723 *write_pointer = 255 - *src_K; // K
724 write_pointer++;
725 src_K++;
726 }
727 }
728
729 free(pixels_black);
730 pixels_black = nullptr;
731 free(pixels_cmy);
732 pixels_cmy = nullptr;
733 }
734#else
735 // CMYK not supported in older Qt
736 m_parseState = ParseJpegXLError;
737 return false;
738#endif
739 } else { // RGB or GRAY
740 m_current_image = imageAlloc(m_basicinfo.xsize, m_basicinfo.ysize, m_input_image_format);
741 if (m_current_image.isNull()) {
742 qWarning("Memory cannot be allocated");
743 m_parseState = ParseJpegXLError;
744 return false;
745 }
746
747 m_current_image.setColorSpace(m_colorspace);
748
749 m_input_pixel_format.align = m_current_image.bytesPerLine();
750
751 size_t rgb_buffer_size = size_t(m_current_image.height() - 1) * size_t(m_current_image.bytesPerLine());
752 switch (m_input_pixel_format.data_type) {
753 case JXL_TYPE_FLOAT:
754 rgb_buffer_size += 4 * size_t(m_input_pixel_format.num_channels) * size_t(m_current_image.width());
755 break;
756 case JXL_TYPE_UINT8:
757 rgb_buffer_size += size_t(m_input_pixel_format.num_channels) * size_t(m_current_image.width());
758 break;
759 case JXL_TYPE_UINT16:
760 case JXL_TYPE_FLOAT16:
761 rgb_buffer_size += 2 * size_t(m_input_pixel_format.num_channels) * size_t(m_current_image.width());
762 break;
763 default:
764 qWarning("ERROR: unsupported data type");
765 m_parseState = ParseJpegXLError;
766 return false;
767 break;
768 }
769
770 if (JxlDecoderSetImageOutBuffer(m_decoder, &m_input_pixel_format, m_current_image.bits(), rgb_buffer_size) != JXL_DEC_SUCCESS) {
771 qWarning("ERROR: JxlDecoderSetImageOutBuffer failed");
772 m_parseState = ParseJpegXLError;
773 return false;
774 }
775
776 status = JxlDecoderProcessInput(m_decoder);
777 if (status != JXL_DEC_FULL_IMAGE) {
778 qWarning("Unexpected event %d instead of JXL_DEC_FULL_IMAGE", status);
779 m_parseState = ParseJpegXLError;
780 return false;
781 }
782
783 if (m_target_image_format != m_input_image_format) {
784 m_current_image.convertTo(m_target_image_format);
785 }
786 }
787
788 if (!m_xmp.isEmpty()) {
789 m_current_image.setText(QStringLiteral(META_KEY_XMP_ADOBE), QString::fromUtf8(m_xmp));
790 }
791
792 if (!m_exif.isEmpty()) {
793 auto exif = MicroExif::fromByteArray(m_exif);
794 exif.updateImageResolution(m_current_image);
795 exif.updateImageMetadata(m_current_image);
796 }
797
798 m_next_image_delay = m_framedelays[m_currentimage_index];
799 m_previousimage_index = m_currentimage_index;
800
801 if (m_framedelays.count() > 1) {
802 m_currentimage_index++;
803
804 if (m_currentimage_index >= m_framedelays.count()) {
805 if (!rewind()) {
806 return false;
807 }
808
809 // all frames in animation have been read
810 m_parseState = ParseJpegXLFinished;
811 } else {
812 m_parseState = ParseJpegXLSuccess;
813 }
814 } else {
815 // the static image has been read
816 m_parseState = ParseJpegXLFinished;
817 }
818
819 return true;
820}
821
822bool QJpegXLHandler::read(QImage *image)
823{
824 if (!ensureALLCounted()) {
825 return false;
826 }
827
828 if (m_currentimage_index == m_previousimage_index) {
829 *image = m_current_image;
830 return jumpToNextImage();
831 }
832
833 if (decode_one_frame()) {
834 *image = m_current_image;
835 return true;
836 } else {
837 return false;
838 }
839}
840
841bool QJpegXLHandler::write(const QImage &image)
842{
843 if (image.format() == QImage::Format_Invalid) {
844 qWarning("No image data to save");
845 return false;
846 }
847
848 if ((image.width() == 0) || (image.height() == 0)) {
849 qWarning("Image has zero dimension!");
850 return false;
851 }
852
853 if ((image.width() > MAX_IMAGE_WIDTH) || (image.height() > MAX_IMAGE_HEIGHT)) {
854 qWarning("Image (%dx%d) is too large to save!", image.width(), image.height());
855 return false;
856 }
857
858 size_t pixel_count = size_t(image.width()) * image.height();
859 if (MAX_IMAGE_PIXELS && pixel_count > MAX_IMAGE_PIXELS) {
860 qWarning("Image (%dx%d) will not be saved because it has more than %d megapixels!", image.width(), image.height(), MAX_IMAGE_PIXELS / 1024 / 1024);
861 return false;
862 }
863
864 JxlEncoder *encoder = JxlEncoderCreate(nullptr);
865 if (!encoder) {
866 qWarning("Failed to create Jxl encoder");
867 return false;
868 }
869
870 void *runner = nullptr;
871 int num_worker_threads = qBound(1, QThread::idealThreadCount(), 64);
872
873 if (num_worker_threads > 1) {
874 runner = JxlThreadParallelRunnerCreate(nullptr, num_worker_threads);
875 if (JxlEncoderSetParallelRunner(encoder, JxlThreadParallelRunner, runner) != JXL_ENC_SUCCESS) {
876 qWarning("JxlEncoderSetParallelRunner failed");
877 JxlThreadParallelRunnerDestroy(runner);
878 JxlEncoderDestroy(encoder);
879 return false;
880 }
881 }
882
883 if (m_quality > 100) {
884 m_quality = 100;
885 } else if (m_quality < 0) {
886 m_quality = 90;
887 }
888
889 JxlEncoderUseContainer(encoder, JXL_TRUE);
890 JxlEncoderUseBoxes(encoder);
891
892 JxlBasicInfo output_info;
893 JxlEncoderInitBasicInfo(&output_info);
894 output_info.have_container = JXL_TRUE;
895
896 output_info.animation.tps_numerator = 10;
897 output_info.animation.tps_denominator = 1;
898 output_info.orientation = JXL_ORIENT_IDENTITY;
899 if (m_transformations == QImageIOHandler::TransformationMirror) {
900 output_info.orientation = JXL_ORIENT_FLIP_HORIZONTAL;
901 } else if (m_transformations == QImageIOHandler::TransformationRotate180) {
902 output_info.orientation = JXL_ORIENT_ROTATE_180;
903 } else if (m_transformations == QImageIOHandler::TransformationFlip) {
904 output_info.orientation = JXL_ORIENT_FLIP_VERTICAL;
905 } else if (m_transformations == QImageIOHandler::TransformationFlipAndRotate90) {
906 output_info.orientation = JXL_ORIENT_TRANSPOSE;
907 } else if (m_transformations == QImageIOHandler::TransformationRotate90) {
908 output_info.orientation = JXL_ORIENT_ROTATE_90_CW;
909 } else if (m_transformations == QImageIOHandler::TransformationMirrorAndRotate90) {
910 output_info.orientation = JXL_ORIENT_ANTI_TRANSPOSE;
911 } else if (m_transformations == QImageIOHandler::TransformationRotate270) {
912 output_info.orientation = JXL_ORIENT_ROTATE_90_CCW;
913 }
914
915 bool save_cmyk = false;
916#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
917 if (image.format() == QImage::Format_CMYK8888 && image.colorSpace().isValid() && image.colorSpace().colorModel() == QColorSpace::ColorModel::Cmyk) {
918 save_cmyk = true;
919 }
920#endif
921
922 JxlEncoderStatus status;
923 JxlPixelFormat pixel_format;
924 pixel_format.endianness = JXL_NATIVE_ENDIAN;
925 pixel_format.align = 0;
926
927 auto exif_data = MicroExif::fromImage(image).toByteArray();
928 auto xmp_data = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8();
929
930 if (save_cmyk) { // CMYK is always lossless
931#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
932 output_info.uses_original_profile = JXL_TRUE;
933 output_info.xsize = image.width();
934 output_info.ysize = image.height();
935 output_info.num_color_channels = 3;
936 output_info.bits_per_sample = 8;
937 output_info.alpha_bits = 0;
938 output_info.num_extra_channels = 1;
939
940 pixel_format.num_channels = 3;
941 pixel_format.data_type = JXL_TYPE_UINT8;
942
943 JxlPixelFormat format_extra;
944 format_extra.num_channels = 1;
945 format_extra.data_type = JXL_TYPE_UINT8;
946 format_extra.endianness = JXL_NATIVE_ENDIAN;
947 format_extra.align = 0;
948
949 JxlExtraChannelInfo extra_black_channel;
950 JxlEncoderInitExtraChannelInfo(JXL_CHANNEL_BLACK, &extra_black_channel);
951 extra_black_channel.bits_per_sample = output_info.bits_per_sample;
952 extra_black_channel.exponent_bits_per_sample = output_info.exponent_bits_per_sample;
953
954 const QByteArray cmyk_profile = image.colorSpace().iccProfile();
955 if (cmyk_profile.isEmpty()) {
956 qWarning("ERROR saving CMYK JXL: empty ICC profile");
957 if (runner) {
958 JxlThreadParallelRunnerDestroy(runner);
959 }
960 JxlEncoderDestroy(encoder);
961 return false;
962 }
963
964 status = JxlEncoderSetBasicInfo(encoder, &output_info);
965 if (status != JXL_ENC_SUCCESS) {
966 qWarning("JxlEncoderSetBasicInfo for CMYK image failed!");
967 if (runner) {
968 JxlThreadParallelRunnerDestroy(runner);
969 }
970 JxlEncoderDestroy(encoder);
971 return false;
972 }
973
974 status = JxlEncoderSetExtraChannelInfo(encoder, 0, &extra_black_channel);
975 if (status != JXL_ENC_SUCCESS) {
976 qWarning("JxlEncoderSetExtraChannelInfo for CMYK image failed!");
977 if (runner) {
978 JxlThreadParallelRunnerDestroy(runner);
979 }
980 JxlEncoderDestroy(encoder);
981 return false;
982 }
983
984 status = JxlEncoderSetICCProfile(encoder, reinterpret_cast<const uint8_t *>(cmyk_profile.constData()), cmyk_profile.size());
985 if (status != JXL_ENC_SUCCESS) {
986 qWarning("JxlEncoderSetICCProfile for CMYK image failed!");
987 if (runner) {
988 JxlThreadParallelRunnerDestroy(runner);
989 }
990 JxlEncoderDestroy(encoder);
991 return false;
992 }
993
994 if (!exif_data.isEmpty()) {
995 exif_data = QByteArray::fromHex("00000000") + exif_data;
996 const char *box_type = "Exif";
997 status = JxlEncoderAddBox(encoder, box_type, reinterpret_cast<const uint8_t *>(exif_data.constData()), exif_data.size(), JXL_FALSE);
998 if (status != JXL_ENC_SUCCESS) {
999 qWarning("JxlEncoderAddBox failed!");
1000 if (runner) {
1001 JxlThreadParallelRunnerDestroy(runner);
1002 }
1003 JxlEncoderDestroy(encoder);
1004 return false;
1005 }
1006 }
1007
1008 if (!xmp_data.isEmpty()) {
1009 const char *box_type = "xml ";
1010 status = JxlEncoderAddBox(encoder, box_type, reinterpret_cast<const uint8_t *>(xmp_data.constData()), xmp_data.size(), JXL_FALSE);
1011 if (status != JXL_ENC_SUCCESS) {
1012 qWarning("JxlEncoderAddBox failed!");
1013 if (runner) {
1014 JxlThreadParallelRunnerDestroy(runner);
1015 }
1016 JxlEncoderDestroy(encoder);
1017 return false;
1018 }
1019 }
1020 JxlEncoderCloseBoxes(encoder); // no more metadata
1021
1022 const size_t extra_buffer_size = size_t(image.width()) * size_t(image.height());
1023 const size_t cmy_buffer_size = extra_buffer_size * 3;
1024
1025 uchar *pixels_cmy = nullptr;
1026 uchar *pixels_black = nullptr;
1027
1028 pixels_cmy = reinterpret_cast<uchar *>(malloc(cmy_buffer_size));
1029 if (!pixels_cmy) {
1030 qWarning("Memory cannot be allocated for CMY buffer");
1031 if (runner) {
1032 JxlThreadParallelRunnerDestroy(runner);
1033 }
1034 JxlEncoderDestroy(encoder);
1035 return false;
1036 }
1037
1038 pixels_black = reinterpret_cast<uchar *>(malloc(extra_buffer_size));
1039 if (!pixels_black) {
1040 qWarning("Memory cannot be allocated for BLACK buffer");
1041 free(pixels_cmy);
1042 pixels_cmy = nullptr;
1043
1044 if (runner) {
1045 JxlThreadParallelRunnerDestroy(runner);
1046 }
1047 JxlEncoderDestroy(encoder);
1048 return false;
1049 }
1050
1051 uchar *dest_CMY = pixels_cmy;
1052 uchar *dest_K = pixels_black;
1053 for (int y = 0; y < image.height(); y++) {
1054 const uchar *src_CMYK = image.constScanLine(y);
1055 for (int x = 0; x < image.width(); x++) {
1056 *dest_CMY = 255 - *src_CMYK; // C
1057 dest_CMY++;
1058 src_CMYK++;
1059 *dest_CMY = 255 - *src_CMYK; // M
1060 dest_CMY++;
1061 src_CMYK++;
1062 *dest_CMY = 255 - *src_CMYK; // Y
1063 dest_CMY++;
1064 src_CMYK++;
1065 *dest_K = 255 - *src_CMYK; // K
1066 dest_K++;
1067 src_CMYK++;
1068 }
1069 }
1070
1071 JxlEncoderFrameSettings *frame_settings_lossless = JxlEncoderFrameSettingsCreate(encoder, nullptr);
1072 JxlEncoderSetFrameDistance(frame_settings_lossless, 0);
1073 JxlEncoderSetFrameLossless(frame_settings_lossless, JXL_TRUE);
1074
1075 status = JxlEncoderAddImageFrame(frame_settings_lossless, &pixel_format, pixels_cmy, cmy_buffer_size);
1076 if (status == JXL_ENC_ERROR) {
1077 qWarning("JxlEncoderAddImageFrame failed!");
1078 free(pixels_black);
1079 pixels_black = nullptr;
1080 free(pixels_cmy);
1081 pixels_cmy = nullptr;
1082 if (runner) {
1083 JxlThreadParallelRunnerDestroy(runner);
1084 }
1085 JxlEncoderDestroy(encoder);
1086 return false;
1087 }
1088
1089 status = JxlEncoderSetExtraChannelBuffer(frame_settings_lossless, &format_extra, pixels_black, extra_buffer_size, 0);
1090
1091 free(pixels_black);
1092 pixels_black = nullptr;
1093 free(pixels_cmy);
1094 pixels_cmy = nullptr;
1095
1096 if (status == JXL_ENC_ERROR) {
1097 qWarning("JxlEncoderSetExtraChannelBuffer failed!");
1098 if (runner) {
1099 JxlThreadParallelRunnerDestroy(runner);
1100 }
1101 JxlEncoderDestroy(encoder);
1102 return false;
1103 }
1104#else
1105 if (runner) {
1106 JxlThreadParallelRunnerDestroy(runner);
1107 }
1108 JxlEncoderDestroy(encoder);
1109 return false;
1110#endif
1111 } else { // RGB or GRAY saving
1112 int save_depth = 8; // 8 / 16 / 32
1113 bool save_fp = false;
1114 bool is_gray = false;
1115 // depth detection
1116 switch (image.format()) {
1120#ifndef JXL_HDR_PRESERVATION_DISABLED
1121 save_depth = 32;
1122 save_fp = true;
1123 break;
1124#endif
1128#ifndef JXL_HDR_PRESERVATION_DISABLED
1129 save_depth = 16;
1130 save_fp = true;
1131 break;
1132#endif
1140 save_depth = 16;
1141 break;
1149#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
1150 case QImage::Format_CMYK8888:
1151#endif
1152 save_depth = 8;
1153 break;
1155 save_depth = 16;
1156 is_gray = true;
1157 break;
1162 save_depth = 8;
1163 is_gray = true;
1164 break;
1166 save_depth = 8;
1167 is_gray = image.isGrayscale();
1168 break;
1169 default:
1170 if (image.depth() > 32) {
1171 save_depth = 16;
1172 } else {
1173 save_depth = 8;
1174 }
1175 break;
1176 }
1177
1178 QImage::Format tmpformat;
1179
1180 if (save_depth > 8 && is_gray) { // 16bit depth gray
1181 pixel_format.data_type = JXL_TYPE_UINT16;
1182 output_info.num_color_channels = 1;
1183 output_info.bits_per_sample = 16;
1184 tmpformat = QImage::Format_Grayscale16;
1185 pixel_format.num_channels = 1;
1186 } else if (is_gray) { // 8bit depth gray
1187 pixel_format.data_type = JXL_TYPE_UINT8;
1188 output_info.num_color_channels = 1;
1189 output_info.bits_per_sample = 8;
1190 tmpformat = QImage::Format_Grayscale8;
1191 pixel_format.num_channels = 1;
1192 } else if (save_depth > 16) { // 32bit depth rgb
1193 pixel_format.data_type = JXL_TYPE_FLOAT;
1194 output_info.exponent_bits_per_sample = 8;
1195 output_info.num_color_channels = 3;
1196 output_info.bits_per_sample = 32;
1197
1198 if (image.hasAlphaChannel()) {
1199 tmpformat = QImage::Format_RGBA32FPx4;
1200 pixel_format.num_channels = 4;
1201 output_info.alpha_bits = 32;
1202 output_info.alpha_exponent_bits = 8;
1203 output_info.num_extra_channels = 1;
1204 } else {
1205 tmpformat = QImage::Format_RGBX32FPx4;
1206 pixel_format.num_channels = 3;
1207 output_info.alpha_bits = 0;
1208 output_info.num_extra_channels = 0;
1209 }
1210 } else if (save_depth > 8) { // 16bit depth rgb
1211 pixel_format.data_type = save_fp ? JXL_TYPE_FLOAT16 : JXL_TYPE_UINT16;
1212 output_info.exponent_bits_per_sample = save_fp ? 5 : 0;
1213 output_info.num_color_channels = 3;
1214 output_info.bits_per_sample = 16;
1215
1216 if (image.hasAlphaChannel()) {
1217 tmpformat = save_fp ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBA64;
1218 pixel_format.num_channels = 4;
1219 output_info.alpha_bits = 16;
1220 output_info.alpha_exponent_bits = save_fp ? 5 : 0;
1221 output_info.num_extra_channels = 1;
1222 } else {
1223 tmpformat = save_fp ? QImage::Format_RGBX16FPx4 : QImage::Format_RGBX64;
1224 pixel_format.num_channels = 3;
1225 output_info.alpha_bits = 0;
1226 output_info.num_extra_channels = 0;
1227 }
1228 } else { // 8bit depth rgb
1229 pixel_format.data_type = JXL_TYPE_UINT8;
1230 output_info.num_color_channels = 3;
1231 output_info.bits_per_sample = 8;
1232
1233 if (image.hasAlphaChannel()) {
1234 tmpformat = QImage::Format_RGBA8888;
1235 pixel_format.num_channels = 4;
1236 output_info.alpha_bits = 8;
1237 output_info.num_extra_channels = 1;
1238 } else {
1239 tmpformat = QImage::Format_RGB888;
1240 pixel_format.num_channels = 3;
1241 output_info.alpha_bits = 0;
1242 output_info.num_extra_channels = 0;
1243 }
1244 }
1245
1246#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
1247 QImage tmpimage;
1248 if (image.colorSpace().isValid()) {
1249 if (is_gray && image.colorSpace().colorModel() != QColorSpace::ColorModel::Gray) {
1250 // convert to Gray profile
1251 QPointF gray_whitePoint = image.colorSpace().whitePoint();
1252 if (gray_whitePoint.isNull()) {
1253 gray_whitePoint = QPointF(0.3127f, 0.329f);
1254 }
1255
1257 float gamma_gray = image.colorSpace().gamma();
1258 if (gray_trc == QColorSpace::TransferFunction::Custom) {
1259 gray_trc = QColorSpace::TransferFunction::SRgb;
1260 }
1261
1262 const QColorSpace gray_profile(gray_whitePoint, gray_trc, gamma_gray);
1263 if (gray_profile.isValid()) {
1264 tmpimage = image.convertedToColorSpace(gray_profile, tmpformat);
1265 } else {
1266 qWarning("JXL plugin created invalid grayscale QColorSpace!");
1267 tmpimage = image.convertToFormat(tmpformat);
1268 }
1269 } else if (!is_gray && image.colorSpace().colorModel() != QColorSpace::ColorModel::Rgb) {
1270 // convert to RGB profile
1271 QPointF whitePoint = image.colorSpace().whitePoint();
1272 if (whitePoint.isNull()) {
1273 whitePoint = QPointF(0.3127f, 0.329f);
1274 }
1275
1276 const QPointF redP(0.64f, 0.33f);
1277 const QPointF greenP(0.3f, 0.6f);
1278 const QPointF blueP(0.15f, 0.06f);
1279
1281 float gamma_rgb = image.colorSpace().gamma();
1282 if (trc_rgb == QColorSpace::TransferFunction::Custom) {
1283 trc_rgb = QColorSpace::TransferFunction::SRgb;
1284 }
1285
1286 const QColorSpace rgb_profile(whitePoint, redP, greenP, blueP, trc_rgb, gamma_rgb);
1287 if (rgb_profile.isValid()) {
1288 tmpimage = image.convertedToColorSpace(rgb_profile, tmpformat);
1289 } else {
1290 qWarning("JXL plugin created invalid RGB QColorSpace!");
1291 tmpimage = image.convertToFormat(tmpformat);
1292 }
1293 } else { // ColorSpace matches the format
1294 tmpimage = image.convertToFormat(tmpformat);
1295 }
1296 } else { // no ColorSpace or invalid
1297 tmpimage = image.convertToFormat(tmpformat);
1298 }
1299#else
1300 QImage tmpimage = image.convertToFormat(tmpformat);
1301#endif
1302
1303 output_info.xsize = tmpimage.width();
1304 output_info.ysize = tmpimage.height();
1305
1306 if (output_info.xsize == 0 || output_info.ysize == 0 || tmpimage.isNull()) {
1307 qWarning("Unable to allocate memory for output image");
1308 if (runner) {
1309 JxlThreadParallelRunnerDestroy(runner);
1310 }
1311 JxlEncoderDestroy(encoder);
1312 return false;
1313 }
1314
1315 JxlColorEncoding color_profile;
1316 JxlColorEncodingSetToSRGB(&color_profile, is_gray ? JXL_TRUE : JXL_FALSE);
1317
1318 QByteArray iccprofile;
1319
1320 if (m_quality == 100) { // try to use ICC for lossless
1321 output_info.uses_original_profile = JXL_TRUE;
1322 iccprofile = tmpimage.colorSpace().iccProfile();
1323 } else { // try to detect encoded profile (smaller than ICC)
1324 output_info.uses_original_profile = JXL_FALSE;
1325
1326 if (tmpimage.colorSpace().isValid()) {
1327 QPointF whiteP(0.3127f, 0.329f);
1328#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
1329 whiteP = image.colorSpace().whitePoint();
1330#endif
1331
1332 switch (tmpimage.colorSpace().primaries()) {
1333 case QColorSpace::Primaries::SRgb:
1334 color_profile.white_point = JXL_WHITE_POINT_D65;
1335 color_profile.primaries = JXL_PRIMARIES_SRGB;
1336 break;
1337 case QColorSpace::Primaries::AdobeRgb:
1338 color_profile.white_point = JXL_WHITE_POINT_D65;
1339 color_profile.primaries = JXL_PRIMARIES_CUSTOM;
1340 color_profile.primaries_red_xy[0] = 0.640;
1341 color_profile.primaries_red_xy[1] = 0.330;
1342 color_profile.primaries_green_xy[0] = 0.210;
1343 color_profile.primaries_green_xy[1] = 0.710;
1344 color_profile.primaries_blue_xy[0] = 0.150;
1345 color_profile.primaries_blue_xy[1] = 0.060;
1346 break;
1347 case QColorSpace::Primaries::DciP3D65:
1348 color_profile.white_point = JXL_WHITE_POINT_D65;
1349 color_profile.primaries = JXL_PRIMARIES_P3;
1350 color_profile.primaries_red_xy[0] = 0.680;
1351 color_profile.primaries_red_xy[1] = 0.320;
1352 color_profile.primaries_green_xy[0] = 0.265;
1353 color_profile.primaries_green_xy[1] = 0.690;
1354 color_profile.primaries_blue_xy[0] = 0.150;
1355 color_profile.primaries_blue_xy[1] = 0.060;
1356 break;
1357 case QColorSpace::Primaries::ProPhotoRgb:
1358 color_profile.white_point = JXL_WHITE_POINT_CUSTOM;
1359#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
1360 whiteP = QPointF(0.3457f, 0.3585f);
1361#endif
1362 color_profile.white_point_xy[0] = whiteP.x();
1363 color_profile.white_point_xy[1] = whiteP.y();
1364 color_profile.primaries = JXL_PRIMARIES_CUSTOM;
1365 color_profile.primaries_red_xy[0] = 0.7347;
1366 color_profile.primaries_red_xy[1] = 0.2653;
1367 color_profile.primaries_green_xy[0] = 0.1596;
1368 color_profile.primaries_green_xy[1] = 0.8404;
1369 color_profile.primaries_blue_xy[0] = 0.0366;
1370 color_profile.primaries_blue_xy[1] = 0.0001;
1371 break;
1372#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
1373 case QColorSpace::Primaries::Bt2020:
1374 color_profile.white_point = JXL_WHITE_POINT_D65;
1375 color_profile.primaries = JXL_PRIMARIES_2100;
1376 color_profile.primaries_red_xy[0] = 0.708;
1377 color_profile.primaries_red_xy[1] = 0.292;
1378 color_profile.primaries_green_xy[0] = 0.170;
1379 color_profile.primaries_green_xy[1] = 0.797;
1380 color_profile.primaries_blue_xy[0] = 0.131;
1381 color_profile.primaries_blue_xy[1] = 0.046;
1382 break;
1383#endif
1384 default:
1385 if (is_gray && !whiteP.isNull()) {
1386 color_profile.white_point = JXL_WHITE_POINT_CUSTOM;
1387 color_profile.white_point_xy[0] = whiteP.x();
1388 color_profile.white_point_xy[1] = whiteP.y();
1389 } else {
1390 iccprofile = tmpimage.colorSpace().iccProfile();
1391 }
1392 break;
1393 }
1394
1395 if (iccprofile.isEmpty()) {
1396 const double gamma_profile = tmpimage.colorSpace().gamma();
1397
1398 switch (tmpimage.colorSpace().transferFunction()) {
1399 case QColorSpace::TransferFunction::Linear:
1400 color_profile.transfer_function = JXL_TRANSFER_FUNCTION_LINEAR;
1401 break;
1402 case QColorSpace::TransferFunction::Gamma:
1403 if (gamma_profile > 0) {
1404 color_profile.transfer_function = JXL_TRANSFER_FUNCTION_GAMMA;
1405 color_profile.gamma = 1.0 / gamma_profile;
1406 } else {
1407 iccprofile = tmpimage.colorSpace().iccProfile();
1408 }
1409 break;
1410 case QColorSpace::TransferFunction::SRgb:
1411 color_profile.transfer_function = JXL_TRANSFER_FUNCTION_SRGB;
1412 break;
1413 default:
1414 iccprofile = tmpimage.colorSpace().iccProfile();
1415 break;
1416 }
1417 }
1418 }
1419 }
1420
1421 status = JxlEncoderSetBasicInfo(encoder, &output_info);
1422 if (status != JXL_ENC_SUCCESS) {
1423 qWarning("JxlEncoderSetBasicInfo failed!");
1424 if (runner) {
1425 JxlThreadParallelRunnerDestroy(runner);
1426 }
1427 JxlEncoderDestroy(encoder);
1428 return false;
1429 }
1430
1431 if (iccprofile.size() > 0) {
1432 status = JxlEncoderSetICCProfile(encoder, reinterpret_cast<const uint8_t *>(iccprofile.constData()), iccprofile.size());
1433 if (status != JXL_ENC_SUCCESS) {
1434 qWarning("JxlEncoderSetICCProfile failed!");
1435 if (runner) {
1436 JxlThreadParallelRunnerDestroy(runner);
1437 }
1438 JxlEncoderDestroy(encoder);
1439 return false;
1440 }
1441 } else {
1442 status = JxlEncoderSetColorEncoding(encoder, &color_profile);
1443 if (status != JXL_ENC_SUCCESS) {
1444 qWarning("JxlEncoderSetColorEncoding failed!");
1445 if (runner) {
1446 JxlThreadParallelRunnerDestroy(runner);
1447 }
1448 JxlEncoderDestroy(encoder);
1449 return false;
1450 }
1451 }
1452
1453 if (!exif_data.isEmpty()) {
1454 exif_data = QByteArray::fromHex("00000000") + exif_data;
1455 const char *box_type = "Exif";
1456 status = JxlEncoderAddBox(encoder, box_type, reinterpret_cast<const uint8_t *>(exif_data.constData()), exif_data.size(), JXL_FALSE);
1457 if (status != JXL_ENC_SUCCESS) {
1458 qWarning("JxlEncoderAddBox failed!");
1459 if (runner) {
1460 JxlThreadParallelRunnerDestroy(runner);
1461 }
1462 JxlEncoderDestroy(encoder);
1463 return false;
1464 }
1465 }
1466
1467 if (!xmp_data.isEmpty()) {
1468 const char *box_type = "xml ";
1469 status = JxlEncoderAddBox(encoder, box_type, reinterpret_cast<const uint8_t *>(xmp_data.constData()), xmp_data.size(), JXL_FALSE);
1470 if (status != JXL_ENC_SUCCESS) {
1471 qWarning("JxlEncoderAddBox failed!");
1472 if (runner) {
1473 JxlThreadParallelRunnerDestroy(runner);
1474 }
1475 JxlEncoderDestroy(encoder);
1476 return false;
1477 }
1478 }
1479 JxlEncoderCloseBoxes(encoder); // no more metadata
1480
1481 JxlEncoderFrameSettings *encoder_options = JxlEncoderFrameSettingsCreate(encoder, nullptr);
1482
1483 if (m_quality == 100) { // lossless
1484 JxlEncoderSetFrameDistance(encoder_options, 0.0f);
1485 JxlEncoderSetFrameLossless(encoder_options, JXL_TRUE);
1486 } else {
1487 JxlEncoderSetFrameDistance(encoder_options, JxlEncoderDistanceFromQuality(m_quality));
1488 JxlEncoderSetFrameLossless(encoder_options, JXL_FALSE);
1489 }
1490
1491 size_t buffer_size;
1492 if (tmpimage.format() == QImage::Format_RGBX32FPx4) { // pack 32-bit depth RGBX -> RGB
1493 buffer_size = 12 * size_t(tmpimage.width()) * size_t(tmpimage.height());
1494
1495 float *packed_pixels32 = reinterpret_cast<float *>(malloc(buffer_size));
1496 if (!packed_pixels32) {
1497 qWarning("ERROR: JXL plug-in failed to allocate memory");
1498 return false;
1499 }
1500
1501 float *dest_pixels32 = packed_pixels32;
1502 for (int y = 0; y < tmpimage.height(); y++) {
1503 const float *src_pixels32 = reinterpret_cast<const float *>(tmpimage.constScanLine(y));
1504 for (int x = 0; x < tmpimage.width(); x++) {
1505 *dest_pixels32 = *src_pixels32; // R
1506 dest_pixels32++;
1507 src_pixels32++;
1508 *dest_pixels32 = *src_pixels32; // G
1509 dest_pixels32++;
1510 src_pixels32++;
1511 *dest_pixels32 = *src_pixels32; // B
1512 dest_pixels32++;
1513 src_pixels32 += 2; // skip X
1514 }
1515 }
1516
1517 status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, packed_pixels32, buffer_size);
1518 free(packed_pixels32);
1519 } else if (tmpimage.format() == QImage::Format_RGBX16FPx4 || tmpimage.format() == QImage::Format_RGBX64) {
1520 // pack 16-bit depth RGBX -> RGB
1521 buffer_size = 6 * size_t(tmpimage.width()) * size_t(tmpimage.height());
1522
1523 quint16 *packed_pixels16 = reinterpret_cast<quint16 *>(malloc(buffer_size));
1524 if (!packed_pixels16) {
1525 qWarning("ERROR: JXL plug-in failed to allocate memory");
1526 return false;
1527 }
1528
1529 quint16 *dest_pixels16 = packed_pixels16;
1530 for (int y = 0; y < tmpimage.height(); y++) {
1531 const quint16 *src_pixels16 = reinterpret_cast<const quint16 *>(tmpimage.constScanLine(y));
1532 for (int x = 0; x < tmpimage.width(); x++) {
1533 *dest_pixels16 = *src_pixels16; // R
1534 dest_pixels16++;
1535 src_pixels16++;
1536 *dest_pixels16 = *src_pixels16; // G
1537 dest_pixels16++;
1538 src_pixels16++;
1539 *dest_pixels16 = *src_pixels16; // B
1540 dest_pixels16++;
1541 src_pixels16 += 2; // skip X
1542 }
1543 }
1544
1545 status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, packed_pixels16, buffer_size);
1546 free(packed_pixels16);
1547 } else { // use QImage's data directly
1548 pixel_format.align = tmpimage.bytesPerLine();
1549
1550 buffer_size = size_t(tmpimage.height() - 1) * size_t(tmpimage.bytesPerLine());
1551 switch (pixel_format.data_type) {
1552 case JXL_TYPE_FLOAT:
1553 buffer_size += 4 * size_t(pixel_format.num_channels) * size_t(tmpimage.width());
1554 break;
1555 case JXL_TYPE_UINT8:
1556 buffer_size += size_t(pixel_format.num_channels) * size_t(tmpimage.width());
1557 break;
1558 case JXL_TYPE_UINT16:
1559 case JXL_TYPE_FLOAT16:
1560 buffer_size += 2 * size_t(pixel_format.num_channels) * size_t(tmpimage.width());
1561 break;
1562 default:
1563 qWarning("ERROR: unsupported data type");
1564 return false;
1565 break;
1566 }
1567
1568 status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, tmpimage.constBits(), buffer_size);
1569 }
1570
1571 if (status == JXL_ENC_ERROR) {
1572 qWarning("JxlEncoderAddImageFrame failed!");
1573 if (runner) {
1574 JxlThreadParallelRunnerDestroy(runner);
1575 }
1576 JxlEncoderDestroy(encoder);
1577 return false;
1578 }
1579 }
1580
1581 JxlEncoderCloseFrames(encoder);
1582
1583 std::vector<uint8_t> compressed;
1584 compressed.resize(4096);
1585 size_t offset = 0;
1586 uint8_t *next_out;
1587 size_t avail_out;
1588 do {
1589 next_out = compressed.data() + offset;
1590 avail_out = compressed.size() - offset;
1591 status = JxlEncoderProcessOutput(encoder, &next_out, &avail_out);
1592
1593 if (status == JXL_ENC_NEED_MORE_OUTPUT) {
1594 offset = next_out - compressed.data();
1595 compressed.resize(compressed.size() * 2);
1596 } else if (status == JXL_ENC_ERROR) {
1597 qWarning("JxlEncoderProcessOutput failed!");
1598 if (runner) {
1599 JxlThreadParallelRunnerDestroy(runner);
1600 }
1601 JxlEncoderDestroy(encoder);
1602 return false;
1603 }
1604 } while (status != JXL_ENC_SUCCESS);
1605
1606 if (runner) {
1607 JxlThreadParallelRunnerDestroy(runner);
1608 }
1609 JxlEncoderDestroy(encoder);
1610
1611 compressed.resize(next_out - compressed.data());
1612
1613 if (compressed.size() > 0) {
1614 qint64 write_status = device()->write(reinterpret_cast<const char *>(compressed.data()), compressed.size());
1615
1616 if (write_status > 0) {
1617 return true;
1618 } else if (write_status == -1) {
1619 qWarning("Write error: %s\n", qUtf8Printable(device()->errorString()));
1620 }
1621 }
1622
1623 return false;
1624}
1625
1626QVariant QJpegXLHandler::option(ImageOption option) const
1627{
1628 if (!supportsOption(option)) {
1629 return QVariant();
1630 }
1631
1632 if (option == Quality) {
1633 return m_quality;
1634 }
1635
1636 if (!ensureParsed()) {
1637#ifdef JXL_QT_AUTOTRANSFORM
1638 if (option == ImageTransformation) {
1639 return int(m_transformations);
1640 }
1641#endif
1642 return QVariant();
1643 }
1644
1645 switch (option) {
1646 case Size:
1647 return QSize(m_basicinfo.xsize, m_basicinfo.ysize);
1648 case Animation:
1649 if (m_basicinfo.have_animation) {
1650 return true;
1651 } else {
1652 return false;
1653 }
1654#ifdef JXL_QT_AUTOTRANSFORM
1655 case ImageTransformation:
1656 if (m_basicinfo.orientation == JXL_ORIENT_IDENTITY) {
1658 } else if (m_basicinfo.orientation == JXL_ORIENT_FLIP_HORIZONTAL) {
1660 } else if (m_basicinfo.orientation == JXL_ORIENT_ROTATE_180) {
1662 } else if (m_basicinfo.orientation == JXL_ORIENT_FLIP_VERTICAL) {
1664 } else if (m_basicinfo.orientation == JXL_ORIENT_TRANSPOSE) {
1666 } else if (m_basicinfo.orientation == JXL_ORIENT_ROTATE_90_CW) {
1668 } else if (m_basicinfo.orientation == JXL_ORIENT_ANTI_TRANSPOSE) {
1670 } else if (m_basicinfo.orientation == JXL_ORIENT_ROTATE_90_CCW) {
1672 }
1673 break;
1674#endif
1675 default:
1676 return QVariant();
1677 }
1678
1679 return QVariant();
1680}
1681
1682void QJpegXLHandler::setOption(ImageOption option, const QVariant &value)
1683{
1684 switch (option) {
1685 case Quality:
1686 m_quality = value.toInt();
1687 if (m_quality > 100) {
1688 m_quality = 100;
1689 } else if (m_quality < 0) {
1690 m_quality = 90;
1691 }
1692 return;
1693#ifdef JXL_QT_AUTOTRANSFORM
1694 case ImageTransformation:
1695 if (auto t = value.toInt()) {
1696 if (t > 0 && t < 8)
1697 m_transformations = QImageIOHandler::Transformations(t);
1698 }
1699 break;
1700#endif
1701 default:
1702 break;
1703 }
1704 QImageIOHandler::setOption(option, value);
1705}
1706
1707bool QJpegXLHandler::supportsOption(ImageOption option) const
1708{
1709 auto supported = option == Quality || option == Size || option == Animation;
1710#ifdef JXL_QT_AUTOTRANSFORM
1711 supported = supported || option == ImageTransformation;
1712#endif
1713 return supported;
1714}
1715
1716int QJpegXLHandler::imageCount() const
1717{
1718 if (!ensureParsed()) {
1719 return 0;
1720 }
1721
1722 if (m_parseState == ParseJpegXLBasicInfoParsed) {
1723 if (!m_basicinfo.have_animation) {
1724 return 1;
1725 }
1726
1727 if (!ensureALLCounted()) {
1728 return 0;
1729 }
1730 }
1731
1732 if (!m_framedelays.isEmpty()) {
1733 return m_framedelays.count();
1734 }
1735 return 0;
1736}
1737
1738int QJpegXLHandler::currentImageNumber() const
1739{
1740 if (m_parseState == ParseJpegXLNotParsed) {
1741 return -1;
1742 }
1743
1744 if (m_parseState == ParseJpegXLError || m_parseState == ParseJpegXLBasicInfoParsed || !m_decoder) {
1745 return 0;
1746 }
1747
1748 return m_currentimage_index;
1749}
1750
1751bool QJpegXLHandler::jumpToNextImage()
1752{
1753 if (!ensureALLCounted()) {
1754 return false;
1755 }
1756
1757 if (m_framedelays.count() > 1) {
1758 m_currentimage_index++;
1759
1760 if (m_currentimage_index >= m_framedelays.count()) {
1761 if (!rewind()) {
1762 return false;
1763 }
1764 } else {
1765 JxlDecoderSkipFrames(m_decoder, 1);
1766 }
1767 }
1768
1769 m_parseState = ParseJpegXLSuccess;
1770 return true;
1771}
1772
1773bool QJpegXLHandler::jumpToImage(int imageNumber)
1774{
1775 if (!ensureALLCounted()) {
1776 return false;
1777 }
1778
1779 if (imageNumber < 0 || imageNumber >= m_framedelays.count()) {
1780 return false;
1781 }
1782
1783 if (imageNumber == m_currentimage_index) {
1784 m_parseState = ParseJpegXLSuccess;
1785 return true;
1786 }
1787
1788 if (imageNumber > m_currentimage_index) {
1789 JxlDecoderSkipFrames(m_decoder, imageNumber - m_currentimage_index);
1790 m_currentimage_index = imageNumber;
1791 m_parseState = ParseJpegXLSuccess;
1792 return true;
1793 }
1794
1795 if (!rewind()) {
1796 return false;
1797 }
1798
1799 if (imageNumber > 0) {
1800 JxlDecoderSkipFrames(m_decoder, imageNumber);
1801 }
1802 m_currentimage_index = imageNumber;
1803 m_parseState = ParseJpegXLSuccess;
1804 return true;
1805}
1806
1807int QJpegXLHandler::nextImageDelay() const
1808{
1809 if (!ensureALLCounted()) {
1810 return 0;
1811 }
1812
1813 if (m_framedelays.count() < 2) {
1814 return 0;
1815 }
1816
1817 return m_next_image_delay;
1818}
1819
1820int QJpegXLHandler::loopCount() const
1821{
1822 if (!ensureParsed()) {
1823 return 0;
1824 }
1825
1826 if (m_basicinfo.have_animation) {
1827 return (m_basicinfo.animation.num_loops > 0) ? m_basicinfo.animation.num_loops - 1 : -1;
1828 } else {
1829 return 0;
1830 }
1831}
1832
1833bool QJpegXLHandler::rewind()
1834{
1835 m_currentimage_index = 0;
1836
1837 JxlDecoderReleaseInput(m_decoder);
1838 JxlDecoderRewind(m_decoder);
1839 if (m_runner) {
1840 if (JxlDecoderSetParallelRunner(m_decoder, JxlThreadParallelRunner, m_runner) != JXL_DEC_SUCCESS) {
1841 qWarning("ERROR: JxlDecoderSetParallelRunner failed");
1842 m_parseState = ParseJpegXLError;
1843 return false;
1844 }
1845 }
1846
1847 if (JxlDecoderSetInput(m_decoder, reinterpret_cast<const uint8_t *>(m_rawData.constData()), m_rawData.size()) != JXL_DEC_SUCCESS) {
1848 qWarning("ERROR: JxlDecoderSetInput failed");
1849 m_parseState = ParseJpegXLError;
1850 return false;
1851 }
1852
1853 JxlDecoderCloseInput(m_decoder);
1854
1855 if (m_basicinfo.uses_original_profile == JXL_FALSE && m_basicinfo.have_animation == JXL_FALSE) {
1856 if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
1857 qWarning("ERROR: JxlDecoderSubscribeEvents failed");
1858 m_parseState = ParseJpegXLError;
1859 return false;
1860 }
1861
1862 JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder);
1863 if (status != JXL_DEC_COLOR_ENCODING) {
1864 qWarning("Unexpected event %d instead of JXL_DEC_COLOR_ENCODING", status);
1865 m_parseState = ParseJpegXLError;
1866 return false;
1867 }
1868
1869 const JxlCmsInterface *jxlcms = JxlGetDefaultCms();
1870 if (jxlcms) {
1871 status = JxlDecoderSetCms(m_decoder, *jxlcms);
1872 if (status != JXL_DEC_SUCCESS) {
1873 qWarning("JxlDecoderSetCms ERROR");
1874 }
1875 } else {
1876 qWarning("No JPEG XL CMS Interface");
1877 }
1878
1879 bool is_gray = m_basicinfo.num_color_channels == 1 && m_basicinfo.alpha_bits == 0;
1880 JxlColorEncoding color_encoding;
1881 JxlColorEncodingSetToSRGB(&color_encoding, is_gray ? JXL_TRUE : JXL_FALSE);
1882 JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding);
1883 } else {
1884 if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
1885 qWarning("ERROR: JxlDecoderSubscribeEvents failed");
1886 m_parseState = ParseJpegXLError;
1887 return false;
1888 }
1889 }
1890
1891 return true;
1892}
1893
1894bool QJpegXLHandler::decodeContainer()
1895{
1896#if JPEGXL_NUMERIC_VERSION >= JPEGXL_COMPUTE_NUMERIC_VERSION(0, 11, 0)
1897 if (m_basicinfo.have_container == JXL_FALSE) {
1898 return true;
1899 }
1900
1901 const size_t len = m_rawData.size();
1902 if (len == 0) {
1903 m_parseState = ParseJpegXLError;
1904 return false;
1905 }
1906
1907 const uint8_t *buf = reinterpret_cast<const uint8_t *>(m_rawData.constData());
1908 if (JxlSignatureCheck(buf, len) != JXL_SIG_CONTAINER) {
1909 return true;
1910 }
1911
1912 JxlDecoderReleaseInput(m_decoder);
1913 JxlDecoderRewind(m_decoder);
1914
1915 if (JxlDecoderSetInput(m_decoder, buf, len) != JXL_DEC_SUCCESS) {
1916 qWarning("ERROR: JxlDecoderSetInput failed");
1917 m_parseState = ParseJpegXLError;
1918 return false;
1919 }
1920
1921 JxlDecoderCloseInput(m_decoder);
1922
1923 if (JxlDecoderSetDecompressBoxes(m_decoder, JXL_TRUE) != JXL_DEC_SUCCESS) {
1924 qWarning("WARNING: JxlDecoderSetDecompressBoxes failed");
1925 }
1926
1927 if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_BOX | JXL_DEC_BOX_COMPLETE) != JXL_DEC_SUCCESS) {
1928 qWarning("ERROR: JxlDecoderSubscribeEvents failed");
1929 m_parseState = ParseJpegXLError;
1930 return false;
1931 }
1932
1933 bool search_exif = true;
1934 bool search_xmp = true;
1935 JxlBoxType box_type;
1936
1937 QByteArray exifBox;
1938 QByteArray xmpBox;
1939
1940 while (search_exif || search_xmp) {
1941 JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder);
1942 switch (status) {
1943 case JXL_DEC_SUCCESS:
1944 search_exif = false;
1945 search_xmp = false;
1946 break;
1947 case JXL_DEC_BOX:
1948 status = JxlDecoderGetBoxType(m_decoder, box_type, JXL_TRUE);
1949 if (status != JXL_DEC_SUCCESS) {
1950 qWarning("Error in JxlDecoderGetBoxType");
1951 m_parseState = ParseJpegXLError;
1952 return false;
1953 }
1954
1955 if (box_type[0] == 'E' && box_type[1] == 'x' && box_type[2] == 'i' && box_type[3] == 'f' && search_exif) {
1956 search_exif = false;
1957 if (!extractBox(exifBox, len)) {
1958 return false;
1959 }
1960 } else if (box_type[0] == 'x' && box_type[1] == 'm' && box_type[2] == 'l' && box_type[3] == ' ' && search_xmp) {
1961 search_xmp = false;
1962 if (!extractBox(xmpBox, len)) {
1963 return false;
1964 }
1965 }
1966 break;
1967 case JXL_DEC_ERROR:
1968 qWarning("JXL Metadata decoding error");
1969 m_parseState = ParseJpegXLError;
1970 return false;
1971 break;
1972 case JXL_DEC_NEED_MORE_INPUT:
1973 qWarning("JXL metadata are probably incomplete");
1974 m_parseState = ParseJpegXLError;
1975 return false;
1976 break;
1977 default:
1978 qWarning("Unexpected event %d instead of JXL_DEC_BOX", status);
1979 m_parseState = ParseJpegXLError;
1980 return false;
1981 break;
1982 }
1983 }
1984
1985 if (xmpBox.size() > 0) {
1986 m_xmp = xmpBox;
1987 }
1988
1989 if (exifBox.size() > 4) {
1990 const char tiffHeaderBE[4] = {'M', 'M', 0, 42};
1991 const char tiffHeaderLE[4] = {'I', 'I', 42, 0};
1992 const QByteArray tiffBE = QByteArray::fromRawData(tiffHeaderBE, 4);
1993 const QByteArray tiffLE = QByteArray::fromRawData(tiffHeaderLE, 4);
1994 auto headerindexBE = exifBox.indexOf(tiffBE);
1995 auto headerindexLE = exifBox.indexOf(tiffLE);
1996
1997 if (headerindexLE != -1) {
1998 if (headerindexBE == -1) {
1999 m_exif = exifBox.mid(headerindexLE);
2000 } else {
2001 m_exif = exifBox.mid((headerindexLE <= headerindexBE) ? headerindexLE : headerindexBE);
2002 }
2003 } else if (headerindexBE != -1) {
2004 m_exif = exifBox.mid(headerindexBE);
2005 } else {
2006 qWarning("Exif box in JXL file doesn't have TIFF header");
2007 }
2008 }
2009#endif
2010 return true;
2011}
2012
2013bool QJpegXLHandler::extractBox(QByteArray &output, size_t container_size)
2014{
2015#if JPEGXL_NUMERIC_VERSION >= JPEGXL_COMPUTE_NUMERIC_VERSION(0, 11, 0)
2016 uint64_t rawboxsize = 0;
2017 JxlDecoderStatus status = JxlDecoderGetBoxSizeRaw(m_decoder, &rawboxsize);
2018 if (status != JXL_DEC_SUCCESS) {
2019 qWarning("ERROR: JxlDecoderGetBoxSizeRaw failed");
2020 m_parseState = ParseJpegXLError;
2021 return false;
2022 }
2023
2024 if (rawboxsize > container_size) {
2025 qWarning("JXL metadata box is incomplete");
2026 m_parseState = ParseJpegXLError;
2027 return false;
2028 }
2029
2030 output.resize(rawboxsize);
2031 status = JxlDecoderSetBoxBuffer(m_decoder, reinterpret_cast<uint8_t *>(output.data()), output.size());
2032 if (status != JXL_DEC_SUCCESS) {
2033 qWarning("ERROR: JxlDecoderSetBoxBuffer failed");
2034 m_parseState = ParseJpegXLError;
2035 return false;
2036 }
2037
2038 do {
2039 status = JxlDecoderProcessInput(m_decoder);
2040 if (status == JXL_DEC_BOX_NEED_MORE_OUTPUT) {
2041 size_t bytes_remains = JxlDecoderReleaseBoxBuffer(m_decoder);
2042
2043 if (output.size() > 4194304) { // approx. 4MB limit for decompressed metadata box
2044 qWarning("JXL metadata box is too large");
2045 m_parseState = ParseJpegXLError;
2046 return false;
2047 }
2048
2049 output.append(16384, '\0');
2050 size_t extension_size = 16384 + bytes_remains;
2051 uint8_t *extension_buffer = reinterpret_cast<uint8_t *>(output.data()) + (output.size() - extension_size);
2052
2053 if (JxlDecoderSetBoxBuffer(m_decoder, extension_buffer, extension_size) != JXL_DEC_SUCCESS) {
2054 qWarning("ERROR: JxlDecoderSetBoxBuffer failed after JXL_DEC_BOX_NEED_MORE_OUTPUT");
2055 m_parseState = ParseJpegXLError;
2056 return false;
2057 }
2058 }
2059 } while (status == JXL_DEC_BOX_NEED_MORE_OUTPUT);
2060
2061 if (status != JXL_DEC_BOX_COMPLETE) {
2062 qWarning("Unexpected event %d instead of JXL_DEC_BOX_COMPLETE", status);
2063 m_parseState = ParseJpegXLError;
2064 return false;
2065 }
2066
2067 size_t unused_bytes = JxlDecoderReleaseBoxBuffer(m_decoder);
2068 output.chop(unused_bytes);
2069#endif
2070 return true;
2071}
2072
2073QImageIOPlugin::Capabilities QJpegXLPlugin::capabilities(QIODevice *device, const QByteArray &format) const
2074{
2075 if (format == "jxl") {
2076 return Capabilities(CanRead | CanWrite);
2077 }
2078
2079 if (!format.isEmpty()) {
2080 return {};
2081 }
2082 if (!device->isOpen()) {
2083 return {};
2084 }
2085
2086 Capabilities cap;
2087 if (device->isReadable() && QJpegXLHandler::canRead(device)) {
2088 cap |= CanRead;
2089 }
2090
2091 if (device->isWritable()) {
2092 cap |= CanWrite;
2093 }
2094
2095 return cap;
2096}
2097
2098QImageIOHandler *QJpegXLPlugin::create(QIODevice *device, const QByteArray &format) const
2099{
2100 QImageIOHandler *handler = new QJpegXLHandler;
2101 handler->setDevice(device);
2102 handler->setFormat(format);
2103 return handler;
2104}
2105
2106#include "moc_jxl_p.cpp"
Q_SCRIPTABLE CaptureState status()
QFlags< Capability > Capabilities
QByteArray & append(QByteArrayView data)
void chop(qsizetype n)
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)
float gamma() const const
QByteArray iccProfile() const const
bool isValid() const const
Primaries primaries() const const
TransferFunction transferFunction() const const
Format_Grayscale16
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
uchar * scanLine(int i)
void setColorSpace(const QColorSpace &colorSpace)
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)
bool isNull() const const
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 Mar 7 2025 11:57:58 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.