483 lines
17 KiB
C++
483 lines
17 KiB
C++
// Copyright (c) the JPEG XL Project Authors. All rights reserved.
|
|
//
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
#include "plugins/gimp/file-jxl-load.h"
|
|
|
|
#include <jxl/decode.h>
|
|
#include <jxl/decode_cxx.h>
|
|
|
|
#define _PROFILE_ORIGIN_ JXL_COLOR_PROFILE_TARGET_ORIGINAL
|
|
#define _PROFILE_TARGET_ JXL_COLOR_PROFILE_TARGET_DATA
|
|
#define LOAD_PROC "file-jxl-load"
|
|
|
|
namespace jxl {
|
|
|
|
bool SetJpegXlOutBuffer(
|
|
std::unique_ptr<JxlDecoderStruct, JxlDecoderDestroyStruct> *dec,
|
|
JxlPixelFormat *format, size_t *buffer_size, gpointer *pixels_buffer_1) {
|
|
if (JXL_DEC_SUCCESS !=
|
|
JxlDecoderImageOutBufferSize(dec->get(), format, buffer_size)) {
|
|
g_printerr(LOAD_PROC " Error: JxlDecoderImageOutBufferSize failed\n");
|
|
return false;
|
|
}
|
|
*pixels_buffer_1 = g_malloc(*buffer_size);
|
|
if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(dec->get(), format,
|
|
*pixels_buffer_1,
|
|
*buffer_size)) {
|
|
g_printerr(LOAD_PROC " Error: JxlDecoderSetImageOutBuffer failed\n");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool LoadJpegXlImage(const gchar *const filename, gint32 *const image_id) {
|
|
bool stop_processing = false;
|
|
JxlDecoderStatus status = JXL_DEC_NEED_MORE_INPUT;
|
|
std::vector<uint8_t> icc_profile;
|
|
GimpColorProfile *profile_icc = nullptr;
|
|
GimpColorProfile *profile_int = nullptr;
|
|
bool is_linear = false;
|
|
uint32_t xsize = 0;
|
|
uint32_t ysize = 0;
|
|
int32_t crop_x0 = 0;
|
|
int32_t crop_y0 = 0;
|
|
size_t layer_idx = 0;
|
|
uint32_t frame_duration = 0;
|
|
double tps_denom = 1.f;
|
|
double tps_numerator = 1.f;
|
|
|
|
gint32 layer;
|
|
|
|
gpointer pixels_buffer_1 = nullptr;
|
|
gpointer pixels_buffer_2 = nullptr;
|
|
size_t buffer_size = 0;
|
|
|
|
GimpImageBaseType image_type = GIMP_RGB;
|
|
GimpImageType layer_type = GIMP_RGB_IMAGE;
|
|
GimpPrecision precision = GIMP_PRECISION_U16_GAMMA;
|
|
JxlBasicInfo info = {};
|
|
JxlPixelFormat format = {};
|
|
JxlAnimationHeader animation = {};
|
|
JxlBlendMode blend_mode = JXL_BLEND_BLEND;
|
|
std::vector<char> frame_name;
|
|
|
|
format.num_channels = 4;
|
|
format.data_type = JXL_TYPE_FLOAT;
|
|
format.endianness = JXL_NATIVE_ENDIAN;
|
|
format.align = 0;
|
|
|
|
bool is_gray = false;
|
|
|
|
JpegXlGimpProgress gimp_load_progress(
|
|
("Opening JPEG XL file:" + std::string(filename)).c_str());
|
|
gimp_load_progress.update();
|
|
|
|
// read file
|
|
std::ifstream instream(filename, std::ios::in | std::ios::binary);
|
|
std::vector<uint8_t> compressed((std::istreambuf_iterator<char>(instream)),
|
|
std::istreambuf_iterator<char>());
|
|
instream.close();
|
|
|
|
gimp_load_progress.update();
|
|
|
|
// multi-threaded parallel runner.
|
|
auto runner = JxlResizableParallelRunnerMake(nullptr);
|
|
|
|
auto dec = JxlDecoderMake(nullptr);
|
|
if (JXL_DEC_SUCCESS !=
|
|
JxlDecoderSubscribeEvents(
|
|
dec.get(), JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING |
|
|
JXL_DEC_FULL_IMAGE | JXL_DEC_FRAME_PROGRESSION |
|
|
JXL_DEC_FRAME)) {
|
|
g_printerr(LOAD_PROC " Error: JxlDecoderSubscribeEvents failed\n");
|
|
return false;
|
|
}
|
|
|
|
if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(),
|
|
JxlResizableParallelRunner,
|
|
runner.get())) {
|
|
g_printerr(LOAD_PROC " Error: JxlDecoderSetParallelRunner failed\n");
|
|
return false;
|
|
}
|
|
// TODO(user): make this work with coalescing set to false, while handling
|
|
// frames with duration 0 and references to earlier frames correctly.
|
|
if (JXL_DEC_SUCCESS != JxlDecoderSetCoalescing(dec.get(), JXL_TRUE)) {
|
|
g_printerr(LOAD_PROC " Error: JxlDecoderSetCoalescing failed\n");
|
|
return false;
|
|
}
|
|
|
|
// grand decode loop...
|
|
JxlDecoderSetInput(dec.get(), compressed.data(), compressed.size());
|
|
|
|
if (JXL_DEC_SUCCESS != JxlDecoderSetProgressiveDetail(
|
|
dec.get(), JxlProgressiveDetail::kPasses)) {
|
|
g_printerr(LOAD_PROC " Error: JxlDecoderSetProgressiveDetail failed\n");
|
|
return false;
|
|
}
|
|
|
|
while (true) {
|
|
gimp_load_progress.update();
|
|
|
|
if (!stop_processing) status = JxlDecoderProcessInput(dec.get());
|
|
|
|
if (status == JXL_DEC_BASIC_INFO) {
|
|
if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec.get(), &info)) {
|
|
g_printerr(LOAD_PROC " Error: JxlDecoderGetBasicInfo failed\n");
|
|
return false;
|
|
}
|
|
|
|
xsize = info.xsize;
|
|
ysize = info.ysize;
|
|
if (info.have_animation) {
|
|
animation = info.animation;
|
|
tps_denom = animation.tps_denominator;
|
|
tps_numerator = animation.tps_numerator;
|
|
}
|
|
|
|
JxlResizableParallelRunnerSetThreads(
|
|
runner.get(), JxlResizableParallelRunnerSuggestThreads(xsize, ysize));
|
|
} else if (status == JXL_DEC_COLOR_ENCODING) {
|
|
// check for ICC profile
|
|
size_t icc_size = 0;
|
|
JxlColorEncoding color_encoding;
|
|
if (JXL_DEC_SUCCESS !=
|
|
JxlDecoderGetColorAsEncodedProfile(dec.get(), _PROFILE_ORIGIN_,
|
|
&color_encoding)) {
|
|
// Attempt to load ICC profile when no internal color encoding
|
|
if (JXL_DEC_SUCCESS != JxlDecoderGetICCProfileSize(
|
|
dec.get(), _PROFILE_ORIGIN_, &icc_size)) {
|
|
g_printerr(LOAD_PROC
|
|
" Warning: JxlDecoderGetICCProfileSize failed\n");
|
|
}
|
|
|
|
if (icc_size > 0) {
|
|
icc_profile.resize(icc_size);
|
|
if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsICCProfile(
|
|
dec.get(), _PROFILE_ORIGIN_,
|
|
icc_profile.data(), icc_profile.size())) {
|
|
g_printerr(LOAD_PROC
|
|
" Warning: JxlDecoderGetColorAsICCProfile failed\n");
|
|
}
|
|
|
|
profile_icc = gimp_color_profile_new_from_icc_profile(
|
|
icc_profile.data(), icc_profile.size(), nullptr);
|
|
|
|
if (profile_icc) {
|
|
is_linear = gimp_color_profile_is_linear(profile_icc);
|
|
g_printerr(LOAD_PROC " Info: Color profile is_linear = %d\n",
|
|
is_linear);
|
|
} else {
|
|
g_printerr(LOAD_PROC " Warning: Failed to read ICC profile.\n");
|
|
}
|
|
} else {
|
|
g_printerr(LOAD_PROC " Warning: Empty ICC data.\n");
|
|
}
|
|
}
|
|
|
|
// Internal color profile detection...
|
|
if (JXL_DEC_SUCCESS ==
|
|
JxlDecoderGetColorAsEncodedProfile(dec.get(), _PROFILE_TARGET_,
|
|
&color_encoding)) {
|
|
g_printerr(LOAD_PROC " Info: Internal color encoding detected.\n");
|
|
|
|
// figure out linearity of internal profile
|
|
switch (color_encoding.transfer_function) {
|
|
case JXL_TRANSFER_FUNCTION_LINEAR:
|
|
is_linear = true;
|
|
break;
|
|
|
|
case JXL_TRANSFER_FUNCTION_709:
|
|
case JXL_TRANSFER_FUNCTION_PQ:
|
|
case JXL_TRANSFER_FUNCTION_HLG:
|
|
case JXL_TRANSFER_FUNCTION_GAMMA:
|
|
case JXL_TRANSFER_FUNCTION_DCI:
|
|
case JXL_TRANSFER_FUNCTION_SRGB:
|
|
is_linear = false;
|
|
break;
|
|
|
|
case JXL_TRANSFER_FUNCTION_UNKNOWN:
|
|
default:
|
|
if (profile_icc) {
|
|
g_printerr(LOAD_PROC
|
|
" Info: Unknown transfer function. "
|
|
"ICC profile is present.");
|
|
} else {
|
|
g_printerr(LOAD_PROC
|
|
" Info: Unknown transfer function. "
|
|
"No ICC profile present.");
|
|
}
|
|
break;
|
|
}
|
|
|
|
switch (color_encoding.color_space) {
|
|
case JXL_COLOR_SPACE_RGB:
|
|
if (color_encoding.white_point == JXL_WHITE_POINT_D65 &&
|
|
color_encoding.primaries == JXL_PRIMARIES_SRGB) {
|
|
if (is_linear) {
|
|
profile_int = gimp_color_profile_new_rgb_srgb_linear();
|
|
} else {
|
|
profile_int = gimp_color_profile_new_rgb_srgb();
|
|
}
|
|
} else if (!is_linear &&
|
|
color_encoding.white_point == JXL_WHITE_POINT_D65 &&
|
|
(color_encoding.primaries_green_xy[0] == 0.2100 ||
|
|
color_encoding.primaries_green_xy[1] == 0.7100)) {
|
|
// Probably Adobe RGB
|
|
profile_int = gimp_color_profile_new_rgb_adobe();
|
|
} else if (profile_icc) {
|
|
g_printerr(LOAD_PROC
|
|
" Info: Unknown RGB colorspace. "
|
|
"Using ICC profile.\n");
|
|
} else {
|
|
g_printerr(LOAD_PROC
|
|
" Info: Unknown RGB colorspace. "
|
|
"Treating as sRGB.\n");
|
|
if (is_linear) {
|
|
profile_int = gimp_color_profile_new_rgb_srgb_linear();
|
|
} else {
|
|
profile_int = gimp_color_profile_new_rgb_srgb();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case JXL_COLOR_SPACE_GRAY:
|
|
is_gray = true;
|
|
if (!profile_icc ||
|
|
color_encoding.white_point == JXL_WHITE_POINT_D65) {
|
|
if (is_linear) {
|
|
profile_int = gimp_color_profile_new_d65_gray_linear();
|
|
} else {
|
|
profile_int = gimp_color_profile_new_d65_gray_srgb_trc();
|
|
}
|
|
}
|
|
break;
|
|
case JXL_COLOR_SPACE_XYB:
|
|
case JXL_COLOR_SPACE_UNKNOWN:
|
|
default:
|
|
if (profile_icc) {
|
|
g_printerr(LOAD_PROC
|
|
" Info: Unknown colorspace. Using ICC profile.\n");
|
|
} else {
|
|
g_error(
|
|
LOAD_PROC
|
|
" Warning: Unknown colorspace. Treating as sRGB profile.\n");
|
|
|
|
if (is_linear) {
|
|
profile_int = gimp_color_profile_new_rgb_srgb_linear();
|
|
} else {
|
|
profile_int = gimp_color_profile_new_rgb_srgb();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// set pixel format
|
|
if (info.num_color_channels > 1) {
|
|
if (info.alpha_bits == 0) {
|
|
image_type = GIMP_RGB;
|
|
layer_type = GIMP_RGB_IMAGE;
|
|
format.num_channels = info.num_color_channels;
|
|
} else {
|
|
image_type = GIMP_RGB;
|
|
layer_type = GIMP_RGBA_IMAGE;
|
|
format.num_channels = info.num_color_channels + 1;
|
|
}
|
|
} else if (info.num_color_channels == 1) {
|
|
if (info.alpha_bits == 0) {
|
|
image_type = GIMP_GRAY;
|
|
layer_type = GIMP_GRAY_IMAGE;
|
|
format.num_channels = info.num_color_channels;
|
|
} else {
|
|
image_type = GIMP_GRAY;
|
|
layer_type = GIMP_GRAYA_IMAGE;
|
|
format.num_channels = info.num_color_channels + 1;
|
|
}
|
|
}
|
|
|
|
// Set image bit depth and linearity
|
|
if (info.bits_per_sample <= 8) {
|
|
if (is_linear) {
|
|
precision = GIMP_PRECISION_U8_LINEAR;
|
|
} else {
|
|
precision = GIMP_PRECISION_U8_GAMMA;
|
|
}
|
|
} else if (info.bits_per_sample <= 16) {
|
|
if (info.exponent_bits_per_sample > 0) {
|
|
if (is_linear) {
|
|
precision = GIMP_PRECISION_HALF_LINEAR;
|
|
} else {
|
|
precision = GIMP_PRECISION_HALF_GAMMA;
|
|
}
|
|
} else if (is_linear) {
|
|
precision = GIMP_PRECISION_U16_LINEAR;
|
|
} else {
|
|
precision = GIMP_PRECISION_U16_GAMMA;
|
|
}
|
|
} else {
|
|
if (info.exponent_bits_per_sample > 0) {
|
|
if (is_linear) {
|
|
precision = GIMP_PRECISION_FLOAT_LINEAR;
|
|
} else {
|
|
precision = GIMP_PRECISION_FLOAT_GAMMA;
|
|
}
|
|
} else if (is_linear) {
|
|
precision = GIMP_PRECISION_U32_LINEAR;
|
|
} else {
|
|
precision = GIMP_PRECISION_U32_GAMMA;
|
|
}
|
|
}
|
|
|
|
// create new image
|
|
if (is_linear) {
|
|
*image_id = gimp_image_new_with_precision(xsize, ysize, image_type,
|
|
GIMP_PRECISION_FLOAT_LINEAR);
|
|
} else {
|
|
*image_id = gimp_image_new_with_precision(xsize, ysize, image_type,
|
|
GIMP_PRECISION_FLOAT_GAMMA);
|
|
}
|
|
|
|
if (profile_int) {
|
|
gimp_image_set_color_profile(*image_id, profile_int);
|
|
} else if (!profile_icc) {
|
|
g_printerr(LOAD_PROC " Warning: No color profile.\n");
|
|
}
|
|
} else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
|
|
// get image from decoder in FLOAT
|
|
format.data_type = JXL_TYPE_FLOAT;
|
|
if (!SetJpegXlOutBuffer(&dec, &format, &buffer_size, &pixels_buffer_1))
|
|
return false;
|
|
} else if (status == JXL_DEC_FULL_IMAGE) {
|
|
// create and insert layer
|
|
gchar *layer_name;
|
|
if (layer_idx == 0 && !info.have_animation) {
|
|
layer_name = g_strdup_printf("Background");
|
|
} else {
|
|
const char *blend = (blend_mode == JXL_BLEND_REPLACE) ? " (replace)"
|
|
: (blend_mode == JXL_BLEND_BLEND) ? " (combine)"
|
|
: "";
|
|
char *temp_frame_name = nullptr;
|
|
bool must_free_frame_name = false;
|
|
if (frame_name.size() == 0) {
|
|
temp_frame_name = g_strdup_printf("Frame %lu", layer_idx + 1);
|
|
must_free_frame_name = true;
|
|
} else {
|
|
temp_frame_name = frame_name.data();
|
|
}
|
|
double fduration = frame_duration * 1000.f * tps_denom / tps_numerator;
|
|
layer_name = g_strdup_printf("%s (%.15gms)%s", temp_frame_name,
|
|
fduration, blend);
|
|
if (must_free_frame_name) free(temp_frame_name);
|
|
}
|
|
layer = gimp_layer_new(*image_id, layer_name, xsize, ysize, layer_type,
|
|
/*opacity=*/100,
|
|
gimp_image_get_default_new_layer_mode(*image_id));
|
|
|
|
gimp_image_insert_layer(*image_id, layer, /*parent_id=*/-1,
|
|
/*position=*/0);
|
|
|
|
pixels_buffer_2 = g_malloc(buffer_size);
|
|
GeglBuffer *buffer = gimp_drawable_get_buffer(layer);
|
|
const Babl *destination_format = gegl_buffer_set_format(buffer, nullptr);
|
|
|
|
std::string babl_format_str = "";
|
|
if (is_gray) {
|
|
babl_format_str += "Y'";
|
|
} else {
|
|
babl_format_str += "R'G'B'";
|
|
}
|
|
if (info.alpha_bits > 0) {
|
|
babl_format_str += "A";
|
|
}
|
|
babl_format_str += " float";
|
|
|
|
const Babl *source_format = babl_format(babl_format_str.c_str());
|
|
|
|
babl_process(babl_fish(source_format, destination_format),
|
|
pixels_buffer_1, pixels_buffer_2, xsize * ysize);
|
|
|
|
gegl_buffer_set(buffer, GEGL_RECTANGLE(0, 0, xsize, ysize), 0, nullptr,
|
|
pixels_buffer_2, GEGL_AUTO_ROWSTRIDE);
|
|
gimp_item_transform_translate(layer, crop_x0, crop_y0);
|
|
|
|
g_clear_object(&buffer);
|
|
g_free(pixels_buffer_1);
|
|
g_free(pixels_buffer_2);
|
|
if (stop_processing) status = JXL_DEC_SUCCESS;
|
|
g_free(layer_name);
|
|
layer_idx++;
|
|
} else if (status == JXL_DEC_FRAME) {
|
|
JxlFrameHeader frame_header;
|
|
if (JxlDecoderGetFrameHeader(dec.get(), &frame_header) !=
|
|
JXL_DEC_SUCCESS) {
|
|
g_printerr(LOAD_PROC " Error: JxlDecoderSetImageOutBuffer failed\n");
|
|
return false;
|
|
}
|
|
xsize = frame_header.layer_info.xsize;
|
|
ysize = frame_header.layer_info.ysize;
|
|
crop_x0 = frame_header.layer_info.crop_x0;
|
|
crop_y0 = frame_header.layer_info.crop_y0;
|
|
frame_duration = frame_header.duration;
|
|
blend_mode = frame_header.layer_info.blend_info.blendmode;
|
|
if (blend_mode != JXL_BLEND_BLEND && blend_mode != JXL_BLEND_REPLACE) {
|
|
g_printerr(
|
|
LOAD_PROC
|
|
" Warning: JxlDecoderGetFrameHeader: Unhandled blend mode: %d\n",
|
|
blend_mode);
|
|
}
|
|
if (frame_header.name_length > 0) {
|
|
frame_name.resize(frame_header.name_length + 1);
|
|
if (JXL_DEC_SUCCESS != JxlDecoderGetFrameName(dec.get(),
|
|
frame_name.data(),
|
|
frame_name.size())) {
|
|
g_printerr(LOAD_PROC "Error: JxlDecoderGetFrameName failed");
|
|
return false;
|
|
}
|
|
} else {
|
|
frame_name.resize(0);
|
|
}
|
|
} else if (status == JXL_DEC_SUCCESS) {
|
|
// All decoding successfully finished.
|
|
// It's not required to call JxlDecoderReleaseInput(dec.get())
|
|
// since the decoder will be destroyed.
|
|
break;
|
|
} else if (status == JXL_DEC_NEED_MORE_INPUT ||
|
|
status == JXL_DEC_FRAME_PROGRESSION) {
|
|
stop_processing = status != JXL_DEC_FRAME_PROGRESSION;
|
|
if (JxlDecoderFlushImage(dec.get()) == JXL_DEC_SUCCESS) {
|
|
status = JXL_DEC_FULL_IMAGE;
|
|
continue;
|
|
}
|
|
g_printerr(LOAD_PROC " Error: Already provided all input\n");
|
|
return false;
|
|
} else if (status == JXL_DEC_ERROR) {
|
|
g_printerr(LOAD_PROC " Error: Decoder error\n");
|
|
return false;
|
|
} else {
|
|
g_printerr(LOAD_PROC " Error: Unknown decoder status\n");
|
|
return false;
|
|
}
|
|
} // end grand decode loop
|
|
|
|
gimp_load_progress.update();
|
|
|
|
if (profile_icc) {
|
|
gimp_image_set_color_profile(*image_id, profile_icc);
|
|
}
|
|
|
|
gimp_load_progress.update();
|
|
|
|
// TODO(xiota): Add option to keep image as float
|
|
if (info.bits_per_sample < 32) {
|
|
gimp_image_convert_precision(*image_id, precision);
|
|
}
|
|
|
|
gimp_image_set_filename(*image_id, filename);
|
|
|
|
gimp_load_progress.finished();
|
|
return true;
|
|
}
|
|
|
|
} // namespace jxl
|