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_PERCENT
*/
#ifdef HAVE_CONFIG_H
@ -55,6 +54,27 @@
#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 {
VipsForeignSave parent_object;
@ -68,48 +88,56 @@ typedef struct _VipsForeignSaveCgif {
/* Derived write params.
*/
VipsForeignSaveCgifMode mode;
VipsImage *in; /* Not a reference */
gboolean has_transparency;
int *delay;
int delay_length;
int loop;
int *global_colour_table;
int global_colour_table_length;
/* The current frame coming from libvips, the y position we write to,
* and some spare pixels we copy down when we move to the next frame.
/* The RGBA palette attached to the input image (if any).
*/
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;
int write_y;
/* The frame as seen by libimagequant.
/* The current frame as seen by libimagequant.
*/
VipsQuantiseAttr *attr;
VipsQuantiseImage *input_image;
VipsQuantiseResult *quantisation_result, *local_quantisation_result;
VipsQuantiseResult *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;
gint64 frame_checksum;
VipsQuantiseResult *previous_quantisation_result;
/* ... and a palette we will need to free.
*/
VipsQuantiseResult *free_quantisation_result;
/* The index frame we get libimagequant to generate.
*/
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.
*/
CGIF *cgif_context;
CGIF_Config cgif_config;
#ifdef DEBUG_PERCENT
int n_cmaps_generated;
#endif/*DEBUG_PERCENT*/
int n_palettes_generated;
} VipsForeignSaveCgif;
typedef VipsForeignSaveClass VipsForeignSaveCgifClass;
@ -122,90 +150,101 @@ vips_foreign_save_cgif_dispose( GObject *gobject )
{
VipsForeignSaveCgif *cgif = (VipsForeignSaveCgif *) gobject;
#ifdef DEBUG_PERCENT
if( cgif->frame ) {
printf( "%d frames\n",
g_info( "cgifsave: %d frames",
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( 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_result_destroy, cgif->
free_quantisation_result );
VIPS_FREEF( vips__quantise_attr_destroy, cgif->attr );
VIPS_UNREF( cgif->frame );
VIPS_UNREF( cgif->target );
VIPS_FREE( cgif->palette_rgb );
VIPS_FREE( cgif->index );
VIPS_FREE( cgif->frame_bytes_head );
VIPS_FREE( cgif->previous_frame );
G_OBJECT_CLASS( vips_foreign_save_cgif_parent_class )->
dispose( gobject );
}
/* Minimal callback wrapper around vips_target_write
*/
static int vips__cgif_write( void *target, const uint8_t *buffer,
const size_t length ) {
return vips_target_write( (VipsTarget *) target,
static int
vips__cgif_write( void *client, const uint8_t *buffer, const size_t length )
{
VipsTarget *target = VIPS_TARGET( client );
return vips_target_write( target,
(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
* less difference between frames and therefore improves the compression ratio.
*/
static gboolean
vips_foreign_save_cgif_pixels_are_equal( const VipsPel *cur, const VipsPel *bef,
double sq_maxerror )
static void
vips_foreign_save_cgif_set_transparent( VipsForeignSaveCgif *cgif,
VipsPel *old, VipsPel *new, VipsPel *index, int n_pels, int trans )
{
if( bef[3] != cur[3] )
/* Alpha channel must be identical.
*/
return FALSE;
if( bef[3] == 0 && cur[3] == 0)
/* RGB component can be ignored.
*/
return TRUE;
int sq_maxerror = cgif->interframe_maxerror * cgif->interframe_maxerror;
/* Test Euclidean distance between the two points.
*/
const int dR = cur[0] - bef[0];
const int dG = cur[1] - bef[1];
const int dB = cur[2] - bef[2];
int i;
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,
const VipsQuantisePalette *old )
{
int i, j;
double best_dist, dist, rd, gd, bd;
double total_dist;
int best_dist, dist, rd, gd, bd;
int total_dist;
g_assert( new->count <= 256 );
g_assert( old->count <= 256 );
total_dist = 0.0;
total_dist = 0;
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++ ) {
if( new->entries[i].a >= 128 ) {
if( new->entries[i].a ) {
/* The new entry is solid.
* If the old entry is transparent, ignore it.
* Otherwise, compare RGB.
*/
if( old->entries[j].a < 128 )
if( !old->entries[j].a )
continue;
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
* the closest color. Otherwise, ignore it.
*/
if( old->entries[j].a < 128 ) {
if( !old->entries[j].a ) {
best_dist = 0;
break;
}
@ -235,7 +274,140 @@ vips__cgif_compare_palettes( const VipsQuantisePalette *new,
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!
@ -246,33 +418,28 @@ vips_foreign_save_cgif_write_frame( VipsForeignSaveCgif *cgif )
VipsObjectClass *class = VIPS_OBJECT_GET_CLASS( cgif );
VipsRect *frame_rect = &cgif->frame->valid;
int page_index = frame_rect->top / frame_rect->height;
/* We know this fits in an int since we limit frame size.
*/
int n_pels = frame_rect->height * frame_rect->width;
VipsPel *frame_bytes =
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;
VipsPel * restrict cur;
VipsPel * restrict bef;
gboolean has_alpha_constraint = FALSE;
VipsPel *rgb;
VipsQuantiseImage *image;
gboolean use_local;
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;
const VipsQuantisePalette *lp;
CGIF_FrameConfig frame_config = { 0 };
int n_colours;
#ifdef DEBUG_VERBOSE
printf( "vips_foreign_save_cgif_write_frame: %d\n", page_index );
#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
* it's a buffer we made.
*
@ -280,180 +447,94 @@ vips_foreign_save_cgif_write_frame( VipsForeignSaveCgif *cgif )
* frame before.
*
* 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
* optimization (maxerror).
*/
cur = frame_bytes;
bef = cgif->frame_bytes_head;
p = frame_bytes;
has_alpha_constraint = FALSE;
for( i = 0; i < n_pels; i++ ) {
if( cur[3] >= 128 )
cur[3] = 255;
if( p[3] >= 128 )
p[3] = 255;
else {
/* Helps the quanizer generate a better palette.
/* Helps the quantiser generate a better palette.
*/
cur[0] = 0;
cur[1] = 0;
cur[2] = 0;
cur[3] = 0;
p[0] = 0;
p[1] = 0;
p[2] = 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;
}
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;
lp = vips__quantise_get_palette( quantisation_result );
}
use_local = FALSE;
}
else {
if( vips__quantise_image_quantize_fixed( cgif->input_image,
cgif->attr, &quantisation_result ) ) {
vips_error( class->nickname,
"%s", _( "quantisation failed" ) );
/* Local mode. Pick the global, this or previous palette.
*/
if( vips_foreign_save_cgif_pick_quantiser( cgif,
image, &quantisation_result, &use_local ) )
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.
*/
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
* one.
*
* We switch to local tables if we find we need a new palette.
/* Dither frame into @index.
*/
vips__quantise_set_dithering_level( quantisation_result, cgif->dither );
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 ) {
cgif->cgif_config.pGlobalPalette = cgif->palette_rgb;
#ifdef HAVE_CGIF_ATTR_NO_LOOP
cgif->cgif_config.attrFlags =
CGIF_ATTR_IS_ANIMATED |
(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->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;
#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.pContext = (void *) cgif->target;
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
* 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
* with alpha channel.
*/
if( cgif->has_transparency ) {
if( has_transparency ) {
frame_config.attrFlags |= CGIF_FRAME_ATTR_HAS_ALPHA;
frame_config.transIndex = 0;
}
/* Pixels which are equal to pixels in the previous frame can be made
* transparent.
*/
if( cgif->frame_bytes_head ) {
cur = frame_bytes;
bef = cgif->frame_bytes_head;
* transparent, provided no alpha channel constraint is present.
*/
if( page_index > 0 &&
!has_alpha_constraint ) {
int trans = has_transparency ? 0 : n_colours;
/* Transparency trick is only possible when no alpha channel
* constraint is present.
*/
if( !has_alpha_constraint ) {
uint8_t trans_index;
double sq_maxerror;
vips_foreign_save_cgif_set_transparent( cgif,
cgif->previous_frame, frame_bytes, cgif->index,
n_pels, trans );
trans_index = lp->count;
if( cgif->has_transparency ) {
trans_index = 0;
frame_config.attrFlags &=
~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( has_transparency )
frame_config.attrFlags &= ~CGIF_FRAME_ATTR_HAS_ALPHA;
frame_config.attrFlags |= CGIF_FRAME_ATTR_HAS_SET_TRANS;
frame_config.transIndex = trans;
}
if( cgif->delay &&
@ -528,21 +575,24 @@ vips_foreign_save_cgif_write_frame( VipsForeignSaveCgif *cgif )
/* 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.pLocalPalette = cgif->palette_rgb;
frame_config.numLocalPaletteEntries = lp->count;
frame_config.pLocalPalette = rgb;
frame_config.numLocalPaletteEntries = n_colours;
}
/* Write frame to cgif.
*/
frame_config.pImageData = cgif->index;
cgif_addframe( cgif->cgif_context, &frame_config );
if( !cgif->frame_bytes_head ) {
/* Keep head frame_bytes in memory for transparency trick
* which avoids the size explosion (#2576).
*/
cgif->frame_bytes_head = g_malloc( 4 * n_pels );
memcpy( cgif->frame_bytes_head, frame_bytes, 4 * n_pels );
}
/* Take a copy of the RGBA frame.
*/
memcpy( cgif->previous_frame, frame_bytes, 4 * n_pels );
return( 0 );
}
@ -597,8 +647,13 @@ vips_foreign_save_cgif_sink_disc( VipsRegion *region, VipsRect *area, void *a )
image.height = cgif->in->Ysize;
vips_rect_intersectrect( &new_frame, &image,
&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 );
}
} 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.
*/
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_addalpha( cgif->in, &t[1], NULL ) )
return( -1 );
@ -673,60 +719,75 @@ vips_foreign_save_cgif_build( VipsObject *object )
*/
vips__region_no_ownership( cgif->frame );
/* The RGB cmap we write with, sometimes updated on frame write, and
* the frame index buffer.
/* The previous RGBA frame (for spotting pixels which haven't changed).
*/
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 );
/* Set up libimagequant.
*/
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_speed( cgif->attr, 11 - cgif->effort );
/* Initialise quantization result using global palette.
/* Read the palette on the input, if any.
*/
if( !cgif->reoptimise &&
vips_image_get_typeof( cgif->in, "gif-palette" ) ) {
VipsQuantiseImage *tmp_image;
int *tmp_gct;
int *gct;
int gct_length;
if( vips_image_get_typeof( cgif->in, "gif-palette" ) ) {
if( vips_image_get_array_int( cgif->in, "gif-palette",
&cgif->palette, &cgif->n_colours ) )
return( -1 );
vips_image_get_array_int( cgif->in, "gif-palette",
&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 ) ) {
if( cgif->n_colours > 256 ) {
vips_error( class->nickname,
"%s", _( "quantisation failed" ) );
"%s", _( "gif-palette too large" ) );
return( -1 );
}
VIPS_FREE( tmp_gct );
VIPS_FREEF( vips__quantise_image_destroy, tmp_image );
}
/* Set up cgif on first use.
/* LOCAL mode if there's no input palette, or reoptimise is set.
*/
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,
vips_foreign_save_cgif_sink_disc, cgif ) )
return( -1 );
@ -807,7 +868,7 @@ vips_foreign_save_cgif_class_init( VipsForeignSaveCgifClass *class )
_( "Maximum inter-palette error for palette reusage" ),
VIPS_ARGUMENT_OPTIONAL_INPUT,
G_STRUCT_OFFSET( VipsForeignSaveCgif, interpalette_maxerror ),
0, 256, 64.0 );
0, 256, 40.0 );
}
static void
@ -818,7 +879,8 @@ vips_foreign_save_cgif_init( VipsForeignSaveCgif *gif )
gif->bitdepth = 8;
gif->interframe_maxerror = 0.0;
gif->reoptimise = FALSE;
gif->interpalette_maxerror = 64.0;
gif->interpalette_maxerror = 40.0;
gif->mode = VIPS_FOREIGN_SAVE_CGIF_MODE_GLOBAL;
}
typedef struct _VipsForeignSaveCgifTarget {

View File

@ -99,7 +99,7 @@ vips__quantise_image_quantize_fixed( VipsQuantiseImage *const input_image,
const liq_palette *palette;
liq_error err;
liq_image *fake_image;
void *fake_image_pixels;
char fake_image_pixels[4] = { 0 };
/* 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
* colors from 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;
}
@ -131,13 +129,12 @@ vips__quantise_image_quantize_fixed( VipsQuantiseImage *const input_image,
liq_result_destroy( result );
/* Finally, quantize the fake image with fixed colors to get the result
* palette which won't be changed during remapping
/* Finally, quantize the fake image with fixed colors to make a
* VipsQuantiseResult with a fixed palette.
*/
err = liq_image_quantize( fake_image, options, result_output );
liq_image_destroy( fake_image );
free( fake_image_pixels );
return err;
}