mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2025-11-14 08:02:42 -05:00
jxl: refactor image saving, native CMYK support
This commit is contained in:
Binary file not shown.
@ -443,7 +443,7 @@ bool QJpegXLHandler::countALLFrames()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!alpha_found) {
|
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;
|
m_parseState = ParseJpegXLError;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -453,7 +453,7 @@ bool QJpegXLHandler::countALLFrames()
|
|||||||
qWarning("JXL has BLACK channel but colorspace is not CMYK!");
|
qWarning("JXL has BLACK channel but colorspace is not CMYK!");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
} else if (channel_info.type == JXL_CHANNEL_ALPHA) {
|
} else if ((channel_info.type == JXL_CHANNEL_ALPHA) && !alpha_found) {
|
||||||
alpha_found = true;
|
alpha_found = true;
|
||||||
m_alpha_channel_id = index;
|
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)
|
bool QJpegXLHandler::write(const QImage &image)
|
||||||
{
|
{
|
||||||
if (image.format() == QImage::Format_Invalid) {
|
if (image.format() == QImage::Format_Invalid) {
|
||||||
@ -890,6 +866,254 @@ bool QJpegXLHandler::write(const QImage &image)
|
|||||||
return false;
|
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
|
int save_depth = 8; // 8 / 16 / 32
|
||||||
bool save_fp = false;
|
bool save_fp = false;
|
||||||
bool is_gray = false;
|
bool is_gray = false;
|
||||||
@ -956,96 +1180,16 @@ bool QJpegXLHandler::write(const QImage &image)
|
|||||||
break;
|
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;
|
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
|
if (save_depth > 8 && is_gray) { // 16bit depth gray
|
||||||
pixel_format.data_type = JXL_TYPE_UINT16;
|
pixel_format.data_type = JXL_TYPE_UINT16;
|
||||||
pixel_format.align = 4;
|
|
||||||
output_info.num_color_channels = 1;
|
output_info.num_color_channels = 1;
|
||||||
output_info.bits_per_sample = 16;
|
output_info.bits_per_sample = 16;
|
||||||
tmpformat = QImage::Format_Grayscale16;
|
tmpformat = QImage::Format_Grayscale16;
|
||||||
pixel_format.num_channels = 1;
|
pixel_format.num_channels = 1;
|
||||||
} else if (is_gray) { // 8bit depth gray
|
} else if (is_gray) { // 8bit depth gray
|
||||||
pixel_format.data_type = JXL_TYPE_UINT8;
|
pixel_format.data_type = JXL_TYPE_UINT8;
|
||||||
pixel_format.align = 4;
|
|
||||||
output_info.num_color_channels = 1;
|
output_info.num_color_channels = 1;
|
||||||
output_info.bits_per_sample = 8;
|
output_info.bits_per_sample = 8;
|
||||||
tmpformat = QImage::Format_Grayscale8;
|
tmpformat = QImage::Format_Grayscale8;
|
||||||
@ -1088,7 +1232,6 @@ bool QJpegXLHandler::write(const QImage &image)
|
|||||||
}
|
}
|
||||||
} else { // 8bit depth rgb
|
} else { // 8bit depth rgb
|
||||||
pixel_format.data_type = JXL_TYPE_UINT8;
|
pixel_format.data_type = JXL_TYPE_UINT8;
|
||||||
pixel_format.align = 4;
|
|
||||||
output_info.num_color_channels = 3;
|
output_info.num_color_channels = 3;
|
||||||
output_info.bits_per_sample = 8;
|
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)
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||||
// TODO: add native CMYK support (libjxl supports CMYK images)
|
|
||||||
QImage tmpimage;
|
QImage tmpimage;
|
||||||
auto cs = image.colorSpace();
|
if (image.colorSpace().isValid()) {
|
||||||
if (cs.isValid() && cs.colorModel() == QColorSpace::ColorModel::Cmyk && image.format() == QImage::Format_CMYK8888) {
|
if (is_gray && image.colorSpace().colorModel() != QColorSpace::ColorModel::Gray) {
|
||||||
tmpimage = image.convertedToColorSpace(QColorSpace(QColorSpace::SRgb), tmpformat);
|
// convert to Gray profile
|
||||||
iccprofile.clear();
|
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 {
|
} 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);
|
tmpimage = image.convertToFormat(tmpformat);
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
QImage tmpimage = image.convertToFormat(tmpformat);
|
QImage tmpimage = image.convertToFormat(tmpformat);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
const size_t xsize = tmpimage.width();
|
output_info.xsize = tmpimage.width();
|
||||||
const size_t ysize = tmpimage.height();
|
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");
|
qWarning("Unable to allocate memory for output image");
|
||||||
if (runner) {
|
if (runner) {
|
||||||
JxlThreadParallelRunnerDestroy(runner);
|
JxlThreadParallelRunnerDestroy(runner);
|
||||||
@ -1131,8 +1317,103 @@ bool QJpegXLHandler::write(const QImage &image)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
output_info.xsize = tmpimage.width();
|
JxlColorEncoding color_profile;
|
||||||
output_info.ysize = tmpimage.height();
|
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);
|
status = JxlEncoderSetBasicInfo(encoder, &output_info);
|
||||||
if (status != JXL_ENC_SUCCESS) {
|
if (status != JXL_ENC_SUCCESS) {
|
||||||
@ -1144,7 +1425,28 @@ bool QJpegXLHandler::write(const QImage &image)
|
|||||||
return false;
|
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()) {
|
if (!exif_data.isEmpty()) {
|
||||||
exif_data = QByteArray::fromHex("00000000") + exif_data;
|
exif_data = QByteArray::fromHex("00000000") + exif_data;
|
||||||
const char *box_type = "Exif";
|
const char *box_type = "Exif";
|
||||||
@ -1158,7 +1460,7 @@ bool QJpegXLHandler::write(const QImage &image)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
auto xmp_data = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8();
|
|
||||||
if (!xmp_data.isEmpty()) {
|
if (!xmp_data.isEmpty()) {
|
||||||
const char *box_type = "xml ";
|
const char *box_type = "xml ";
|
||||||
status = JxlEncoderAddBox(encoder, box_type, reinterpret_cast<const uint8_t *>(xmp_data.constData()), xmp_data.size(), JXL_FALSE);
|
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
|
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);
|
JxlEncoderFrameSettings *encoder_options = JxlEncoderFrameSettingsCreate(encoder, nullptr);
|
||||||
|
|
||||||
JxlEncoderSetFrameDistance(encoder_options, (100.0f - m_quality) / 10.0f);
|
if (m_quality == 100) { // lossless
|
||||||
|
JxlEncoderSetFrameDistance(encoder_options, 0.0f);
|
||||||
JxlEncoderSetFrameLossless(encoder_options, (m_quality == 100) ? JXL_TRUE : JXL_FALSE);
|
JxlEncoderSetFrameLossless(encoder_options, JXL_TRUE);
|
||||||
|
} else {
|
||||||
size_t buffer_size = size_t(tmpimage.bytesPerLine()) * tmpimage.height();
|
JxlEncoderSetFrameDistance(encoder_options, JxlEncoderDistanceFromQuality(m_quality));
|
||||||
if (!image.hasAlphaChannel() && save_depth > 8 && !is_gray) { // pack pixel on tmpimage
|
JxlEncoderSetFrameLossless(encoder_options, JXL_FALSE);
|
||||||
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);
|
|
||||||
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// pack pixel data
|
float *dest_pixels32 = packed_pixels32;
|
||||||
if (save_depth > 16 && save_fp)
|
for (int y = 0; y < tmpimage.height(); y++) {
|
||||||
packRGBPixels<float>(tmpimage);
|
const float *src_pixels32 = reinterpret_cast<const float *>(tmpimage.constScanLine(y));
|
||||||
else if (save_fp)
|
for (int x = 0; x < tmpimage.width(); x++) {
|
||||||
packRGBPixels<qfloat16>(tmpimage);
|
*dest_pixels32 = *src_pixels32; // R
|
||||||
else
|
dest_pixels32++;
|
||||||
packRGBPixels<quint16>(tmpimage);
|
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) {
|
if (status == JXL_ENC_ERROR) {
|
||||||
qWarning("JxlEncoderAddImageFrame failed!");
|
qWarning("JxlEncoderAddImageFrame failed!");
|
||||||
@ -1237,8 +1573,9 @@ bool QJpegXLHandler::write(const QImage &image)
|
|||||||
JxlEncoderDestroy(encoder);
|
JxlEncoderDestroy(encoder);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
JxlEncoderCloseInput(encoder);
|
JxlEncoderCloseFrames(encoder);
|
||||||
|
|
||||||
std::vector<uint8_t> compressed;
|
std::vector<uint8_t> compressed;
|
||||||
compressed.resize(4096);
|
compressed.resize(4096);
|
||||||
|
|||||||
Reference in New Issue
Block a user