cgifsave: palette change POC (#2824)

* cgifsave: compare palettes instead of frame sum

* Add vips__quantise_image_quantize_fixed function
This commit is contained in:
Sergey Alexandrovich 2022-06-08 20:27:29 +06:00 committed by GitHub
parent 5106e9b49e
commit 19bef959d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 255 additions and 77 deletions

View File

@ -61,8 +61,9 @@ typedef struct _VipsForeignSaveCgif {
double dither; double dither;
int effort; int effort;
int bitdepth; int bitdepth;
double maxerror; double interframe_maxerror;
gboolean reoptimise; gboolean reoptimise;
double interpalette_maxerror;
VipsTarget *target; VipsTarget *target;
/* Derived write params. /* Derived write params.
@ -85,8 +86,7 @@ typedef struct _VipsForeignSaveCgif {
*/ */
VipsQuantiseAttr *attr; VipsQuantiseAttr *attr;
VipsQuantiseImage *input_image; VipsQuantiseImage *input_image;
VipsQuantiseResult *quantisation_result; VipsQuantiseResult *quantisation_result, *local_quantisation_result;
const VipsQuantisePalette *lp;
/* The current colourmap, updated on a significant frame change. /* The current colourmap, updated on a significant frame change.
*/ */
@ -133,6 +133,8 @@ vips_foreign_save_cgif_dispose( GObject *gobject )
VIPS_FREEF( cgif_close, cgif->cgif_context ); VIPS_FREEF( cgif_close, cgif->cgif_context );
VIPS_FREEF( vips__quantise_result_destroy, cgif->quantisation_result ); VIPS_FREEF( vips__quantise_result_destroy, cgif->quantisation_result );
VIPS_FREEF( vips__quantise_result_destroy,
cgif->local_quantisation_result );
VIPS_FREEF( vips__quantise_image_destroy, cgif->input_image ); VIPS_FREEF( vips__quantise_image_destroy, cgif->input_image );
VIPS_FREEF( vips__quantise_attr_destroy, cgif->attr ); VIPS_FREEF( vips__quantise_attr_destroy, cgif->attr );
@ -182,6 +184,58 @@ vips_foreign_save_cgif_pixels_are_equal( const VipsPel *cur, const VipsPel *bef,
return( dR * dR + dG * dG + dB * dB <= sq_maxerror ); return( dR * dR + dG * dG + dB * dB <= sq_maxerror );
} }
static double
vips__cgif_compare_palettes(const VipsQuantisePalette *new,
const VipsQuantisePalette *old)
{
g_assert( new->count <= 256 );
g_assert( old->count <= 256 );
int i, j;
double best_dist, dist, rd, gd, bd;
double total_dist = 0.0;
for( i = 0; i < new->count; i++ ) {
best_dist = 255.0 * 255.0 * 3;
for( j = 0; j < old->count; j++ ) {
if( new->entries[i].a >= 128 ) {
/* The new entry is solid.
* If the old entry is transparent, ignore it.
* Otherwise, compare RGB.
*/
if( old->entries[j].a < 128 )
continue;
rd = new->entries[i].r - old->entries[j].r;
gd = new->entries[i].g - old->entries[j].g;
bd = new->entries[i].b - old->entries[j].b;
dist = rd*rd + gd*gd + bd*bd;
best_dist = VIPS_MIN(best_dist, dist);
/* We found the closest entry
*/
if( best_dist == 0 )
break;
} else {
/* The new entry is transparent.
* If the old entry is transparent too, it's
* the closest color. Otherwise, ignore it.
*/
if( old->entries[j].a < 128 ) {
best_dist = 0;
break;
}
}
}
total_dist += best_dist;
}
return total_dist / new->count;
}
/* We have a complete frame --- write! /* We have a complete frame --- write!
*/ */
static int static int
@ -201,6 +255,10 @@ vips_foreign_save_cgif_write_frame( VipsForeignSaveCgif *cgif )
VipsPel * restrict bef; VipsPel * restrict bef;
gboolean has_alpha_constraint = FALSE; gboolean has_alpha_constraint = FALSE;
VipsPel *rgb; VipsPel *rgb;
VipsQuantiseResult *quantisation_result;
const VipsQuantisePalette *lp, *pal_global, *pal_local;
double pal_change_global, pal_change_local;
gboolean use_local_palette = FALSE;
CGIF_FrameConfig frame_config; CGIF_FrameConfig frame_config;
#ifdef DEBUG_VERBOSE #ifdef DEBUG_VERBOSE
@ -243,90 +301,123 @@ vips_foreign_save_cgif_write_frame( VipsForeignSaveCgif *cgif )
cur += 4; cur += 4;
} }
/* Do we need to compute a new palette? Do it if the frame sum /* Do we need to compute a new palette? Do it if the palette changes.
* changes.
*
* frame_checksum 0 means no current colourmap.
*/ */
if( !cgif->global_colour_table ) { if( cgif->global_colour_table ) {
gint64 checksum; quantisation_result = cgif->quantisation_result;
lp = vips__quantise_get_palette( quantisation_result );
/* We need a checksum which detects colour changes, but } else {
* doesn't care about pixel ordering. if( vips__quantise_image_quantize_fixed( cgif->input_image,
* cgif->attr, &quantisation_result ) ) {
* Scale RGBA differently so that changes like [0, 255, 0] vips_error( class->nickname,
* to [255, 0, 0] are detected. "%s", _( "quantisation failed" ) );
*/ return( -1 );
checksum = 0;
cur = frame_bytes;
for( i = 0; i < n_pels; i++ ) {
checksum += cur[0] * 1000;
checksum += cur[1] * 100;
checksum += cur[2] * 10;
checksum += cur[3];
cur += 4;
} }
lp = vips__quantise_get_palette( quantisation_result );
if( cgif->frame_checksum == 0 || if( !cgif->quantisation_result ) {
checksum != cgif->frame_checksum ) { /* This is the first frame, save global quantization
cgif->frame_checksum = checksum; * result and palette
/* If this is not our first cmap, make a note that we
* need to attach it as a local cmap when we write.
*/ */
if( cgif->quantisation_result ) cgif->quantisation_result = quantisation_result;
cgif->cgif_config.attrFlags |=
CGIF_ATTR_NO_GLOBAL_TABLE;
VIPS_FREEF( vips__quantise_result_destroy, } else {
pal_global = vips__quantise_get_palette(
cgif->quantisation_result ); cgif->quantisation_result );
if( vips__quantise_image_quantize( cgif->input_image, pal_change_global = vips__cgif_compare_palettes(
cgif->attr, &cgif->quantisation_result ) ) { lp, pal_global );
vips_error( class->nickname,
"%s", _( "quantisation failed" ) ); if ( !cgif->local_quantisation_result )
return( -1 ); pal_change_local = 255*255*3;
else {
pal_local = vips__quantise_get_palette(
cgif->local_quantisation_result );
pal_change_local = vips__cgif_compare_palettes(
lp, pal_local );
} }
if(
pal_change_local <= pal_change_global &&
pal_change_local <= cgif->interpalette_maxerror
) {
/* Local palette change is low, use previous
* local quantization result and palette
*/
VIPS_FREEF( vips__quantise_result_destroy,
quantisation_result );
quantisation_result =
cgif->local_quantisation_result;
lp = pal_local;
use_local_palette = 1;
} else if(
pal_change_global <= cgif->interpalette_maxerror
) {
/* Global palette change is low, use global
* quantization result and palette
*/
VIPS_FREEF( vips__quantise_result_destroy,
quantisation_result );
quantisation_result = cgif->quantisation_result;
lp = pal_global;
/* Also drop saved local result as it's usage
* doesn't make sense now and it's better to
* use a new local result if neeeded
*/
VIPS_FREEF( vips__quantise_result_destroy,
cgif->local_quantisation_result );
cgif->local_quantisation_result = NULL;
} else {
/* Palette change is high, use local
* quantization result and palette
*/
VIPS_FREEF( vips__quantise_result_destroy,
cgif->local_quantisation_result );
cgif->local_quantisation_result =
quantisation_result;
use_local_palette = 1;
#ifdef DEBUG_PERCENT #ifdef DEBUG_PERCENT
cgif->n_cmaps_generated += 1; cgif->n_cmaps_generated += 1;
cgif->lp = vips__quantise_get_palette( printf( "frame %d, new %d item colourmap\n",
cgif->quantisation_result ); page_index, lp->count );
printf( "frame %d, new %d item colourmap\n",
page_index, cgif->lp->count );
#endif/*DEBUG_PERCENT*/ #endif/*DEBUG_PERCENT*/
}
} }
} }
/* Dither frame. /* Dither frame.
*/ */
vips__quantise_set_dithering_level( cgif->quantisation_result, vips__quantise_set_dithering_level( quantisation_result, cgif->dither );
cgif->dither ); if( vips__quantise_write_remapped_image( quantisation_result,
if( vips__quantise_write_remapped_image( cgif->quantisation_result,
cgif->input_image, cgif->index, n_pels ) ) { cgif->input_image, cgif->index, n_pels ) ) {
vips_error( class->nickname, "%s", _( "dither failed" ) ); vips_error( class->nickname, "%s", _( "dither failed" ) );
return( -1 ); return( -1 );
} }
/* Call vips__quantise_get_palette() after /* Call vips__quantise_get_palette() after vips__quantise_write_remapped_image(),
* vips__quantise_write_remapped_image(), as palette is improved * as palette is improved during remapping.
* during remapping. */
*/ lp = vips__quantise_get_palette( quantisation_result );
cgif->lp = vips__quantise_get_palette( cgif->quantisation_result );
rgb = cgif->palette_rgb;
g_assert( cgif->lp->count <= 256 );
for( i = 0; i < cgif->lp->count; i++ ) {
rgb[0] = cgif->lp->entries[i].r;
rgb[1] = cgif->lp->entries[i].g;
rgb[2] = cgif->lp->entries[i].b;
rgb += 3; if (use_local_palette || !cgif->cgif_context ) {
rgb = cgif->palette_rgb;
g_assert( lp->count <= 256 );
for( i = 0; i < lp->count; i++ ) {
rgb[0] = lp->entries[i].r;
rgb[1] = lp->entries[i].g;
rgb[2] = lp->entries[i].b;
rgb += 3;
}
} }
/* If there's a transparent pixel, it's always first. /* If there's a transparent pixel, it's always first.
*/ */
cgif->has_transparency = cgif->lp->entries[0].a == 0; cgif->has_transparency = lp->entries[0].a == 0;
/* Set up cgif on first use, so we can set the first cmap as the global /* Set up cgif on first use, so we can set the first cmap as the global
* one. * one.
@ -344,7 +435,7 @@ vips_foreign_save_cgif_write_frame( VipsForeignSaveCgif *cgif )
#endif/*HAVE_CGIF_ATTR_NO_LOOP*/ #endif/*HAVE_CGIF_ATTR_NO_LOOP*/
cgif->cgif_config.width = frame_rect->width; cgif->cgif_config.width = frame_rect->width;
cgif->cgif_config.height = frame_rect->height; cgif->cgif_config.height = frame_rect->height;
cgif->cgif_config.numGlobalPaletteEntries = cgif->lp->count; cgif->cgif_config.numGlobalPaletteEntries = lp->count;
#ifdef HAVE_CGIF_ATTR_NO_LOOP #ifdef HAVE_CGIF_ATTR_NO_LOOP
cgif->cgif_config.numLoops = cgif->loop > 1 ? cgif->cgif_config.numLoops = cgif->loop > 1 ?
cgif->loop - 1 : cgif->loop; cgif->loop - 1 : cgif->loop;
@ -392,14 +483,14 @@ vips_foreign_save_cgif_write_frame( VipsForeignSaveCgif *cgif )
uint8_t trans_index; uint8_t trans_index;
double sq_maxerror; double sq_maxerror;
trans_index = cgif->lp->count; trans_index = lp->count;
if( cgif->has_transparency ) { if( cgif->has_transparency ) {
trans_index = 0; trans_index = 0;
frame_config.attrFlags &= frame_config.attrFlags &=
~CGIF_FRAME_ATTR_HAS_ALPHA; ~CGIF_FRAME_ATTR_HAS_ALPHA;
} }
sq_maxerror = cgif->maxerror * cgif->maxerror; sq_maxerror = cgif->interframe_maxerror * cgif->interframe_maxerror;
for( i = 0; i < n_pels; i++ ) { for( i = 0; i < n_pels; i++ ) {
if( vips_foreign_save_cgif_pixels_are_equal( if( vips_foreign_save_cgif_pixels_are_equal(
@ -435,10 +526,10 @@ vips_foreign_save_cgif_write_frame( VipsForeignSaveCgif *cgif )
/* Attach a local palette, if we need one. /* Attach a local palette, if we need one.
*/ */
if( cgif->cgif_config.attrFlags & CGIF_ATTR_NO_GLOBAL_TABLE ) { if( use_local_palette ) {
frame_config.attrFlags |= CGIF_FRAME_ATTR_USE_LOCAL_TABLE; frame_config.attrFlags |= CGIF_FRAME_ATTR_USE_LOCAL_TABLE;
frame_config.pLocalPalette = cgif->palette_rgb; frame_config.pLocalPalette = cgif->palette_rgb;
frame_config.numLocalPaletteEntries = cgif->lp->count; frame_config.numLocalPaletteEntries = lp->count;
} }
cgif_addframe( cgif->cgif_context, &frame_config ); cgif_addframe( cgif->cgif_context, &frame_config );
@ -451,6 +542,7 @@ vips_foreign_save_cgif_write_frame( VipsForeignSaveCgif *cgif )
memcpy( cgif->frame_bytes_head, frame_bytes, 4 * n_pels ); memcpy( cgif->frame_bytes_head, frame_bytes, 4 * n_pels );
} }
return( 0 ); return( 0 );
} }
@ -560,7 +652,10 @@ vips_foreign_save_cgif_build( VipsObject *object )
frame_rect.top = 0; frame_rect.top = 0;
frame_rect.width = cgif->in->Xsize; frame_rect.width = cgif->in->Xsize;
frame_rect.height = page_height; frame_rect.height = page_height;
if( (guint64) frame_rect.width * frame_rect.height > 5000 * 5000 ) {
/* GIF has a limit of 64k per axis -- double-check this.
*/
if( frame_rect.width > 65535 || frame_rect.height > 65535 ) {
vips_error( class->nickname, "%s", _( "frame too large" ) ); vips_error( class->nickname, "%s", _( "frame too large" ) );
return( -1 ); return( -1 );
} }
@ -614,7 +709,7 @@ vips_foreign_save_cgif_build( VipsObject *object )
tmp_image = vips__quantise_image_create_rgba( cgif->attr, tmp_image = vips__quantise_image_create_rgba( cgif->attr,
tmp_gct, gct_length + 1, 1, 0 ); tmp_gct, gct_length + 1, 1, 0 );
if( vips__quantise_image_quantize( tmp_image, if( vips__quantise_image_quantize_fixed( tmp_image,
cgif->attr, &cgif->quantisation_result ) ) { cgif->attr, &cgif->quantisation_result ) ) {
vips_error( class->nickname, vips_error( class->nickname,
"%s", _( "quantisation failed" ) ); "%s", _( "quantisation failed" ) );
@ -691,11 +786,11 @@ vips_foreign_save_cgif_class_init( VipsForeignSaveCgifClass *class )
G_STRUCT_OFFSET( VipsForeignSaveCgif, bitdepth ), G_STRUCT_OFFSET( VipsForeignSaveCgif, bitdepth ),
1, 8, 8 ); 1, 8, 8 );
VIPS_ARG_DOUBLE( class, "maxerror", 13, VIPS_ARG_DOUBLE( class, "interframe_maxerror", 13,
_( "Maximum error" ), _( "Maximum inter-frame error" ),
_( "Maximum inter-frame error for transparency" ), _( "Maximum inter-frame error for transparency" ),
VIPS_ARGUMENT_OPTIONAL_INPUT, VIPS_ARGUMENT_OPTIONAL_INPUT,
G_STRUCT_OFFSET( VipsForeignSaveCgif, maxerror ), G_STRUCT_OFFSET( VipsForeignSaveCgif, interframe_maxerror ),
0, 32, 0.0 ); 0, 32, 0.0 );
VIPS_ARG_BOOL( class, "reoptimise", 14, VIPS_ARG_BOOL( class, "reoptimise", 14,
@ -704,6 +799,13 @@ vips_foreign_save_cgif_class_init( VipsForeignSaveCgifClass *class )
VIPS_ARGUMENT_OPTIONAL_INPUT, VIPS_ARGUMENT_OPTIONAL_INPUT,
G_STRUCT_OFFSET( VipsForeignSaveCgif, reoptimise ), G_STRUCT_OFFSET( VipsForeignSaveCgif, reoptimise ),
FALSE ); FALSE );
VIPS_ARG_DOUBLE( class, "interpalette_maxerror", 15,
_( "Maximum inter-palette error" ),
_( "Maximum inter-palette error for palette reusage" ),
VIPS_ARGUMENT_OPTIONAL_INPUT,
G_STRUCT_OFFSET( VipsForeignSaveCgif, interpalette_maxerror ),
0, 256, 64.0 );
} }
static void static void
@ -712,8 +814,9 @@ vips_foreign_save_cgif_init( VipsForeignSaveCgif *gif )
gif->dither = 1.0; gif->dither = 1.0;
gif->effort = 7; gif->effort = 7;
gif->bitdepth = 8; gif->bitdepth = 8;
gif->maxerror = 0.0; gif->interframe_maxerror = 0.0;
gif->reoptimise = FALSE; gif->reoptimise = FALSE;
gif->interpalette_maxerror = 64.0;
} }
typedef struct _VipsForeignSaveCgifTarget { typedef struct _VipsForeignSaveCgifTarget {
@ -894,8 +997,10 @@ vips_foreign_save_cgif_buffer_init( VipsForeignSaveCgifBuffer *buffer )
* * @dither: %gdouble, quantisation dithering level * * @dither: %gdouble, quantisation dithering level
* * @effort: %gint, quantisation CPU effort * * @effort: %gint, quantisation CPU effort
* * @bitdepth: %gint, number of bits per pixel * * @bitdepth: %gint, number of bits per pixel
* * @maxerror: %gdouble, maximum inter-frame error for transparency * * @interframe_maxerror: %gdouble, maximum inter-frame error for transparency
* * @reoptimise: %gboolean, reoptimise colour palettes * * @reoptimise: %gboolean, reoptimise colour palettes
* * @interpalette_maxerror: %gdouble, maximum inter-palette error for palette
* reusage
* *
* Write to a file in GIF format. * Write to a file in GIF format.
* *
@ -908,10 +1013,15 @@ vips_foreign_save_cgif_buffer_init( VipsForeignSaveCgifBuffer *buffer )
* always reserved for transparency. For example, a bitdepth of * always reserved for transparency. For example, a bitdepth of
* 4 will allow the output to contain up to 15 colours. * 4 will allow the output to contain up to 15 colours.
* *
* Use @maxerror to set the threshold below which pixels are considered equal. * Use @interframe_maxerror to set the threshold below which pixels are
* considered equal.
* Pixels which don't change from frame to frame can be made transparent, * Pixels which don't change from frame to frame can be made transparent,
* improving the compression rate. Default 0. * improving the compression rate. Default 0.
* *
* If @reoptimise is TRUE, new palettes will be generated. Use
* @interpalette_maxerror to set the threshold below which one of the previously
* generated palettes will be reused.
*
* See also: vips_image_new_from_file(). * See also: vips_image_new_from_file().
* *
* Returns: 0 on success, -1 on error. * Returns: 0 on success, -1 on error.
@ -941,8 +1051,10 @@ vips_gifsave( VipsImage *in, const char *filename, ... )
* * @dither: %gdouble, quantisation dithering level * * @dither: %gdouble, quantisation dithering level
* * @effort: %gint, quantisation CPU effort * * @effort: %gint, quantisation CPU effort
* * @bitdepth: %gint, number of bits per pixel * * @bitdepth: %gint, number of bits per pixel
* * @maxerror: %gdouble, maximum inter-frame error for transparency * * @interframe_maxerror: %gdouble, maximum inter-frame error for transparency
* * @reoptimise: %gboolean, reoptimise colour palettes * * @reoptimise: %gboolean, reoptimise colour palettes
* * @interpalette_maxerror: %gdouble, maximum inter-palette error for palette
* reusage
* *
* As vips_gifsave(), but save to a memory buffer. * As vips_gifsave(), but save to a memory buffer.
* *
@ -993,8 +1105,10 @@ vips_gifsave_buffer( VipsImage *in, void **buf, size_t *len, ... )
* * @dither: %gdouble, quantisation dithering level * * @dither: %gdouble, quantisation dithering level
* * @effort: %gint, quantisation CPU effort * * @effort: %gint, quantisation CPU effort
* * @bitdepth: %gint, number of bits per pixel * * @bitdepth: %gint, number of bits per pixel
* * @maxerror: %gdouble, maximum inter-frame error for transparency * * @interframe_maxerror: %gdouble, maximum inter-frame error for transparency
* * @reoptimise: %gboolean, reoptimise colour palettes * * @reoptimise: %gboolean, reoptimise colour palettes
* * @interpalette_maxerror: %gdouble, maximum inter-palette error for palette
* reusage
* *
* As vips_gifsave(), but save to a target. * As vips_gifsave(), but save to a target.
* *

View File

@ -87,6 +87,57 @@ vips__quantise_image_quantize( VipsQuantiseImage *const input_image,
return liq_image_quantize( input_image, options, result_output ); return liq_image_quantize( input_image, options, result_output );
} }
VipsQuantiseError
vips__quantise_image_quantize_fixed( VipsQuantiseImage *const input_image,
VipsQuantiseAttr *const options, VipsQuantiseResult **result_output )
{
int i;
liq_result *result;
const liq_palette *palette;
liq_error err;
liq_image *fake_image;
void *fake_image_pixels;
/* First, quantize the image and get its palette
*/
err = liq_image_quantize( input_image, options, &result );
if( err != LIQ_OK )
return err;
palette = liq_get_palette( result );
/* Now, we need a fake 1 pixel image that will be quantized on the
* next step. Its pixel color doesn't matter since we'll add all the
* colors frm the palette further.
*/
fake_image_pixels = malloc( 4 );
fake_image = liq_image_create_rgba( options, fake_image_pixels, 1, 1, 0 );
if( !fake_image ) {
liq_result_destroy( result );
free( fake_image_pixels );
return LIQ_OUT_OF_MEMORY;
}
/* Add all the colors from the palette as fixed colors to the fake
* image. Since the fixed colors number is the same as required colors
* number, no new colors will be added.
*/
for( i = 0; i < palette->count; i++ )
liq_image_add_fixed_color( fake_image, palette->entries[i] );
liq_result_destroy( result );
/* Finally, quantize the fake image with fixed colors to get the result
* which palette won't be changed during remapping
*/
err = liq_image_quantize( fake_image, options, result_output );
liq_image_destroy( fake_image );
free( fake_image_pixels );
return err;
}
VipsQuantiseError VipsQuantiseError
vips__quantise_set_dithering_level( VipsQuantiseResult *res, vips__quantise_set_dithering_level( VipsQuantiseResult *res,
float dither_level ) float dither_level )
@ -174,6 +225,17 @@ vips__quantise_image_quantize( VipsQuantiseImage *const input_image,
return 0; return 0;
} }
VipsQuantiseError
vips__quantise_image_quantize_fixed( VipsQuantiseImage *const input_image,
VipsQuantiseAttr *const options, VipsQuantiseResult **result_output )
{
/* Quantizr doesn't change the palette during remapping, so we don't
* need a special implementation for this
*/
return vips__quantise_image_quantize( input_image, options,
result_output );
}
VipsQuantiseError VipsQuantiseError
vips__quantise_set_dithering_level( VipsQuantiseResult *res, vips__quantise_set_dithering_level( VipsQuantiseResult *res,
float dither_level ) float dither_level )

View File

@ -69,6 +69,8 @@ VipsQuantiseImage *vips__quantise_image_create_rgba( const VipsQuantiseAttr *att
const void *bitmap, int width, int height, double gamma ); const void *bitmap, int width, int height, double gamma );
VipsQuantiseError vips__quantise_image_quantize( VipsQuantiseImage *input_image, VipsQuantiseError vips__quantise_image_quantize( VipsQuantiseImage *input_image,
VipsQuantiseAttr *options, VipsQuantiseResult **result_output ); VipsQuantiseAttr *options, VipsQuantiseResult **result_output );
VipsQuantiseError vips__quantise_image_quantize_fixed( VipsQuantiseImage *input_image,
VipsQuantiseAttr *options, VipsQuantiseResult **result_output );
VipsQuantiseError vips__quantise_set_dithering_level( VipsQuantiseResult *res, VipsQuantiseError vips__quantise_set_dithering_level( VipsQuantiseResult *res,
float dither_level ); float dither_level );
const VipsQuantisePalette *vips__quantise_get_palette( VipsQuantiseResult *result ); const VipsQuantisePalette *vips__quantise_get_palette( VipsQuantiseResult *result );