mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2025-05-28 00:30:23 -04:00
Add plugin for JPEG XL (JXL)
This commit is contained in:
parent
fb66044714
commit
41c4b5930c
@ -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)
|
||||
|
@ -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
|
||||
|
BIN
autotests/read/jxl/rgb.jxl
Normal file
BIN
autotests/read/jxl/rgb.jxl
Normal file
Binary file not shown.
BIN
autotests/read/jxl/rgb.png
Normal file
BIN
autotests/read/jxl/rgb.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 528 KiB |
BIN
autotests/read/jxl/rgba.jxl
Normal file
BIN
autotests/read/jxl/rgba.jxl
Normal file
Binary file not shown.
BIN
autotests/read/jxl/rgba.png
Normal file
BIN
autotests/read/jxl/rgba.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 117 KiB |
@ -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/)
|
||||
|
||||
|
876
src/imageformats/jxl.cpp
Normal file
876
src/imageformats/jxl.cpp
Normal file
@ -0,0 +1,876 @@
|
||||
/*
|
||||
JPEG XL (JXL) support for QImage.
|
||||
|
||||
SPDX-FileCopyrightText: 2021 Daniel Novomesky <dnovomesky@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <QThread>
|
||||
#include <QtGlobal>
|
||||
|
||||
#include "jxl_p.h"
|
||||
#include <jxl/encode.h>
|
||||
#include <jxl/thread_parallel_runner.h>
|
||||
|
||||
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<QJpegXLHandler *>(this);
|
||||
|
||||
return that->ensureDecoder();
|
||||
}
|
||||
|
||||
bool QJpegXLHandler::ensureALLCounted() const
|
||||
{
|
||||
if (!ensureParsed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_parseState == ParseJpegXLSuccess) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QJpegXLHandler *that = const_cast<QJpegXLHandler *>(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<const uint16_t *>(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<uint8_t> 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;
|
||||
}
|
7
src/imageformats/jxl.desktop
Normal file
7
src/imageformats/jxl.desktop
Normal file
@ -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
|
4
src/imageformats/jxl.json
Normal file
4
src/imageformats/jxl.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Keys": [ "jxl" ],
|
||||
"MimeTypes": [ "image/jxl" ]
|
||||
}
|
96
src/imageformats/jxl_p.h
Normal file
96
src/imageformats/jxl_p.h
Normal file
@ -0,0 +1,96 @@
|
||||
/*
|
||||
JPEG XL (JXL) support for QImage.
|
||||
|
||||
SPDX-FileCopyrightText: 2021 Daniel Novomesky <dnovomesky@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#ifndef KIMG_JXL_P_H
|
||||
#define KIMG_JXL_P_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QColorSpace>
|
||||
#include <QImage>
|
||||
#include <QImageIOHandler>
|
||||
#include <QImageIOPlugin>
|
||||
#include <QVariant>
|
||||
#include <QVector>
|
||||
|
||||
#include <jxl/decode.h>
|
||||
|
||||
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<int> 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
|
Loading…
Reference in New Issue
Block a user