PIKApp/plug-ins/common/animation-optimize.c

1424 lines
46 KiB
C

/*
* Animation Optimizer plug-in version 1.1.2
*
* (c) Adam D. Moss, 1997-2003
* adam@gimp.org
* adam@foxbox.org
*
* PIKA - Photo and Image Kooker Application
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
#define EXPERIMENTAL_BACKDROP_CODE
*/
#include "config.h"
#include <string.h>
#include <libpika/pika.h>
#include "libpika/stdplugins-intl.h"
#define OPTIMIZE_PROC "plug-in-animationoptimize"
#define OPTIMIZE_DIFF_PROC "plug-in-animationoptimize-diff"
#define UNOPTIMIZE_PROC "plug-in-animationunoptimize"
#define REMOVE_BACKDROP_PROC "plug-in-animation-remove-backdrop"
#define FIND_BACKDROP_PROC "plug-in-animation-find-backdrop"
typedef enum
{
DISPOSE_UNDEFINED = 0x00,
DISPOSE_COMBINE = 0x01,
DISPOSE_REPLACE = 0x02
} DisposeType;
typedef enum
{
OPOPTIMIZE = 0L,
OPUNOPTIMIZE = 1L,
OPFOREGROUND = 2L,
OPBACKGROUND = 3L
} operatingMode;
typedef struct _Optimize Optimize;
typedef struct _OptimizeClass OptimizeClass;
struct _Optimize
{
PikaPlugIn parent_instance;
};
struct _OptimizeClass
{
PikaPlugInClass parent_class;
};
#define OPTIMIZE_TYPE (optimize_get_type ())
#define OPTIMIZE (obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), OPTIMIZE_TYPE, Optimize))
GType optimize_get_type (void) G_GNUC_CONST;
static GList * optimize_query_procedures (PikaPlugIn *plug_in);
static PikaProcedure * optimize_create_procedure (PikaPlugIn *plug_in,
const gchar *name);
static PikaValueArray * optimize_run (PikaProcedure *procedure,
PikaRunMode run_mode,
PikaImage *image,
gint n_drawables,
PikaDrawable **drawables,
const PikaValueArray *args,
gpointer run_data);
static PikaImage * do_optimizations (PikaRunMode run_mode,
PikaImage *image,
gboolean diff_only);
/* tag util functions*/
static gint parse_ms_tag (const gchar *str);
static DisposeType parse_disposal_tag (const gchar *str);
static DisposeType get_frame_disposal (guint whichframe);
static guint32 get_frame_duration (guint whichframe);
static void remove_disposal_tag (gchar *dest,
gchar *src);
static void remove_ms_tag (gchar *dest,
gchar *src);
static gboolean is_disposal_tag (const gchar *str,
DisposeType *disposal,
gint *taglength);
static gboolean is_ms_tag (const gchar *str,
gint *duration,
gint *taglength);
G_DEFINE_TYPE (Optimize, optimize, PIKA_TYPE_PLUG_IN)
PIKA_MAIN (OPTIMIZE_TYPE)
DEFINE_STD_SET_I18N
/* Global widgets'n'stuff */
static guint width, height;
static gint32 total_frames;
static PikaLayer **layers;
static guchar pixelstep;
static guchar *palette;
static gint ncolors;
static operatingMode opmode;
static void
optimize_class_init (OptimizeClass *klass)
{
PikaPlugInClass *plug_in_class = PIKA_PLUG_IN_CLASS (klass);
plug_in_class->query_procedures = optimize_query_procedures;
plug_in_class->create_procedure = optimize_create_procedure;
plug_in_class->set_i18n = STD_SET_I18N;
}
static void
optimize_init (Optimize *optimize)
{
}
static GList *
optimize_query_procedures (PikaPlugIn *plug_in)
{
GList *list = NULL;
list = g_list_append (list, g_strdup (OPTIMIZE_PROC));
list = g_list_append (list, g_strdup (OPTIMIZE_DIFF_PROC));
list = g_list_append (list, g_strdup (UNOPTIMIZE_PROC));
#ifdef EXPERIMENTAL_BACKDROP_CODE
list = g_list_append (list, g_strdup (REMOVE_BACKDROP_PROC));
list = g_list_append (list, g_strdup (FIND_BACKDROP_PROC));
#endif
return list;
}
static PikaProcedure *
optimize_create_procedure (PikaPlugIn *plug_in,
const gchar *name)
{
PikaProcedure *procedure = NULL;
if (! strcmp (name, OPTIMIZE_PROC))
{
procedure = pika_image_procedure_new (plug_in, name,
PIKA_PDB_PROC_TYPE_PLUGIN,
optimize_run, NULL, NULL);
pika_procedure_set_sensitivity_mask (procedure,
PIKA_PROCEDURE_SENSITIVE_DRAWABLE |
PIKA_PROCEDURE_SENSITIVE_DRAWABLES |
PIKA_PROCEDURE_SENSITIVE_NO_DRAWABLES);
pika_procedure_set_menu_label (procedure, _("Optimize (for _GIF)"));
pika_procedure_set_documentation (procedure,
_("Modify image to reduce size when "
"saved as GIF animation"),
"This procedure applies various "
"optimizations to a PIKA layer-based "
"animation in an attempt to reduce the "
"final file size. If a frame of the"
"animation can use the 'combine' "
"mode, this procedure attempts to "
"maximize the number of ajdacent "
"pixels having the same color, which"
"improves the compression for some "
"image formats such as GIF or MNG.",
name);
}
else if (! strcmp (name, OPTIMIZE_DIFF_PROC))
{
procedure = pika_image_procedure_new (plug_in, name,
PIKA_PDB_PROC_TYPE_PLUGIN,
optimize_run, NULL, NULL);
pika_procedure_set_sensitivity_mask (procedure,
PIKA_PROCEDURE_SENSITIVE_DRAWABLE |
PIKA_PROCEDURE_SENSITIVE_DRAWABLES |
PIKA_PROCEDURE_SENSITIVE_NO_DRAWABLES);
pika_procedure_set_menu_label (procedure, _("_Optimize (Difference)"));
pika_procedure_set_documentation (procedure,
_("Reduce file size where "
"combining layers is possible"),
"This procedure applies various "
"optimizations to a PIKA layer-based "
"animation in an attempt to reduce "
"the final file size. If a frame of "
"the animation can use the 'combine' "
"mode, this procedure uses a simple "
"difference between the frames.",
name);
}
else if (! strcmp (name, UNOPTIMIZE_PROC))
{
procedure = pika_image_procedure_new (plug_in, name,
PIKA_PDB_PROC_TYPE_PLUGIN,
optimize_run, NULL, NULL);
pika_procedure_set_sensitivity_mask (procedure,
PIKA_PROCEDURE_SENSITIVE_DRAWABLE |
PIKA_PROCEDURE_SENSITIVE_DRAWABLES |
PIKA_PROCEDURE_SENSITIVE_NO_DRAWABLES);
pika_procedure_set_menu_label (procedure, _("_Unoptimize"));
pika_procedure_set_documentation (procedure,
_("Remove optimization to make "
"editing easier"),
"This procedure 'simplifies' a PIKA "
"layer-based animation that has been "
"optimized for animation. This makes "
"editing the animation much easier.",
name);
}
else if (! strcmp (name, REMOVE_BACKDROP_PROC))
{
procedure = pika_image_procedure_new (plug_in, name,
PIKA_PDB_PROC_TYPE_PLUGIN,
optimize_run, NULL, NULL);
pika_procedure_set_sensitivity_mask (procedure,
PIKA_PROCEDURE_SENSITIVE_DRAWABLE |
PIKA_PROCEDURE_SENSITIVE_DRAWABLES |
PIKA_PROCEDURE_SENSITIVE_NO_DRAWABLES);
pika_procedure_set_menu_label (procedure, _("_Remove Backdrop"));
pika_procedure_set_documentation (procedure,
"This procedure attempts to remove "
"the backdrop from a PIKA layer-based "
"animation, leaving the foreground "
"animation over transparency.",
NULL,
name);
}
else if (! strcmp (name, FIND_BACKDROP_PROC))
{
procedure = pika_image_procedure_new (plug_in, name,
PIKA_PDB_PROC_TYPE_PLUGIN,
optimize_run, NULL, NULL);
pika_procedure_set_sensitivity_mask (procedure,
PIKA_PROCEDURE_SENSITIVE_DRAWABLE |
PIKA_PROCEDURE_SENSITIVE_DRAWABLES |
PIKA_PROCEDURE_SENSITIVE_NO_DRAWABLES);
pika_procedure_set_menu_label (procedure, _("_Find Backdrop"));
pika_procedure_set_documentation (procedure,
"This procedure attempts to remove "
"the foreground from a PIKA "
"layer-based animation, leaving"
" a one-layered image containing only "
"the constant backdrop image.",
NULL,
name);
}
if (procedure)
{
pika_procedure_set_image_types (procedure, "*");
pika_procedure_add_menu_path (procedure, "<Image>/Filters/Animation");
pika_procedure_set_attribution (procedure,
"Adam D. Moss <adam@gimp.org>",
"Adam D. Moss <adam@gimp.org>",
"1997-2003");
PIKA_PROC_VAL_IMAGE (procedure, "result",
"Result",
"Resultimg image",
FALSE,
G_PARAM_READWRITE);
}
return procedure;
}
static PikaValueArray *
optimize_run (PikaProcedure *procedure,
PikaRunMode run_mode,
PikaImage *image,
gint n_drawables,
PikaDrawable **drawables,
const PikaValueArray *args,
gpointer run_data)
{
PikaValueArray *return_vals;
const gchar *name = pika_procedure_get_name (procedure);
gboolean diff_only = FALSE;
gegl_init (NULL, NULL);
if (! strcmp (name, OPTIMIZE_PROC))
{
opmode = OPOPTIMIZE;
}
else if (! strcmp (name, OPTIMIZE_DIFF_PROC))
{
opmode = OPOPTIMIZE;
diff_only = TRUE;
}
else if (! strcmp (name, UNOPTIMIZE_PROC))
{
opmode = OPUNOPTIMIZE;
}
else if (strcmp (name, FIND_BACKDROP_PROC))
{
opmode = OPBACKGROUND;
}
else if (strcmp (name, REMOVE_BACKDROP_PROC))
{
opmode = OPFOREGROUND;
}
image = do_optimizations (run_mode, image, diff_only);
if (run_mode != PIKA_RUN_NONINTERACTIVE)
pika_displays_flush ();
return_vals = pika_procedure_new_return_values (procedure,
PIKA_PDB_SUCCESS,
NULL);
PIKA_VALUES_SET_IMAGE (return_vals, 1, image);
return return_vals;
}
/* Rendering Functions */
static void
total_alpha (guchar *imdata,
guint32 numpix,
guchar bytespp)
{
/* Set image to total-transparency w/black
*/
memset (imdata, 0, numpix * bytespp);
}
static const Babl *
get_format (PikaDrawable *drawable)
{
if (pika_drawable_is_rgb (drawable))
{
if (pika_drawable_has_alpha (drawable))
return babl_format ("R'G'B'A u8");
else
return babl_format ("R'G'B' u8");
}
else if (pika_drawable_is_gray (drawable))
{
if (pika_drawable_has_alpha (drawable))
return babl_format ("Y'A u8");
else
return babl_format ("Y' u8");
}
return pika_drawable_get_format (drawable);
}
static void
compose_row (gint frame_num,
DisposeType dispose,
gint row_num,
guchar *dest,
gint dest_width,
PikaDrawable *drawable,
gboolean cleanup)
{
static guchar *line_buf = NULL;
GeglBuffer *src_buffer;
const Babl *format;
guchar *srcptr;
gint rawx, rawy, rawbpp, rawwidth, rawheight;
gint i;
gboolean has_alpha;
if (cleanup)
{
if (line_buf)
{
g_free (line_buf);
line_buf = NULL;
}
return;
}
if (dispose == DISPOSE_REPLACE)
{
total_alpha (dest, dest_width, pixelstep);
}
pika_drawable_get_offsets (drawable, &rawx, &rawy);
rawwidth = pika_drawable_get_width (drawable);
rawheight = pika_drawable_get_height (drawable);
/* this frame has nothing to give us for this row; return */
if (row_num >= rawheight + rawy ||
row_num < rawy)
return;
format = get_format (drawable);
has_alpha = pika_drawable_has_alpha (drawable);
rawbpp = babl_format_get_bytes_per_pixel (format);
if (line_buf)
{
g_free (line_buf);
line_buf = NULL;
}
line_buf = g_malloc (rawwidth * rawbpp);
/* Initialise and fetch the raw new frame row */
src_buffer = pika_drawable_get_buffer (drawable);
gegl_buffer_get (src_buffer, GEGL_RECTANGLE (0, row_num - rawy,
rawwidth, 1), 1.0,
format, line_buf,
GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
g_object_unref (src_buffer);
/* render... */
srcptr = line_buf;
for (i=rawx; i<rawwidth+rawx; i++)
{
if (i>=0 && i<dest_width)
{
if ((!has_alpha) || ((*(srcptr+rawbpp-1))&128))
{
gint pi;
for (pi = 0; pi < pixelstep-1; pi++)
{
dest[i*pixelstep +pi] = *(srcptr + pi);
}
dest[i*pixelstep + pixelstep - 1] = 255;
}
}
srcptr += rawbpp;
}
}
static PikaImage *
do_optimizations (PikaRunMode run_mode,
PikaImage *image,
gboolean diff_only)
{
PikaImage *new_image;
PikaImageBaseType imagetype;
PikaImageType drawabletype_alpha;
static guchar *rawframe = NULL;
guchar *srcptr;
guchar *destptr;
gint row, this_frame_num;
guint32 frame_sizebytes;
PikaLayer *new_layer;
DisposeType dispose;
guchar *this_frame = NULL;
guchar *last_frame = NULL;
guchar *opti_frame = NULL;
guchar *back_frame = NULL;
gint this_delay;
gint cumulated_delay = 0;
PikaLayer *last_true_frame = NULL;
gint buflen;
gchar *oldlayer_name;
gchar *newlayer_name;
gboolean can_combine;
gint32 bbox_top, bbox_bottom, bbox_left, bbox_right;
gint32 rbox_top, rbox_bottom, rbox_left, rbox_right;
switch (opmode)
{
case OPUNOPTIMIZE:
pika_progress_init (_("Unoptimizing animation"));
break;
case OPFOREGROUND:
pika_progress_init (_("Removing animation background"));
break;
case OPBACKGROUND:
pika_progress_init (_("Finding animation background"));
break;
case OPOPTIMIZE:
default:
pika_progress_init (_("Optimizing animation"));
break;
}
width = pika_image_get_width (image);
height = pika_image_get_height (image);
layers = pika_image_get_layers (image, &total_frames);
imagetype = pika_image_get_base_type (image);
pixelstep = (imagetype == PIKA_RGB) ? 4 : 2;
drawabletype_alpha = (imagetype == PIKA_RGB) ? PIKA_RGBA_IMAGE :
((imagetype == PIKA_INDEXED) ? PIKA_INDEXEDA_IMAGE : PIKA_GRAYA_IMAGE);
frame_sizebytes = width * height * pixelstep;
this_frame = g_malloc (frame_sizebytes);
last_frame = g_malloc (frame_sizebytes);
opti_frame = g_malloc (frame_sizebytes);
if (opmode == OPBACKGROUND ||
opmode == OPFOREGROUND)
back_frame = g_malloc (frame_sizebytes);
total_alpha (this_frame, width*height, pixelstep);
total_alpha (last_frame, width*height, pixelstep);
new_image = pika_image_new (width, height, imagetype);
pika_image_undo_disable (new_image);
if (imagetype == PIKA_INDEXED)
{
palette = pika_image_get_colormap (image, NULL, &ncolors);
pika_image_set_colormap (new_image, palette, ncolors);
}
#if 1
if (opmode == OPBACKGROUND ||
opmode == OPFOREGROUND)
{
/* iterate through all rows of all frames, find statistical
mode for each pixel position. */
gint i;
guint j;
guchar **these_rows;
guchar **red;
guchar **green;
guchar **blue;
guint **count;
guint *num_colors;
these_rows = g_new (guchar *, total_frames);
red = g_new (guchar *, total_frames);
green = g_new (guchar *, total_frames);
blue = g_new (guchar *, total_frames);
count = g_new (guint *, total_frames);
num_colors = g_new (guint, width);
for (this_frame_num=0; this_frame_num<total_frames; this_frame_num++)
{
these_rows[this_frame_num] = g_malloc (width * pixelstep);
red[this_frame_num] = g_new (guchar, width);
green[this_frame_num] = g_new (guchar, width);
blue[this_frame_num] = g_new (guchar, width);
count[this_frame_num] = g_new0(guint, width);
}
for (row = 0; row < height; row++)
{
memset (num_colors, 0, width * sizeof (guint));
for (this_frame_num=0; this_frame_num<total_frames; this_frame_num++)
{
PikaDrawable *drawable =
PIKA_DRAWABLE (layers[total_frames-(this_frame_num+1)]);
dispose = get_frame_disposal (this_frame_num);
compose_row (this_frame_num,
dispose,
row,
these_rows[this_frame_num],
width,
drawable,
FALSE);
}
for (this_frame_num=0; this_frame_num<total_frames; this_frame_num++)
{
for (i=0; i<width; i++)
{
if (these_rows[this_frame_num][i * pixelstep + pixelstep -1]
>= 128)
{
for (j=0; j<num_colors[i]; j++)
{
switch (pixelstep)
{
case 4:
if (these_rows[this_frame_num][i * 4 +0] ==
red[j][i] &&
these_rows[this_frame_num][i * 4 +1] ==
green[j][i] &&
these_rows[this_frame_num][i * 4 +2] ==
blue[j][i])
{
(count[j][i])++;
goto same;
}
break;
case 2:
if (these_rows[this_frame_num][i * 2 +0] ==
red[j][i])
{
(count[j][i])++;
goto same;
}
break;
default:
g_error ("Eeep!");
break;
}
}
count[num_colors[i]][i] = 1;
red[num_colors[i]][i] =
these_rows[this_frame_num][i * pixelstep];
if (pixelstep == 4)
{
green[num_colors[i]][i] =
these_rows[this_frame_num][i * 4 +1];
blue[num_colors[i]][i] =
these_rows[this_frame_num][i * 4 +2];
}
num_colors[i]++;
}
same:
/* nop */;
}
}
for (i=0; i<width; i++)
{
guint best_count = 0;
guchar best_r = 255, best_g = 0, best_b = 255;
for (j=0; j<num_colors[i]; j++)
{
if (count[j][i] > best_count)
{
best_count = count[j][i];
best_r = red[j][i];
best_g = green[j][i];
best_b = blue[j][i];
}
}
back_frame[width * pixelstep * row +i*pixelstep + 0] = best_r;
if (pixelstep == 4)
{
back_frame[width * pixelstep * row +i*pixelstep + 1] =
best_g;
back_frame[width * pixelstep * row +i*pixelstep + 2] =
best_b;
}
back_frame[width * pixelstep * row +i*pixelstep +pixelstep-1] =
(best_count == 0) ? 0 : 255;
if (best_count == 0)
g_warning ("yayyyy!");
}
/* memcpy (&back_frame[width * pixelstep * row],
these_rows[0],
width * pixelstep);*/
}
for (this_frame_num=0; this_frame_num<total_frames; this_frame_num++)
{
g_free (these_rows[this_frame_num]);
g_free (red[this_frame_num]);
g_free (green[this_frame_num]);
g_free (blue[this_frame_num]);
g_free (count[this_frame_num]);
}
g_free (these_rows);
g_free (red);
g_free (green);
g_free (blue);
g_free (count);
g_free (num_colors);
}
#endif
if (opmode == OPBACKGROUND)
{
GeglBuffer *buffer;
const Babl *format;
new_layer = pika_layer_new (new_image,
"Backgroundx",
width, height,
drawabletype_alpha,
100.0,
pika_image_get_default_new_layer_mode (new_image));
pika_image_insert_layer (new_image, new_layer, NULL, 0);
buffer = pika_drawable_get_buffer (PIKA_DRAWABLE (new_layer));
format = get_format (PIKA_DRAWABLE (new_layer));
gegl_buffer_set (buffer, GEGL_RECTANGLE (0, 0, width, height), 0,
format, back_frame,
GEGL_ABYSS_NONE);
g_object_unref (buffer);
}
else
{
for (this_frame_num=0; this_frame_num<total_frames; this_frame_num++)
{
/*
* BUILD THIS FRAME into our 'this_frame' buffer.
*/
PikaDrawable *drawable =
PIKA_DRAWABLE (layers[total_frames-(this_frame_num+1)]);
/* Image has been closed/etc since we got the layer list? */
/* FIXME - How do we tell if a pika_drawable_get () fails? */
if (pika_drawable_get_width (drawable) == 0)
{
pika_quit ();
}
this_delay = get_frame_duration (this_frame_num);
dispose = get_frame_disposal (this_frame_num);
for (row = 0; row < height; row++)
{
compose_row (this_frame_num,
dispose,
row,
&this_frame[pixelstep*width * row],
width,
drawable,
FALSE
);
}
if (opmode == OPFOREGROUND)
{
gint xit, yit, byteit;
for (yit=0; yit<height; yit++)
{
for (xit=0; xit<width; xit++)
{
for (byteit=0; byteit<pixelstep-1; byteit++)
{
if (back_frame[yit*width*pixelstep + xit*pixelstep
+ byteit]
!=
this_frame[yit*width*pixelstep + xit*pixelstep
+ byteit])
{
goto enough;
}
}
this_frame[yit*width*pixelstep + xit*pixelstep
+ pixelstep - 1] = 0;
enough:
/* nop */;
}
}
}
can_combine = FALSE;
bbox_left = 0;
bbox_top = 0;
bbox_right = width;
bbox_bottom = height;
rbox_left = 0;
rbox_top = 0;
rbox_right = width;
rbox_bottom = height;
/* copy 'this' frame into a buffer which we can safely molest */
memcpy (opti_frame, this_frame, frame_sizebytes);
/*
*
* OPTIMIZE HERE!
*
*/
if (
(this_frame_num != 0) /* Can't delta bottom frame! */
&& (opmode == OPOPTIMIZE)
)
{
gint xit, yit, byteit;
can_combine = TRUE;
/*
* SEARCH FOR BOUNDING BOX
*/
bbox_left = width;
bbox_top = height;
bbox_right = 0;
bbox_bottom = 0;
rbox_left = width;
rbox_top = height;
rbox_right = 0;
rbox_bottom = 0;
for (yit=0; yit<height; yit++)
{
for (xit=0; xit<width; xit++)
{
gboolean keep_pix;
gboolean opaq_pix;
/* Check if 'this' and 'last' are transparent */
if (!(this_frame[yit*width*pixelstep + xit*pixelstep
+ pixelstep-1]&128)
&&
!(last_frame[yit*width*pixelstep + xit*pixelstep
+ pixelstep-1]&128))
{
keep_pix = FALSE;
opaq_pix = FALSE;
goto decided;
}
/* Check if just 'this' is transparent */
if ((last_frame[yit*width*pixelstep + xit*pixelstep
+ pixelstep-1]&128)
&&
!(this_frame[yit*width*pixelstep + xit*pixelstep
+ pixelstep-1]&128))
{
keep_pix = TRUE;
opaq_pix = FALSE;
can_combine = FALSE;
goto decided;
}
/* Check if just 'last' is transparent */
if (!(last_frame[yit*width*pixelstep + xit*pixelstep
+ pixelstep-1]&128)
&&
(this_frame[yit*width*pixelstep + xit*pixelstep
+ pixelstep-1]&128))
{
keep_pix = TRUE;
opaq_pix = TRUE;
goto decided;
}
/* If 'last' and 'this' are opaque, we have
* to check if they're the same color - we
* only have to keep the pixel if 'last' or
* 'this' are opaque and different.
*/
keep_pix = FALSE;
opaq_pix = TRUE;
for (byteit=0; byteit<pixelstep-1; byteit++)
{
if ((last_frame[yit*width*pixelstep + xit*pixelstep
+ byteit]
!=
this_frame[yit*width*pixelstep + xit*pixelstep
+ byteit])
)
{
keep_pix = TRUE;
goto decided;
}
}
decided:
if (opaq_pix)
{
if (xit<rbox_left) rbox_left=xit;
if (xit>rbox_right) rbox_right=xit;
if (yit<rbox_top) rbox_top=yit;
if (yit>rbox_bottom) rbox_bottom=yit;
}
if (keep_pix)
{
if (xit<bbox_left) bbox_left=xit;
if (xit>bbox_right) bbox_right=xit;
if (yit<bbox_top) bbox_top=yit;
if (yit>bbox_bottom) bbox_bottom=yit;
}
else
{
/* pixel didn't change this frame - make
* it transparent in our optimized buffer!
*/
opti_frame[yit*width*pixelstep + xit*pixelstep
+ pixelstep-1] = 0;
}
} /* xit */
} /* yit */
if (!can_combine)
{
bbox_left = rbox_left;
bbox_top = rbox_top;
bbox_right = rbox_right;
bbox_bottom = rbox_bottom;
}
bbox_right++;
bbox_bottom++;
if (can_combine && !diff_only)
{
/* Try to optimize the pixel data for RLE or LZW compression
* by making some transparent pixels non-transparent if they
* would have the same color as the adjacent pixels. This
* gives a better compression if the algorithm compresses
* the image line by line.
* See: http://bugzilla.gnome.org/show_bug.cgi?id=66367
* It may not be very efficient to add two additional passes
* over the pixels, but this hopefully makes the code easier
* to maintain and less error-prone.
*/
for (yit = bbox_top; yit < bbox_bottom; yit++)
{
/* Compare with previous pixels from left to right */
for (xit = bbox_left + 1; xit < bbox_right; xit++)
{
if (!(opti_frame[yit*width*pixelstep
+ xit*pixelstep
+ pixelstep-1]&128)
&& (opti_frame[yit*width*pixelstep
+ (xit-1)*pixelstep
+ pixelstep-1]&128)
&& (last_frame[yit*width*pixelstep
+ xit*pixelstep
+ pixelstep-1]&128))
{
for (byteit=0; byteit<pixelstep-1; byteit++)
{
if (opti_frame[yit*width*pixelstep
+ (xit-1)*pixelstep
+ byteit]
!=
last_frame[yit*width*pixelstep
+ xit*pixelstep
+ byteit])
{
goto skip_right;
}
}
/* copy the color and alpha */
for (byteit=0; byteit<pixelstep; byteit++)
{
opti_frame[yit*width*pixelstep
+ xit*pixelstep
+ byteit]
= last_frame[yit*width*pixelstep
+ xit*pixelstep
+ byteit];
}
}
skip_right:
/* nop */;
} /* xit */
/* Compare with next pixels from right to left */
for (xit = bbox_right - 2; xit >= bbox_left; xit--)
{
if (!(opti_frame[yit*width*pixelstep
+ xit*pixelstep
+ pixelstep-1]&128)
&& (opti_frame[yit*width*pixelstep
+ (xit+1)*pixelstep
+ pixelstep-1]&128)
&& (last_frame[yit*width*pixelstep
+ xit*pixelstep
+ pixelstep-1]&128))
{
for (byteit=0; byteit<pixelstep-1; byteit++)
{
if (opti_frame[yit*width*pixelstep
+ (xit+1)*pixelstep
+ byteit]
!=
last_frame[yit*width*pixelstep
+ xit*pixelstep
+ byteit])
{
goto skip_left;
}
}
/* copy the color and alpha */
for (byteit=0; byteit<pixelstep; byteit++)
{
opti_frame[yit*width*pixelstep
+ xit*pixelstep
+ byteit]
= last_frame[yit*width*pixelstep
+ xit*pixelstep
+ byteit];
}
}
skip_left:
/* nop */;
} /* xit */
} /* yit */
}
/*
* Collapse opti_frame data down such that the data
* which occupies the bounding box sits at the start
* of the data (for convenience with ..set_rect ()).
*/
destptr = opti_frame;
/*
* If can_combine, then it's safe to use our optimized
* alpha information. Otherwise, an opaque pixel became
* transparent this frame, and we'll have to use the
* actual true frame's alpha.
*/
if (can_combine)
srcptr = opti_frame;
else
srcptr = this_frame;
for (yit=bbox_top; yit<bbox_bottom; yit++)
{
for (xit=bbox_left; xit<bbox_right; xit++)
{
for (byteit=0; byteit<pixelstep; byteit++)
{
*(destptr++) = srcptr[yit*pixelstep*width +
pixelstep*xit + byteit];
}
}
}
} /* !bot frame? */
else
{
memcpy (opti_frame, this_frame, frame_sizebytes);
}
/*
*
* REMEMBER THE ANIMATION STATUS TO DELTA AGAINST NEXT TIME
*
*/
memcpy (last_frame, this_frame, frame_sizebytes);
/*
*
* PUT THIS FRAME INTO A NEW LAYER IN THE NEW IMAGE
*
*/
oldlayer_name =
pika_item_get_name (PIKA_ITEM (layers[total_frames-(this_frame_num+1)]));
buflen = strlen (oldlayer_name) + 40;
newlayer_name = g_malloc (buflen);
remove_disposal_tag (newlayer_name, oldlayer_name);
g_free (oldlayer_name);
oldlayer_name = g_malloc (buflen);
remove_ms_tag (oldlayer_name, newlayer_name);
g_snprintf (newlayer_name, buflen, "%s(%dms)%s",
oldlayer_name, this_delay,
(this_frame_num == 0) ? "" :
can_combine ? "(combine)" : "(replace)");
g_free (oldlayer_name);
/* Empty frame! */
if (bbox_right <= bbox_left ||
bbox_bottom <= bbox_top)
{
cumulated_delay += this_delay;
g_free (newlayer_name);
oldlayer_name = pika_item_get_name (PIKA_ITEM (last_true_frame));
buflen = strlen (oldlayer_name) + 40;
newlayer_name = g_malloc (buflen);
remove_disposal_tag (newlayer_name, oldlayer_name);
g_free (oldlayer_name);
oldlayer_name = g_malloc (buflen);
remove_ms_tag (oldlayer_name, newlayer_name);
g_snprintf (newlayer_name, buflen, "%s(%dms)%s",
oldlayer_name, cumulated_delay,
(this_frame_num == 0) ? "" :
can_combine ? "(combine)" : "(replace)");
pika_item_set_name (PIKA_ITEM (last_true_frame), newlayer_name);
g_free (newlayer_name);
g_free (oldlayer_name);
}
else
{
GeglBuffer *buffer;
const Babl *format;
cumulated_delay = this_delay;
last_true_frame =
new_layer = pika_layer_new (new_image,
newlayer_name,
bbox_right-bbox_left,
bbox_bottom-bbox_top,
drawabletype_alpha,
100.0,
pika_image_get_default_new_layer_mode (new_image));
g_free (newlayer_name);
pika_image_insert_layer (new_image, new_layer, NULL, 0);
buffer = pika_drawable_get_buffer (PIKA_DRAWABLE (new_layer));
format = get_format (PIKA_DRAWABLE (new_layer));
gegl_buffer_set (buffer,
GEGL_RECTANGLE (0, 0,
bbox_right-bbox_left,
bbox_bottom-bbox_top), 0,
format, opti_frame,
GEGL_AUTO_ROWSTRIDE);
g_object_unref (buffer);
pika_item_transform_translate (PIKA_ITEM (new_layer), bbox_left, bbox_top);
}
pika_progress_update (((gdouble) this_frame_num + 1.0) /
((gdouble) total_frames));
}
pika_progress_update (1.0);
}
pika_image_undo_enable (new_image);
if (run_mode != PIKA_RUN_NONINTERACTIVE)
pika_display_new (new_image);
g_free (rawframe);
rawframe = NULL;
g_free (last_frame);
last_frame = NULL;
g_free (this_frame);
this_frame = NULL;
g_free (opti_frame);
opti_frame = NULL;
g_free (back_frame);
back_frame = NULL;
return new_image;
}
/* Util. */
static DisposeType
get_frame_disposal (guint whichframe)
{
gchar *layer_name;
DisposeType disposal;
layer_name = pika_item_get_name (PIKA_ITEM (layers[total_frames-(whichframe+1)]));
disposal = parse_disposal_tag (layer_name);
g_free (layer_name);
return disposal;
}
static guint32
get_frame_duration (guint whichframe)
{
gchar* layer_name;
gint duration = 0;
layer_name = pika_item_get_name (PIKA_ITEM (layers[total_frames-(whichframe+1)]));
if (layer_name)
{
duration = parse_ms_tag (layer_name);
g_free (layer_name);
}
if (duration < 0) duration = 100; /* FIXME for default-if-not-said */
if (duration == 0) duration = 100; /* FIXME - 0-wait is nasty */
return (guint32) duration;
}
static gboolean
is_ms_tag (const gchar *str,
gint *duration,
gint *taglength)
{
gint sum = 0;
gint offset;
gint length;
length = strlen (str);
if (str[0] != '(')
return FALSE;
offset = 1;
/* eat any spaces between open-parenthesis and number */
while ((offset<length) && (str[offset] == ' '))
offset++;
if ((offset>=length) || (!g_ascii_isdigit (str[offset])))
return 0;
do
{
sum *= 10;
sum += str[offset] - '0';
offset++;
}
while ((offset<length) && (g_ascii_isdigit (str[offset])));
if (length-offset <= 2)
return FALSE;
/* eat any spaces between number and 'ms' */
while ((offset<length) && (str[offset] == ' '))
offset++;
if ((length-offset <= 2) ||
(g_ascii_toupper (str[offset]) != 'M') ||
(g_ascii_toupper (str[offset+1]) != 'S'))
return FALSE;
offset += 2;
/* eat any spaces between 'ms' and close-parenthesis */
while ((offset<length) && (str[offset] == ' '))
offset++;
if ((length-offset < 1) || (str[offset] != ')'))
return FALSE;
offset++;
*duration = sum;
*taglength = offset;
return TRUE;
}
static int
parse_ms_tag (const char *str)
{
gint i;
gint rtn;
gint dummy;
gint length;
length = strlen (str);
for (i = 0; i < length; i++)
{
if (is_ms_tag (&str[i], &rtn, &dummy))
return rtn;
}
return -1;
}
static gboolean
is_disposal_tag (const gchar *str,
DisposeType *disposal,
gint *taglength)
{
if (strlen (str) != 9)
return FALSE;
if (strncmp (str, "(combine)", 9) == 0)
{
*taglength = 9;
*disposal = DISPOSE_COMBINE;
return TRUE;
}
else if (strncmp (str, "(replace)", 9) == 0)
{
*taglength = 9;
*disposal = DISPOSE_REPLACE;
return TRUE;
}
return FALSE;
}
static DisposeType
parse_disposal_tag (const gchar *str)
{
DisposeType rtn;
gint i, dummy;
gint length;
length = strlen (str);
for (i=0; i<length; i++)
{
if (is_disposal_tag (&str[i], &rtn, &dummy))
{
return rtn;
}
}
return DISPOSE_UNDEFINED; /* FIXME */
}
static void
remove_disposal_tag (gchar *dest,
gchar *src)
{
gint offset = 0;
gint destoffset = 0;
gint length;
int taglength;
DisposeType dummy;
length = strlen (src);
strcpy (dest, src);
while (offset<=length)
{
if (is_disposal_tag (&src[offset], &dummy, &taglength))
{
offset += taglength;
}
dest[destoffset] = src[offset];
destoffset++;
offset++;
}
dest[offset] = '\0';
}
static void
remove_ms_tag (gchar *dest,
gchar *src)
{
gint offset = 0;
gint destoffset = 0;
gint length;
gint taglength;
gint dummy;
length = strlen (src);
strcpy (dest, src);
while (offset<=length)
{
if (is_ms_tag (&src[offset], &dummy, &taglength))
{
offset += taglength;
}
dest[destoffset] = src[offset];
destoffset++;
offset++;
}
dest[offset] = '\0';
}