// 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 #include #include #include #include #define GDK_PIXBUF_ENABLE_BACKEND #include #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"; }