
with proper value names. Mark most values as _BROKEN because they use weird alpha compositing that has to die. Move GimpLayerModeEffects to libgimpbase, deprecate it, and set it as compat enum for GimpLayerMode. Add the GimpLayerModeEffects values as compat constants to script-fu and pygimp.
1329 lines
42 KiB
C
1329 lines
42 KiB
C
/*
|
|
* Animation Optimizer plug-in version 1.1.2
|
|
*
|
|
* (c) Adam D. Moss, 1997-2003
|
|
* adam@gimp.org
|
|
* adam@foxbox.org
|
|
*
|
|
* GIMP - The GNU Image Manipulation Program
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/*
|
|
#define EXPERIMENTAL_BACKDROP_CODE
|
|
*/
|
|
|
|
|
|
#include "config.h"
|
|
|
|
#include <string.h>
|
|
|
|
#include <libgimp/gimp.h>
|
|
|
|
#include "libgimp/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;
|
|
|
|
|
|
/* Declare local functions. */
|
|
static void query (void);
|
|
static void run (const gchar *name,
|
|
gint nparams,
|
|
const GimpParam *param,
|
|
gint *nreturn_vals,
|
|
GimpParam **return_vals);
|
|
|
|
static gint32 do_optimizations (GimpRunMode run_mode,
|
|
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);
|
|
|
|
|
|
const GimpPlugInInfo PLUG_IN_INFO =
|
|
{
|
|
NULL, /* init_proc */
|
|
NULL, /* quit_proc */
|
|
query, /* query_proc */
|
|
run, /* run_proc */
|
|
};
|
|
|
|
|
|
/* Global widgets'n'stuff */
|
|
static guint width, height;
|
|
static gint32 image_id;
|
|
static gint32 new_image_id;
|
|
static gint32 total_frames;
|
|
static gint32 *layers;
|
|
static GimpDrawable *drawable;
|
|
static GimpImageBaseType imagetype;
|
|
static GimpImageType drawabletype_alpha;
|
|
static guchar pixelstep;
|
|
static guchar *palette;
|
|
static gint ncolors;
|
|
static operatingMode opmode;
|
|
|
|
|
|
MAIN ()
|
|
|
|
static void
|
|
query (void)
|
|
{
|
|
static const GimpParamDef args[] =
|
|
{
|
|
{ GIMP_PDB_INT32, "run-mode", "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }" },
|
|
{ GIMP_PDB_IMAGE, "image", "Input image" },
|
|
{ GIMP_PDB_DRAWABLE, "drawable", "Input drawable (unused)" }
|
|
};
|
|
static const GimpParamDef return_args[] =
|
|
{
|
|
{ GIMP_PDB_IMAGE, "result", "Resulting image" }
|
|
};
|
|
|
|
gimp_install_procedure (OPTIMIZE_PROC,
|
|
N_("Modify image to reduce size when saved as GIF animation"),
|
|
"This procedure applies various optimizations to"
|
|
" a GIMP 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.",
|
|
"Adam D. Moss <adam@gimp.org>",
|
|
"Adam D. Moss <adam@gimp.org>",
|
|
"1997-2003",
|
|
N_("Optimize (for _GIF)"),
|
|
"RGB*, INDEXED*, GRAY*",
|
|
GIMP_PLUGIN,
|
|
G_N_ELEMENTS (args),
|
|
G_N_ELEMENTS (return_args),
|
|
args, return_args);
|
|
|
|
gimp_install_procedure (OPTIMIZE_DIFF_PROC,
|
|
N_("Reduce file size where combining layers is possible"),
|
|
"This procedure applies various optimizations to"
|
|
" a GIMP 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.",
|
|
"Adam D. Moss <adam@gimp.org>",
|
|
"Adam D. Moss <adam@gimp.org>",
|
|
"1997-2001",
|
|
N_("_Optimize (Difference)"),
|
|
"RGB*, INDEXED*, GRAY*",
|
|
GIMP_PLUGIN,
|
|
G_N_ELEMENTS (args),
|
|
G_N_ELEMENTS (return_args),
|
|
args, return_args);
|
|
|
|
gimp_install_procedure (UNOPTIMIZE_PROC,
|
|
N_("Remove optimization to make editing easier"),
|
|
"This procedure 'simplifies' a GIMP layer-based"
|
|
" animation that has been optimized for animation. "
|
|
"This makes editing the animation much easier.",
|
|
"Adam D. Moss <adam@gimp.org>",
|
|
"Adam D. Moss <adam@gimp.org>",
|
|
"1997-2001",
|
|
N_("_Unoptimize"),
|
|
"RGB*, INDEXED*, GRAY*",
|
|
GIMP_PLUGIN,
|
|
G_N_ELEMENTS (args),
|
|
G_N_ELEMENTS (return_args),
|
|
args, return_args);
|
|
|
|
gimp_plugin_menu_register (OPTIMIZE_PROC, "<Image>/Filters/Animation");
|
|
gimp_plugin_menu_register (OPTIMIZE_DIFF_PROC, "<Image>/Filters/Animation");
|
|
gimp_plugin_menu_register (UNOPTIMIZE_PROC, "<Image>/Filters/Animation");
|
|
|
|
#ifdef EXPERIMENTAL_BACKDROP_CODE
|
|
gimp_install_procedure (REMOVE_BACKDROP_PROC,
|
|
"This procedure attempts to remove the backdrop"
|
|
" from a GIMP layer-based animation, leaving"
|
|
" the foreground animation over transparency.",
|
|
"",
|
|
"Adam D. Moss <adam@gimp.org>",
|
|
"Adam D. Moss <adam@gimp.org>",
|
|
"2001",
|
|
N_("_Remove Backdrop"),
|
|
"RGB*, INDEXED*, GRAY*",
|
|
GIMP_PLUGIN,
|
|
G_N_ELEMENTS (args),
|
|
G_N_ELEMENTS (return_args),
|
|
args, return_args);
|
|
|
|
gimp_install_procedure (FIND_BACKDROP_PROC,
|
|
"This procedure attempts to remove the foreground"
|
|
" from a GIMP layer-based animation, leaving"
|
|
" a one-layered image containing only the"
|
|
" constant backdrop image.",
|
|
"",
|
|
"Adam D. Moss <adam@gimp.org>",
|
|
"Adam D. Moss <adam@gimp.org>",
|
|
"2001",
|
|
N_("_Find Backdrop"),
|
|
"RGB*, INDEXED*, GRAY*",
|
|
GIMP_PLUGIN,
|
|
G_N_ELEMENTS (args),
|
|
G_N_ELEMENTS (return_args),
|
|
args, return_args);
|
|
|
|
gimp_plugin_menu_register (REMOVE_BACKDROP_PROC, "<Image>/Filters/Animation");
|
|
gimp_plugin_menu_register (FIND_BACKDROP_PROC, "<Image>/Filters/Animation");
|
|
#endif
|
|
}
|
|
|
|
static void
|
|
run (const gchar *name,
|
|
gint n_params,
|
|
const GimpParam *param,
|
|
gint *nreturn_vals,
|
|
GimpParam **return_vals)
|
|
{
|
|
static GimpParam values[2];
|
|
GimpRunMode run_mode;
|
|
GimpPDBStatusType status = GIMP_PDB_SUCCESS;
|
|
gboolean diff_only = FALSE;
|
|
|
|
*nreturn_vals = 2;
|
|
*return_vals = values;
|
|
|
|
run_mode = param[0].data.d_int32;
|
|
|
|
INIT_I18N ();
|
|
|
|
if (run_mode == GIMP_RUN_NONINTERACTIVE && n_params != 3)
|
|
{
|
|
status = GIMP_PDB_CALLING_ERROR;
|
|
}
|
|
|
|
/* Check the procedure name we were called with, to decide
|
|
what needs to be done. */
|
|
if (strcmp (name, OPTIMIZE_PROC) == 0)
|
|
opmode = OPOPTIMIZE;
|
|
else if (strcmp (name, OPTIMIZE_DIFF_PROC) == 0)
|
|
{
|
|
opmode = OPOPTIMIZE;
|
|
diff_only = TRUE;
|
|
}
|
|
else if (strcmp (name, UNOPTIMIZE_PROC) == 0)
|
|
opmode = OPUNOPTIMIZE;
|
|
else if (strcmp (name, FIND_BACKDROP_PROC) == 0)
|
|
opmode = OPBACKGROUND;
|
|
else if (strcmp (name, REMOVE_BACKDROP_PROC) == 0)
|
|
opmode = OPFOREGROUND;
|
|
else
|
|
g_error("GAH!!!");
|
|
|
|
if (status == GIMP_PDB_SUCCESS)
|
|
{
|
|
image_id = param[1].data.d_image;
|
|
|
|
new_image_id = do_optimizations (run_mode, diff_only);
|
|
|
|
if (run_mode != GIMP_RUN_NONINTERACTIVE)
|
|
gimp_displays_flush();
|
|
}
|
|
|
|
values[0].type = GIMP_PDB_STATUS;
|
|
values[0].data.d_status = status;
|
|
|
|
values[1].type = GIMP_PDB_IMAGE;
|
|
values[1].data.d_image = new_image_id;
|
|
}
|
|
|
|
|
|
|
|
/* 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 void
|
|
compose_row (gint frame_num,
|
|
DisposeType dispose,
|
|
gint row_num,
|
|
guchar *dest,
|
|
gint dest_width,
|
|
GimpDrawable *drawable,
|
|
gboolean cleanup)
|
|
{
|
|
static guchar *line_buf = NULL;
|
|
guchar *srcptr;
|
|
GimpPixelRgn pixel_rgn;
|
|
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);
|
|
}
|
|
|
|
gimp_drawable_offsets (drawable->drawable_id,
|
|
&rawx,
|
|
&rawy);
|
|
|
|
rawheight = gimp_drawable_height (drawable->drawable_id);
|
|
|
|
/* this frame has nothing to give us for this row; return */
|
|
if (row_num >= rawheight + rawy ||
|
|
row_num < rawy)
|
|
return;
|
|
|
|
rawbpp = gimp_drawable_bpp (drawable->drawable_id);
|
|
rawwidth = gimp_drawable_width (drawable->drawable_id);
|
|
has_alpha = gimp_drawable_has_alpha (drawable->drawable_id);
|
|
|
|
if (line_buf)
|
|
{
|
|
g_free (line_buf);
|
|
line_buf = NULL;
|
|
}
|
|
line_buf = g_malloc (rawwidth * rawbpp);
|
|
|
|
/* Initialise and fetch the raw new frame row */
|
|
|
|
gimp_pixel_rgn_init (&pixel_rgn,
|
|
drawable,
|
|
0, row_num - rawy,
|
|
rawwidth, 1,
|
|
FALSE,
|
|
FALSE);
|
|
gimp_pixel_rgn_get_rect (&pixel_rgn,
|
|
line_buf,
|
|
0, row_num - rawy,
|
|
rawwidth, 1);
|
|
|
|
/* 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 gint32
|
|
do_optimizations (GimpRunMode run_mode,
|
|
gboolean diff_only)
|
|
{
|
|
GimpPixelRgn pixel_rgn;
|
|
static guchar *rawframe = NULL;
|
|
guchar *srcptr;
|
|
guchar *destptr;
|
|
gint row, this_frame_num;
|
|
guint32 frame_sizebytes;
|
|
gint32 new_layer_id;
|
|
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;
|
|
gint last_true_frame = -1;
|
|
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:
|
|
gimp_progress_init (_("Unoptimizing animation"));
|
|
break;
|
|
case OPFOREGROUND:
|
|
gimp_progress_init (_("Removing animation background"));
|
|
break;
|
|
case OPBACKGROUND:
|
|
gimp_progress_init (_("Finding animation background"));
|
|
break;
|
|
case OPOPTIMIZE:
|
|
default:
|
|
gimp_progress_init (_("Optimizing animation"));
|
|
break;
|
|
}
|
|
|
|
width = gimp_image_width (image_id);
|
|
height = gimp_image_height (image_id);
|
|
layers = gimp_image_get_layers (image_id, &total_frames);
|
|
imagetype = gimp_image_base_type (image_id);
|
|
pixelstep = (imagetype == GIMP_RGB) ? 4 : 2;
|
|
|
|
/* gimp_tile_cache_ntiles(total_frames * (width / gimp_tile_width() + 1) );*/
|
|
|
|
|
|
drawabletype_alpha = (imagetype == GIMP_RGB) ? GIMP_RGBA_IMAGE :
|
|
((imagetype == GIMP_INDEXED) ? GIMP_INDEXEDA_IMAGE : GIMP_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_id = gimp_image_new(width, height, imagetype);
|
|
gimp_image_undo_disable (new_image_id);
|
|
|
|
if (imagetype == GIMP_INDEXED)
|
|
{
|
|
palette = gimp_image_get_colormap (image_id, &ncolors);
|
|
gimp_image_set_colormap (new_image_id, 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,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++)
|
|
{
|
|
drawable =
|
|
gimp_drawable_get (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
|
|
);
|
|
|
|
gimp_drawable_detach(drawable);
|
|
}
|
|
|
|
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)
|
|
{
|
|
new_layer_id = gimp_layer_new(new_image_id,
|
|
"Backgroundx",
|
|
width, height,
|
|
drawabletype_alpha,
|
|
100.0,
|
|
GIMP_LAYER_MODE_NORMAL);
|
|
|
|
gimp_image_insert_layer (new_image_id, new_layer_id, -1, 0);
|
|
|
|
drawable = gimp_drawable_get (new_layer_id);
|
|
|
|
gimp_pixel_rgn_init (&pixel_rgn, drawable,
|
|
0, 0,
|
|
width, height,
|
|
TRUE, FALSE);
|
|
gimp_pixel_rgn_set_rect (&pixel_rgn, back_frame,
|
|
0, 0,
|
|
width, height);
|
|
gimp_drawable_flush (drawable);
|
|
gimp_drawable_detach (drawable);
|
|
}
|
|
else
|
|
{
|
|
for (this_frame_num=0; this_frame_num<total_frames; this_frame_num++)
|
|
{
|
|
/*
|
|
* BUILD THIS FRAME into our 'this_frame' buffer.
|
|
*/
|
|
|
|
drawable =
|
|
gimp_drawable_get (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 gimp_drawable_get() fails? */
|
|
if (gimp_drawable_width (drawable->drawable_id) == 0)
|
|
{
|
|
gimp_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
|
|
);
|
|
}
|
|
|
|
/* clean up */
|
|
gimp_drawable_detach(drawable);
|
|
|
|
|
|
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 =
|
|
gimp_item_get_name(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 = gimp_item_get_name (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)");
|
|
|
|
gimp_item_set_name (last_true_frame, newlayer_name);
|
|
|
|
g_free (newlayer_name);
|
|
}
|
|
else
|
|
{
|
|
cumulated_delay = this_delay;
|
|
|
|
last_true_frame =
|
|
new_layer_id = gimp_layer_new (new_image_id,
|
|
newlayer_name,
|
|
bbox_right-bbox_left,
|
|
bbox_bottom-bbox_top,
|
|
drawabletype_alpha,
|
|
100.0,
|
|
GIMP_LAYER_MODE_NORMAL);
|
|
g_free (newlayer_name);
|
|
|
|
gimp_image_insert_layer (new_image_id, new_layer_id, -1, 0);
|
|
|
|
drawable = gimp_drawable_get (new_layer_id);
|
|
|
|
gimp_pixel_rgn_init (&pixel_rgn, drawable, 0, 0,
|
|
bbox_right-bbox_left,
|
|
bbox_bottom-bbox_top,
|
|
TRUE, FALSE);
|
|
gimp_pixel_rgn_set_rect (&pixel_rgn, opti_frame, 0, 0,
|
|
bbox_right-bbox_left,
|
|
bbox_bottom-bbox_top);
|
|
gimp_drawable_flush (drawable);
|
|
gimp_drawable_detach (drawable);
|
|
gimp_layer_translate (new_layer_id, bbox_left, bbox_top);
|
|
}
|
|
|
|
gimp_progress_update (((gdouble) this_frame_num + 1.0) /
|
|
((gdouble) total_frames));
|
|
}
|
|
gimp_progress_update (1.0);
|
|
}
|
|
|
|
gimp_image_undo_enable (new_image_id);
|
|
|
|
if (run_mode != GIMP_RUN_NONINTERACTIVE)
|
|
gimp_display_new (new_image_id);
|
|
|
|
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_id;
|
|
}
|
|
|
|
/* Util. */
|
|
|
|
static DisposeType
|
|
get_frame_disposal (guint whichframe)
|
|
{
|
|
gchar *layer_name;
|
|
DisposeType disposal;
|
|
|
|
layer_name = gimp_item_get_name(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 = gimp_item_get_name(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';
|
|
}
|