tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | 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 }