summaryrefslogtreecommitdiffstats
path: root/third_party/jpeg-xl/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/jpeg-xl/plugins')
-rw-r--r--third_party/jpeg-xl/plugins/CMakeLists.txt21
-rw-r--r--third_party/jpeg-xl/plugins/gdk-pixbuf/CMakeLists.txt83
-rw-r--r--third_party/jpeg-xl/plugins/gdk-pixbuf/README.md50
-rw-r--r--third_party/jpeg-xl/plugins/gdk-pixbuf/jxl.thumbnailer4
-rw-r--r--third_party/jpeg-xl/plugins/gdk-pixbuf/loaders_test.cache16
-rw-r--r--third_party/jpeg-xl/plugins/gdk-pixbuf/pixbufloader-jxl.c816
-rw-r--r--third_party/jpeg-xl/plugins/gdk-pixbuf/pixbufloader_test.cc41
-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
-rw-r--r--third_party/jpeg-xl/plugins/mime/CMakeLists.txt6
-rw-r--r--third_party/jpeg-xl/plugins/mime/README.md37
-rw-r--r--third_party/jpeg-xl/plugins/mime/image-jxl.xml13
18 files changed, 2761 insertions, 0 deletions
diff --git a/third_party/jpeg-xl/plugins/CMakeLists.txt b/third_party/jpeg-xl/plugins/CMakeLists.txt
new file mode 100644
index 0000000000..bff1bff29d
--- /dev/null
+++ b/third_party/jpeg-xl/plugins/CMakeLists.txt
@@ -0,0 +1,21 @@
+# 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.
+
+if(NOT MSVC)
+ option(JPEGXL_ENABLE_PLUGIN_GDKPIXBUF "Enable plugin for GdkPixbuf image loading library" ON)
+ if(JPEGXL_ENABLE_PLUGIN_GDKPIXBUF)
+ add_subdirectory(gdk-pixbuf)
+ endif()
+endif()
+
+option(JPEGXL_ENABLE_PLUGIN_GIMP210 "Enable plugin for GIMP 2.10.x series" ON)
+if(JPEGXL_ENABLE_PLUGIN_GIMP210)
+ add_subdirectory(gimp)
+endif()
+
+option(JPEGXL_ENABLE_PLUGIN_MIME "Enable image/jxl declaration for shared-mime-info" ON)
+if(JPEGXL_ENABLE_PLUGIN_MIME)
+ add_subdirectory(mime)
+endif()
diff --git a/third_party/jpeg-xl/plugins/gdk-pixbuf/CMakeLists.txt b/third_party/jpeg-xl/plugins/gdk-pixbuf/CMakeLists.txt
new file mode 100644
index 0000000000..7b53b98c66
--- /dev/null
+++ b/third_party/jpeg-xl/plugins/gdk-pixbuf/CMakeLists.txt
@@ -0,0 +1,83 @@
+# 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(Gdk-Pixbuf IMPORTED_TARGET gdk-pixbuf-2.0>=2.36)
+
+include(GNUInstallDirs)
+
+if (NOT Gdk-Pixbuf_FOUND)
+ message(WARNING "GDK Pixbuf development libraries not found, \
+ the Gdk-Pixbuf plugin will not be built")
+ return ()
+endif ()
+
+add_library(pixbufloader-jxl MODULE pixbufloader-jxl.c)
+
+# Mark all symbols as hidden by default. The PkgConfig::Gdk-Pixbuf dependency
+# will cause fill_info and fill_vtable entry points to be made public.
+set_target_properties(pixbufloader-jxl PROPERTIES
+ CXX_VISIBILITY_PRESET hidden
+ VISIBILITY_INLINES_HIDDEN 1
+)
+
+# Note: This only needs the decoder library, but we don't install the decoder
+# shared library.
+target_link_libraries(pixbufloader-jxl jxl jxl_threads lcms2 PkgConfig::Gdk-Pixbuf)
+
+execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} gdk-pixbuf-2.0 --variable gdk_pixbuf_moduledir --define-variable=prefix=${CMAKE_INSTALL_PREFIX} OUTPUT_VARIABLE GDK_PIXBUF_MODULEDIR OUTPUT_STRIP_TRAILING_WHITESPACE)
+install(TARGETS pixbufloader-jxl DESTINATION "${GDK_PIXBUF_MODULEDIR}")
+
+# Instead of the following, we might instead add the
+# mime type image/jxl to
+# /usr/share/thumbnailers/gdk-pixbuf-thumbnailer.thumbnailer
+install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/jxl.thumbnailer DESTINATION "${CMAKE_INSTALL_DATADIR}/thumbnailers/")
+
+if(BUILD_TESTING AND NOT CMAKE_CROSSCOMPILING)
+ pkg_check_modules(Gdk IMPORTED_TARGET gdk-2.0)
+ if (Gdk_FOUND)
+ # Test for loading a .jxl file using the pixbufloader library via GDK. This
+ # requires to have the image/jxl mime type and loader library configured,
+ # which we do in a fake environment in the CMAKE_CURRENT_BINARY_DIR.
+ add_executable(pixbufloader_test pixbufloader_test.cc)
+ target_link_libraries(pixbufloader_test PkgConfig::Gdk)
+
+ # Create a mime cache for test.
+ add_custom_command(
+ OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/mime/mime.cache"
+ COMMAND env XDG_DATA_HOME=${CMAKE_CURRENT_BINARY_DIR}
+ xdg-mime install --novendor
+ "${CMAKE_SOURCE_DIR}/plugins/mime/image-jxl.xml"
+ DEPENDS "${CMAKE_SOURCE_DIR}/plugins/mime/image-jxl.xml"
+ )
+ add_custom_target(pixbufloader_test_mime
+ DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/mime/mime.cache"
+ )
+ add_dependencies(pixbufloader_test pixbufloader_test_mime)
+
+ # Use a fake X server to run the test if xvfb is installed.
+ find_program (XVFB_PROGRAM xvfb-run)
+ if(XVFB_PROGRAM)
+ set(XVFB_PROGRAM_PREFIX "${XVFB_PROGRAM};-a")
+ else()
+ set(XVFB_PROGRAM_PREFIX "")
+ endif()
+
+ # libX11.so and libgdk-x11-2.0.so are not compiled with MSAN -> report
+ # use-of-uninitialized-value for string some internal string value.
+ # TODO(eustas): investigate direct memory leak (32 bytes).
+ if (NOT (SANITIZER STREQUAL "msan") AND NOT (SANITIZER STREQUAL "asan"))
+ add_test(
+ NAME pixbufloader_test_jxl
+ COMMAND
+ ${XVFB_PROGRAM_PREFIX} $<TARGET_FILE:pixbufloader_test>
+ "${CMAKE_CURRENT_SOURCE_DIR}/loaders_test.cache"
+ "${CMAKE_SOURCE_DIR}/testdata/jxl/blending/cropped_traffic_light.jxl"
+ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+ )
+ set_tests_properties(pixbufloader_test_jxl PROPERTIES SKIP_RETURN_CODE 254)
+ endif()
+ endif() # Gdk_FOUND
+endif() # BUILD_TESTING
diff --git a/third_party/jpeg-xl/plugins/gdk-pixbuf/README.md b/third_party/jpeg-xl/plugins/gdk-pixbuf/README.md
new file mode 100644
index 0000000000..185919436f
--- /dev/null
+++ b/third_party/jpeg-xl/plugins/gdk-pixbuf/README.md
@@ -0,0 +1,50 @@
+## JPEG XL GDK Pixbuf
+
+
+The plugin may already have been installed when following the instructions from the
+[Installing section of BUILDING.md](../../BUILDING.md#installing), in which case it should
+already be in the correct place, e.g.
+
+```/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-jxl.so```
+
+Otherwise we can copy it manually:
+
+```bash
+sudo cp $your_build_directory/plugins/gdk-pixbuf/libpixbufloader-jxl.so /usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-jxl.so
+```
+
+
+Then we need to update the cache, for example with:
+
+```bash
+sudo /usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders --update-cache
+```
+
+In order to get thumbnails with this, first one has to add the jxl MIME type, see
+[../mime/README.md](../mime/README.md).
+
+Ensure that the thumbnailer file is installed in the correct place,
+`/usr/share/thumbnailers/jxl.thumbnailer` or `/usr/local/share/thumbnailers/jxl.thumbnailer`.
+
+The file should have been copied automatically when following the instructions
+in the [Installing section of README.md](../../README.md#installing), but
+otherwise it can be copied manually:
+
+```bash
+sudo cp plugins/gdk-pixbuf/jxl.thumbnailer /usr/local/share/thumbnailers/jxl.thumbnailer
+```
+
+Update the Mime database with
+```bash
+update-mime --local
+```
+or
+```bash
+sudo update-desktop-database
+```
+
+Then possibly delete the thumbnail cache with
+```bash
+rm -r ~/.cache/thumbnails
+```
+and restart the application displaying thumbnails, e.g. `nautilus -q` to display thumbnails.
diff --git a/third_party/jpeg-xl/plugins/gdk-pixbuf/jxl.thumbnailer b/third_party/jpeg-xl/plugins/gdk-pixbuf/jxl.thumbnailer
new file mode 100644
index 0000000000..1bcaab61fc
--- /dev/null
+++ b/third_party/jpeg-xl/plugins/gdk-pixbuf/jxl.thumbnailer
@@ -0,0 +1,4 @@
+[Thumbnailer Entry]
+TryExec=/usr/bin/gdk-pixbuf-thumbnailer
+Exec=/usr/bin/gdk-pixbuf-thumbnailer -s %s %u %o
+MimeType=image/jxl;
diff --git a/third_party/jpeg-xl/plugins/gdk-pixbuf/loaders_test.cache b/third_party/jpeg-xl/plugins/gdk-pixbuf/loaders_test.cache
new file mode 100644
index 0000000000..95c62c8fc3
--- /dev/null
+++ b/third_party/jpeg-xl/plugins/gdk-pixbuf/loaders_test.cache
@@ -0,0 +1,16 @@
+# GdkPixbuf Image Loader Modules file for testing
+# Automatically generated file, do not edit
+# Created by gdk-pixbuf-query-loaders from gdk-pixbuf-2.42.2
+#
+# Generated with:
+# GDK_PIXBUF_MODULEDIR=`pwd`/build/plugins/gdk-pixbuf/ gdk-pixbuf-query-loaders
+#
+# Modified to use the library from the current working directory at runtime.
+"./libpixbufloader-jxl.so"
+"jxl" 4 "gdk-pixbuf" "JPEG XL image" "BSD-3"
+"image/jxl" ""
+"jxl" ""
+"\377\n" " " 100
+"...\fJXL \r\n\207\n" "zzz " 100
+
+
diff --git a/third_party/jpeg-xl/plugins/gdk-pixbuf/pixbufloader-jxl.c b/third_party/jpeg-xl/plugins/gdk-pixbuf/pixbufloader-jxl.c
new file mode 100644
index 0000000000..9df9611b39
--- /dev/null
+++ b/third_party/jpeg-xl/plugins/gdk-pixbuf/pixbufloader-jxl.c
@@ -0,0 +1,816 @@
+// 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 <jxl/codestream_header.h>
+#include <jxl/decode.h>
+#include <jxl/encode.h>
+#include <jxl/resizable_parallel_runner.h>
+#include <jxl/types.h>
+
+#include "lcms2.h"
+
+#define GDK_PIXBUF_ENABLE_BACKEND
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#undef GDK_PIXBUF_ENABLE_BACKEND
+
+G_BEGIN_DECLS
+
+// Information about a single frame.
+typedef struct {
+ uint64_t duration_ms;
+ GdkPixbuf *data;
+ gboolean decoded;
+} GdkPixbufJxlAnimationFrame;
+
+// Represent a whole JPEG XL animation; all its fields are owned; as a GObject,
+// the Animation struct itself is reference counted (as are the GdkPixbufs for
+// individual frames).
+struct _GdkPixbufJxlAnimation {
+ GdkPixbufAnimation parent_instance;
+
+ // GDK interface implementation callbacks.
+ GdkPixbufModuleSizeFunc image_size_callback;
+ GdkPixbufModulePreparedFunc pixbuf_prepared_callback;
+ GdkPixbufModuleUpdatedFunc area_updated_callback;
+ gpointer user_data;
+
+ // All frames known so far; a frame is added when the JXL_DEC_FRAME event is
+ // received from the decoder; initially frame.decoded is FALSE, until
+ // the JXL_DEC_IMAGE event is received.
+ GArray *frames;
+
+ // JPEG XL decoder and related structures.
+ JxlParallelRunner *parallel_runner;
+ JxlDecoder *decoder;
+ JxlPixelFormat pixel_format;
+
+ // Decoding is `done` when JXL_DEC_SUCCESS is received; calling
+ // load_increment afterwards gives an error.
+ gboolean done;
+
+ // Image information.
+ size_t xsize;
+ size_t ysize;
+ gboolean alpha_premultiplied;
+ gboolean has_animation;
+ gboolean has_alpha;
+ uint64_t total_duration_ms;
+ uint64_t tick_duration_us;
+ uint64_t repetition_count; // 0 = loop forever
+
+ gpointer icc_buff;
+ cmsContext context;
+ cmsHPROFILE profile, srgb;
+ cmsHTRANSFORM transform;
+};
+
+#define GDK_TYPE_PIXBUF_JXL_ANIMATION (gdk_pixbuf_jxl_animation_get_type())
+G_DECLARE_FINAL_TYPE(GdkPixbufJxlAnimation, gdk_pixbuf_jxl_animation, GDK,
+ JXL_ANIMATION, GdkPixbufAnimation);
+
+G_DEFINE_TYPE(GdkPixbufJxlAnimation, gdk_pixbuf_jxl_animation,
+ GDK_TYPE_PIXBUF_ANIMATION);
+
+// Iterator to a given point in time in the animation; contains a pointer to the
+// full animation.
+struct _GdkPixbufJxlAnimationIter {
+ GdkPixbufAnimationIter parent_instance;
+ GdkPixbufJxlAnimation *animation;
+ size_t current_frame;
+ uint64_t time_offset;
+};
+
+#define GDK_TYPE_PIXBUF_JXL_ANIMATION_ITER \
+ (gdk_pixbuf_jxl_animation_iter_get_type())
+G_DECLARE_FINAL_TYPE(GdkPixbufJxlAnimationIter, gdk_pixbuf_jxl_animation_iter,
+ GDK, JXL_ANIMATION_ITER, GdkPixbufAnimationIter);
+G_DEFINE_TYPE(GdkPixbufJxlAnimationIter, gdk_pixbuf_jxl_animation_iter,
+ GDK_TYPE_PIXBUF_ANIMATION_ITER);
+
+static void gdk_pixbuf_jxl_animation_init(GdkPixbufJxlAnimation *obj) {
+ // Suppress "unused function" warnings.
+ (void)glib_autoptr_cleanup_GdkPixbufJxlAnimation;
+ (void)GDK_JXL_ANIMATION;
+ (void)GDK_IS_JXL_ANIMATION;
+}
+
+static gboolean gdk_pixbuf_jxl_animation_is_static_image(
+ GdkPixbufAnimation *anim) {
+ GdkPixbufJxlAnimation *jxl_anim = (GdkPixbufJxlAnimation *)anim;
+ return !jxl_anim->has_animation;
+}
+
+static GdkPixbuf *gdk_pixbuf_jxl_animation_get_static_image(
+ GdkPixbufAnimation *anim) {
+ GdkPixbufJxlAnimation *jxl_anim = (GdkPixbufJxlAnimation *)anim;
+ if (jxl_anim->frames == NULL || jxl_anim->frames->len == 0) return NULL;
+ GdkPixbufJxlAnimationFrame *frame =
+ &g_array_index(jxl_anim->frames, GdkPixbufJxlAnimationFrame, 0);
+ return frame->decoded ? frame->data : NULL;
+}
+
+static void gdk_pixbuf_jxl_animation_get_size(GdkPixbufAnimation *anim,
+ int *width, int *height) {
+ GdkPixbufJxlAnimation *jxl_anim = (GdkPixbufJxlAnimation *)anim;
+ if (width) *width = jxl_anim->xsize;
+ if (height) *height = jxl_anim->ysize;
+}
+
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+static gboolean gdk_pixbuf_jxl_animation_iter_advance(
+ GdkPixbufAnimationIter *iter, const GTimeVal *current_time);
+
+static GdkPixbufAnimationIter *gdk_pixbuf_jxl_animation_get_iter(
+ GdkPixbufAnimation *anim, const GTimeVal *start_time) {
+ GdkPixbufJxlAnimationIter *iter =
+ g_object_new(GDK_TYPE_PIXBUF_JXL_ANIMATION_ITER, NULL);
+ iter->animation = (GdkPixbufJxlAnimation *)anim;
+ iter->time_offset = start_time->tv_sec * 1000ULL + start_time->tv_usec / 1000;
+ g_object_ref(iter->animation);
+ gdk_pixbuf_jxl_animation_iter_advance((GdkPixbufAnimationIter *)iter,
+ start_time);
+ return (GdkPixbufAnimationIter *)iter;
+}
+G_GNUC_END_IGNORE_DEPRECATIONS
+
+static void gdk_pixbuf_jxl_animation_finalize(GObject *obj) {
+ GdkPixbufJxlAnimation *decoder_state = (GdkPixbufJxlAnimation *)obj;
+ if (decoder_state->frames != NULL) {
+ for (size_t i = 0; i < decoder_state->frames->len; i++) {
+ g_object_unref(
+ g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame, i)
+ .data);
+ }
+ g_array_free(decoder_state->frames, /*free_segment=*/TRUE);
+ }
+ JxlResizableParallelRunnerDestroy(decoder_state->parallel_runner);
+ JxlDecoderDestroy(decoder_state->decoder);
+ cmsDeleteTransform(decoder_state->transform);
+ cmsCloseProfile(decoder_state->srgb);
+ cmsCloseProfile(decoder_state->profile);
+ cmsDeleteContext(decoder_state->context);
+ g_free(decoder_state->icc_buff);
+}
+
+static void gdk_pixbuf_jxl_animation_class_init(
+ GdkPixbufJxlAnimationClass *klass) {
+ G_OBJECT_CLASS(klass)->finalize = gdk_pixbuf_jxl_animation_finalize;
+ klass->parent_class.is_static_image =
+ gdk_pixbuf_jxl_animation_is_static_image;
+ klass->parent_class.get_static_image =
+ gdk_pixbuf_jxl_animation_get_static_image;
+ klass->parent_class.get_size = gdk_pixbuf_jxl_animation_get_size;
+ klass->parent_class.get_iter = gdk_pixbuf_jxl_animation_get_iter;
+}
+
+static void gdk_pixbuf_jxl_animation_iter_init(GdkPixbufJxlAnimationIter *obj) {
+ (void)glib_autoptr_cleanup_GdkPixbufJxlAnimationIter;
+ (void)GDK_JXL_ANIMATION_ITER;
+ (void)GDK_IS_JXL_ANIMATION_ITER;
+}
+
+static int gdk_pixbuf_jxl_animation_iter_get_delay_time(
+ GdkPixbufAnimationIter *iter) {
+ GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter;
+ if (jxl_iter->animation->frames->len <= jxl_iter->current_frame) {
+ return 0;
+ }
+ return g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame,
+ jxl_iter->current_frame)
+ .duration_ms;
+}
+
+static GdkPixbuf *gdk_pixbuf_jxl_animation_iter_get_pixbuf(
+ GdkPixbufAnimationIter *iter) {
+ GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter;
+ if (jxl_iter->animation->frames->len <= jxl_iter->current_frame) {
+ return NULL;
+ }
+ return g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame,
+ jxl_iter->current_frame)
+ .data;
+}
+
+static gboolean gdk_pixbuf_jxl_animation_iter_on_currently_loading_frame(
+ GdkPixbufAnimationIter *iter) {
+ GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter;
+ if (jxl_iter->animation->frames->len <= jxl_iter->current_frame) {
+ return TRUE;
+ }
+ return !g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame,
+ jxl_iter->current_frame)
+ .decoded;
+}
+
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+static gboolean gdk_pixbuf_jxl_animation_iter_advance(
+ GdkPixbufAnimationIter *iter, const GTimeVal *current_time) {
+ GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter;
+ size_t old_frame = jxl_iter->current_frame;
+
+ uint64_t current_time_ms = current_time->tv_sec * 1000ULL +
+ current_time->tv_usec / 1000 -
+ jxl_iter->time_offset;
+
+ if (jxl_iter->animation->frames->len == 0) {
+ jxl_iter->current_frame = 0;
+ } else if (!jxl_iter->animation->done &&
+ current_time_ms >= jxl_iter->animation->total_duration_ms) {
+ jxl_iter->current_frame = jxl_iter->animation->frames->len - 1;
+ } else if (jxl_iter->animation->repetition_count != 0 &&
+ current_time_ms > jxl_iter->animation->repetition_count *
+ jxl_iter->animation->total_duration_ms) {
+ jxl_iter->current_frame = jxl_iter->animation->frames->len - 1;
+ } else {
+ uint64_t total_duration_ms = jxl_iter->animation->total_duration_ms;
+ // Guard against divide-by-0 in malicious files.
+ if (total_duration_ms == 0) total_duration_ms = 1;
+ uint64_t loop_offset = current_time_ms % total_duration_ms;
+ jxl_iter->current_frame = 0;
+ while (TRUE) {
+ uint64_t duration =
+ g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame,
+ jxl_iter->current_frame)
+ .duration_ms;
+ if (duration >= loop_offset) {
+ break;
+ }
+ loop_offset -= duration;
+ jxl_iter->current_frame++;
+ }
+ }
+
+ return old_frame != jxl_iter->current_frame;
+}
+G_GNUC_END_IGNORE_DEPRECATIONS
+
+static void gdk_pixbuf_jxl_animation_iter_finalize(GObject *obj) {
+ GdkPixbufJxlAnimationIter *iter = (GdkPixbufJxlAnimationIter *)obj;
+ g_object_unref(iter->animation);
+}
+
+static void gdk_pixbuf_jxl_animation_iter_class_init(
+ GdkPixbufJxlAnimationIterClass *klass) {
+ G_OBJECT_CLASS(klass)->finalize = gdk_pixbuf_jxl_animation_iter_finalize;
+ klass->parent_class.get_delay_time =
+ gdk_pixbuf_jxl_animation_iter_get_delay_time;
+ klass->parent_class.get_pixbuf = gdk_pixbuf_jxl_animation_iter_get_pixbuf;
+ klass->parent_class.on_currently_loading_frame =
+ gdk_pixbuf_jxl_animation_iter_on_currently_loading_frame;
+ klass->parent_class.advance = gdk_pixbuf_jxl_animation_iter_advance;
+}
+
+G_END_DECLS
+
+static gpointer begin_load(GdkPixbufModuleSizeFunc size_func,
+ GdkPixbufModulePreparedFunc prepare_func,
+ GdkPixbufModuleUpdatedFunc update_func,
+ gpointer user_data, GError **error) {
+ GdkPixbufJxlAnimation *decoder_state =
+ g_object_new(GDK_TYPE_PIXBUF_JXL_ANIMATION, NULL);
+ if (decoder_state == NULL) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "Creation of the animation state failed");
+ return NULL;
+ }
+ decoder_state->image_size_callback = size_func;
+ decoder_state->pixbuf_prepared_callback = prepare_func;
+ decoder_state->area_updated_callback = update_func;
+ decoder_state->user_data = user_data;
+ decoder_state->frames =
+ g_array_new(/*zero_terminated=*/FALSE, /*clear_=*/TRUE,
+ sizeof(GdkPixbufJxlAnimationFrame));
+
+ if (decoder_state->frames == NULL) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "Creation of the frame array failed");
+ goto cleanup;
+ }
+
+ if (!(decoder_state->parallel_runner =
+ JxlResizableParallelRunnerCreate(NULL))) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "Creation of the JXL parallel runner failed");
+ goto cleanup;
+ }
+
+ if (!(decoder_state->decoder = JxlDecoderCreate(NULL))) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "Creation of the JXL decoder failed");
+ goto cleanup;
+ }
+
+ JxlDecoderStatus status;
+
+ if ((status = JxlDecoderSetParallelRunner(
+ decoder_state->decoder, JxlResizableParallelRunner,
+ decoder_state->parallel_runner)) != JXL_DEC_SUCCESS) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "JxlDecoderSetParallelRunner failed: %x", status);
+ goto cleanup;
+ }
+ if ((status = JxlDecoderSubscribeEvents(
+ decoder_state->decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING |
+ JXL_DEC_FULL_IMAGE | JXL_DEC_FRAME)) !=
+ JXL_DEC_SUCCESS) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "JxlDecoderSubscribeEvents failed: %x", status);
+ goto cleanup;
+ }
+
+ decoder_state->pixel_format.data_type = JXL_TYPE_FLOAT;
+ decoder_state->pixel_format.endianness = JXL_NATIVE_ENDIAN;
+
+ return decoder_state;
+cleanup:
+ JxlResizableParallelRunnerDestroy(decoder_state->parallel_runner);
+ JxlDecoderDestroy(decoder_state->decoder);
+ g_object_unref(decoder_state);
+ return NULL;
+}
+
+static gboolean stop_load(gpointer context, GError **error) {
+ g_object_unref(context);
+ return TRUE;
+}
+
+static void draw_pixels(void *context, size_t x, size_t y, size_t num_pixels,
+ const void *pixels) {
+ GdkPixbufJxlAnimation *decoder_state = context;
+
+ GdkPixbuf *output =
+ g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame,
+ decoder_state->frames->len - 1)
+ .data;
+
+ guchar *dst = gdk_pixbuf_get_pixels(output) +
+ decoder_state->pixel_format.num_channels * x +
+ gdk_pixbuf_get_rowstride(output) * y;
+
+ cmsDoTransform(decoder_state->transform, pixels, dst, num_pixels);
+}
+
+static gboolean load_increment(gpointer context, const guchar *buf, guint size,
+ GError **error) {
+ GdkPixbufJxlAnimation *decoder_state = context;
+ if (decoder_state->done == TRUE) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "JXL decoder load_increment called after end of file");
+ return FALSE;
+ }
+
+ JxlDecoderStatus status;
+
+ if ((status = JxlDecoderSetInput(decoder_state->decoder, buf, size)) !=
+ JXL_DEC_SUCCESS) {
+ // Should never happen if things are done properly.
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "JXL decoder logic error: %x", status);
+ return FALSE;
+ }
+
+ for (;;) {
+ status = JxlDecoderProcessInput(decoder_state->decoder);
+ switch (status) {
+ case JXL_DEC_NEED_MORE_INPUT: {
+ JxlDecoderReleaseInput(decoder_state->decoder);
+ return TRUE;
+ }
+
+ case JXL_DEC_BASIC_INFO: {
+ JxlBasicInfo info;
+ if (JxlDecoderGetBasicInfo(decoder_state->decoder, &info) !=
+ JXL_DEC_SUCCESS) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "JXLDecoderGetBasicInfo failed");
+ return FALSE;
+ }
+ decoder_state->pixel_format.num_channels = info.alpha_bits > 0 ? 4 : 3;
+ decoder_state->alpha_premultiplied = info.alpha_premultiplied;
+ decoder_state->xsize = info.xsize;
+ decoder_state->ysize = info.ysize;
+ decoder_state->has_animation = info.have_animation;
+ decoder_state->has_alpha = info.alpha_bits > 0;
+ if (info.have_animation) {
+ decoder_state->repetition_count = info.animation.num_loops;
+ decoder_state->tick_duration_us = 1000000ULL *
+ info.animation.tps_denominator /
+ info.animation.tps_numerator;
+ }
+ gint width = info.xsize;
+ gint height = info.ysize;
+ if (decoder_state->image_size_callback) {
+ decoder_state->image_size_callback(&width, &height,
+ decoder_state->user_data);
+ }
+
+ // GDK convention for signaling being interested only in the basic info.
+ if (width == 0 || height == 0) {
+ decoder_state->done = TRUE;
+ return TRUE;
+ }
+
+ // Set an appropriate number of threads for the image size.
+ JxlResizableParallelRunnerSetThreads(
+ decoder_state->parallel_runner,
+ JxlResizableParallelRunnerSuggestThreads(info.xsize, info.ysize));
+ break;
+ }
+
+ case JXL_DEC_COLOR_ENCODING: {
+ // Get the ICC color profile of the pixel data
+ size_t icc_size;
+ if (JXL_DEC_SUCCESS != JxlDecoderGetICCProfileSize(
+ decoder_state->decoder,
+ &decoder_state->pixel_format,
+ JXL_COLOR_PROFILE_TARGET_DATA, &icc_size)) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "JxlDecoderGetICCProfileSize failed");
+ return FALSE;
+ }
+ if (!(decoder_state->icc_buff = g_malloc(icc_size))) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "Allocating ICC profile failed");
+ return FALSE;
+ }
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderGetColorAsICCProfile(decoder_state->decoder,
+ &decoder_state->pixel_format,
+ JXL_COLOR_PROFILE_TARGET_DATA,
+ decoder_state->icc_buff, icc_size)) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "JxlDecoderGetColorAsICCProfile failed");
+ return FALSE;
+ }
+ decoder_state->context = cmsCreateContext(NULL, NULL);
+ if (!decoder_state->context) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "Failed to create LCMS2 context");
+ return FALSE;
+ }
+ decoder_state->profile = cmsOpenProfileFromMemTHR(
+ decoder_state->context, decoder_state->icc_buff, icc_size);
+ if (!decoder_state->profile) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "Invalid ICC profile from JXL image decoder");
+ return FALSE;
+ }
+ decoder_state->srgb = cmsCreate_sRGBProfileTHR(decoder_state->context);
+ if (!decoder_state->srgb) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "Failed to create sRGB profile");
+ return FALSE;
+ }
+ decoder_state->transform = cmsCreateTransformTHR(
+ decoder_state->context, decoder_state->profile,
+ decoder_state->has_alpha ? TYPE_RGBA_FLT : TYPE_RGB_FLT,
+ decoder_state->srgb,
+ decoder_state->has_alpha ? TYPE_RGBA_8 : TYPE_RGB_8,
+ INTENT_RELATIVE_COLORIMETRIC, cmsFLAGS_COPY_ALPHA);
+ if (!decoder_state->transform) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "Failed to create LCMS2 color transform");
+ return FALSE;
+ }
+
+ break;
+ }
+
+ case JXL_DEC_FRAME: {
+ // TODO(veluca): support rescaling.
+ JxlFrameHeader frame_header;
+ if (JxlDecoderGetFrameHeader(decoder_state->decoder, &frame_header) !=
+ JXL_DEC_SUCCESS) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "Failed to retrieve frame info");
+ return FALSE;
+ }
+
+ {
+ GdkPixbufJxlAnimationFrame frame;
+ frame.decoded = FALSE;
+ frame.duration_ms =
+ frame_header.duration * decoder_state->tick_duration_us / 1000;
+ decoder_state->total_duration_ms += frame.duration_ms;
+ frame.data =
+ gdk_pixbuf_new(GDK_COLORSPACE_RGB, decoder_state->has_alpha,
+ /*bits_per_sample=*/8, decoder_state->xsize,
+ decoder_state->ysize);
+ if (frame.data == NULL) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "Failed to allocate output pixel buffer");
+ return FALSE;
+ }
+ decoder_state->pixel_format.align =
+ gdk_pixbuf_get_rowstride(frame.data);
+ g_array_append_val(decoder_state->frames, frame);
+ }
+ if (decoder_state->pixbuf_prepared_callback &&
+ decoder_state->frames->len == 1) {
+ decoder_state->pixbuf_prepared_callback(
+ g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame,
+ 0)
+ .data,
+ decoder_state->has_animation ? (GdkPixbufAnimation *)decoder_state
+ : NULL,
+ decoder_state->user_data);
+ }
+ break;
+ }
+
+ case JXL_DEC_NEED_IMAGE_OUT_BUFFER: {
+ if (JXL_DEC_SUCCESS !=
+ JxlDecoderSetImageOutCallback(decoder_state->decoder,
+ &decoder_state->pixel_format,
+ draw_pixels, decoder_state)) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "JxlDecoderSetImageOutCallback failed");
+ return FALSE;
+ }
+ break;
+ }
+
+ case JXL_DEC_FULL_IMAGE: {
+ // TODO(veluca): consider doing partial updates.
+ if (decoder_state->area_updated_callback) {
+ GdkPixbuf *output = g_array_index(decoder_state->frames,
+ GdkPixbufJxlAnimationFrame, 0)
+ .data;
+ decoder_state->area_updated_callback(
+ output, 0, 0, gdk_pixbuf_get_width(output),
+ gdk_pixbuf_get_height(output), decoder_state->user_data);
+ }
+ g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame,
+ decoder_state->frames->len - 1)
+ .decoded = TRUE;
+ break;
+ }
+
+ case JXL_DEC_SUCCESS: {
+ decoder_state->done = TRUE;
+ return TRUE;
+ }
+
+ default: {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "Unexpected JxlDecoderProcessInput return code: %x",
+ status);
+ return FALSE;
+ }
+ }
+ }
+ return TRUE;
+}
+
+static gboolean jxl_is_save_option_supported(const gchar *option_key) {
+ if (g_strcmp0(option_key, "quality") == 0) {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gboolean jxl_image_saver(FILE *f, GdkPixbuf *pixbuf, gchar **keys,
+ gchar **values, GError **error) {
+ long quality = 90; /* default; must be between 0 and 100 */
+ double distance;
+ gboolean save_alpha;
+ JxlEncoder *encoder;
+ void *parallel_runner;
+ JxlEncoderFrameSettings *frame_settings;
+ JxlBasicInfo output_info;
+ JxlPixelFormat pixel_format;
+ JxlColorEncoding color_profile;
+ JxlEncoderStatus status;
+
+ GByteArray *compressed;
+ size_t offset = 0;
+ uint8_t *next_out;
+ size_t avail_out;
+
+ if (f == NULL || pixbuf == NULL) {
+ return FALSE;
+ }
+
+ if (keys && *keys) {
+ gchar **kiter = keys;
+ gchar **viter = values;
+
+ while (*kiter) {
+ if (strcmp(*kiter, "quality") == 0) {
+ char *endptr = NULL;
+ quality = strtol(*viter, &endptr, 10);
+
+ if (endptr == *viter) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_BAD_OPTION,
+ "JXL quality must be a value between 0 and 100; value "
+ "\"%s\" could not be parsed.",
+ *viter);
+
+ return FALSE;
+ }
+
+ if (quality < 0 || quality > 100) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_BAD_OPTION,
+ "JXL quality must be a value between 0 and 100; value "
+ "\"%ld\" is not allowed.",
+ quality);
+
+ return FALSE;
+ }
+ } else {
+ g_warning("Unrecognized parameter (%s) passed to JXL saver.", *kiter);
+ }
+
+ ++kiter;
+ ++viter;
+ }
+ }
+
+ if (gdk_pixbuf_get_bits_per_sample(pixbuf) != 8) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
+ "Sorry, only 8bit images are supported by this JXL saver");
+ return FALSE;
+ }
+
+ JxlEncoderInitBasicInfo(&output_info);
+ output_info.have_container = JXL_FALSE;
+ output_info.xsize = gdk_pixbuf_get_width(pixbuf);
+ output_info.ysize = gdk_pixbuf_get_height(pixbuf);
+ output_info.bits_per_sample = 8;
+ output_info.orientation = JXL_ORIENT_IDENTITY;
+ output_info.num_color_channels = 3;
+
+ if (output_info.xsize == 0 || output_info.ysize == 0) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
+ "Empty image, nothing to save");
+ return FALSE;
+ }
+
+ save_alpha = gdk_pixbuf_get_has_alpha(pixbuf);
+
+ pixel_format.data_type = JXL_TYPE_UINT8;
+ pixel_format.endianness = JXL_NATIVE_ENDIAN;
+ pixel_format.align = gdk_pixbuf_get_rowstride(pixbuf);
+
+ if (save_alpha) {
+ if (gdk_pixbuf_get_n_channels(pixbuf) != 4) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
+ "Unsupported number of channels");
+ return FALSE;
+ }
+
+ output_info.num_extra_channels = 1;
+ output_info.alpha_bits = 8;
+ pixel_format.num_channels = 4;
+ } else {
+ if (gdk_pixbuf_get_n_channels(pixbuf) != 3) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
+ "Unsupported number of channels");
+ return FALSE;
+ }
+
+ output_info.num_extra_channels = 0;
+ output_info.alpha_bits = 0;
+ pixel_format.num_channels = 3;
+ }
+
+ encoder = JxlEncoderCreate(NULL);
+ if (!encoder) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "Creation of the JXL encoder failed");
+ return FALSE;
+ }
+
+ parallel_runner = JxlResizableParallelRunnerCreate(NULL);
+ if (!parallel_runner) {
+ JxlEncoderDestroy(encoder);
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "Creation of the JXL decoder failed");
+ return FALSE;
+ }
+
+ JxlResizableParallelRunnerSetThreads(
+ parallel_runner, JxlResizableParallelRunnerSuggestThreads(
+ output_info.xsize, output_info.ysize));
+
+ status = JxlEncoderSetParallelRunner(encoder, JxlResizableParallelRunner,
+ parallel_runner);
+ if (status != JXL_ENC_SUCCESS) {
+ JxlResizableParallelRunnerDestroy(parallel_runner);
+ JxlEncoderDestroy(encoder);
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "JxlDecoderSetParallelRunner failed: %x", status);
+ return FALSE;
+ }
+
+ if (quality > 99) {
+ output_info.uses_original_profile = JXL_TRUE;
+ distance = 0;
+ } else {
+ output_info.uses_original_profile = JXL_FALSE;
+ if (quality >= 30) {
+ distance = 0.1 + (100 - quality) * 0.09;
+ } else {
+ distance =
+ 53.0 / 3000.0 * quality * quality - 23.0 / 20.0 * quality + 25.0;
+ }
+ }
+
+ status = JxlEncoderSetBasicInfo(encoder, &output_info);
+ if (status != JXL_ENC_SUCCESS) {
+ JxlResizableParallelRunnerDestroy(parallel_runner);
+ JxlEncoderDestroy(encoder);
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "JxlEncoderSetBasicInfo failed: %x", status);
+ return FALSE;
+ }
+
+ JxlColorEncodingSetToSRGB(&color_profile, JXL_FALSE);
+ status = JxlEncoderSetColorEncoding(encoder, &color_profile);
+ if (status != JXL_ENC_SUCCESS) {
+ JxlResizableParallelRunnerDestroy(parallel_runner);
+ JxlEncoderDestroy(encoder);
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "JxlEncoderSetColorEncoding failed: %x", status);
+ return FALSE;
+ }
+
+ frame_settings = JxlEncoderFrameSettingsCreate(encoder, NULL);
+ JxlEncoderSetFrameDistance(frame_settings, distance);
+ JxlEncoderSetFrameLossless(frame_settings, output_info.uses_original_profile);
+
+ status = JxlEncoderAddImageFrame(frame_settings, &pixel_format,
+ gdk_pixbuf_read_pixels(pixbuf),
+ gdk_pixbuf_get_byte_length(pixbuf));
+ if (status != JXL_ENC_SUCCESS) {
+ JxlResizableParallelRunnerDestroy(parallel_runner);
+ JxlEncoderDestroy(encoder);
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "JxlEncoderAddImageFrame failed: %x", status);
+ return FALSE;
+ }
+
+ JxlEncoderCloseInput(encoder);
+
+ compressed = g_byte_array_sized_new(4096);
+ g_byte_array_set_size(compressed, 4096);
+ do {
+ next_out = compressed->data + offset;
+ avail_out = compressed->len - offset;
+ status = JxlEncoderProcessOutput(encoder, &next_out, &avail_out);
+
+ if (status == JXL_ENC_NEED_MORE_OUTPUT) {
+ offset = next_out - compressed->data;
+ g_byte_array_set_size(compressed, compressed->len * 2);
+ } else if (status == JXL_ENC_ERROR) {
+ JxlResizableParallelRunnerDestroy(parallel_runner);
+ JxlEncoderDestroy(encoder);
+ g_set_error(error, G_FILE_ERROR, 0, "JxlEncoderProcessOutput failed: %x",
+ status);
+ return FALSE;
+ }
+ } while (status != JXL_ENC_SUCCESS);
+
+ JxlResizableParallelRunnerDestroy(parallel_runner);
+ JxlEncoderDestroy(encoder);
+
+ g_byte_array_set_size(compressed, next_out - compressed->data);
+ if (compressed->len > 0) {
+ fwrite(compressed->data, 1, compressed->len, f);
+ g_byte_array_free(compressed, TRUE);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+void fill_vtable(GdkPixbufModule *module) {
+ module->begin_load = begin_load;
+ module->stop_load = stop_load;
+ module->load_increment = load_increment;
+ module->is_save_option_supported = jxl_is_save_option_supported;
+ module->save = jxl_image_saver;
+}
+
+void fill_info(GdkPixbufFormat *info) {
+ static GdkPixbufModulePattern signature[] = {
+ {"\xFF\x0A", " ", 100},
+ {"...\x0CJXL \x0D\x0A\x87\x0A", "zzz ", 100},
+ {NULL, NULL, 0},
+ };
+
+ static gchar *mime_types[] = {"image/jxl", NULL};
+
+ static gchar *extensions[] = {"jxl", NULL};
+
+ info->name = "jxl";
+ info->signature = signature;
+ info->description = "JPEG XL image";
+ info->mime_types = mime_types;
+ info->extensions = extensions;
+ info->flags = GDK_PIXBUF_FORMAT_WRITABLE | GDK_PIXBUF_FORMAT_THREADSAFE;
+ info->license = "BSD-3";
+}
diff --git a/third_party/jpeg-xl/plugins/gdk-pixbuf/pixbufloader_test.cc b/third_party/jpeg-xl/plugins/gdk-pixbuf/pixbufloader_test.cc
new file mode 100644
index 0000000000..5e5642d491
--- /dev/null
+++ b/third_party/jpeg-xl/plugins/gdk-pixbuf/pixbufloader_test.cc
@@ -0,0 +1,41 @@
+// 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 <gdk-pixbuf/gdk-pixbuf.h>
+#include <gdk/gdk.h>
+#include <glib.h>
+#include <stdlib.h>
+
+int main(int argc, char* argv[]) {
+ if (argc != 3) {
+ fprintf(stderr, "Usage: %s <loaders.cache> <image.jxl>\n", argv[0]);
+ return 1;
+ }
+
+ const char* loaders_cache = argv[1];
+ const char* filename = argv[2];
+ setenv("GDK_PIXBUF_MODULE_FILE", loaders_cache, true);
+
+ // XDG_DATA_HOME is the path where we look for the mime cache.
+ // XDG_DATA_DIRS directories are used in addition to XDG_DATA_HOME.
+ setenv("XDG_DATA_HOME", ".", true);
+ setenv("XDG_DATA_DIRS", "", true);
+
+ if (!gdk_init_check(nullptr, nullptr)) {
+ fprintf(stderr, "This test requires a DISPLAY\n");
+ // Signals ctest that we should mark this test as skipped.
+ return 254;
+ }
+ GError* error = nullptr;
+ GdkPixbuf* pb = gdk_pixbuf_new_from_file(filename, &error);
+ if (pb != nullptr) {
+ g_object_unref(pb);
+ return 0;
+ } else {
+ fprintf(stderr, "Error loading file: %s\n", filename);
+ g_assert_no_error(error);
+ return 1;
+ }
+}
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()
diff --git a/third_party/jpeg-xl/plugins/mime/CMakeLists.txt b/third_party/jpeg-xl/plugins/mime/CMakeLists.txt
new file mode 100644
index 0000000000..6f2a0f919c
--- /dev/null
+++ b/third_party/jpeg-xl/plugins/mime/CMakeLists.txt
@@ -0,0 +1,6 @@
+# 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.
+
+install(FILES image-jxl.xml DESTINATION share/mime/packages/)
diff --git a/third_party/jpeg-xl/plugins/mime/README.md b/third_party/jpeg-xl/plugins/mime/README.md
new file mode 100644
index 0000000000..4d398c7b90
--- /dev/null
+++ b/third_party/jpeg-xl/plugins/mime/README.md
@@ -0,0 +1,37 @@
+## :warning: Not needed anymore
+
+As `image/jxl` is now supported by [shared-mine-info 2.2](https://gitlab.freedesktop.org/xdg/shared-mime-info/-/releases/2.2), it should not be necessary anymore to install this plugin.
+
+You can test if your system correctly understand the MIME type of JPEG XL image by obtaining a JPEG XL image, e.g. with
+```bash
+wget https://raw.githubusercontent.com/libjxl/conformance/master/testcases/bicycles/input.jxl
+```
+and with that sample JPEG XL file `input.jxl` (or any other valid JPEG XL file), run any of the following commands:
+```bash
+xdg-mime query filetype input.jxl
+file --mime-type input.jxl
+mimetype input.jxl
+```
+If the output contains `image/jxl` you are all set!
+
+
+## JPEG XL MIME type
+
+If not already installed by the [Installing section of BUILDING.md](../../BUILDING.md#installing), then it can be done manually:
+
+### Install
+```bash
+sudo xdg-mime install --novendor image-jxl.xml
+```
+
+Then run:
+```
+update-mime --local
+```
+
+
+### Uninstall
+```bash
+sudo xdg-mime uninstall image-jxl.xml
+```
+
diff --git a/third_party/jpeg-xl/plugins/mime/image-jxl.xml b/third_party/jpeg-xl/plugins/mime/image-jxl.xml
new file mode 100644
index 0000000000..cab9018c7d
--- /dev/null
+++ b/third_party/jpeg-xl/plugins/mime/image-jxl.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
+ <mime-type type="image/jxl">
+ <comment>JPEG XL image</comment>
+ <comment xml:lang="fr">image JPEG XL</comment>
+ <comment xml:lang="nl">JPEG XL afbeelding</comment>
+ <magic priority="50">
+ <match type="string" offset="0" value="\xFF\x0A"/>
+ <match type="string" offset="0" value="\0\0\0\x0CJXL \x0D\x0A\x87\x0A"/>
+ </magic>
+ <glob pattern="*.jxl"/>
+ </mime-type>
+</mime-info>