summaryrefslogtreecommitdiffstats
path: root/third_party/jpeg-xl/plugins/gdk-pixbuf
diff options
context:
space:
mode:
Diffstat (limited to '')
-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.c783
-rw-r--r--third_party/jpeg-xl/plugins/gdk-pixbuf/pixbufloader_test.cc41
6 files changed, 977 insertions, 0 deletions
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..12c1a83753
--- /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"
+ "${JPEGXL_TEST_DATA_PATH}/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..bafa57b167
--- /dev/null
+++ b/third_party/jpeg-xl/plugins/gdk-pixbuf/pixbufloader-jxl.c
@@ -0,0 +1,783 @@
+// 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>
+
+#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
+
+ gchar *icc_base64;
+};
+
+#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);
+ g_free(decoder_state->icc_base64);
+}
+
+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 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
+ gpointer icc_buff;
+ size_t icc_size;
+ JxlColorEncoding color_encoding;
+ if (JXL_DEC_SUCCESS == JxlDecoderGetColorAsEncodedProfile(
+ decoder_state->decoder,
+ JXL_COLOR_PROFILE_TARGET_ORIGINAL,
+ &color_encoding)) {
+ // we don't check the return status here because it's not a problem if
+ // this fails
+ JxlDecoderSetPreferredColorProfile(decoder_state->decoder,
+ &color_encoding);
+ }
+ if (JXL_DEC_SUCCESS != JxlDecoderGetICCProfileSize(
+ decoder_state->decoder,
+ JXL_COLOR_PROFILE_TARGET_DATA, &icc_size)) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "JxlDecoderGetICCProfileSize failed");
+ return FALSE;
+ }
+ if (!(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,
+ JXL_COLOR_PROFILE_TARGET_DATA,
+ icc_buff, icc_size)) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "JxlDecoderGetColorAsICCProfile failed");
+ g_free(icc_buff);
+ return FALSE;
+ }
+ decoder_state->icc_base64 = g_base64_encode(icc_buff, icc_size);
+ g_free(icc_buff);
+ if (!decoder_state->icc_base64) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "Allocating ICC profile base64 string failed");
+ 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;
+ }
+ gdk_pixbuf_set_option(frame.data, "icc-profile",
+ decoder_state->icc_base64);
+ decoder_state->pixel_format.align =
+ gdk_pixbuf_get_rowstride(frame.data);
+ decoder_state->pixel_format.data_type = JXL_TYPE_UINT8;
+ 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: {
+ GdkPixbuf *output =
+ g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame,
+ decoder_state->frames->len - 1)
+ .data;
+ decoder_state->pixel_format.align = gdk_pixbuf_get_rowstride(output);
+ guchar *dst = gdk_pixbuf_get_pixels(output);
+ size_t num_pixels = decoder_state->xsize * decoder_state->ysize;
+ size_t size = num_pixels * decoder_state->pixel_format.num_channels;
+ if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(
+ decoder_state->decoder,
+ &decoder_state->pixel_format, dst, size)) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "JxlDecoderSetImageOutBuffer 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;
+ distance = JxlEncoderDistanceFromQuality((float)quality);
+ }
+
+ 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;
+ }
+}