diff --git a/README.md b/README.md index e7c00a3..ae20cd9 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,8 @@ For example, native support for CMYK images is only available since Qt 6.8. ### HDR images -HDR images are supported via floating point image formats from EXR, HDR, JXR, -PFM and PSD plugins. +HDR images are supported via floating point image formats from EXR, HDR, JXL, +JXR, PFM and PSD plugins. It is important to note that in the past these plugins stripped away HDR information, returning SDR images. @@ -116,7 +116,7 @@ plugin ('n/a' means no limit, i.e. the limit depends on the format encoding). - EXR: 300,000 x 300,000 pixels - HDR: n/a (large image) - HEIF: n/a -- JXL: 65,535 x 65,535 pixels, in any case no larger than 256 megapixels +- JXL: 262,144 x 262,144 pixels, in any case no larger than 256 megapixels - JXR: n/a, in any case no larger than 4 GB - PCX: 65,535 x 65,535 pixels - PFM: n/a (large image) @@ -181,6 +181,10 @@ The following defines can be defined in cmake to modify the behavior of the plug **The current version of the plugin limits the image size to 256 megapixels according to feature level 5 of the JXL stream encoding.** +The following defines can be defined in cmake to modify the behavior of the plugin: +- `JXL_HDR_PRESERVATION_DISABLED`: disable floating point images (both read and write) by converting them to UINT16 images. Any HDR data is lost. Note that FP images are always disabled when compiling with libJXL less than v0.9. +- `JXL_DECODE_BOXES_DISABLED`: disable reading of metadata (e.g. XMP). + ### The JXR plugin **This plugin is disabled by default. It can be enabled with the diff --git a/autotests/read/jxl/testcard_gray16.jxl b/autotests/read/jxl/testcard_gray16.jxl new file mode 100644 index 0000000..0dc49c4 Binary files /dev/null and b/autotests/read/jxl/testcard_gray16.jxl differ diff --git a/autotests/read/jxl/testcard_gray16.png b/autotests/read/jxl/testcard_gray16.png new file mode 100644 index 0000000..85887f1 Binary files /dev/null and b/autotests/read/jxl/testcard_gray16.png differ diff --git a/autotests/read/jxl/testcard_gray8.jxl b/autotests/read/jxl/testcard_gray8.jxl new file mode 100644 index 0000000..7a2341a Binary files /dev/null and b/autotests/read/jxl/testcard_gray8.jxl differ diff --git a/autotests/read/jxl/testcard_gray8.png b/autotests/read/jxl/testcard_gray8.png new file mode 100644 index 0000000..2538235 Binary files /dev/null and b/autotests/read/jxl/testcard_gray8.png differ diff --git a/autotests/read/jxl/testcard_rgb_fp16.jxl b/autotests/read/jxl/testcard_rgb_fp16.jxl new file mode 100644 index 0000000..8382e74 Binary files /dev/null and b/autotests/read/jxl/testcard_rgb_fp16.jxl differ diff --git a/autotests/read/jxl/testcard_rgb_fp16.png b/autotests/read/jxl/testcard_rgb_fp16.png new file mode 100644 index 0000000..735a378 Binary files /dev/null and b/autotests/read/jxl/testcard_rgb_fp16.png differ diff --git a/autotests/read/jxl/testcard_rgb_fp32.jxl b/autotests/read/jxl/testcard_rgb_fp32.jxl new file mode 100644 index 0000000..96203b8 Binary files /dev/null and b/autotests/read/jxl/testcard_rgb_fp32.jxl differ diff --git a/autotests/read/jxl/testcard_rgb_fp32.png b/autotests/read/jxl/testcard_rgb_fp32.png new file mode 100644 index 0000000..735a378 Binary files /dev/null and b/autotests/read/jxl/testcard_rgb_fp32.png differ diff --git a/autotests/read/jxl/testcard_rgba_fp16.jxl b/autotests/read/jxl/testcard_rgba_fp16.jxl new file mode 100644 index 0000000..21c4a2b Binary files /dev/null and b/autotests/read/jxl/testcard_rgba_fp16.jxl differ diff --git a/autotests/read/jxl/testcard_rgba_fp16.jxl.json b/autotests/read/jxl/testcard_rgba_fp16.jxl.json new file mode 100644 index 0000000..5230930 --- /dev/null +++ b/autotests/read/jxl/testcard_rgba_fp16.jxl.json @@ -0,0 +1,21 @@ +[ + { + "minQtVersion" : "6.2.11", + "maxQtVersion" : "6.2.99", + "fileName" : "testcard_rgba_fp16.png" + }, + { + "minQtVersion" : "6.5.5", + "maxQtVersion" : "6.5.99", + "fileName" : "testcard_rgba_fp16.png" + }, + { + "minQtVersion" : "6.6.2", + "fileName" : "testcard_rgba_fp16.png" + }, + { + "unsupportedFormat" : true, + "comment" : "Skipped due to QTBUG-120614.", + "seeAlso" : "https://bugreports.qt.io/browse/QTBUG-120614" + } +] diff --git a/autotests/read/jxl/testcard_rgba_fp16.png b/autotests/read/jxl/testcard_rgba_fp16.png new file mode 100644 index 0000000..09090dc Binary files /dev/null and b/autotests/read/jxl/testcard_rgba_fp16.png differ diff --git a/autotests/read/jxl/testcard_rgba_fp32.jxl b/autotests/read/jxl/testcard_rgba_fp32.jxl new file mode 100644 index 0000000..45a1e34 Binary files /dev/null and b/autotests/read/jxl/testcard_rgba_fp32.jxl differ diff --git a/autotests/read/jxl/testcard_rgba_fp32.jxl.json b/autotests/read/jxl/testcard_rgba_fp32.jxl.json new file mode 100644 index 0000000..7d26f19 --- /dev/null +++ b/autotests/read/jxl/testcard_rgba_fp32.jxl.json @@ -0,0 +1,21 @@ +[ + { + "minQtVersion" : "6.2.11", + "maxQtVersion" : "6.2.99", + "fileName" : "testcard_rgba_fp32.png" + }, + { + "minQtVersion" : "6.5.5", + "maxQtVersion" : "6.5.99", + "fileName" : "testcard_rgba_fp32.png" + }, + { + "minQtVersion" : "6.6.2", + "fileName" : "testcard_rgba_fp32.png" + }, + { + "unsupportedFormat" : true, + "comment" : "Skipped due to QTBUG-120614.", + "seeAlso" : "https://bugreports.qt.io/browse/QTBUG-120614" + } +] diff --git a/autotests/read/jxl/testcard_rgba_fp32.png b/autotests/read/jxl/testcard_rgba_fp32.png new file mode 100644 index 0000000..09090dc Binary files /dev/null and b/autotests/read/jxl/testcard_rgba_fp32.png differ diff --git a/src/imageformats/jxl.cpp b/src/imageformats/jxl.cpp index ec021e6..ee32c67 100644 --- a/src/imageformats/jxl.cpp +++ b/src/imageformats/jxl.cpp @@ -23,6 +23,34 @@ #endif #endif +#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0) +#ifndef JXL_HDR_PRESERVATION_DISABLED +// Define JXL_HDR_PRESERVATION_DISABLED to disable HDR preservation +// (HDR images are saved as UINT16). +#define JXL_HDR_PRESERVATION_DISABLED +#endif +#endif + +#ifndef JXL_DECODE_BOXES_DISABLED +// Decode Boxes in order to read optional metadata (XMP, Exif, etc...). +// Define JXL_DECODE_BOXES_DISABLED to disable Boxes decoding. +// #define JXL_DECODE_BOXES_DISABLED +#endif + +#define FEATURE_LEVEL_5_WIDTH 262144 +#define FEATURE_LEVEL_5_HEIGHT 262144 +#define FEATURE_LEVEL_5_PIXELS 268435456 + +#if QT_POINTER_SIZE < 8 +#define MAX_IMAGE_WIDTH 32767 +#define MAX_IMAGE_HEIGHT 32767 +#define MAX_IMAGE_PIXELS FEATURE_LEVEL_5_PIXELS +#else // JXL code stream level 5 +#define MAX_IMAGE_WIDTH FEATURE_LEVEL_5_WIDTH +#define MAX_IMAGE_HEIGHT FEATURE_LEVEL_5_HEIGHT +#define MAX_IMAGE_PIXELS FEATURE_LEVEL_5_PIXELS +#endif + QJpegXLHandler::QJpegXLHandler() : m_parseState(ParseJpegXLNotParsed) , m_quality(90) @@ -164,23 +192,18 @@ bool QJpegXLHandler::ensureDecoder() } JxlDecoderCloseInput(m_decoder); - +#ifndef JXL_DECODE_BOXES_DISABLED + JxlDecoderStatus status = JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME | JXL_DEC_BOX); +#else JxlDecoderStatus status = JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME); +#endif if (status == JXL_DEC_ERROR) { qWarning("ERROR: JxlDecoderSubscribeEvents failed"); m_parseState = ParseJpegXLError; return false; } - status = JxlDecoderProcessInput(m_decoder); - if (status == JXL_DEC_ERROR) { - qWarning("ERROR: JXL decoding failed"); - m_parseState = ParseJpegXLError; - return false; - } - if (status == JXL_DEC_NEED_MORE_INPUT) { - qWarning("ERROR: JXL data incomplete"); - m_parseState = ParseJpegXLError; + if (!decodeBoxes()) { return false; } @@ -197,32 +220,12 @@ bool QJpegXLHandler::ensureDecoder() return false; } - if (m_basicinfo.xsize > 65535 || m_basicinfo.ysize > 65535) { + if (m_basicinfo.xsize > MAX_IMAGE_WIDTH || m_basicinfo.ysize > MAX_IMAGE_HEIGHT) { qWarning("JXL image (%dx%d) is too large", m_basicinfo.xsize, m_basicinfo.ysize); m_parseState = ParseJpegXLError; return false; } - if (sizeof(void *) <= 4) { - /* On 32bit systems, there is limited address space. - * We skip imagess bigger than 8192 x 8192 pixels. - * If we don't do it, abort() in libjxl may close whole application */ - if (m_basicinfo.xsize > ((8192 * 8192) / m_basicinfo.ysize)) { - qWarning("JXL image (%dx%d) is too large for 32bit build of the plug-in", m_basicinfo.xsize, m_basicinfo.ysize); - m_parseState = ParseJpegXLError; - return false; - } - } else { - /* On 64bit systems - * We skip images bigger than 16384 x 16384 pixels. - * It is an artificial limit not to use extreme amount of memory */ - if (m_basicinfo.xsize > ((16384 * 16384) / m_basicinfo.ysize)) { - qWarning("JXL image (%dx%d) is bigger than security limit 256 megapixels", m_basicinfo.xsize, m_basicinfo.ysize); - m_parseState = ParseJpegXLError; - return false; - } - } - m_parseState = ParseJpegXLBasicInfoParsed; return true; } @@ -256,27 +259,50 @@ bool QJpegXLHandler::countALLFrames() m_input_pixel_format.endianness = JXL_NATIVE_ENDIAN; m_input_pixel_format.align = 0; - m_input_pixel_format.num_channels = 4; + m_input_pixel_format.num_channels = m_basicinfo.num_color_channels == 1 ? 1 : 4; if (m_basicinfo.bits_per_sample > 8) { // high bit depth - m_input_pixel_format.data_type = JXL_TYPE_UINT16; - m_buffer_size = 8 * (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize; - m_input_image_format = QImage::Format_RGBA64; +#ifdef JXL_HDR_PRESERVATION_DISABLED + bool is_fp = false; +#else + bool is_fp = m_basicinfo.exponent_bits_per_sample > 0 && m_basicinfo.num_color_channels == 3; +#endif + m_input_pixel_format.data_type = is_fp ? JXL_TYPE_FLOAT16 : JXL_TYPE_UINT16; + m_buffer_size = (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize * m_input_pixel_format.num_channels * 2; - if (loadalpha) { - m_target_image_format = QImage::Format_RGBA64; + if (m_basicinfo.num_color_channels == 1) { + m_input_pixel_format.data_type = JXL_TYPE_UINT16; + m_input_image_format = m_target_image_format = QImage::Format_Grayscale16; + m_buffer_size = (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize * m_input_pixel_format.num_channels * 2; + } else if (m_basicinfo.bits_per_sample > 16 && is_fp) { + m_input_pixel_format.data_type = JXL_TYPE_FLOAT; + m_input_image_format = QImage::Format_RGBA32FPx4; + m_buffer_size = (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize * m_input_pixel_format.num_channels * 4; + if (loadalpha) + m_target_image_format = QImage::Format_RGBA32FPx4; + else + m_target_image_format = QImage::Format_RGBX32FPx4; } else { - m_target_image_format = QImage::Format_RGBX64; + m_buffer_size = (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize * m_input_pixel_format.num_channels * 2; + m_input_image_format = is_fp ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBA64; + if (loadalpha) + m_target_image_format = is_fp ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBA64; + else + m_target_image_format = is_fp ? QImage::Format_RGBX16FPx4 : QImage::Format_RGBX64; } } else { // 8bit depth m_input_pixel_format.data_type = JXL_TYPE_UINT8; - m_buffer_size = 4 * (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize; - m_input_image_format = QImage::Format_RGBA8888; + m_buffer_size = (size_t)m_basicinfo.xsize * (size_t)m_basicinfo.ysize * m_input_pixel_format.num_channels; - if (loadalpha) { - m_target_image_format = QImage::Format_ARGB32; + if (m_basicinfo.num_color_channels == 1) { + m_input_image_format = m_target_image_format = QImage::Format_Grayscale8; } else { - m_target_image_format = QImage::Format_RGB32; + m_input_image_format = QImage::Format_RGBA8888; + if (loadalpha) { + m_target_image_format = QImage::Format_ARGB32; + } else { + m_target_image_format = QImage::Format_RGB32; + } } } @@ -376,6 +402,12 @@ bool QJpegXLHandler::countALLFrames() m_framedelays[0] = 0; } +#ifndef JXL_DECODE_BOXES_DISABLED + if (!decodeBoxes()) { + return false; + } +#endif + if (!rewind()) { return false; } @@ -402,6 +434,9 @@ bool QJpegXLHandler::decode_one_frame() } m_current_image.setColorSpace(m_colorspace); + if (!m_xmp.isEmpty()) { + m_current_image.setText(QStringLiteral(META_KEY_XMP_ADOBE), QString::fromUtf8(m_xmp)); + } if (JxlDecoderSetImageOutBuffer(m_decoder, &m_input_pixel_format, m_current_image.bits(), m_buffer_size) != JXL_DEC_SUCCESS) { qWarning("ERROR: JxlDecoderSetImageOutBuffer failed"); @@ -463,6 +498,30 @@ bool QJpegXLHandler::read(QImage *image) } } +template +void packRGBPixels(QImage &img) +{ + // pack pixel data + auto dest_pixels = reinterpret_cast(img.bits()); + for (qint32 y = 0; y < img.height(); y++) { + auto src_pixels = reinterpret_cast(img.constScanLine(y)); + for (qint32 x = 0; x < img.width(); x++) { + // R + *dest_pixels = *src_pixels; + dest_pixels++; + src_pixels++; + // G + *dest_pixels = *src_pixels; + dest_pixels++; + src_pixels++; + // B + *dest_pixels = *src_pixels; + dest_pixels++; + src_pixels += 2; // skipalpha + } + } +} + bool QJpegXLHandler::write(const QImage &image) { if (image.format() == QImage::Format_Invalid) { @@ -470,36 +529,47 @@ bool QJpegXLHandler::write(const QImage &image) return false; } - if ((image.width() > 0) && (image.height() > 0)) { - if ((image.width() > 65535) || (image.height() > 65535)) { - qWarning("Image (%dx%d) is too large to save!", image.width(), image.height()); - return false; - } - - if (sizeof(void *) <= 4) { - if (image.width() > ((8192 * 8192) / image.height())) { - qWarning("Image (%dx%d) is too large save via 32bit build of JXL plug-in", image.width(), image.height()); - return false; - } - } else { - if (image.width() > ((16384 * 16384) / image.height())) { - qWarning("Image (%dx%d) will not be saved because it has more than 256 megapixels", image.width(), image.height()); - return false; - } - } - } else { + if ((image.width() == 0) || (image.height() == 0)) { qWarning("Image has zero dimension!"); return false; } - int save_depth = 8; // 8 or 16 + if ((image.width() > MAX_IMAGE_WIDTH) || (image.height() > MAX_IMAGE_HEIGHT)) { + qWarning("Image (%dx%d) is too large to save!", image.width(), image.height()); + return false; + } + + size_t pixel_count = size_t(image.width()) * image.height(); + if (MAX_IMAGE_PIXELS && pixel_count > MAX_IMAGE_PIXELS) { + qWarning("Image (%dx%d) will not be saved because it has more than %d megapixels!", image.width(), image.height(), MAX_IMAGE_PIXELS / 1024 / 1024); + return false; + } + + int save_depth = 8; // 8 / 16 / 32 + bool save_fp = false; + bool is_gray = false; // depth detection switch (image.format()) { + case QImage::Format_RGBX32FPx4: + case QImage::Format_RGBA32FPx4: + case QImage::Format_RGBA32FPx4_Premultiplied: +#ifndef JXL_HDR_PRESERVATION_DISABLED + save_depth = 32; + save_fp = true; + break; +#endif + case QImage::Format_RGBX16FPx4: + case QImage::Format_RGBA16FPx4: + case QImage::Format_RGBA16FPx4_Premultiplied: +#ifndef JXL_HDR_PRESERVATION_DISABLED + save_depth = 16; + save_fp = true; + break; +#endif case QImage::Format_BGR30: case QImage::Format_A2BGR30_Premultiplied: case QImage::Format_RGB30: case QImage::Format_A2RGB30_Premultiplied: - case QImage::Format_Grayscale16: case QImage::Format_RGBX64: case QImage::Format_RGBA64: case QImage::Format_RGBA64_Premultiplied: @@ -514,6 +584,21 @@ bool QJpegXLHandler::write(const QImage &image) case QImage::Format_RGBA8888_Premultiplied: save_depth = 8; break; + case QImage::Format_Grayscale16: + save_depth = 16; + is_gray = true; + break; + case QImage::Format_Grayscale8: + case QImage::Format_Alpha8: + case QImage::Format_Mono: + case QImage::Format_MonoLSB: + save_depth = 8; + is_gray = true; + break; + case QImage::Format_Indexed8: + save_depth = 8; + is_gray = image.isGrayscale(); + break; default: if (image.depth() > 32) { save_depth = 16; @@ -528,6 +613,7 @@ bool QJpegXLHandler::write(const QImage &image) qWarning("Failed to create Jxl encoder"); return false; } + JxlEncoderUseBoxes(encoder); if (m_quality > 100) { m_quality = 100; @@ -538,28 +624,28 @@ bool QJpegXLHandler::write(const QImage &image) JxlBasicInfo output_info; JxlEncoderInitBasicInfo(&output_info); - bool convert_color_profile; QByteArray iccprofile; - - if (image.colorSpace().isValid() && (m_quality < 100)) { - if (image.colorSpace().primaries() != QColorSpace::Primaries::SRgb || image.colorSpace().transferFunction() != QColorSpace::TransferFunction::SRgb) { - convert_color_profile = true; - } else { - convert_color_profile = false; - } - } else { // lossless or no profile or Qt-unsupported ICC profile - convert_color_profile = false; - iccprofile = image.colorSpace().iccProfile(); + QColorSpace tmpcs = image.colorSpace(); + if (!tmpcs.isValid() || tmpcs.primaries() != QColorSpace::Primaries::SRgb || tmpcs.transferFunction() != QColorSpace::TransferFunction::SRgb || m_quality == 100) { + // no profile or Qt-unsupported ICC profile + iccprofile = tmpcs.iccProfile(); + // note: lossless encoding requires uses_original_profile = JXL_TRUE if (iccprofile.size() > 0 || m_quality == 100) { output_info.uses_original_profile = JXL_TRUE; } } - if (save_depth == 16 && (image.hasAlphaChannel() || output_info.uses_original_profile)) { + // clang-format off + if ( (save_depth > 8 && (image.hasAlphaChannel() || output_info.uses_original_profile)) + || (save_depth > 16) + || (pixel_count > FEATURE_LEVEL_5_PIXELS) + || (image.width() > FEATURE_LEVEL_5_WIDTH) + || (image.height() > FEATURE_LEVEL_5_HEIGHT)) { output_info.have_container = JXL_TRUE; JxlEncoderUseContainer(encoder, JXL_TRUE); JxlEncoderSetCodestreamLevel(encoder, 10); } + // clang-format on void *runner = nullptr; int num_worker_threads = qBound(1, QThread::idealThreadCount(), 64); @@ -581,7 +667,6 @@ bool QJpegXLHandler::write(const QImage &image) pixel_format.endianness = JXL_NATIVE_ENDIAN; pixel_format.align = 0; - output_info.num_color_channels = 3; output_info.animation.tps_numerator = 10; output_info.animation.tps_denominator = 1; output_info.orientation = JXL_ORIENT_IDENTITY; @@ -601,24 +686,60 @@ bool QJpegXLHandler::write(const QImage &image) output_info.orientation = JXL_ORIENT_ROTATE_90_CCW; } - if (save_depth > 8) { // 16bit depth + if (save_depth > 8 && is_gray) { // 16bit depth gray pixel_format.data_type = JXL_TYPE_UINT16; + pixel_format.align = 4; + output_info.num_color_channels = 1; + output_info.bits_per_sample = 16; + tmpformat = QImage::Format_Grayscale16; + pixel_format.num_channels = 1; + } else if (is_gray) { // 8bit depth gray + pixel_format.data_type = JXL_TYPE_UINT8; + pixel_format.align = 4; + output_info.num_color_channels = 1; + output_info.bits_per_sample = 8; + tmpformat = QImage::Format_Grayscale8; + pixel_format.num_channels = 1; + } else if (save_depth > 16) { // 32bit depth rgb + pixel_format.data_type = JXL_TYPE_FLOAT; + output_info.exponent_bits_per_sample = 8; + output_info.num_color_channels = 3; + output_info.bits_per_sample = 32; + if (image.hasAlphaChannel()) { + tmpformat = QImage::Format_RGBA32FPx4; + pixel_format.num_channels = 4; + output_info.alpha_bits = 32; + output_info.alpha_exponent_bits = 8; + output_info.num_extra_channels = 1; + } else { + tmpformat = QImage::Format_RGBX32FPx4; + pixel_format.num_channels = 3; + output_info.alpha_bits = 0; + output_info.num_extra_channels = 0; + } + } else if (save_depth > 8) { // 16bit depth rgb + pixel_format.data_type = save_fp ? JXL_TYPE_FLOAT16 : JXL_TYPE_UINT16; + output_info.exponent_bits_per_sample = save_fp ? 5 : 0; + output_info.num_color_channels = 3; output_info.bits_per_sample = 16; if (image.hasAlphaChannel()) { - tmpformat = QImage::Format_RGBA64; + tmpformat = save_fp ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBA64; pixel_format.num_channels = 4; output_info.alpha_bits = 16; + output_info.alpha_exponent_bits = save_fp ? 5 : 0; output_info.num_extra_channels = 1; } else { - tmpformat = QImage::Format_RGBX64; + tmpformat = save_fp ? QImage::Format_RGBX16FPx4 : QImage::Format_RGBX64; pixel_format.num_channels = 3; output_info.alpha_bits = 0; + output_info.num_extra_channels = 0; } - } else { // 8bit depth + } else { // 8bit depth rgb pixel_format.data_type = JXL_TYPE_UINT8; - + pixel_format.align = 4; + output_info.num_color_channels = 3; output_info.bits_per_sample = 8; if (image.hasAlphaChannel()) { @@ -630,15 +751,13 @@ bool QJpegXLHandler::write(const QImage &image) tmpformat = QImage::Format_RGB888; pixel_format.num_channels = 3; output_info.alpha_bits = 0; + output_info.num_extra_channels = 0; } } - const QImage tmpimage = - convert_color_profile ? image.convertToFormat(tmpformat).convertedToColorSpace(QColorSpace(QColorSpace::SRgb)) : image.convertToFormat(tmpformat); - + QImage tmpimage = image.convertToFormat(tmpformat); const size_t xsize = tmpimage.width(); const size_t ysize = tmpimage.height(); - const size_t buffer_size = (save_depth > 8) ? (2 * pixel_format.num_channels * xsize * ysize) : (pixel_format.num_channels * xsize * ysize); if (xsize == 0 || ysize == 0 || tmpimage.isNull()) { qWarning("Unable to allocate memory for output image"); @@ -662,7 +781,22 @@ bool QJpegXLHandler::write(const QImage &image) return false; } - if (!convert_color_profile && iccprofile.size() > 0) { + auto xmp_data = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8(); + if (!xmp_data.isEmpty()) { + const char *box_type = "xml "; + status = JxlEncoderAddBox(encoder, box_type, reinterpret_cast(xmp_data.constData()), xmp_data.size(), JXL_FALSE); + if (status != JXL_ENC_SUCCESS) { + qWarning("JxlEncoderAddBox failed!"); + if (runner) { + JxlThreadParallelRunnerDestroy(runner); + } + JxlEncoderDestroy(encoder); + return false; + } + } + JxlEncoderCloseBoxes(encoder); // no more metadata + + if (iccprofile.size() > 0) { status = JxlEncoderSetICCProfile(encoder, reinterpret_cast(iccprofile.constData()), iccprofile.size()); if (status != JXL_ENC_SUCCESS) { qWarning("JxlEncoderSetICCProfile failed!"); @@ -693,61 +827,30 @@ bool QJpegXLHandler::write(const QImage &image) JxlEncoderSetFrameLossless(encoder_options, (m_quality == 100) ? JXL_TRUE : JXL_FALSE); - if (image.hasAlphaChannel() || ((save_depth == 8) && (xsize % 4 == 0))) { - status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, static_cast(tmpimage.constBits()), buffer_size); - } else { - if (save_depth > 8) { // 16bit depth without alpha channel - uint16_t *tmp_buffer = new (std::nothrow) uint16_t[3 * xsize * ysize]; - if (!tmp_buffer) { - qWarning("Memory allocation error"); - if (runner) { - JxlThreadParallelRunnerDestroy(runner); - } - JxlEncoderDestroy(encoder); - return false; - } + size_t buffer_size = size_t(tmpimage.bytesPerLine()) * tmpimage.height(); + if (!image.hasAlphaChannel() && save_depth > 8 && !is_gray) { // pack pixel on tmpimage + buffer_size = (size_t(save_depth / 8) * pixel_format.num_channels * xsize * ysize); - uint16_t *dest_pixels = tmp_buffer; - for (int y = 0; y < tmpimage.height(); y++) { - const uint16_t *src_pixels = reinterpret_cast(tmpimage.constScanLine(y)); - for (int x = 0; x < tmpimage.width(); x++) { - // R - *dest_pixels = *src_pixels; - dest_pixels++; - src_pixels++; - // G - *dest_pixels = *src_pixels; - dest_pixels++; - src_pixels++; - // B - *dest_pixels = *src_pixels; - dest_pixels++; - src_pixels += 2; // skipalpha - } + // detaching image + tmpimage.detach(); + if (tmpimage.isNull()) { + qWarning("Memory allocation error"); + if (runner) { + JxlThreadParallelRunnerDestroy(runner); } - status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, static_cast(tmp_buffer), buffer_size); - delete[] tmp_buffer; - } else { // 8bit depth without alpha channel - uchar *tmp_buffer8 = new (std::nothrow) uchar[3 * xsize * ysize]; - if (!tmp_buffer8) { - qWarning("Memory allocation error"); - if (runner) { - JxlThreadParallelRunnerDestroy(runner); - } - JxlEncoderDestroy(encoder); - return false; - } - - uchar *dest_pixels8 = tmp_buffer8; - const size_t rowbytes = 3 * xsize; - for (int y = 0; y < tmpimage.height(); y++) { - memcpy(dest_pixels8, tmpimage.constScanLine(y), rowbytes); - dest_pixels8 += rowbytes; - } - status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, static_cast(tmp_buffer8), buffer_size); - delete[] tmp_buffer8; + JxlEncoderDestroy(encoder); + return false; } + + // pack pixel data + if (save_depth > 16 && save_fp) + packRGBPixels(tmpimage); + else if (save_fp) + packRGBPixels(tmpimage); + else + packRGBPixels(tmpimage); } + status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, static_cast(tmpimage.constBits()), buffer_size); if (status == JXL_ENC_ERROR) { qWarning("JxlEncoderAddImageFrame failed!"); @@ -1060,6 +1163,37 @@ bool QJpegXLHandler::rewind() return true; } +bool QJpegXLHandler::decodeBoxes() +{ + JxlDecoderStatus status; + do { // decode metadata + status = JxlDecoderProcessInput(m_decoder); + if (status == JXL_DEC_BOX) { + JxlBoxType type; + JxlDecoderGetBoxType(m_decoder, type, JXL_FALSE); + if (memcmp(type, "xml ", 4) == 0) { + uint64_t size; + if (JxlDecoderGetBoxSizeRaw(m_decoder, &size) == JXL_DEC_SUCCESS) { + m_xmp = QByteArray(size, '\0'); + JxlDecoderSetBoxBuffer(m_decoder, reinterpret_cast(m_xmp.data()), m_xmp.size()); + } + } + } + } while (status == JXL_DEC_BOX); + + if (status == JXL_DEC_ERROR) { + qWarning("ERROR: JXL decoding failed"); + m_parseState = ParseJpegXLError; + return false; + } + if (status == JXL_DEC_NEED_MORE_INPUT) { + qWarning("ERROR: JXL data incomplete"); + m_parseState = ParseJpegXLError; + return false; + } + return true; +} + QImageIOPlugin::Capabilities QJpegXLPlugin::capabilities(QIODevice *device, const QByteArray &format) const { if (format == "jxl") { diff --git a/src/imageformats/jxl_p.h b/src/imageformats/jxl_p.h index 1563012..7cffafb 100644 --- a/src/imageformats/jxl_p.h +++ b/src/imageformats/jxl_p.h @@ -51,6 +51,7 @@ private: bool countALLFrames(); bool decode_one_frame(); bool rewind(); + bool decodeBoxes(); enum ParseJpegXLState { ParseJpegXLError = -1, @@ -77,6 +78,7 @@ private: QImage m_current_image; QColorSpace m_colorspace; + QByteArray m_xmp; QImage::Format m_input_image_format; QImage::Format m_target_image_format;