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 "util_p.h"
14
15#include <jxl/encode.h>
16#include <jxl/thread_parallel_runner.h>
17#include <string.h>
18
19// Avoid rotation on buggy Qts (see also https://bugreports.qt.io/browse/QTBUG-126575)
20#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))
21#ifndef JXL_QT_AUTOTRANSFORM
22#define JXL_QT_AUTOTRANSFORM
23#endif
24#endif
25
26#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
27#ifndef JXL_HDR_PRESERVATION_DISABLED
28// Define JXL_HDR_PRESERVATION_DISABLED to disable HDR preservation
29// (HDR images are saved as UINT16).
30#define JXL_HDR_PRESERVATION_DISABLED
31#endif
32#endif
33
34#ifndef JXL_DECODE_BOXES_DISABLED
35// Decode Boxes in order to read optional metadata (XMP, Exif, etc...).
36// Define JXL_DECODE_BOXES_DISABLED to disable Boxes decoding.
37// #define JXL_DECODE_BOXES_DISABLED
38#endif
39
40#define FEATURE_LEVEL_5_WIDTH 262144
41#define FEATURE_LEVEL_5_HEIGHT 262144
42#define FEATURE_LEVEL_5_PIXELS 268435456
43
44#if QT_POINTER_SIZE < 8
45#define MAX_IMAGE_WIDTH 32767
46#define MAX_IMAGE_HEIGHT 32767
47#define MAX_IMAGE_PIXELS FEATURE_LEVEL_5_PIXELS
48#else // JXL code stream level 5
49#define MAX_IMAGE_WIDTH FEATURE_LEVEL_5_WIDTH
50#define MAX_IMAGE_HEIGHT FEATURE_LEVEL_5_HEIGHT
51#define MAX_IMAGE_PIXELS FEATURE_LEVEL_5_PIXELS
52#endif
53
54QJpegXLHandler::QJpegXLHandler()
55 : m_parseState(ParseJpegXLNotParsed)
56 , m_quality(90)
57 , m_currentimage_index(0)
58 , m_previousimage_index(-1)
59 , m_transformations(QImageIOHandler::TransformationNone)
60 , m_decoder(nullptr)
61 , m_runner(nullptr)
62 , m_next_image_delay(0)
63 , m_input_image_format(QImage::Format_Invalid)
64 , m_target_image_format(QImage::Format_Invalid)
65 , m_buffer_size(0)
66{
67}
68
69QJpegXLHandler::~QJpegXLHandler()
70{
71 if (m_runner) {
72 JxlThreadParallelRunnerDestroy(m_runner);
73 }
74 if (m_decoder) {
75 JxlDecoderDestroy(m_decoder);
76 }
77}
78
79bool QJpegXLHandler::canRead() const
80{
81 if (m_parseState == ParseJpegXLNotParsed && !canRead(device())) {
82 return false;
83 }
84
85 if (m_parseState != ParseJpegXLError) {
86 setFormat("jxl");
87
88 if (m_parseState == ParseJpegXLFinished) {
89 return false;
90 }
91
92 return true;
93 }
94 return false;
95}
96
97bool QJpegXLHandler::canRead(QIODevice *device)
98{
99 if (!device) {
100 return false;
101 }
102 QByteArray header = device->peek(32);
103 if (header.size() < 12) {
104 return false;
105 }
106
107 JxlSignature signature = JxlSignatureCheck(reinterpret_cast<const uint8_t *>(header.constData()), header.size());
108 if (signature == JXL_SIG_CODESTREAM || signature == JXL_SIG_CONTAINER) {
109 return true;
110 }
111 return false;
112}
113
114bool QJpegXLHandler::ensureParsed() const
115{
116 if (m_parseState == ParseJpegXLSuccess || m_parseState == ParseJpegXLBasicInfoParsed || m_parseState == ParseJpegXLFinished) {
117 return true;
118 }
119 if (m_parseState == ParseJpegXLError) {
120 return false;
121 }
122
123 QJpegXLHandler *that = const_cast<QJpegXLHandler *>(this);
124
125 return that->ensureDecoder();
126}
127
128bool QJpegXLHandler::ensureALLCounted() const
129{
130 if (!ensureParsed()) {
131 return false;
132 }
133
134 if (m_parseState == ParseJpegXLSuccess || m_parseState == ParseJpegXLFinished) {
135 return true;
136 }
137
138 QJpegXLHandler *that = const_cast<QJpegXLHandler *>(this);
139
140 return that->countALLFrames();
141}
142
143bool QJpegXLHandler::ensureDecoder()
144{
145 if (m_decoder) {
146 return true;
147 }
148
149 m_rawData = device()->readAll();
150
151 if (m_rawData.isEmpty()) {
152 return false;
153 }
154
155 JxlSignature signature = JxlSignatureCheck(reinterpret_cast<const uint8_t *>(m_rawData.constData()), m_rawData.size());
156 if (signature != JXL_SIG_CODESTREAM && signature != JXL_SIG_CONTAINER) {
157 m_parseState = ParseJpegXLError;
158 return false;
159 }
160
161 m_decoder = JxlDecoderCreate(nullptr);
162 if (!m_decoder) {
163 qWarning("ERROR: JxlDecoderCreate failed");
164 m_parseState = ParseJpegXLError;
165 return false;
166 }
167
168#ifdef JXL_QT_AUTOTRANSFORM
169 // Let Qt handle the orientation.
170 JxlDecoderSetKeepOrientation(m_decoder, true);
171#endif
172
173 int num_worker_threads = QThread::idealThreadCount();
174 if (!m_runner && num_worker_threads >= 4) {
175 /* use half of the threads because plug-in is usually used in environment
176 * where application performs another tasks in backround (pre-load other images) */
177 num_worker_threads = num_worker_threads / 2;
178 num_worker_threads = qBound(2, num_worker_threads, 64);
179 m_runner = JxlThreadParallelRunnerCreate(nullptr, num_worker_threads);
180
181 if (JxlDecoderSetParallelRunner(m_decoder, JxlThreadParallelRunner, m_runner) != JXL_DEC_SUCCESS) {
182 qWarning("ERROR: JxlDecoderSetParallelRunner failed");
183 m_parseState = ParseJpegXLError;
184 return false;
185 }
186 }
187
188 if (JxlDecoderSetInput(m_decoder, reinterpret_cast<const uint8_t *>(m_rawData.constData()), m_rawData.size()) != JXL_DEC_SUCCESS) {
189 qWarning("ERROR: JxlDecoderSetInput failed");
190 m_parseState = ParseJpegXLError;
191 return false;
192 }
193
194 JxlDecoderCloseInput(m_decoder);
195#ifndef JXL_DECODE_BOXES_DISABLED
196 JxlDecoderStatus status = JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME | JXL_DEC_BOX);
197#else
198 JxlDecoderStatus status = JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME);
199#endif
200 if (status == JXL_DEC_ERROR) {
201 qWarning("ERROR: JxlDecoderSubscribeEvents failed");
202 m_parseState = ParseJpegXLError;
203 return false;
204 }
205
206 if (!decodeBoxes(status)) {
207 return false;
208 }
209
210 status = JxlDecoderGetBasicInfo(m_decoder, &m_basicinfo);
211 if (status != JXL_DEC_SUCCESS) {
212 qWarning("ERROR: JXL basic info not available");
213 m_parseState = ParseJpegXLError;
214 return false;
215 }
216
217 if (m_basicinfo.xsize == 0 || m_basicinfo.ysize == 0) {
218 qWarning("ERROR: JXL image has zero dimensions");
219 m_parseState = ParseJpegXLError;
220 return false;
221 }
222
223 if (m_basicinfo.xsize > MAX_IMAGE_WIDTH || m_basicinfo.ysize > MAX_IMAGE_HEIGHT) {
224 qWarning("JXL image (%dx%d) is too large", m_basicinfo.xsize, m_basicinfo.ysize);
225 m_parseState = ParseJpegXLError;
226 return false;
227 }
228
229 m_parseState = ParseJpegXLBasicInfoParsed;
230 return true;
231}
232
233bool QJpegXLHandler::countALLFrames()
234{
235 if (m_parseState != ParseJpegXLBasicInfoParsed) {
236 return false;
237 }
238
239 JxlDecoderStatus status;
240 if (!decodeBoxes(status)) {
241 return false;
242 }
243
244 if (status != JXL_DEC_COLOR_ENCODING) {
245 qWarning("Unexpected event %d instead of JXL_DEC_COLOR_ENCODING", status);
246 m_parseState = ParseJpegXLError;
247 return false;
248 }
249
250 bool is_gray = m_basicinfo.num_color_channels == 1 && m_basicinfo.num_extra_channels == 0;
251 JxlColorEncoding color_encoding;
252 if (m_basicinfo.uses_original_profile == JXL_FALSE) {
253 JxlColorEncodingSetToSRGB(&color_encoding, is_gray ? JXL_TRUE : JXL_FALSE);
254 JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding);
255 }
256
257 bool loadalpha = false;
258 if (m_basicinfo.alpha_bits > 0) {
259 loadalpha = true;
260 }
261
262 m_input_pixel_format.endianness = JXL_NATIVE_ENDIAN;
263 m_input_pixel_format.align = 0;
264 m_input_pixel_format.num_channels = is_gray ? 1 : 4;
265
266 if (m_basicinfo.bits_per_sample > 8) { // high bit depth
267#ifdef JXL_HDR_PRESERVATION_DISABLED
268 bool is_fp = false;
269#else
270 bool is_fp = m_basicinfo.exponent_bits_per_sample > 0 && m_basicinfo.num_color_channels == 3;
271#endif
272 m_input_pixel_format.data_type = is_fp ? JXL_TYPE_FLOAT16 : JXL_TYPE_UINT16;
273 m_buffer_size = (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize * m_input_pixel_format.num_channels * 2;
274
275 if (is_gray) {
276 m_input_pixel_format.data_type = JXL_TYPE_UINT16;
277 m_input_image_format = m_target_image_format = QImage::Format_Grayscale16;
278 m_buffer_size = (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize * m_input_pixel_format.num_channels * 2;
279 } else if (m_basicinfo.bits_per_sample > 16 && is_fp) {
280 m_input_pixel_format.data_type = JXL_TYPE_FLOAT;
281 m_input_image_format = QImage::Format_RGBA32FPx4;
282 m_buffer_size = (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize * m_input_pixel_format.num_channels * 4;
283 if (loadalpha)
284 m_target_image_format = QImage::Format_RGBA32FPx4;
285 else
286 m_target_image_format = QImage::Format_RGBX32FPx4;
287 } else {
288 m_buffer_size = (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize * m_input_pixel_format.num_channels * 2;
289 m_input_image_format = is_fp ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBA64;
290 if (loadalpha)
291 m_target_image_format = is_fp ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBA64;
292 else
293 m_target_image_format = is_fp ? QImage::Format_RGBX16FPx4 : QImage::Format_RGBX64;
294 }
295 } else { // 8bit depth
296 m_input_pixel_format.data_type = JXL_TYPE_UINT8;
297 m_buffer_size = (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize * m_input_pixel_format.num_channels;
298
299 if (is_gray) {
300 m_input_image_format = m_target_image_format = QImage::Format_Grayscale8;
301 } else {
302 m_input_image_format = QImage::Format_RGBA8888;
303 if (loadalpha) {
304 m_target_image_format = QImage::Format_ARGB32;
305 } else {
306 m_target_image_format = QImage::Format_RGB32;
307 }
308 }
309 }
310
311 status = JxlDecoderGetColorAsEncodedProfile(m_decoder,
312#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
313 &m_input_pixel_format,
314#endif
315 JXL_COLOR_PROFILE_TARGET_DATA,
316 &color_encoding);
317
318 if (status == JXL_DEC_SUCCESS && color_encoding.color_space == JXL_COLOR_SPACE_RGB && color_encoding.white_point == JXL_WHITE_POINT_D65
319 && color_encoding.primaries == JXL_PRIMARIES_SRGB && color_encoding.transfer_function == JXL_TRANSFER_FUNCTION_SRGB) {
320 m_colorspace = QColorSpace(QColorSpace::SRgb);
321 } else {
322 size_t icc_size = 0;
323 if (JxlDecoderGetICCProfileSize(m_decoder,
324#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
325 &m_input_pixel_format,
326#endif
327 JXL_COLOR_PROFILE_TARGET_DATA,
328 &icc_size)
329 == JXL_DEC_SUCCESS) {
330 if (icc_size > 0) {
331 QByteArray icc_data(icc_size, 0);
332 if (JxlDecoderGetColorAsICCProfile(m_decoder,
333#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
334 &m_input_pixel_format,
335#endif
336 JXL_COLOR_PROFILE_TARGET_DATA,
337 reinterpret_cast<uint8_t *>(icc_data.data()),
338 icc_data.size())
339 == JXL_DEC_SUCCESS) {
340 m_colorspace = QColorSpace::fromIccProfile(icc_data);
341
342 if (!m_colorspace.isValid()) {
343 qWarning("JXL image has Qt-unsupported or invalid ICC profile!");
344 }
345 } else {
346 qWarning("Failed to obtain data from JPEG XL decoder");
347 }
348 } else {
349 qWarning("Empty ICC data");
350 }
351 } else {
352 qWarning("no ICC, other color profile");
353 }
354 }
355
356 if (m_basicinfo.have_animation) { // count all frames
357 JxlFrameHeader frame_header;
358 int delay;
359
360 for (status = JxlDecoderProcessInput(m_decoder); status != JXL_DEC_SUCCESS; status = JxlDecoderProcessInput(m_decoder)) {
361 if (status != JXL_DEC_FRAME) {
362 switch (status) {
363 case JXL_DEC_ERROR:
364 qWarning("ERROR: JXL decoding failed");
365 break;
366 case JXL_DEC_NEED_MORE_INPUT:
367 qWarning("ERROR: JXL data incomplete");
368 break;
369 default:
370 qWarning("Unexpected event %d instead of JXL_DEC_FRAME", status);
371 break;
372 }
373 m_parseState = ParseJpegXLError;
374 return false;
375 }
376
377 if (JxlDecoderGetFrameHeader(m_decoder, &frame_header) != JXL_DEC_SUCCESS) {
378 qWarning("ERROR: JxlDecoderGetFrameHeader failed");
379 m_parseState = ParseJpegXLError;
380 return false;
381 }
382
383 if (m_basicinfo.animation.tps_denominator > 0 && m_basicinfo.animation.tps_numerator > 0) {
384 delay = (int)(0.5 + 1000.0 * frame_header.duration * m_basicinfo.animation.tps_denominator / m_basicinfo.animation.tps_numerator);
385 } else {
386 delay = 0;
387 }
388
389 m_framedelays.append(delay);
390 }
391
392 if (m_framedelays.isEmpty()) {
393 qWarning("no frames loaded by the JXL plug-in");
394 m_parseState = ParseJpegXLError;
395 return false;
396 }
397
398 if (m_framedelays.count() == 1) {
399 qWarning("JXL file was marked as animation but it has only one frame.");
400 m_basicinfo.have_animation = JXL_FALSE;
401 }
402 } else { // static picture
403 m_framedelays.resize(1);
404 m_framedelays[0] = 0;
405 }
406
407#ifndef JXL_DECODE_BOXES_DISABLED
408 if (!decodeBoxes(status)) {
409 return false;
410 }
411#endif
412
413 if (!rewind()) {
414 return false;
415 }
416
417 m_next_image_delay = m_framedelays[0];
418 m_parseState = ParseJpegXLSuccess;
419 return true;
420}
421
422bool QJpegXLHandler::decode_one_frame()
423{
424 JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder);
425 if (status != JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
426 qWarning("Unexpected event %d instead of JXL_DEC_NEED_IMAGE_OUT_BUFFER", status);
427 m_parseState = ParseJpegXLError;
428 return false;
429 }
430
431 m_current_image = imageAlloc(m_basicinfo.xsize, m_basicinfo.ysize, m_input_image_format);
432 if (m_current_image.isNull()) {
433 qWarning("Memory cannot be allocated");
434 m_parseState = ParseJpegXLError;
435 return false;
436 }
437
438 m_current_image.setColorSpace(m_colorspace);
439 if (!m_xmp.isEmpty()) {
440 m_current_image.setText(QStringLiteral(META_KEY_XMP_ADOBE), QString::fromUtf8(m_xmp));
441 }
442
443 if (JxlDecoderSetImageOutBuffer(m_decoder, &m_input_pixel_format, m_current_image.bits(), m_buffer_size) != JXL_DEC_SUCCESS) {
444 qWarning("ERROR: JxlDecoderSetImageOutBuffer failed");
445 m_parseState = ParseJpegXLError;
446 return false;
447 }
448
449 status = JxlDecoderProcessInput(m_decoder);
450 if (status != JXL_DEC_FULL_IMAGE) {
451 qWarning("Unexpected event %d instead of JXL_DEC_FULL_IMAGE", status);
452 m_parseState = ParseJpegXLError;
453 return false;
454 }
455
456 if (m_target_image_format != m_input_image_format) {
457 m_current_image.convertTo(m_target_image_format);
458 }
459
460 m_next_image_delay = m_framedelays[m_currentimage_index];
461 m_previousimage_index = m_currentimage_index;
462
463 if (m_framedelays.count() > 1) {
464 m_currentimage_index++;
465
466 if (m_currentimage_index >= m_framedelays.count()) {
467 if (!rewind()) {
468 return false;
469 }
470
471 // all frames in animation have been read
472 m_parseState = ParseJpegXLFinished;
473 } else {
474 m_parseState = ParseJpegXLSuccess;
475 }
476 } else {
477 // the static image has been read
478 m_parseState = ParseJpegXLFinished;
479 }
480
481 return true;
482}
483
484bool QJpegXLHandler::read(QImage *image)
485{
486 if (!ensureALLCounted()) {
487 return false;
488 }
489
490 if (m_currentimage_index == m_previousimage_index) {
491 *image = m_current_image;
492 return jumpToNextImage();
493 }
494
495 if (decode_one_frame()) {
496 *image = m_current_image;
497 return true;
498 } else {
499 return false;
500 }
501}
502
503template<class T>
504void packRGBPixels(QImage &img)
505{
506 // pack pixel data
507 auto dest_pixels = reinterpret_cast<T *>(img.bits());
508 for (qint32 y = 0; y < img.height(); y++) {
509 auto src_pixels = reinterpret_cast<const T *>(img.constScanLine(y));
510 for (qint32 x = 0; x < img.width(); x++) {
511 // R
512 *dest_pixels = *src_pixels;
513 dest_pixels++;
514 src_pixels++;
515 // G
516 *dest_pixels = *src_pixels;
517 dest_pixels++;
518 src_pixels++;
519 // B
520 *dest_pixels = *src_pixels;
521 dest_pixels++;
522 src_pixels += 2; // skipalpha
523 }
524 }
525}
526
527bool QJpegXLHandler::write(const QImage &image)
528{
529 if (image.format() == QImage::Format_Invalid) {
530 qWarning("No image data to save");
531 return false;
532 }
533
534 if ((image.width() == 0) || (image.height() == 0)) {
535 qWarning("Image has zero dimension!");
536 return false;
537 }
538
539 if ((image.width() > MAX_IMAGE_WIDTH) || (image.height() > MAX_IMAGE_HEIGHT)) {
540 qWarning("Image (%dx%d) is too large to save!", image.width(), image.height());
541 return false;
542 }
543
544 size_t pixel_count = size_t(image.width()) * image.height();
545 if (MAX_IMAGE_PIXELS && pixel_count > MAX_IMAGE_PIXELS) {
546 qWarning("Image (%dx%d) will not be saved because it has more than %d megapixels!", image.width(), image.height(), MAX_IMAGE_PIXELS / 1024 / 1024);
547 return false;
548 }
549
550 int save_depth = 8; // 8 / 16 / 32
551 bool save_fp = false;
552 bool is_gray = false;
553 // depth detection
554 switch (image.format()) {
558#ifndef JXL_HDR_PRESERVATION_DISABLED
559 save_depth = 32;
560 save_fp = true;
561 break;
562#endif
566#ifndef JXL_HDR_PRESERVATION_DISABLED
567 save_depth = 16;
568 save_fp = true;
569 break;
570#endif
578 save_depth = 16;
579 break;
587 save_depth = 8;
588 break;
590 save_depth = 16;
591 is_gray = true;
592 break;
597 save_depth = 8;
598 is_gray = true;
599 break;
601 save_depth = 8;
602 is_gray = image.isGrayscale();
603 break;
604 default:
605 if (image.depth() > 32) {
606 save_depth = 16;
607 } else {
608 save_depth = 8;
609 }
610 break;
611 }
612
613 JxlEncoder *encoder = JxlEncoderCreate(nullptr);
614 if (!encoder) {
615 qWarning("Failed to create Jxl encoder");
616 return false;
617 }
618 JxlEncoderUseBoxes(encoder);
619
620 if (m_quality > 100) {
621 m_quality = 100;
622 } else if (m_quality < 0) {
623 m_quality = 90;
624 }
625
626 JxlBasicInfo output_info;
627 JxlEncoderInitBasicInfo(&output_info);
628
629 QByteArray iccprofile;
630 QColorSpace tmpcs = image.colorSpace();
631 if (!tmpcs.isValid() || tmpcs.primaries() != QColorSpace::Primaries::SRgb || tmpcs.transferFunction() != QColorSpace::TransferFunction::SRgb || m_quality == 100) {
632 // no profile or Qt-unsupported ICC profile
633 iccprofile = tmpcs.iccProfile();
634 // note: lossless encoding requires uses_original_profile = JXL_TRUE
635 if (iccprofile.size() > 0 || m_quality == 100 || is_gray) {
636 output_info.uses_original_profile = JXL_TRUE;
637 }
638 }
639
640 // clang-format off
641 if ( (save_depth > 8 && (image.hasAlphaChannel() || output_info.uses_original_profile))
642 || (save_depth > 16)
643 || (pixel_count > FEATURE_LEVEL_5_PIXELS)
644 || (image.width() > FEATURE_LEVEL_5_WIDTH)
645 || (image.height() > FEATURE_LEVEL_5_HEIGHT)) {
646 output_info.have_container = JXL_TRUE;
647 JxlEncoderUseContainer(encoder, JXL_TRUE);
648 JxlEncoderSetCodestreamLevel(encoder, 10);
649 }
650 // clang-format on
651
652 void *runner = nullptr;
653 int num_worker_threads = qBound(1, QThread::idealThreadCount(), 64);
654
655 if (num_worker_threads > 1) {
656 runner = JxlThreadParallelRunnerCreate(nullptr, num_worker_threads);
657 if (JxlEncoderSetParallelRunner(encoder, JxlThreadParallelRunner, runner) != JXL_ENC_SUCCESS) {
658 qWarning("JxlEncoderSetParallelRunner failed");
659 JxlThreadParallelRunnerDestroy(runner);
660 JxlEncoderDestroy(encoder);
661 return false;
662 }
663 }
664
665 JxlPixelFormat pixel_format;
666 QImage::Format tmpformat;
667 JxlEncoderStatus status;
668
669 pixel_format.endianness = JXL_NATIVE_ENDIAN;
670 pixel_format.align = 0;
671
672 output_info.animation.tps_numerator = 10;
673 output_info.animation.tps_denominator = 1;
674 output_info.orientation = JXL_ORIENT_IDENTITY;
675 if (m_transformations == QImageIOHandler::TransformationMirror) {
676 output_info.orientation = JXL_ORIENT_FLIP_HORIZONTAL;
677 } else if (m_transformations == QImageIOHandler::TransformationRotate180) {
678 output_info.orientation = JXL_ORIENT_ROTATE_180;
679 } else if (m_transformations == QImageIOHandler::TransformationFlip) {
680 output_info.orientation = JXL_ORIENT_FLIP_VERTICAL;
681 } else if (m_transformations == QImageIOHandler::TransformationFlipAndRotate90) {
682 output_info.orientation = JXL_ORIENT_TRANSPOSE;
683 } else if (m_transformations == QImageIOHandler::TransformationRotate90) {
684 output_info.orientation = JXL_ORIENT_ROTATE_90_CW;
685 } else if (m_transformations == QImageIOHandler::TransformationMirrorAndRotate90) {
686 output_info.orientation = JXL_ORIENT_ANTI_TRANSPOSE;
687 } else if (m_transformations == QImageIOHandler::TransformationRotate270) {
688 output_info.orientation = JXL_ORIENT_ROTATE_90_CCW;
689 }
690
691 if (save_depth > 8 && is_gray) { // 16bit depth gray
692 pixel_format.data_type = JXL_TYPE_UINT16;
693 pixel_format.align = 4;
694 output_info.num_color_channels = 1;
695 output_info.bits_per_sample = 16;
696 tmpformat = QImage::Format_Grayscale16;
697 pixel_format.num_channels = 1;
698 } else if (is_gray) { // 8bit depth gray
699 pixel_format.data_type = JXL_TYPE_UINT8;
700 pixel_format.align = 4;
701 output_info.num_color_channels = 1;
702 output_info.bits_per_sample = 8;
703 tmpformat = QImage::Format_Grayscale8;
704 pixel_format.num_channels = 1;
705 } else if (save_depth > 16) { // 32bit depth rgb
706 pixel_format.data_type = JXL_TYPE_FLOAT;
707 output_info.exponent_bits_per_sample = 8;
708 output_info.num_color_channels = 3;
709 output_info.bits_per_sample = 32;
710
711 if (image.hasAlphaChannel()) {
712 tmpformat = QImage::Format_RGBA32FPx4;
713 pixel_format.num_channels = 4;
714 output_info.alpha_bits = 32;
715 output_info.alpha_exponent_bits = 8;
716 output_info.num_extra_channels = 1;
717 } else {
718 tmpformat = QImage::Format_RGBX32FPx4;
719 pixel_format.num_channels = 3;
720 output_info.alpha_bits = 0;
721 output_info.num_extra_channels = 0;
722 }
723 } else if (save_depth > 8) { // 16bit depth rgb
724 pixel_format.data_type = save_fp ? JXL_TYPE_FLOAT16 : JXL_TYPE_UINT16;
725 output_info.exponent_bits_per_sample = save_fp ? 5 : 0;
726 output_info.num_color_channels = 3;
727 output_info.bits_per_sample = 16;
728
729 if (image.hasAlphaChannel()) {
730 tmpformat = save_fp ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBA64;
731 pixel_format.num_channels = 4;
732 output_info.alpha_bits = 16;
733 output_info.alpha_exponent_bits = save_fp ? 5 : 0;
734 output_info.num_extra_channels = 1;
735 } else {
736 tmpformat = save_fp ? QImage::Format_RGBX16FPx4 : QImage::Format_RGBX64;
737 pixel_format.num_channels = 3;
738 output_info.alpha_bits = 0;
739 output_info.num_extra_channels = 0;
740 }
741 } else { // 8bit depth rgb
742 pixel_format.data_type = JXL_TYPE_UINT8;
743 pixel_format.align = 4;
744 output_info.num_color_channels = 3;
745 output_info.bits_per_sample = 8;
746
747 if (image.hasAlphaChannel()) {
748 tmpformat = QImage::Format_RGBA8888;
749 pixel_format.num_channels = 4;
750 output_info.alpha_bits = 8;
751 output_info.num_extra_channels = 1;
752 } else {
753 tmpformat = QImage::Format_RGB888;
754 pixel_format.num_channels = 3;
755 output_info.alpha_bits = 0;
756 output_info.num_extra_channels = 0;
757 }
758 }
759
760 QImage tmpimage = image.convertToFormat(tmpformat);
761 const size_t xsize = tmpimage.width();
762 const size_t ysize = tmpimage.height();
763
764 if (xsize == 0 || ysize == 0 || tmpimage.isNull()) {
765 qWarning("Unable to allocate memory for output image");
766 if (runner) {
767 JxlThreadParallelRunnerDestroy(runner);
768 }
769 JxlEncoderDestroy(encoder);
770 return false;
771 }
772
773 output_info.xsize = tmpimage.width();
774 output_info.ysize = tmpimage.height();
775
776 status = JxlEncoderSetBasicInfo(encoder, &output_info);
777 if (status != JXL_ENC_SUCCESS) {
778 qWarning("JxlEncoderSetBasicInfo failed!");
779 if (runner) {
780 JxlThreadParallelRunnerDestroy(runner);
781 }
782 JxlEncoderDestroy(encoder);
783 return false;
784 }
785
786 auto xmp_data = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8();
787 if (!xmp_data.isEmpty()) {
788 const char *box_type = "xml ";
789 status = JxlEncoderAddBox(encoder, box_type, reinterpret_cast<const uint8_t *>(xmp_data.constData()), xmp_data.size(), JXL_FALSE);
790 if (status != JXL_ENC_SUCCESS) {
791 qWarning("JxlEncoderAddBox failed!");
792 if (runner) {
793 JxlThreadParallelRunnerDestroy(runner);
794 }
795 JxlEncoderDestroy(encoder);
796 return false;
797 }
798 }
799 JxlEncoderCloseBoxes(encoder); // no more metadata
800
801 if (iccprofile.size() > 0) {
802 status = JxlEncoderSetICCProfile(encoder, reinterpret_cast<const uint8_t *>(iccprofile.constData()), iccprofile.size());
803 if (status != JXL_ENC_SUCCESS) {
804 qWarning("JxlEncoderSetICCProfile failed!");
805 if (runner) {
806 JxlThreadParallelRunnerDestroy(runner);
807 }
808 JxlEncoderDestroy(encoder);
809 return false;
810 }
811 } else {
812 JxlColorEncoding color_profile;
813 JxlColorEncodingSetToSRGB(&color_profile, is_gray ? JXL_TRUE : JXL_FALSE);
814
815 status = JxlEncoderSetColorEncoding(encoder, &color_profile);
816 if (status != JXL_ENC_SUCCESS) {
817 qWarning("JxlEncoderSetColorEncoding failed!");
818 if (runner) {
819 JxlThreadParallelRunnerDestroy(runner);
820 }
821 JxlEncoderDestroy(encoder);
822 return false;
823 }
824 }
825
826 JxlEncoderFrameSettings *encoder_options = JxlEncoderFrameSettingsCreate(encoder, nullptr);
827
828 JxlEncoderSetFrameDistance(encoder_options, (100.0f - m_quality) / 10.0f);
829
830 JxlEncoderSetFrameLossless(encoder_options, (m_quality == 100) ? JXL_TRUE : JXL_FALSE);
831
832 size_t buffer_size = size_t(tmpimage.bytesPerLine()) * tmpimage.height();
833 if (!image.hasAlphaChannel() && save_depth > 8 && !is_gray) { // pack pixel on tmpimage
834 buffer_size = (size_t(save_depth / 8) * pixel_format.num_channels * xsize * ysize);
835
836 // detaching image
837 tmpimage.detach();
838 if (tmpimage.isNull()) {
839 qWarning("Memory allocation error");
840 if (runner) {
841 JxlThreadParallelRunnerDestroy(runner);
842 }
843 JxlEncoderDestroy(encoder);
844 return false;
845 }
846
847 // pack pixel data
848 if (save_depth > 16 && save_fp)
849 packRGBPixels<float>(tmpimage);
850 else if (save_fp)
851 packRGBPixels<qfloat16>(tmpimage);
852 else
853 packRGBPixels<quint16>(tmpimage);
854 }
855 status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, static_cast<const void *>(tmpimage.constBits()), buffer_size);
856
857 if (status == JXL_ENC_ERROR) {
858 qWarning("JxlEncoderAddImageFrame failed!");
859 if (runner) {
860 JxlThreadParallelRunnerDestroy(runner);
861 }
862 JxlEncoderDestroy(encoder);
863 return false;
864 }
865
866 JxlEncoderCloseInput(encoder);
867
868 std::vector<uint8_t> compressed;
869 compressed.resize(4096);
870 size_t offset = 0;
871 uint8_t *next_out;
872 size_t avail_out;
873 do {
874 next_out = compressed.data() + offset;
875 avail_out = compressed.size() - offset;
876 status = JxlEncoderProcessOutput(encoder, &next_out, &avail_out);
877
878 if (status == JXL_ENC_NEED_MORE_OUTPUT) {
879 offset = next_out - compressed.data();
880 compressed.resize(compressed.size() * 2);
881 } else if (status == JXL_ENC_ERROR) {
882 qWarning("JxlEncoderProcessOutput failed!");
883 if (runner) {
884 JxlThreadParallelRunnerDestroy(runner);
885 }
886 JxlEncoderDestroy(encoder);
887 return false;
888 }
889 } while (status != JXL_ENC_SUCCESS);
890
891 if (runner) {
892 JxlThreadParallelRunnerDestroy(runner);
893 }
894 JxlEncoderDestroy(encoder);
895
896 compressed.resize(next_out - compressed.data());
897
898 if (compressed.size() > 0) {
899 qint64 write_status = device()->write(reinterpret_cast<const char *>(compressed.data()), compressed.size());
900
901 if (write_status > 0) {
902 return true;
903 } else if (write_status == -1) {
904 qWarning("Write error: %s\n", qUtf8Printable(device()->errorString()));
905 }
906 }
907
908 return false;
909}
910
911QVariant QJpegXLHandler::option(ImageOption option) const
912{
913 if (!supportsOption(option)) {
914 return QVariant();
915 }
916
917 if (option == Quality) {
918 return m_quality;
919 }
920
921 if (!ensureParsed()) {
922#ifdef JXL_QT_AUTOTRANSFORM
923 if (option == ImageTransformation) {
924 return int(m_transformations);
925 }
926#endif
927 return QVariant();
928 }
929
930
931 switch (option) {
932 case Size:
933 return QSize(m_basicinfo.xsize, m_basicinfo.ysize);
934 case Animation:
935 if (m_basicinfo.have_animation) {
936 return true;
937 } else {
938 return false;
939 }
940#ifdef JXL_QT_AUTOTRANSFORM
941 case ImageTransformation:
942 if (m_basicinfo.orientation == JXL_ORIENT_IDENTITY) {
944 } else if (m_basicinfo.orientation == JXL_ORIENT_FLIP_HORIZONTAL) {
946 } else if (m_basicinfo.orientation == JXL_ORIENT_ROTATE_180) {
948 } else if (m_basicinfo.orientation == JXL_ORIENT_FLIP_VERTICAL) {
950 } else if (m_basicinfo.orientation == JXL_ORIENT_TRANSPOSE) {
952 } else if (m_basicinfo.orientation == JXL_ORIENT_ROTATE_90_CW) {
954 } else if (m_basicinfo.orientation == JXL_ORIENT_ANTI_TRANSPOSE) {
956 } else if (m_basicinfo.orientation == JXL_ORIENT_ROTATE_90_CCW) {
958 }
959 break;
960#endif
961 default:
962 return QVariant();
963 }
964
965 return QVariant();
966}
967
968void QJpegXLHandler::setOption(ImageOption option, const QVariant &value)
969{
970 switch (option) {
971 case Quality:
972 m_quality = value.toInt();
973 if (m_quality > 100) {
974 m_quality = 100;
975 } else if (m_quality < 0) {
976 m_quality = 90;
977 }
978 return;
979#ifdef JXL_QT_AUTOTRANSFORM
980 case ImageTransformation:
981 if (auto t = value.toInt()) {
982 if (t > 0 && t < 8)
983 m_transformations = QImageIOHandler::Transformations(t);
984 }
985 break;
986#endif
987 default:
988 break;
989 }
990 QImageIOHandler::setOption(option, value);
991}
992
993bool QJpegXLHandler::supportsOption(ImageOption option) const
994{
995 auto supported = option == Quality || option == Size || option == Animation;
996#ifdef JXL_QT_AUTOTRANSFORM
997 supported = supported || option == ImageTransformation;
998#endif
999 return supported;
1000}
1001
1002int QJpegXLHandler::imageCount() const
1003{
1004 if (!ensureParsed()) {
1005 return 0;
1006 }
1007
1008 if (m_parseState == ParseJpegXLBasicInfoParsed) {
1009 if (!m_basicinfo.have_animation) {
1010 return 1;
1011 }
1012
1013 if (!ensureALLCounted()) {
1014 return 0;
1015 }
1016 }
1017
1018 if (!m_framedelays.isEmpty()) {
1019 return m_framedelays.count();
1020 }
1021 return 0;
1022}
1023
1024int QJpegXLHandler::currentImageNumber() const
1025{
1026 if (m_parseState == ParseJpegXLNotParsed) {
1027 return -1;
1028 }
1029
1030 if (m_parseState == ParseJpegXLError || m_parseState == ParseJpegXLBasicInfoParsed || !m_decoder) {
1031 return 0;
1032 }
1033
1034 return m_currentimage_index;
1035}
1036
1037bool QJpegXLHandler::jumpToNextImage()
1038{
1039 if (!ensureALLCounted()) {
1040 return false;
1041 }
1042
1043 if (m_framedelays.count() > 1) {
1044 m_currentimage_index++;
1045
1046 if (m_currentimage_index >= m_framedelays.count()) {
1047 if (!rewind()) {
1048 return false;
1049 }
1050 } else {
1051 JxlDecoderSkipFrames(m_decoder, 1);
1052 }
1053 }
1054
1055 m_parseState = ParseJpegXLSuccess;
1056 return true;
1057}
1058
1059bool QJpegXLHandler::jumpToImage(int imageNumber)
1060{
1061 if (!ensureALLCounted()) {
1062 return false;
1063 }
1064
1065 if (imageNumber < 0 || imageNumber >= m_framedelays.count()) {
1066 return false;
1067 }
1068
1069 if (imageNumber == m_currentimage_index) {
1070 m_parseState = ParseJpegXLSuccess;
1071 return true;
1072 }
1073
1074 if (imageNumber > m_currentimage_index) {
1075 JxlDecoderSkipFrames(m_decoder, imageNumber - m_currentimage_index);
1076 m_currentimage_index = imageNumber;
1077 m_parseState = ParseJpegXLSuccess;
1078 return true;
1079 }
1080
1081 if (!rewind()) {
1082 return false;
1083 }
1084
1085 if (imageNumber > 0) {
1086 JxlDecoderSkipFrames(m_decoder, imageNumber);
1087 }
1088 m_currentimage_index = imageNumber;
1089 m_parseState = ParseJpegXLSuccess;
1090 return true;
1091}
1092
1093int QJpegXLHandler::nextImageDelay() const
1094{
1095 if (!ensureALLCounted()) {
1096 return 0;
1097 }
1098
1099 if (m_framedelays.count() < 2) {
1100 return 0;
1101 }
1102
1103 return m_next_image_delay;
1104}
1105
1106int QJpegXLHandler::loopCount() const
1107{
1108 if (!ensureParsed()) {
1109 return 0;
1110 }
1111
1112 if (m_basicinfo.have_animation) {
1113 return (m_basicinfo.animation.num_loops > 0) ? m_basicinfo.animation.num_loops - 1 : -1;
1114 } else {
1115 return 0;
1116 }
1117}
1118
1119bool QJpegXLHandler::rewind()
1120{
1121 m_currentimage_index = 0;
1122
1123 JxlDecoderReleaseInput(m_decoder);
1124 JxlDecoderRewind(m_decoder);
1125 if (m_runner) {
1126 if (JxlDecoderSetParallelRunner(m_decoder, JxlThreadParallelRunner, m_runner) != JXL_DEC_SUCCESS) {
1127 qWarning("ERROR: JxlDecoderSetParallelRunner failed");
1128 m_parseState = ParseJpegXLError;
1129 return false;
1130 }
1131 }
1132
1133 if (JxlDecoderSetInput(m_decoder, reinterpret_cast<const uint8_t *>(m_rawData.constData()), m_rawData.size()) != JXL_DEC_SUCCESS) {
1134 qWarning("ERROR: JxlDecoderSetInput failed");
1135 m_parseState = ParseJpegXLError;
1136 return false;
1137 }
1138
1139 JxlDecoderCloseInput(m_decoder);
1140
1141 if (m_basicinfo.uses_original_profile) {
1142 if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
1143 qWarning("ERROR: JxlDecoderSubscribeEvents failed");
1144 m_parseState = ParseJpegXLError;
1145 return false;
1146 }
1147 } else {
1148 if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
1149 qWarning("ERROR: JxlDecoderSubscribeEvents failed");
1150 m_parseState = ParseJpegXLError;
1151 return false;
1152 }
1153
1154 JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder);
1155 if (status != JXL_DEC_COLOR_ENCODING) {
1156 qWarning("Unexpected event %d instead of JXL_DEC_COLOR_ENCODING", status);
1157 m_parseState = ParseJpegXLError;
1158 return false;
1159 }
1160
1161 JxlColorEncoding color_encoding;
1162 JxlColorEncodingSetToSRGB(&color_encoding, JXL_FALSE);
1163 JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding);
1164 }
1165
1166 return true;
1167}
1168
1169bool QJpegXLHandler::decodeBoxes(JxlDecoderStatus &status)
1170{
1171 do { // decode metadata
1172 status = JxlDecoderProcessInput(m_decoder);
1173 if (status == JXL_DEC_BOX) {
1174 JxlBoxType type;
1175 JxlDecoderGetBoxType(m_decoder, type, JXL_FALSE);
1176 if (memcmp(type, "xml ", 4) == 0) {
1177 uint64_t size;
1178 if (JxlDecoderGetBoxSizeRaw(m_decoder, &size) == JXL_DEC_SUCCESS && size < uint64_t(kMaxQVectorSize)) {
1179 m_xmp = QByteArray(size, '\0');
1180 JxlDecoderSetBoxBuffer(m_decoder, reinterpret_cast<uint8_t *>(m_xmp.data()), m_xmp.size());
1181 }
1182 }
1183 }
1184 } while (status == JXL_DEC_BOX);
1185
1186 if (status == JXL_DEC_ERROR) {
1187 qWarning("ERROR: JXL decoding failed");
1188 m_parseState = ParseJpegXLError;
1189 return false;
1190 }
1191 if (status == JXL_DEC_NEED_MORE_INPUT) {
1192 qWarning("ERROR: JXL data incomplete");
1193 m_parseState = ParseJpegXLError;
1194 return false;
1195 }
1196 return true;
1197}
1198
1199QImageIOPlugin::Capabilities QJpegXLPlugin::capabilities(QIODevice *device, const QByteArray &format) const
1200{
1201 if (format == "jxl") {
1202 return Capabilities(CanRead | CanWrite);
1203 }
1204
1205 if (!format.isEmpty()) {
1206 return {};
1207 }
1208 if (!device->isOpen()) {
1209 return {};
1210 }
1211
1212 Capabilities cap;
1213 if (device->isReadable() && QJpegXLHandler::canRead(device)) {
1214 cap |= CanRead;
1215 }
1216
1217 if (device->isWritable()) {
1218 cap |= CanWrite;
1219 }
1220
1221 return cap;
1222}
1223
1224QImageIOHandler *QJpegXLPlugin::create(QIODevice *device, const QByteArray &format) const
1225{
1226 QImageIOHandler *handler = new QJpegXLHandler;
1227 handler->setDevice(device);
1228 handler->setFormat(format);
1229 return handler;
1230}
1231
1232#include "moc_jxl_p.cpp"
Q_SCRIPTABLE CaptureState status()
QFlags< Capability > Capabilities
VehicleSection::Type type(QStringView coachNumber, QStringView coachClassification)
const char * constData() const const
bool isEmpty() const const
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) &&
int depth() const const
Format format() const const
bool hasAlphaChannel() const const
int height() const const
bool isGrayscale() const const
bool isNull() const const
QString text(const QString &key) const const
int width() const const
void setDevice(QIODevice *device)
void setFormat(const QByteArray &format)
virtual void setOption(ImageOption option, const QVariant &value)
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-2024 The KDE developers.
Generated on Fri Nov 22 2024 12:10:55 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.