diff --git a/ChangeLog b/ChangeLog index 3966b177..cc007f1f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -31,8 +31,9 @@ - deprecate heifload autorotate -- it's now always on - revised resize improves accuracy [kleisauke] - add --vips-config flag to show configuration info -- add "bitdepth" param to tiff load and save, deprecate "squash" [MathemanFlo] +- add "bitdepth" param to tiff save, deprecate "squash" [MathemanFlo] - tiff load and save now supports 2 and 4 bit data [MathemanFlo] +- pngsave @bitdepth parameter lets you write 1, 2 and 4 bit PNGs - ppmsave also uses "bitdepth" now, for consistency 24/4/20 started 8.9.3 diff --git a/libvips/foreign/pforeign.h b/libvips/foreign/pforeign.h index e5a5e07e..6adea1bc 100644 --- a/libvips/foreign/pforeign.h +++ b/libvips/foreign/pforeign.h @@ -184,7 +184,8 @@ extern const char *vips__png_suffs[]; int vips__png_write_target( VipsImage *in, VipsTarget *target, int compress, int interlace, const char *profile, VipsForeignPngFilter filter, gboolean strip, - gboolean palette, int colours, int Q, double dither ); + gboolean palette, int Q, double dither, + int bitdepth ); /* Map WEBP metadata names to vips names. */ diff --git a/libvips/foreign/pngsave.c b/libvips/foreign/pngsave.c index 2af92e39..7113d972 100644 --- a/libvips/foreign/pngsave.c +++ b/libvips/foreign/pngsave.c @@ -6,6 +6,8 @@ * - compression should be 0-9, not 1-10 * 20/6/18 [felixbuenemann] * - support png8 palette write with palette, colours, Q, dither + * 24/6/20 + * - add @bitdepth, deprecate @colours */ /* @@ -63,9 +65,17 @@ typedef struct _VipsForeignSavePng { char *profile; VipsForeignPngFilter filter; gboolean palette; - int colours; int Q; double dither; + int bitdepth; + + /* Set by subclasses. + */ + VipsTarget *target; + + /* Deprecated. + */ + int colours; } VipsForeignSavePng; typedef VipsForeignSaveClass VipsForeignSavePngClass; @@ -73,6 +83,55 @@ typedef VipsForeignSaveClass VipsForeignSavePngClass; G_DEFINE_ABSTRACT_TYPE( VipsForeignSavePng, vips_foreign_save_png, VIPS_TYPE_FOREIGN_SAVE ); +static void +vips_foreign_save_png_dispose( GObject *gobject ) +{ + VipsForeignSavePng *png = (VipsForeignSavePng *) gobject; + + if( png->target ) + vips_target_finish( png->target ); + VIPS_UNREF( png->target ); + + G_OBJECT_CLASS( vips_foreign_save_png_parent_class )-> + dispose( gobject ); +} + +static int +vips_foreign_save_png_build( VipsObject *object ) +{ + VipsForeignSave *save = (VipsForeignSave *) object; + VipsForeignSavePng *png = (VipsForeignSavePng *) object; + + if( VIPS_OBJECT_CLASS( vips_foreign_save_png_parent_class )-> + build( object ) ) + return( -1 ); + + /* Deprecated "colours" arg just sets bitdepth large enough to hold + * that many colours. + */ + if( vips_object_argument_isset( object, "colours" ) ) + png->bitdepth = ceil( log2( png->colours ) ); + + if( !vips_object_argument_isset( object, "bitdepth" ) ) + png->bitdepth = + save->ready->BandFmt == VIPS_FORMAT_UCHAR ? 8 : 16; + + /* If this is a RGB or RGBA image and a low bit depth has been + * requested, enable palettization. + */ + if( save->ready->Bands > 2 && + png->bitdepth < 8 ) + png->palette = TRUE; + + if( vips__png_write_target( save->ready, png->target, + png->compression, png->interlace, png->profile, png->filter, + save->strip, png->palette, png->Q, png->dither, + png->bitdepth ) ) + return( -1 ); + + return( 0 ); +} + /* Save a bit of typing. */ #define UC VIPS_FORMAT_UCHAR @@ -99,11 +158,13 @@ vips_foreign_save_png_class_init( VipsForeignSavePngClass *class ) VipsForeignClass *foreign_class = (VipsForeignClass *) class; VipsForeignSaveClass *save_class = (VipsForeignSaveClass *) class; + gobject_class->dispose = vips_foreign_save_png_dispose; gobject_class->set_property = vips_object_set_property; gobject_class->get_property = vips_object_get_property; object_class->nickname = "pngsave_base"; object_class->description = _( "save png" ); + object_class->build = vips_foreign_save_png_build; foreign_class->suffs = vips__png_suffs; @@ -146,13 +207,6 @@ vips_foreign_save_png_class_init( VipsForeignSavePngClass *class ) G_STRUCT_OFFSET( VipsForeignSavePng, palette ), FALSE ); - VIPS_ARG_INT( class, "colours", 14, - _( "Colours" ), - _( "Max number of palette colours" ), - VIPS_ARGUMENT_OPTIONAL_INPUT, - G_STRUCT_OFFSET( VipsForeignSavePng, colours ), - 2, 256, 256 ); - VIPS_ARG_INT( class, "Q", 15, _( "Quality" ), _( "Quantisation quality" ), @@ -167,6 +221,20 @@ vips_foreign_save_png_class_init( VipsForeignSavePngClass *class ) G_STRUCT_OFFSET( VipsForeignSavePng, dither ), 0.0, 1.0, 1.0 ); + VIPS_ARG_INT( class, "bitdepth", 17, + _( "Bit depth" ), + _( "Write as a 1, 2, 4 or 8 bit image" ), + VIPS_ARGUMENT_OPTIONAL_INPUT, + G_STRUCT_OFFSET( VipsForeignSavePng, bitdepth ), + 0, 8, 0 ); + + VIPS_ARG_INT( class, "colours", 14, + _( "Colours" ), + _( "Max number of palette colours" ), + VIPS_ARGUMENT_OPTIONAL_INPUT | VIPS_ARGUMENT_DEPRECATED, + G_STRUCT_OFFSET( VipsForeignSavePng, colours ), + 2, 256, 256 ); + } static void @@ -174,7 +242,6 @@ vips_foreign_save_png_init( VipsForeignSavePng *png ) { png->compression = 6; png->filter = VIPS_FOREIGN_PNG_FILTER_ALL; - png->colours = 256; png->Q = 100; png->dither = 1.0; } @@ -193,19 +260,16 @@ G_DEFINE_TYPE( VipsForeignSavePngTarget, vips_foreign_save_png_target, static int vips_foreign_save_png_target_build( VipsObject *object ) { - VipsForeignSave *save = (VipsForeignSave *) object; VipsForeignSavePng *png = (VipsForeignSavePng *) object; VipsForeignSavePngTarget *target = (VipsForeignSavePngTarget *) object; + png->target = target->target; + g_object_ref( png->target ); + if( VIPS_OBJECT_CLASS( vips_foreign_save_png_target_parent_class )-> build( object ) ) return( -1 ); - if( vips__png_write_target( save->ready, target->target, - png->compression, png->interlace, png->profile, png->filter, - save->strip, png->palette, png->colours, png->Q, png->dither ) ) - return( -1 ); - return( 0 ); } @@ -250,27 +314,16 @@ G_DEFINE_TYPE( VipsForeignSavePngFile, vips_foreign_save_png_file, static int vips_foreign_save_png_file_build( VipsObject *object ) { - VipsForeignSave *save = (VipsForeignSave *) object; VipsForeignSavePng *png = (VipsForeignSavePng *) object; - VipsForeignSavePngFile *png_file = (VipsForeignSavePngFile *) object; + VipsForeignSavePngFile *file = (VipsForeignSavePngFile *) object; - VipsTarget *target; + if( !(png->target = vips_target_new_to_file( file->filename )) ) + return( -1 ); if( VIPS_OBJECT_CLASS( vips_foreign_save_png_file_parent_class )-> build( object ) ) return( -1 ); - if( !(target = vips_target_new_to_file( png_file->filename )) ) - return( -1 ); - if( vips__png_write_target( save->ready, target, - png->compression, png->interlace, - png->profile, png->filter, save->strip, png->palette, - png->colours, png->Q, png->dither ) ) { - VIPS_UNREF( target ); - return( -1 ); - } - VIPS_UNREF( target ); - return( 0 ); } @@ -314,34 +367,22 @@ G_DEFINE_TYPE( VipsForeignSavePngBuffer, vips_foreign_save_png_buffer, static int vips_foreign_save_png_buffer_build( VipsObject *object ) { - VipsForeignSave *save = (VipsForeignSave *) object; VipsForeignSavePng *png = (VipsForeignSavePng *) object; VipsForeignSavePngBuffer *buffer = (VipsForeignSavePngBuffer *) object; - VipsTarget *target; VipsBlob *blob; + if( !(png->target = vips_target_new_to_memory()) ) + return( -1 ); + if( VIPS_OBJECT_CLASS( vips_foreign_save_png_buffer_parent_class )-> build( object ) ) return( -1 ); - if( !(target = vips_target_new_to_memory()) ) - return( -1 ); - - if( vips__png_write_target( save->ready, target, - png->compression, png->interlace, png->profile, png->filter, - save->strip, png->palette, png->colours, png->Q, - png->dither ) ) { - VIPS_UNREF( target ); - return( -1 ); - } - - g_object_get( target, "blob", &blob, NULL ); + g_object_get( png->target, "blob", &blob, NULL ); g_object_set( buffer, "buffer", blob, NULL ); vips_area_unref( VIPS_AREA( blob ) ); - VIPS_UNREF( target ); - return( 0 ); } @@ -386,9 +427,9 @@ vips_foreign_save_png_buffer_init( VipsForeignSavePngBuffer *buffer ) * * @profile: %gchararray, ICC profile to embed * * @filter: #VipsForeignPngFilter row filter flag(s) * * @palette: %gboolean, enable quantisation to 8bpp palette - * * @colours: %gint, max number of palette colours for quantisation - * * @Q: %gint, quality for 8bpp quantisation (does not exceed @colours) + * * @Q: %gint, quality for 8bpp quantisation * * @dither: %gdouble, amount of dithering for 8bpp quantization + * * @bitdepth: %int, set write bit depth to 1, 2, 4 or 8 * * Write a VIPS image to a file as PNG. * @@ -414,13 +455,15 @@ vips_foreign_save_png_buffer_init( VipsForeignSavePngBuffer *buffer ) * alpha before saving. Images with more than one byte per band element are * saved as 16-bit PNG, others are saved as 8-bit PNG. * - * Set @palette to %TRUE to enable quantisation to an 8-bit per pixel palette - * image with alpha transparency support. If @colours is given, it limits the - * maximum number of palette entries. Similar to JPEG the quality can also be - * be changed with the @Q parameter which further reduces the palette size and - * @dither controls the amount of Floyd-Steinberg dithering. + * Set @palette to %TRUE to enable palette mode for RGB or RGBA images. A + * palette will be computed with enough space for @bitdepth (1, 2, 4 or 8) + * bits. Use @Q to set the optimisation effort, and @dither to set the degree of + * Floyd-Steinberg dithering. * This feature requires libvips to be compiled with libimagequant. * + * You can also set @bitdepth for mono and mono + alpha images, and the image + * will be quantized. + * * XMP metadata is written to the XMP chunk. PNG comments are written to * separate text chunks. * @@ -455,9 +498,9 @@ vips_pngsave( VipsImage *in, const char *filename, ... ) * * @profile: %gchararray, ICC profile to embed * * @filter: #VipsForeignPngFilter row filter flag(s) * * @palette: %gboolean, enable quantisation to 8bpp palette - * * @colours: %gint, max number of palette colours for quantisation - * * @Q: %gint, quality for 8bpp quantisation (does not exceed @colours) + * * @Q: %gint, quality for 8bpp quantisation * * @dither: %gdouble, amount of dithering for 8bpp quantization + * * @bitdepth: %int, set write bit depth to 1, 2, 4 or 8 * * As vips_pngsave(), but save to a memory buffer. * @@ -510,9 +553,9 @@ vips_pngsave_buffer( VipsImage *in, void **buf, size_t *len, ... ) * * @profile: ICC profile to embed * * @filter: libpng row filter flag(s) * * @palette: enable quantisation to 8bpp palette - * * @colours: max number of palette colours for quantisation - * * @Q: quality for 8bpp quantisation (does not exceed @colours) + * * @Q: quality for 8bpp quantisation * * @dither: amount of dithering for 8bpp quantization + * * @bitdepth: %int, set write bit depth to 1, 2, 4 or 8 * * As vips_pngsave(), but save to a target. * diff --git a/libvips/foreign/vipspng.c b/libvips/foreign/vipspng.c index 1d9d6330..c5c3e730 100644 --- a/libvips/foreign/vipspng.c +++ b/libvips/foreign/vipspng.c @@ -355,7 +355,7 @@ static int png2vips_header( Read *read, VipsImage *out ) { png_uint_32 width, height; - int bit_depth, color_type; + int bitdepth, color_type; int interlace_type; png_uint_32 res_x, res_y; @@ -385,7 +385,7 @@ png2vips_header( Read *read, VipsImage *out ) return( -1 ); png_get_IHDR( read->pPng, read->pInfo, - &width, &height, &bit_depth, &color_type, + &width, &height, &bitdepth, &color_type, &interlace_type, NULL, NULL ); /* png_get_channels() gives us 1 band for palette images ... so look @@ -413,7 +413,7 @@ png2vips_header( Read *read, VipsImage *out ) return( -1 ); } - if( bit_depth > 8 ) { + if( bitdepth > 8 ) { if( bands < 3 ) interpretation = VIPS_INTERPRETATION_GREY16; else @@ -448,13 +448,13 @@ png2vips_header( Read *read, VipsImage *out ) /* Expand <8 bit images to full bytes. */ if( color_type == PNG_COLOR_TYPE_GRAY && - bit_depth < 8 ) + bitdepth < 8 ) png_set_expand_gray_1_2_4_to_8( read->pPng ); /* If we're an INTEL byte order machine and this is 16bits, we need * to swap bytes. */ - if( bit_depth > 8 && + if( bitdepth > 8 && !vips_amiMSBfirst() ) png_set_swap( read->pPng ); @@ -480,8 +480,7 @@ png2vips_header( Read *read, VipsImage *out ) */ vips_image_init_fields( out, width, height, bands, - bit_depth > 8 ? - VIPS_FORMAT_USHORT : VIPS_FORMAT_UCHAR, + bitdepth > 8 ? VIPS_FORMAT_USHORT : VIPS_FORMAT_UCHAR, VIPS_CODING_NONE, interpretation, Xres, Yres ); @@ -556,7 +555,7 @@ png2vips_header( Read *read, VipsImage *out ) /* Attach original palette bit depth, if any, as metadata. */ if( color_type == PNG_COLOR_TYPE_PALETTE ) - vips_image_set_int( out, "palette-bit-depth", bit_depth ); + vips_image_set_int( out, "palette-bit-depth", bitdepth ); return( 0 ); } @@ -924,11 +923,11 @@ static int write_vips( Write *write, int compress, int interlace, const char *profile, VipsForeignPngFilter filter, gboolean strip, - gboolean palette, int colours, int Q, double dither ) + gboolean palette, int Q, double dither, + int bitdepth ) { VipsImage *in = write->in; - int bit_depth; int color_type; int interlace_type; int i, nb_passes; @@ -970,8 +969,6 @@ write_vips( Write *write, */ png_set_filter( write->pPng, 0, filter ); - bit_depth = in->BandFmt == VIPS_FORMAT_UCHAR ? 8 : 16; - switch( in->Bands ) { case 1: color_type = PNG_COLOR_TYPE_GRAY; break; case 2: color_type = PNG_COLOR_TYPE_GRAY_ALPHA; break; @@ -987,12 +984,8 @@ write_vips( Write *write, #ifdef HAVE_IMAGEQUANT /* Enable image quantisation to paletted 8bpp PNG if colours is set. */ - if( palette ) { - g_assert( colours >= 2 && - colours <= 256 ); - bit_depth = 8; + if( palette ) color_type = PNG_COLOR_TYPE_PALETTE; - } #else if( palette ) g_warning( "%s", @@ -1002,7 +995,7 @@ write_vips( Write *write, interlace_type = interlace ? PNG_INTERLACE_ADAM7 : PNG_INTERLACE_NONE; png_set_IHDR( write->pPng, write->pInfo, - in->Xsize, in->Ysize, bit_depth, color_type, interlace_type, + in->Xsize, in->Ysize, bitdepth, color_type, interlace_type, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT ); /* Set resolution. libpng uses pixels per meter. @@ -1107,7 +1100,7 @@ write_vips( Write *write, int trans_count; if( vips__quantise_image( in, &im_index, &im_palette, - colours, Q, dither ) ) + 1 << bitdepth, Q, dither ) ) return( -1 ); palette_count = im_palette->Xsize; @@ -1167,10 +1160,14 @@ write_vips( Write *write, /* If we're an intel byte order CPU and this is a 16bit image, we need * to swap bytes. */ - if( bit_depth > 8 && + if( bitdepth > 8 && !vips_amiMSBfirst() ) png_set_swap( write->pPng ); + /* If bitdepth is 1/2/4, pack pixels into bytes. + */ + png_set_packing( write->pPng ); + if( interlace ) nb_passes = png_set_interlace_handling( write->pPng ); else @@ -1196,7 +1193,8 @@ int vips__png_write_target( VipsImage *in, VipsTarget *target, int compression, int interlace, const char *profile, VipsForeignPngFilter filter, gboolean strip, - gboolean palette, int colours, int Q, double dither ) + gboolean palette, int Q, double dither, + int bitdepth ) { Write *write; @@ -1205,7 +1203,7 @@ vips__png_write_target( VipsImage *in, VipsTarget *target, if( write_vips( write, compression, interlace, profile, filter, strip, palette, - colours, Q, dither ) ) { + Q, dither, bitdepth ) ) { write_finish( write ); vips_error( "vips2png", "%s", _( "unable to write to target" ) ); diff --git a/test/test-suite/test_foreign.py b/test/test-suite/test_foreign.py index 698cc835..ce9fbfb7 100644 --- a/test/test-suite/test_foreign.py +++ b/test/test-suite/test_foreign.py @@ -313,6 +313,22 @@ class TestForeign: self.save_load_file(".png", "[interlace]", self.colour, 0) self.save_load_file(".png", "[interlace]", self.mono, 0) + # size of a regular mono PNG + len_mono = len(self.mono.write_to_buffer(".png")) + + # 4-bit should be smaller + len_mono4 = len(self.mono.write_to_buffer(".png", bitdepth=4)) + assert( len_mono4 < len_mono ) + + len_mono2 = len(self.mono.write_to_buffer(".png", bitdepth=2)) + assert( len_mono2 < len_mono4 ) + + len_mono1 = len(self.mono.write_to_buffer(".png", bitdepth=1)) + assert( len_mono1 < len_mono2 ) + + # we can't test palette save since we can't be sure libimagequant is + # available + @skip_if_no("tiffload") def test_tiff(self): def tiff_valid(im):