Bug 773450 - Animated WEBP images should be able to set frame delay.

animated webp saving: parse time-stamp from layer_name (instead of using
default value of '100')

also:
- add a default delay field to UI, in case time-stamps are not present.
- add a 'force delay' checkbox
- revamp the whole UI to look like the GIF saving UI.
This commit is contained in:
Pascal Massimino 2016-11-04 15:36:02 +01:00 committed by Jehan
parent 87d38194d7
commit d516f9bef8
4 changed files with 275 additions and 134 deletions

View File

@ -92,27 +92,25 @@ save_dialog (WebPSaveParams *params,
gint32 image_ID,
gint32 n_layers)
{
GtkWidget *dialog;
GtkWidget *vbox;
GtkWidget *label;
GtkWidget *table;
GtkWidget *expander;
GtkWidget *frame;
GtkWidget *vbox2;
GtkWidget *save_exif;
GtkWidget *save_xmp;
GtkWidget *preset_label;
GtkListStore *preset_list;
GtkWidget *preset_combo;
GtkWidget *lossless_checkbox;
GtkWidget *animation_checkbox;
GtkWidget *loop_anim_checkbox;
GtkObject *quality_scale;
GtkObject *alpha_quality_scale;
gboolean animation_supported = FALSE;
gint slider1 , slider2;
gboolean run;
gchar *text;
GtkWidget *dialog;
GtkWidget *vbox;
GtkWidget *label;
GtkWidget *table;
GtkWidget *expander;
GtkWidget *frame;
GtkWidget *vbox2;
GtkWidget *save_exif;
GtkWidget *save_xmp;
GtkWidget *preset_label;
GtkListStore *preset_list;
GtkWidget *preset_combo;
GtkWidget *lossless_checkbox;
GtkWidget *animation_checkbox;
GtkObject *quality_scale;
GtkObject *alpha_quality_scale;
gboolean animation_supported = FALSE;
gboolean run;
gchar *text;
animation_supported = n_layers > 1;
@ -127,7 +125,11 @@ save_dialog (WebPSaveParams *params,
gtk_widget_show (vbox);
/* Create the descriptive label at the top */
label = gtk_label_new (_("Use the options below to customize the image."));
text = g_strdup_printf ("<b>%s</b>", _("WebP Options"));
label = gtk_label_new (NULL);
gtk_label_set_markup(GTK_LABEL(label), text);
g_free (text);
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, FALSE, 0);
gtk_widget_show (label);
@ -138,80 +140,9 @@ save_dialog (WebPSaveParams *params,
gtk_box_pack_start (GTK_BOX (vbox), table, FALSE, FALSE, 0);
gtk_widget_show (table);
/* Create the label for the selecting a preset */
preset_label = gtk_label_new (_("Preset:"));
gtk_label_set_xalign (GTK_LABEL (preset_label), 0.0);
gtk_table_attach (GTK_TABLE (table), preset_label,
0, 1, 0, 1,
GTK_FILL, GTK_FILL, 0, 0);
gtk_widget_show (preset_label);
/* Create the combobox containing the presets */
preset_list = save_dialog_presets ();
preset_combo = gimp_string_combo_box_new (GTK_TREE_MODEL (preset_list), 0, 1);
g_object_unref (preset_list);
gimp_string_combo_box_set_active (GIMP_STRING_COMBO_BOX (preset_combo),
params->preset);
gtk_table_attach (GTK_TABLE (table), preset_combo,
1, 3, 0, 1,
GTK_FILL, GTK_FILL, 0, 0);
gtk_widget_show (preset_combo);
g_signal_connect (preset_combo, "changed",
G_CALLBACK (save_dialog_preset_changed),
&params->preset);
/* Create the lossless checkbox */
lossless_checkbox = gtk_check_button_new_with_label (_("Lossless"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (lossless_checkbox),
params->lossless);
gtk_table_attach (GTK_TABLE (table), lossless_checkbox,
1, 3, 1, 2,
GTK_FILL, GTK_FILL, 0, 0);
gtk_widget_show (lossless_checkbox);
g_signal_connect (lossless_checkbox, "toggled",
G_CALLBACK (gimp_toggle_button_update),
&params->lossless);
slider1 = 2;
slider2 = 3;
if (animation_supported)
{
slider1 = 4;
slider2 = 5;
/* Create the animation checkbox */
animation_checkbox = gtk_check_button_new_with_label (_("Use animation"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (animation_checkbox),
params->animation);
gtk_table_attach (GTK_TABLE (table), animation_checkbox,
1, 3, 2, 3,
GTK_FILL, GTK_FILL, 0, 0);
gtk_widget_show (animation_checkbox);
g_signal_connect (animation_checkbox, "toggled",
G_CALLBACK (gimp_toggle_button_update),
&params->animation);
/* Create the loop animation checkbox */
loop_anim_checkbox = gtk_check_button_new_with_label (_("Loop infinitely"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (loop_anim_checkbox),
params->loop);
gtk_table_attach (GTK_TABLE (table), loop_anim_checkbox,
1, 3, 3, 4,
GTK_FILL, GTK_FILL, 0, 0);
gtk_widget_show (loop_anim_checkbox);
g_signal_connect (loop_anim_checkbox, "toggled",
G_CALLBACK (gimp_toggle_button_update),
&params->loop);
}
/* Create the slider for image quality */
quality_scale = gimp_scale_entry_new (GTK_TABLE (table),
0, slider1,
0, 0,
_("Image quality:"),
125,
0,
@ -230,7 +161,7 @@ save_dialog (WebPSaveParams *params,
/* Create the slider for alpha channel quality */
alpha_quality_scale = gimp_scale_entry_new (GTK_TABLE (table),
0, slider2,
0, 1,
_("Alpha quality:"),
125,
0,
@ -247,6 +178,43 @@ save_dialog (WebPSaveParams *params,
G_CALLBACK (gimp_float_adjustment_update),
&params->alpha_quality);
/* Create the label for the selecting a preset */
preset_label = gtk_label_new (_("Preset:"));
gtk_label_set_xalign (GTK_LABEL (preset_label), 0.0);
gtk_table_attach (GTK_TABLE (table), preset_label,
0, 1, 2, 3,
GTK_FILL, GTK_FILL, 0, 0);
gtk_widget_show (preset_label);
/* Create the combobox containing the presets */
preset_list = save_dialog_presets ();
preset_combo = gimp_string_combo_box_new (GTK_TREE_MODEL (preset_list), 0, 1);
g_object_unref (preset_list);
gimp_string_combo_box_set_active (GIMP_STRING_COMBO_BOX (preset_combo),
params->preset);
gtk_table_attach (GTK_TABLE (table), preset_combo,
1, 3, 2, 3,
GTK_FILL, GTK_FILL, 0, 0);
gtk_widget_show (preset_combo);
g_signal_connect (preset_combo, "changed",
G_CALLBACK (save_dialog_preset_changed),
&params->preset);
/* Create the lossless checkbox */
lossless_checkbox = gtk_check_button_new_with_label (_("Lossless"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (lossless_checkbox),
params->lossless);
gtk_table_attach (GTK_TABLE (table), lossless_checkbox,
0, 4, 3, 4,
GTK_FILL, GTK_FILL, 0, 0);
gtk_widget_show (lossless_checkbox);
g_signal_connect (lossless_checkbox, "toggled",
G_CALLBACK (gimp_toggle_button_update),
&params->lossless);
/* Enable and disable the sliders when the lossless option is selected */
g_signal_connect (lossless_checkbox, "toggled",
G_CALLBACK (save_dialog_toggle_scale),
@ -255,6 +223,113 @@ save_dialog (WebPSaveParams *params,
G_CALLBACK (save_dialog_toggle_scale),
alpha_quality_scale);
if (animation_supported)
{
GtkWidget *animation_box;
GtkWidget *animation_box2;
GtkWidget *animation_label;
GtkWidget *loop_anim_checkbox;
GtkWidget *animation_expander;
GtkWidget *animation_frame;
GtkAdjustment *adj;
GtkWidget *delay;
GtkWidget *delay_label;
GtkWidget *delay_label2;
GtkWidget *delay_hbox;
GtkWidget *delay_checkbox;
/* Create the top-level animation checkbox */
animation_checkbox = gtk_check_button_new_with_label (_("As animation"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (animation_checkbox),
params->animation);
gtk_table_attach (GTK_TABLE (table), animation_checkbox,
0, 4, 4, 5,
GTK_FILL, GTK_FILL, 0, 0);
gtk_widget_set_sensitive (animation_checkbox, TRUE);
g_signal_connect (animation_checkbox, "toggled",
G_CALLBACK (gimp_toggle_button_update),
&params->animation);
gtk_widget_show (animation_checkbox);
text = g_strdup_printf ("<b>%s</b>", _("Animated WebP Options"));
animation_expander = gtk_expander_new_with_mnemonic (text);
gtk_expander_set_use_markup (GTK_EXPANDER (animation_expander), TRUE);
g_free (text);
gtk_box_pack_start (GTK_BOX (vbox), animation_expander, TRUE, TRUE, 0);
gtk_widget_show (animation_expander);
/* animation options box */
animation_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12);
gtk_container_set_border_width (GTK_CONTAINER (animation_box), 6);
gtk_container_add (GTK_CONTAINER (animation_expander), animation_box);
gtk_widget_show (animation_box);
animation_frame = gimp_frame_new ("<expander>");
gtk_box_pack_start (GTK_BOX (animation_box), animation_frame, FALSE, FALSE, 0);
gtk_widget_show (animation_frame);
animation_box2 = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
gtk_container_add (GTK_CONTAINER (animation_frame), animation_box2);
gtk_widget_show (animation_box2);
/* loop animation checkbox */
loop_anim_checkbox = gtk_check_button_new_with_label (_("Loop forever"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (loop_anim_checkbox),
params->loop);
gtk_box_pack_start (GTK_BOX (animation_box2), loop_anim_checkbox, FALSE, FALSE, 0);
gtk_widget_show (loop_anim_checkbox);
g_signal_connect (loop_anim_checkbox, "toggled",
G_CALLBACK (gimp_toggle_button_update),
&params->loop);
/* create a hbox for delay */
delay_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12);
gtk_box_pack_start (GTK_BOX (animation_box2), delay_hbox, FALSE, FALSE, 0);
gtk_widget_show (delay_hbox);
/* label for 'delay' adjustment */
delay_label = gtk_label_new (_("Delay between frames where unspecified:"));
gtk_label_set_xalign (GTK_LABEL (delay_label), 0.0);
gtk_box_pack_start (GTK_BOX (delay_hbox), delay_label, FALSE, FALSE, 0);
gtk_widget_show (delay_label);
/* default delay */
adj = (GtkAdjustment *) gtk_adjustment_new (params->delay, 1, 10000, 1, 10, 0);
delay = gtk_spin_button_new (adj, 1, 0);
gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (delay), TRUE);
gtk_box_pack_start (GTK_BOX (delay_hbox), delay, FALSE, FALSE, 0);
gtk_widget_show (delay);
g_signal_connect (adj, "value-changed",
G_CALLBACK (gimp_int_adjustment_update),
&params->delay);
/* label for 'ms' adjustment */
delay_label2 = gtk_label_new (_("milliseconds"));
gtk_box_pack_start (GTK_BOX (delay_hbox), delay_label2, FALSE, FALSE, 0);
gtk_widget_show (delay_label2);
/* Create the force-delay checkbox */
delay_checkbox = gtk_check_button_new_with_label (_("Use delay entered above for all frames"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (delay_checkbox),
params->force_delay);
gtk_box_pack_start (GTK_BOX (animation_box2), delay_checkbox, FALSE, FALSE, 0);
gtk_widget_show (delay_checkbox);
g_signal_connect (delay_checkbox, "toggled",
G_CALLBACK (gimp_toggle_button_update),
&params->force_delay);
/* bind the animation checkbox to the parameters */
g_object_bind_property (animation_checkbox, "active",
animation_expander, "sensitive",
G_BINDING_SYNC_CREATE);
}
/* Advanced options */
text = g_strdup_printf ("<b>%s</b>", _("_Advanced Options"));
expander = gtk_expander_new_with_mnemonic (text);
gtk_expander_set_use_markup (GTK_EXPANDER (expander), TRUE);
@ -295,6 +370,7 @@ save_dialog (WebPSaveParams *params,
G_CALLBACK (gimp_toggle_button_update),
&params->xmp);
gtk_widget_show (dialog);
run = (gimp_dialog_run (GIMP_DIALOG (dialog)) == GTK_RESPONSE_OK);

View File

@ -368,6 +368,53 @@ save_layer (const gchar *filename,
return status;
}
static gint
parse_ms_tag (const gchar *str)
{
gint sum = 0;
gint offset = 0;
gint length;
length = strlen (str);
find_another_bra:
while ((offset < length) && (str[offset] != '('))
offset++;
if (offset >= length)
return(-1);
if (! g_ascii_isdigit (str[++offset]))
goto find_another_bra;
do
{
sum *= 10;
sum += str[offset] - '0';
offset++;
}
while ((offset < length) && (g_ascii_isdigit (str[offset])));
if (length - offset <= 2)
return(-3);
if ((g_ascii_toupper (str[offset]) != 'M') ||
(g_ascii_toupper (str[offset + 1]) != 'S'))
return -4;
return sum;
}
static gint
get_layer_delay(gint32 layer)
{
gchar *layer_name = gimp_item_get_name (layer);
gint delay_ms = parse_ms_tag (layer_name);
g_free (layer_name);
return delay_ms;
}
gboolean
save_animation (const gchar *filename,
gint32 nLayers,
@ -399,7 +446,9 @@ save_animation (const gchar *filename,
do
{
gint loop;
gint loop;
gint default_delay = params->delay;
gboolean force_delay = params->force_delay;
/* Begin displaying export progress */
gimp_progress_init_printf (_("Saving '%s'"),
@ -438,6 +487,7 @@ save_animation (const gchar *filename,
WebPPicture picture;
WebPMemoryWriter mw = { 0 };
gint32 drawable = allLayers[nLayers - 1 - loop];
gint delay = get_layer_delay (drawable);
/* Obtain the drawable type */
has_alpha = gimp_drawable_has_alpha (drawable);
@ -537,7 +587,7 @@ save_animation (const gchar *filename,
g_object_unref (geglbuffer);
gimp_progress_update ((loop + 1.0) / nLayers);
frame_timestamp += 100; /* TODO: should extract the real time stamp from layer */
frame_timestamp += (delay <= 0 || force_delay) ? default_delay : delay;
}
if (status == FALSE)

View File

@ -34,6 +34,8 @@ typedef struct
gboolean exif;
gboolean iptc;
gboolean xmp;
gint delay;
gboolean force_delay;
} WebPSaveParams;

View File

@ -44,7 +44,7 @@ static void run (const gchar *name,
GimpParam **return_vals);
GimpPlugInInfo PLUG_IN_INFO =
const GimpPlugInInfo PLUG_IN_INFO =
{
NULL,
NULL,
@ -85,7 +85,9 @@ query (void)
{ GIMP_PDB_INT32, "anim-loop", "Loop animation infinitely (0/1)" },
{ GIMP_PDB_INT32, "exif", "Toggle saving exif data (0/1)" },
{ GIMP_PDB_INT32, "iptc", "Toggle saving iptc data (0/1)" },
{ GIMP_PDB_INT32, "xmp", "Toggle saving xmp data (0/1)" }
{ GIMP_PDB_INT32, "xmp", "Toggle saving xmp data (0/1)" },
{ GIMP_PDB_INT32, "delay", "Delay to use when timestamp are not available or forced" },
{ GIMP_PDB_INT32, "force-delay", "Toggle to for delay" }
};
gimp_install_procedure (LOAD_PROC,
@ -172,28 +174,23 @@ run (const gchar *name,
{
WebPSaveParams params;
GimpExportReturn export = GIMP_EXPORT_CANCEL;
gint32 *layers;
gint32 *layers = NULL;
gint32 n_layers;
/* Initialize the parameters to their defaults */
params.preset = g_strdup ("default");
params.lossless = FALSE;
params.animation = FALSE;
params.loop = TRUE;
params.quality = 90.0f;
params.alpha_quality = 100.0f;
params.exif = TRUE;
params.iptc = TRUE;
params.xmp = TRUE;
if (run_mode == GIMP_RUN_INTERACTIVE ||
run_mode == GIMP_RUN_WITH_LAST_VALS)
gimp_ui_init (PLUG_IN_BINARY, FALSE);
image_ID = param[1].data.d_int32;
drawable_ID = param[2].data.d_int32;
switch (run_mode)
{
case GIMP_RUN_INTERACTIVE:
case GIMP_RUN_WITH_LAST_VALS:
gimp_ui_init (PLUG_IN_BINARY, FALSE);
case GIMP_RUN_INTERACTIVE:
/* Possibly retrieve data */
gimp_get_data (SAVE_PROC, &params);
params.preset = g_strdup ("default"); /* can't serialize strings, so restore default */
export = gimp_export_image (&image_ID, &drawable_ID, "WebP",
GIMP_EXPORT_CAN_HANDLE_RGB |
@ -205,31 +202,19 @@ run (const gchar *name,
if (export == GIMP_EXPORT_CANCEL)
{
values[0].data.d_status = GIMP_PDB_CANCEL;
return;
status = GIMP_PDB_CANCEL;
break;
}
break;
default:
break;
}
layers = gimp_image_get_layers (image_ID, &n_layers);
switch (run_mode)
{
case GIMP_RUN_INTERACTIVE:
if (! save_dialog (&params, image_ID, n_layers))
status = GIMP_PDB_CANCEL;
break;
case GIMP_RUN_NONINTERACTIVE:
if (nparams != 10)
if (nparams != 16)
{
status = GIMP_PDB_CALLING_ERROR;
}
else
{
g_free (params.preset);
params.preset = g_strdup (param[5].data.d_string);
params.lossless = param[6].data.d_int32;
params.quality = param[7].data.d_float;
@ -239,6 +224,8 @@ run (const gchar *name,
params.exif = param[11].data.d_int32;
params.iptc = param[12].data.d_int32;
params.xmp = param[13].data.d_int32;
params.delay = param[14].data.d_int32;
params.force_delay = param[15].data.d_int32;
}
break;
@ -246,24 +233,50 @@ run (const gchar *name,
break;
}
if (status == GIMP_PDB_SUCCESS)
{
if (! save_image (param[3].data.d_string,
n_layers, layers,
image_ID,
drawable_ID,
&params,
&error))
layers = gimp_image_get_layers (image_ID, &n_layers);
if (run_mode == GIMP_RUN_INTERACTIVE)
{
status = GIMP_PDB_EXECUTION_ERROR;
if (! save_dialog (&params, image_ID, n_layers))
{
status = GIMP_PDB_CANCEL;
}
}
}
if (status != GIMP_PDB_SUCCESS)
{
g_free(params.preset);
g_free (layers);
return;
}
if (! save_image (param[3].data.d_string,
n_layers, layers,
image_ID,
drawable_ID,
&params,
&error))
{
status = GIMP_PDB_EXECUTION_ERROR;
}
g_free (params.preset);
params.preset = NULL;
g_free (layers);
if (export == GIMP_EXPORT_EXPORT)
gimp_image_delete (image_ID);
if (status == GIMP_PDB_SUCCESS)
{
/* save parameters for later */
/* we can't serialize strings this way. params.preset isn't saved. */
gimp_set_data (SAVE_PROC, &params, sizeof (params));
}
}
/* If an error was supplied, include it in the return values */