diff --git a/CMakeLists.txt b/CMakeLists.txt index 7dfb189..590b023 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ include(KDEGitCommitHooks) include(CheckIncludeFiles) +include(FindPkgConfig) set(REQUIRED_QT_VERSION 5.15.2) find_package(Qt5Gui ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE) @@ -58,11 +59,17 @@ set_package_properties(libavif PROPERTIES option(KIMAGEFORMATS_HEIF "Enable plugin for HEIF format" OFF) if(KIMAGEFORMATS_HEIF) - include(FindPkgConfig) pkg_check_modules(LibHeif IMPORTED_TARGET libheif>=1.10.0) endif() add_feature_info(LibHeif LibHeif_FOUND "required for the QImage plugin for HEIF/HEIC images") +option(KIMAGEFORMATS_JXL "Enable plugin for JPEG XL format" ON) +if(KIMAGEFORMATS_JXL) + pkg_check_modules(LibJXL IMPORTED_TARGET libjxl>=0.6.1) + pkg_check_modules(LibJXLThreads IMPORTED_TARGET libjxl_threads>=0.6.1) +endif() +add_feature_info(LibJXL LibJXL_FOUND "required for the QImage plugin for JPEG XL images") + # 050d00 (5.13) triggers a BIC in qimageiohandler.h, in Qt 5.13, so do not enable that until we can require 5.14 # https://codereview.qt-project.org/c/qt/qtbase/+/279215 add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050f02) diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 2de3b9c..39fb28f 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -82,6 +82,12 @@ if (LibHeif_FOUND) ) endif() +if (LibJXL_FOUND AND LibJXLThreads_FOUND) + kimageformats_read_tests( + jxl + ) +endif() + # Allow some fuzziness when reading this formats, to allow for # rounding errors (eg: in alpha blending). kimageformats_read_tests(FUZZ 1 diff --git a/autotests/read/jxl/rgb.jxl b/autotests/read/jxl/rgb.jxl new file mode 100644 index 0000000..7cf0a8d Binary files /dev/null and b/autotests/read/jxl/rgb.jxl differ diff --git a/autotests/read/jxl/rgb.png b/autotests/read/jxl/rgb.png new file mode 100644 index 0000000..1318428 Binary files /dev/null and b/autotests/read/jxl/rgb.png differ diff --git a/autotests/read/jxl/rgba.jxl b/autotests/read/jxl/rgba.jxl new file mode 100644 index 0000000..334475b Binary files /dev/null and b/autotests/read/jxl/rgba.jxl differ diff --git a/autotests/read/jxl/rgba.png b/autotests/read/jxl/rgba.png new file mode 100644 index 0000000..3def37a Binary files /dev/null and b/autotests/read/jxl/rgba.png differ diff --git a/src/imageformats/CMakeLists.txt b/src/imageformats/CMakeLists.txt index b830a7c..79570c1 100644 --- a/src/imageformats/CMakeLists.txt +++ b/src/imageformats/CMakeLists.txt @@ -83,6 +83,14 @@ endif() ################################## +if (LibJXL_FOUND AND LibJXLThreads_FOUND) + kimageformats_add_plugin(kimg_jxl SOURCES jxl.cpp) + target_link_libraries(kimg_jxl PkgConfig::LibJXL PkgConfig::LibJXLThreads) + install(FILES jxl.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/) +endif() + +################################## + kimageformats_add_plugin(kimg_pcx SOURCES pcx.cpp) install(FILES pcx.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/qimageioplugins/) diff --git a/src/imageformats/jxl.cpp b/src/imageformats/jxl.cpp new file mode 100644 index 0000000..98f6fe8 --- /dev/null +++ b/src/imageformats/jxl.cpp @@ -0,0 +1,876 @@ +/* + JPEG XL (JXL) support for QImage. + + SPDX-FileCopyrightText: 2021 Daniel Novomesky + + SPDX-License-Identifier: BSD-2-Clause +*/ + +#include +#include + +#include "jxl_p.h" +#include +#include + +QJpegXLHandler::QJpegXLHandler() + : m_parseState(ParseJpegXLNotParsed) + , m_quality(90) + , m_currentimage_index(0) + , m_previousimage_index(-1) + , m_decoder(nullptr) + , m_runner(nullptr) + , m_next_image_delay(0) + , m_input_image_format(QImage::Format_Invalid) + , m_target_image_format(QImage::Format_Invalid) + , m_buffer_size(0) +{ +} + +QJpegXLHandler::~QJpegXLHandler() +{ + if (m_runner) { + JxlThreadParallelRunnerDestroy(m_runner); + } + if (m_decoder) { + JxlDecoderDestroy(m_decoder); + } +} + +bool QJpegXLHandler::canRead() const +{ + if (m_parseState == ParseJpegXLNotParsed && !canRead(device())) { + return false; + } + + if (m_parseState != ParseJpegXLError) { + setFormat("jxl"); + return true; + } + return false; +} + +bool QJpegXLHandler::canRead(QIODevice *device) +{ + if (!device) { + return false; + } + QByteArray header = device->peek(32); + if (header.size() < 12) { + return false; + } + + JxlSignature signature = JxlSignatureCheck((const uint8_t *)header.constData(), header.size()); + if (signature == JXL_SIG_CODESTREAM || signature == JXL_SIG_CONTAINER) { + return true; + } + return false; +} + +bool QJpegXLHandler::ensureParsed() const +{ + if (m_parseState == ParseJpegXLSuccess || m_parseState == ParseJpegXLBasicInfoParsed) { + return true; + } + if (m_parseState == ParseJpegXLError) { + return false; + } + + QJpegXLHandler *that = const_cast(this); + + return that->ensureDecoder(); +} + +bool QJpegXLHandler::ensureALLCounted() const +{ + if (!ensureParsed()) { + return false; + } + + if (m_parseState == ParseJpegXLSuccess) { + return true; + } + + QJpegXLHandler *that = const_cast(this); + + return that->countALLFrames(); +} + +bool QJpegXLHandler::ensureDecoder() +{ + if (m_decoder) { + return true; + } + + m_rawData = device()->readAll(); + + if (m_rawData.isEmpty()) { + return false; + } + + JxlSignature signature = JxlSignatureCheck((const uint8_t *)m_rawData.constData(), m_rawData.size()); + if (signature != JXL_SIG_CODESTREAM && signature != JXL_SIG_CONTAINER) { + m_parseState = ParseJpegXLError; + return false; + } + + m_decoder = JxlDecoderCreate(nullptr); + if (!m_decoder) { + qWarning("ERROR: JxlDecoderCreate failed"); + m_parseState = ParseJpegXLError; + return false; + } + + int num_worker_threads = QThread::idealThreadCount(); + if (!m_runner && num_worker_threads >= 4) { + /* use half of the threads because plug-in is usually used in environment + * where application performs another tasks in backround (pre-load other images) */ + num_worker_threads = num_worker_threads / 2; + num_worker_threads = qBound(2, num_worker_threads, 64); + m_runner = JxlThreadParallelRunnerCreate(nullptr, num_worker_threads); + + if (JxlDecoderSetParallelRunner(m_decoder, JxlThreadParallelRunner, m_runner) != JXL_DEC_SUCCESS) { + qWarning("ERROR: JxlDecoderSetParallelRunner failed"); + m_parseState = ParseJpegXLError; + return false; + } + } + + if (JxlDecoderSetInput(m_decoder, (const uint8_t *)m_rawData.constData(), m_rawData.size()) != JXL_DEC_SUCCESS) { + qWarning("ERROR: JxlDecoderSetInput failed"); + m_parseState = ParseJpegXLError; + return false; + } + + JxlDecoderStatus status = JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME); + 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; + return false; + } + + status = JxlDecoderGetBasicInfo(m_decoder, &m_basicinfo); + if (status != JXL_DEC_SUCCESS) { + qWarning("ERROR: JXL basic info not available"); + m_parseState = ParseJpegXLError; + return false; + } + + if (m_basicinfo.xsize == 0 || m_basicinfo.ysize == 0) { + qWarning("ERROR: JXL image has zero dimensions"); + m_parseState = ParseJpegXLError; + return false; + } + + if (m_basicinfo.xsize > 32768 || m_basicinfo.ysize > 32768) { + qWarning("JXL image (%dx%d) is too large", m_basicinfo.xsize, m_basicinfo.ysize); + m_parseState = ParseJpegXLError; + return false; + } else 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 * m_basicinfo.ysize) > 67108864) { + 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; + } + } + + m_parseState = ParseJpegXLBasicInfoParsed; + return true; +} + +bool QJpegXLHandler::countALLFrames() +{ + if (m_parseState != ParseJpegXLBasicInfoParsed) { + return false; + } + + JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder); + if (status != JXL_DEC_COLOR_ENCODING) { + qWarning("Unexpected event %d instead of JXL_DEC_COLOR_ENCODING", status); + m_parseState = ParseJpegXLError; + return false; + } + + JxlColorEncoding color_encoding; + if (m_basicinfo.uses_original_profile == JXL_FALSE) { + JxlColorEncodingSetToSRGB(&color_encoding, JXL_FALSE); + JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding); + } + + bool loadalpha; + + if (m_basicinfo.alpha_bits > 0) { + loadalpha = true; + } else { + loadalpha = false; + } + + m_input_pixel_format.endianness = JXL_NATIVE_ENDIAN; + m_input_pixel_format.align = 0; + m_input_pixel_format.num_channels = 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; + + if (loadalpha) { + m_target_image_format = QImage::Format_RGBA64; + } else { + m_target_image_format = 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; + + if (loadalpha) { + m_target_image_format = QImage::Format_ARGB32; + } else { + m_target_image_format = QImage::Format_RGB32; + } + } + + status = JxlDecoderGetColorAsEncodedProfile(m_decoder, &m_input_pixel_format, JXL_COLOR_PROFILE_TARGET_DATA, &color_encoding); + + if (status == JXL_DEC_SUCCESS && color_encoding.color_space == JXL_COLOR_SPACE_RGB && color_encoding.white_point == JXL_WHITE_POINT_D65 + && color_encoding.primaries == JXL_PRIMARIES_SRGB && color_encoding.transfer_function == JXL_TRANSFER_FUNCTION_SRGB) { + m_colorspace = QColorSpace(QColorSpace::SRgb); + } else { + size_t icc_size = 0; + if (JxlDecoderGetICCProfileSize(m_decoder, &m_input_pixel_format, JXL_COLOR_PROFILE_TARGET_DATA, &icc_size) == JXL_DEC_SUCCESS) { + if (icc_size > 0) { + QByteArray icc_data((int)icc_size, 0); + if (JxlDecoderGetColorAsICCProfile(m_decoder, &m_input_pixel_format, JXL_COLOR_PROFILE_TARGET_DATA, (uint8_t *)icc_data.data(), icc_data.size()) + == JXL_DEC_SUCCESS) { + m_colorspace = QColorSpace::fromIccProfile(icc_data); + + if (!m_colorspace.isValid()) { + qWarning("JXL image has Qt-unsupported or invalid ICC profile!"); + } + } else { + qWarning("Failed to obtain data from JPEG XL decoder"); + } + } else { + qWarning("Empty ICC data"); + } + } else { + qWarning("no ICC, other color profile"); + } + } + + if (m_basicinfo.have_animation) { // count all frames + JxlFrameHeader frame_header; + int delay; + + for (status = JxlDecoderProcessInput(m_decoder); status != JXL_DEC_SUCCESS; status = JxlDecoderProcessInput(m_decoder)) { + if (status != JXL_DEC_FRAME) { + switch (status) { + case JXL_DEC_ERROR: + qWarning("ERROR: JXL decoding failed"); + break; + case JXL_DEC_NEED_MORE_INPUT: + qWarning("ERROR: JXL data incomplete"); + break; + default: + qWarning("Unexpected event %d instead of JXL_DEC_FRAME", status); + break; + } + m_parseState = ParseJpegXLError; + return false; + } + + if (JxlDecoderGetFrameHeader(m_decoder, &frame_header) != JXL_DEC_SUCCESS) { + qWarning("ERROR: JxlDecoderGetFrameHeader failed"); + m_parseState = ParseJpegXLError; + return false; + } + + if (m_basicinfo.animation.tps_denominator > 0 && m_basicinfo.animation.tps_numerator > 0) { + delay = (int)(0.5 + 1000.0 * frame_header.duration * m_basicinfo.animation.tps_denominator / m_basicinfo.animation.tps_numerator); + } else { + delay = 0; + } + + m_framedelays.append(delay); + } + + if (m_framedelays.isEmpty()) { + qWarning("no frames loaded by the JXL plug-in"); + m_parseState = ParseJpegXLError; + return false; + } + + if (m_framedelays.count() == 1) { + qWarning("JXL file was marked as animation but it has only one frame."); + m_basicinfo.have_animation = JXL_FALSE; + } + } else { // static picture + m_framedelays.resize(1); + m_framedelays[0] = 0; + } + + if (!rewind()) { + return false; + } + + m_next_image_delay = m_framedelays[0]; + m_parseState = ParseJpegXLSuccess; + return true; +} + +bool QJpegXLHandler::decode_one_frame() +{ + JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder); + if (status != JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + qWarning("Unexpected event %d instead of JXL_DEC_NEED_IMAGE_OUT_BUFFER", status); + m_parseState = ParseJpegXLError; + return false; + } + + m_current_image = QImage(m_basicinfo.xsize, m_basicinfo.ysize, m_input_image_format); + if (m_current_image.isNull()) { + qWarning("Memory cannot be allocated"); + m_parseState = ParseJpegXLError; + return false; + } + + m_current_image.setColorSpace(m_colorspace); + + if (JxlDecoderSetImageOutBuffer(m_decoder, &m_input_pixel_format, m_current_image.bits(), m_buffer_size) != JXL_DEC_SUCCESS) { + qWarning("ERROR: JxlDecoderSetImageOutBuffer failed"); + m_parseState = ParseJpegXLError; + return false; + } + + status = JxlDecoderProcessInput(m_decoder); + if (status != JXL_DEC_FULL_IMAGE) { + qWarning("Unexpected event %d instead of JXL_DEC_FULL_IMAGE", status); + m_parseState = ParseJpegXLError; + return false; + } + + if (m_target_image_format != m_input_image_format) { + m_current_image.convertTo(m_target_image_format); + } + + m_next_image_delay = m_framedelays[m_currentimage_index]; + m_previousimage_index = m_currentimage_index; + + if (m_framedelays.count() > 1) { + m_currentimage_index++; + + if (m_currentimage_index >= m_framedelays.count()) { + if (!rewind()) { + return false; + } + } + } + + return true; +} + +bool QJpegXLHandler::read(QImage *image) +{ + if (!ensureALLCounted()) { + return false; + } + + if (m_currentimage_index == m_previousimage_index) { + *image = m_current_image; + return jumpToNextImage(); + } + + if (decode_one_frame()) { + *image = m_current_image; + return true; + } else { + return false; + } +} + +bool QJpegXLHandler::write(const QImage &image) +{ + if (image.format() == QImage::Format_Invalid) { + qWarning("No image data to save"); + return false; + } + + if ((image.width() > 32768) || (image.height() > 32768)) { + qWarning("Image is too large"); + return false; + } + + JxlEncoder *encoder = JxlEncoderCreate(nullptr); + if (!encoder) { + qWarning("Failed to create Jxl encoder"); + return false; + } + + void *runner = nullptr; + int num_worker_threads = qBound(1, QThread::idealThreadCount(), 64); + + if (num_worker_threads > 1) { + runner = JxlThreadParallelRunnerCreate(nullptr, num_worker_threads); + if (JxlEncoderSetParallelRunner(encoder, JxlThreadParallelRunner, runner) != JXL_ENC_SUCCESS) { + qWarning("JxlEncoderSetParallelRunner failed"); + JxlThreadParallelRunnerDestroy(runner); + JxlEncoderDestroy(encoder); + return false; + } + } + + JxlEncoderOptions *encoder_options = JxlEncoderOptionsCreate(encoder, nullptr); + + if (m_quality > 100) { + m_quality = 100; + } else if (m_quality < 0) { + m_quality = 90; + } + + JxlEncoderOptionsSetDistance(encoder_options, (100.0f - m_quality) / 10.0f); + + JxlEncoderOptionsSetLossless(encoder_options, (m_quality == 100) ? JXL_TRUE : JXL_FALSE); + + JxlBasicInfo output_info; + JxlEncoderInitBasicInfo(&output_info); + + JxlColorEncoding color_profile; + JxlColorEncodingSetToSRGB(&color_profile, JXL_FALSE); + + bool convert_color_profile; + QByteArray iccprofile; + + if (image.colorSpace().isValid()) { + if (image.colorSpace().primaries() != QColorSpace::Primaries::SRgb || image.colorSpace().transferFunction() != QColorSpace::TransferFunction::SRgb) { + convert_color_profile = true; + } else { + convert_color_profile = false; + } + } else { // no profile or Qt-unsupported ICC profile + convert_color_profile = false; + iccprofile = image.colorSpace().iccProfile(); + if (iccprofile.size() > 0) { + output_info.uses_original_profile = 1; + } + } + + JxlPixelFormat pixel_format; + QImage::Format tmpformat; + JxlEncoderStatus status; + + pixel_format.data_type = JXL_TYPE_UINT16; + pixel_format.endianness = JXL_NATIVE_ENDIAN; + pixel_format.align = 0; + + if (image.hasAlphaChannel()) { + tmpformat = QImage::Format_RGBA64; + pixel_format.num_channels = 4; + output_info.alpha_bits = 16; + output_info.num_extra_channels = 1; + } else { + tmpformat = QImage::Format_RGBX64; + pixel_format.num_channels = 3; + output_info.alpha_bits = 0; + } + + const QImage tmpimage = + convert_color_profile ? image.convertToFormat(tmpformat).convertedToColorSpace(QColorSpace(QColorSpace::SRgb)) : image.convertToFormat(tmpformat); + + const size_t xsize = tmpimage.width(); + const size_t ysize = tmpimage.height(); + const size_t buffer_size = 2 * pixel_format.num_channels * xsize * ysize; + + if (xsize == 0 || ysize == 0 || tmpimage.isNull()) { + qWarning("Unable to allocate memory for output image"); + if (runner) { + JxlThreadParallelRunnerDestroy(runner); + } + JxlEncoderDestroy(encoder); + return false; + } + + output_info.xsize = tmpimage.width(); + output_info.ysize = tmpimage.height(); + output_info.bits_per_sample = 16; + output_info.intensity_target = 255.0f; + output_info.orientation = JXL_ORIENT_IDENTITY; + output_info.num_color_channels = 3; + output_info.animation.tps_numerator = 10; + output_info.animation.tps_denominator = 1; + + status = JxlEncoderSetBasicInfo(encoder, &output_info); + if (status != JXL_ENC_SUCCESS) { + qWarning("JxlEncoderSetBasicInfo failed!"); + if (runner) { + JxlThreadParallelRunnerDestroy(runner); + } + JxlEncoderDestroy(encoder); + return false; + } + + if (!convert_color_profile && iccprofile.size() > 0) { + status = JxlEncoderSetICCProfile(encoder, (const uint8_t *)iccprofile.constData(), iccprofile.size()); + if (status != JXL_ENC_SUCCESS) { + qWarning("JxlEncoderSetICCProfile failed!"); + if (runner) { + JxlThreadParallelRunnerDestroy(runner); + } + JxlEncoderDestroy(encoder); + return false; + } + } else { + status = JxlEncoderSetColorEncoding(encoder, &color_profile); + if (status != JXL_ENC_SUCCESS) { + qWarning("JxlEncoderSetColorEncoding failed!"); + if (runner) { + JxlThreadParallelRunnerDestroy(runner); + } + JxlEncoderDestroy(encoder); + return false; + } + } + + if (image.hasAlphaChannel()) { + status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, (void *)tmpimage.constBits(), buffer_size); + } else { + 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; + } + + 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 + } + } + status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, (void *)tmp_buffer, buffer_size); + delete[] tmp_buffer; + } + + if (status == JXL_ENC_ERROR) { + qWarning("JxlEncoderAddImageFrame failed!"); + if (runner) { + JxlThreadParallelRunnerDestroy(runner); + } + JxlEncoderDestroy(encoder); + return false; + } + + JxlEncoderCloseInput(encoder); + + std::vector compressed; + compressed.resize(4096); + size_t offset = 0; + uint8_t *next_out; + size_t avail_out; + do { + next_out = compressed.data() + offset; + avail_out = compressed.size() - offset; + status = JxlEncoderProcessOutput(encoder, &next_out, &avail_out); + + if (status == JXL_ENC_NEED_MORE_OUTPUT) { + offset = next_out - compressed.data(); + compressed.resize(compressed.size() * 2); + } else if (status == JXL_ENC_ERROR) { + qWarning("JxlEncoderProcessOutput failed!"); + if (runner) { + JxlThreadParallelRunnerDestroy(runner); + } + JxlEncoderDestroy(encoder); + return false; + } + } while (status != JXL_ENC_SUCCESS); + + if (runner) { + JxlThreadParallelRunnerDestroy(runner); + } + JxlEncoderDestroy(encoder); + + compressed.resize(next_out - compressed.data()); + + if (compressed.size() > 0) { + qint64 write_status = device()->write((const char *)compressed.data(), compressed.size()); + + if (write_status > 0) { + return true; + } else if (write_status == -1) { + qWarning("Write error: %s\n", qUtf8Printable(device()->errorString())); + } + } + + return false; +} + +QVariant QJpegXLHandler::option(ImageOption option) const +{ + if (option == Quality) { + return m_quality; + } + + if (!supportsOption(option) || !ensureParsed()) { + return QVariant(); + } + + switch (option) { + case Size: + return QSize(m_basicinfo.xsize, m_basicinfo.ysize); + case Animation: + if (m_basicinfo.have_animation) { + return true; + } else { + return false; + } + default: + return QVariant(); + } +} + +void QJpegXLHandler::setOption(ImageOption option, const QVariant &value) +{ + switch (option) { + case Quality: + m_quality = value.toInt(); + if (m_quality > 100) { + m_quality = 100; + } else if (m_quality < 0) { + m_quality = 90; + } + return; + default: + break; + } + QImageIOHandler::setOption(option, value); +} + +bool QJpegXLHandler::supportsOption(ImageOption option) const +{ + return option == Quality || option == Size || option == Animation; +} + +int QJpegXLHandler::imageCount() const +{ + if (!ensureParsed()) { + return 0; + } + + if (m_parseState == ParseJpegXLBasicInfoParsed) { + if (!m_basicinfo.have_animation) { + return 1; + } + + if (!ensureALLCounted()) { + return 0; + } + } + + if (!m_framedelays.isEmpty()) { + return m_framedelays.count(); + } + return 0; +} + +int QJpegXLHandler::currentImageNumber() const +{ + if (m_parseState == ParseJpegXLNotParsed) { + return -1; + } + + if (m_parseState == ParseJpegXLError || m_parseState == ParseJpegXLBasicInfoParsed || !m_decoder) { + return 0; + } + + return m_currentimage_index; +} + +bool QJpegXLHandler::jumpToNextImage() +{ + if (!ensureALLCounted()) { + return false; + } + + if (m_framedelays.count() > 1) { + m_currentimage_index++; + + if (m_currentimage_index >= m_framedelays.count()) { + if (!rewind()) { + return false; + } + } else { + JxlDecoderSkipFrames(m_decoder, 1); + } + } + + return true; +} + +bool QJpegXLHandler::jumpToImage(int imageNumber) +{ + if (!ensureALLCounted()) { + return false; + } + + if (imageNumber < 0 || imageNumber >= m_framedelays.count()) { + return false; + } + + if (imageNumber == m_currentimage_index) { + return true; + } + + if (imageNumber > m_currentimage_index) { + JxlDecoderSkipFrames(m_decoder, imageNumber - m_currentimage_index); + m_currentimage_index = imageNumber; + return true; + } + + if (!rewind()) { + return false; + } + + if (imageNumber > 0) { + JxlDecoderSkipFrames(m_decoder, imageNumber); + } + m_currentimage_index = imageNumber; + return true; +} + +int QJpegXLHandler::nextImageDelay() const +{ + if (!ensureALLCounted()) { + return 0; + } + + if (m_framedelays.count() < 2) { + return 0; + } + + return m_next_image_delay; +} + +int QJpegXLHandler::loopCount() const +{ + if (!ensureParsed()) { + return 0; + } + + if (m_basicinfo.have_animation) { + return 1; + } else { + return 0; + } +} + +bool QJpegXLHandler::rewind() +{ + m_currentimage_index = 0; + + JxlDecoderReleaseInput(m_decoder); + JxlDecoderRewind(m_decoder); + if (m_runner) { + if (JxlDecoderSetParallelRunner(m_decoder, JxlThreadParallelRunner, m_runner) != JXL_DEC_SUCCESS) { + qWarning("ERROR: JxlDecoderSetParallelRunner failed"); + m_parseState = ParseJpegXLError; + return false; + } + } + + if (JxlDecoderSetInput(m_decoder, (const uint8_t *)m_rawData.constData(), m_rawData.size()) != JXL_DEC_SUCCESS) { + qWarning("ERROR: JxlDecoderSetInput failed"); + m_parseState = ParseJpegXLError; + return false; + } + + if (m_basicinfo.uses_original_profile) { + if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) { + qWarning("ERROR: JxlDecoderSubscribeEvents failed"); + m_parseState = ParseJpegXLError; + return false; + } + } else { + if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) { + qWarning("ERROR: JxlDecoderSubscribeEvents failed"); + m_parseState = ParseJpegXLError; + return false; + } + + JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder); + if (status != JXL_DEC_COLOR_ENCODING) { + qWarning("Unexpected event %d instead of JXL_DEC_COLOR_ENCODING", status); + m_parseState = ParseJpegXLError; + return false; + } + + JxlColorEncoding color_encoding; + JxlColorEncodingSetToSRGB(&color_encoding, JXL_FALSE); + JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding); + } + + return true; +} + +QImageIOPlugin::Capabilities QJpegXLPlugin::capabilities(QIODevice *device, const QByteArray &format) const +{ + if (format == "jxl") { + return Capabilities(CanRead | CanWrite); + } + + if (!format.isEmpty()) { + return {}; + } + if (!device->isOpen()) { + return {}; + } + + Capabilities cap; + if (device->isReadable() && QJpegXLHandler::canRead(device)) { + cap |= CanRead; + } + + if (device->isWritable()) { + cap |= CanWrite; + } + + return cap; +} + +QImageIOHandler *QJpegXLPlugin::create(QIODevice *device, const QByteArray &format) const +{ + QImageIOHandler *handler = new QJpegXLHandler; + handler->setDevice(device); + handler->setFormat(format); + return handler; +} diff --git a/src/imageformats/jxl.desktop b/src/imageformats/jxl.desktop new file mode 100644 index 0000000..2d04498 --- /dev/null +++ b/src/imageformats/jxl.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=QImageIOPlugins +X-KDE-ImageFormat=jxl +X-KDE-MimeType=image/jxl +X-KDE-Read=true +X-KDE-Write=true diff --git a/src/imageformats/jxl.json b/src/imageformats/jxl.json new file mode 100644 index 0000000..d0a5c8c --- /dev/null +++ b/src/imageformats/jxl.json @@ -0,0 +1,4 @@ +{ + "Keys": [ "jxl" ], + "MimeTypes": [ "image/jxl" ] +} diff --git a/src/imageformats/jxl_p.h b/src/imageformats/jxl_p.h new file mode 100644 index 0000000..8339d7e --- /dev/null +++ b/src/imageformats/jxl_p.h @@ -0,0 +1,96 @@ +/* + JPEG XL (JXL) support for QImage. + + SPDX-FileCopyrightText: 2021 Daniel Novomesky + + SPDX-License-Identifier: BSD-2-Clause +*/ + +#ifndef KIMG_JXL_P_H +#define KIMG_JXL_P_H + +#include +#include +#include +#include +#include +#include +#include + +#include + +class QJpegXLHandler : public QImageIOHandler +{ +public: + QJpegXLHandler(); + ~QJpegXLHandler(); + + bool canRead() const override; + bool read(QImage *image) override; + bool write(const QImage &image) override; + + static bool canRead(QIODevice *device); + + QVariant option(ImageOption option) const override; + void setOption(ImageOption option, const QVariant &value) override; + bool supportsOption(ImageOption option) const override; + + int imageCount() const override; + int currentImageNumber() const override; + bool jumpToNextImage() override; + bool jumpToImage(int imageNumber) override; + + int nextImageDelay() const override; + + int loopCount() const override; + +private: + bool ensureParsed() const; + bool ensureALLCounted() const; + bool ensureDecoder(); + bool countALLFrames(); + bool decode_one_frame(); + bool rewind(); + + enum ParseJpegXLState { + ParseJpegXLError = -1, + ParseJpegXLNotParsed = 0, + ParseJpegXLSuccess = 1, + ParseJpegXLBasicInfoParsed = 2, + }; + + ParseJpegXLState m_parseState; + int m_quality; + int m_currentimage_index; + int m_previousimage_index; + + QByteArray m_rawData; + + JxlDecoder *m_decoder; + void *m_runner; + JxlBasicInfo m_basicinfo; + + QVector m_framedelays; + int m_next_image_delay; + + QImage m_current_image; + QColorSpace m_colorspace; + + QImage::Format m_input_image_format; + QImage::Format m_target_image_format; + + JxlPixelFormat m_input_pixel_format; + size_t m_buffer_size; +}; + +class QJpegXLPlugin : public QImageIOPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "jxl.json") + +public: + Capabilities capabilities(QIODevice *device, const QByteArray &format) const override; + QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override; +}; + +#endif // KIMG_JXL_P_H