Skip to content

Commit 605fbb9

Browse files
committed
jxlsave: add animation support
1 parent 41b432f commit 605fbb9

File tree

1 file changed

+219
-56
lines changed

1 file changed

+219
-56
lines changed

libvips/foreign/jxlsave.c

Lines changed: 219 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,17 @@ typedef struct _VipsForeignSaveJxl {
8585
gboolean lossless;
8686
int Q;
8787

88+
/* Animated jxl options.
89+
*/
90+
int gif_delay;
91+
int *delay;
92+
int delay_length;
93+
94+
/* The image we save. This is a copy of save->ready since we need to
95+
* be able to update the metadata.
96+
*/
97+
VipsImage *image;
98+
8899
/* Base image properties.
89100
*/
90101
JxlBasicInfo info;
@@ -96,6 +107,20 @@ typedef struct _VipsForeignSaveJxl {
96107
void *runner;
97108
JxlEncoder *encoder;
98109

110+
/* The current y position in the frame, page height,
111+
* total number of pages, and the current page index.
112+
*/
113+
int write_y;
114+
int page_height;
115+
int page_count;
116+
int page_number;
117+
118+
/* VipsRegion is not always contiguous, but we need contiguous RGB(A)
119+
* for libjxl. We need to copy each frame to a local buffer.
120+
*/
121+
VipsPel *frame_bytes;
122+
size_t frame_size;
123+
99124
/* Write buffer.
100125
*/
101126
uint8_t output_buffer[OUTPUT_BUFFER_SIZE];
@@ -126,6 +151,8 @@ vips_foreign_save_jxl_dispose(GObject *gobject)
126151

127152
VIPS_FREEF(JxlThreadParallelRunnerDestroy, jxl->runner);
128153
VIPS_FREEF(JxlEncoderDestroy, jxl->encoder);
154+
VIPS_FREE(jxl->frame_bytes);
155+
129156
VIPS_UNREF(jxl->target);
130157

131158
G_OBJECT_CLASS(vips_foreign_save_jxl_parent_class)->dispose(gobject);
@@ -233,6 +260,128 @@ vips_foreign_save_jxl_print_status(JxlEncoderStatus status)
233260
}
234261
#endif /*DEBUG*/
235262

263+
static int
264+
vips_foreign_save_jxl_process_output(VipsForeignSaveJxl *jxl)
265+
{
266+
JxlEncoderStatus status;
267+
uint8_t *out;
268+
size_t avail_out;
269+
270+
do {
271+
out = jxl->output_buffer;
272+
avail_out = OUTPUT_BUFFER_SIZE;
273+
status = JxlEncoderProcessOutput(jxl->encoder,
274+
&out, &avail_out);
275+
switch (status) {
276+
case JXL_ENC_SUCCESS:
277+
case JXL_ENC_NEED_MORE_OUTPUT:
278+
if (OUTPUT_BUFFER_SIZE > avail_out &&
279+
vips_target_write(jxl->target,
280+
jxl->output_buffer,
281+
OUTPUT_BUFFER_SIZE - avail_out))
282+
return -1;
283+
break;
284+
285+
default:
286+
vips_foreign_save_jxl_error(jxl,
287+
"JxlEncoderProcessOutput");
288+
#ifdef DEBUG
289+
vips_foreign_save_jxl_print_status(status);
290+
#endif /*DEBUG*/
291+
return -1;
292+
}
293+
} while (status != JXL_ENC_SUCCESS);
294+
295+
return 0;
296+
}
297+
298+
static int
299+
vips_foreign_save_jxl_add_frame(VipsForeignSaveJxl *jxl)
300+
{
301+
#ifdef HAVE_LIBJXL_0_7
302+
JxlEncoderFrameSettings *frame_settings;
303+
#else
304+
JxlEncoderOptions *frame_settings;
305+
#endif
306+
307+
#ifdef HAVE_LIBJXL_0_7
308+
frame_settings = JxlEncoderFrameSettingsCreate(jxl->encoder, NULL);
309+
JxlEncoderFrameSettingsSetOption(frame_settings,
310+
JXL_ENC_FRAME_SETTING_DECODING_SPEED, jxl->tier);
311+
JxlEncoderSetFrameDistance(frame_settings, jxl->distance);
312+
JxlEncoderFrameSettingsSetOption(frame_settings,
313+
JXL_ENC_FRAME_SETTING_EFFORT, jxl->effort);
314+
JxlEncoderSetFrameLossless(frame_settings, jxl->lossless);
315+
316+
if (jxl->info.have_animation) {
317+
JxlFrameHeader header;
318+
memset(&header, 0, sizeof(JxlFrameHeader));
319+
320+
if (jxl->delay && jxl->page_number < jxl->delay_length)
321+
header.duration = jxl->delay[jxl->page_number];
322+
else
323+
header.duration = jxl->gif_delay * 10;
324+
325+
JxlEncoderSetFrameHeader(frame_settings, &header);
326+
}
327+
#else
328+
frame_settings = JxlEncoderOptionsCreate(jxl->encoder, NULL);
329+
JxlEncoderOptionsSetDecodingSpeed(frame_settings, jxl->tier);
330+
JxlEncoderOptionsSetDistance(frame_settings, jxl->distance);
331+
JxlEncoderOptionsSetEffort(frame_settings, jxl->effort);
332+
JxlEncoderOptionsSetLossless(frame_settings, jxl->lossless);
333+
#endif
334+
335+
if (JxlEncoderAddImageFrame(frame_settings, &jxl->format,
336+
jxl->frame_bytes, jxl->frame_size)) {
337+
vips_foreign_save_jxl_error(jxl, "JxlEncoderAddImageFrame");
338+
return -1;
339+
}
340+
341+
jxl->page_number += 1;
342+
343+
/* We should close frames before processing the output
344+
* if we have written the last frame
345+
*/
346+
if (jxl->page_number == jxl->page_count)
347+
JxlEncoderCloseFrames(jxl->encoder);
348+
349+
return vips_foreign_save_jxl_process_output(jxl);
350+
}
351+
352+
/* Another chunk of pixels have arrived from the pipeline. Add to frame, and
353+
* if the frame completes, compress and write to the target.
354+
*/
355+
static int
356+
vips_foreign_save_jxl_sink_disc(VipsRegion *region, VipsRect *area, void *a)
357+
{
358+
VipsForeignSaveJxl *jxl = (VipsForeignSaveJxl *) a;
359+
size_t sz = VIPS_IMAGE_SIZEOF_PEL(region->im) * area->width;
360+
361+
int i;
362+
363+
/* Write the new pixels into the frame.
364+
*/
365+
for (i = 0; i < area->height; i++) {
366+
memcpy(jxl->frame_bytes + sz * jxl->write_y,
367+
VIPS_REGION_ADDR(region, 0, area->top + i),
368+
sz);
369+
370+
jxl->write_y += 1;
371+
372+
/* If we've filled the frame, add it to the encoder.
373+
*/
374+
if (jxl->write_y == jxl->page_height) {
375+
if (vips_foreign_save_jxl_add_frame(jxl))
376+
return -1;
377+
378+
jxl->write_y = 0;
379+
}
380+
}
381+
382+
return 0;
383+
}
384+
236385
static int
237386
vips_foreign_save_jxl_add_metadata(VipsForeignSaveJxl *jxl, VipsImage *in)
238387
{
@@ -312,18 +461,23 @@ vips_foreign_save_jxl_build(VipsObject *object)
312461
VipsForeignSaveJxl *jxl = (VipsForeignSaveJxl *) object;
313462
VipsImage **t = (VipsImage **) vips_object_local_array(object, 5);
314463

315-
#ifdef HAVE_LIBJXL_0_7
316-
JxlEncoderFrameSettings *frame_settings;
317-
#else
318-
JxlEncoderOptions *frame_settings;
319-
#endif
320-
JxlEncoderStatus status;
321464
VipsImage *in;
322465
VipsBandFormat format;
466+
int i;
323467

324468
if (VIPS_OBJECT_CLASS(vips_foreign_save_jxl_parent_class)->build(object))
325469
return -1;
326470

471+
#ifdef HAVE_LIBJXL_0_7
472+
jxl->page_height = vips_image_get_page_height(save->ready);
473+
#else
474+
/* libjxl prior to 0.7 doesn't seem to have API for saving animations
475+
*/
476+
jxl->page_height = save->ready->Ysize;
477+
#endif /*HAVE_LIBJXL_0_7*/
478+
479+
jxl->page_count = save->ready->Ysize / jxl->page_height;
480+
327481
/* If Q is set and distance is not, use Q to set a rough distance
328482
* value. Formula stolen from cjxl.c and very roughly approximates
329483
* libjpeg values.
@@ -411,11 +565,26 @@ vips_foreign_save_jxl_build(VipsObject *object)
411565
in->Bands - jxl->info.num_color_channels);
412566

413567
jxl->info.xsize = in->Xsize;
414-
jxl->info.ysize = in->Ysize;
568+
jxl->info.ysize = jxl->page_height;
415569
jxl->format.num_channels = in->Bands;
416570
jxl->format.endianness = JXL_NATIVE_ENDIAN;
417571
jxl->format.align = 0;
418572

573+
#ifdef HAVE_LIBJXL_0_7
574+
if (jxl->page_count > 1) {
575+
int num_loops = 0;
576+
577+
if (vips_image_get_typeof(in, "loop"))
578+
vips_image_get_int(in, "loop", &num_loops);
579+
580+
jxl->info.have_animation = TRUE;
581+
jxl->info.animation.tps_numerator = 1000;
582+
jxl->info.animation.tps_denominator = 1;
583+
jxl->info.animation.num_loops = num_loops;
584+
jxl->info.animation.have_timecodes = FALSE;
585+
}
586+
#endif /*HAVE_LIBJXL_0_7*/
587+
419588
if (vips_image_hasalpha(in)) {
420589
jxl->info.alpha_bits = jxl->info.bits_per_sample;
421590
jxl->info.alpha_exponent_bits =
@@ -496,27 +665,39 @@ vips_foreign_save_jxl_build(VipsObject *object)
496665
if (vips_foreign_save_jxl_add_metadata(jxl, in))
497666
return -1;
498667

499-
/* Render the entire image in memory. libjxl seems to be missing
500-
* tile-based write at the moment.
501-
*/
502-
if (vips_image_wio_input(in))
668+
if (vips_foreign_save_jxl_process_output(jxl))
503669
return -1;
504670

505-
#ifdef HAVE_LIBJXL_0_7
506-
frame_settings = JxlEncoderFrameSettingsCreate(jxl->encoder, NULL);
507-
JxlEncoderFrameSettingsSetOption(frame_settings,
508-
JXL_ENC_FRAME_SETTING_DECODING_SPEED, jxl->tier);
509-
JxlEncoderSetFrameDistance(frame_settings, jxl->distance);
510-
JxlEncoderFrameSettingsSetOption(frame_settings,
511-
JXL_ENC_FRAME_SETTING_EFFORT, jxl->effort);
512-
JxlEncoderSetFrameLossless(frame_settings, jxl->lossless);
513-
#else
514-
frame_settings = JxlEncoderOptionsCreate(jxl->encoder, NULL);
515-
JxlEncoderOptionsSetDecodingSpeed(frame_settings, jxl->tier);
516-
JxlEncoderOptionsSetDistance(frame_settings, jxl->distance);
517-
JxlEncoderOptionsSetEffort(frame_settings, jxl->effort);
518-
JxlEncoderOptionsSetLossless(frame_settings, jxl->lossless);
519-
#endif
671+
if (jxl->info.have_animation) {
672+
/* Get delay array
673+
*
674+
* There might just be the old gif-delay field. This is centiseconds.
675+
*/
676+
jxl->gif_delay = 10;
677+
if (vips_image_get_typeof(save->ready, "gif-delay") &&
678+
vips_image_get_int(save->ready, "gif-delay",
679+
&jxl->gif_delay))
680+
return -1;
681+
682+
/* New images have an array of ints instead.
683+
*/
684+
jxl->delay = NULL;
685+
if (vips_image_get_typeof(save->ready, "delay") &&
686+
vips_image_get_array_int(save->ready, "delay",
687+
&jxl->delay, &jxl->delay_length))
688+
return -1;
689+
690+
/* Force frames with a small or no duration to 100ms
691+
* to be consistent with web browsers and other
692+
* transcoding tools.
693+
*/
694+
if (jxl->gif_delay <= 1)
695+
jxl->gif_delay = 10;
696+
697+
for (i = 0; i < jxl->delay_length; i++)
698+
if (jxl->delay[i] <= 10)
699+
jxl->delay[i] = 100;
700+
}
520701

521702
#ifdef DEBUG
522703
vips_foreign_save_jxl_print_info(&jxl->info);
@@ -528,44 +709,26 @@ vips_foreign_save_jxl_build(VipsObject *object)
528709
printf(" lossless = %d\n", jxl->lossless);
529710
#endif /*DEBUG*/
530711

531-
if (JxlEncoderAddImageFrame(frame_settings, &jxl->format,
532-
VIPS_IMAGE_ADDR(in, 0, 0),
533-
VIPS_IMAGE_SIZEOF_IMAGE(in))) {
534-
vips_foreign_save_jxl_error(jxl, "JxlEncoderAddImageFrame");
712+
/* RGB(A) frame as a contiguous buffer.
713+
*/
714+
jxl->frame_size = VIPS_IMAGE_SIZEOF_LINE(in) * jxl->page_height;
715+
jxl->frame_bytes = g_try_malloc(jxl->frame_size);
716+
if (jxl->frame_bytes == NULL) {
717+
vips_error("jxlsave",
718+
_("failed to allocate %zu bytes"), jxl->frame_size);
535719
return -1;
536720
}
537721

722+
if (vips_sink_disc(in, vips_foreign_save_jxl_sink_disc, jxl))
723+
return -1;
724+
538725
/* This function must be called after the final frame and/or box,
539726
* otherwise the codestream will not be encoded correctly.
540727
*/
541728
JxlEncoderCloseInput(jxl->encoder);
542729

543-
do {
544-
uint8_t *out;
545-
size_t avail_out;
546-
547-
out = jxl->output_buffer;
548-
avail_out = OUTPUT_BUFFER_SIZE;
549-
status = JxlEncoderProcessOutput(jxl->encoder,
550-
&out, &avail_out);
551-
switch (status) {
552-
case JXL_ENC_SUCCESS:
553-
case JXL_ENC_NEED_MORE_OUTPUT:
554-
if (vips_target_write(jxl->target,
555-
jxl->output_buffer,
556-
OUTPUT_BUFFER_SIZE - avail_out))
557-
return -1;
558-
break;
559-
560-
default:
561-
vips_foreign_save_jxl_error(jxl,
562-
"JxlEncoderProcessOutput");
563-
#ifdef DEBUG
564-
vips_foreign_save_jxl_print_status(status);
565-
#endif /*DEBUG*/
566-
return -1;
567-
}
568-
} while (status != JXL_ENC_SUCCESS);
730+
if (vips_foreign_save_jxl_process_output(jxl))
731+
return -1;
569732

570733
if (vips_target_end(jxl->target))
571734
return -1;

0 commit comments

Comments
 (0)