diff options
Diffstat (limited to 'third_party/jpeg-xl/plugins/gimp/file-jxl-load.cc')
-rw-r--r-- | third_party/jpeg-xl/plugins/gimp/file-jxl-load.cc | 487 |
1 files changed, 487 insertions, 0 deletions
diff --git a/third_party/jpeg-xl/plugins/gimp/file-jxl-load.cc b/third_party/jpeg-xl/plugins/gimp/file-jxl-load.cc new file mode 100644 index 0000000000..361a74920c --- /dev/null +++ b/third_party/jpeg-xl/plugins/gimp/file-jxl-load.cc @@ -0,0 +1,487 @@ +// 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; + unsigned long xsize = 0, ysize = 0; + long crop_x0 = 0, crop_y0 = 0; + size_t layer_idx = 0; + uint32_t frame_duration = 0; + double tps_denom = 1.f, tps_numer = 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; + char *frame_name = nullptr; // will be realloced + size_t frame_name_len = 0; + + 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: 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_numer = 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(), &format, _PROFILE_ORIGIN_, &color_encoding)) { + // Attempt to load ICC profile when no internal color encoding + if (JXL_DEC_SUCCESS != JxlDecoderGetICCProfileSize(dec.get(), &format, + _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(), &format, _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(), &format, _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 GString *blend_null_flag = g_string_new(""); + const GString *blend_replace_flag = g_string_new(" (replace)"); + const GString *blend_combine_flag = g_string_new(" (combine)"); + GString *blend; + if (blend_mode == JXL_BLEND_REPLACE) { + blend = (GString *)blend_replace_flag; + } else if (blend_mode == JXL_BLEND_BLEND) { + blend = (GString *)blend_combine_flag; + } else { + blend = (GString *)blend_null_flag; + } + char *temp_frame_name = nullptr; + bool must_free_frame_name = false; + if (frame_name_len == 0) { + temp_frame_name = g_strdup_printf("Frame %lu", layer_idx + 1); + must_free_frame_name = true; + } else { + temp_frame_name = frame_name; + } + double fduration = frame_duration * 1000.f * tps_denom / tps_numer; + layer_name = g_strdup_printf("%s (%.15gms)%s", temp_frame_name, + fduration, blend->str); + 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_name_len = frame_header.name_length) > 0) { + frame_name = (char *)realloc(frame_name, frame_name_len); + if (JXL_DEC_SUCCESS != + JxlDecoderGetFrameName(dec.get(), frame_name, frame_name_len)) { + g_printerr(LOAD_PROC "Error: JxlDecoderGetFrameName failed"); + return false; + }; + } + } 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 |