add 1/2/4 bit PNG save

- new @bitdepth param
- deprecate @colours param
This commit is contained in:
John Cupitt 2020-06-25 10:40:43 +01:00
parent 31184ddb57
commit 4bf27acccb
5 changed files with 139 additions and 80 deletions

View File

@ -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

View File

@ -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.
*/

View File

@ -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.
*

View File

@ -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" ) );

View File

@ -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):