jxl: refactor image saving, native CMYK support

This commit is contained in:
Daniel Novomeský 2025-02-04 11:17:46 +01:00
parent 18a729f7a1
commit 397957a976
2 changed files with 646 additions and 309 deletions

View File

@ -443,7 +443,7 @@ bool QJpegXLHandler::countALLFrames()
}
if (!alpha_found) {
qWarning("JXL BasicInfo indate Alpha channel but it was not found");
qWarning("JXL BasicInfo indicate Alpha channel but it was not found");
m_parseState = ParseJpegXLError;
return false;
}
@ -453,7 +453,7 @@ bool QJpegXLHandler::countALLFrames()
qWarning("JXL has BLACK channel but colorspace is not CMYK!");
}
break;
} else if (channel_info.type == JXL_CHANNEL_ALPHA) {
} else if ((channel_info.type == JXL_CHANNEL_ALPHA) && !alpha_found) {
alpha_found = true;
m_alpha_channel_id = index;
}
@ -843,30 +843,6 @@ 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) {
@ -890,6 +866,254 @@ bool QJpegXLHandler::write(const QImage &image)
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;
}
}
if (m_quality > 100) {
m_quality = 100;
} else if (m_quality < 0) {
m_quality = 90;
}
JxlEncoderUseContainer(encoder, JXL_TRUE);
JxlEncoderUseBoxes(encoder);
JxlBasicInfo output_info;
JxlEncoderInitBasicInfo(&output_info);
output_info.have_container = JXL_TRUE;
output_info.animation.tps_numerator = 10;
output_info.animation.tps_denominator = 1;
output_info.orientation = JXL_ORIENT_IDENTITY;
if (m_transformations == QImageIOHandler::TransformationMirror) {
output_info.orientation = JXL_ORIENT_FLIP_HORIZONTAL;
} else if (m_transformations == QImageIOHandler::TransformationRotate180) {
output_info.orientation = JXL_ORIENT_ROTATE_180;
} else if (m_transformations == QImageIOHandler::TransformationFlip) {
output_info.orientation = JXL_ORIENT_FLIP_VERTICAL;
} else if (m_transformations == QImageIOHandler::TransformationFlipAndRotate90) {
output_info.orientation = JXL_ORIENT_TRANSPOSE;
} else if (m_transformations == QImageIOHandler::TransformationRotate90) {
output_info.orientation = JXL_ORIENT_ROTATE_90_CW;
} else if (m_transformations == QImageIOHandler::TransformationMirrorAndRotate90) {
output_info.orientation = JXL_ORIENT_ANTI_TRANSPOSE;
} else if (m_transformations == QImageIOHandler::TransformationRotate270) {
output_info.orientation = JXL_ORIENT_ROTATE_90_CCW;
}
bool save_cmyk = false;
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
if (image.format() == QImage::Format_CMYK8888 && image.colorSpace().isValid() && image.colorSpace().colorModel() == QColorSpace::ColorModel::Cmyk) {
save_cmyk = true;
}
#endif
JxlEncoderStatus status;
JxlPixelFormat pixel_format;
pixel_format.endianness = JXL_NATIVE_ENDIAN;
pixel_format.align = 0;
auto exif_data = MicroExif::fromImage(image).toByteArray();
auto xmp_data = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8();
if (save_cmyk) { // CMYK is always lossless
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
output_info.uses_original_profile = JXL_TRUE;
output_info.xsize = image.width();
output_info.ysize = image.height();
output_info.num_color_channels = 3;
output_info.bits_per_sample = 8;
output_info.alpha_bits = 0;
output_info.num_extra_channels = 1;
pixel_format.num_channels = 3;
pixel_format.data_type = JXL_TYPE_UINT8;
JxlPixelFormat format_extra;
format_extra.num_channels = 1;
format_extra.data_type = JXL_TYPE_UINT8;
format_extra.endianness = JXL_NATIVE_ENDIAN;
format_extra.align = 0;
JxlExtraChannelInfo extra_black_channel;
JxlEncoderInitExtraChannelInfo(JXL_CHANNEL_BLACK, &extra_black_channel);
extra_black_channel.bits_per_sample = output_info.bits_per_sample;
extra_black_channel.exponent_bits_per_sample = output_info.exponent_bits_per_sample;
const QByteArray cmyk_profile = image.colorSpace().iccProfile();
if (cmyk_profile.isEmpty()) {
qWarning("ERROR saving CMYK JXL: empty ICC profile");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
status = JxlEncoderSetBasicInfo(encoder, &output_info);
if (status != JXL_ENC_SUCCESS) {
qWarning("JxlEncoderSetBasicInfo for CMYK image failed!");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
status = JxlEncoderSetExtraChannelInfo(encoder, 0, &extra_black_channel);
if (status != JXL_ENC_SUCCESS) {
qWarning("JxlEncoderSetExtraChannelInfo for CMYK image failed!");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
status = JxlEncoderSetICCProfile(encoder, reinterpret_cast<const uint8_t *>(cmyk_profile.constData()), cmyk_profile.size());
if (status != JXL_ENC_SUCCESS) {
qWarning("JxlEncoderSetICCProfile for CMYK image failed!");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
if (!exif_data.isEmpty()) {
exif_data = QByteArray::fromHex("00000000") + exif_data;
const char *box_type = "Exif";
status = JxlEncoderAddBox(encoder, box_type, reinterpret_cast<const uint8_t *>(exif_data.constData()), exif_data.size(), JXL_FALSE);
if (status != JXL_ENC_SUCCESS) {
qWarning("JxlEncoderAddBox failed!");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
}
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
const size_t extra_buffer_size = size_t(image.width()) * size_t(image.height());
const size_t cmy_buffer_size = extra_buffer_size * 3;
uchar *pixels_cmy = nullptr;
uchar *pixels_black = nullptr;
pixels_cmy = reinterpret_cast<uchar *>(malloc(cmy_buffer_size));
if (!pixels_cmy) {
qWarning("Memory cannot be allocated for CMY buffer");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
pixels_black = reinterpret_cast<uchar *>(malloc(extra_buffer_size));
if (!pixels_black) {
qWarning("Memory cannot be allocated for BLACK buffer");
free(pixels_cmy);
pixels_cmy = nullptr;
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
uchar *dest_CMY = pixels_cmy;
uchar *dest_K = pixels_black;
for (int y = 0; y < image.height(); y++) {
const uchar *src_CMYK = image.constScanLine(y);
for (int x = 0; x < image.width(); x++) {
*dest_CMY = 255 - *src_CMYK; // C
dest_CMY++;
src_CMYK++;
*dest_CMY = 255 - *src_CMYK; // M
dest_CMY++;
src_CMYK++;
*dest_CMY = 255 - *src_CMYK; // Y
dest_CMY++;
src_CMYK++;
*dest_K = 255 - *src_CMYK; // K
dest_K++;
src_CMYK++;
}
}
JxlEncoderFrameSettings *frame_settings_lossless = JxlEncoderFrameSettingsCreate(encoder, nullptr);
JxlEncoderSetFrameDistance(frame_settings_lossless, 0);
JxlEncoderSetFrameLossless(frame_settings_lossless, JXL_TRUE);
status = JxlEncoderAddImageFrame(frame_settings_lossless, &pixel_format, pixels_cmy, cmy_buffer_size);
if (status == JXL_ENC_ERROR) {
qWarning("JxlEncoderAddImageFrame failed!");
free(pixels_black);
pixels_black = nullptr;
free(pixels_cmy);
pixels_cmy = nullptr;
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
status = JxlEncoderSetExtraChannelBuffer(frame_settings_lossless, &format_extra, pixels_black, extra_buffer_size, 0);
free(pixels_black);
pixels_black = nullptr;
free(pixels_cmy);
pixels_cmy = nullptr;
if (status == JXL_ENC_ERROR) {
qWarning("JxlEncoderSetExtraChannelBuffer failed!");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
#else
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
#endif
} else { // RGB or GRAY saving
int save_depth = 8; // 8 / 16 / 32
bool save_fp = false;
bool is_gray = false;
@ -956,96 +1180,16 @@ bool QJpegXLHandler::write(const QImage &image)
break;
}
JxlEncoder *encoder = JxlEncoderCreate(nullptr);
if (!encoder) {
qWarning("Failed to create Jxl encoder");
return false;
}
if (m_quality > 100) {
m_quality = 100;
} else if (m_quality < 0) {
m_quality = 90;
}
JxlEncoderUseContainer(encoder, JXL_TRUE);
JxlEncoderUseBoxes(encoder);
JxlBasicInfo output_info;
JxlEncoderInitBasicInfo(&output_info);
output_info.have_container = JXL_TRUE;
QByteArray 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 || is_gray) {
output_info.uses_original_profile = JXL_TRUE;
}
}
// 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)) {
JxlEncoderSetCodestreamLevel(encoder, 10);
}
// clang-format on
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;
}
}
JxlPixelFormat pixel_format;
QImage::Format tmpformat;
JxlEncoderStatus status;
pixel_format.endianness = JXL_NATIVE_ENDIAN;
pixel_format.align = 0;
output_info.animation.tps_numerator = 10;
output_info.animation.tps_denominator = 1;
output_info.orientation = JXL_ORIENT_IDENTITY;
if (m_transformations == QImageIOHandler::TransformationMirror) {
output_info.orientation = JXL_ORIENT_FLIP_HORIZONTAL;
} else if (m_transformations == QImageIOHandler::TransformationRotate180) {
output_info.orientation = JXL_ORIENT_ROTATE_180;
} else if (m_transformations == QImageIOHandler::TransformationFlip) {
output_info.orientation = JXL_ORIENT_FLIP_VERTICAL;
} else if (m_transformations == QImageIOHandler::TransformationFlipAndRotate90) {
output_info.orientation = JXL_ORIENT_TRANSPOSE;
} else if (m_transformations == QImageIOHandler::TransformationRotate90) {
output_info.orientation = JXL_ORIENT_ROTATE_90_CW;
} else if (m_transformations == QImageIOHandler::TransformationMirrorAndRotate90) {
output_info.orientation = JXL_ORIENT_ANTI_TRANSPOSE;
} else if (m_transformations == QImageIOHandler::TransformationRotate270) {
output_info.orientation = JXL_ORIENT_ROTATE_90_CCW;
}
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;
@ -1088,7 +1232,6 @@ bool QJpegXLHandler::write(const QImage &image)
}
} 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;
@ -1106,23 +1249,66 @@ bool QJpegXLHandler::write(const QImage &image)
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
// TODO: add native CMYK support (libjxl supports CMYK images)
QImage tmpimage;
auto cs = image.colorSpace();
if (cs.isValid() && cs.colorModel() == QColorSpace::ColorModel::Cmyk && image.format() == QImage::Format_CMYK8888) {
tmpimage = image.convertedToColorSpace(QColorSpace(QColorSpace::SRgb), tmpformat);
iccprofile.clear();
if (image.colorSpace().isValid()) {
if (is_gray && image.colorSpace().colorModel() != QColorSpace::ColorModel::Gray) {
// convert to Gray profile
QPointF gray_whitePoint = image.colorSpace().whitePoint();
if (gray_whitePoint.isNull()) {
gray_whitePoint = QPointF(0.3127f, 0.329f);
}
QColorSpace::TransferFunction gray_trc = image.colorSpace().transferFunction();
float gamma_gray = image.colorSpace().gamma();
if (gray_trc == QColorSpace::TransferFunction::Custom) {
gray_trc = QColorSpace::TransferFunction::SRgb;
}
const QColorSpace gray_profile(gray_whitePoint, gray_trc, gamma_gray);
if (gray_profile.isValid()) {
tmpimage = image.convertedToColorSpace(gray_profile, tmpformat);
} else {
qWarning("JXL plugin created invalid grayscale QColorSpace!");
tmpimage = image.convertToFormat(tmpformat);
}
} else if (!is_gray && image.colorSpace().colorModel() != QColorSpace::ColorModel::Rgb) {
// convert to RGB profile
QPointF whitePoint = image.colorSpace().whitePoint();
if (whitePoint.isNull()) {
whitePoint = QPointF(0.3127f, 0.329f);
}
const QPointF redP(0.64f, 0.33f);
const QPointF greenP(0.3f, 0.6f);
const QPointF blueP(0.15f, 0.06f);
QColorSpace::TransferFunction trc_rgb = image.colorSpace().transferFunction();
float gamma_rgb = image.colorSpace().gamma();
if (trc_rgb == QColorSpace::TransferFunction::Custom) {
trc_rgb = QColorSpace::TransferFunction::SRgb;
}
const QColorSpace rgb_profile(whitePoint, redP, greenP, blueP, trc_rgb, gamma_rgb);
if (rgb_profile.isValid()) {
tmpimage = image.convertedToColorSpace(rgb_profile, tmpformat);
} else {
qWarning("JXL plugin created invalid RGB QColorSpace!");
tmpimage = image.convertToFormat(tmpformat);
}
} else { // ColorSpace matches the format
tmpimage = image.convertToFormat(tmpformat);
}
} else { // no ColorSpace or invalid
tmpimage = image.convertToFormat(tmpformat);
}
#else
QImage tmpimage = image.convertToFormat(tmpformat);
#endif
const size_t xsize = tmpimage.width();
const size_t ysize = tmpimage.height();
output_info.xsize = tmpimage.width();
output_info.ysize = tmpimage.height();
if (xsize == 0 || ysize == 0 || tmpimage.isNull()) {
if (output_info.xsize == 0 || output_info.ysize == 0 || tmpimage.isNull()) {
qWarning("Unable to allocate memory for output image");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
@ -1131,8 +1317,103 @@ bool QJpegXLHandler::write(const QImage &image)
return false;
}
output_info.xsize = tmpimage.width();
output_info.ysize = tmpimage.height();
JxlColorEncoding color_profile;
JxlColorEncodingSetToSRGB(&color_profile, is_gray ? JXL_TRUE : JXL_FALSE);
QByteArray iccprofile;
if (m_quality == 100) { // try to use ICC for lossless
output_info.uses_original_profile = JXL_TRUE;
iccprofile = tmpimage.colorSpace().iccProfile();
} else { // try to detect encoded profile (smaller than ICC)
output_info.uses_original_profile = JXL_FALSE;
if (tmpimage.colorSpace().isValid()) {
const QPointF whiteP = image.colorSpace().whitePoint();
switch (tmpimage.colorSpace().primaries()) {
case QColorSpace::Primaries::SRgb:
color_profile.white_point = JXL_WHITE_POINT_D65;
color_profile.primaries = JXL_PRIMARIES_SRGB;
break;
case QColorSpace::Primaries::AdobeRgb:
color_profile.white_point = JXL_WHITE_POINT_D65;
color_profile.primaries = JXL_PRIMARIES_CUSTOM;
color_profile.primaries_red_xy[0] = 0.640;
color_profile.primaries_red_xy[1] = 0.330;
color_profile.primaries_green_xy[0] = 0.210;
color_profile.primaries_green_xy[1] = 0.710;
color_profile.primaries_blue_xy[0] = 0.150;
color_profile.primaries_blue_xy[1] = 0.060;
break;
case QColorSpace::Primaries::DciP3D65:
color_profile.white_point = JXL_WHITE_POINT_D65;
color_profile.primaries = JXL_PRIMARIES_P3;
color_profile.primaries_red_xy[0] = 0.680;
color_profile.primaries_red_xy[1] = 0.320;
color_profile.primaries_green_xy[0] = 0.265;
color_profile.primaries_green_xy[1] = 0.690;
color_profile.primaries_blue_xy[0] = 0.150;
color_profile.primaries_blue_xy[1] = 0.060;
break;
case QColorSpace::Primaries::ProPhotoRgb:
color_profile.white_point = JXL_WHITE_POINT_CUSTOM;
color_profile.white_point_xy[0] = whiteP.x();
color_profile.white_point_xy[1] = whiteP.y();
color_profile.primaries = JXL_PRIMARIES_CUSTOM;
color_profile.primaries_red_xy[0] = 0.7347;
color_profile.primaries_red_xy[1] = 0.2653;
color_profile.primaries_green_xy[0] = 0.1596;
color_profile.primaries_green_xy[1] = 0.8404;
color_profile.primaries_blue_xy[0] = 0.0366;
color_profile.primaries_blue_xy[1] = 0.0001;
break;
case QColorSpace::Primaries::Bt2020:
color_profile.white_point = JXL_WHITE_POINT_D65;
color_profile.primaries = JXL_PRIMARIES_2100;
color_profile.primaries_red_xy[0] = 0.708;
color_profile.primaries_red_xy[1] = 0.292;
color_profile.primaries_green_xy[0] = 0.170;
color_profile.primaries_green_xy[1] = 0.797;
color_profile.primaries_blue_xy[0] = 0.131;
color_profile.primaries_blue_xy[1] = 0.046;
break;
default:
if (is_gray && !whiteP.isNull()) {
color_profile.white_point = JXL_WHITE_POINT_CUSTOM;
color_profile.white_point_xy[0] = whiteP.x();
color_profile.white_point_xy[1] = whiteP.y();
} else {
iccprofile = tmpimage.colorSpace().iccProfile();
}
break;
}
if (iccprofile.isEmpty()) {
const double gamma_profile = tmpimage.colorSpace().gamma();
switch (tmpimage.colorSpace().transferFunction()) {
case QColorSpace::TransferFunction::Linear:
color_profile.transfer_function = JXL_TRANSFER_FUNCTION_LINEAR;
break;
case QColorSpace::TransferFunction::Gamma:
if (gamma_profile > 0) {
color_profile.transfer_function = JXL_TRANSFER_FUNCTION_GAMMA;
color_profile.gamma = 1.0 / gamma_profile;
} else {
iccprofile = tmpimage.colorSpace().iccProfile();
}
break;
case QColorSpace::TransferFunction::SRgb:
color_profile.transfer_function = JXL_TRANSFER_FUNCTION_SRGB;
break;
default:
iccprofile = tmpimage.colorSpace().iccProfile();
break;
}
}
}
}
status = JxlEncoderSetBasicInfo(encoder, &output_info);
if (status != JXL_ENC_SUCCESS) {
@ -1144,7 +1425,28 @@ bool QJpegXLHandler::write(const QImage &image)
return false;
}
auto exif_data = MicroExif::fromImage(image).toByteArray();
if (iccprofile.size() > 0) {
status = JxlEncoderSetICCProfile(encoder, reinterpret_cast<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 (!exif_data.isEmpty()) {
exif_data = QByteArray::fromHex("00000000") + exif_data;
const char *box_type = "Exif";
@ -1158,7 +1460,7 @@ bool QJpegXLHandler::write(const QImage &image)
return false;
}
}
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);
@ -1173,61 +1475,95 @@ bool QJpegXLHandler::write(const QImage &image)
}
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!");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
} else {
JxlColorEncoding color_profile;
JxlColorEncodingSetToSRGB(&color_profile, is_gray ? JXL_TRUE : JXL_FALSE);
status = JxlEncoderSetColorEncoding(encoder, &color_profile);
if (status != JXL_ENC_SUCCESS) {
qWarning("JxlEncoderSetColorEncoding failed!");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
}
JxlEncoderDestroy(encoder);
return false;
}
}
JxlEncoderFrameSettings *encoder_options = JxlEncoderFrameSettingsCreate(encoder, nullptr);
JxlEncoderSetFrameDistance(encoder_options, (100.0f - m_quality) / 10.0f);
JxlEncoderSetFrameLossless(encoder_options, (m_quality == 100) ? JXL_TRUE : JXL_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);
// detaching image
tmpimage.detach();
if (tmpimage.isNull()) {
qWarning("Memory allocation error");
if (runner) {
JxlThreadParallelRunnerDestroy(runner);
if (m_quality == 100) { // lossless
JxlEncoderSetFrameDistance(encoder_options, 0.0f);
JxlEncoderSetFrameLossless(encoder_options, JXL_TRUE);
} else {
JxlEncoderSetFrameDistance(encoder_options, JxlEncoderDistanceFromQuality(m_quality));
JxlEncoderSetFrameLossless(encoder_options, JXL_FALSE);
}
JxlEncoderDestroy(encoder);
size_t buffer_size;
if (tmpimage.format() == QImage::Format_RGBX32FPx4) { // pack 32-bit depth RGBX -> RGB
buffer_size = 12 * size_t(tmpimage.width()) * size_t(tmpimage.height());
float *packed_pixels32 = reinterpret_cast<float *>(malloc(buffer_size));
if (!packed_pixels32) {
qWarning("ERROR: JXL plug-in failed to allocate memory");
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);
float *dest_pixels32 = packed_pixels32;
for (int y = 0; y < tmpimage.height(); y++) {
const float *src_pixels32 = reinterpret_cast<const float *>(tmpimage.constScanLine(y));
for (int x = 0; x < tmpimage.width(); x++) {
*dest_pixels32 = *src_pixels32; // R
dest_pixels32++;
src_pixels32++;
*dest_pixels32 = *src_pixels32; // G
dest_pixels32++;
src_pixels32++;
*dest_pixels32 = *src_pixels32; // B
dest_pixels32++;
src_pixels32 += 2; // skip X
}
}
status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, packed_pixels32, buffer_size);
free(packed_pixels32);
} else if (tmpimage.format() == QImage::Format_RGBX16FPx4 || tmpimage.format() == QImage::Format_RGBX64) {
// pack 16-bit depth RGBX -> RGB
buffer_size = 6 * size_t(tmpimage.width()) * size_t(tmpimage.height());
quint16 *packed_pixels16 = reinterpret_cast<quint16 *>(malloc(buffer_size));
if (!packed_pixels16) {
qWarning("ERROR: JXL plug-in failed to allocate memory");
return false;
}
quint16 *dest_pixels16 = packed_pixels16;
for (int y = 0; y < tmpimage.height(); y++) {
const quint16 *src_pixels16 = reinterpret_cast<const quint16 *>(tmpimage.constScanLine(y));
for (int x = 0; x < tmpimage.width(); x++) {
*dest_pixels16 = *src_pixels16; // R
dest_pixels16++;
src_pixels16++;
*dest_pixels16 = *src_pixels16; // G
dest_pixels16++;
src_pixels16++;
*dest_pixels16 = *src_pixels16; // B
dest_pixels16++;
src_pixels16 += 2; // skip X
}
}
status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, packed_pixels16, buffer_size);
free(packed_pixels16);
} else { // use QImage's data directly
pixel_format.align = tmpimage.bytesPerLine();
buffer_size = size_t(tmpimage.height() - 1) * size_t(tmpimage.bytesPerLine());
switch (pixel_format.data_type) {
case JXL_TYPE_FLOAT:
buffer_size += 4 * size_t(pixel_format.num_channels) * size_t(tmpimage.width());
break;
case JXL_TYPE_UINT8:
buffer_size += size_t(pixel_format.num_channels) * size_t(tmpimage.width());
break;
case JXL_TYPE_UINT16:
case JXL_TYPE_FLOAT16:
buffer_size += 2 * size_t(pixel_format.num_channels) * size_t(tmpimage.width());
break;
default:
qWarning("ERROR: unsupported data type");
return false;
break;
}
status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, tmpimage.constBits(), buffer_size);
}
status = JxlEncoderAddImageFrame(encoder_options, &pixel_format, static_cast<const void *>(tmpimage.constBits()), buffer_size);
if (status == JXL_ENC_ERROR) {
qWarning("JxlEncoderAddImageFrame failed!");
@ -1237,8 +1573,9 @@ bool QJpegXLHandler::write(const QImage &image)
JxlEncoderDestroy(encoder);
return false;
}
}
JxlEncoderCloseInput(encoder);
JxlEncoderCloseFrames(encoder);
std::vector<uint8_t> compressed;
compressed.resize(4096);