Skip to content

Commit

Permalink
add support for multipage jxl (libvips#4231)
Browse files Browse the repository at this point in the history
* add support for multipage jxl

Multipage JXL images are just animations with the duration param set to
0xffffffff. This PR detects animations with this magick value for all
frames and represents them as libvips multipage images (no duration
metadata).

A matching change in jxlsave sets duration to 0xffffffff for multipage
images.

* Update libvips/foreign/jxlsave.c

Co-authored-by: Kleis Auke Wolthuizen <[email protected]>

---------

Co-authored-by: Kleis Auke Wolthuizen <[email protected]>
  • Loading branch information
jcupitt and kleisauke authored Oct 31, 2024
1 parent 8dd0510 commit 94502d8
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 81 deletions.
4 changes: 4 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
8.16.1

- support multipage JXL

10/10/24 8.16.0

- allow small offsets for the PDF magic string [project0]
Expand Down
131 changes: 57 additions & 74 deletions libvips/foreign/jxlload.c
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,12 @@ typedef struct _VipsForeignLoadJxl {
uint8_t *xmp_data;

int frame_count;
int *delay;
int delay_count;
GArray *delay;

/* JXL multipage and animated images are the same, but multipage has
* all the frame delays set to -1 (duration 0xffffffff).
*/
gboolean is_animated;

/* The current accumulated frame as a VipsImage. These are the pixels
* we send to the output. It's a info->xsize * info->ysize memory
Expand Down Expand Up @@ -166,7 +170,7 @@ vips_foreign_load_jxl_dispose(GObject *gobject)
VIPS_FREE(jxl->icc_data);
VIPS_FREE(jxl->exif_data);
VIPS_FREE(jxl->xmp_data);
VIPS_FREE(jxl->delay);
VIPS_FREEF(g_array_unref, jxl->delay);
VIPS_UNREF(jxl->frame);
VIPS_UNREF(jxl->source);

Expand Down Expand Up @@ -492,8 +496,7 @@ vips_foreign_load_jxl_read_frame(VipsForeignLoadJxl *jxl, VipsImage *frame,
int skip = frame_no - jxl->frame_no - 1;
if (skip > 0) {
#ifdef DEBUG_VERBOSE
printf("vips_foreign_load_jxl_read_frame: skipping %d frames\n",
skip);
printf("vips_foreign_load_jxl_read_frame: skipping %d frames\n", skip);
#endif /*DEBUG_VERBOSE*/
JxlDecoderSkipFrames(jxl->decoder, skip);
jxl->frame_no += skip;
Expand All @@ -504,8 +507,7 @@ vips_foreign_load_jxl_read_frame(VipsForeignLoadJxl *jxl, VipsImage *frame,
do {
switch ((status = vips_foreign_load_jxl_process(jxl))) {
case JXL_DEC_ERROR:
vips_foreign_load_jxl_error(jxl,
"JxlDecoderProcessInput");
vips_foreign_load_jxl_error(jxl, "JxlDecoderProcessInput");
return -1;

case JXL_DEC_FRAME:
Expand All @@ -514,24 +516,19 @@ vips_foreign_load_jxl_read_frame(VipsForeignLoadJxl *jxl, VipsImage *frame,

case JXL_DEC_NEED_IMAGE_OUT_BUFFER:
if (JxlDecoderImageOutBufferSize(jxl->decoder,
&jxl->format,
&buffer_size)) {
&jxl->format, &buffer_size)) {
vips_foreign_load_jxl_error(jxl,
"JxlDecoderImageOutBufferSize");
return -1;
}
if (buffer_size !=
VIPS_IMAGE_SIZEOF_IMAGE(frame)) {
vips_error(class->nickname,
"%s", _("bad buffer size"));
if (buffer_size != VIPS_IMAGE_SIZEOF_IMAGE(frame)) {
vips_error(class->nickname, "%s", _("bad buffer size"));
return -1;
}
if (JxlDecoderSetImageOutBuffer(jxl->decoder,
&jxl->format,
if (JxlDecoderSetImageOutBuffer(jxl->decoder, &jxl->format,
VIPS_IMAGE_ADDR(frame, 0, 0),
VIPS_IMAGE_SIZEOF_IMAGE(frame))) {
vips_foreign_load_jxl_error(jxl,
"JxlDecoderSetImageOutBuffer");
vips_foreign_load_jxl_error(jxl, "JxlDecoderSetImageOutBuffer");
return -1;
}
break;
Expand All @@ -551,8 +548,7 @@ vips_foreign_load_jxl_read_frame(VipsForeignLoadJxl *jxl, VipsImage *frame,

/* We didn't find the required frame
*/
vips_error(class->nickname,
"%s", _("not enough frames"));
vips_error(class->nickname, "%s", _("not enough frames"));
return -1;
}

Expand Down Expand Up @@ -633,8 +629,7 @@ vips_foreign_load_jxl_set_header(VipsForeignLoadJxl *jxl, VipsImage *out)

if (jxl->info.xsize >= VIPS_MAX_COORD ||
jxl->info.ysize >= VIPS_MAX_COORD) {
vips_error(class->nickname,
"%s", _("image size out of bounds"));
vips_error(class->nickname, "%s", _("image size out of bounds"));
return -1;
}

Expand Down Expand Up @@ -704,27 +699,26 @@ vips_foreign_load_jxl_set_header(VipsForeignLoadJxl *jxl, VipsImage *out)
if (jxl->page < 0 ||
jxl->n <= 0 ||
jxl->page + jxl->n > jxl->frame_count) {
vips_error(class->nickname,
"%s", _("bad page number"));
vips_error(class->nickname, "%s", _("bad page number"));
return -1;
}

vips_image_set_int(out, VIPS_META_N_PAGES, jxl->frame_count);

if (jxl->n > 1)
vips_image_set_int(out,
VIPS_META_PAGE_HEIGHT, jxl->info.ysize);
vips_image_set_int(out, VIPS_META_PAGE_HEIGHT, jxl->info.ysize);

g_assert(jxl->delay_count >= jxl->frame_count);
vips_image_set_array_int(out,
"delay", jxl->delay, jxl->frame_count);
if (jxl->is_animated) {
int *delay = (int *) jxl->delay->data;

/* gif uses centiseconds for delays
*/
vips_image_set_int(out, "gif-delay",
VIPS_RINT(jxl->delay[0] / 10.0));
vips_image_set_array_int(out, "delay", delay, jxl->frame_count);

/* gif uses centiseconds for delays
*/
vips_image_set_int(out, "gif-delay", VIPS_RINT(delay[0] / 10.0));

vips_image_set_int(out, "loop", jxl->info.animation.num_loops);
vips_image_set_int(out, "loop", jxl->info.animation.num_loops);
}
}
else {
jxl->n = 1;
Expand Down Expand Up @@ -759,32 +753,28 @@ vips_foreign_load_jxl_set_header(VipsForeignLoadJxl *jxl, VipsImage *out)
if (jxl->icc_data &&
jxl->icc_size > 0) {
vips_image_set_blob(out, VIPS_META_ICC_NAME,
(VipsCallbackFn) vips_area_free_cb,
jxl->icc_data, jxl->icc_size);
(VipsCallbackFn) vips_area_free_cb, jxl->icc_data, jxl->icc_size);
jxl->icc_data = NULL;
jxl->icc_size = 0;
}

if (jxl->exif_data &&
jxl->exif_size > 0) {
vips_image_set_blob(out, VIPS_META_EXIF_NAME,
(VipsCallbackFn) vips_area_free_cb,
jxl->exif_data, jxl->exif_size);
(VipsCallbackFn) vips_area_free_cb, jxl->exif_data, jxl->exif_size);
jxl->exif_data = NULL;
jxl->exif_size = 0;
}

if (jxl->xmp_data &&
jxl->xmp_size > 0) {
vips_image_set_blob(out, VIPS_META_XMP_NAME,
(VipsCallbackFn) vips_area_free_cb,
jxl->xmp_data, jxl->xmp_size);
(VipsCallbackFn) vips_area_free_cb, jxl->xmp_data, jxl->xmp_size);
jxl->xmp_data = NULL;
jxl->xmp_size = 0;
}

vips_image_set_int(out,
VIPS_META_ORIENTATION, jxl->info.orientation);
vips_image_set_int(out, VIPS_META_ORIENTATION, jxl->info.orientation);

vips_image_set_int(out, VIPS_META_BITS_PER_SAMPLE,
jxl->info.bits_per_sample);
Expand All @@ -795,7 +785,6 @@ vips_foreign_load_jxl_set_header(VipsForeignLoadJxl *jxl, VipsImage *out)
static int
vips_foreign_load_jxl_header(VipsForeignLoad *load)
{
VipsObjectClass *class = VIPS_OBJECT_GET_CLASS(load);
VipsForeignLoadJxl *jxl = (VipsForeignLoadJxl *) load;

JxlDecoderStatus status;
Expand All @@ -815,8 +804,7 @@ vips_foreign_load_jxl_header(VipsForeignLoad *load)
JXL_DEC_BASIC_INFO |
JXL_DEC_BOX |
JXL_DEC_FRAME)) {
vips_foreign_load_jxl_error(jxl,
"JxlDecoderSubscribeEvents");
vips_foreign_load_jxl_error(jxl, "JxlDecoderSubscribeEvents");
return -1;
}

Expand All @@ -825,8 +813,7 @@ vips_foreign_load_jxl_header(VipsForeignLoad *load)

if (vips_foreign_load_jxl_fill_input(jxl, 0) < 0)
return -1;
JxlDecoderSetInput(jxl->decoder,
jxl->input_buffer, jxl->bytes_in_buffer);
JxlDecoderSetInput(jxl->decoder, jxl->input_buffer, jxl->bytes_in_buffer);

jxl->frame_count = 0;

Expand Down Expand Up @@ -919,21 +906,17 @@ vips_foreign_load_jxl_header(VipsForeignLoad *load)
#ifndef HAVE_LIBJXL_0_9
&jxl->format,
#endif
JXL_COLOR_PROFILE_TARGET_DATA,
&jxl->icc_size)) {
JXL_COLOR_PROFILE_TARGET_DATA, &jxl->icc_size)) {
vips_foreign_load_jxl_error(jxl,
"JxlDecoderGetICCProfileSize");
return -1;
}

#ifdef DEBUG
printf(
"vips_foreign_load_jxl_header: "
"%zd byte profile\n",
printf("vips_foreign_load_jxl_header: %zd byte profile\n",
jxl->icc_size);
#endif /*DEBUG*/
if (!(jxl->icc_data = vips_malloc(NULL,
jxl->icc_size)))
if (!(jxl->icc_data = vips_malloc(NULL, jxl->icc_size)))
return -1;

if (JxlDecoderGetColorAsICCProfile(jxl->decoder,
Expand All @@ -950,42 +933,41 @@ vips_foreign_load_jxl_header(VipsForeignLoad *load)

case JXL_DEC_FRAME:
if (JxlDecoderGetFrameHeader(jxl->decoder, &h) != JXL_DEC_SUCCESS) {
vips_foreign_load_jxl_error(jxl,
"JxlDecoderGetFrameHeader");
vips_foreign_load_jxl_error(jxl, "JxlDecoderGetFrameHeader");
return -1;
}

if (jxl->info.have_animation) {
if (jxl->delay_count <= jxl->frame_count) {
jxl->delay_count += 128;
int *new_delay = g_try_realloc(jxl->delay,
jxl->delay_count * sizeof(int));
if (!new_delay) {
vips_error(class->nickname, "%s", _("out of memory"));
return -1;
}
jxl->delay = new_delay;
}

jxl->delay[jxl->frame_count] = VIPS_RINT(1000.0 * h.duration *
jxl->info.animation.tps_denominator /
jxl->info.animation.tps_numerator);
// tick duration in seconds
double tick = (double) jxl->info.animation.tps_denominator /
jxl->info.animation.tps_numerator;
// this duration in ms
int ms = VIPS_RINT(1000.0 * h.duration * tick);
// h.duration of 0xffffffff is used for multipage JXL ... map
// this to -1 in delay
int duration = h.duration == 0xffffffff ? -1 : ms;

jxl->delay = g_array_append_vals(jxl->delay, &duration, 1);
}

jxl->frame_count++;

/* This is the last frame, we can stop right here
*/
if (h.is_last || !jxl->info.have_animation)
status = JXL_DEC_SUCCESS;

break;

default:
break;
}
} while (status != JXL_DEC_SUCCESS);

/* Detect JXL multipage (rather than animated).
*/
int *delay = (int *) jxl->delay->data;
for (int i = 0; i < jxl->delay->len; i++)
if (delay[i] != -1) {
jxl->is_animated = TRUE;
break;
}

/* Flush box data if any
*/
if (vips_foreign_load_jxl_release_box_buffer(jxl))
Expand Down Expand Up @@ -1113,6 +1095,7 @@ static void
vips_foreign_load_jxl_init(VipsForeignLoadJxl *jxl)
{
jxl->n = 1;
jxl->delay = g_array_new(FALSE, FALSE, sizeof(int));
}

typedef struct _VipsForeignLoadJxlFile {
Expand Down
24 changes: 19 additions & 5 deletions libvips/foreign/jxlsave.c
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ typedef struct _VipsForeignSaveJxl {
gboolean lossless;
int Q;

/* JXL multipage and animated images are the same, but multipage has
* all the frame delays set to -1 (duration 0xffffffff).
*/
gboolean is_animated;

/* Animated jxl options.
*/
int gif_delay;
Expand Down Expand Up @@ -319,7 +324,9 @@ vips_foreign_save_jxl_add_frame(VipsForeignSaveJxl *jxl)
JxlFrameHeader header;
memset(&header, 0, sizeof(JxlFrameHeader));

if (jxl->delay && jxl->page_number < jxl->delay_length)
if (!jxl->is_animated)
header.duration = 0xffffffff;
else if (jxl->delay && jxl->page_number < jxl->delay_length)
header.duration = jxl->delay[jxl->page_number];
else
header.duration = jxl->gif_delay * 10;
Expand Down Expand Up @@ -593,6 +600,8 @@ vips_foreign_save_jxl_build(VipsObject *object)
if (vips_image_get_typeof(in, "loop"))
vips_image_get_int(in, "loop", &num_loops);

// libjxl uses "have_animation" for multipage images too, but sets
// duration to 0xffffffff
jxl->info.have_animation = TRUE;
jxl->info.animation.tps_numerator = 1000;
jxl->info.animation.tps_denominator = 1;
Expand Down Expand Up @@ -670,10 +679,8 @@ vips_foreign_save_jxl_build(VipsObject *object)
jxl->format.num_channels < 3);
}

if (JxlEncoderSetColorEncoding(jxl->encoder,
&jxl->color_encoding)) {
vips_foreign_save_jxl_error(jxl,
"JxlEncoderSetColorEncoding");
if (JxlEncoderSetColorEncoding(jxl->encoder, &jxl->color_encoding)) {
vips_foreign_save_jxl_error(jxl, "JxlEncoderSetColorEncoding");
return -1;
}
}
Expand Down Expand Up @@ -703,6 +710,13 @@ vips_foreign_save_jxl_build(VipsObject *object)
&jxl->delay, &jxl->delay_length))
return -1;

/* If there's delay metadata, this is an animated image (as opposed to
* a multipage one).
*/
if (vips_image_get_typeof(save->ready, "delay") ||
vips_image_get_typeof(save->ready, "gif-delay"))
jxl->is_animated = TRUE;

/* Force frames with a small or no duration to 100ms
* to be consistent with web browsers and other
* transcoding tools.
Expand Down
4 changes: 2 additions & 2 deletions meson.build
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
project('vips', 'c', 'cpp',
version: '8.16.0',
version: '8.16.1',
meson_version: '>=0.55',
default_options: [
# this is what glib uses (one of our required deps), so we use it too
Expand All @@ -23,7 +23,7 @@ version_patch = version_parts[2]
# binary interface changed: increment current, reset revision to 0
# binary interface changes backwards compatible?: increment age
# binary interface changes not backwards compatible?: reset age to 0
library_revision = 0
library_revision = 1
library_current = 60
library_age = 18
library_version = '@0@.@1@.@2@'.format(library_current - library_age, library_age, library_revision)
Expand Down

0 comments on commit 94502d8

Please sign in to comment.