781 lines
28 KiB
C
781 lines
28 KiB
C
// Copyright (c) the JPEG XL Project Authors. All rights reserved.
|
|
//
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
#include <jxl/codestream_header.h>
|
|
#include <jxl/decode.h>
|
|
#include <jxl/encode.h>
|
|
#include <jxl/resizable_parallel_runner.h>
|
|
#include <jxl/types.h>
|
|
|
|
#define GDK_PIXBUF_ENABLE_BACKEND
|
|
#include <gdk-pixbuf/gdk-pixbuf.h>
|
|
#undef GDK_PIXBUF_ENABLE_BACKEND
|
|
|
|
G_BEGIN_DECLS
|
|
|
|
// Information about a single frame.
|
|
typedef struct {
|
|
uint64_t duration_ms;
|
|
GdkPixbuf *data;
|
|
gboolean decoded;
|
|
} GdkPixbufJxlAnimationFrame;
|
|
|
|
// Represent a whole JPEG XL animation; all its fields are owned; as a GObject,
|
|
// the Animation struct itself is reference counted (as are the GdkPixbufs for
|
|
// individual frames).
|
|
struct _GdkPixbufJxlAnimation {
|
|
GdkPixbufAnimation parent_instance;
|
|
|
|
// GDK interface implementation callbacks.
|
|
GdkPixbufModuleSizeFunc image_size_callback;
|
|
GdkPixbufModulePreparedFunc pixbuf_prepared_callback;
|
|
GdkPixbufModuleUpdatedFunc area_updated_callback;
|
|
gpointer user_data;
|
|
|
|
// All frames known so far; a frame is added when the JXL_DEC_FRAME event is
|
|
// received from the decoder; initially frame.decoded is FALSE, until
|
|
// the JXL_DEC_IMAGE event is received.
|
|
GArray *frames;
|
|
|
|
// JPEG XL decoder and related structures.
|
|
JxlParallelRunner *parallel_runner;
|
|
JxlDecoder *decoder;
|
|
JxlPixelFormat pixel_format;
|
|
|
|
// Decoding is `done` when JXL_DEC_SUCCESS is received; calling
|
|
// load_increment afterwards gives an error.
|
|
gboolean done;
|
|
|
|
// Image information.
|
|
size_t xsize;
|
|
size_t ysize;
|
|
gboolean alpha_premultiplied;
|
|
gboolean has_animation;
|
|
gboolean has_alpha;
|
|
uint64_t total_duration_ms;
|
|
uint64_t tick_duration_us;
|
|
uint64_t repetition_count; // 0 = loop forever
|
|
|
|
gchar *icc_base64;
|
|
};
|
|
|
|
#define GDK_TYPE_PIXBUF_JXL_ANIMATION (gdk_pixbuf_jxl_animation_get_type())
|
|
G_DECLARE_FINAL_TYPE(GdkPixbufJxlAnimation, gdk_pixbuf_jxl_animation, GDK,
|
|
JXL_ANIMATION, GdkPixbufAnimation);
|
|
|
|
G_DEFINE_TYPE(GdkPixbufJxlAnimation, gdk_pixbuf_jxl_animation,
|
|
GDK_TYPE_PIXBUF_ANIMATION);
|
|
|
|
// Iterator to a given point in time in the animation; contains a pointer to the
|
|
// full animation.
|
|
struct _GdkPixbufJxlAnimationIter {
|
|
GdkPixbufAnimationIter parent_instance;
|
|
GdkPixbufJxlAnimation *animation;
|
|
size_t current_frame;
|
|
uint64_t time_offset;
|
|
};
|
|
|
|
#define GDK_TYPE_PIXBUF_JXL_ANIMATION_ITER \
|
|
(gdk_pixbuf_jxl_animation_iter_get_type())
|
|
G_DECLARE_FINAL_TYPE(GdkPixbufJxlAnimationIter, gdk_pixbuf_jxl_animation_iter,
|
|
GDK, JXL_ANIMATION_ITER, GdkPixbufAnimationIter);
|
|
G_DEFINE_TYPE(GdkPixbufJxlAnimationIter, gdk_pixbuf_jxl_animation_iter,
|
|
GDK_TYPE_PIXBUF_ANIMATION_ITER);
|
|
|
|
static void gdk_pixbuf_jxl_animation_init(GdkPixbufJxlAnimation *obj) {
|
|
// Suppress "unused function" warnings.
|
|
(void)glib_autoptr_cleanup_GdkPixbufJxlAnimation;
|
|
(void)GDK_JXL_ANIMATION;
|
|
(void)GDK_IS_JXL_ANIMATION;
|
|
}
|
|
|
|
static gboolean gdk_pixbuf_jxl_animation_is_static_image(
|
|
GdkPixbufAnimation *anim) {
|
|
GdkPixbufJxlAnimation *jxl_anim = (GdkPixbufJxlAnimation *)anim;
|
|
return !jxl_anim->has_animation;
|
|
}
|
|
|
|
static GdkPixbuf *gdk_pixbuf_jxl_animation_get_static_image(
|
|
GdkPixbufAnimation *anim) {
|
|
GdkPixbufJxlAnimation *jxl_anim = (GdkPixbufJxlAnimation *)anim;
|
|
if (jxl_anim->frames == NULL || jxl_anim->frames->len == 0) return NULL;
|
|
GdkPixbufJxlAnimationFrame *frame =
|
|
&g_array_index(jxl_anim->frames, GdkPixbufJxlAnimationFrame, 0);
|
|
return frame->decoded ? frame->data : NULL;
|
|
}
|
|
|
|
static void gdk_pixbuf_jxl_animation_get_size(GdkPixbufAnimation *anim,
|
|
int *width, int *height) {
|
|
GdkPixbufJxlAnimation *jxl_anim = (GdkPixbufJxlAnimation *)anim;
|
|
if (width) *width = jxl_anim->xsize;
|
|
if (height) *height = jxl_anim->ysize;
|
|
}
|
|
|
|
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
|
|
static gboolean gdk_pixbuf_jxl_animation_iter_advance(
|
|
GdkPixbufAnimationIter *iter, const GTimeVal *current_time);
|
|
|
|
static GdkPixbufAnimationIter *gdk_pixbuf_jxl_animation_get_iter(
|
|
GdkPixbufAnimation *anim, const GTimeVal *start_time) {
|
|
GdkPixbufJxlAnimationIter *iter =
|
|
g_object_new(GDK_TYPE_PIXBUF_JXL_ANIMATION_ITER, NULL);
|
|
iter->animation = (GdkPixbufJxlAnimation *)anim;
|
|
iter->time_offset = start_time->tv_sec * 1000ULL + start_time->tv_usec / 1000;
|
|
g_object_ref(iter->animation);
|
|
gdk_pixbuf_jxl_animation_iter_advance((GdkPixbufAnimationIter *)iter,
|
|
start_time);
|
|
return (GdkPixbufAnimationIter *)iter;
|
|
}
|
|
G_GNUC_END_IGNORE_DEPRECATIONS
|
|
|
|
static void gdk_pixbuf_jxl_animation_finalize(GObject *obj) {
|
|
GdkPixbufJxlAnimation *decoder_state = (GdkPixbufJxlAnimation *)obj;
|
|
if (decoder_state->frames != NULL) {
|
|
for (size_t i = 0; i < decoder_state->frames->len; i++) {
|
|
g_object_unref(
|
|
g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame, i)
|
|
.data);
|
|
}
|
|
g_array_free(decoder_state->frames, /*free_segment=*/TRUE);
|
|
}
|
|
JxlResizableParallelRunnerDestroy(decoder_state->parallel_runner);
|
|
JxlDecoderDestroy(decoder_state->decoder);
|
|
g_free(decoder_state->icc_base64);
|
|
}
|
|
|
|
static void gdk_pixbuf_jxl_animation_class_init(
|
|
GdkPixbufJxlAnimationClass *klass) {
|
|
G_OBJECT_CLASS(klass)->finalize = gdk_pixbuf_jxl_animation_finalize;
|
|
klass->parent_class.is_static_image =
|
|
gdk_pixbuf_jxl_animation_is_static_image;
|
|
klass->parent_class.get_static_image =
|
|
gdk_pixbuf_jxl_animation_get_static_image;
|
|
klass->parent_class.get_size = gdk_pixbuf_jxl_animation_get_size;
|
|
klass->parent_class.get_iter = gdk_pixbuf_jxl_animation_get_iter;
|
|
}
|
|
|
|
static void gdk_pixbuf_jxl_animation_iter_init(GdkPixbufJxlAnimationIter *obj) {
|
|
(void)glib_autoptr_cleanup_GdkPixbufJxlAnimationIter;
|
|
(void)GDK_JXL_ANIMATION_ITER;
|
|
(void)GDK_IS_JXL_ANIMATION_ITER;
|
|
}
|
|
|
|
static int gdk_pixbuf_jxl_animation_iter_get_delay_time(
|
|
GdkPixbufAnimationIter *iter) {
|
|
GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter;
|
|
if (jxl_iter->animation->frames->len <= jxl_iter->current_frame) {
|
|
return 0;
|
|
}
|
|
return g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame,
|
|
jxl_iter->current_frame)
|
|
.duration_ms;
|
|
}
|
|
|
|
static GdkPixbuf *gdk_pixbuf_jxl_animation_iter_get_pixbuf(
|
|
GdkPixbufAnimationIter *iter) {
|
|
GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter;
|
|
if (jxl_iter->animation->frames->len <= jxl_iter->current_frame) {
|
|
return NULL;
|
|
}
|
|
return g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame,
|
|
jxl_iter->current_frame)
|
|
.data;
|
|
}
|
|
|
|
static gboolean gdk_pixbuf_jxl_animation_iter_on_currently_loading_frame(
|
|
GdkPixbufAnimationIter *iter) {
|
|
GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter;
|
|
if (jxl_iter->animation->frames->len <= jxl_iter->current_frame) {
|
|
return TRUE;
|
|
}
|
|
return !g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame,
|
|
jxl_iter->current_frame)
|
|
.decoded;
|
|
}
|
|
|
|
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
|
|
static gboolean gdk_pixbuf_jxl_animation_iter_advance(
|
|
GdkPixbufAnimationIter *iter, const GTimeVal *current_time) {
|
|
GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter;
|
|
size_t old_frame = jxl_iter->current_frame;
|
|
|
|
uint64_t current_time_ms = current_time->tv_sec * 1000ULL +
|
|
current_time->tv_usec / 1000 -
|
|
jxl_iter->time_offset;
|
|
|
|
if (jxl_iter->animation->frames->len == 0) {
|
|
jxl_iter->current_frame = 0;
|
|
} else if (!jxl_iter->animation->done &&
|
|
current_time_ms >= jxl_iter->animation->total_duration_ms) {
|
|
jxl_iter->current_frame = jxl_iter->animation->frames->len - 1;
|
|
} else if (jxl_iter->animation->repetition_count != 0 &&
|
|
current_time_ms > jxl_iter->animation->repetition_count *
|
|
jxl_iter->animation->total_duration_ms) {
|
|
jxl_iter->current_frame = jxl_iter->animation->frames->len - 1;
|
|
} else {
|
|
uint64_t total_duration_ms = jxl_iter->animation->total_duration_ms;
|
|
// Guard against divide-by-0 in malicious files.
|
|
if (total_duration_ms == 0) total_duration_ms = 1;
|
|
uint64_t loop_offset = current_time_ms % total_duration_ms;
|
|
jxl_iter->current_frame = 0;
|
|
while (TRUE) {
|
|
uint64_t duration =
|
|
g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame,
|
|
jxl_iter->current_frame)
|
|
.duration_ms;
|
|
if (duration >= loop_offset) {
|
|
break;
|
|
}
|
|
loop_offset -= duration;
|
|
jxl_iter->current_frame++;
|
|
}
|
|
}
|
|
|
|
return old_frame != jxl_iter->current_frame;
|
|
}
|
|
G_GNUC_END_IGNORE_DEPRECATIONS
|
|
|
|
static void gdk_pixbuf_jxl_animation_iter_finalize(GObject *obj) {
|
|
GdkPixbufJxlAnimationIter *iter = (GdkPixbufJxlAnimationIter *)obj;
|
|
g_object_unref(iter->animation);
|
|
}
|
|
|
|
static void gdk_pixbuf_jxl_animation_iter_class_init(
|
|
GdkPixbufJxlAnimationIterClass *klass) {
|
|
G_OBJECT_CLASS(klass)->finalize = gdk_pixbuf_jxl_animation_iter_finalize;
|
|
klass->parent_class.get_delay_time =
|
|
gdk_pixbuf_jxl_animation_iter_get_delay_time;
|
|
klass->parent_class.get_pixbuf = gdk_pixbuf_jxl_animation_iter_get_pixbuf;
|
|
klass->parent_class.on_currently_loading_frame =
|
|
gdk_pixbuf_jxl_animation_iter_on_currently_loading_frame;
|
|
klass->parent_class.advance = gdk_pixbuf_jxl_animation_iter_advance;
|
|
}
|
|
|
|
G_END_DECLS
|
|
|
|
static gpointer begin_load(GdkPixbufModuleSizeFunc size_func,
|
|
GdkPixbufModulePreparedFunc prepare_func,
|
|
GdkPixbufModuleUpdatedFunc update_func,
|
|
gpointer user_data, GError **error) {
|
|
GdkPixbufJxlAnimation *decoder_state =
|
|
g_object_new(GDK_TYPE_PIXBUF_JXL_ANIMATION, NULL);
|
|
if (decoder_state == NULL) {
|
|
g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
|
|
"Creation of the animation state failed");
|
|
return NULL;
|
|
}
|
|
decoder_state->image_size_callback = size_func;
|
|
decoder_state->pixbuf_prepared_callback = prepare_func;
|
|
decoder_state->area_updated_callback = update_func;
|
|
decoder_state->user_data = user_data;
|
|
decoder_state->frames =
|
|
g_array_new(/*zero_terminated=*/FALSE, /*clear_=*/TRUE,
|
|
sizeof(GdkPixbufJxlAnimationFrame));
|
|
|
|
if (decoder_state->frames == NULL) {
|
|
g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
|
|
"Creation of the frame array failed");
|
|
goto cleanup;
|
|
}
|
|
|
|
if (!(decoder_state->parallel_runner =
|
|
JxlResizableParallelRunnerCreate(NULL))) {
|
|
g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
|
|
"Creation of the JXL parallel runner failed");
|
|
goto cleanup;
|
|
}
|
|
|
|
if (!(decoder_state->decoder = JxlDecoderCreate(NULL))) {
|
|
g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
|
|
"Creation of the JXL decoder failed");
|
|
goto cleanup;
|
|
}
|
|
|
|
JxlDecoderStatus status;
|
|
|
|
if ((status = JxlDecoderSetParallelRunner(
|
|
decoder_state->decoder, JxlResizableParallelRunner,
|
|
decoder_state->parallel_runner)) != JXL_DEC_SUCCESS) {
|
|
g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
|
|
"JxlDecoderSetParallelRunner failed: %x", status);
|
|
goto cleanup;
|
|
}
|
|
if ((status = JxlDecoderSubscribeEvents(
|
|
decoder_state->decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING |
|
|
JXL_DEC_FULL_IMAGE | JXL_DEC_FRAME)) !=
|
|
JXL_DEC_SUCCESS) {
|
|
g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
|
|
"JxlDecoderSubscribeEvents failed: %x", status);
|
|
goto cleanup;
|
|
}
|
|
|
|
decoder_state->pixel_format.data_type = JXL_TYPE_FLOAT;
|
|
decoder_state->pixel_format.endianness = JXL_NATIVE_ENDIAN;
|
|
|
|
return decoder_state;
|
|
cleanup:
|
|
JxlResizableParallelRunnerDestroy(decoder_state->parallel_runner);
|
|
JxlDecoderDestroy(decoder_state->decoder);
|
|
g_object_unref(decoder_state);
|
|
return NULL;
|
|
}
|
|
|
|
static gboolean stop_load(gpointer context, GError **error) {
|
|
g_object_unref(context);
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean load_increment(gpointer context, const guchar *buf, guint size,
|
|
GError **error) {
|
|
GdkPixbufJxlAnimation *decoder_state = context;
|
|
if (decoder_state->done == TRUE) {
|
|
g_warning_once("Trailing data found at end of JXL file");
|
|
return TRUE;
|
|
}
|
|
|
|
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);
|
|
guint size;
|
|
guchar *dst = gdk_pixbuf_get_pixels_with_length(output, &size);
|
|
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";
|
|
}
|