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
26QJpegXLHandler::QJpegXLHandler()
27 : m_parseState(ParseJpegXLNotParsed)
28 , m_quality(90)
29 , m_currentimage_index(0)
30 , m_previousimage_index(-1)
31 , m_transformations(QImageIOHandler::TransformationNone)
32 , m_decoder(nullptr)
33 , m_runner(nullptr)
34 , m_next_image_delay(0)
35 , m_input_image_format(QImage::Format_Invalid)
36 , m_target_image_format(QImage::Format_Invalid)
37 , m_buffer_size(0)
38{
39}
40
41QJpegXLHandler::~QJpegXLHandler()
42{
43 if (m_runner) {
44 JxlThreadParallelRunnerDestroy(m_runner);
45 }
46 if (m_decoder) {
47 JxlDecoderDestroy(m_decoder);
48 }
49}
50
51bool QJpegXLHandler::canRead() const
52{
53 if (m_parseState == ParseJpegXLNotParsed && !canRead(device())) {
54 return false;
55 }
56
57 if (m_parseState != ParseJpegXLError) {
58 setFormat("jxl");
59
60 if (m_parseState == ParseJpegXLFinished) {
61 return false;
62 }
63
64 return true;
65 }
66 return false;
67}
68
69bool QJpegXLHandler::canRead(QIODevice *device)
70{
71 if (!device) {
72 return false;
73 }
74 QByteArray header = device->peek(32);
75 if (header.size() < 12) {
76 return false;
77 }
78
79 JxlSignature signature = JxlSignatureCheck(reinterpret_cast<const uint8_t *>(header.constData()), header.size());
80 if (signature == JXL_SIG_CODESTREAM || signature == JXL_SIG_CONTAINER) {
81 return true;
82 }
83 return false;
84}
85
86bool QJpegXLHandler::ensureParsed() const
87{
88 if (m_parseState == ParseJpegXLSuccess || m_parseState == ParseJpegXLBasicInfoParsed || m_parseState == ParseJpegXLFinished) {
89 return true;
90 }
91 if (m_parseState == ParseJpegXLError) {
92 return false;
93 }
94
95 QJpegXLHandler *that = const_cast<QJpegXLHandler *>(this);
96
97 return that->ensureDecoder();
98}
99
100bool QJpegXLHandler::ensureALLCounted() const
101{
102 if (!ensureParsed()) {
103 return false;
104 }
105
106 if (m_parseState == ParseJpegXLSuccess || m_parseState == ParseJpegXLFinished) {
107 return true;
108 }
109
110 QJpegXLHandler *that = const_cast<QJpegXLHandler *>(this);
111
112 return that->countALLFrames();
113}
114
115bool QJpegXLHandler::ensureDecoder()
116{
117 if (m_decoder) {
118 return true;
119 }
120
121 m_rawData = device()->readAll();
122
123 if (m_rawData.isEmpty()) {
124 return false;
125 }
126
127 JxlSignature signature = JxlSignatureCheck(reinterpret_cast<const uint8_t *>(m_rawData.constData()), m_rawData.size());
128 if (signature != JXL_SIG_CODESTREAM && signature != JXL_SIG_CONTAINER) {
129 m_parseState = ParseJpegXLError;
130 return false;
131 }
132
133 m_decoder = JxlDecoderCreate(nullptr);
134 if (!m_decoder) {
135 qWarning("ERROR: JxlDecoderCreate failed");
136 m_parseState = ParseJpegXLError;
137 return false;
138 }
139
140#ifdef JXL_QT_AUTOTRANSFORM
141 // Let Qt handle the orientation.
142 JxlDecoderSetKeepOrientation(m_decoder, true);
143#endif
144
145 int num_worker_threads = QThread::idealThreadCount();
146 if (!m_runner && num_worker_threads >= 4) {
147 /* use half of the threads because plug-in is usually used in environment
148 * where application performs another tasks in backround (pre-load other images) */
149 num_worker_threads = num_worker_threads / 2;
150 num_worker_threads = qBound(2, num_worker_threads, 64);
151 m_runner = JxlThreadParallelRunnerCreate(nullptr, num_worker_threads);
152
153 if (JxlDecoderSetParallelRunner(m_decoder, JxlThreadParallelRunner, m_runner) != JXL_DEC_SUCCESS) {
154 qWarning("ERROR: JxlDecoderSetParallelRunner failed");
155 m_parseState = ParseJpegXLError;
156 return false;
157 }
158 }
159
160 if (JxlDecoderSetInput(m_decoder, reinterpret_cast<const uint8_t *>(m_rawData.constData()), m_rawData.size()) != JXL_DEC_SUCCESS) {
161 qWarning("ERROR: JxlDecoderSetInput failed");
162 m_parseState = ParseJpegXLError;
163 return false;
164 }
165
166 JxlDecoderCloseInput(m_decoder);
167
168 JxlDecoderStatus status = JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME);
169 if (status == JXL_DEC_ERROR) {
170 qWarning("ERROR: JxlDecoderSubscribeEvents failed");
171 m_parseState = ParseJpegXLError;
172 return false;
173 }
174
175 status = JxlDecoderProcessInput(m_decoder);
176 if (status == JXL_DEC_ERROR) {
177 qWarning("ERROR: JXL decoding failed");
178 m_parseState = ParseJpegXLError;
179 return false;
180 }
181 if (status == JXL_DEC_NEED_MORE_INPUT) {
182 qWarning("ERROR: JXL data incomplete");
183 m_parseState = ParseJpegXLError;
184 return false;
185 }
186
187 status = JxlDecoderGetBasicInfo(m_decoder, &m_basicinfo);
188 if (status != JXL_DEC_SUCCESS) {
189 qWarning("ERROR: JXL basic info not available");
190 m_parseState = ParseJpegXLError;
191 return false;
192 }
193
194 if (m_basicinfo.xsize == 0 || m_basicinfo.ysize == 0) {
195 qWarning("ERROR: JXL image has zero dimensions");
196 m_parseState = ParseJpegXLError;
197 return false;
198 }
199
200 if (m_basicinfo.xsize > 65535 || m_basicinfo.ysize > 65535) {
201 qWarning("JXL image (%dx%d) is too large", m_basicinfo.xsize, m_basicinfo.ysize);
202 m_parseState = ParseJpegXLError;
203 return false;
204 }
205
206 if (sizeof(void *) <= 4) {
207 /* On 32bit systems, there is limited address space.
208 * We skip imagess bigger than 8192 x 8192 pixels.
209 * If we don't do it, abort() in libjxl may close whole application */
210 if (m_basicinfo.xsize > ((8192 * 8192) / m_basicinfo.ysize)) {
211 qWarning("JXL image (%dx%d) is too large for 32bit build of the plug-in", m_basicinfo.xsize, m_basicinfo.ysize);
212 m_parseState = ParseJpegXLError;
213 return false;
214 }
215 } else {
216 /* On 64bit systems
217 * We skip images bigger than 16384 x 16384 pixels.
218 * It is an artificial limit not to use extreme amount of memory */
219 if (m_basicinfo.xsize > ((16384 * 16384) / m_basicinfo.ysize)) {
220 qWarning("JXL image (%dx%d) is bigger than security limit 256 megapixels", m_basicinfo.xsize, m_basicinfo.ysize);
221 m_parseState = ParseJpegXLError;
222 return false;
223 }
224 }
225
226 m_parseState = ParseJpegXLBasicInfoParsed;
227 return true;
228}
229
230bool QJpegXLHandler::countALLFrames()
231{
232 if (m_parseState != ParseJpegXLBasicInfoParsed) {
233 return false;
234 }
235
236 JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder);
237 if (status != JXL_DEC_COLOR_ENCODING) {
238 qWarning("Unexpected event %d instead of JXL_DEC_COLOR_ENCODING", status);
239 m_parseState = ParseJpegXLError;
240 return false;
241 }
242
243 JxlColorEncoding color_encoding;
244 if (m_basicinfo.uses_original_profile == JXL_FALSE) {
245 JxlColorEncodingSetToSRGB(&color_encoding, JXL_FALSE);
246 JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding);
247 }
248
249 bool loadalpha;
250
251 if (m_basicinfo.alpha_bits > 0) {
252 loadalpha = true;
253 } else {
254 loadalpha = false;
255 }
256
257 m_input_pixel_format.endianness = JXL_NATIVE_ENDIAN;
258 m_input_pixel_format.align = 0;
259 m_input_pixel_format.num_channels = 4;
260
261 if (m_basicinfo.bits_per_sample > 8) { // high bit depth
262 m_input_pixel_format.data_type = JXL_TYPE_UINT16;
263 m_buffer_size = 8 * (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize;
264 m_input_image_format = QImage::Format_RGBA64;
265
266 if (loadalpha) {
267 m_target_image_format = QImage::Format_RGBA64;
268 } else {
269 m_target_image_format = QImage::Format_RGBX64;
270 }
271 } else { // 8bit depth
272 m_input_pixel_format.data_type = JXL_TYPE_UINT8;
273 m_buffer_size = 4 * (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize;
274 m_input_image_format = QImage::Format_RGBA8888;
275
276 if (loadalpha) {
277 m_target_image_format = QImage::Format_ARGB32;
278 } else {
279 m_target_image_format = QImage::Format_RGB32;
280 }
281 }
282
283 status = JxlDecoderGetColorAsEncodedProfile(m_decoder,
284#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
285 &m_input_pixel_format,
286#endif
287 JXL_COLOR_PROFILE_TARGET_DATA,
288 &color_encoding);
289
290 if (status == JXL_DEC_SUCCESS && color_encoding.color_space == JXL_COLOR_SPACE_RGB && color_encoding.white_point == JXL_WHITE_POINT_D65
291 && color_encoding.primaries == JXL_PRIMARIES_SRGB && color_encoding.transfer_function == JXL_TRANSFER_FUNCTION_SRGB) {
292 m_colorspace = QColorSpace(QColorSpace::SRgb);
293 } else {
294 size_t icc_size = 0;
295 if (JxlDecoderGetICCProfileSize(m_decoder,
296#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
297 &m_input_pixel_format,
298#endif
299 JXL_COLOR_PROFILE_TARGET_DATA,
300 &icc_size)
301 == JXL_DEC_SUCCESS) {
302 if (icc_size > 0) {
303 QByteArray icc_data(icc_size, 0);
304 if (JxlDecoderGetColorAsICCProfile(m_decoder,
305#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
306 &m_input_pixel_format,
307#endif
308 JXL_COLOR_PROFILE_TARGET_DATA,
309 reinterpret_cast<uint8_t *>(icc_data.data()),
310 icc_data.size())
311 == JXL_DEC_SUCCESS) {
312 m_colorspace = QColorSpace::fromIccProfile(icc_data);
313
314 if (!m_colorspace.isValid()) {
315 qWarning("JXL image has Qt-unsupported or invalid ICC profile!");
316 }
317 } else {
318 qWarning("Failed to obtain data from JPEG XL decoder");
319 }
320 } else {
321 qWarning("Empty ICC data");
322 }
323 } else {
324 qWarning("no ICC, other color profile");
325 }
326 }
327
328 if (m_basicinfo.have_animation) { // count all frames
329 JxlFrameHeader frame_header;
330 int delay;
331
332 for (status = JxlDecoderProcessInput(m_decoder); status != JXL_DEC_SUCCESS; status = JxlDecoderProcessInput(m_decoder)) {
333 if (status != JXL_DEC_FRAME) {
334 switch (status) {
335 case JXL_DEC_ERROR:
336 qWarning("ERROR: JXL decoding failed");
337 break;
338 case JXL_DEC_NEED_MORE_INPUT:
339 qWarning("ERROR: JXL data incomplete");
340 break;
341 default:
342 qWarning("Unexpected event %d instead of JXL_DEC_FRAME", status);
343 break;
344 }
345 m_parseState = ParseJpegXLError;
346 return false;
347 }
348
349 if (JxlDecoderGetFrameHeader(m_decoder, &frame_header) != JXL_DEC_SUCCESS) {
350 qWarning("ERROR: JxlDecoderGetFrameHeader failed");
351 m_parseState = ParseJpegXLError;
352 return false;
353 }
354
355 if (m_basicinfo.animation.tps_denominator > 0 && m_basicinfo.animation.tps_numerator > 0) {
356 delay = (int)(0.5 + 1000.0 * frame_header.duration * m_basicinfo.animation.tps_denominator / m_basicinfo.animation.tps_numerator);
357 } else {
358 delay = 0;
359 }
360
361 m_framedelays.append(delay);
362 }
363
364 if (m_framedelays.isEmpty()) {
365 qWarning("no frames loaded by the JXL plug-in");
366 m_parseState = ParseJpegXLError;
367 return false;
368 }
369
370 if (m_framedelays.count() == 1) {
371 qWarning("JXL file was marked as animation but it has only one frame.");
372 m_basicinfo.have_animation = JXL_FALSE;
373 }
374 } else { // static picture
375 m_framedelays.resize(1);
376 m_framedelays[0] = 0;
377 }
378
379 if (!rewind()) {
380 return false;
381 }
382
383 m_next_image_delay = m_framedelays[0];
384 m_parseState = ParseJpegXLSuccess;
385 return true;
386}
387
388bool QJpegXLHandler::decode_one_frame()
389{
390 JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder);
391 if (status != JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
392 qWarning("Unexpected event %d instead of JXL_DEC_NEED_IMAGE_OUT_BUFFER", status);
393 m_parseState = ParseJpegXLError;
394 return false;
395 }
396
397 m_current_image = imageAlloc(m_basicinfo.xsize, m_basicinfo.ysize, m_input_image_format);
398 if (m_current_image.isNull()) {
399 qWarning("Memory cannot be allocated");
400 m_parseState = ParseJpegXLError;
401 return false;
402 }
403
404 m_current_image.setColorSpace(m_colorspace);
405
406 if (JxlDecoderSetImageOutBuffer(m_decoder, &m_input_pixel_format, m_current_image.bits(), m_buffer_size) != JXL_DEC_SUCCESS) {
407 qWarning("ERROR: JxlDecoderSetImageOutBuffer failed");
408 m_parseState = ParseJpegXLError;
409 return false;
410 }
411
412 status = JxlDecoderProcessInput(m_decoder);
413 if (status != JXL_DEC_FULL_IMAGE) {
414 qWarning("Unexpected event %d instead of JXL_DEC_FULL_IMAGE", status);
415 m_parseState = ParseJpegXLError;
416 return false;
417 }
418
419 if (m_target_image_format != m_input_image_format) {
420 m_current_image.convertTo(m_target_image_format);
421 }
422
423 m_next_image_delay = m_framedelays[m_currentimage_index];
424 m_previousimage_index = m_currentimage_index;
425
426 if (m_framedelays.count() > 1) {
427 m_currentimage_index++;
428
429 if (m_currentimage_index >= m_framedelays.count()) {
430 if (!rewind()) {
431 return false;
432 }
433
434 // all frames in animation have been read
435 m_parseState = ParseJpegXLFinished;
436 } else {
437 m_parseState = ParseJpegXLSuccess;
438 }
439 } else {
440 // the static image has been read
441 m_parseState = ParseJpegXLFinished;
442 }
443
444 return true;
445}
446
447bool QJpegXLHandler::read(QImage *image)
448{
449 if (!ensureALLCounted()) {
450 return false;
451 }
452
453 if (m_currentimage_index == m_previousimage_index) {
454 *image = m_current_image;
455 return jumpToNextImage();
456 }
457
458 if (decode_one_frame()) {
459 *image = m_current_image;
460 return true;
461 } else {
462 return false;
463 }
464}
465
466bool QJpegXLHandler::write(const QImage &image)
467{
468 if (image.format() == QImage::Format_Invalid) {
469 qWarning("No image data to save");
470 return false;
471 }
472
473 if ((image.width() > 0) && (image.height() > 0)) {
474 if ((image.width() > 65535) || (image.height() > 65535)) {
475 qWarning("Image (%dx%d) is too large to save!", image.width(), image.height());
476 return false;
477 }
478
479 if (sizeof(void *) <= 4) {
480 if (image.width() > ((8192 * 8192) / image.height())) {
481 qWarning("Image (%dx%d) is too large save via 32bit build of JXL plug-in", image.width(), image.height());
482 return false;
483 }
484 } else {
485 if (image.width() > ((16384 * 16384) / image.height())) {
486 qWarning("Image (%dx%d) will not be saved because it has more than 256 megapixels", image.width(), image.height());
487 return false;
488 }
489 }
490 } else {
491 qWarning("Image has zero dimension!");
492 return false;
493 }
494
495 int save_depth = 8; // 8 or 16
496 // depth detection
497 switch (image.format()) {
506 save_depth = 16;
507 break;
515 save_depth = 8;
516 break;
517 default:
518 if (image.depth() > 32) {
519 save_depth = 16;
520 } else {
521 save_depth = 8;
522 }
523 break;
524 }
525
526 JxlEncoder *encoder = JxlEncoderCreate(nullptr);
527 if (!encoder) {
528 qWarning("Failed to create Jxl encoder");
529 return false;
530 }
531
532 if (m_quality > 100) {
533 m_quality = 100;
534 } else if (m_quality < 0) {
535 m_quality = 90;
536 }
537
538 JxlBasicInfo output_info;
539 JxlEncoderInitBasicInfo(&output_info);
540
541 bool convert_color_profile;
542 QByteArray iccprofile;
543
544 if (image.colorSpace().isValid() && (m_quality < 100)) {
545 if (image.colorSpace().primaries() != QColorSpace::Primaries::SRgb || image.colorSpace().transferFunction() != QColorSpace::TransferFunction::SRgb) {
546 convert_color_profile = true;
547 } else {
548 convert_color_profile = false;
549 }
550 } else { // lossless or no profile or Qt-unsupported ICC profile
551 convert_color_profile = false;
552 iccprofile = image.colorSpace().iccProfile();
553 if (iccprofile.size() > 0 || m_quality == 100) {
554 output_info.uses_original_profile = JXL_TRUE;
555 }
556 }
557
558 if (save_depth == 16 && (image.hasAlphaChannel() || output_info.uses_original_profile)) {
559 output_info.have_container = JXL_TRUE;
560 JxlEncoderUseContainer(encoder, JXL_TRUE);
561 JxlEncoderSetCodestreamLevel(encoder, 10);
562 }
563
564 void *runner = nullptr;
565 int num_worker_threads = qBound(1, QThread::idealThreadCount(), 64);
566
567 if (num_worker_threads > 1) {
568 runner = JxlThreadParallelRunnerCreate(nullptr, num_worker_threads);
569 if (JxlEncoderSetParallelRunner(encoder, JxlThreadParallelRunner, runner) != JXL_ENC_SUCCESS) {
570 qWarning("JxlEncoderSetParallelRunner failed");
571 JxlThreadParallelRunnerDestroy(runner);
572 JxlEncoderDestroy(encoder);
573 return false;
574 }
575 }
576
577 JxlPixelFormat pixel_format;
578 QImage::Format tmpformat;
579 JxlEncoderStatus status;
580
581 pixel_format.endianness = JXL_NATIVE_ENDIAN;
582 pixel_format.align = 0;
583
584 output_info.num_color_channels = 3;
585 output_info.animation.tps_numerator = 10;
586 output_info.animation.tps_denominator = 1;
587 output_info.orientation = JXL_ORIENT_IDENTITY;
588 if (m_transformations == QImageIOHandler::TransformationMirror) {
589 output_info.orientation = JXL_ORIENT_FLIP_HORIZONTAL;
590 } else if (m_transformations == QImageIOHandler::TransformationRotate180) {
591 output_info.orientation = JXL_ORIENT_ROTATE_180;
592 } else if (m_transformations == QImageIOHandler::TransformationFlip) {
593 output_info.orientation = JXL_ORIENT_FLIP_VERTICAL;
594 } else if (m_transformations == QImageIOHandler::TransformationFlipAndRotate90) {
595 output_info.orientation = JXL_ORIENT_TRANSPOSE;
596 } else if (m_transformations == QImageIOHandler::TransformationRotate90) {
597 output_info.orientation = JXL_ORIENT_ROTATE_90_CW;
598 } else if (m_transformations == QImageIOHandler::TransformationMirrorAndRotate90) {
599 output_info.orientation = JXL_ORIENT_ANTI_TRANSPOSE;
600 } else if (m_transformations == QImageIOHandler::TransformationRotate270) {
601 output_info.orientation = JXL_ORIENT_ROTATE_90_CCW;
602 }
603
604 if (save_depth > 8) { // 16bit depth
605 pixel_format.data_type = JXL_TYPE_UINT16;
606
607 output_info.bits_per_sample = 16;
608
609 if (image.hasAlphaChannel()) {
610 tmpformat = QImage::Format_RGBA64;
611 pixel_format.num_channels = 4;
612 output_info.alpha_bits = 16;
613 output_info.num_extra_channels = 1;
614 } else {
615 tmpformat = QImage::Format_RGBX64;
616 pixel_format.num_channels = 3;
617 output_info.alpha_bits = 0;
618 }
619 } else { // 8bit depth
620 pixel_format.data_type = JXL_TYPE_UINT8;
621
622 output_info.bits_per_sample = 8;
623
624 if (image.hasAlphaChannel()) {
625 tmpformat = QImage::Format_RGBA8888;
626 pixel_format.num_channels = 4;
627 output_info.alpha_bits = 8;
628 output_info.num_extra_channels = 1;
629 } else {
630 tmpformat = QImage::Format_RGB888;
631 pixel_format.num_channels = 3;
632 output_info.alpha_bits = 0;
633 }
634 }
635
636 const QImage tmpimage =
637 convert_color_profile ? image.convertToFormat(tmpformat).convertedToColorSpace(QColorSpace(QColorSpace::SRgb)) : image.convertToFormat(tmpformat);
638
639 const size_t xsize = tmpimage.width();
640 const size_t ysize = tmpimage.height();
641 const size_t buffer_size = (save_depth > 8) ? (2 * pixel_format.num_channels * xsize * ysize) : (pixel_format.num_channels * xsize * ysize);
642
643 if (xsize == 0 || ysize == 0 || tmpimage.isNull()) {
644 qWarning("Unable to allocate memory for output image");
645 if (runner) {
646 JxlThreadParallelRunnerDestroy(runner);
647 }
648 JxlEncoderDestroy(encoder);
649 return false;
650 }
651
652 output_info.xsize = tmpimage.width();
653 output_info.ysize = tmpimage.height();
654
655 status = JxlEncoderSetBasicInfo(encoder, &output_info);
656 if (status != JXL_ENC_SUCCESS) {
657 qWarning("JxlEncoderSetBasicInfo failed!");
658 if (runner) {
659 JxlThreadParallelRunnerDestroy(runner);
660 }
661 JxlEncoderDestroy(encoder);
662 return false;
663 }
664
665 if (!convert_color_profile && iccprofile.size() > 0) {
666 status = JxlEncoderSetICCProfile(encoder, reinterpret_cast<const uint8_t *>(iccprofile.constData()), iccprofile.size());
667 if (status != JXL_ENC_SUCCESS) {
668 qWarning("JxlEncoderSetICCProfile failed!");
669 if (runner) {
670 JxlThreadParallelRunnerDestroy(runner);
671 }
672 JxlEncoderDestroy(encoder);
673 return false;
674 }
675 } else {
676 JxlColorEncoding color_profile;
677 JxlColorEncodingSetToSRGB(&color_profile, JXL_FALSE);
678
679 status = JxlEncoderSetColorEncoding(encoder, &color_profile);
680 if (status != JXL_ENC_SUCCESS) {
681 qWarning("JxlEncoderSetColorEncoding failed!");
682 if (runner) {
683 JxlThreadParallelRunnerDestroy(runner);
684 }
685 JxlEncoderDestroy(encoder);
686 return false;
687 }
688 }
689
690 JxlEncoderFrameSettings *encoder_options = JxlEncoderFrameSettingsCreate(encoder, nullptr);
691
692 JxlEncoderSetFrameDistance(encoder_options, (100.0f - m_quality) / 10.0f);
693
694 JxlEncoderSetFrameLossless(encoder_options, (m_quality == 100) ? JXL_TRUE : JXL_FALSE);
695
696 if (image.hasAlphaChannel() || ((save_depth == 8) && (xsize % 4 == 0))) {
697 status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, static_cast<const void *>(tmpimage.constBits()), buffer_size);
698 } else {
699 if (save_depth > 8) { // 16bit depth without alpha channel
700 uint16_t *tmp_buffer = new (std::nothrow) uint16_t[3 * xsize * ysize];
701 if (!tmp_buffer) {
702 qWarning("Memory allocation error");
703 if (runner) {
704 JxlThreadParallelRunnerDestroy(runner);
705 }
706 JxlEncoderDestroy(encoder);
707 return false;
708 }
709
710 uint16_t *dest_pixels = tmp_buffer;
711 for (int y = 0; y < tmpimage.height(); y++) {
712 const uint16_t *src_pixels = reinterpret_cast<const uint16_t *>(tmpimage.constScanLine(y));
713 for (int x = 0; x < tmpimage.width(); x++) {
714 // R
715 *dest_pixels = *src_pixels;
716 dest_pixels++;
717 src_pixels++;
718 // G
719 *dest_pixels = *src_pixels;
720 dest_pixels++;
721 src_pixels++;
722 // B
723 *dest_pixels = *src_pixels;
724 dest_pixels++;
725 src_pixels += 2; // skipalpha
726 }
727 }
728 status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, static_cast<const void *>(tmp_buffer), buffer_size);
729 delete[] tmp_buffer;
730 } else { // 8bit depth without alpha channel
731 uchar *tmp_buffer8 = new (std::nothrow) uchar[3 * xsize * ysize];
732 if (!tmp_buffer8) {
733 qWarning("Memory allocation error");
734 if (runner) {
735 JxlThreadParallelRunnerDestroy(runner);
736 }
737 JxlEncoderDestroy(encoder);
738 return false;
739 }
740
741 uchar *dest_pixels8 = tmp_buffer8;
742 const size_t rowbytes = 3 * xsize;
743 for (int y = 0; y < tmpimage.height(); y++) {
744 memcpy(dest_pixels8, tmpimage.constScanLine(y), rowbytes);
745 dest_pixels8 += rowbytes;
746 }
747 status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, static_cast<const void *>(tmp_buffer8), buffer_size);
748 delete[] tmp_buffer8;
749 }
750 }
751
752 if (status == JXL_ENC_ERROR) {
753 qWarning("JxlEncoderAddImageFrame failed!");
754 if (runner) {
755 JxlThreadParallelRunnerDestroy(runner);
756 }
757 JxlEncoderDestroy(encoder);
758 return false;
759 }
760
761 JxlEncoderCloseInput(encoder);
762
763 std::vector<uint8_t> compressed;
764 compressed.resize(4096);
765 size_t offset = 0;
766 uint8_t *next_out;
767 size_t avail_out;
768 do {
769 next_out = compressed.data() + offset;
770 avail_out = compressed.size() - offset;
771 status = JxlEncoderProcessOutput(encoder, &next_out, &avail_out);
772
773 if (status == JXL_ENC_NEED_MORE_OUTPUT) {
774 offset = next_out - compressed.data();
775 compressed.resize(compressed.size() * 2);
776 } else if (status == JXL_ENC_ERROR) {
777 qWarning("JxlEncoderProcessOutput failed!");
778 if (runner) {
779 JxlThreadParallelRunnerDestroy(runner);
780 }
781 JxlEncoderDestroy(encoder);
782 return false;
783 }
784 } while (status != JXL_ENC_SUCCESS);
785
786 if (runner) {
787 JxlThreadParallelRunnerDestroy(runner);
788 }
789 JxlEncoderDestroy(encoder);
790
791 compressed.resize(next_out - compressed.data());
792
793 if (compressed.size() > 0) {
794 qint64 write_status = device()->write(reinterpret_cast<const char *>(compressed.data()), compressed.size());
795
796 if (write_status > 0) {
797 return true;
798 } else if (write_status == -1) {
799 qWarning("Write error: %s\n", qUtf8Printable(device()->errorString()));
800 }
801 }
802
803 return false;
804}
805
806QVariant QJpegXLHandler::option(ImageOption option) const
807{
808 if (!supportsOption(option)) {
809 return QVariant();
810 }
811
812 if (option == Quality) {
813 return m_quality;
814 }
815
816 if (!ensureParsed()) {
817#ifdef JXL_QT_AUTOTRANSFORM
818 if (option == ImageTransformation) {
819 return int(m_transformations);
820 }
821#endif
822 return QVariant();
823 }
824
825
826 switch (option) {
827 case Size:
828 return QSize(m_basicinfo.xsize, m_basicinfo.ysize);
829 case Animation:
830 if (m_basicinfo.have_animation) {
831 return true;
832 } else {
833 return false;
834 }
835#ifdef JXL_QT_AUTOTRANSFORM
836 case ImageTransformation:
837 if (m_basicinfo.orientation == JXL_ORIENT_IDENTITY) {
839 } else if (m_basicinfo.orientation == JXL_ORIENT_FLIP_HORIZONTAL) {
841 } else if (m_basicinfo.orientation == JXL_ORIENT_ROTATE_180) {
843 } else if (m_basicinfo.orientation == JXL_ORIENT_FLIP_VERTICAL) {
845 } else if (m_basicinfo.orientation == JXL_ORIENT_TRANSPOSE) {
847 } else if (m_basicinfo.orientation == JXL_ORIENT_ROTATE_90_CW) {
849 } else if (m_basicinfo.orientation == JXL_ORIENT_ANTI_TRANSPOSE) {
851 } else if (m_basicinfo.orientation == JXL_ORIENT_ROTATE_90_CCW) {
853 }
854#endif
855 default:
856 return QVariant();
857 }
858
859 return QVariant();
860}
861
862void QJpegXLHandler::setOption(ImageOption option, const QVariant &value)
863{
864 switch (option) {
865 case Quality:
866 m_quality = value.toInt();
867 if (m_quality > 100) {
868 m_quality = 100;
869 } else if (m_quality < 0) {
870 m_quality = 90;
871 }
872 return;
873#ifdef JXL_QT_AUTOTRANSFORM
874 case ImageTransformation:
875 if (auto t = value.toInt()) {
876 if (t > 0 && t < 8)
877 m_transformations = QImageIOHandler::Transformations(t);
878 }
879 break;
880#endif
881 default:
882 break;
883 }
884 QImageIOHandler::setOption(option, value);
885}
886
887bool QJpegXLHandler::supportsOption(ImageOption option) const
888{
889 auto supported = option == Quality || option == Size || option == Animation;
890#ifdef JXL_QT_AUTOTRANSFORM
891 supported = supported || option == ImageTransformation;
892#endif
893 return supported;
894}
895
896int QJpegXLHandler::imageCount() const
897{
898 if (!ensureParsed()) {
899 return 0;
900 }
901
902 if (m_parseState == ParseJpegXLBasicInfoParsed) {
903 if (!m_basicinfo.have_animation) {
904 return 1;
905 }
906
907 if (!ensureALLCounted()) {
908 return 0;
909 }
910 }
911
912 if (!m_framedelays.isEmpty()) {
913 return m_framedelays.count();
914 }
915 return 0;
916}
917
918int QJpegXLHandler::currentImageNumber() const
919{
920 if (m_parseState == ParseJpegXLNotParsed) {
921 return -1;
922 }
923
924 if (m_parseState == ParseJpegXLError || m_parseState == ParseJpegXLBasicInfoParsed || !m_decoder) {
925 return 0;
926 }
927
928 return m_currentimage_index;
929}
930
931bool QJpegXLHandler::jumpToNextImage()
932{
933 if (!ensureALLCounted()) {
934 return false;
935 }
936
937 if (m_framedelays.count() > 1) {
938 m_currentimage_index++;
939
940 if (m_currentimage_index >= m_framedelays.count()) {
941 if (!rewind()) {
942 return false;
943 }
944 } else {
945 JxlDecoderSkipFrames(m_decoder, 1);
946 }
947 }
948
949 m_parseState = ParseJpegXLSuccess;
950 return true;
951}
952
953bool QJpegXLHandler::jumpToImage(int imageNumber)
954{
955 if (!ensureALLCounted()) {
956 return false;
957 }
958
959 if (imageNumber < 0 || imageNumber >= m_framedelays.count()) {
960 return false;
961 }
962
963 if (imageNumber == m_currentimage_index) {
964 m_parseState = ParseJpegXLSuccess;
965 return true;
966 }
967
968 if (imageNumber > m_currentimage_index) {
969 JxlDecoderSkipFrames(m_decoder, imageNumber - m_currentimage_index);
970 m_currentimage_index = imageNumber;
971 m_parseState = ParseJpegXLSuccess;
972 return true;
973 }
974
975 if (!rewind()) {
976 return false;
977 }
978
979 if (imageNumber > 0) {
980 JxlDecoderSkipFrames(m_decoder, imageNumber);
981 }
982 m_currentimage_index = imageNumber;
983 m_parseState = ParseJpegXLSuccess;
984 return true;
985}
986
987int QJpegXLHandler::nextImageDelay() const
988{
989 if (!ensureALLCounted()) {
990 return 0;
991 }
992
993 if (m_framedelays.count() < 2) {
994 return 0;
995 }
996
997 return m_next_image_delay;
998}
999
1000int QJpegXLHandler::loopCount() const
1001{
1002 if (!ensureParsed()) {
1003 return 0;
1004 }
1005
1006 if (m_basicinfo.have_animation) {
1007 return (m_basicinfo.animation.num_loops > 0) ? m_basicinfo.animation.num_loops - 1 : -1;
1008 } else {
1009 return 0;
1010 }
1011}
1012
1013bool QJpegXLHandler::rewind()
1014{
1015 m_currentimage_index = 0;
1016
1017 JxlDecoderReleaseInput(m_decoder);
1018 JxlDecoderRewind(m_decoder);
1019 if (m_runner) {
1020 if (JxlDecoderSetParallelRunner(m_decoder, JxlThreadParallelRunner, m_runner) != JXL_DEC_SUCCESS) {
1021 qWarning("ERROR: JxlDecoderSetParallelRunner failed");
1022 m_parseState = ParseJpegXLError;
1023 return false;
1024 }
1025 }
1026
1027 if (JxlDecoderSetInput(m_decoder, reinterpret_cast<const uint8_t *>(m_rawData.constData()), m_rawData.size()) != JXL_DEC_SUCCESS) {
1028 qWarning("ERROR: JxlDecoderSetInput failed");
1029 m_parseState = ParseJpegXLError;
1030 return false;
1031 }
1032
1033 JxlDecoderCloseInput(m_decoder);
1034
1035 if (m_basicinfo.uses_original_profile) {
1036 if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
1037 qWarning("ERROR: JxlDecoderSubscribeEvents failed");
1038 m_parseState = ParseJpegXLError;
1039 return false;
1040 }
1041 } else {
1042 if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
1043 qWarning("ERROR: JxlDecoderSubscribeEvents failed");
1044 m_parseState = ParseJpegXLError;
1045 return false;
1046 }
1047
1048 JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder);
1049 if (status != JXL_DEC_COLOR_ENCODING) {
1050 qWarning("Unexpected event %d instead of JXL_DEC_COLOR_ENCODING", status);
1051 m_parseState = ParseJpegXLError;
1052 return false;
1053 }
1054
1055 JxlColorEncoding color_encoding;
1056 JxlColorEncodingSetToSRGB(&color_encoding, JXL_FALSE);
1057 JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding);
1058 }
1059
1060 return true;
1061}
1062
1063QImageIOPlugin::Capabilities QJpegXLPlugin::capabilities(QIODevice *device, const QByteArray &format) const
1064{
1065 if (format == "jxl") {
1066 return Capabilities(CanRead | CanWrite);
1067 }
1068
1069 if (!format.isEmpty()) {
1070 return {};
1071 }
1072 if (!device->isOpen()) {
1073 return {};
1074 }
1075
1076 Capabilities cap;
1077 if (device->isReadable() && QJpegXLHandler::canRead(device)) {
1078 cap |= CanRead;
1079 }
1080
1081 if (device->isWritable()) {
1082 cap |= CanWrite;
1083 }
1084
1085 return cap;
1086}
1087
1088QImageIOHandler *QJpegXLPlugin::create(QIODevice *device, const QByteArray &format) const
1089{
1090 QImageIOHandler *handler = new QJpegXLHandler;
1091 handler->setDevice(device);
1092 handler->setFormat(format);
1093 return handler;
1094}
1095
1096#include "moc_jxl_p.cpp"
Q_SCRIPTABLE CaptureState status()
QFlags< Capability > Capabilities
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
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 isNull() 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)
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 Oct 11 2024 12:15:45 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.