diff --git a/libvips/foreign/pforeign.h b/libvips/foreign/pforeign.h index bc521f47..9bb72022 100644 --- a/libvips/foreign/pforeign.h +++ b/libvips/foreign/pforeign.h @@ -197,10 +197,11 @@ int vips__png_header_buffer( const void *buffer, size_t length, VipsImage *out ) int vips__png_write( VipsImage *in, const char *filename, int compress, int interlace, const char *profile, - VipsForeignPngFilter filter, gboolean strip ); + VipsForeignPngFilter filter, gboolean strip, int colors, int Q, double dither ); int vips__png_write_buf( VipsImage *in, void **obuf, size_t *olen, int compression, int interlace, - const char *profile, VipsForeignPngFilter filter, gboolean strip ); + const char *profile, VipsForeignPngFilter filter, gboolean strip, + int colors, int Q, double dither ); /* Map WEBP metadata names to vips names. */ diff --git a/libvips/foreign/pngsave.c b/libvips/foreign/pngsave.c index a6e2cf13..32cbb6ce 100644 --- a/libvips/foreign/pngsave.c +++ b/libvips/foreign/pngsave.c @@ -60,6 +60,9 @@ typedef struct _VipsForeignSavePng { gboolean interlace; char *profile; VipsForeignPngFilter filter; + int colors; + int Q; + double dither; } VipsForeignSavePng; typedef VipsForeignSaveClass VipsForeignSavePngClass; @@ -133,6 +136,27 @@ vips_foreign_save_png_class_init( VipsForeignSavePngClass *class ) VIPS_TYPE_FOREIGN_PNG_FILTER, VIPS_FOREIGN_PNG_FILTER_ALL ); + VIPS_ARG_INT( class, "colors", 13, + _( "Colors" ), + _( "Number of palette entries" ), + VIPS_ARGUMENT_OPTIONAL_INPUT, + G_STRUCT_OFFSET( VipsForeignSavePng, colors ), + 2, 256, 256 ); + + VIPS_ARG_INT( class, "Q", 14, + _( "Q" ), + _( "Q factor" ), + VIPS_ARGUMENT_OPTIONAL_INPUT, + G_STRUCT_OFFSET( VipsForeignSavePng, Q ), + 0, 100, 75 ); + + VIPS_ARG_DOUBLE( class, "dither", 15, + _( "Dithering" ), + _( "Amount of dithering" ), + VIPS_ARGUMENT_OPTIONAL_INPUT, + G_STRUCT_OFFSET( VipsForeignSavePng, dither ), + 0.0, 1.0, 1.0 ); + } static void @@ -140,6 +164,9 @@ vips_foreign_save_png_init( VipsForeignSavePng *png ) { png->compression = 6; png->filter = VIPS_FOREIGN_PNG_FILTER_ALL; + png->colors = 0; + png->Q = 75; + png->dither = 1.0; } typedef struct _VipsForeignSavePngFile { @@ -166,7 +193,8 @@ vips_foreign_save_png_file_build( VipsObject *object ) if( vips__png_write( save->ready, png_file->filename, png->compression, png->interlace, - png->profile, png->filter, save->strip ) ) + png->profile, png->filter, save->strip, png->colors, png->Q, + png->dither ) ) return( -1 ); return( 0 ); @@ -225,7 +253,7 @@ vips_foreign_save_png_buffer_build( VipsObject *object ) if( vips__png_write_buf( save->ready, &obuf, &olen, png->compression, png->interlace, png->profile, png->filter, - save->strip ) ) + save->strip, png->colors, png->Q, png->dither ) ) return( -1 ); /* vips__png_write_buf() makes a buffer that needs g_free(), not @@ -278,6 +306,9 @@ vips_foreign_save_png_buffer_init( VipsForeignSavePngBuffer *buffer ) * * @interlace: interlace image * * @profile: ICC profile to embed * * @filter: #VipsForeignPngFilter row filter flag(s) + * * @colors: enable 8bpp quantization with max n colors + * * @Q: quality for 8bpp quantization (does not exceed @colors) + * * @dither: amount of dithering for 8bpp quantization * * Write a VIPS image to a file as PNG. * @@ -304,6 +335,13 @@ 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. * + * If @colors is given, it limits the maximum number of colors in the image + * and the source image will be quantized down to an 8-Bit one band indexed + * image with palette based alpha transparency. Similar to JPEG the quality + * can be controlled with the @Q parameter and the amount of Floyd-Steinberg + * dithering is set with @dither. + * This feature requires libvips to be compiled with libimagequant. + * * See also: vips_image_new_from_file(). * * Returns: 0 on success, -1 on error. diff --git a/libvips/foreign/vipspng.c b/libvips/foreign/vipspng.c index e4913c97..5b810ecd 100644 --- a/libvips/foreign/vipspng.c +++ b/libvips/foreign/vipspng.c @@ -124,6 +124,10 @@ #error "PNG library too old." #endif +#ifdef HAVE_IMAGEQUANT +#include +#endif + static void user_error_function( png_structp png_ptr, png_const_charp error_msg ) { @@ -877,12 +881,110 @@ write_png_block( VipsRegion *region, VipsRect *area, void *a ) return( 0 ); } +#ifdef HAVE_IMAGEQUANT +static int +quantize_image( VipsImage *in, VipsImage *out, VipsImage *palette_out, + int colors, int Q, double dither ) +{ + /* Ensure input is sRGB. */ + if( in->Type != VIPS_INTERPRETATION_sRGB) { + VipsImage *srgb; + if( vips_colourspace( in, &srgb, VIPS_INTERPRETATION_sRGB, + NULL ) ) + return( -1 ); + in = srgb; + VIPS_UNREF( srgb ); + } + /* Add alpha channel if missing. */ + if( in->Bands == 3 ) { + VipsImage *srgba; + if( vips_bandjoin_const1( in, &srgba, 255, NULL ) ) + return( -1 ); + in = srgba; + VIPS_UNREF( srgba ); + } + VipsImage *memory; + if( !(memory = vips_image_copy_memory( in )) ) + return( -1 ); + in = memory; + + liq_attr *attr = liq_attr_create(); + liq_set_max_colors( attr, colors ); + liq_set_quality( attr, 0, Q ); + + liq_image *input_image = liq_image_create_rgba( attr, + VIPS_IMAGE_ADDR( in, 0, 0 ), in->Xsize, in->Ysize, 0 ); + + liq_result *quantization_result; + if ( liq_image_quantize( input_image, attr, &quantization_result ) ) { + liq_result_destroy( quantization_result ); + liq_image_destroy( input_image ); + liq_attr_destroy( attr ); + VIPS_UNREF( memory ); + return( -1 ); + } + + liq_set_dithering_level( quantization_result, (float) dither ); + + vips_image_init_fields( out, in->Xsize, in->Ysize, 1, VIPS_FORMAT_UCHAR, + VIPS_CODING_NONE, VIPS_INTERPRETATION_B_W, 1.0, 1.0 ); + + if( vips_image_write_prepare( out ) ) { + liq_result_destroy( quantization_result ); + liq_image_destroy( input_image ); + liq_attr_destroy( attr ); + VIPS_UNREF( memory ); + return( -1 ); + } + + if( liq_write_remapped_image( quantization_result, input_image, + VIPS_IMAGE_ADDR( out, 0, 0 ), VIPS_IMAGE_N_PELS( out ) ) ) { + liq_result_destroy( quantization_result ); + liq_image_destroy( input_image ); + liq_attr_destroy( attr ); + VIPS_UNREF( memory ); + return( -1 ); + } + + const liq_palette *palette = liq_get_palette( quantization_result ); + + vips_image_init_fields( palette_out, palette->count, 1, 4, + VIPS_FORMAT_UCHAR, VIPS_CODING_NONE, VIPS_INTERPRETATION_sRGB, + 1.0, 1.0 ); + + if( vips_image_write_prepare( palette_out ) ) { + liq_result_destroy( quantization_result ); + liq_image_destroy( input_image ); + liq_attr_destroy( attr ); + VIPS_UNREF( memory ); + return( -1 ); + } + + int i; + for( i = 0; i < palette->count; i++ ) { + unsigned char *p = VIPS_IMAGE_ADDR( palette_out, i, 0 ); + p[0] = palette->entries[i].r; + p[1] = palette->entries[i].g; + p[2] = palette->entries[i].b; + p[3] = palette->entries[i].a; + } + + liq_result_destroy( quantization_result ); + liq_image_destroy( input_image ); + liq_attr_destroy( attr ); + VIPS_UNREF( memory ); + + return( 0 ); +} +#endif + /* Write a VIPS image to PNG. */ static int write_vips( Write *write, int compress, int interlace, const char *profile, - VipsForeignPngFilter filter, gboolean strip ) + VipsForeignPngFilter filter, gboolean strip, int colors, int Q, + double dither ) { VipsImage *in = write->in; @@ -942,6 +1044,19 @@ write_vips( Write *write, return( -1 ); } +#ifdef HAVE_IMAGEQUANT + /* Enable image quantization to paletted 8bpp PNG if colors is set. + */ + if( colors ) { + g_assert( colors >= 2 && colors <= 256 ); + bit_depth = 8; + color_type = PNG_COLOR_TYPE_PALETTE; + } +#else + if( colors ) + g_warning( "%s", _( "ignoring colors" ) ); +#endif + interlace_type = interlace ? PNG_INTERLACE_ADAM7 : PNG_INTERLACE_NONE; png_set_IHDR( write->pPng, write->pInfo, @@ -994,7 +1109,66 @@ write_vips( Write *write, PNG_COMPRESSION_TYPE_BASE, data, length ); } - png_write_info( write->pPng, write->pInfo ); +#ifdef HAVE_IMAGEQUANT + if( colors ) { + VipsImage *quantized = vips_image_new_memory(); + VipsImage *palette = vips_image_new_memory(); + if( quantize_image( in, quantized, palette, colors, Q, + dither ) ) { + vips_error( "vips2png", + "%s", _( "quantization failed" ) ); + VIPS_UNREF( quantized ); + VIPS_UNREF( palette ); + return( -1 ); + } + + int palette_count = palette->Xsize; + g_assert( palette_count <= PNG_MAX_PALETTE_LENGTH); + + png_color *png_palette = (png_color *) png_malloc( write->pPng, + palette_count * sizeof( png_color ) ); + png_byte *png_trans = (png_byte *) png_malloc( write->pPng, + palette_count * sizeof( png_byte ) ); + int trans_count = 0; + + for( i = 0; i < palette_count; i++ ) { + png_byte *p = (png_byte *) VIPS_IMAGE_ADDR( palette, i, + 0 ); + png_color *col = &png_palette[i]; + col->red = p[0]; + col->green = p[1]; + col->blue = p[2]; + png_trans[i] = p[3]; + if( p[3] != 255 ) + trans_count = i + 1; +#ifdef DEBUG + printf( "write_vips: palette[%d] %d %d %d %d\n", + i + 1, p[0], p[1], p[2], p[3] ); +#endif /*DEBUG*/ + } + +#ifdef DEBUG + printf( "write_vips: attaching %d color palette\n", + palette_count ); +#endif /*DEBUG*/ + png_set_PLTE( write->pPng, write->pInfo, png_palette, + palette_count ); + if( trans_count ) { +#ifdef DEBUG + printf( "write_vips: attaching %d alpha values\n", + trans_count ); +#endif /*DEBUG*/ + png_set_tRNS( write->pPng, write->pInfo, png_trans, + trans_count, NULL ); + } + VIPS_UNREF( palette ); + + VIPS_UNREF( write->memory ); + write->memory = quantized; + in = write->memory; + } +#endif + png_write_info( write->pPng, write->pInfo ); /* If we're an intel byte order CPU and this is a 16bit image, we need * to swap bytes. @@ -1027,7 +1201,8 @@ write_vips( Write *write, int vips__png_write( VipsImage *in, const char *filename, int compress, int interlace, const char *profile, - VipsForeignPngFilter filter, gboolean strip ) + VipsForeignPngFilter filter, gboolean strip, + int colors, int Q, double dither ) { Write *write; @@ -1047,7 +1222,8 @@ vips__png_write( VipsImage *in, const char *filename, /* Convert it! */ if( write_vips( write, - compress, interlace, profile, filter, strip ) ) { + compress, interlace, profile, filter, strip, colors, Q, + dither ) ) { vips_error( "vips2png", _( "unable to write \"%s\"" ), filename ); @@ -1074,7 +1250,8 @@ user_write_data( png_structp png_ptr, png_bytep data, png_size_t length ) int vips__png_write_buf( VipsImage *in, void **obuf, size_t *olen, int compression, int interlace, - const char *profile, VipsForeignPngFilter filter, gboolean strip ) + const char *profile, VipsForeignPngFilter filter, gboolean strip, + int colors, int Q, double dither ) { Write *write; @@ -1086,7 +1263,8 @@ vips__png_write_buf( VipsImage *in, /* Convert it! */ if( write_vips( write, - compression, interlace, profile, filter, strip ) ) { + compression, interlace, profile, filter, strip, colors, Q, + dither ) ) { vips_error( "vips2png", "%s", _( "unable to write to buffer" ) ); diff --git a/test/test_formats.sh b/test/test_formats.sh index abaa5241..5ff76dc5 100755 --- a/test/test_formats.sh +++ b/test/test_formats.sh @@ -196,7 +196,8 @@ if test_supported tiffload; then fi if test_supported pngload; then test_format $image png 0 - test_format $image png 0 [compression=9,interlace=1] + test_format $image png 0 [compression=9,interlace=1] + test_format $image png 90 [colors=256,Q=100,dither=0,compression=9,interlace=1] fi if test_supported jpegload; then test_format $image jpg 90