diff --git a/ChangeLog b/ChangeLog index b865ed75..2824d7b9 100644 --- a/ChangeLog +++ b/ChangeLog @@ -11,6 +11,7 @@ - don't use atexit for cleanup, it's too unreliable - tiff writer loops for the whole image rather than per page [LionelArn2] - fix VipsSource with named pipes [vibbix] +- added restart_interval option to jpegsave [manthey] 16/8/21 started 8.11.4 - fix off-by-one error in new rank fast path diff --git a/libvips/foreign/jpegsave.c b/libvips/foreign/jpegsave.c index be3e2ec9..87a519ca 100644 --- a/libvips/foreign/jpegsave.c +++ b/libvips/foreign/jpegsave.c @@ -103,6 +103,10 @@ typedef struct _VipsForeignSaveJpeg { */ int quant_table; + /* Use an MCU restart interval. + */ + int restart_interval; + } VipsForeignSaveJpeg; typedef VipsForeignSaveClass VipsForeignSaveJpegClass; @@ -231,6 +235,14 @@ vips_foreign_save_jpeg_class_init( VipsForeignSaveJpegClass *class ) G_STRUCT_OFFSET( VipsForeignSaveJpeg, subsample_mode ), VIPS_TYPE_FOREIGN_SUBSAMPLE, VIPS_FOREIGN_SUBSAMPLE_AUTO ); + + VIPS_ARG_INT( class, "restart_interval", 20, + _( "Restart interval" ), + _( "Add restart markers every specified number of mcu" ), + VIPS_ARGUMENT_OPTIONAL_INPUT, + G_STRUCT_OFFSET( VipsForeignSaveJpeg, restart_interval ), + 0, INT_MAX, 0 ); + } static void @@ -268,7 +280,8 @@ vips_foreign_save_jpeg_target_build( VipsObject *object ) jpeg->Q, jpeg->profile, jpeg->optimize_coding, jpeg->interlace, save->strip, jpeg->trellis_quant, jpeg->overshoot_deringing, jpeg->optimize_scans, - jpeg->quant_table, jpeg->subsample_mode ) ) + jpeg->quant_table, jpeg->subsample_mode, + jpeg->restart_interval ) ) return( -1 ); return( 0 ); @@ -334,7 +347,8 @@ vips_foreign_save_jpeg_file_build( VipsObject *object ) jpeg->Q, jpeg->profile, jpeg->optimize_coding, jpeg->interlace, save->strip, jpeg->trellis_quant, jpeg->overshoot_deringing, jpeg->optimize_scans, - jpeg->quant_table, jpeg->subsample_mode ) ) { + jpeg->quant_table, jpeg->subsample_mode, + jpeg->restart_interval ) ) { VIPS_UNREF( target ); return( -1 ); } @@ -404,7 +418,8 @@ vips_foreign_save_jpeg_buffer_build( VipsObject *object ) jpeg->Q, jpeg->profile, jpeg->optimize_coding, jpeg->interlace, save->strip, jpeg->trellis_quant, jpeg->overshoot_deringing, jpeg->optimize_scans, - jpeg->quant_table, jpeg->subsample_mode ) ) { + jpeg->quant_table, jpeg->subsample_mode, + jpeg->restart_interval ) ) { VIPS_UNREF( target ); return( -1 ); } @@ -477,7 +492,8 @@ vips_foreign_save_jpeg_mime_build( VipsObject *object ) jpeg->Q, jpeg->profile, jpeg->optimize_coding, jpeg->interlace, save->strip, jpeg->trellis_quant, jpeg->overshoot_deringing, jpeg->optimize_scans, - jpeg->quant_table, jpeg->subsample_mode ) ) { + jpeg->quant_table, jpeg->subsample_mode, + jpeg->restart_interval ) ) { VIPS_UNREF( target ); return( -1 ); } @@ -534,6 +550,7 @@ vips_foreign_save_jpeg_mime_init( VipsForeignSaveJpegMime *mime ) * * @overshoot_deringing: %gboolean, overshoot samples with extreme values * * @optimize_scans: %gboolean, split DCT coefficients into separate scans * * @quant_table: %gint, quantization table index + * * @restart_interval: %gint, restart interval in mcu * * Write a VIPS image to a file as JPEG. * @@ -604,6 +621,12 @@ vips_foreign_save_jpeg_mime_init( VipsForeignSaveJpegMime *mime ) * For maximum compression with mozjpeg, a useful set of options is `strip, * optimize-coding, interlace, optimize-scans, trellis-quant, quant_table=3`. * + * By default, the output stream won't have restart markers. If a non-zero + * restart_interval is specified, a restart marker will be added after each + * specified number of MCU blocks. This makes the stream more recoverable + * if there are transmission errors, but also allows for some decoders to read + * part of the JPEG without decoding the whole stream. + * * The image is automatically converted to RGB, Monochrome or CMYK before * saving. * @@ -650,6 +673,7 @@ vips_jpegsave( VipsImage *in, const char *filename, ... ) * * @overshoot_deringing: %gboolean, overshoot samples with extreme values * * @optimize_scans: %gboolean, split DCT coefficients into separate scans * * @quant_table: %gint, quantization table index + * * @restart_interval: %gint, restart interval in mcu * * As vips_jpegsave(), but save to a target. * @@ -689,6 +713,7 @@ vips_jpegsave_target( VipsImage *in, VipsTarget *target, ... ) * * @overshoot_deringing: %gboolean, overshoot samples with extreme values * * @optimize_scans: %gboolean, split DCT coefficients into separate scans * * @quant_table: %gint, quantization table index + * * @restart_interval: %gint, restart interval in mcu * * As vips_jpegsave(), but save to a memory buffer. * @@ -745,6 +770,7 @@ vips_jpegsave_buffer( VipsImage *in, void **buf, size_t *len, ... ) * * @overshoot_deringing: %gboolean, overshoot samples with extreme values * * @optimize_scans: %gboolean, split DCT coefficients into separate scans * * @quant_table: %gint, quantization table index + * * @restart_interval: %gint, restart interval in mcu * * As vips_jpegsave(), but save as a mime jpeg on stdout. * diff --git a/libvips/foreign/pforeign.h b/libvips/foreign/pforeign.h index 1f8fcee2..393f37db 100644 --- a/libvips/foreign/pforeign.h +++ b/libvips/foreign/pforeign.h @@ -174,7 +174,7 @@ int vips__jpeg_write_target( VipsImage *in, VipsTarget *target, gboolean optimize_coding, gboolean progressive, gboolean strip, gboolean trellis_quant, gboolean overshoot_deringing, gboolean optimize_scans, int quant_table, - VipsForeignSubsample subsample_mode ); + VipsForeignSubsample subsample_mode, int restart_interval ); int vips__jpeg_read_source( VipsSource *source, VipsImage *out, gboolean header_only, int shrink, int fail, gboolean autorotate ); @@ -252,5 +252,3 @@ extern const char *vips__jxl_suffs[]; #endif /*__cplusplus*/ #endif /*VIPS_PFOREIGN_H*/ - - diff --git a/libvips/foreign/vips2jpeg.c b/libvips/foreign/vips2jpeg.c index 18240558..e8183179 100644 --- a/libvips/foreign/vips2jpeg.c +++ b/libvips/foreign/vips2jpeg.c @@ -96,6 +96,8 @@ * - add subsample_mode, deprecate no_subsample * 13/9/20 * - only write JFIF resolution if we don't have EXIF + * 7/10/21 Manthey + * - add restart_interval */ /* @@ -540,7 +542,7 @@ write_vips( Write *write, int qfac, const char *profile, gboolean optimize_coding, gboolean progressive, gboolean strip, gboolean trellis_quant, gboolean overshoot_deringing, gboolean optimize_scans, int quant_table, - VipsForeignSubsample subsample_mode ) + VipsForeignSubsample subsample_mode, int restart_interval ) { VipsImage *in; J_COLOR_SPACE space; @@ -607,6 +609,10 @@ write_vips( Write *write, int qfac, const char *profile, */ write->cinfo.optimize_coding = optimize_coding; + /* Use a restart interval. + */ + if( restart_interval > 0 ) + write->cinfo.restart_interval = restart_interval; #ifdef HAVE_JPEG_EXT_PARAMS /* Apply trellis quantisation to each 8x8 block. Implies * "optimize_coding". @@ -846,7 +852,8 @@ vips__jpeg_write_target( VipsImage *in, VipsTarget *target, gboolean optimize_coding, gboolean progressive, gboolean strip, gboolean trellis_quant, gboolean overshoot_deringing, gboolean optimize_scans, - int quant_table, VipsForeignSubsample subsample_mode) + int quant_table, VipsForeignSubsample subsample_mode, + int restart_interval) { Write *write; @@ -873,7 +880,7 @@ vips__jpeg_write_target( VipsImage *in, VipsTarget *target, if( write_vips( write, Q, profile, optimize_coding, progressive, strip, trellis_quant, overshoot_deringing, optimize_scans, - quant_table, subsample_mode ) ) { + quant_table, subsample_mode, restart_interval ) ) { write_destroy( write ); return( -1 ); } diff --git a/test/test-suite/test_foreign.py b/test/test-suite/test_foreign.py index 5c4dd7f2..38e129a9 100644 --- a/test/test-suite/test_foreign.py +++ b/test/test-suite/test_foreign.py @@ -294,6 +294,15 @@ class TestForeign: assert len(q90_subsample_on) < len(q90) assert len(q90_subsample_off) == len(q90_subsample_auto) + # A non-zero restart_interval should result in a bigger file. + # Otherwise, smaller restart intervals will have more restart markers + # and therefore be larger + r0 = im.jpegsave_buffer(restart_interval=0) + r10 = im.jpegsave_buffer(restart_interval=10) + r2 = im.jpegsave_buffer(restart_interval=2) + assert len(r10) > len(r0) + assert len(r2) > len(r10) + @skip_if_no("jpegload") def test_truncated(self): # This should open (there's enough there for the header)