try to make cgifsave easier to read (#2853)

* try to make cgifsave easier to read

and fix a few memory errors

* move another loop out of write_frame

slightly smaller
This commit is contained in:
John Cupitt 2022-06-12 11:25:32 +01:00 committed by GitHub
parent b74504b985
commit 7fbdb01fb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 381 additions and 322 deletions

View File

@ -34,7 +34,6 @@
/* /*
#define DEBUG_VERBOSE #define DEBUG_VERBOSE
#define DEBUG_PERCENT
*/ */
#ifdef HAVE_CONFIG_H #ifdef HAVE_CONFIG_H
@ -55,6 +54,27 @@
#include <cgif.h> #include <cgif.h>
/* The modes we work in.
*
* VIPS_FOREIGN_SAVE_CGIF_MODE_GLOBAL:
*
* Each frame is dithered to single global colour table taken from the
* input image "gif-palette" metadata item.
*
* VIPS_FOREIGN_SAVE_CGIF_MODE_LOCAL:
*
* We find a global palette from the first frame, then write subsequent
* frames with a local palette if they start to drift too far from the
* first frame.
*
* We pick GLOBAL if "gif-palette" is set. We pick LOCAL if there is
* no "gif-palette", or if @reoptimise is set.
*/
typedef enum _VipsForeignSaveCgifMode {
VIPS_FOREIGN_SAVE_CGIF_MODE_GLOBAL,
VIPS_FOREIGN_SAVE_CGIF_MODE_LOCAL
} VipsForeignSaveCgifMode;
typedef struct _VipsForeignSaveCgif { typedef struct _VipsForeignSaveCgif {
VipsForeignSave parent_object; VipsForeignSave parent_object;
@ -68,48 +88,56 @@ typedef struct _VipsForeignSaveCgif {
/* Derived write params. /* Derived write params.
*/ */
VipsForeignSaveCgifMode mode;
VipsImage *in; /* Not a reference */ VipsImage *in; /* Not a reference */
gboolean has_transparency;
int *delay; int *delay;
int delay_length; int delay_length;
int loop; int loop;
int *global_colour_table;
int global_colour_table_length;
/* The current frame coming from libvips, the y position we write to, /* The RGBA palette attached to the input image (if any).
* and some spare pixels we copy down when we move to the next frame. */
int *palette;
int n_colours;
/* The global palette as RGB (not RGBA).
*/
VipsPel palette_rgb[256 * 3];
/* The current frame coming from libvips, and the y position
* in the input image.
*/ */
VipsRegion *frame; VipsRegion *frame;
int write_y; int write_y;
/* The frame as seen by libimagequant. /* The current frame as seen by libimagequant.
*/ */
VipsQuantiseAttr *attr; VipsQuantiseAttr *attr;
VipsQuantiseImage *input_image; VipsQuantiseResult *quantisation_result;
VipsQuantiseResult *quantisation_result, *local_quantisation_result;
/* The current colourmap, updated on a significant frame change. /* The palette we used for the previous frame. This can be equal to
* quantisation_result if we used the global palette for the previous
* frame, so don't free this.
*/ */
VipsPel *palette_rgb; VipsQuantiseResult *previous_quantisation_result;
gint64 frame_checksum;
/* ... and a palette we will need to free.
*/
VipsQuantiseResult *free_quantisation_result;
/* The index frame we get libimagequant to generate. /* The index frame we get libimagequant to generate.
*/ */
VipsPel *index; VipsPel *index;
/* frame_bytes head (needed for transparency trick). /* The previous RGBA frame (needed for transparency trick).
*/ */
VipsPel *frame_bytes_head; VipsPel *previous_frame;
/* The frame as written by libcgif. /* The frame as written by libcgif.
*/ */
CGIF *cgif_context; CGIF *cgif_context;
CGIF_Config cgif_config; CGIF_Config cgif_config;
#ifdef DEBUG_PERCENT int n_palettes_generated;
int n_cmaps_generated;
#endif/*DEBUG_PERCENT*/
} VipsForeignSaveCgif; } VipsForeignSaveCgif;
typedef VipsForeignSaveClass VipsForeignSaveCgifClass; typedef VipsForeignSaveClass VipsForeignSaveCgifClass;
@ -122,90 +150,101 @@ vips_foreign_save_cgif_dispose( GObject *gobject )
{ {
VipsForeignSaveCgif *cgif = (VipsForeignSaveCgif *) gobject; VipsForeignSaveCgif *cgif = (VipsForeignSaveCgif *) gobject;
#ifdef DEBUG_PERCENT
if( cgif->frame ) { if( cgif->frame ) {
printf( "%d frames\n", g_info( "cgifsave: %d frames",
cgif->frame->im->Ysize / cgif->frame->valid.height ); cgif->frame->im->Ysize / cgif->frame->valid.height );
printf( "%d cmaps\n", cgif->n_cmaps_generated ); g_info( "cgifsave: %d unique palettes",
cgif->n_palettes_generated );
} }
#endif/*DEBUG_PERCENT*/
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, VIPS_FREEF( vips__quantise_result_destroy, cgif->
cgif->local_quantisation_result ); free_quantisation_result );
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 );
VIPS_UNREF( cgif->frame ); VIPS_UNREF( cgif->frame );
VIPS_UNREF( cgif->target ); VIPS_UNREF( cgif->target );
VIPS_FREE( cgif->palette_rgb );
VIPS_FREE( cgif->index ); VIPS_FREE( cgif->index );
VIPS_FREE( cgif->frame_bytes_head ); VIPS_FREE( cgif->previous_frame );
G_OBJECT_CLASS( vips_foreign_save_cgif_parent_class )-> G_OBJECT_CLASS( vips_foreign_save_cgif_parent_class )->
dispose( gobject ); dispose( gobject );
} }
/* Minimal callback wrapper around vips_target_write static int
*/ vips__cgif_write( void *client, const uint8_t *buffer, const size_t length )
static int vips__cgif_write( void *target, const uint8_t *buffer, {
const size_t length ) { VipsTarget *target = VIPS_TARGET( client );
return vips_target_write( (VipsTarget *) target,
return vips_target_write( target,
(const void *) buffer, (size_t) length ); (const void *) buffer, (size_t) length );
} }
/* Compare pixels in a lossy way (allow a slight colour difference). /* Set pixels in index transparent if they are equal RGB to the previous
* frame.
*
* In combination with the GIF transparency optimization this leads to * In combination with the GIF transparency optimization this leads to
* less difference between frames and therefore improves the compression ratio. * less difference between frames and therefore improves the compression ratio.
*/ */
static gboolean static void
vips_foreign_save_cgif_pixels_are_equal( const VipsPel *cur, const VipsPel *bef, vips_foreign_save_cgif_set_transparent( VipsForeignSaveCgif *cgif,
double sq_maxerror ) VipsPel *old, VipsPel *new, VipsPel *index, int n_pels, int trans )
{ {
if( bef[3] != cur[3] ) int sq_maxerror = cgif->interframe_maxerror * cgif->interframe_maxerror;
/* Alpha channel must be identical.
*/
return FALSE;
if( bef[3] == 0 && cur[3] == 0)
/* RGB component can be ignored.
*/
return TRUE;
/* Test Euclidean distance between the two points. int i;
*/
const int dR = cur[0] - bef[0];
const int dG = cur[1] - bef[1];
const int dB = cur[2] - bef[2];
return( dR * dR + dG * dG + dB * dB <= sq_maxerror ); for( i = 0; i < n_pels; i++ ) {
/* Alpha must match
*/
if( old[3] == new[3] ) {
/* Both transparent ... no need to check RGB.
*/
if( !old[3] && !new[3] )
index[i] = trans;
else {
/* Compare RGB.
*/
const int dR = old[0] - new[0];
const int dG = old[1] - new[1];
const int dB = old[2] - new[2];
if( dR * dR + dG * dG + dB * dB <= sq_maxerror )
index[i] = trans;
}
}
old += 4;
new += 4;
}
} }
static double static int
vips__cgif_compare_palettes( const VipsQuantisePalette *new, vips__cgif_compare_palettes( const VipsQuantisePalette *new,
const VipsQuantisePalette *old ) const VipsQuantisePalette *old )
{ {
int i, j; int i, j;
double best_dist, dist, rd, gd, bd; int best_dist, dist, rd, gd, bd;
double total_dist; int total_dist;
g_assert( new->count <= 256 ); g_assert( new->count <= 256 );
g_assert( old->count <= 256 ); g_assert( old->count <= 256 );
total_dist = 0.0; total_dist = 0;
for( i = 0; i < new->count; i++ ) { for( i = 0; i < new->count; i++ ) {
best_dist = 255.0 * 255.0 * 3; best_dist = 255 * 255 * 3;
for( j = 0; j < old->count; j++ ) { for( j = 0; j < old->count; j++ ) {
if( new->entries[i].a >= 128 ) { if( new->entries[i].a ) {
/* The new entry is solid. /* The new entry is solid.
* If the old entry is transparent, ignore it. * If the old entry is transparent, ignore it.
* Otherwise, compare RGB. * Otherwise, compare RGB.
*/ */
if( old->entries[j].a < 128 ) if( !old->entries[j].a )
continue; continue;
rd = new->entries[i].r - old->entries[j].r; rd = new->entries[i].r - old->entries[j].r;
@ -225,7 +264,7 @@ vips__cgif_compare_palettes( const VipsQuantisePalette *new,
* If the old entry is transparent too, it's * If the old entry is transparent too, it's
* the closest color. Otherwise, ignore it. * the closest color. Otherwise, ignore it.
*/ */
if( old->entries[j].a < 128 ) { if( !old->entries[j].a ) {
best_dist = 0; best_dist = 0;
break; break;
} }
@ -235,7 +274,140 @@ vips__cgif_compare_palettes( const VipsQuantisePalette *new,
total_dist += best_dist; total_dist += best_dist;
} }
return( total_dist / new->count ); return( sqrt( total_dist / (3 * new->count) ) );
}
/* Extract the generated palette as RGB.
*/
static void
vips_foreign_save_cgif_get_rgb_palette( VipsForeignSaveCgif *cgif,
VipsQuantiseResult *quantisation_result, VipsPel *rgb )
{
const VipsQuantisePalette *lp =
vips__quantise_get_palette( quantisation_result );
int i;
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;
}
}
/* Pick a quantiser for LOCAL mode.
*/
int
vips_foreign_save_cgif_pick_quantiser( VipsForeignSaveCgif *cgif,
VipsQuantiseImage *image,
VipsQuantiseResult **result, gboolean *use_local )
{
VipsObjectClass *class = VIPS_OBJECT_GET_CLASS( cgif );
VipsQuantiseResult *this_result;
if( vips__quantise_image_quantize_fixed( image, cgif->attr,
&this_result ) ) {
vips_error( class->nickname, "%s", _( "quantisation failed" ) );
return( -1 );
}
/* No global quantiser set up yet? Use this.
*/
if( !cgif->quantisation_result ) {
#ifdef DEBUG_VERBOSE
printf( "vips_foreign_save_cgif_pick_quantiser: "
"global palette from first frame\n" );
#endif/*DEBUG_VERBOSE*/
cgif->quantisation_result = this_result;
vips_foreign_save_cgif_get_rgb_palette( cgif,
this_result, cgif->palette_rgb );
cgif->n_palettes_generated += 1;
*result = this_result;
*use_local = FALSE;
}
else {
/* Compare the palette we just made to the palette
* for the previous frame, and to the global palette.
*/
const VipsQuantisePalette *global = vips__quantise_get_palette(
cgif->quantisation_result );
const VipsQuantisePalette *this = vips__quantise_get_palette(
this_result );
const VipsQuantisePalette *prev = vips__quantise_get_palette(
cgif->previous_quantisation_result );
#ifdef DEBUG_VERBOSE
printf( "vips_foreign_save_cgif_write_frame: "
"this -> global distance = %d\n",
vips__cgif_compare_palettes( this, global ) );
printf( "vips_foreign_save_cgif_write_frame: "
"this -> prev distance = %d\n",
vips__cgif_compare_palettes( this, prev ) );
printf( "vips_foreign_save_cgif_write_frame: "
"threshold = %g\n", cgif->interpalette_maxerror );
#endif/*DEBUG_VERBOSE*/
if( vips__cgif_compare_palettes( this, global ) <
cgif->interpalette_maxerror ) {
/* Global is good enough, use that.
*/
#ifdef DEBUG_VERBOSE
printf( "vips_foreign_save_cgif_write_frame: "
"using global palette\n" );
#endif/*DEBUG_VERBOSE*/
VIPS_FREEF( vips__quantise_result_destroy,
this_result );
VIPS_FREEF( vips__quantise_result_destroy,
cgif->free_quantisation_result );
*result = cgif->quantisation_result;
*use_local = FALSE;
}
else if( vips__cgif_compare_palettes( this, prev ) <
cgif->interpalette_maxerror ) {
/* Previous is good enough, use that again.
*/
#ifdef DEBUG_VERBOSE
printf( "vips_foreign_save_cgif_write_frame: "
"using previous palette\n" );
#endif/*DEBUG_VERBOSE*/
VIPS_FREEF( vips__quantise_result_destroy,
this_result );
*result = cgif->previous_quantisation_result;
*use_local = TRUE;
}
else {
/* Nothing else works, we need a new local
* palette.
*/
#ifdef DEBUG_VERBOSE
printf( "vips_foreign_save_cgif_write_frame: "
"using new local palette\n" );
#endif/*DEBUG_VERBOSE*/
VIPS_FREEF( vips__quantise_result_destroy,
cgif->free_quantisation_result );
cgif->free_quantisation_result = this_result;
cgif->n_palettes_generated += 1;
*result = this_result;
*use_local = TRUE;
}
}
cgif->previous_quantisation_result = *result;
return( 0 );
} }
/* We have a complete frame --- write! /* We have a complete frame --- write!
@ -246,33 +418,28 @@ vips_foreign_save_cgif_write_frame( VipsForeignSaveCgif *cgif )
VipsObjectClass *class = VIPS_OBJECT_GET_CLASS( cgif ); VipsObjectClass *class = VIPS_OBJECT_GET_CLASS( cgif );
VipsRect *frame_rect = &cgif->frame->valid; VipsRect *frame_rect = &cgif->frame->valid;
int page_index = frame_rect->top / frame_rect->height; int page_index = frame_rect->top / frame_rect->height;
/* We know this fits in an int since we limit frame size. /* We know this fits in an int since we limit frame size.
*/ */
int n_pels = frame_rect->height * frame_rect->width;
VipsPel *frame_bytes = VipsPel *frame_bytes =
VIPS_REGION_ADDR( cgif->frame, 0, frame_rect->top ); VIPS_REGION_ADDR( cgif->frame, 0, frame_rect->top );
int n_pels = frame_rect->height * frame_rect->width;
gboolean has_transparency;
gboolean has_alpha_constraint;
VipsPel * restrict p;
int i; int i;
VipsPel * restrict cur; VipsQuantiseImage *image;
VipsPel * restrict bef; gboolean use_local;
gboolean has_alpha_constraint = FALSE;
VipsPel *rgb;
VipsQuantiseResult *quantisation_result; VipsQuantiseResult *quantisation_result;
const VipsQuantisePalette *lp, *pal_global, *pal_local; const VipsQuantisePalette *lp;
double pal_change_global, pal_change_local; CGIF_FrameConfig frame_config = { 0 };
gboolean use_local_palette = FALSE; int n_colours;
CGIF_FrameConfig frame_config;
#ifdef DEBUG_VERBOSE #ifdef DEBUG_VERBOSE
printf( "vips_foreign_save_cgif_write_frame: %d\n", page_index ); printf( "vips_foreign_save_cgif_write_frame: %d\n", page_index );
#endif/*DEBUG_VERBOSE*/ #endif/*DEBUG_VERBOSE*/
/* Set up new frame for libimagequant.
*/
VIPS_FREEF( vips__quantise_image_destroy, cgif->input_image );
cgif->input_image = vips__quantise_image_create_rgba( cgif->attr,
frame_bytes, frame_rect->width, frame_rect->height, 0 );
/* Threshold the alpha channel. It's safe to modify the region since /* Threshold the alpha channel. It's safe to modify the region since
* it's a buffer we made. * it's a buffer we made.
* *
@ -280,180 +447,94 @@ vips_foreign_save_cgif_write_frame( VipsForeignSaveCgif *cgif )
* frame before. * frame before.
* *
* If the current frame has an alpha component which is not identical * If the current frame has an alpha component which is not identical
* to the frame from before we are forced to use the transparency index * to the previous frame we are forced to use the transparency index
* for the alpha channel instead of for the transparency size * for the alpha channel instead of for the transparency size
* optimization (maxerror). * optimization (maxerror).
*/ */
cur = frame_bytes; p = frame_bytes;
bef = cgif->frame_bytes_head; has_alpha_constraint = FALSE;
for( i = 0; i < n_pels; i++ ) { for( i = 0; i < n_pels; i++ ) {
if( cur[3] >= 128 ) if( p[3] >= 128 )
cur[3] = 255; p[3] = 255;
else { else {
/* Helps the quanizer generate a better palette. /* Helps the quantiser generate a better palette.
*/ */
cur[0] = 0; p[0] = 0;
cur[1] = 0; p[1] = 0;
cur[2] = 0; p[2] = 0;
cur[3] = 0; p[3] = 0;
if( bef && bef[i * 4 + 3] != 0 ) if( page_index > 0 &&
cgif->previous_frame[i * 4 + 3] )
has_alpha_constraint = TRUE; has_alpha_constraint = TRUE;
} }
cur += 4; p += 4;
} }
/* Do we need to compute a new palette? Do it if the palette changes. /* Set up new frame for libimagequant.
*/ */
if( cgif->global_colour_table ) { image = vips__quantise_image_create_rgba( cgif->attr,
frame_bytes, frame_rect->width, frame_rect->height, 0 );
/* Quantise.
*/
if( cgif->mode == VIPS_FOREIGN_SAVE_CGIF_MODE_GLOBAL ) {
/* Global mode: use the global palette.
*/
quantisation_result = cgif->quantisation_result; quantisation_result = cgif->quantisation_result;
lp = vips__quantise_get_palette( quantisation_result ); use_local = FALSE;
} }
else { else {
if( vips__quantise_image_quantize_fixed( cgif->input_image, /* Local mode. Pick the global, this or previous palette.
cgif->attr, &quantisation_result ) ) { */
vips_error( class->nickname, if( vips_foreign_save_cgif_pick_quantiser( cgif,
"%s", _( "quantisation failed" ) ); image, &quantisation_result, &use_local ) )
return( -1 ); return( -1 );
}
lp = vips__quantise_get_palette( quantisation_result );
if( !cgif->quantisation_result )
/* This is the first frame, save global quantization
* result and palette
*/
cgif->quantisation_result = quantisation_result;
else {
pal_global = vips__quantise_get_palette(
cgif->quantisation_result );
pal_change_global = vips__cgif_compare_palettes(
lp, pal_global );
if( !cgif->local_quantisation_result )
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
cgif->n_cmaps_generated += 1;
printf( "frame %d, new %d item colourmap\n",
page_index, lp->count );
#endif/*DEBUG_PERCENT*/
}
}
}
/* Extract palette.
*/
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;
}
}
/* Dither frame.
*/
vips__quantise_set_dithering_level( quantisation_result, cgif->dither );
if( vips__quantise_write_remapped_image( quantisation_result,
cgif->input_image, cgif->index, n_pels ) ) {
vips_error( class->nickname, "%s", _( "dither failed" ) );
return( -1 );
} }
/* If there's a transparent pixel, it's always first. /* If there's a transparent pixel, it's always first.
*/ */
cgif->has_transparency = lp->entries[0].a == 0; lp = vips__quantise_get_palette( quantisation_result );
has_transparency = lp->entries[0].a == 0;
n_colours = lp->count;
/* Set up cgif on first use, so we can set the first cmap as the global /* Dither frame into @index.
* one. */
* vips__quantise_set_dithering_level( quantisation_result, cgif->dither );
* We switch to local tables if we find we need a new palette. if( vips__quantise_write_remapped_image( quantisation_result,
image, cgif->index, n_pels ) ) {
vips_error( class->nickname, "%s", _( "dither failed" ) );
VIPS_FREEF( vips__quantise_image_destroy, image );
return( -1 );
}
VIPS_FREEF( vips__quantise_image_destroy, image );
/* Set up cgif on first use.
*/ */
if( !cgif->cgif_context ) { if( !cgif->cgif_context ) {
cgif->cgif_config.pGlobalPalette = cgif->palette_rgb;
#ifdef HAVE_CGIF_ATTR_NO_LOOP #ifdef HAVE_CGIF_ATTR_NO_LOOP
cgif->cgif_config.attrFlags = cgif->cgif_config.attrFlags =
CGIF_ATTR_IS_ANIMATED | CGIF_ATTR_IS_ANIMATED |
(cgif->loop == 1 ? CGIF_ATTR_NO_LOOP : 0); (cgif->loop == 1 ? CGIF_ATTR_NO_LOOP : 0);
#else
cgif->cgif_config.attrFlags = CGIF_ATTR_IS_ANIMATED;
#endif/*HAVE_CGIF_ATTR_NO_LOOP*/
cgif->cgif_config.width = frame_rect->width;
cgif->cgif_config.height = frame_rect->height;
cgif->cgif_config.numGlobalPaletteEntries = lp->count;
#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;
#else #else /*!HAVE_CGIF_ATTR_NO_LOOP*/
cgif->cgif_config.attrFlags = CGIF_ATTR_IS_ANIMATED;
cgif->cgif_config.numLoops = cgif->loop; cgif->cgif_config.numLoops = cgif->loop;
#endif/*HAVE_CGIF_ATTR_NO_LOOP*/ #endif/*HAVE_CGIF_ATTR_NO_LOOP*/
cgif->cgif_config.width = frame_rect->width;
cgif->cgif_config.height = frame_rect->height;
cgif->cgif_config.pGlobalPalette = cgif->palette_rgb;
cgif->cgif_config.numGlobalPaletteEntries = n_colours;
cgif->cgif_config.pWriteFn = vips__cgif_write; cgif->cgif_config.pWriteFn = vips__cgif_write;
cgif->cgif_config.pContext = (void *) cgif->target; cgif->cgif_config.pContext = (void *) cgif->target;
cgif->cgif_context = cgif_newgif( &cgif->cgif_config ); cgif->cgif_context = cgif_newgif( &cgif->cgif_config );
} }
/* Write frame to cgif.
*/
memset( &frame_config, 0, sizeof( CGIF_FrameConfig ) );
frame_config.pImageData = cgif->index;
/* Allow cgif to optimise by adding transparency. These optimisations /* Allow cgif to optimise by adding transparency. These optimisations
* will be automatically disabled if they are not possible. * will be automatically disabled if they are not possible.
*/ */
@ -465,60 +546,26 @@ vips_foreign_save_cgif_write_frame( VipsForeignSaveCgif *cgif )
/* Switch per-frame alpha channel on. Index 0 is used for pixels /* Switch per-frame alpha channel on. Index 0 is used for pixels
* with alpha channel. * with alpha channel.
*/ */
if( cgif->has_transparency ) { if( has_transparency ) {
frame_config.attrFlags |= CGIF_FRAME_ATTR_HAS_ALPHA; frame_config.attrFlags |= CGIF_FRAME_ATTR_HAS_ALPHA;
frame_config.transIndex = 0; frame_config.transIndex = 0;
} }
/* Pixels which are equal to pixels in the previous frame can be made /* Pixels which are equal to pixels in the previous frame can be made
* transparent. * transparent, provided no alpha channel constraint is present.
*/ */
if( cgif->frame_bytes_head ) { if( page_index > 0 &&
cur = frame_bytes; !has_alpha_constraint ) {
bef = cgif->frame_bytes_head; int trans = has_transparency ? 0 : n_colours;
/* Transparency trick is only possible when no alpha channel vips_foreign_save_cgif_set_transparent( cgif,
* constraint is present. cgif->previous_frame, frame_bytes, cgif->index,
*/ n_pels, trans );
if( !has_alpha_constraint ) {
uint8_t trans_index;
double sq_maxerror;
trans_index = lp->count; if( has_transparency )
if( cgif->has_transparency ) { frame_config.attrFlags &= ~CGIF_FRAME_ATTR_HAS_ALPHA;
trans_index = 0; frame_config.attrFlags |= CGIF_FRAME_ATTR_HAS_SET_TRANS;
frame_config.attrFlags &= frame_config.transIndex = trans;
~CGIF_FRAME_ATTR_HAS_ALPHA;
}
sq_maxerror = cgif->interframe_maxerror *
cgif->interframe_maxerror;
for( i = 0; i < n_pels; i++ ) {
if( vips_foreign_save_cgif_pixels_are_equal(
cur, bef, sq_maxerror ) )
cgif->index[i] = trans_index;
else {
bef[0] = cur[0];
bef[1] = cur[1];
bef[2] = cur[2];
bef[3] = cur[3];
}
cur += 4;
bef += 4;
}
frame_config.attrFlags |=
CGIF_FRAME_ATTR_HAS_SET_TRANS;
frame_config.transIndex = trans_index;
}
else {
/* Transparency trick not possible (constraining alpha
* channel present). Update head.
*/
memcpy( bef, cur, 4 * n_pels );
}
} }
if( cgif->delay && if( cgif->delay &&
@ -528,21 +575,24 @@ 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( use_local_palette ) { if( use_local ) {
VipsPel rgb[256 * 3];
vips_foreign_save_cgif_get_rgb_palette( cgif,
quantisation_result, rgb );
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 = rgb;
frame_config.numLocalPaletteEntries = lp->count; frame_config.numLocalPaletteEntries = n_colours;
} }
/* Write frame to cgif.
*/
frame_config.pImageData = cgif->index;
cgif_addframe( cgif->cgif_context, &frame_config ); cgif_addframe( cgif->cgif_context, &frame_config );
if( !cgif->frame_bytes_head ) { /* Take a copy of the RGBA frame.
/* Keep head frame_bytes in memory for transparency trick */
* which avoids the size explosion (#2576). memcpy( cgif->previous_frame, frame_bytes, 4 * n_pels );
*/
cgif->frame_bytes_head = g_malloc( 4 * n_pels );
memcpy( cgif->frame_bytes_head, frame_bytes, 4 * n_pels );
}
return( 0 ); return( 0 );
} }
@ -597,8 +647,13 @@ vips_foreign_save_cgif_sink_disc( VipsRegion *region, VipsRect *area, void *a )
image.height = cgif->in->Ysize; image.height = cgif->in->Ysize;
vips_rect_intersectrect( &new_frame, &image, vips_rect_intersectrect( &new_frame, &image,
&new_frame ); &new_frame );
if( !vips_rect_isempty( &new_frame ) &&
vips_region_buffer( cgif->frame, &new_frame ) ) /* End of image?
*/
if( vips_rect_isempty( &new_frame ) )
break;
if( vips_region_buffer( cgif->frame, &new_frame ) )
return( -1 ); return( -1 );
} }
} while( VIPS_RECT_BOTTOM( area ) > cgif->write_y ); } while( VIPS_RECT_BOTTOM( area ) > cgif->write_y );
@ -626,15 +681,6 @@ vips_foreign_save_cgif_build( VipsObject *object )
/* libimagequant only works with RGBA images. /* libimagequant only works with RGBA images.
*/ */
if( cgif->in->Type != VIPS_INTERPRETATION_sRGB ) {
if( vips_colourspace( cgif->in, &t[0],
VIPS_INTERPRETATION_sRGB, NULL ) )
return( -1 );
cgif->in = t[0];
}
/* Add alpha channel if missing.
*/
if( !vips_image_hasalpha( cgif->in ) ) { if( !vips_image_hasalpha( cgif->in ) ) {
if( vips_addalpha( cgif->in, &t[1], NULL ) ) if( vips_addalpha( cgif->in, &t[1], NULL ) )
return( -1 ); return( -1 );
@ -673,60 +719,75 @@ vips_foreign_save_cgif_build( VipsObject *object )
*/ */
vips__region_no_ownership( cgif->frame ); vips__region_no_ownership( cgif->frame );
/* The RGB cmap we write with, sometimes updated on frame write, and /* The previous RGBA frame (for spotting pixels which haven't changed).
* the frame index buffer. */
cgif->previous_frame =
g_malloc0( 4 * frame_rect.width * frame_rect.height );
/* The frame index buffer.
*/ */
cgif->palette_rgb = g_malloc0( 256 * 3 );
cgif->index = g_malloc0( frame_rect.width * frame_rect.height ); cgif->index = g_malloc0( frame_rect.width * frame_rect.height );
/* Set up libimagequant. /* Set up libimagequant.
*/ */
cgif->attr = vips__quantise_attr_create(); cgif->attr = vips__quantise_attr_create();
vips__quantise_set_max_colors( cgif->attr, (1 << cgif->bitdepth) - 1 ); vips__quantise_set_max_colors( cgif->attr, 1 << cgif->bitdepth );
vips__quantise_set_quality( cgif->attr, 0, 100 ); vips__quantise_set_quality( cgif->attr, 0, 100 );
vips__quantise_set_speed( cgif->attr, 11 - cgif->effort ); vips__quantise_set_speed( cgif->attr, 11 - cgif->effort );
/* Initialise quantization result using global palette. /* Read the palette on the input, if any.
*/ */
if( !cgif->reoptimise && if( vips_image_get_typeof( cgif->in, "gif-palette" ) ) {
vips_image_get_typeof( cgif->in, "gif-palette" ) ) { if( vips_image_get_array_int( cgif->in, "gif-palette",
VipsQuantiseImage *tmp_image; &cgif->palette, &cgif->n_colours ) )
int *tmp_gct; return( -1 );
int *gct;
int gct_length;
vips_image_get_array_int( cgif->in, "gif-palette", if( cgif->n_colours > 256 ) {
&cgif->global_colour_table,
&cgif->global_colour_table_length);
gct = cgif->global_colour_table;
gct_length = cgif->global_colour_table_length;
/* Attach fake alpha channel.
* That's necessary, because we do not know whether there is an
* alpha channel before processing the animation.
*/
tmp_gct = g_malloc((gct_length + 1) * sizeof(int));
memcpy(tmp_gct, gct, gct_length * sizeof(int));
tmp_gct[gct_length] = 0;
tmp_image = vips__quantise_image_create_rgba( cgif->attr,
tmp_gct, gct_length + 1, 1, 0 );
if( vips__quantise_image_quantize_fixed( tmp_image,
cgif->attr, &cgif->quantisation_result ) ) {
vips_error( class->nickname, vips_error( class->nickname,
"%s", _( "quantisation failed" ) ); "%s", _( "gif-palette too large" ) );
return( -1 ); return( -1 );
} }
VIPS_FREE( tmp_gct );
VIPS_FREEF( vips__quantise_image_destroy, tmp_image );
} }
/* LOCAL mode if there's no input palette, or reoptimise is set.
/* Set up cgif on first use.
*/ */
if( cgif->reoptimise ||
!cgif->palette )
cgif->mode = VIPS_FOREIGN_SAVE_CGIF_MODE_LOCAL;
/* Loop down the image, computing it in chunks. /* Set up GLOBAL mode. Init the quantisation_result we will
* use to dither frames with a fixed palette taken from the input
* image.
*/ */
if( cgif->mode == VIPS_FOREIGN_SAVE_CGIF_MODE_GLOBAL ) {
/* Make a fake image from the input palette, and quantise that.
* Add a zero pixel (transparent) in case the input image has
* transparency.
*
* We know palette fits in 256 entries.
*/
guint32 fake_image[257];
VipsQuantiseImage *image;
memcpy( fake_image, cgif->palette,
cgif->n_colours * sizeof( int ) );
fake_image[cgif->n_colours] = 0;
image = vips__quantise_image_create_rgba( cgif->attr,
fake_image, cgif->n_colours + 1, 1, 0 );
if( vips__quantise_image_quantize_fixed( image,
cgif->attr, &cgif->quantisation_result ) ) {
vips_error( class->nickname,
"%s", _( "quantisation failed" ) );
return( -1 );
}
VIPS_FREEF( vips__quantise_image_destroy, image );
vips_foreign_save_cgif_get_rgb_palette( cgif,
cgif->quantisation_result, cgif->palette_rgb );
}
if( vips_sink_disc( cgif->in, if( vips_sink_disc( cgif->in,
vips_foreign_save_cgif_sink_disc, cgif ) ) vips_foreign_save_cgif_sink_disc, cgif ) )
return( -1 ); return( -1 );
@ -807,7 +868,7 @@ vips_foreign_save_cgif_class_init( VipsForeignSaveCgifClass *class )
_( "Maximum inter-palette error for palette reusage" ), _( "Maximum inter-palette error for palette reusage" ),
VIPS_ARGUMENT_OPTIONAL_INPUT, VIPS_ARGUMENT_OPTIONAL_INPUT,
G_STRUCT_OFFSET( VipsForeignSaveCgif, interpalette_maxerror ), G_STRUCT_OFFSET( VipsForeignSaveCgif, interpalette_maxerror ),
0, 256, 64.0 ); 0, 256, 40.0 );
} }
static void static void
@ -818,7 +879,8 @@ vips_foreign_save_cgif_init( VipsForeignSaveCgif *gif )
gif->bitdepth = 8; gif->bitdepth = 8;
gif->interframe_maxerror = 0.0; gif->interframe_maxerror = 0.0;
gif->reoptimise = FALSE; gif->reoptimise = FALSE;
gif->interpalette_maxerror = 64.0; gif->interpalette_maxerror = 40.0;
gif->mode = VIPS_FOREIGN_SAVE_CGIF_MODE_GLOBAL;
} }
typedef struct _VipsForeignSaveCgifTarget { typedef struct _VipsForeignSaveCgifTarget {

View File

@ -99,7 +99,7 @@ vips__quantise_image_quantize_fixed( VipsQuantiseImage *const input_image,
const liq_palette *palette; const liq_palette *palette;
liq_error err; liq_error err;
liq_image *fake_image; liq_image *fake_image;
void *fake_image_pixels; char fake_image_pixels[4] = { 0 };
/* First, quantize the image and get its palette. /* First, quantize the image and get its palette.
*/ */
@ -113,12 +113,10 @@ vips__quantise_image_quantize_fixed( VipsQuantiseImage *const input_image,
* next step. Its pixel color doesn't matter since we'll add all the * next step. Its pixel color doesn't matter since we'll add all the
* colors from the palette further. * colors from the palette further.
*/ */
fake_image_pixels = malloc( 4 );
fake_image = fake_image =
liq_image_create_rgba( options, fake_image_pixels, 1, 1, 0 ); liq_image_create_rgba( options, fake_image_pixels, 1, 1, 0 );
if( !fake_image ) { if( !fake_image ) {
liq_result_destroy( result ); liq_result_destroy( result );
free( fake_image_pixels );
return LIQ_OUT_OF_MEMORY; return LIQ_OUT_OF_MEMORY;
} }
@ -131,13 +129,12 @@ vips__quantise_image_quantize_fixed( VipsQuantiseImage *const input_image,
liq_result_destroy( result ); liq_result_destroy( result );
/* Finally, quantize the fake image with fixed colors to get the result /* Finally, quantize the fake image with fixed colors to make a
* palette which won't be changed during remapping * VipsQuantiseResult with a fixed palette.
*/ */
err = liq_image_quantize( fake_image, options, result_output ); err = liq_image_quantize( fake_image, options, result_output );
liq_image_destroy( fake_image ); liq_image_destroy( fake_image );
free( fake_image_pixels );
return err; return err;
} }