Merge pull request #1009 from felixbuenemann/8bit-png-quantization-support

8-Bit PNG Quantization Support
This commit is contained in:
John Cupitt 2018-06-20 13:09:17 +01:00 committed by GitHub
commit a3780a761e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 262 additions and 14 deletions

View File

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

View File

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

View File

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

View File

@ -124,6 +124,10 @@
#error "PNG library too old."
#endif
#ifdef HAVE_IMAGEQUANT
#include <libimagequant.h>
#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 );
}

View File

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