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 indate 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) {
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 // set image resolution
795 if (exif.horizontalResolution() > 0)
796 m_current_image.setDotsPerMeterX(qRound(exif.horizontalResolution() / 25.4 * 1000));
797 if (exif.verticalResolution() > 0)
798 m_current_image.setDotsPerMeterY(qRound(exif.verticalResolution() / 25.4 * 1000));
799 // set image metadata
800 exif.toImageMetadata(m_current_image);
801 }
802
803 m_next_image_delay = m_framedelays[m_currentimage_index];
804 m_previousimage_index = m_currentimage_index;
805
806 if (m_framedelays.count() > 1) {
807 m_currentimage_index++;
808
809 if (m_currentimage_index >= m_framedelays.count()) {
810 if (!rewind()) {
811 return false;
812 }
813
814 // all frames in animation have been read
815 m_parseState = ParseJpegXLFinished;
816 } else {
817 m_parseState = ParseJpegXLSuccess;
818 }
819 } else {
820 // the static image has been read
821 m_parseState = ParseJpegXLFinished;
822 }
823
824 return true;
825}
826
827bool QJpegXLHandler::read(QImage *image)
828{
829 if (!ensureALLCounted()) {
830 return false;
831 }
832
833 if (m_currentimage_index == m_previousimage_index) {
834 *image = m_current_image;
835 return jumpToNextImage();
836 }
837
838 if (decode_one_frame()) {
839 *image = m_current_image;
840 return true;
841 } else {
842 return false;
843 }
844}
845
846template<class T>
847void packRGBPixels(QImage &img)
848{
849 // pack pixel data
850 auto dest_pixels = reinterpret_cast<T *>(img.bits());
851 for (qint32 y = 0; y < img.height(); y++) {
852 auto src_pixels = reinterpret_cast<const T *>(img.constScanLine(y));
853 for (qint32 x = 0; x < img.width(); x++) {
854 // R
855 *dest_pixels = *src_pixels;
856 dest_pixels++;
857 src_pixels++;
858 // G
859 *dest_pixels = *src_pixels;
860 dest_pixels++;
861 src_pixels++;
862 // B
863 *dest_pixels = *src_pixels;
864 dest_pixels++;
865 src_pixels += 2; // skipalpha
866 }
867 }
868}
869
870bool QJpegXLHandler::write(const QImage &image)
871{
872 if (image.format() == QImage::Format_Invalid) {
873 qWarning("No image data to save");
874 return false;
875 }
876
877 if ((image.width() == 0) || (image.height() == 0)) {
878 qWarning("Image has zero dimension!");
879 return false;
880 }
881
882 if ((image.width() > MAX_IMAGE_WIDTH) || (image.height() > MAX_IMAGE_HEIGHT)) {
883 qWarning("Image (%dx%d) is too large to save!", image.width(), image.height());
884 return false;
885 }
886
887 size_t pixel_count = size_t(image.width()) * image.height();
888 if (MAX_IMAGE_PIXELS && pixel_count > MAX_IMAGE_PIXELS) {
889 qWarning("Image (%dx%d) will not be saved because it has more than %d megapixels!", image.width(), image.height(), MAX_IMAGE_PIXELS / 1024 / 1024);
890 return false;
891 }
892
893 int save_depth = 8; // 8 / 16 / 32
894 bool save_fp = false;
895 bool is_gray = false;
896 // depth detection
897 switch (image.format()) {
901#ifndef JXL_HDR_PRESERVATION_DISABLED
902 save_depth = 32;
903 save_fp = true;
904 break;
905#endif
909#ifndef JXL_HDR_PRESERVATION_DISABLED
910 save_depth = 16;
911 save_fp = true;
912 break;
913#endif
921 save_depth = 16;
922 break;
930#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
931 case QImage::Format_CMYK8888:
932#endif
933 save_depth = 8;
934 break;
936 save_depth = 16;
937 is_gray = true;
938 break;
943 save_depth = 8;
944 is_gray = true;
945 break;
947 save_depth = 8;
948 is_gray = image.isGrayscale();
949 break;
950 default:
951 if (image.depth() > 32) {
952 save_depth = 16;
953 } else {
954 save_depth = 8;
955 }
956 break;
957 }
958
959 JxlEncoder *encoder = JxlEncoderCreate(nullptr);
960 if (!encoder) {
961 qWarning("Failed to create Jxl encoder");
962 return false;
963 }
964
965 if (m_quality > 100) {
966 m_quality = 100;
967 } else if (m_quality < 0) {
968 m_quality = 90;
969 }
970
971 JxlEncoderUseContainer(encoder, JXL_TRUE);
972 JxlEncoderUseBoxes(encoder);
973
974 JxlBasicInfo output_info;
975 JxlEncoderInitBasicInfo(&output_info);
976 output_info.have_container = JXL_TRUE;
977
978 QByteArray iccprofile;
979 QColorSpace tmpcs = image.colorSpace();
980 if (!tmpcs.isValid() || tmpcs.primaries() != QColorSpace::Primaries::SRgb || tmpcs.transferFunction() != QColorSpace::TransferFunction::SRgb
981 || m_quality == 100) {
982 // no profile or Qt-unsupported ICC profile
983 iccprofile = tmpcs.iccProfile();
984 // note: lossless encoding requires uses_original_profile = JXL_TRUE
985 if (iccprofile.size() > 0 || m_quality == 100 || is_gray) {
986 output_info.uses_original_profile = JXL_TRUE;
987 }
988 }
989
990 // clang-format off
991 if ( (save_depth > 8 && (image.hasAlphaChannel() || output_info.uses_original_profile))
992 || (save_depth > 16)
993 || (pixel_count > FEATURE_LEVEL_5_PIXELS)
994 || (image.width() > FEATURE_LEVEL_5_WIDTH)
995 || (image.height() > FEATURE_LEVEL_5_HEIGHT)) {
996 JxlEncoderSetCodestreamLevel(encoder, 10);
997 }
998 // clang-format on
999
1000 void *runner = nullptr;
1001 int num_worker_threads = qBound(1, QThread::idealThreadCount(), 64);
1002
1003 if (num_worker_threads > 1) {
1004 runner = JxlThreadParallelRunnerCreate(nullptr, num_worker_threads);
1005 if (JxlEncoderSetParallelRunner(encoder, JxlThreadParallelRunner, runner) != JXL_ENC_SUCCESS) {
1006 qWarning("JxlEncoderSetParallelRunner failed");
1007 JxlThreadParallelRunnerDestroy(runner);
1008 JxlEncoderDestroy(encoder);
1009 return false;
1010 }
1011 }
1012
1013 JxlPixelFormat pixel_format;
1014 QImage::Format tmpformat;
1015 JxlEncoderStatus status;
1016
1017 pixel_format.endianness = JXL_NATIVE_ENDIAN;
1018 pixel_format.align = 0;
1019
1020 output_info.animation.tps_numerator = 10;
1021 output_info.animation.tps_denominator = 1;
1022 output_info.orientation = JXL_ORIENT_IDENTITY;
1023 if (m_transformations == QImageIOHandler::TransformationMirror) {
1024 output_info.orientation = JXL_ORIENT_FLIP_HORIZONTAL;
1025 } else if (m_transformations == QImageIOHandler::TransformationRotate180) {
1026 output_info.orientation = JXL_ORIENT_ROTATE_180;
1027 } else if (m_transformations == QImageIOHandler::TransformationFlip) {
1028 output_info.orientation = JXL_ORIENT_FLIP_VERTICAL;
1029 } else if (m_transformations == QImageIOHandler::TransformationFlipAndRotate90) {
1030 output_info.orientation = JXL_ORIENT_TRANSPOSE;
1031 } else if (m_transformations == QImageIOHandler::TransformationRotate90) {
1032 output_info.orientation = JXL_ORIENT_ROTATE_90_CW;
1033 } else if (m_transformations == QImageIOHandler::TransformationMirrorAndRotate90) {
1034 output_info.orientation = JXL_ORIENT_ANTI_TRANSPOSE;
1035 } else if (m_transformations == QImageIOHandler::TransformationRotate270) {
1036 output_info.orientation = JXL_ORIENT_ROTATE_90_CCW;
1037 }
1038
1039 if (save_depth > 8 && is_gray) { // 16bit depth gray
1040 pixel_format.data_type = JXL_TYPE_UINT16;
1041 pixel_format.align = 4;
1042 output_info.num_color_channels = 1;
1043 output_info.bits_per_sample = 16;
1044 tmpformat = QImage::Format_Grayscale16;
1045 pixel_format.num_channels = 1;
1046 } else if (is_gray) { // 8bit depth gray
1047 pixel_format.data_type = JXL_TYPE_UINT8;
1048 pixel_format.align = 4;
1049 output_info.num_color_channels = 1;
1050 output_info.bits_per_sample = 8;
1051 tmpformat = QImage::Format_Grayscale8;
1052 pixel_format.num_channels = 1;
1053 } else if (save_depth > 16) { // 32bit depth rgb
1054 pixel_format.data_type = JXL_TYPE_FLOAT;
1055 output_info.exponent_bits_per_sample = 8;
1056 output_info.num_color_channels = 3;
1057 output_info.bits_per_sample = 32;
1058
1059 if (image.hasAlphaChannel()) {
1060 tmpformat = QImage::Format_RGBA32FPx4;
1061 pixel_format.num_channels = 4;
1062 output_info.alpha_bits = 32;
1063 output_info.alpha_exponent_bits = 8;
1064 output_info.num_extra_channels = 1;
1065 } else {
1066 tmpformat = QImage::Format_RGBX32FPx4;
1067 pixel_format.num_channels = 3;
1068 output_info.alpha_bits = 0;
1069 output_info.num_extra_channels = 0;
1070 }
1071 } else if (save_depth > 8) { // 16bit depth rgb
1072 pixel_format.data_type = save_fp ? JXL_TYPE_FLOAT16 : JXL_TYPE_UINT16;
1073 output_info.exponent_bits_per_sample = save_fp ? 5 : 0;
1074 output_info.num_color_channels = 3;
1075 output_info.bits_per_sample = 16;
1076
1077 if (image.hasAlphaChannel()) {
1078 tmpformat = save_fp ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBA64;
1079 pixel_format.num_channels = 4;
1080 output_info.alpha_bits = 16;
1081 output_info.alpha_exponent_bits = save_fp ? 5 : 0;
1082 output_info.num_extra_channels = 1;
1083 } else {
1084 tmpformat = save_fp ? QImage::Format_RGBX16FPx4 : QImage::Format_RGBX64;
1085 pixel_format.num_channels = 3;
1086 output_info.alpha_bits = 0;
1087 output_info.num_extra_channels = 0;
1088 }
1089 } else { // 8bit depth rgb
1090 pixel_format.data_type = JXL_TYPE_UINT8;
1091 pixel_format.align = 4;
1092 output_info.num_color_channels = 3;
1093 output_info.bits_per_sample = 8;
1094
1095 if (image.hasAlphaChannel()) {
1096 tmpformat = QImage::Format_RGBA8888;
1097 pixel_format.num_channels = 4;
1098 output_info.alpha_bits = 8;
1099 output_info.num_extra_channels = 1;
1100 } else {
1101 tmpformat = QImage::Format_RGB888;
1102 pixel_format.num_channels = 3;
1103 output_info.alpha_bits = 0;
1104 output_info.num_extra_channels = 0;
1105 }
1106 }
1107
1108#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
1109 // TODO: add native CMYK support (libjxl supports CMYK images)
1110 QImage tmpimage;
1111 auto cs = image.colorSpace();
1112 if (cs.isValid() && cs.colorModel() == QColorSpace::ColorModel::Cmyk && image.format() == QImage::Format_CMYK8888) {
1113 tmpimage = image.convertedToColorSpace(QColorSpace(QColorSpace::SRgb), tmpformat);
1114 iccprofile.clear();
1115 } else {
1116 tmpimage = image.convertToFormat(tmpformat);
1117 }
1118#else
1119 QImage tmpimage = image.convertToFormat(tmpformat);
1120#endif
1121
1122 const size_t xsize = tmpimage.width();
1123 const size_t ysize = tmpimage.height();
1124
1125 if (xsize == 0 || ysize == 0 || tmpimage.isNull()) {
1126 qWarning("Unable to allocate memory for output image");
1127 if (runner) {
1128 JxlThreadParallelRunnerDestroy(runner);
1129 }
1130 JxlEncoderDestroy(encoder);
1131 return false;
1132 }
1133
1134 output_info.xsize = tmpimage.width();
1135 output_info.ysize = tmpimage.height();
1136
1137 status = JxlEncoderSetBasicInfo(encoder, &output_info);
1138 if (status != JXL_ENC_SUCCESS) {
1139 qWarning("JxlEncoderSetBasicInfo failed!");
1140 if (runner) {
1141 JxlThreadParallelRunnerDestroy(runner);
1142 }
1143 JxlEncoderDestroy(encoder);
1144 return false;
1145 }
1146
1147 auto exif_data = MicroExif::fromImage(image).toByteArray();
1148 if (!exif_data.isEmpty()) {
1149 exif_data = QByteArray::fromHex("00000000") + exif_data;
1150 const char *box_type = "Exif";
1151 status = JxlEncoderAddBox(encoder, box_type, reinterpret_cast<const uint8_t *>(exif_data.constData()), exif_data.size(), JXL_FALSE);
1152 if (status != JXL_ENC_SUCCESS) {
1153 qWarning("JxlEncoderAddBox failed!");
1154 if (runner) {
1155 JxlThreadParallelRunnerDestroy(runner);
1156 }
1157 JxlEncoderDestroy(encoder);
1158 return false;
1159 }
1160 }
1161 auto xmp_data = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8();
1162 if (!xmp_data.isEmpty()) {
1163 const char *box_type = "xml ";
1164 status = JxlEncoderAddBox(encoder, box_type, reinterpret_cast<const uint8_t *>(xmp_data.constData()), xmp_data.size(), JXL_FALSE);
1165 if (status != JXL_ENC_SUCCESS) {
1166 qWarning("JxlEncoderAddBox failed!");
1167 if (runner) {
1168 JxlThreadParallelRunnerDestroy(runner);
1169 }
1170 JxlEncoderDestroy(encoder);
1171 return false;
1172 }
1173 }
1174 JxlEncoderCloseBoxes(encoder); // no more metadata
1175
1176 if (iccprofile.size() > 0) {
1177 status = JxlEncoderSetICCProfile(encoder, reinterpret_cast<const uint8_t *>(iccprofile.constData()), iccprofile.size());
1178 if (status != JXL_ENC_SUCCESS) {
1179 qWarning("JxlEncoderSetICCProfile failed!");
1180 if (runner) {
1181 JxlThreadParallelRunnerDestroy(runner);
1182 }
1183 JxlEncoderDestroy(encoder);
1184 return false;
1185 }
1186 } else {
1187 JxlColorEncoding color_profile;
1188 JxlColorEncodingSetToSRGB(&color_profile, is_gray ? JXL_TRUE : JXL_FALSE);
1189
1190 status = JxlEncoderSetColorEncoding(encoder, &color_profile);
1191 if (status != JXL_ENC_SUCCESS) {
1192 qWarning("JxlEncoderSetColorEncoding failed!");
1193 if (runner) {
1194 JxlThreadParallelRunnerDestroy(runner);
1195 }
1196 JxlEncoderDestroy(encoder);
1197 return false;
1198 }
1199 }
1200
1201 JxlEncoderFrameSettings *encoder_options = JxlEncoderFrameSettingsCreate(encoder, nullptr);
1202
1203 JxlEncoderSetFrameDistance(encoder_options, (100.0f - m_quality) / 10.0f);
1204
1205 JxlEncoderSetFrameLossless(encoder_options, (m_quality == 100) ? JXL_TRUE : JXL_FALSE);
1206
1207 size_t buffer_size = size_t(tmpimage.bytesPerLine()) * tmpimage.height();
1208 if (!image.hasAlphaChannel() && save_depth > 8 && !is_gray) { // pack pixel on tmpimage
1209 buffer_size = (size_t(save_depth / 8) * pixel_format.num_channels * xsize * ysize);
1210
1211 // detaching image
1212 tmpimage.detach();
1213 if (tmpimage.isNull()) {
1214 qWarning("Memory allocation error");
1215 if (runner) {
1216 JxlThreadParallelRunnerDestroy(runner);
1217 }
1218 JxlEncoderDestroy(encoder);
1219 return false;
1220 }
1221
1222 // pack pixel data
1223 if (save_depth > 16 && save_fp)
1224 packRGBPixels<float>(tmpimage);
1225 else if (save_fp)
1226 packRGBPixels<qfloat16>(tmpimage);
1227 else
1228 packRGBPixels<quint16>(tmpimage);
1229 }
1230 status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, static_cast<const void *>(tmpimage.constBits()), buffer_size);
1231
1232 if (status == JXL_ENC_ERROR) {
1233 qWarning("JxlEncoderAddImageFrame failed!");
1234 if (runner) {
1235 JxlThreadParallelRunnerDestroy(runner);
1236 }
1237 JxlEncoderDestroy(encoder);
1238 return false;
1239 }
1240
1241 JxlEncoderCloseInput(encoder);
1242
1243 std::vector<uint8_t> compressed;
1244 compressed.resize(4096);
1245 size_t offset = 0;
1246 uint8_t *next_out;
1247 size_t avail_out;
1248 do {
1249 next_out = compressed.data() + offset;
1250 avail_out = compressed.size() - offset;
1251 status = JxlEncoderProcessOutput(encoder, &next_out, &avail_out);
1252
1253 if (status == JXL_ENC_NEED_MORE_OUTPUT) {
1254 offset = next_out - compressed.data();
1255 compressed.resize(compressed.size() * 2);
1256 } else if (status == JXL_ENC_ERROR) {
1257 qWarning("JxlEncoderProcessOutput failed!");
1258 if (runner) {
1259 JxlThreadParallelRunnerDestroy(runner);
1260 }
1261 JxlEncoderDestroy(encoder);
1262 return false;
1263 }
1264 } while (status != JXL_ENC_SUCCESS);
1265
1266 if (runner) {
1267 JxlThreadParallelRunnerDestroy(runner);
1268 }
1269 JxlEncoderDestroy(encoder);
1270
1271 compressed.resize(next_out - compressed.data());
1272
1273 if (compressed.size() > 0) {
1274 qint64 write_status = device()->write(reinterpret_cast<const char *>(compressed.data()), compressed.size());
1275
1276 if (write_status > 0) {
1277 return true;
1278 } else if (write_status == -1) {
1279 qWarning("Write error: %s\n", qUtf8Printable(device()->errorString()));
1280 }
1281 }
1282
1283 return false;
1284}
1285
1286QVariant QJpegXLHandler::option(ImageOption option) const
1287{
1288 if (!supportsOption(option)) {
1289 return QVariant();
1290 }
1291
1292 if (option == Quality) {
1293 return m_quality;
1294 }
1295
1296 if (!ensureParsed()) {
1297#ifdef JXL_QT_AUTOTRANSFORM
1298 if (option == ImageTransformation) {
1299 return int(m_transformations);
1300 }
1301#endif
1302 return QVariant();
1303 }
1304
1305 switch (option) {
1306 case Size:
1307 return QSize(m_basicinfo.xsize, m_basicinfo.ysize);
1308 case Animation:
1309 if (m_basicinfo.have_animation) {
1310 return true;
1311 } else {
1312 return false;
1313 }
1314#ifdef JXL_QT_AUTOTRANSFORM
1315 case ImageTransformation:
1316 if (m_basicinfo.orientation == JXL_ORIENT_IDENTITY) {
1318 } else if (m_basicinfo.orientation == JXL_ORIENT_FLIP_HORIZONTAL) {
1320 } else if (m_basicinfo.orientation == JXL_ORIENT_ROTATE_180) {
1322 } else if (m_basicinfo.orientation == JXL_ORIENT_FLIP_VERTICAL) {
1324 } else if (m_basicinfo.orientation == JXL_ORIENT_TRANSPOSE) {
1326 } else if (m_basicinfo.orientation == JXL_ORIENT_ROTATE_90_CW) {
1328 } else if (m_basicinfo.orientation == JXL_ORIENT_ANTI_TRANSPOSE) {
1330 } else if (m_basicinfo.orientation == JXL_ORIENT_ROTATE_90_CCW) {
1332 }
1333 break;
1334#endif
1335 default:
1336 return QVariant();
1337 }
1338
1339 return QVariant();
1340}
1341
1342void QJpegXLHandler::setOption(ImageOption option, const QVariant &value)
1343{
1344 switch (option) {
1345 case Quality:
1346 m_quality = value.toInt();
1347 if (m_quality > 100) {
1348 m_quality = 100;
1349 } else if (m_quality < 0) {
1350 m_quality = 90;
1351 }
1352 return;
1353#ifdef JXL_QT_AUTOTRANSFORM
1354 case ImageTransformation:
1355 if (auto t = value.toInt()) {
1356 if (t > 0 && t < 8)
1357 m_transformations = QImageIOHandler::Transformations(t);
1358 }
1359 break;
1360#endif
1361 default:
1362 break;
1363 }
1364 QImageIOHandler::setOption(option, value);
1365}
1366
1367bool QJpegXLHandler::supportsOption(ImageOption option) const
1368{
1369 auto supported = option == Quality || option == Size || option == Animation;
1370#ifdef JXL_QT_AUTOTRANSFORM
1371 supported = supported || option == ImageTransformation;
1372#endif
1373 return supported;
1374}
1375
1376int QJpegXLHandler::imageCount() const
1377{
1378 if (!ensureParsed()) {
1379 return 0;
1380 }
1381
1382 if (m_parseState == ParseJpegXLBasicInfoParsed) {
1383 if (!m_basicinfo.have_animation) {
1384 return 1;
1385 }
1386
1387 if (!ensureALLCounted()) {
1388 return 0;
1389 }
1390 }
1391
1392 if (!m_framedelays.isEmpty()) {
1393 return m_framedelays.count();
1394 }
1395 return 0;
1396}
1397
1398int QJpegXLHandler::currentImageNumber() const
1399{
1400 if (m_parseState == ParseJpegXLNotParsed) {
1401 return -1;
1402 }
1403
1404 if (m_parseState == ParseJpegXLError || m_parseState == ParseJpegXLBasicInfoParsed || !m_decoder) {
1405 return 0;
1406 }
1407
1408 return m_currentimage_index;
1409}
1410
1411bool QJpegXLHandler::jumpToNextImage()
1412{
1413 if (!ensureALLCounted()) {
1414 return false;
1415 }
1416
1417 if (m_framedelays.count() > 1) {
1418 m_currentimage_index++;
1419
1420 if (m_currentimage_index >= m_framedelays.count()) {
1421 if (!rewind()) {
1422 return false;
1423 }
1424 } else {
1425 JxlDecoderSkipFrames(m_decoder, 1);
1426 }
1427 }
1428
1429 m_parseState = ParseJpegXLSuccess;
1430 return true;
1431}
1432
1433bool QJpegXLHandler::jumpToImage(int imageNumber)
1434{
1435 if (!ensureALLCounted()) {
1436 return false;
1437 }
1438
1439 if (imageNumber < 0 || imageNumber >= m_framedelays.count()) {
1440 return false;
1441 }
1442
1443 if (imageNumber == m_currentimage_index) {
1444 m_parseState = ParseJpegXLSuccess;
1445 return true;
1446 }
1447
1448 if (imageNumber > m_currentimage_index) {
1449 JxlDecoderSkipFrames(m_decoder, imageNumber - m_currentimage_index);
1450 m_currentimage_index = imageNumber;
1451 m_parseState = ParseJpegXLSuccess;
1452 return true;
1453 }
1454
1455 if (!rewind()) {
1456 return false;
1457 }
1458
1459 if (imageNumber > 0) {
1460 JxlDecoderSkipFrames(m_decoder, imageNumber);
1461 }
1462 m_currentimage_index = imageNumber;
1463 m_parseState = ParseJpegXLSuccess;
1464 return true;
1465}
1466
1467int QJpegXLHandler::nextImageDelay() const
1468{
1469 if (!ensureALLCounted()) {
1470 return 0;
1471 }
1472
1473 if (m_framedelays.count() < 2) {
1474 return 0;
1475 }
1476
1477 return m_next_image_delay;
1478}
1479
1480int QJpegXLHandler::loopCount() const
1481{
1482 if (!ensureParsed()) {
1483 return 0;
1484 }
1485
1486 if (m_basicinfo.have_animation) {
1487 return (m_basicinfo.animation.num_loops > 0) ? m_basicinfo.animation.num_loops - 1 : -1;
1488 } else {
1489 return 0;
1490 }
1491}
1492
1493bool QJpegXLHandler::rewind()
1494{
1495 m_currentimage_index = 0;
1496
1497 JxlDecoderReleaseInput(m_decoder);
1498 JxlDecoderRewind(m_decoder);
1499 if (m_runner) {
1500 if (JxlDecoderSetParallelRunner(m_decoder, JxlThreadParallelRunner, m_runner) != JXL_DEC_SUCCESS) {
1501 qWarning("ERROR: JxlDecoderSetParallelRunner failed");
1502 m_parseState = ParseJpegXLError;
1503 return false;
1504 }
1505 }
1506
1507 if (JxlDecoderSetInput(m_decoder, reinterpret_cast<const uint8_t *>(m_rawData.constData()), m_rawData.size()) != JXL_DEC_SUCCESS) {
1508 qWarning("ERROR: JxlDecoderSetInput failed");
1509 m_parseState = ParseJpegXLError;
1510 return false;
1511 }
1512
1513 JxlDecoderCloseInput(m_decoder);
1514
1515 if (m_basicinfo.uses_original_profile == JXL_FALSE && m_basicinfo.have_animation == JXL_FALSE) {
1516 if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
1517 qWarning("ERROR: JxlDecoderSubscribeEvents failed");
1518 m_parseState = ParseJpegXLError;
1519 return false;
1520 }
1521
1522 JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder);
1523 if (status != JXL_DEC_COLOR_ENCODING) {
1524 qWarning("Unexpected event %d instead of JXL_DEC_COLOR_ENCODING", status);
1525 m_parseState = ParseJpegXLError;
1526 return false;
1527 }
1528
1529 const JxlCmsInterface *jxlcms = JxlGetDefaultCms();
1530 if (jxlcms) {
1531 status = JxlDecoderSetCms(m_decoder, *jxlcms);
1532 if (status != JXL_DEC_SUCCESS) {
1533 qWarning("JxlDecoderSetCms ERROR");
1534 }
1535 } else {
1536 qWarning("No JPEG XL CMS Interface");
1537 }
1538
1539 bool is_gray = m_basicinfo.num_color_channels == 1 && m_basicinfo.alpha_bits == 0;
1540 JxlColorEncoding color_encoding;
1541 JxlColorEncodingSetToSRGB(&color_encoding, is_gray ? JXL_TRUE : JXL_FALSE);
1542 JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding);
1543 } else {
1544 if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
1545 qWarning("ERROR: JxlDecoderSubscribeEvents failed");
1546 m_parseState = ParseJpegXLError;
1547 return false;
1548 }
1549 }
1550
1551 return true;
1552}
1553
1554bool QJpegXLHandler::decodeContainer()
1555{
1556#if JPEGXL_NUMERIC_VERSION >= JPEGXL_COMPUTE_NUMERIC_VERSION(0, 11, 0)
1557 if (m_basicinfo.have_container == JXL_FALSE) {
1558 return true;
1559 }
1560
1561 const size_t len = m_rawData.size();
1562 if (len == 0) {
1563 m_parseState = ParseJpegXLError;
1564 return false;
1565 }
1566
1567 const uint8_t *buf = reinterpret_cast<const uint8_t *>(m_rawData.constData());
1568 if (JxlSignatureCheck(buf, len) != JXL_SIG_CONTAINER) {
1569 return true;
1570 }
1571
1572 JxlDecoderReleaseInput(m_decoder);
1573 JxlDecoderRewind(m_decoder);
1574
1575 if (JxlDecoderSetInput(m_decoder, buf, len) != JXL_DEC_SUCCESS) {
1576 qWarning("ERROR: JxlDecoderSetInput failed");
1577 m_parseState = ParseJpegXLError;
1578 return false;
1579 }
1580
1581 JxlDecoderCloseInput(m_decoder);
1582
1583 if (JxlDecoderSetDecompressBoxes(m_decoder, JXL_TRUE) != JXL_DEC_SUCCESS) {
1584 qWarning("WARNING: JxlDecoderSetDecompressBoxes failed");
1585 }
1586
1587 if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_BOX | JXL_DEC_BOX_COMPLETE) != JXL_DEC_SUCCESS) {
1588 qWarning("ERROR: JxlDecoderSubscribeEvents failed");
1589 m_parseState = ParseJpegXLError;
1590 return false;
1591 }
1592
1593 bool search_exif = true;
1594 bool search_xmp = true;
1595 JxlBoxType box_type;
1596
1597 QByteArray exifBox;
1598 QByteArray xmpBox;
1599
1600 while (search_exif || search_xmp) {
1601 JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder);
1602 switch (status) {
1603 case JXL_DEC_SUCCESS:
1604 search_exif = false;
1605 search_xmp = false;
1606 break;
1607 case JXL_DEC_BOX:
1608 status = JxlDecoderGetBoxType(m_decoder, box_type, JXL_TRUE);
1609 if (status != JXL_DEC_SUCCESS) {
1610 qWarning("Error in JxlDecoderGetBoxType");
1611 m_parseState = ParseJpegXLError;
1612 return false;
1613 }
1614
1615 if (box_type[0] == 'E' && box_type[1] == 'x' && box_type[2] == 'i' && box_type[3] == 'f' && search_exif) {
1616 search_exif = false;
1617 if (!extractBox(exifBox, len)) {
1618 return false;
1619 }
1620 } else if (box_type[0] == 'x' && box_type[1] == 'm' && box_type[2] == 'l' && box_type[3] == ' ' && search_xmp) {
1621 search_xmp = false;
1622 if (!extractBox(xmpBox, len)) {
1623 return false;
1624 }
1625 }
1626 break;
1627 case JXL_DEC_ERROR:
1628 qWarning("JXL Metadata decoding error");
1629 m_parseState = ParseJpegXLError;
1630 return false;
1631 break;
1632 case JXL_DEC_NEED_MORE_INPUT:
1633 qWarning("JXL metadata are probably incomplete");
1634 m_parseState = ParseJpegXLError;
1635 return false;
1636 break;
1637 default:
1638 qWarning("Unexpected event %d instead of JXL_DEC_BOX", status);
1639 m_parseState = ParseJpegXLError;
1640 return false;
1641 break;
1642 }
1643 }
1644
1645 if (xmpBox.size() > 0) {
1646 m_xmp = xmpBox;
1647 }
1648
1649 if (exifBox.size() > 4) {
1650 const char tiffHeaderBE[4] = {'M', 'M', 0, 42};
1651 const char tiffHeaderLE[4] = {'I', 'I', 42, 0};
1652 const QByteArray tiffBE = QByteArray::fromRawData(tiffHeaderBE, 4);
1653 const QByteArray tiffLE = QByteArray::fromRawData(tiffHeaderLE, 4);
1654 auto headerindexBE = exifBox.indexOf(tiffBE);
1655 auto headerindexLE = exifBox.indexOf(tiffLE);
1656
1657 if (headerindexLE != -1) {
1658 if (headerindexBE == -1) {
1659 m_exif = exifBox.mid(headerindexLE);
1660 } else {
1661 m_exif = exifBox.mid((headerindexLE <= headerindexBE) ? headerindexLE : headerindexBE);
1662 }
1663 } else if (headerindexBE != -1) {
1664 m_exif = exifBox.mid(headerindexBE);
1665 } else {
1666 qWarning("Exif box in JXL file doesn't have TIFF header");
1667 }
1668 }
1669#endif
1670 return true;
1671}
1672
1673bool QJpegXLHandler::extractBox(QByteArray &output, size_t container_size)
1674{
1675#if JPEGXL_NUMERIC_VERSION >= JPEGXL_COMPUTE_NUMERIC_VERSION(0, 11, 0)
1676 uint64_t rawboxsize = 0;
1677 JxlDecoderStatus status = JxlDecoderGetBoxSizeRaw(m_decoder, &rawboxsize);
1678 if (status != JXL_DEC_SUCCESS) {
1679 qWarning("ERROR: JxlDecoderGetBoxSizeRaw failed");
1680 m_parseState = ParseJpegXLError;
1681 return false;
1682 }
1683
1684 if (rawboxsize > container_size) {
1685 qWarning("JXL metadata box is incomplete");
1686 m_parseState = ParseJpegXLError;
1687 return false;
1688 }
1689
1690 output.resize(rawboxsize);
1691 status = JxlDecoderSetBoxBuffer(m_decoder, reinterpret_cast<uint8_t *>(output.data()), output.size());
1692 if (status != JXL_DEC_SUCCESS) {
1693 qWarning("ERROR: JxlDecoderSetBoxBuffer failed");
1694 m_parseState = ParseJpegXLError;
1695 return false;
1696 }
1697
1698 do {
1699 status = JxlDecoderProcessInput(m_decoder);
1700 if (status == JXL_DEC_BOX_NEED_MORE_OUTPUT) {
1701 size_t bytes_remains = JxlDecoderReleaseBoxBuffer(m_decoder);
1702
1703 if (output.size() > 4194304) { // approx. 4MB limit for decompressed metadata box
1704 qWarning("JXL metadata box is too large");
1705 m_parseState = ParseJpegXLError;
1706 return false;
1707 }
1708
1709 output.append(16384, '\0');
1710 size_t extension_size = 16384 + bytes_remains;
1711 uint8_t *extension_buffer = reinterpret_cast<uint8_t *>(output.data()) + (output.size() - extension_size);
1712
1713 if (JxlDecoderSetBoxBuffer(m_decoder, extension_buffer, extension_size) != JXL_DEC_SUCCESS) {
1714 qWarning("ERROR: JxlDecoderSetBoxBuffer failed after JXL_DEC_BOX_NEED_MORE_OUTPUT");
1715 m_parseState = ParseJpegXLError;
1716 return false;
1717 }
1718 }
1719 } while (status == JXL_DEC_BOX_NEED_MORE_OUTPUT);
1720
1721 if (status != JXL_DEC_BOX_COMPLETE) {
1722 qWarning("Unexpected event %d instead of JXL_DEC_BOX_COMPLETE", status);
1723 m_parseState = ParseJpegXLError;
1724 return false;
1725 }
1726
1727 size_t unused_bytes = JxlDecoderReleaseBoxBuffer(m_decoder);
1728 output.chop(unused_bytes);
1729#endif
1730 return true;
1731}
1732
1733QImageIOPlugin::Capabilities QJpegXLPlugin::capabilities(QIODevice *device, const QByteArray &format) const
1734{
1735 if (format == "jxl") {
1736 return Capabilities(CanRead | CanWrite);
1737 }
1738
1739 if (!format.isEmpty()) {
1740 return {};
1741 }
1742 if (!device->isOpen()) {
1743 return {};
1744 }
1745
1746 Capabilities cap;
1747 if (device->isReadable() && QJpegXLHandler::canRead(device)) {
1748 cap |= CanRead;
1749 }
1750
1751 if (device->isWritable()) {
1752 cap |= CanWrite;
1753 }
1754
1755 return cap;
1756}
1757
1758QImageIOHandler *QJpegXLPlugin::create(QIODevice *device, const QByteArray &format) const
1759{
1760 QImageIOHandler *handler = new QJpegXLHandler;
1761 handler->setDevice(device);
1762 handler->setFormat(format);
1763 return handler;
1764}
1765
1766#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
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)
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 Feb 7 2025 11:58:06 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.