summaryrefslogtreecommitdiffstats
path: root/third_party/jpeg-xl/plugins/gimp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--third_party/jpeg-xl/plugins/gimp/CMakeLists.txt28
-rw-r--r--third_party/jpeg-xl/plugins/gimp/common.cc27
-rw-r--r--third_party/jpeg-xl/plugins/gimp/common.h45
-rw-r--r--third_party/jpeg-xl/plugins/gimp/file-jxl-load.cc487
-rw-r--r--third_party/jpeg-xl/plugins/gimp/file-jxl-load.h17
-rw-r--r--third_party/jpeg-xl/plugins/gimp/file-jxl-save.cc895
-rw-r--r--third_party/jpeg-xl/plugins/gimp/file-jxl-save.h18
-rw-r--r--third_party/jpeg-xl/plugins/gimp/file-jxl.cc157
8 files changed, 1674 insertions, 0 deletions
diff --git a/third_party/jpeg-xl/plugins/gimp/CMakeLists.txt b/third_party/jpeg-xl/plugins/gimp/CMakeLists.txt
new file mode 100644
index 0000000000..f0a49005ed
--- /dev/null
+++ b/third_party/jpeg-xl/plugins/gimp/CMakeLists.txt
@@ -0,0 +1,28 @@
+# 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.
+
+find_package(PkgConfig)
+pkg_check_modules(Gimp IMPORTED_TARGET gimp-2.0>=2.10 gimpui-2.0>=2.10)
+
+if (NOT Gimp_FOUND)
+ message(WARNING "Gimp development libraries not found, the Gimp plugin will not be built")
+ return ()
+endif ()
+
+add_executable(file-jxl WIN32
+ common.h
+ common.cc
+ file-jxl-load.cc
+ file-jxl-load.h
+ file-jxl-save.cc
+ file-jxl-save.h
+ file-jxl.cc)
+target_link_libraries(file-jxl jxl jxl_threads PkgConfig::Gimp)
+
+target_include_directories(file-jxl PUBLIC
+ ${PROJECT_SOURCE_DIR}) # for plugins/gimp absolute paths.
+
+pkg_get_variable(GIMP_LIB_DIR gimp-2.0 gimplibdir)
+install(TARGETS file-jxl RUNTIME DESTINATION "${GIMP_LIB_DIR}/plug-ins/file-jxl/")
diff --git a/third_party/jpeg-xl/plugins/gimp/common.cc b/third_party/jpeg-xl/plugins/gimp/common.cc
new file mode 100644
index 0000000000..1a884570cb
--- /dev/null
+++ b/third_party/jpeg-xl/plugins/gimp/common.cc
@@ -0,0 +1,27 @@
+// 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/common.h"
+
+namespace jxl {
+
+JpegXlGimpProgress::JpegXlGimpProgress(const char *message) {
+ cur_progress = 0;
+ max_progress = 100;
+
+ gimp_progress_init_printf("%s\n", message);
+}
+
+void JpegXlGimpProgress::update() {
+ gimp_progress_update((float)++cur_progress / (float)max_progress);
+ return;
+}
+
+void JpegXlGimpProgress::finished() {
+ gimp_progress_update(1.0);
+ return;
+}
+
+} // namespace jxl
diff --git a/third_party/jpeg-xl/plugins/gimp/common.h b/third_party/jpeg-xl/plugins/gimp/common.h
new file mode 100644
index 0000000000..3fe63c1a47
--- /dev/null
+++ b/third_party/jpeg-xl/plugins/gimp/common.h
@@ -0,0 +1,45 @@
+// 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.
+
+#ifndef PLUGINS_GIMP_COMMON_H_
+#define PLUGINS_GIMP_COMMON_H_
+
+#include <libgimp/gimp.h>
+#include <libgimp/gimpui.h>
+#include <math.h>
+
+#include <fstream>
+#include <iterator>
+#include <string>
+#include <vector>
+
+#define PLUG_IN_BINARY "file-jxl"
+#define SAVE_PROC "file-jxl-save"
+
+// Defined by both FUIF and glib.
+#undef MAX
+#undef MIN
+#undef CLAMP
+
+#include <jxl/resizable_parallel_runner.h>
+#include <jxl/resizable_parallel_runner_cxx.h>
+
+namespace jxl {
+
+class JpegXlGimpProgress {
+ public:
+ explicit JpegXlGimpProgress(const char *message);
+ void update();
+ void finished();
+
+ private:
+ int cur_progress;
+ int max_progress;
+
+}; // class JpegXlGimpProgress
+
+} // namespace jxl
+
+#endif // PLUGINS_GIMP_COMMON_H_
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
diff --git a/third_party/jpeg-xl/plugins/gimp/file-jxl-load.h b/third_party/jpeg-xl/plugins/gimp/file-jxl-load.h
new file mode 100644
index 0000000000..ef5b92fef6
--- /dev/null
+++ b/third_party/jpeg-xl/plugins/gimp/file-jxl-load.h
@@ -0,0 +1,17 @@
+// 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.
+
+#ifndef PLUGINS_GIMP_FILE_JXL_LOAD_H_
+#define PLUGINS_GIMP_FILE_JXL_LOAD_H_
+
+#include "plugins/gimp/common.h"
+
+namespace jxl {
+
+bool LoadJpegXlImage(const gchar* filename, gint32* image_id);
+
+} // namespace jxl
+
+#endif // PLUGINS_GIMP_FILE_JXL_LOAD_H_
diff --git a/third_party/jpeg-xl/plugins/gimp/file-jxl-save.cc b/third_party/jpeg-xl/plugins/gimp/file-jxl-save.cc
new file mode 100644
index 0000000000..c1e1ebd9af
--- /dev/null
+++ b/third_party/jpeg-xl/plugins/gimp/file-jxl-save.cc
@@ -0,0 +1,895 @@
+// 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-save.h"
+
+#include <jxl/encode.h>
+#include <jxl/encode_cxx.h>
+
+#include <cmath>
+#include <utility>
+
+#include "gobject/gsignal.h"
+
+#define PLUG_IN_BINARY "file-jxl"
+#define SAVE_PROC "file-jxl-save"
+
+#define SCALE_WIDTH 200
+
+namespace jxl {
+
+namespace {
+
+#ifndef g_clear_signal_handler
+// g_clear_signal_handler was added in glib 2.62
+void g_clear_signal_handler(gulong* handler, gpointer instance) {
+ if (handler != nullptr && *handler != 0) {
+ g_signal_handler_disconnect(instance, *handler);
+ *handler = 0;
+ }
+}
+#endif // g_clear_signal_handler
+
+class JpegXlSaveOpts {
+ public:
+ float distance;
+ float quality;
+
+ bool lossless = false;
+ bool is_linear = false;
+ bool has_alpha = false;
+ bool is_gray = false;
+ bool icc_attached = false;
+
+ bool advanced_mode = false;
+ bool use_container = true;
+ bool save_exif = false;
+ int encoding_effort = 7;
+ int faster_decoding = 0;
+
+ std::string babl_format_str = "RGB u16";
+ std::string babl_type_str = "u16";
+ std::string babl_model_str = "RGB";
+
+ JxlPixelFormat pixel_format;
+ JxlBasicInfo basic_info;
+
+ // functions
+ JpegXlSaveOpts();
+
+ bool SetDistance(float dist);
+ bool SetQuality(float qual);
+ bool SetDimensions(int x, int y);
+ bool SetNumChannels(int channels);
+
+ bool UpdateDistance();
+ bool UpdateQuality();
+
+ bool SetModel(bool is_linear_);
+
+ bool UpdateBablFormat();
+ bool SetBablModel(std::string model);
+ bool SetBablType(std::string type);
+
+ bool SetPrecision(int gimp_precision);
+
+ private:
+}; // class JpegXlSaveOpts
+
+JpegXlSaveOpts jxl_save_opts;
+
+class JpegXlSaveGui {
+ public:
+ bool SaveDialog();
+
+ private:
+ GtkWidget* toggle_lossless = nullptr;
+ GtkAdjustment* entry_distance = nullptr;
+ GtkAdjustment* entry_quality = nullptr;
+ GtkAdjustment* entry_effort = nullptr;
+ GtkAdjustment* entry_faster = nullptr;
+ GtkWidget* frame_advanced = nullptr;
+ GtkWidget* toggle_no_xyb = nullptr;
+ GtkWidget* toggle_raw = nullptr;
+ gulong handle_toggle_lossless = 0;
+ gulong handle_entry_quality = 0;
+ gulong handle_entry_distance = 0;
+
+ static bool GuiOnChangeQuality(GtkAdjustment* adj_qual, void* this_pointer);
+
+ static bool GuiOnChangeDistance(GtkAdjustment* adj_dist, void* this_pointer);
+
+ static bool GuiOnChangeEffort(GtkAdjustment* adj_effort);
+ static bool GuiOnChangeLossless(GtkWidget* toggle, void* this_pointer);
+ static bool GuiOnChangeCodestream(GtkWidget* toggle);
+ static bool GuiOnChangeNoXYB(GtkWidget* toggle);
+
+ static bool GuiOnChangeAdvancedMode(GtkWidget* toggle, void* this_pointer);
+}; // class JpegXlSaveGui
+
+JpegXlSaveGui jxl_save_gui;
+
+bool JpegXlSaveGui::GuiOnChangeQuality(GtkAdjustment* adj_qual,
+ void* this_pointer) {
+ JpegXlSaveGui* self = static_cast<JpegXlSaveGui*>(this_pointer);
+
+ g_clear_signal_handler(&self->handle_entry_distance, self->entry_distance);
+ g_clear_signal_handler(&self->handle_entry_quality, self->entry_quality);
+ g_clear_signal_handler(&self->handle_toggle_lossless, self->toggle_lossless);
+
+ GtkAdjustment* adj_dist = self->entry_distance;
+ jxl_save_opts.SetQuality(gtk_adjustment_get_value(adj_qual));
+ gtk_adjustment_set_value(adj_dist, jxl_save_opts.distance);
+
+ self->handle_toggle_lossless = g_signal_connect(
+ self->toggle_lossless, "toggled", G_CALLBACK(GuiOnChangeLossless), self);
+ self->handle_entry_distance =
+ g_signal_connect(self->entry_distance, "value-changed",
+ G_CALLBACK(GuiOnChangeDistance), self);
+ self->handle_entry_quality =
+ g_signal_connect(self->entry_quality, "value-changed",
+ G_CALLBACK(GuiOnChangeQuality), self);
+ return true;
+}
+
+bool JpegXlSaveGui::GuiOnChangeDistance(GtkAdjustment* adj_dist,
+ void* this_pointer) {
+ JpegXlSaveGui* self = static_cast<JpegXlSaveGui*>(this_pointer);
+ GtkAdjustment* adj_qual = self->entry_quality;
+
+ g_clear_signal_handler(&self->handle_entry_distance, self->entry_distance);
+ g_clear_signal_handler(&self->handle_entry_quality, self->entry_quality);
+ g_clear_signal_handler(&self->handle_toggle_lossless, self->toggle_lossless);
+
+ jxl_save_opts.SetDistance(gtk_adjustment_get_value(adj_dist));
+ gtk_adjustment_set_value(adj_qual, jxl_save_opts.quality);
+
+ if (!(jxl_save_opts.distance < 0.001)) {
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_lossless),
+ false);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_no_xyb), false);
+ }
+
+ self->handle_toggle_lossless = g_signal_connect(
+ self->toggle_lossless, "toggled", G_CALLBACK(GuiOnChangeLossless), self);
+ self->handle_entry_distance =
+ g_signal_connect(self->entry_distance, "value-changed",
+ G_CALLBACK(GuiOnChangeDistance), self);
+ self->handle_entry_quality =
+ g_signal_connect(self->entry_quality, "value-changed",
+ G_CALLBACK(GuiOnChangeQuality), self);
+ return true;
+}
+
+bool JpegXlSaveGui::GuiOnChangeEffort(GtkAdjustment* adj_effort) {
+ float new_effort = 10 - gtk_adjustment_get_value(adj_effort);
+ jxl_save_opts.encoding_effort = new_effort;
+ return true;
+}
+
+bool JpegXlSaveGui::GuiOnChangeLossless(GtkWidget* toggle, void* this_pointer) {
+ JpegXlSaveGui* self = static_cast<JpegXlSaveGui*>(this_pointer);
+ GtkAdjustment* adj_distance = self->entry_distance;
+ GtkAdjustment* adj_quality = self->entry_quality;
+ GtkAdjustment* adj_effort = self->entry_effort;
+
+ jxl_save_opts.lossless =
+ gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle));
+
+ g_clear_signal_handler(&self->handle_entry_distance, self->entry_distance);
+ g_clear_signal_handler(&self->handle_entry_quality, self->entry_quality);
+ g_clear_signal_handler(&self->handle_toggle_lossless, self->toggle_lossless);
+
+ if (jxl_save_opts.lossless) {
+ gtk_adjustment_set_value(adj_quality, 100.0);
+ gtk_adjustment_set_value(adj_distance, 0.0);
+ jxl_save_opts.distance = 0;
+ jxl_save_opts.UpdateQuality();
+ gtk_adjustment_set_value(adj_effort, 7);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_no_xyb), true);
+ } else {
+ gtk_adjustment_set_value(adj_quality, 90.0);
+ gtk_adjustment_set_value(adj_distance, 1.0);
+ jxl_save_opts.distance = 1.0;
+ jxl_save_opts.UpdateQuality();
+ gtk_adjustment_set_value(adj_effort, 3);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_no_xyb), false);
+ }
+ self->handle_toggle_lossless = g_signal_connect(
+ self->toggle_lossless, "toggled", G_CALLBACK(GuiOnChangeLossless), self);
+ self->handle_entry_distance =
+ g_signal_connect(self->entry_distance, "value-changed",
+ G_CALLBACK(GuiOnChangeDistance), self);
+ self->handle_entry_quality =
+ g_signal_connect(self->entry_quality, "value-changed",
+ G_CALLBACK(GuiOnChangeQuality), self);
+ return true;
+}
+
+bool JpegXlSaveGui::GuiOnChangeCodestream(GtkWidget* toggle) {
+ jxl_save_opts.use_container =
+ !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle));
+ return true;
+}
+
+bool JpegXlSaveGui::GuiOnChangeNoXYB(GtkWidget* toggle) {
+ jxl_save_opts.basic_info.uses_original_profile =
+ gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle));
+ return true;
+}
+
+bool JpegXlSaveGui::GuiOnChangeAdvancedMode(GtkWidget* toggle,
+ void* this_pointer) {
+ JpegXlSaveGui* self = static_cast<JpegXlSaveGui*>(this_pointer);
+ jxl_save_opts.advanced_mode =
+ gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle));
+
+ gtk_widget_set_sensitive(self->frame_advanced, jxl_save_opts.advanced_mode);
+
+ if (!jxl_save_opts.advanced_mode) {
+ jxl_save_opts.basic_info.uses_original_profile = false;
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_no_xyb), false);
+
+ jxl_save_opts.use_container = true;
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_raw), false);
+
+ jxl_save_opts.faster_decoding = 0;
+ gtk_adjustment_set_value(GTK_ADJUSTMENT(self->entry_faster), 0);
+ }
+ return true;
+}
+
+bool JpegXlSaveGui::SaveDialog() {
+ gboolean run;
+ GtkWidget* dialog;
+ GtkWidget* content_area;
+ GtkWidget* main_vbox;
+ GtkWidget* frame;
+ GtkWidget* toggle;
+ GtkWidget* table;
+ GtkWidget* vbox;
+ GtkWidget* separator;
+
+ // initialize export dialog
+ gimp_ui_init(PLUG_IN_BINARY, true);
+ dialog = gimp_export_dialog_new("JPEG XL", PLUG_IN_BINARY, SAVE_PROC);
+
+ gtk_window_set_resizable(GTK_WINDOW(dialog), false);
+ content_area = gimp_export_dialog_get_content_area(dialog);
+
+ main_vbox = gtk_vbox_new(false, 6);
+ gtk_container_set_border_width(GTK_CONTAINER(main_vbox), 6);
+ gtk_box_pack_start(GTK_BOX(content_area), main_vbox, true, true, 0);
+ gtk_widget_show(main_vbox);
+
+ // Standard Settings Frame
+ frame = gtk_frame_new(nullptr);
+ gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_ETCHED_IN);
+ gtk_box_pack_start(GTK_BOX(main_vbox), frame, false, false, 0);
+ gtk_widget_show(frame);
+
+ vbox = gtk_vbox_new(false, 6);
+ gtk_container_set_border_width(GTK_CONTAINER(vbox), 6);
+ gtk_container_add(GTK_CONTAINER(frame), vbox);
+ gtk_widget_show(vbox);
+
+ // Layout Table
+ table = gtk_table_new(20, 3, false);
+ gtk_table_set_col_spacings(GTK_TABLE(table), 6);
+ gtk_box_pack_start(GTK_BOX(vbox), table, false, false, 0);
+ gtk_widget_show(table);
+
+ // Distance Slider
+ static gchar distance_help[] =
+ "Butteraugli distance target. Suggested values:"
+ "\n\td\u00A0=\u00A00.3\tExcellent"
+ "\n\td\u00A0=\u00A01\tVery Good"
+ "\n\td\u00A0=\u00A02\tGood"
+ "\n\td\u00A0=\u00A03\tFair"
+ "\n\td\u00A0=\u00A06\tPoor";
+
+ entry_distance = (GtkAdjustment*)gimp_scale_entry_new(
+ GTK_TABLE(table), 0, 0, "Distance", SCALE_WIDTH, 0,
+ jxl_save_opts.distance, 0.0, 15.0, 0.001, 1.0, 3, true, 0.0, 0.0,
+ distance_help, SAVE_PROC);
+ gimp_scale_entry_set_logarithmic((GtkObject*)entry_distance, true);
+
+ // Quality Slider
+ static gchar quality_help[] =
+ "JPEG-style Quality is remapped to distance. "
+ "Values roughly match libjpeg quality settings.";
+ entry_quality = (GtkAdjustment*)gimp_scale_entry_new(
+ GTK_TABLE(table), 0, 1, "Quality", SCALE_WIDTH, 0, jxl_save_opts.quality,
+ 8.26, 100.0, 1.0, 10.0, 2, true, 0.0, 0.0, quality_help, SAVE_PROC);
+
+ // Distance and Quality Signals
+ handle_entry_distance = g_signal_connect(
+ entry_distance, "value-changed", G_CALLBACK(GuiOnChangeDistance), this);
+ handle_entry_quality = g_signal_connect(entry_quality, "value-changed",
+ G_CALLBACK(GuiOnChangeQuality), this);
+
+ // ----------
+ separator = gtk_vseparator_new();
+ gtk_table_attach(GTK_TABLE(table), separator, 0, 2, 2, 3, GTK_EXPAND,
+ GTK_EXPAND, 9, 9);
+ gtk_widget_show(separator);
+
+ // Encoding Effort / Speed
+ static gchar effort_help[] =
+ "Adjust encoding speed. Higher values are faster because "
+ "the encoder uses less effort to hit distance targets. "
+ "As\u00A0a\u00A0result, image quality may be decreased. "
+ "Default\u00A0=\u00A03.";
+ entry_effort = (GtkAdjustment*)gimp_scale_entry_new(
+ GTK_TABLE(table), 0, 3, "Speed", SCALE_WIDTH, 0,
+ 10 - jxl_save_opts.encoding_effort, 1, 9, 1, 2, 0, true, 0.0, 0.0,
+ effort_help, SAVE_PROC);
+
+ // effort signal
+ g_signal_connect(entry_effort, "value-changed", G_CALLBACK(GuiOnChangeEffort),
+ nullptr);
+
+ // ----------
+ separator = gtk_vseparator_new();
+ gtk_table_attach(GTK_TABLE(table), separator, 0, 2, 4, 5, GTK_EXPAND,
+ GTK_EXPAND, 9, 9);
+ gtk_widget_show(separator);
+
+ // Lossless Mode Convenience Checkbox
+ static gchar lossless_help[] =
+ "Compress using modular lossless mode. "
+ "Speed\u00A0is adjusted to improve performance.";
+ toggle_lossless = gtk_check_button_new_with_label("Lossless Mode");
+ gimp_help_set_help_data(toggle_lossless, lossless_help, nullptr);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle_lossless),
+ jxl_save_opts.lossless);
+ gtk_table_attach_defaults(GTK_TABLE(table), toggle_lossless, 0, 2, 5, 6);
+ gtk_widget_show(toggle_lossless);
+
+ // lossless signal
+ handle_toggle_lossless = g_signal_connect(
+ toggle_lossless, "toggled", G_CALLBACK(GuiOnChangeLossless), this);
+
+ // ----------
+ separator = gtk_vseparator_new();
+ gtk_box_pack_start(GTK_BOX(main_vbox), separator, false, false, 1);
+ gtk_widget_show(separator);
+
+ // Advanced Settings Frame
+ std::vector<GtkWidget*> advanced_opts;
+
+ frame_advanced = gtk_frame_new("Advanced Settings");
+ gimp_help_set_help_data(frame_advanced,
+ "Some advanced settings may produce malformed files.",
+ nullptr);
+ gtk_frame_set_shadow_type(GTK_FRAME(frame_advanced), GTK_SHADOW_ETCHED_IN);
+ gtk_box_pack_start(GTK_BOX(main_vbox), frame_advanced, true, true, 0);
+ gtk_widget_show(frame_advanced);
+
+ gtk_widget_set_sensitive(frame_advanced, false);
+
+ vbox = gtk_vbox_new(false, 6);
+ gtk_container_set_border_width(GTK_CONTAINER(vbox), 6);
+ gtk_container_add(GTK_CONTAINER(frame_advanced), vbox);
+ gtk_widget_show(vbox);
+
+ // uses_original_profile
+ static gchar uses_original_profile_help[] =
+ "Prevents conversion to the XYB colorspace. "
+ "File sizes are approximately doubled.";
+ toggle_no_xyb = gtk_check_button_new_with_label("Do not use XYB colorspace");
+ gimp_help_set_help_data(toggle_no_xyb, uses_original_profile_help, nullptr);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle_no_xyb),
+ jxl_save_opts.basic_info.uses_original_profile);
+ gtk_box_pack_start(GTK_BOX(vbox), toggle_no_xyb, false, false, 0);
+ gtk_widget_show(toggle_no_xyb);
+
+ g_signal_connect(toggle_no_xyb, "toggled", G_CALLBACK(GuiOnChangeNoXYB),
+ nullptr);
+
+ // save raw codestream
+ static gchar codestream_help[] =
+ "Save the raw codestream, without a container. "
+ "The container is required for metadata and some other features.";
+ toggle_raw = gtk_check_button_new_with_label("Save Raw Codestream");
+ gimp_help_set_help_data(toggle_raw, codestream_help, nullptr);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle_raw),
+ !jxl_save_opts.use_container);
+ gtk_box_pack_start(GTK_BOX(vbox), toggle_raw, false, false, 0);
+ gtk_widget_show(toggle_raw);
+
+ g_signal_connect(toggle_raw, "toggled", G_CALLBACK(GuiOnChangeCodestream),
+ nullptr);
+
+ // ----------
+ separator = gtk_vseparator_new();
+ gtk_box_pack_start(GTK_BOX(vbox), separator, false, false, 1);
+ gtk_widget_show(separator);
+
+ // Faster Decoding / Decoding Speed
+ static gchar faster_help[] =
+ "Improve decoding speed at the expense of quality. "
+ "Default\u00A0=\u00A00.";
+ table = gtk_table_new(1, 3, false);
+ gtk_table_set_col_spacings(GTK_TABLE(table), 6);
+ gtk_container_add(GTK_CONTAINER(vbox), table);
+ gtk_widget_show(table);
+
+ entry_faster = (GtkAdjustment*)gimp_scale_entry_new(
+ GTK_TABLE(table), 0, 0, "Faster Decoding", SCALE_WIDTH, 0,
+ jxl_save_opts.faster_decoding, 0, 4, 1, 1, 0, true, 0.0, 0.0, faster_help,
+ SAVE_PROC);
+
+ // Faster Decoding Signals
+ g_signal_connect(entry_faster, "value-changed",
+ G_CALLBACK(gimp_int_adjustment_update),
+ &jxl_save_opts.faster_decoding);
+
+ // Enable Advanced Settings
+ frame = gtk_frame_new(nullptr);
+ gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_NONE);
+ gtk_box_pack_start(GTK_BOX(main_vbox), frame, true, true, 0);
+ gtk_widget_show(frame);
+
+ vbox = gtk_vbox_new(false, 6);
+ gtk_container_set_border_width(GTK_CONTAINER(vbox), 6);
+ gtk_container_add(GTK_CONTAINER(frame), vbox);
+ gtk_widget_show(vbox);
+
+ static gchar advanced_help[] =
+ "Some advanced settings may produce malformed files.";
+ toggle = gtk_check_button_new_with_label("Enable Advanced Settings");
+ gimp_help_set_help_data(toggle, advanced_help, nullptr);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle),
+ jxl_save_opts.advanced_mode);
+ gtk_box_pack_start(GTK_BOX(vbox), toggle, false, false, 0);
+ gtk_widget_show(toggle);
+
+ g_signal_connect(toggle, "toggled", G_CALLBACK(GuiOnChangeAdvancedMode),
+ this);
+
+ // show dialog
+ gtk_widget_show(dialog);
+
+ GtkAllocation allocation;
+ gtk_widget_get_allocation(dialog, &allocation);
+
+ int height = allocation.height;
+ gtk_widget_set_size_request(dialog, height * 1.5, height);
+
+ run = (gimp_dialog_run(GIMP_DIALOG(dialog)) == GTK_RESPONSE_OK);
+ gtk_widget_destroy(dialog);
+
+ return run;
+} // JpegXlSaveGui::SaveDialog
+
+JpegXlSaveOpts::JpegXlSaveOpts() {
+ SetDistance(1.0);
+
+ pixel_format.num_channels = 4;
+ pixel_format.data_type = JXL_TYPE_FLOAT;
+ pixel_format.endianness = JXL_NATIVE_ENDIAN;
+ pixel_format.align = 0;
+
+ JxlEncoderInitBasicInfo(&basic_info);
+ return;
+} // JpegXlSaveOpts constructor
+
+bool JpegXlSaveOpts::SetModel(bool is_linear_) {
+ int channels;
+ std::string model;
+
+ if (is_gray) {
+ channels = 1;
+ if (is_linear_) {
+ model = "Y";
+ } else {
+ model = "Y'";
+ }
+ } else {
+ channels = 3;
+ if (is_linear_) {
+ model = "RGB";
+ } else {
+ model = "R'G'B'";
+ }
+ }
+ if (has_alpha) {
+ SetBablModel(model + "A");
+ SetNumChannels(channels + 1);
+ } else {
+ SetBablModel(model);
+ SetNumChannels(channels);
+ }
+ return true;
+} // JpegXlSaveOpts::SetModel
+
+bool JpegXlSaveOpts::SetDistance(float dist) {
+ distance = dist;
+ return UpdateQuality();
+}
+
+bool JpegXlSaveOpts::SetQuality(float qual) {
+ quality = qual;
+ return UpdateDistance();
+}
+
+bool JpegXlSaveOpts::UpdateQuality() {
+ float qual;
+
+ if (distance < 0.1) {
+ qual = 100;
+ } else if (distance > 6.4) {
+ qual = -5.0 / 53.0 * sqrt(6360.0 * distance - 39975.0) + 1725.0 / 53.0;
+ lossless = false;
+ } else {
+ qual = 100 - (distance - 0.1) / 0.09;
+ lossless = false;
+ }
+
+ if (qual < 0) {
+ quality = 0.0;
+ } else if (qual >= 100) {
+ quality = 100.0;
+ } else {
+ quality = qual;
+ }
+
+ return true;
+}
+
+bool JpegXlSaveOpts::UpdateDistance() {
+ float dist;
+ if (quality >= 30) {
+ dist = 0.1 + (100 - quality) * 0.09;
+ } else {
+ dist = 53.0 / 3000.0 * quality * quality - 23.0 / 20.0 * quality + 25.0;
+ }
+
+ if (dist > 25) {
+ distance = 25;
+ } else {
+ distance = dist;
+ }
+ return true;
+}
+
+bool JpegXlSaveOpts::SetDimensions(int x, int y) {
+ basic_info.xsize = x;
+ basic_info.ysize = y;
+ return true;
+}
+
+bool JpegXlSaveOpts::SetNumChannels(int channels) {
+ switch (channels) {
+ case 1:
+ pixel_format.num_channels = 1;
+ basic_info.num_color_channels = 1;
+ basic_info.num_extra_channels = 0;
+ basic_info.alpha_bits = 0;
+ basic_info.alpha_exponent_bits = 0;
+ break;
+ case 2:
+ pixel_format.num_channels = 2;
+ basic_info.num_color_channels = 1;
+ basic_info.num_extra_channels = 1;
+ basic_info.alpha_bits = int(std::fmin(16, basic_info.bits_per_sample));
+ basic_info.alpha_exponent_bits = 0;
+ break;
+ case 3:
+ pixel_format.num_channels = 3;
+ basic_info.num_color_channels = 3;
+ basic_info.num_extra_channels = 0;
+ basic_info.alpha_bits = 0;
+ basic_info.alpha_exponent_bits = 0;
+ break;
+ case 4:
+ pixel_format.num_channels = 4;
+ basic_info.num_color_channels = 3;
+ basic_info.num_extra_channels = 1;
+ basic_info.alpha_bits = int(std::fmin(16, basic_info.bits_per_sample));
+ basic_info.alpha_exponent_bits = 0;
+ break;
+ default:
+ SetNumChannels(3);
+ } // switch
+ return true;
+} // JpegXlSaveOpts::SetNumChannels
+
+bool JpegXlSaveOpts::UpdateBablFormat() {
+ babl_format_str = babl_model_str + " " + babl_type_str;
+ return true;
+}
+
+bool JpegXlSaveOpts::SetBablModel(std::string model) {
+ babl_model_str = std::move(model);
+ return UpdateBablFormat();
+}
+
+bool JpegXlSaveOpts::SetBablType(std::string type) {
+ babl_type_str = std::move(type);
+ return UpdateBablFormat();
+}
+
+bool JpegXlSaveOpts::SetPrecision(int gimp_precision) {
+ switch (gimp_precision) {
+ case GIMP_PRECISION_HALF_GAMMA:
+ case GIMP_PRECISION_HALF_LINEAR:
+ basic_info.bits_per_sample = 16;
+ basic_info.exponent_bits_per_sample = 5;
+ break;
+
+ // UINT32 not supported by encoder; using FLOAT instead
+ case GIMP_PRECISION_U32_GAMMA:
+ case GIMP_PRECISION_U32_LINEAR:
+ case GIMP_PRECISION_FLOAT_GAMMA:
+ case GIMP_PRECISION_FLOAT_LINEAR:
+ basic_info.bits_per_sample = 32;
+ basic_info.exponent_bits_per_sample = 8;
+ break;
+
+ case GIMP_PRECISION_U16_GAMMA:
+ case GIMP_PRECISION_U16_LINEAR:
+ basic_info.bits_per_sample = 16;
+ basic_info.exponent_bits_per_sample = 0;
+ break;
+
+ default:
+ case GIMP_PRECISION_U8_LINEAR:
+ case GIMP_PRECISION_U8_GAMMA:
+ basic_info.bits_per_sample = 8;
+ basic_info.exponent_bits_per_sample = 0;
+ break;
+ }
+ return true;
+} // JpegXlSaveOpts::SetPrecision
+
+} // namespace
+
+bool SaveJpegXlImage(const gint32 image_id, const gint32 drawable_id,
+ const gint32 orig_image_id, const gchar* const filename) {
+ if (!jxl_save_gui.SaveDialog()) {
+ return true;
+ }
+
+ gint32 nlayers;
+ gint32* layers;
+ gint32 duplicate = gimp_image_duplicate(image_id);
+
+ JpegXlGimpProgress gimp_save_progress(
+ ("Saving JPEG XL file:" + std::string(filename)).c_str());
+ gimp_save_progress.update();
+
+ // try to get ICC color profile...
+ std::vector<uint8_t> icc;
+
+ GimpColorProfile* profile = gimp_image_get_effective_color_profile(image_id);
+ jxl_save_opts.is_gray = gimp_color_profile_is_gray(profile);
+ jxl_save_opts.is_linear = gimp_color_profile_is_linear(profile);
+
+ profile = gimp_image_get_color_profile(image_id);
+ if (profile) {
+ g_printerr(SAVE_PROC " Info: Extracting ICC Profile...\n");
+ gsize icc_size;
+ const guint8* const icc_bytes =
+ gimp_color_profile_get_icc_profile(profile, &icc_size);
+
+ icc.assign(icc_bytes, icc_bytes + icc_size);
+ } else {
+ g_printerr(SAVE_PROC " Info: No ICC profile. Exporting image anyway.\n");
+ }
+
+ gimp_save_progress.update();
+
+ jxl_save_opts.SetDimensions(gimp_image_width(image_id),
+ gimp_image_height(image_id));
+
+ jxl_save_opts.SetPrecision(gimp_image_get_precision(image_id));
+ layers = gimp_image_get_layers(duplicate, &nlayers);
+
+ for (int i = 0; i < nlayers; i++) {
+ if (gimp_drawable_has_alpha(layers[i])) {
+ jxl_save_opts.has_alpha = true;
+ break;
+ }
+ }
+
+ gimp_save_progress.update();
+
+ // layers need to match image size, for now
+ for (int i = 0; i < nlayers; i++) {
+ gimp_layer_resize_to_image_size(layers[i]);
+ }
+
+ // treat layers as animation frames, for now
+ if (nlayers > 1) {
+ jxl_save_opts.basic_info.have_animation = true;
+ jxl_save_opts.basic_info.animation.tps_numerator = 100;
+ }
+
+ gimp_save_progress.update();
+
+ // multi-threaded parallel runner.
+ auto runner = JxlResizableParallelRunnerMake(nullptr);
+
+ JxlResizableParallelRunnerSetThreads(
+ runner.get(),
+ JxlResizableParallelRunnerSuggestThreads(jxl_save_opts.basic_info.xsize,
+ jxl_save_opts.basic_info.ysize));
+
+ auto enc = JxlEncoderMake(/*memory_manager=*/nullptr);
+ JxlEncoderUseContainer(enc.get(), jxl_save_opts.use_container);
+
+ if (JXL_ENC_SUCCESS != JxlEncoderSetParallelRunner(enc.get(),
+ JxlResizableParallelRunner,
+ runner.get())) {
+ g_printerr(SAVE_PROC " Error: JxlEncoderSetParallelRunner failed\n");
+ return false;
+ }
+
+ // this sets some basic_info properties
+ jxl_save_opts.SetModel(jxl_save_opts.is_linear);
+
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetBasicInfo(enc.get(), &jxl_save_opts.basic_info)) {
+ g_printerr(SAVE_PROC " Error: JxlEncoderSetBasicInfo failed\n");
+ return false;
+ }
+
+ // try to use ICC profile
+ if (!icc.empty() && !jxl_save_opts.is_gray) {
+ if (JXL_ENC_SUCCESS ==
+ JxlEncoderSetICCProfile(enc.get(), icc.data(), icc.size())) {
+ jxl_save_opts.icc_attached = true;
+ } else {
+ g_printerr(SAVE_PROC " Warning: JxlEncoderSetICCProfile failed.\n");
+ jxl_save_opts.basic_info.uses_original_profile = false;
+ jxl_save_opts.lossless = false;
+ }
+ } else {
+ g_printerr(SAVE_PROC " Warning: Using internal profile.\n");
+ jxl_save_opts.basic_info.uses_original_profile = false;
+ jxl_save_opts.lossless = false;
+ }
+
+ // set up internal color profile
+ JxlColorEncoding color_encoding = {};
+
+ if (jxl_save_opts.is_linear) {
+ JxlColorEncodingSetToLinearSRGB(&color_encoding, jxl_save_opts.is_gray);
+ } else {
+ JxlColorEncodingSetToSRGB(&color_encoding, jxl_save_opts.is_gray);
+ }
+
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderSetColorEncoding(enc.get(), &color_encoding)) {
+ g_printerr(SAVE_PROC " Warning: JxlEncoderSetColorEncoding failed\n");
+ }
+
+ // set encoder options
+ JxlEncoderFrameSettings* frame_settings;
+ frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), nullptr);
+
+ JxlEncoderFrameSettingsSetOption(frame_settings, JXL_ENC_FRAME_SETTING_EFFORT,
+ jxl_save_opts.encoding_effort);
+ JxlEncoderFrameSettingsSetOption(frame_settings,
+ JXL_ENC_FRAME_SETTING_DECODING_SPEED,
+ jxl_save_opts.faster_decoding);
+
+ // lossless mode
+ if (jxl_save_opts.lossless || jxl_save_opts.distance < 0.01) {
+ if (jxl_save_opts.basic_info.exponent_bits_per_sample > 0) {
+ // lossless mode doesn't work well with floating point
+ jxl_save_opts.distance = 0.01;
+ jxl_save_opts.lossless = false;
+ JxlEncoderSetFrameLossless(frame_settings, false);
+ JxlEncoderSetFrameDistance(frame_settings, 0.01);
+ } else {
+ JxlEncoderSetFrameDistance(frame_settings, 0);
+ JxlEncoderSetFrameLossless(frame_settings, true);
+ }
+ } else {
+ jxl_save_opts.lossless = false;
+ JxlEncoderSetFrameLossless(frame_settings, false);
+ JxlEncoderSetFrameDistance(frame_settings, jxl_save_opts.distance);
+ }
+
+ // convert precision and colorspace
+ if (jxl_save_opts.is_linear &&
+ jxl_save_opts.basic_info.bits_per_sample < 32) {
+ gimp_image_convert_precision(duplicate, GIMP_PRECISION_FLOAT_LINEAR);
+ } else {
+ gimp_image_convert_precision(duplicate, GIMP_PRECISION_FLOAT_GAMMA);
+ }
+
+ // process layers and compress into JXL
+ size_t buffer_size =
+ jxl_save_opts.basic_info.xsize * jxl_save_opts.basic_info.ysize *
+ jxl_save_opts.pixel_format.num_channels * 4; // bytes per sample
+
+ for (int i = nlayers - 1; i >= 0; i--) {
+ gimp_save_progress.update();
+
+ // copy image into buffer...
+ gpointer pixels_buffer_1;
+ gpointer pixels_buffer_2;
+ pixels_buffer_1 = g_malloc(buffer_size);
+ pixels_buffer_2 = g_malloc(buffer_size);
+
+ gimp_layer_resize_to_image_size(layers[i]);
+
+ GeglBuffer* buffer = gimp_drawable_get_buffer(layers[i]);
+
+ // using gegl_buffer_set_format to get the format because
+ // gegl_buffer_get_format doesn't always get the original format
+ const Babl* native_format = gegl_buffer_set_format(buffer, nullptr);
+
+ gegl_buffer_get(buffer,
+ GEGL_RECTANGLE(0, 0, jxl_save_opts.basic_info.xsize,
+ jxl_save_opts.basic_info.ysize),
+ 1.0, native_format, pixels_buffer_1, GEGL_AUTO_ROWSTRIDE,
+ GEGL_ABYSS_NONE);
+ g_clear_object(&buffer);
+
+ // use babl to fix gamma mismatch issues
+ jxl_save_opts.SetModel(jxl_save_opts.is_linear);
+ jxl_save_opts.pixel_format.data_type = JXL_TYPE_FLOAT;
+ jxl_save_opts.SetBablType("float");
+ const Babl* destination_format =
+ babl_format(jxl_save_opts.babl_format_str.c_str());
+
+ babl_process(
+ babl_fish(native_format, destination_format), pixels_buffer_1,
+ pixels_buffer_2,
+ jxl_save_opts.basic_info.xsize * jxl_save_opts.basic_info.ysize);
+
+ gimp_save_progress.update();
+
+ // send layer to encoder
+ if (JXL_ENC_SUCCESS !=
+ JxlEncoderAddImageFrame(frame_settings, &jxl_save_opts.pixel_format,
+ pixels_buffer_2, buffer_size)) {
+ g_printerr(SAVE_PROC " Error: JxlEncoderAddImageFrame failed\n");
+ return false;
+ }
+ }
+
+ JxlEncoderCloseInput(enc.get());
+
+ // get data from encoder
+ std::vector<uint8_t> compressed;
+ compressed.resize(262144);
+ uint8_t* next_out = compressed.data();
+ size_t avail_out = compressed.size();
+
+ JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT;
+ while (process_result == JXL_ENC_NEED_MORE_OUTPUT) {
+ gimp_save_progress.update();
+
+ process_result = JxlEncoderProcessOutput(enc.get(), &next_out, &avail_out);
+ if (process_result == JXL_ENC_NEED_MORE_OUTPUT) {
+ size_t offset = next_out - compressed.data();
+ compressed.resize(compressed.size() + 262144);
+ next_out = compressed.data() + offset;
+ avail_out = compressed.size() - offset;
+ }
+ }
+ compressed.resize(next_out - compressed.data());
+
+ if (JXL_ENC_SUCCESS != process_result) {
+ g_printerr(SAVE_PROC " Error: JxlEncoderProcessOutput failed\n");
+ return false;
+ }
+
+ // write file
+ std::ofstream outstream(filename, std::ios::out | std::ios::binary);
+ copy(compressed.begin(), compressed.end(),
+ std::ostream_iterator<uint8_t>(outstream));
+
+ gimp_save_progress.finished();
+ return true;
+} // SaveJpegXlImage()
+
+} // namespace jxl
diff --git a/third_party/jpeg-xl/plugins/gimp/file-jxl-save.h b/third_party/jpeg-xl/plugins/gimp/file-jxl-save.h
new file mode 100644
index 0000000000..c9d0e8091f
--- /dev/null
+++ b/third_party/jpeg-xl/plugins/gimp/file-jxl-save.h
@@ -0,0 +1,18 @@
+// 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.
+
+#ifndef PLUGINS_GIMP_FILE_JXL_SAVE_H_
+#define PLUGINS_GIMP_FILE_JXL_SAVE_H_
+
+#include "plugins/gimp/common.h"
+
+namespace jxl {
+
+bool SaveJpegXlImage(gint32 image_id, gint32 drawable_id, gint32 orig_image_id,
+ const gchar* filename);
+
+} // namespace jxl
+
+#endif // PLUGINS_GIMP_FILE_JXL_SAVE_H_
diff --git a/third_party/jpeg-xl/plugins/gimp/file-jxl.cc b/third_party/jpeg-xl/plugins/gimp/file-jxl.cc
new file mode 100644
index 0000000000..743495a2e0
--- /dev/null
+++ b/third_party/jpeg-xl/plugins/gimp/file-jxl.cc
@@ -0,0 +1,157 @@
+// 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 <string.h>
+
+#include <string>
+
+#include "plugins/gimp/common.h"
+#include "plugins/gimp/file-jxl-load.h"
+#include "plugins/gimp/file-jxl-save.h"
+
+namespace jxl {
+namespace {
+
+constexpr char kLoadProc[] = "file-jxl-load";
+constexpr char kSaveProc[] = "file-jxl-save";
+
+void Query() {
+ {
+ static char run_mode_name[] = "run-mode";
+ static char run_mode_description[] = "Run mode";
+ static char filename_name[] = "filename";
+ static char filename_description[] = "The name of the file to load";
+ static char raw_filename_name[] = "raw-filename";
+ static char raw_filename_description[] =
+ "The name of the file, as entered by the user";
+ static const GimpParamDef load_args[] = {
+ {GIMP_PDB_INT32, run_mode_name, run_mode_description},
+ {GIMP_PDB_STRING, filename_name, filename_description},
+ {GIMP_PDB_STRING, raw_filename_name, raw_filename_description},
+ };
+ static char image_name[] = "image";
+ static char image_description[] = "Loaded image";
+ static const GimpParamDef load_return_vals[] = {
+ {GIMP_PDB_IMAGE, image_name, image_description},
+ };
+
+ gimp_install_procedure(
+ /*name=*/kLoadProc, /*blurb=*/"Loads JPEG XL image files",
+ /*help=*/"Loads JPEG XL image files", /*author=*/"JPEG XL Project",
+ /*copyright=*/"JPEG XL Project", /*date=*/"2019",
+ /*menu_label=*/"JPEG XL image", /*image_types=*/nullptr,
+ /*type=*/GIMP_PLUGIN, /*n_params=*/G_N_ELEMENTS(load_args),
+ /*n_return_vals=*/G_N_ELEMENTS(load_return_vals), /*params=*/load_args,
+ /*return_vals=*/load_return_vals);
+ gimp_register_file_handler_mime(kLoadProc, "image/jxl");
+ gimp_register_magic_load_handler(
+ kLoadProc, "jxl", "",
+ "0,string,\xFF\x0A,"
+ "0,string,\\000\\000\\000\x0CJXL\\040\\015\\012\x87\\012");
+ }
+
+ {
+ static char run_mode_name[] = "run-mode";
+ static char run_mode_description[] = "Run mode";
+ static char image_name[] = "image";
+ static char image_description[] = "Input image";
+ static char drawable_name[] = "drawable";
+ static char drawable_description[] = "Drawable to save";
+ static char filename_name[] = "filename";
+ static char filename_description[] = "The name of the file to save";
+ static char raw_filename_name[] = "raw-filename";
+ static char raw_filename_description[] = "The name of the file to save";
+ static const GimpParamDef save_args[] = {
+ {GIMP_PDB_INT32, run_mode_name, run_mode_description},
+ {GIMP_PDB_IMAGE, image_name, image_description},
+ {GIMP_PDB_DRAWABLE, drawable_name, drawable_description},
+ {GIMP_PDB_STRING, filename_name, filename_description},
+ {GIMP_PDB_STRING, raw_filename_name, raw_filename_description},
+ };
+
+ gimp_install_procedure(
+ /*name=*/kSaveProc, /*blurb=*/"Saves JPEG XL image files",
+ /*help=*/"Saves JPEG XL image files", /*author=*/"JPEG XL Project",
+ /*copyright=*/"JPEG XL Project", /*date=*/"2019",
+ /*menu_label=*/"JPEG XL image", /*image_types=*/"RGB*, GRAY*",
+ /*type=*/GIMP_PLUGIN, /*n_params=*/G_N_ELEMENTS(save_args),
+ /*n_return_vals=*/0, /*params=*/save_args,
+ /*return_vals=*/nullptr);
+ gimp_register_file_handler_mime(kSaveProc, "image/jxl");
+ gimp_register_save_handler(kSaveProc, "jxl", "");
+ }
+}
+
+void Run(const gchar* const name, const gint nparams,
+ const GimpParam* const params, gint* const nreturn_vals,
+ GimpParam** const return_vals) {
+ gegl_init(nullptr, nullptr);
+
+ static GimpParam values[2];
+
+ *nreturn_vals = 1;
+ *return_vals = values;
+
+ values[0].type = GIMP_PDB_STATUS;
+ values[0].data.d_status = GIMP_PDB_EXECUTION_ERROR;
+
+ if (strcmp(name, kLoadProc) == 0) {
+ if (nparams != 3) {
+ values[0].data.d_status = GIMP_PDB_CALLING_ERROR;
+ return;
+ }
+
+ const gchar* const filename = params[1].data.d_string;
+ gint32 image_id;
+ if (!LoadJpegXlImage(filename, &image_id)) {
+ values[0].data.d_status = GIMP_PDB_EXECUTION_ERROR;
+ return;
+ }
+
+ *nreturn_vals = 2;
+ values[0].data.d_status = GIMP_PDB_SUCCESS;
+ values[1].type = GIMP_PDB_IMAGE;
+ values[1].data.d_image = image_id;
+ } else if (strcmp(name, kSaveProc) == 0) {
+ if (nparams != 5) {
+ values[0].data.d_status = GIMP_PDB_CALLING_ERROR;
+ return;
+ }
+
+ gint32 image_id = params[1].data.d_image;
+ gint32 drawable_id = params[2].data.d_drawable;
+ const gchar* const filename = params[3].data.d_string;
+ const gint32 orig_image_id = image_id;
+ const GimpExportReturn export_result = gimp_export_image(
+ &image_id, &drawable_id, "JPEG XL",
+ static_cast<GimpExportCapabilities>(GIMP_EXPORT_CAN_HANDLE_RGB |
+ GIMP_EXPORT_CAN_HANDLE_GRAY |
+ GIMP_EXPORT_CAN_HANDLE_ALPHA));
+ switch (export_result) {
+ case GIMP_EXPORT_CANCEL:
+ values[0].data.d_status = GIMP_PDB_CANCEL;
+ return;
+ case GIMP_EXPORT_IGNORE:
+ break;
+ case GIMP_EXPORT_EXPORT:
+ break;
+ }
+ if (!SaveJpegXlImage(image_id, drawable_id, orig_image_id, filename)) {
+ return;
+ }
+ if (image_id != orig_image_id) {
+ gimp_image_delete(image_id);
+ }
+ values[0].data.d_status = GIMP_PDB_SUCCESS;
+ }
+}
+
+} // namespace
+} // namespace jxl
+
+static const GimpPlugInInfo PLUG_IN_INFO = {nullptr, nullptr, &jxl::Query,
+ &jxl::Run};
+
+MAIN()