@@ -85,6 +85,17 @@ typedef struct _VipsForeignSaveJxl {
85
85
gboolean lossless ;
86
86
int Q ;
87
87
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
+
88
99
/* Base image properties.
89
100
*/
90
101
JxlBasicInfo info ;
@@ -96,6 +107,20 @@ typedef struct _VipsForeignSaveJxl {
96
107
void * runner ;
97
108
JxlEncoder * encoder ;
98
109
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
+
99
124
/* Write buffer.
100
125
*/
101
126
uint8_t output_buffer [OUTPUT_BUFFER_SIZE ];
@@ -126,6 +151,8 @@ vips_foreign_save_jxl_dispose(GObject *gobject)
126
151
127
152
VIPS_FREEF (JxlThreadParallelRunnerDestroy , jxl -> runner );
128
153
VIPS_FREEF (JxlEncoderDestroy , jxl -> encoder );
154
+ VIPS_FREE (jxl -> frame_bytes );
155
+
129
156
VIPS_UNREF (jxl -> target );
130
157
131
158
G_OBJECT_CLASS (vips_foreign_save_jxl_parent_class )-> dispose (gobject );
@@ -233,6 +260,128 @@ vips_foreign_save_jxl_print_status(JxlEncoderStatus status)
233
260
}
234
261
#endif /*DEBUG*/
235
262
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
+
236
385
static int
237
386
vips_foreign_save_jxl_add_metadata (VipsForeignSaveJxl * jxl , VipsImage * in )
238
387
{
@@ -312,18 +461,23 @@ vips_foreign_save_jxl_build(VipsObject *object)
312
461
VipsForeignSaveJxl * jxl = (VipsForeignSaveJxl * ) object ;
313
462
VipsImage * * t = (VipsImage * * ) vips_object_local_array (object , 5 );
314
463
315
- #ifdef HAVE_LIBJXL_0_7
316
- JxlEncoderFrameSettings * frame_settings ;
317
- #else
318
- JxlEncoderOptions * frame_settings ;
319
- #endif
320
- JxlEncoderStatus status ;
321
464
VipsImage * in ;
322
465
VipsBandFormat format ;
466
+ int i ;
323
467
324
468
if (VIPS_OBJECT_CLASS (vips_foreign_save_jxl_parent_class )-> build (object ))
325
469
return -1 ;
326
470
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
+
327
481
/* If Q is set and distance is not, use Q to set a rough distance
328
482
* value. Formula stolen from cjxl.c and very roughly approximates
329
483
* libjpeg values.
@@ -411,11 +565,26 @@ vips_foreign_save_jxl_build(VipsObject *object)
411
565
in -> Bands - jxl -> info .num_color_channels );
412
566
413
567
jxl -> info .xsize = in -> Xsize ;
414
- jxl -> info .ysize = in -> Ysize ;
568
+ jxl -> info .ysize = jxl -> page_height ;
415
569
jxl -> format .num_channels = in -> Bands ;
416
570
jxl -> format .endianness = JXL_NATIVE_ENDIAN ;
417
571
jxl -> format .align = 0 ;
418
572
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
+
419
588
if (vips_image_hasalpha (in )) {
420
589
jxl -> info .alpha_bits = jxl -> info .bits_per_sample ;
421
590
jxl -> info .alpha_exponent_bits =
@@ -496,27 +665,39 @@ vips_foreign_save_jxl_build(VipsObject *object)
496
665
if (vips_foreign_save_jxl_add_metadata (jxl , in ))
497
666
return -1 ;
498
667
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 ))
503
669
return -1 ;
504
670
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
+ }
520
701
521
702
#ifdef DEBUG
522
703
vips_foreign_save_jxl_print_info (& jxl -> info );
@@ -528,44 +709,26 @@ vips_foreign_save_jxl_build(VipsObject *object)
528
709
printf (" lossless = %d\n" , jxl -> lossless );
529
710
#endif /*DEBUG*/
530
711
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 );
535
719
return -1 ;
536
720
}
537
721
722
+ if (vips_sink_disc (in , vips_foreign_save_jxl_sink_disc , jxl ))
723
+ return -1 ;
724
+
538
725
/* This function must be called after the final frame and/or box,
539
726
* otherwise the codestream will not be encoded correctly.
540
727
*/
541
728
JxlEncoderCloseInput (jxl -> encoder );
542
729
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 ;
569
732
570
733
if (vips_target_end (jxl -> target ))
571
734
return -1 ;
0 commit comments