summaryrefslogtreecommitdiff
path: root/media/libjxl/src/plugins/gdk-pixbuf
diff options
context:
space:
mode:
Diffstat (limited to 'media/libjxl/src/plugins/gdk-pixbuf')
-rw-r--r--media/libjxl/src/plugins/gdk-pixbuf/CMakeLists.txt80
-rw-r--r--media/libjxl/src/plugins/gdk-pixbuf/README.md50
-rw-r--r--media/libjxl/src/plugins/gdk-pixbuf/jxl.thumbnailer4
-rw-r--r--media/libjxl/src/plugins/gdk-pixbuf/loaders_test.cache16
-rw-r--r--media/libjxl/src/plugins/gdk-pixbuf/pixbufloader-jxl.c569
-rw-r--r--media/libjxl/src/plugins/gdk-pixbuf/pixbufloader_test.cc41
6 files changed, 760 insertions, 0 deletions
diff --git a/media/libjxl/src/plugins/gdk-pixbuf/CMakeLists.txt b/media/libjxl/src/plugins/gdk-pixbuf/CMakeLists.txt
new file mode 100644
index 0000000000..1d595008a5
--- /dev/null
+++ b/media/libjxl/src/plugins/gdk-pixbuf/CMakeLists.txt
@@ -0,0 +1,80 @@
+# 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)
+
+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 SHARED 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 skcms-interface 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 LIBRARY 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 share/thumbnailers/)
+
+if(BUILD_TESTING AND NOT MINGW)
+ 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.
+ if (NOT (SANITIZER STREQUAL "msan"))
+ add_test(
+ NAME pixbufloader_test_jxl
+ COMMAND
+ ${XVFB_PROGRAM_PREFIX} $<TARGET_FILE:pixbufloader_test>
+ "${CMAKE_CURRENT_SOURCE_DIR}/loaders_test.cache"
+ "${CMAKE_SOURCE_DIR}/third_party/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/media/libjxl/src/plugins/gdk-pixbuf/README.md b/media/libjxl/src/plugins/gdk-pixbuf/README.md
new file mode 100644
index 0000000000..f7174baf3c
--- /dev/null
+++ b/media/libjxl/src/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 README.md](../../README.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/media/libjxl/src/plugins/gdk-pixbuf/jxl.thumbnailer b/media/libjxl/src/plugins/gdk-pixbuf/jxl.thumbnailer
new file mode 100644
index 0000000000..1bcaab61fc
--- /dev/null
+++ b/media/libjxl/src/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/media/libjxl/src/plugins/gdk-pixbuf/loaders_test.cache b/media/libjxl/src/plugins/gdk-pixbuf/loaders_test.cache
new file mode 100644
index 0000000000..95c62c8fc3
--- /dev/null
+++ b/media/libjxl/src/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/media/libjxl/src/plugins/gdk-pixbuf/pixbufloader-jxl.c b/media/libjxl/src/plugins/gdk-pixbuf/pixbufloader-jxl.c
new file mode 100644
index 0000000000..24bbcf8cfe
--- /dev/null
+++ b/media/libjxl/src/plugins/gdk-pixbuf/pixbufloader-jxl.c
@@ -0,0 +1,569 @@
+// 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/resizable_parallel_runner.h"
+#include "jxl/types.h"
+#include "skcms.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
+
+ // ICC profile, to which `icc` might refer to.
+ gpointer icc_buff;
+ skcms_ICCProfile icc;
+};
+
+#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_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;
+ gboolean has_alpha = decoder_state->pixel_format.num_channels == 4;
+
+ 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;
+
+ skcms_Transform(
+ pixels,
+ has_alpha ? skcms_PixelFormat_RGBA_ffff : skcms_PixelFormat_RGB_fff,
+ decoder_state->alpha_premultiplied ? skcms_AlphaFormat_PremulAsEncoded
+ : skcms_AlphaFormat_Unpremul,
+ &decoder_state->icc, dst,
+ has_alpha ? skcms_PixelFormat_RGBA_8888 : skcms_PixelFormat_RGB_888,
+ skcms_AlphaFormat_Unpremul, skcms_sRGB_profile(), 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;
+ }
+ if (!skcms_Parse(decoder_state->icc_buff, icc_size,
+ &decoder_state->icc)) {
+ g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
+ "Invalid ICC profile from JXL image decoder");
+ 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;
+}
+
+void fill_vtable(GdkPixbufModule *module) {
+ module->begin_load = begin_load;
+ module->stop_load = stop_load;
+ module->load_increment = load_increment;
+ // TODO(veluca): implement saving.
+}
+
+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;
+ // TODO(veluca): add writing support.
+ info->flags = GDK_PIXBUF_FORMAT_THREADSAFE;
+ info->license = "BSD-3";
+}
diff --git a/media/libjxl/src/plugins/gdk-pixbuf/pixbufloader_test.cc b/media/libjxl/src/plugins/gdk-pixbuf/pixbufloader_test.cc
new file mode 100644
index 0000000000..5e5642d491
--- /dev/null
+++ b/media/libjxl/src/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;
+ }
+}