libjxl

FORK: libjxl patches used on blog
git clone https://git.neptards.moe/blog/libjxl.git
Log | Files | Refs | Submodules | README | LICENSE

pixbufloader-jxl.c (28488B)


      1 // Copyright (c) the JPEG XL Project Authors. All rights reserved.
      2 //
      3 // Use of this source code is governed by a BSD-style
      4 // license that can be found in the LICENSE file.
      5 
      6 #include <jxl/codestream_header.h>
      7 #include <jxl/decode.h>
      8 #include <jxl/encode.h>
      9 #include <jxl/resizable_parallel_runner.h>
     10 #include <jxl/types.h>
     11 
     12 #define GDK_PIXBUF_ENABLE_BACKEND
     13 #include <gdk-pixbuf/gdk-pixbuf.h>
     14 #undef GDK_PIXBUF_ENABLE_BACKEND
     15 
     16 G_BEGIN_DECLS
     17 
     18 // Information about a single frame.
     19 typedef struct {
     20   uint64_t duration_ms;
     21   GdkPixbuf *data;
     22   gboolean decoded;
     23 } GdkPixbufJxlAnimationFrame;
     24 
     25 // Represent a whole JPEG XL animation; all its fields are owned; as a GObject,
     26 // the Animation struct itself is reference counted (as are the GdkPixbufs for
     27 // individual frames).
     28 struct _GdkPixbufJxlAnimation {
     29   GdkPixbufAnimation parent_instance;
     30 
     31   // GDK interface implementation callbacks.
     32   GdkPixbufModuleSizeFunc image_size_callback;
     33   GdkPixbufModulePreparedFunc pixbuf_prepared_callback;
     34   GdkPixbufModuleUpdatedFunc area_updated_callback;
     35   gpointer user_data;
     36 
     37   // All frames known so far; a frame is added when the JXL_DEC_FRAME event is
     38   // received from the decoder; initially frame.decoded is FALSE, until
     39   // the JXL_DEC_IMAGE event is received.
     40   GArray *frames;
     41 
     42   // JPEG XL decoder and related structures.
     43   JxlParallelRunner *parallel_runner;
     44   JxlDecoder *decoder;
     45   JxlPixelFormat pixel_format;
     46 
     47   // Decoding is `done` when JXL_DEC_SUCCESS is received; calling
     48   // load_increment afterwards gives an error.
     49   gboolean done;
     50 
     51   // Image information.
     52   size_t xsize;
     53   size_t ysize;
     54   gboolean alpha_premultiplied;
     55   gboolean has_animation;
     56   gboolean has_alpha;
     57   uint64_t total_duration_ms;
     58   uint64_t tick_duration_us;
     59   uint64_t repetition_count;  // 0 = loop forever
     60 
     61   gchar *icc_base64;
     62 };
     63 
     64 #define GDK_TYPE_PIXBUF_JXL_ANIMATION (gdk_pixbuf_jxl_animation_get_type())
     65 G_DECLARE_FINAL_TYPE(GdkPixbufJxlAnimation, gdk_pixbuf_jxl_animation, GDK,
     66                      JXL_ANIMATION, GdkPixbufAnimation);
     67 
     68 G_DEFINE_TYPE(GdkPixbufJxlAnimation, gdk_pixbuf_jxl_animation,
     69               GDK_TYPE_PIXBUF_ANIMATION);
     70 
     71 // Iterator to a given point in time in the animation; contains a pointer to the
     72 // full animation.
     73 struct _GdkPixbufJxlAnimationIter {
     74   GdkPixbufAnimationIter parent_instance;
     75   GdkPixbufJxlAnimation *animation;
     76   size_t current_frame;
     77   uint64_t time_offset;
     78 };
     79 
     80 #define GDK_TYPE_PIXBUF_JXL_ANIMATION_ITER \
     81   (gdk_pixbuf_jxl_animation_iter_get_type())
     82 G_DECLARE_FINAL_TYPE(GdkPixbufJxlAnimationIter, gdk_pixbuf_jxl_animation_iter,
     83                      GDK, JXL_ANIMATION_ITER, GdkPixbufAnimationIter);
     84 G_DEFINE_TYPE(GdkPixbufJxlAnimationIter, gdk_pixbuf_jxl_animation_iter,
     85               GDK_TYPE_PIXBUF_ANIMATION_ITER);
     86 
     87 static void gdk_pixbuf_jxl_animation_init(GdkPixbufJxlAnimation *obj) {
     88   // Suppress "unused function" warnings.
     89   (void)glib_autoptr_cleanup_GdkPixbufJxlAnimation;
     90   (void)GDK_JXL_ANIMATION;
     91   (void)GDK_IS_JXL_ANIMATION;
     92 }
     93 
     94 static gboolean gdk_pixbuf_jxl_animation_is_static_image(
     95     GdkPixbufAnimation *anim) {
     96   GdkPixbufJxlAnimation *jxl_anim = (GdkPixbufJxlAnimation *)anim;
     97   return !jxl_anim->has_animation;
     98 }
     99 
    100 static GdkPixbuf *gdk_pixbuf_jxl_animation_get_static_image(
    101     GdkPixbufAnimation *anim) {
    102   GdkPixbufJxlAnimation *jxl_anim = (GdkPixbufJxlAnimation *)anim;
    103   if (jxl_anim->frames == NULL || jxl_anim->frames->len == 0) return NULL;
    104   GdkPixbufJxlAnimationFrame *frame =
    105       &g_array_index(jxl_anim->frames, GdkPixbufJxlAnimationFrame, 0);
    106   return frame->decoded ? frame->data : NULL;
    107 }
    108 
    109 static void gdk_pixbuf_jxl_animation_get_size(GdkPixbufAnimation *anim,
    110                                               int *width, int *height) {
    111   GdkPixbufJxlAnimation *jxl_anim = (GdkPixbufJxlAnimation *)anim;
    112   if (width) *width = jxl_anim->xsize;
    113   if (height) *height = jxl_anim->ysize;
    114 }
    115 
    116 G_GNUC_BEGIN_IGNORE_DEPRECATIONS
    117 static gboolean gdk_pixbuf_jxl_animation_iter_advance(
    118     GdkPixbufAnimationIter *iter, const GTimeVal *current_time);
    119 
    120 static GdkPixbufAnimationIter *gdk_pixbuf_jxl_animation_get_iter(
    121     GdkPixbufAnimation *anim, const GTimeVal *start_time) {
    122   GdkPixbufJxlAnimationIter *iter =
    123       g_object_new(GDK_TYPE_PIXBUF_JXL_ANIMATION_ITER, NULL);
    124   iter->animation = (GdkPixbufJxlAnimation *)anim;
    125   iter->time_offset = start_time->tv_sec * 1000ULL + start_time->tv_usec / 1000;
    126   g_object_ref(iter->animation);
    127   gdk_pixbuf_jxl_animation_iter_advance((GdkPixbufAnimationIter *)iter,
    128                                         start_time);
    129   return (GdkPixbufAnimationIter *)iter;
    130 }
    131 G_GNUC_END_IGNORE_DEPRECATIONS
    132 
    133 static void gdk_pixbuf_jxl_animation_finalize(GObject *obj) {
    134   GdkPixbufJxlAnimation *decoder_state = (GdkPixbufJxlAnimation *)obj;
    135   if (decoder_state->frames != NULL) {
    136     for (size_t i = 0; i < decoder_state->frames->len; i++) {
    137       g_object_unref(
    138           g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame, i)
    139               .data);
    140     }
    141     g_array_free(decoder_state->frames, /*free_segment=*/TRUE);
    142   }
    143   JxlResizableParallelRunnerDestroy(decoder_state->parallel_runner);
    144   JxlDecoderDestroy(decoder_state->decoder);
    145   g_free(decoder_state->icc_base64);
    146 }
    147 
    148 static void gdk_pixbuf_jxl_animation_class_init(
    149     GdkPixbufJxlAnimationClass *klass) {
    150   G_OBJECT_CLASS(klass)->finalize = gdk_pixbuf_jxl_animation_finalize;
    151   klass->parent_class.is_static_image =
    152       gdk_pixbuf_jxl_animation_is_static_image;
    153   klass->parent_class.get_static_image =
    154       gdk_pixbuf_jxl_animation_get_static_image;
    155   klass->parent_class.get_size = gdk_pixbuf_jxl_animation_get_size;
    156   klass->parent_class.get_iter = gdk_pixbuf_jxl_animation_get_iter;
    157 }
    158 
    159 static void gdk_pixbuf_jxl_animation_iter_init(GdkPixbufJxlAnimationIter *obj) {
    160   (void)glib_autoptr_cleanup_GdkPixbufJxlAnimationIter;
    161   (void)GDK_JXL_ANIMATION_ITER;
    162   (void)GDK_IS_JXL_ANIMATION_ITER;
    163 }
    164 
    165 static int gdk_pixbuf_jxl_animation_iter_get_delay_time(
    166     GdkPixbufAnimationIter *iter) {
    167   GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter;
    168   if (jxl_iter->animation->frames->len <= jxl_iter->current_frame) {
    169     return 0;
    170   }
    171   return g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame,
    172                        jxl_iter->current_frame)
    173       .duration_ms;
    174 }
    175 
    176 static GdkPixbuf *gdk_pixbuf_jxl_animation_iter_get_pixbuf(
    177     GdkPixbufAnimationIter *iter) {
    178   GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter;
    179   if (jxl_iter->animation->frames->len <= jxl_iter->current_frame) {
    180     return NULL;
    181   }
    182   return g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame,
    183                        jxl_iter->current_frame)
    184       .data;
    185 }
    186 
    187 static gboolean gdk_pixbuf_jxl_animation_iter_on_currently_loading_frame(
    188     GdkPixbufAnimationIter *iter) {
    189   GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter;
    190   if (jxl_iter->animation->frames->len <= jxl_iter->current_frame) {
    191     return TRUE;
    192   }
    193   return !g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame,
    194                         jxl_iter->current_frame)
    195               .decoded;
    196 }
    197 
    198 G_GNUC_BEGIN_IGNORE_DEPRECATIONS
    199 static gboolean gdk_pixbuf_jxl_animation_iter_advance(
    200     GdkPixbufAnimationIter *iter, const GTimeVal *current_time) {
    201   GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter;
    202   size_t old_frame = jxl_iter->current_frame;
    203 
    204   uint64_t current_time_ms = current_time->tv_sec * 1000ULL +
    205                              current_time->tv_usec / 1000 -
    206                              jxl_iter->time_offset;
    207 
    208   if (jxl_iter->animation->frames->len == 0) {
    209     jxl_iter->current_frame = 0;
    210   } else if (!jxl_iter->animation->done &&
    211              current_time_ms >= jxl_iter->animation->total_duration_ms) {
    212     jxl_iter->current_frame = jxl_iter->animation->frames->len - 1;
    213   } else if (jxl_iter->animation->repetition_count != 0 &&
    214              current_time_ms > jxl_iter->animation->repetition_count *
    215                                    jxl_iter->animation->total_duration_ms) {
    216     jxl_iter->current_frame = jxl_iter->animation->frames->len - 1;
    217   } else {
    218     uint64_t total_duration_ms = jxl_iter->animation->total_duration_ms;
    219     // Guard against divide-by-0 in malicious files.
    220     if (total_duration_ms == 0) total_duration_ms = 1;
    221     uint64_t loop_offset = current_time_ms % total_duration_ms;
    222     jxl_iter->current_frame = 0;
    223     while (TRUE) {
    224       uint64_t duration =
    225           g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame,
    226                         jxl_iter->current_frame)
    227               .duration_ms;
    228       if (duration >= loop_offset) {
    229         break;
    230       }
    231       loop_offset -= duration;
    232       jxl_iter->current_frame++;
    233     }
    234   }
    235 
    236   return old_frame != jxl_iter->current_frame;
    237 }
    238 G_GNUC_END_IGNORE_DEPRECATIONS
    239 
    240 static void gdk_pixbuf_jxl_animation_iter_finalize(GObject *obj) {
    241   GdkPixbufJxlAnimationIter *iter = (GdkPixbufJxlAnimationIter *)obj;
    242   g_object_unref(iter->animation);
    243 }
    244 
    245 static void gdk_pixbuf_jxl_animation_iter_class_init(
    246     GdkPixbufJxlAnimationIterClass *klass) {
    247   G_OBJECT_CLASS(klass)->finalize = gdk_pixbuf_jxl_animation_iter_finalize;
    248   klass->parent_class.get_delay_time =
    249       gdk_pixbuf_jxl_animation_iter_get_delay_time;
    250   klass->parent_class.get_pixbuf = gdk_pixbuf_jxl_animation_iter_get_pixbuf;
    251   klass->parent_class.on_currently_loading_frame =
    252       gdk_pixbuf_jxl_animation_iter_on_currently_loading_frame;
    253   klass->parent_class.advance = gdk_pixbuf_jxl_animation_iter_advance;
    254 }
    255 
    256 G_END_DECLS
    257 
    258 static gpointer begin_load(GdkPixbufModuleSizeFunc size_func,
    259                            GdkPixbufModulePreparedFunc prepare_func,
    260                            GdkPixbufModuleUpdatedFunc update_func,
    261                            gpointer user_data, GError **error) {
    262   GdkPixbufJxlAnimation *decoder_state =
    263       g_object_new(GDK_TYPE_PIXBUF_JXL_ANIMATION, NULL);
    264   if (decoder_state == NULL) {
    265     g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    266                 "Creation of the animation state failed");
    267     return NULL;
    268   }
    269   decoder_state->image_size_callback = size_func;
    270   decoder_state->pixbuf_prepared_callback = prepare_func;
    271   decoder_state->area_updated_callback = update_func;
    272   decoder_state->user_data = user_data;
    273   decoder_state->frames =
    274       g_array_new(/*zero_terminated=*/FALSE, /*clear_=*/TRUE,
    275                   sizeof(GdkPixbufJxlAnimationFrame));
    276 
    277   if (decoder_state->frames == NULL) {
    278     g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    279                 "Creation of the frame array failed");
    280     goto cleanup;
    281   }
    282 
    283   if (!(decoder_state->parallel_runner =
    284             JxlResizableParallelRunnerCreate(NULL))) {
    285     g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    286                 "Creation of the JXL parallel runner failed");
    287     goto cleanup;
    288   }
    289 
    290   if (!(decoder_state->decoder = JxlDecoderCreate(NULL))) {
    291     g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    292                 "Creation of the JXL decoder failed");
    293     goto cleanup;
    294   }
    295 
    296   JxlDecoderStatus status;
    297 
    298   if ((status = JxlDecoderSetParallelRunner(
    299            decoder_state->decoder, JxlResizableParallelRunner,
    300            decoder_state->parallel_runner)) != JXL_DEC_SUCCESS) {
    301     g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    302                 "JxlDecoderSetParallelRunner failed: %x", status);
    303     goto cleanup;
    304   }
    305   if ((status = JxlDecoderSubscribeEvents(
    306            decoder_state->decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING |
    307                                        JXL_DEC_FULL_IMAGE | JXL_DEC_FRAME)) !=
    308       JXL_DEC_SUCCESS) {
    309     g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    310                 "JxlDecoderSubscribeEvents failed: %x", status);
    311     goto cleanup;
    312   }
    313 
    314   decoder_state->pixel_format.data_type = JXL_TYPE_FLOAT;
    315   decoder_state->pixel_format.endianness = JXL_NATIVE_ENDIAN;
    316 
    317   return decoder_state;
    318 cleanup:
    319   JxlResizableParallelRunnerDestroy(decoder_state->parallel_runner);
    320   JxlDecoderDestroy(decoder_state->decoder);
    321   g_object_unref(decoder_state);
    322   return NULL;
    323 }
    324 
    325 static gboolean stop_load(gpointer context, GError **error) {
    326   g_object_unref(context);
    327   return TRUE;
    328 }
    329 
    330 static gboolean load_increment(gpointer context, const guchar *buf, guint size,
    331                                GError **error) {
    332   GdkPixbufJxlAnimation *decoder_state = context;
    333   if (decoder_state->done == TRUE) {
    334     g_warning_once("Trailing data found at end of JXL file");
    335     return TRUE;
    336   }
    337 
    338   JxlDecoderStatus status;
    339 
    340   if ((status = JxlDecoderSetInput(decoder_state->decoder, buf, size)) !=
    341       JXL_DEC_SUCCESS) {
    342     // Should never happen if things are done properly.
    343     g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    344                 "JXL decoder logic error: %x", status);
    345     return FALSE;
    346   }
    347 
    348   for (;;) {
    349     status = JxlDecoderProcessInput(decoder_state->decoder);
    350     switch (status) {
    351       case JXL_DEC_NEED_MORE_INPUT: {
    352         JxlDecoderReleaseInput(decoder_state->decoder);
    353         return TRUE;
    354       }
    355 
    356       case JXL_DEC_BASIC_INFO: {
    357         JxlBasicInfo info;
    358         if (JxlDecoderGetBasicInfo(decoder_state->decoder, &info) !=
    359             JXL_DEC_SUCCESS) {
    360           g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    361                       "JXLDecoderGetBasicInfo failed");
    362           return FALSE;
    363         }
    364         decoder_state->pixel_format.num_channels = info.alpha_bits > 0 ? 4 : 3;
    365         decoder_state->alpha_premultiplied = info.alpha_premultiplied;
    366         decoder_state->xsize = info.xsize;
    367         decoder_state->ysize = info.ysize;
    368         decoder_state->has_animation = info.have_animation;
    369         decoder_state->has_alpha = info.alpha_bits > 0;
    370         if (info.have_animation) {
    371           decoder_state->repetition_count = info.animation.num_loops;
    372           decoder_state->tick_duration_us = 1000000ULL *
    373                                             info.animation.tps_denominator /
    374                                             info.animation.tps_numerator;
    375         }
    376         gint width = info.xsize;
    377         gint height = info.ysize;
    378         if (decoder_state->image_size_callback) {
    379           decoder_state->image_size_callback(&width, &height,
    380                                              decoder_state->user_data);
    381         }
    382 
    383         // GDK convention for signaling being interested only in the basic info.
    384         if (width == 0 || height == 0) {
    385           decoder_state->done = TRUE;
    386           return TRUE;
    387         }
    388 
    389         // Set an appropriate number of threads for the image size.
    390         JxlResizableParallelRunnerSetThreads(
    391             decoder_state->parallel_runner,
    392             JxlResizableParallelRunnerSuggestThreads(info.xsize, info.ysize));
    393         break;
    394       }
    395 
    396       case JXL_DEC_COLOR_ENCODING: {
    397         // Get the ICC color profile of the pixel data
    398         gpointer icc_buff;
    399         size_t icc_size;
    400         JxlColorEncoding color_encoding;
    401         if (JXL_DEC_SUCCESS == JxlDecoderGetColorAsEncodedProfile(
    402                                    decoder_state->decoder,
    403                                    JXL_COLOR_PROFILE_TARGET_ORIGINAL,
    404                                    &color_encoding)) {
    405           // we don't check the return status here because it's not a problem if
    406           // this fails
    407           JxlDecoderSetPreferredColorProfile(decoder_state->decoder,
    408                                              &color_encoding);
    409         }
    410         if (JXL_DEC_SUCCESS != JxlDecoderGetICCProfileSize(
    411                                    decoder_state->decoder,
    412                                    JXL_COLOR_PROFILE_TARGET_DATA, &icc_size)) {
    413           g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    414                       "JxlDecoderGetICCProfileSize failed");
    415           return FALSE;
    416         }
    417         if (!(icc_buff = g_malloc(icc_size))) {
    418           g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    419                       "Allocating ICC profile failed");
    420           return FALSE;
    421         }
    422         if (JXL_DEC_SUCCESS !=
    423             JxlDecoderGetColorAsICCProfile(decoder_state->decoder,
    424                                            JXL_COLOR_PROFILE_TARGET_DATA,
    425                                            icc_buff, icc_size)) {
    426           g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    427                       "JxlDecoderGetColorAsICCProfile failed");
    428           g_free(icc_buff);
    429           return FALSE;
    430         }
    431         decoder_state->icc_base64 = g_base64_encode(icc_buff, icc_size);
    432         g_free(icc_buff);
    433         if (!decoder_state->icc_base64) {
    434           g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    435                       "Allocating ICC profile base64 string failed");
    436           return FALSE;
    437         }
    438 
    439         break;
    440       }
    441 
    442       case JXL_DEC_FRAME: {
    443         // TODO(veluca): support rescaling.
    444         JxlFrameHeader frame_header;
    445         if (JxlDecoderGetFrameHeader(decoder_state->decoder, &frame_header) !=
    446             JXL_DEC_SUCCESS) {
    447           g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    448                       "Failed to retrieve frame info");
    449           return FALSE;
    450         }
    451 
    452         {
    453           GdkPixbufJxlAnimationFrame frame;
    454           frame.decoded = FALSE;
    455           frame.duration_ms =
    456               frame_header.duration * decoder_state->tick_duration_us / 1000;
    457           decoder_state->total_duration_ms += frame.duration_ms;
    458           frame.data =
    459               gdk_pixbuf_new(GDK_COLORSPACE_RGB, decoder_state->has_alpha,
    460                              /*bits_per_sample=*/8, decoder_state->xsize,
    461                              decoder_state->ysize);
    462           if (frame.data == NULL) {
    463             g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    464                         "Failed to allocate output pixel buffer");
    465             return FALSE;
    466           }
    467           gdk_pixbuf_set_option(frame.data, "icc-profile",
    468                                 decoder_state->icc_base64);
    469           decoder_state->pixel_format.align =
    470               gdk_pixbuf_get_rowstride(frame.data);
    471           decoder_state->pixel_format.data_type = JXL_TYPE_UINT8;
    472           g_array_append_val(decoder_state->frames, frame);
    473         }
    474         if (decoder_state->pixbuf_prepared_callback &&
    475             decoder_state->frames->len == 1) {
    476           decoder_state->pixbuf_prepared_callback(
    477               g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame,
    478                             0)
    479                   .data,
    480               decoder_state->has_animation ? (GdkPixbufAnimation *)decoder_state
    481                                            : NULL,
    482               decoder_state->user_data);
    483         }
    484         break;
    485       }
    486 
    487       case JXL_DEC_NEED_IMAGE_OUT_BUFFER: {
    488         GdkPixbuf *output =
    489             g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame,
    490                           decoder_state->frames->len - 1)
    491                 .data;
    492         decoder_state->pixel_format.align = gdk_pixbuf_get_rowstride(output);
    493         guint size;
    494         guchar *dst = gdk_pixbuf_get_pixels_with_length(output, &size);
    495         if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(
    496                                    decoder_state->decoder,
    497                                    &decoder_state->pixel_format, dst, size)) {
    498           g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    499                       "JxlDecoderSetImageOutBuffer failed");
    500           return FALSE;
    501         }
    502         break;
    503       }
    504 
    505       case JXL_DEC_FULL_IMAGE: {
    506         // TODO(veluca): consider doing partial updates.
    507         if (decoder_state->area_updated_callback) {
    508           GdkPixbuf *output = g_array_index(decoder_state->frames,
    509                                             GdkPixbufJxlAnimationFrame, 0)
    510                                   .data;
    511           decoder_state->area_updated_callback(
    512               output, 0, 0, gdk_pixbuf_get_width(output),
    513               gdk_pixbuf_get_height(output), decoder_state->user_data);
    514         }
    515         g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame,
    516                       decoder_state->frames->len - 1)
    517             .decoded = TRUE;
    518         break;
    519       }
    520 
    521       case JXL_DEC_SUCCESS: {
    522         decoder_state->done = TRUE;
    523         return TRUE;
    524       }
    525 
    526       default: {
    527         g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    528                     "Unexpected JxlDecoderProcessInput return code: %x",
    529                     status);
    530         return FALSE;
    531       }
    532     }
    533   }
    534   return TRUE;
    535 }
    536 
    537 static gboolean jxl_is_save_option_supported(const gchar *option_key) {
    538   if (g_strcmp0(option_key, "quality") == 0) {
    539     return TRUE;
    540   }
    541 
    542   return FALSE;
    543 }
    544 
    545 static gboolean jxl_image_saver(FILE *f, GdkPixbuf *pixbuf, gchar **keys,
    546                                 gchar **values, GError **error) {
    547   long quality = 90; /* default; must be between 0 and 100 */
    548   double distance;
    549   gboolean save_alpha;
    550   JxlEncoder *encoder;
    551   void *parallel_runner;
    552   JxlEncoderFrameSettings *frame_settings;
    553   JxlBasicInfo output_info;
    554   JxlPixelFormat pixel_format;
    555   JxlColorEncoding color_profile;
    556   JxlEncoderStatus status;
    557 
    558   GByteArray *compressed;
    559   size_t offset = 0;
    560   uint8_t *next_out;
    561   size_t avail_out;
    562 
    563   if (f == NULL || pixbuf == NULL) {
    564     return FALSE;
    565   }
    566 
    567   if (keys && *keys) {
    568     gchar **kiter = keys;
    569     gchar **viter = values;
    570 
    571     while (*kiter) {
    572       if (strcmp(*kiter, "quality") == 0) {
    573         char *endptr = NULL;
    574         quality = strtol(*viter, &endptr, 10);
    575 
    576         if (endptr == *viter) {
    577           g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_BAD_OPTION,
    578                       "JXL quality must be a value between 0 and 100; value "
    579                       "\"%s\" could not be parsed.",
    580                       *viter);
    581 
    582           return FALSE;
    583         }
    584 
    585         if (quality < 0 || quality > 100) {
    586           g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_BAD_OPTION,
    587                       "JXL quality must be a value between 0 and 100; value "
    588                       "\"%ld\" is not allowed.",
    589                       quality);
    590 
    591           return FALSE;
    592         }
    593       } else {
    594         g_warning("Unrecognized parameter (%s) passed to JXL saver.", *kiter);
    595       }
    596 
    597       ++kiter;
    598       ++viter;
    599     }
    600   }
    601 
    602   if (gdk_pixbuf_get_bits_per_sample(pixbuf) != 8) {
    603     g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
    604                 "Sorry, only 8bit images are supported by this JXL saver");
    605     return FALSE;
    606   }
    607 
    608   JxlEncoderInitBasicInfo(&output_info);
    609   output_info.have_container = JXL_FALSE;
    610   output_info.xsize = gdk_pixbuf_get_width(pixbuf);
    611   output_info.ysize = gdk_pixbuf_get_height(pixbuf);
    612   output_info.bits_per_sample = 8;
    613   output_info.orientation = JXL_ORIENT_IDENTITY;
    614   output_info.num_color_channels = 3;
    615 
    616   if (output_info.xsize == 0 || output_info.ysize == 0) {
    617     g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
    618                 "Empty image, nothing to save");
    619     return FALSE;
    620   }
    621 
    622   save_alpha = gdk_pixbuf_get_has_alpha(pixbuf);
    623 
    624   pixel_format.data_type = JXL_TYPE_UINT8;
    625   pixel_format.endianness = JXL_NATIVE_ENDIAN;
    626   pixel_format.align = gdk_pixbuf_get_rowstride(pixbuf);
    627 
    628   if (save_alpha) {
    629     if (gdk_pixbuf_get_n_channels(pixbuf) != 4) {
    630       g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
    631                   "Unsupported number of channels");
    632       return FALSE;
    633     }
    634 
    635     output_info.num_extra_channels = 1;
    636     output_info.alpha_bits = 8;
    637     pixel_format.num_channels = 4;
    638   } else {
    639     if (gdk_pixbuf_get_n_channels(pixbuf) != 3) {
    640       g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
    641                   "Unsupported number of channels");
    642       return FALSE;
    643     }
    644 
    645     output_info.num_extra_channels = 0;
    646     output_info.alpha_bits = 0;
    647     pixel_format.num_channels = 3;
    648   }
    649 
    650   encoder = JxlEncoderCreate(NULL);
    651   if (!encoder) {
    652     g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    653                 "Creation of the JXL encoder failed");
    654     return FALSE;
    655   }
    656 
    657   parallel_runner = JxlResizableParallelRunnerCreate(NULL);
    658   if (!parallel_runner) {
    659     JxlEncoderDestroy(encoder);
    660     g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    661                 "Creation of the JXL decoder failed");
    662     return FALSE;
    663   }
    664 
    665   JxlResizableParallelRunnerSetThreads(
    666       parallel_runner, JxlResizableParallelRunnerSuggestThreads(
    667                            output_info.xsize, output_info.ysize));
    668 
    669   status = JxlEncoderSetParallelRunner(encoder, JxlResizableParallelRunner,
    670                                        parallel_runner);
    671   if (status != JXL_ENC_SUCCESS) {
    672     JxlResizableParallelRunnerDestroy(parallel_runner);
    673     JxlEncoderDestroy(encoder);
    674     g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    675                 "JxlDecoderSetParallelRunner failed: %x", status);
    676     return FALSE;
    677   }
    678 
    679   if (quality > 99) {
    680     output_info.uses_original_profile = JXL_TRUE;
    681     distance = 0;
    682   } else {
    683     output_info.uses_original_profile = JXL_FALSE;
    684     distance = JxlEncoderDistanceFromQuality((float)quality);
    685   }
    686 
    687   status = JxlEncoderSetBasicInfo(encoder, &output_info);
    688   if (status != JXL_ENC_SUCCESS) {
    689     JxlResizableParallelRunnerDestroy(parallel_runner);
    690     JxlEncoderDestroy(encoder);
    691     g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    692                 "JxlEncoderSetBasicInfo failed: %x", status);
    693     return FALSE;
    694   }
    695 
    696   JxlColorEncodingSetToSRGB(&color_profile, JXL_FALSE);
    697   status = JxlEncoderSetColorEncoding(encoder, &color_profile);
    698   if (status != JXL_ENC_SUCCESS) {
    699     JxlResizableParallelRunnerDestroy(parallel_runner);
    700     JxlEncoderDestroy(encoder);
    701     g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    702                 "JxlEncoderSetColorEncoding failed: %x", status);
    703     return FALSE;
    704   }
    705 
    706   frame_settings = JxlEncoderFrameSettingsCreate(encoder, NULL);
    707   JxlEncoderSetFrameDistance(frame_settings, distance);
    708   JxlEncoderSetFrameLossless(frame_settings, output_info.uses_original_profile);
    709 
    710   status = JxlEncoderAddImageFrame(frame_settings, &pixel_format,
    711                                    gdk_pixbuf_read_pixels(pixbuf),
    712                                    gdk_pixbuf_get_byte_length(pixbuf));
    713   if (status != JXL_ENC_SUCCESS) {
    714     JxlResizableParallelRunnerDestroy(parallel_runner);
    715     JxlEncoderDestroy(encoder);
    716     g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
    717                 "JxlEncoderAddImageFrame failed: %x", status);
    718     return FALSE;
    719   }
    720 
    721   JxlEncoderCloseInput(encoder);
    722 
    723   compressed = g_byte_array_sized_new(4096);
    724   g_byte_array_set_size(compressed, 4096);
    725   do {
    726     next_out = compressed->data + offset;
    727     avail_out = compressed->len - offset;
    728     status = JxlEncoderProcessOutput(encoder, &next_out, &avail_out);
    729 
    730     if (status == JXL_ENC_NEED_MORE_OUTPUT) {
    731       offset = next_out - compressed->data;
    732       g_byte_array_set_size(compressed, compressed->len * 2);
    733     } else if (status == JXL_ENC_ERROR) {
    734       JxlResizableParallelRunnerDestroy(parallel_runner);
    735       JxlEncoderDestroy(encoder);
    736       g_set_error(error, G_FILE_ERROR, 0, "JxlEncoderProcessOutput failed: %x",
    737                   status);
    738       return FALSE;
    739     }
    740   } while (status != JXL_ENC_SUCCESS);
    741 
    742   JxlResizableParallelRunnerDestroy(parallel_runner);
    743   JxlEncoderDestroy(encoder);
    744 
    745   g_byte_array_set_size(compressed, next_out - compressed->data);
    746   if (compressed->len > 0) {
    747     fwrite(compressed->data, 1, compressed->len, f);
    748     g_byte_array_free(compressed, TRUE);
    749     return TRUE;
    750   }
    751 
    752   return FALSE;
    753 }
    754 
    755 void fill_vtable(GdkPixbufModule *module) {
    756   module->begin_load = begin_load;
    757   module->stop_load = stop_load;
    758   module->load_increment = load_increment;
    759   module->is_save_option_supported = jxl_is_save_option_supported;
    760   module->save = jxl_image_saver;
    761 }
    762 
    763 void fill_info(GdkPixbufFormat *info) {
    764   static GdkPixbufModulePattern signature[] = {
    765       {"\xFF\x0A", "  ", 100},
    766       {"...\x0CJXL \x0D\x0A\x87\x0A", "zzz         ", 100},
    767       {NULL, NULL, 0},
    768   };
    769 
    770   static gchar *mime_types[] = {"image/jxl", NULL};
    771 
    772   static gchar *extensions[] = {"jxl", NULL};
    773 
    774   info->name = "jxl";
    775   info->signature = signature;
    776   info->description = "JPEG XL image";
    777   info->mime_types = mime_types;
    778   info->extensions = extensions;
    779   info->flags = GDK_PIXBUF_FORMAT_WRITABLE | GDK_PIXBUF_FORMAT_THREADSAFE;
    780   info->license = "BSD-3";
    781 }