mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2025-05-28 00:30:23 -04:00
JXL improvements
Highlights of the patch: - Supersede MR !249 - Added FP16 and FP32 images support thus preserving HDR values (read / write, required libjxl 0.9+). - Added Gray8 and Gray16 support (read / write). - Indexed images are saved as Gray8 when palette is gray scale. - Binary images are saved as Gray8 (does JXL natively support binary images?). - Simplified writing process by partially removing the use of additional buffers. - Added XMP metadata support by decoding/encoding Boxes. - Changed maximum image size in pixels in accordance with JXL feature level 5 (still limited to 256 megapixels). Compatibility: - Older versions of this plugin load FP images correctly as UINT16 (obviously losing HDR info). - HDR images saved with this patch are also loaded correctly by Gimp and Photoshop. - Grayscale images saved with this patch are also loaded correctly by Gimp and Photoshop. Compilation modifiers for cmake file: - `JXL_HDR_PRESERVATION_DISABLED`: disable the FP support (behaves like previous versions). - `JXL_DECODE_BOXES_DISABLED`: disable metadata reading (behaves like previous versions).
This commit is contained in:
parent
3f4690d1e9
commit
b5d5abe0ea
10
README.md
10
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
|
||||
|
BIN
autotests/read/jxl/testcard_gray16.jxl
Normal file
BIN
autotests/read/jxl/testcard_gray16.jxl
Normal file
Binary file not shown.
BIN
autotests/read/jxl/testcard_gray16.png
Normal file
BIN
autotests/read/jxl/testcard_gray16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
autotests/read/jxl/testcard_gray8.jxl
Normal file
BIN
autotests/read/jxl/testcard_gray8.jxl
Normal file
Binary file not shown.
BIN
autotests/read/jxl/testcard_gray8.png
Normal file
BIN
autotests/read/jxl/testcard_gray8.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
BIN
autotests/read/jxl/testcard_rgb_fp16.jxl
Normal file
BIN
autotests/read/jxl/testcard_rgb_fp16.jxl
Normal file
Binary file not shown.
BIN
autotests/read/jxl/testcard_rgb_fp16.png
Normal file
BIN
autotests/read/jxl/testcard_rgb_fp16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
autotests/read/jxl/testcard_rgb_fp32.jxl
Normal file
BIN
autotests/read/jxl/testcard_rgb_fp32.jxl
Normal file
Binary file not shown.
BIN
autotests/read/jxl/testcard_rgb_fp32.png
Normal file
BIN
autotests/read/jxl/testcard_rgb_fp32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
autotests/read/jxl/testcard_rgba_fp16.jxl
Normal file
BIN
autotests/read/jxl/testcard_rgba_fp16.jxl
Normal file
Binary file not shown.
21
autotests/read/jxl/testcard_rgba_fp16.jxl.json
Normal file
21
autotests/read/jxl/testcard_rgba_fp16.jxl.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
BIN
autotests/read/jxl/testcard_rgba_fp16.png
Normal file
BIN
autotests/read/jxl/testcard_rgba_fp16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
autotests/read/jxl/testcard_rgba_fp32.jxl
Normal file
BIN
autotests/read/jxl/testcard_rgba_fp32.jxl
Normal file
Binary file not shown.
21
autotests/read/jxl/testcard_rgba_fp32.jxl.json
Normal file
21
autotests/read/jxl/testcard_rgba_fp32.jxl.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
BIN
autotests/read/jxl/testcard_rgba_fp32.png
Normal file
BIN
autotests/read/jxl/testcard_rgba_fp32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
@ -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,29 +259,52 @@ 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 (m_basicinfo.num_color_channels == 1) {
|
||||
m_input_image_format = m_target_image_format = QImage::Format_Grayscale8;
|
||||
} else {
|
||||
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,
|
||||
#if JPEGXL_NUMERIC_VERSION < JPEGXL_COMPUTE_NUMERIC_VERSION(0, 9, 0)
|
||||
@ -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<class T>
|
||||
void packRGBPixels(QImage &img)
|
||||
{
|
||||
// pack pixel data
|
||||
auto dest_pixels = reinterpret_cast<T *>(img.bits());
|
||||
for (qint32 y = 0; y < img.height(); y++) {
|
||||
auto src_pixels = reinterpret_cast<const T *>(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<const uint8_t *>(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<const uint8_t *>(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))) {
|
||||
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);
|
||||
|
||||
// detaching image
|
||||
tmpimage.detach();
|
||||
if (tmpimage.isNull()) {
|
||||
qWarning("Memory allocation error");
|
||||
if (runner) {
|
||||
JxlThreadParallelRunnerDestroy(runner);
|
||||
}
|
||||
JxlEncoderDestroy(encoder);
|
||||
return false;
|
||||
}
|
||||
|
||||
// pack pixel data
|
||||
if (save_depth > 16 && save_fp)
|
||||
packRGBPixels<float>(tmpimage);
|
||||
else if (save_fp)
|
||||
packRGBPixels<qfloat16>(tmpimage);
|
||||
else
|
||||
packRGBPixels<quint16>(tmpimage);
|
||||
}
|
||||
status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, static_cast<const void *>(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;
|
||||
}
|
||||
|
||||
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, static_cast<const void *>(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<const void *>(tmp_buffer8), buffer_size);
|
||||
delete[] tmp_buffer8;
|
||||
}
|
||||
}
|
||||
|
||||
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<uint8_t *>(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") {
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user