diff --git a/configure.ac b/configure.ac index 76b4f054..f707c791 100644 --- a/configure.ac +++ b/configure.ac @@ -1142,6 +1142,24 @@ if test x"$with_png" != "xno"; then ) fi +# look for libimagequant with pkg-config (only if libpng is enabled) +AC_ARG_WITH([imagequant], + AS_HELP_STRING([--without-imagequant], [build without imagequant (default: test)])) + +if test x"$with_imagequant" != "xno" && test x"$with_png" != "xno"; then + PKG_CHECK_MODULES(IMAGEQUANT, imagequant, + [AC_DEFINE(HAVE_IMAGEQUANT,1,[define if you have imagequant installed.]) + with_imagequant=yes + PACKAGES_USED="$PACKAGES_USED imagequant" + ], + [AC_MSG_WARN([libimagequant not found; disabling 8bpp PNG support]) + with_imagequant=no + ] + ) +else + with_imagequant=no +fi + # look for libjpeg with pkg-config ... fall back to our tester AC_ARG_WITH([jpeg], AS_HELP_STRING([--without-jpeg], [build without libjpeg (default: test)])) @@ -1260,14 +1278,14 @@ fi # Gather all up for VIPS_CFLAGS, VIPS_INCLUDES, VIPS_LIBS # sort includes to get longer, more specific dirs first # helps, for example, selecting graphicsmagick over imagemagick -VIPS_CFLAGS=`for i in $VIPS_CFLAGS $GTHREAD_CFLAGS $REQUIRED_CFLAGS $EXPAT_CFLAGS $ZLIB_CFLAGS $PANGOFT2_CFLAGS $GSF_CFLAGS $FFTW_CFLAGS $MAGICK_CFLAGS $JPEG_CFLAGS $PNG_CFLAGS $EXIF_CFLAGS $MATIO_CFLAGS $CFITSIO_CFLAGS $LIBWEBP_CFLAGS $LIBWEBPMUX_CFLAGS $GIFLIB_INCLUDES $RSVG_CFLAGS $PDFIUM_INCLUDES $POPPLER_CFLAGS $OPENEXR_CFLAGS $OPENSLIDE_CFLAGS $ORC_CFLAGS $TIFF_CFLAGS $LCMS_CFLAGS +VIPS_CFLAGS=`for i in $VIPS_CFLAGS $GTHREAD_CFLAGS $REQUIRED_CFLAGS $EXPAT_CFLAGS $ZLIB_CFLAGS $PANGOFT2_CFLAGS $GSF_CFLAGS $FFTW_CFLAGS $MAGICK_CFLAGS $JPEG_CFLAGS $PNG_CFLAGS $IMAGEQUANT_CFLAGS $EXIF_CFLAGS $MATIO_CFLAGS $CFITSIO_CFLAGS $LIBWEBP_CFLAGS $LIBWEBPMUX_CFLAGS $GIFLIB_INCLUDES $RSVG_CFLAGS $PDFIUM_INCLUDES $POPPLER_CFLAGS $OPENEXR_CFLAGS $OPENSLIDE_CFLAGS $ORC_CFLAGS $TIFF_CFLAGS $LCMS_CFLAGS do echo $i done | sort -ru` VIPS_CFLAGS=`echo $VIPS_CFLAGS` VIPS_CFLAGS="$VIPS_DEBUG_FLAGS $VIPS_CFLAGS" VIPS_INCLUDES="$ZLIB_INCLUDES $PNG_INCLUDES $TIFF_INCLUDES $JPEG_INCLUDES" -VIPS_LIBS="$ZLIB_LIBS $MAGICK_LIBS $PNG_LIBS $TIFF_LIBS $JPEG_LIBS $GTHREAD_LIBS $REQUIRED_LIBS $EXPAT_LIBS $PANGOFT2_LIBS $GSF_LIBS $FFTW_LIBS $ORC_LIBS $LCMS_LIBS $GIFLIB_LIBS $RSVG_LIBS $PDFIUM_LIBS $POPPLER_LIBS $OPENEXR_LIBS $OPENSLIDE_LIBS $CFITSIO_LIBS $LIBWEBP_LIBS $LIBWEBPMUX_LIBS $MATIO_LIBS $EXIF_LIBS -lm" +VIPS_LIBS="$ZLIB_LIBS $MAGICK_LIBS $PNG_LIBS $IMAGEQUANT_LIBS $TIFF_LIBS $JPEG_LIBS $GTHREAD_LIBS $REQUIRED_LIBS $EXPAT_LIBS $PANGOFT2_LIBS $GSF_LIBS $FFTW_LIBS $ORC_LIBS $LCMS_LIBS $GIFLIB_LIBS $RSVG_LIBS $PDFIUM_LIBS $POPPLER_LIBS $OPENEXR_LIBS $OPENSLIDE_LIBS $CFITSIO_LIBS $LIBWEBP_LIBS $LIBWEBPMUX_LIBS $MATIO_LIBS $EXIF_LIBS -lm" AC_SUBST(VIPS_LIBDIR) @@ -1375,6 +1393,7 @@ support webp metadata: $with_libwebpmux text rendering with pangoft2: $with_pangoft2 file import/export with libpng: $with_png (requires libpng-1.2.9 or later) +support 8bpp PNG quantisation: $with_imagequant file import/export with libtiff: $with_tiff file import/export with giflib: $with_giflib file import/export with libjpeg: $with_jpeg diff --git a/libvips/foreign/pforeign.h b/libvips/foreign/pforeign.h index bc521f47..50e632e7 100644 --- a/libvips/foreign/pforeign.h +++ b/libvips/foreign/pforeign.h @@ -197,10 +197,12 @@ 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, + gboolean palette, int colours, 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, + gboolean palette, int colours, 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..2761a291 100644 --- a/libvips/foreign/pngsave.c +++ b/libvips/foreign/pngsave.c @@ -60,6 +60,10 @@ typedef struct _VipsForeignSavePng { gboolean interlace; char *profile; VipsForeignPngFilter filter; + gboolean palette; + int colours; + int Q; + double dither; } VipsForeignSavePng; typedef VipsForeignSaveClass VipsForeignSavePngClass; @@ -133,6 +137,34 @@ vips_foreign_save_png_class_init( VipsForeignSavePngClass *class ) VIPS_TYPE_FOREIGN_PNG_FILTER, VIPS_FOREIGN_PNG_FILTER_ALL ); + VIPS_ARG_BOOL( class, "palette", 13, + _( "Palette" ), + _( "Quantise to 8bpp palette" ), + VIPS_ARGUMENT_OPTIONAL_INPUT, + 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" ), + VIPS_ARGUMENT_OPTIONAL_INPUT, + G_STRUCT_OFFSET( VipsForeignSavePng, Q ), + 0, 100, 100 ); + + VIPS_ARG_DOUBLE( class, "dither", 16, + _( "Dithering" ), + _( "Amount of dithering" ), + VIPS_ARGUMENT_OPTIONAL_INPUT, + G_STRUCT_OFFSET( VipsForeignSavePng, dither ), + 0.0, 1.0, 1.0 ); + } static void @@ -140,6 +172,9 @@ 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; } typedef struct _VipsForeignSavePngFile { @@ -166,7 +201,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->palette, + png->colours, png->Q, png->dither ) ) return( -1 ); return( 0 ); @@ -225,7 +261,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->palette, png->colours, png->Q, png->dither ) ) return( -1 ); /* vips__png_write_buf() makes a buffer that needs g_free(), not @@ -278,6 +314,10 @@ vips_foreign_save_png_buffer_init( VipsForeignSavePngBuffer *buffer ) * * @interlace: interlace image * * @profile: ICC profile to embed * * @filter: #VipsForeignPngFilter 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) + * * @dither: amount of dithering for 8bpp quantization * * Write a VIPS image to a file as PNG. * @@ -304,6 +344,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. * + * 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. + * 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..fd9da69e 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 +quantise_image( VipsImage *in, VipsImage *out, VipsImage *palette_out, + int colours, 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( !vips_image_hasalpha(in) ) { + 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, colours ); + 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 *quantisation_result; + if ( liq_image_quantize( input_image, attr, &quantisation_result ) ) { + liq_result_destroy( quantisation_result ); + liq_image_destroy( input_image ); + liq_attr_destroy( attr ); + VIPS_UNREF( memory ); + return( -1 ); + } + + liq_set_dithering_level( quantisation_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( quantisation_result ); + liq_image_destroy( input_image ); + liq_attr_destroy( attr ); + VIPS_UNREF( memory ); + return( -1 ); + } + + if( liq_write_remapped_image( quantisation_result, input_image, + VIPS_IMAGE_ADDR( out, 0, 0 ), VIPS_IMAGE_N_PELS( out ) ) ) { + liq_result_destroy( quantisation_result ); + liq_image_destroy( input_image ); + liq_attr_destroy( attr ); + VIPS_UNREF( memory ); + return( -1 ); + } + + const liq_palette *palette = liq_get_palette( quantisation_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( quantisation_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( quantisation_result ); + liq_image_destroy( input_image ); + liq_attr_destroy( attr ); + VIPS_UNREF( memory ); + + return( 0 ); +} +#endif /*HAVE_IMAGEQUANT*/ + /* 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, + gboolean palette, int colours, int Q, double dither ) { VipsImage *in = write->in; @@ -942,6 +1044,20 @@ write_vips( Write *write, return( -1 ); } +#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; + color_type = PNG_COLOR_TYPE_PALETTE; + } +#else + if( palette ) + g_warning( "%s", + _( "ignoring palette (no quantisation support)" ) ); +#endif /*HAVE_IMAGEQUANT*/ + interlace_type = interlace ? PNG_INTERLACE_ADAM7 : PNG_INTERLACE_NONE; png_set_IHDR( write->pPng, write->pInfo, @@ -994,7 +1110,66 @@ write_vips( Write *write, PNG_COMPRESSION_TYPE_BASE, data, length ); } - png_write_info( write->pPng, write->pInfo ); +#ifdef HAVE_IMAGEQUANT + if( palette ) { + VipsImage *im_quantised = vips_image_new_memory(); + VipsImage *im_palette = vips_image_new_memory(); + if( quantise_image( in, im_quantised, im_palette, colours, Q, + dither ) ) { + vips_error( "vips2png", + "%s", _( "quantisation failed" ) ); + VIPS_UNREF( im_quantised ); + VIPS_UNREF( im_palette ); + return( -1 ); + } + + int palette_count = im_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( im_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( im_palette ); + + VIPS_UNREF( write->memory ); + write->memory = im_quantised; + in = write->memory; + } +#endif /*HAVE_IMAGEQUANT*/ + 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 +1202,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, + gboolean palette, int colours, int Q, double dither ) { Write *write; @@ -1047,7 +1223,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, palette, + colours, Q, dither ) ) { vips_error( "vips2png", _( "unable to write \"%s\"" ), filename ); @@ -1074,7 +1251,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, + gboolean palette, int colours, int Q, double dither ) { Write *write; @@ -1086,10 +1264,11 @@ vips__png_write_buf( VipsImage *in, /* Convert it! */ if( write_vips( write, - compression, interlace, profile, filter, strip ) ) { + compression, interlace, profile, filter, strip, palette, + colours, Q, dither ) ) { vips_error( "vips2png", "%s", _( "unable to write to buffer" ) ); - + return( -1 ); } diff --git a/test/test_formats.sh b/test/test_formats.sh index abaa5241..60a4cccc 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 [palette,colours=256,Q=100,dither=0,interlace=1] fi if test_supported jpegload; then test_format $image jpg 90