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

KDE's Doxygen guidelines are available online.