From aa51b9e19ece8a8c54a513fe33b6d65abcb0fbfb Mon Sep 17 00:00:00 2001 From: Alx Sa Date: Mon, 8 Aug 2022 18:31:45 +0000 Subject: [PATCH] plug-ins: Add .ani file import/export --- plug-ins/file-ico/ico-dialog.c | 118 ++++++++- plug-ins/file-ico/ico-dialog.h | 10 +- plug-ins/file-ico/ico-load.c | 195 ++++++++++++++- plug-ins/file-ico/ico-load.h | 7 + plug-ins/file-ico/ico-save.c | 424 ++++++++++++++++++++++++++++----- plug-ins/file-ico/ico-save.h | 11 + plug-ins/file-ico/ico.c | 303 ++++++++++++++++++++++- plug-ins/file-ico/ico.h | 16 ++ 8 files changed, 1001 insertions(+), 83 deletions(-) diff --git a/plug-ins/file-ico/ico-dialog.c b/plug-ins/file-ico/ico-dialog.c index fe48e736e4..2cb893561f 100644 --- a/plug-ins/file-ico/ico-dialog.c +++ b/plug-ins/file-ico/ico-dialog.c @@ -38,10 +38,16 @@ static void ico_dialog_toggle_compress (GtkWidget *checkbox, GObject *hbox); static void ico_dialog_check_compat (GtkWidget *dialog, IcoSaveInfo *info); +static void ico_dialog_ani_update_inam (GtkEntry *entry, + gpointer data); +static void ico_dialog_ani_update_iart (GtkEntry *entry, + gpointer data); GtkWidget * -ico_dialog_new (IcoSaveInfo *info) +ico_dialog_new (IcoSaveInfo *info, + AniFileHeader *ani_header, + AniSaveInfo *ani_info) { GtkWidget *dialog; GtkWidget *main_vbox; @@ -51,7 +57,8 @@ ico_dialog_new (IcoSaveInfo *info) GtkWidget *viewport; GtkWidget *warning; - dialog = gimp_export_dialog_new (info->is_cursor ? + dialog = gimp_export_dialog_new (ani_header ? + _("Windows Animated Cursor") : info->is_cursor ? _("Windows Cursor") : _("Windows Icon"), PLUG_IN_BINARY, "plug-in-winicon"); @@ -65,6 +72,11 @@ ico_dialog_new (IcoSaveInfo *info) */ g_object_set_data (G_OBJECT (dialog), "save_info", info); + if (ani_header) + { + g_object_set_data (G_OBJECT (dialog), "save_ani_header", ani_header); + g_object_set_data (G_OBJECT (dialog), "save_ani_info", ani_info); + } main_vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); gtk_container_set_border_width (GTK_CONTAINER (main_vbox), 6); @@ -72,6 +84,82 @@ ico_dialog_new (IcoSaveInfo *info) main_vbox, TRUE, TRUE, 0); gtk_widget_show (main_vbox); + /*Animated Cursor */ + if (ani_header) + { + GtkWidget *grid; + GtkAdjustment *adjustment; + GtkWidget *spin; + GtkWidget *label; + GtkWidget *hbox; + GtkWidget *entry; + + frame = gimp_frame_new (_("Animated Cursor Settings")); + gtk_box_pack_start (GTK_BOX (main_vbox), frame, FALSE, FALSE, 0); + gtk_widget_show (frame); + + grid = gtk_grid_new (); + gtk_grid_set_column_spacing (GTK_GRID (grid), 6); + gtk_grid_set_row_spacing (GTK_GRID (grid), 6); + gtk_container_add (GTK_CONTAINER (frame), grid); + gtk_widget_show (grid); + + /* Cursor Name */ + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gimp_grid_attach_aligned (GTK_GRID (grid), 0, 1, + _("_Cursor Name (Optional)"), + 0.0, 0.5, + hbox, 1); + + entry = gtk_entry_new (); + gtk_entry_set_text (GTK_ENTRY (entry), + ani_info->inam ? ani_info->inam : ""); + gtk_box_pack_start (GTK_BOX (hbox), entry, FALSE, FALSE, 0); + gtk_widget_show (entry); + + g_signal_connect (GTK_ENTRY (entry), "focus-out-event", + G_CALLBACK (ico_dialog_ani_update_inam), + NULL); + + /* Author Name */ + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gimp_grid_attach_aligned (GTK_GRID (grid), 0, 3, + _("_Author Name (Optional)"), + 0.0, 0.5, + hbox, 1); + + entry = gtk_entry_new (); + gtk_entry_set_text (GTK_ENTRY (entry), + ani_info->iart ? ani_info->iart : ""); + gtk_box_pack_start (GTK_BOX (hbox), entry, FALSE, FALSE, 0); + gtk_widget_show (entry); + + g_signal_connect (GTK_ENTRY (entry), "focus-out-event", + G_CALLBACK (ico_dialog_ani_update_iart), + NULL); + + /* Default delay spin */ + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gimp_grid_attach_aligned (GTK_GRID (grid), 0, 5, + _("_Delay between frames:"), + 0.0, 0.5, + hbox, 1); + + adjustment = gtk_adjustment_new (ani_header->jif_rate, 1, G_MAXINT, + 1, 10, 0); + spin = gimp_spin_button_new (adjustment, 1, 0); + gtk_box_pack_start (GTK_BOX (hbox), spin, FALSE, FALSE, 0); + gtk_widget_show (spin); + + label = gtk_label_new (_(" jiffies (16.66 ms)")); + gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0); + gtk_widget_show (label); + + g_signal_connect (adjustment, "value-changed", + G_CALLBACK (gimp_int_adjustment_update), + &ani_header->jif_rate); + } + /* Cursor */ frame = gimp_frame_new (_("Icon Details")); gtk_box_pack_start (GTK_BOX (main_vbox), frame, TRUE, TRUE, 4); @@ -572,3 +660,29 @@ ico_dialog_check_compat (GtkWidget *dialog, gtk_widget_set_visible (warning, warn); } + +static void +ico_dialog_ani_update_inam (GtkEntry *entry, + gpointer data) +{ + AniSaveInfo *ani_info; + GtkWidget *dialog; + + dialog = gtk_widget_get_toplevel (GTK_WIDGET (entry)); + + ani_info = g_object_get_data (G_OBJECT (dialog), "save_ani_info"); + ani_info->inam = g_strdup_printf ("%s", gtk_entry_get_text (entry)); +} + +static void +ico_dialog_ani_update_iart (GtkEntry *entry, + gpointer data) +{ + AniSaveInfo *ani_info; + GtkWidget *dialog; + + dialog = gtk_widget_get_toplevel (GTK_WIDGET (entry)); + + ani_info = g_object_get_data (G_OBJECT (dialog), "save_ani_info"); + ani_info->iart = g_strdup_printf ("%s", gtk_entry_get_text (entry)); +} diff --git a/plug-ins/file-ico/ico-dialog.h b/plug-ins/file-ico/ico-dialog.h index f39315d80c..ff071af5fb 100644 --- a/plug-ins/file-ico/ico-dialog.h +++ b/plug-ins/file-ico/ico-dialog.h @@ -22,9 +22,11 @@ #define __ICO_DIALOG_H__ -GtkWidget * ico_dialog_new (IcoSaveInfo *info); -void ico_dialog_add_icon (GtkWidget *dialog, - GimpDrawable *layer, - gint layer_num); +GtkWidget * ico_dialog_new (IcoSaveInfo *info, + AniFileHeader *ani_header, + AniSaveInfo *ani_info); +void ico_dialog_add_icon (GtkWidget *dialog, + GimpDrawable *layer, + gint layer_num); #endif /* __ICO_DIALOG_H__ */ diff --git a/plug-ins/file-ico/ico-load.c b/plug-ins/file-ico/ico-load.c index 36c3b402bb..b29dab85a8 100644 --- a/plug-ins/file-ico/ico-load.c +++ b/plug-ins/file-ico/ico-load.c @@ -142,6 +142,7 @@ ico_read_init (FILE *fp) static gboolean ico_read_size (FILE *fp, + gint32 file_offset, IcoLoadInfo *info) { png_structp png_ptr; @@ -151,7 +152,7 @@ ico_read_size (FILE *fp, gint32 color_type; guint32 magic; - if (fseek (fp, info->offset, SEEK_SET) < 0) + if (fseek (fp, info->offset + file_offset, SEEK_SET) < 0) return FALSE; ico_read_int32 (fp, &magic, 1); @@ -208,6 +209,7 @@ ico_read_size (FILE *fp, static IcoLoadInfo* ico_read_info (FILE *fp, gint icon_count, + gint32 file_offset, GError **error) { gint i; @@ -237,7 +239,7 @@ ico_read_info (FILE *fp, if (info[i].width == 0 || info[i].height == 0) { - ico_read_size (fp, info + i); + ico_read_size (fp, file_offset, info + i); } D(("ico_read_info: %ix%i (%i bits, size: %i, offset: %i)\n", @@ -605,6 +607,7 @@ ico_load_layer (FILE *fp, gint32 icon_num, guchar *buf, gint maxsize, + gint32 file_offset, IcoLoadInfo *info) { gint width, height; @@ -613,7 +616,7 @@ ico_load_layer (FILE *fp, GeglBuffer *buffer; gchar name[ICO_MAXBUF]; - if (fseek (fp, info->offset, SEEK_SET) < 0 || + if (fseek (fp, info->offset + file_offset, SEEK_SET) < 0 || ! ico_read_int32 (fp, &first_bytes, 1)) return NULL; @@ -653,6 +656,7 @@ ico_load_layer (FILE *fp, GimpImage * ico_load_image (GFile *file, + gint32 *file_offset, GError **error) { FILE *fp; @@ -666,8 +670,9 @@ ico_load_image (GFile *file, gint maxsize; gchar *str; - gimp_progress_init_printf (_("Opening '%s'"), - gimp_file_get_utf8_name (file)); + if (! file_offset) + gimp_progress_init_printf (_("Opening '%s'"), + gimp_file_get_utf8_name (file)); fp = g_fopen (g_file_peek_path (file), "rb"); @@ -679,6 +684,9 @@ ico_load_image (GFile *file, return NULL; } + if (file_offset) + fseek (fp, *file_offset, SEEK_SET); + header = ico_read_init (fp); icon_count = header.icon_count; if (!icon_count) @@ -687,7 +695,7 @@ ico_load_image (GFile *file, return NULL; } - info = ico_read_info (fp, icon_count, error); + info = ico_read_info (fp, icon_count, file_offset ? *file_offset : 0, error); if (! info) { fclose (fp); @@ -713,7 +721,8 @@ ico_load_image (GFile *file, D(("image size: %ix%i\n", max_width, max_height)); image = gimp_image_new (max_width, max_height, GIMP_RGB); - gimp_image_set_file (image, file); + if (! file_offset) + gimp_image_set_file (image, file); maxsize = max_width * max_height * 4; buf = g_new (guchar, max_width * max_height * 4); @@ -721,7 +730,7 @@ ico_load_image (GFile *file, { GimpLayer *layer; - layer = ico_load_layer (fp, image, i, buf, maxsize, info+i); + layer = ico_load_layer (fp, image, i, buf, maxsize, file_offset ? *file_offset : 0, info+i); /* Save CUR hot spot information */ if (header.resource_type == 2) @@ -737,10 +746,172 @@ ico_load_image (GFile *file, gimp_parasite_free (parasite); } } + + if (file_offset) + *file_offset = ftell (fp); + g_free (buf); g_free (info); fclose (fp); + /* Don't update progress here if .ani file */ + if (! file_offset) + gimp_progress_update (1.0); + + return image; +} + +/* Ported from James Huang's ani.c code, under the GPL license, version 3 + * or any later version of the license */ +GimpImage * +ani_load_image (GFile *file, + gboolean load_thumb, + gint *width, + gint *height, + GError **error) +{ + FILE *fp; + GimpImage *image = NULL; + GimpParasite *parasite; + gchar id[4]; + guint32 size; + gint32 file_offset; + gint frame = 1; + AniFileHeader header; + gchar inam[G_MAXSHORT] = {0}; + gchar iart[G_MAXSHORT] = {0}; + gchar *str; + + gimp_progress_init_printf (_("Opening '%s'"), + gimp_file_get_utf8_name (file)); + + fp = g_fopen (g_file_peek_path (file), "rb"); + + if (! fp) + { + g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), + _("Could not open '%s' for reading: %s"), + gimp_file_get_utf8_name (file), g_strerror (errno)); + return NULL; + } + + while (fread (id, 1, 4, fp) == 4) + { + if (memcmp (id, "RIFF", 4) == 0 ) + { + fread (&size, sizeof (size), 1, fp); + } + else if (memcmp (id, "anih", 4) == 0) + { + fread (&size, sizeof (size), 1, fp); + fread (&header, sizeof (header), 1, fp); + } + else if (memcmp (id, "rate", 4) == 0) + { + fread (&size, sizeof (size), 1, fp); + fseek (fp, size, SEEK_CUR); + } + else if (memcmp (id, "seq ", 4) == 0) + { + fread (&size, sizeof (size), 1, fp); + fseek (fp, size, SEEK_CUR); + } + else if (memcmp (id, "LIST", 4) == 0) + { + fread (&size, sizeof (size), 1, fp); + } + else if (memcmp (id, "INAM", 4) == 0) + { + fread (&size, sizeof (size), 1, fp); + fread (&inam, sizeof (char), size, fp); + inam[size] = '\0'; + } + else if (memcmp (id, "IART", 4) == 0) + { + fread (&size, sizeof (size), 1, fp); + fread (&iart, sizeof (char), size, fp); + iart[size] = '\0'; + } + else if (memcmp (id, "icon", 4) == 0) + { + fread (&size, sizeof (size), 1, fp); + file_offset = ftell (fp); + if (load_thumb) + { + image = ico_load_thumbnail_image (file, width, height, file_offset, error); + break; + } + else + { + if (! image) + { + image = ico_load_image (file, &file_offset, error); + } + else + { + GimpImage *temp_image = NULL; + GimpLayer **layers; + GimpLayer *new_layer; + gint nlayers; + + temp_image = ico_load_image (file, &file_offset, error); + layers = gimp_image_get_layers (temp_image, &nlayers); + if (layers) + { + for (gint i = 0; i < nlayers; i++) + { + new_layer = gimp_layer_new_from_drawable (GIMP_DRAWABLE (layers[i]), + image); + gimp_image_insert_layer (image, new_layer, NULL, frame); + frame++; + } + } + gimp_image_delete (temp_image); + } + + /* Update position after reading icon data */ + fseek (fp, file_offset, SEEK_SET); + if (header.frames > 0) + gimp_progress_update ((gdouble) frame / + (gdouble) header.frames); + } + } + } + fclose (fp); + + /* Saving header metadata */ + str = g_strdup_printf ("%d", header.jif_rate); + parasite = gimp_parasite_new ("ani-header", + GIMP_PARASITE_PERSISTENT, + strlen (str) + 1, (gpointer) str); + g_free (str); + gimp_image_attach_parasite (image, parasite); + gimp_parasite_free (parasite); + + /* Saving INFO block */ + if (strlen (inam) > 0) + { + str = g_strdup_printf ("%s", inam); + parasite = gimp_parasite_new ("ani-info-inam", + GIMP_PARASITE_PERSISTENT, + strlen (str) + 1, (gpointer) str); + g_free (str); + gimp_image_attach_parasite (image, parasite); + gimp_parasite_free (parasite); + } + + if (strlen (iart) > 0) + { + str = g_strdup_printf ("%s", iart); + parasite = gimp_parasite_new ("ani-info-iart", + GIMP_PARASITE_PERSISTENT, + strlen (str) + 1, (gpointer) str); + g_free (str); + gimp_image_attach_parasite (image, parasite); + gimp_parasite_free (parasite); + } + + gimp_image_set_file (image, file); gimp_progress_update (1.0); return image; @@ -750,6 +921,7 @@ GimpImage * ico_load_thumbnail_image (GFile *file, gint *width, gint *height, + gint32 file_offset, GError **error) { FILE *fp; @@ -776,6 +948,9 @@ ico_load_thumbnail_image (GFile *file, return NULL; } + if (file_offset > 0) + fseek (fp, file_offset, SEEK_SET); + header = ico_read_init (fp); icon_count = header.icon_count; if (! icon_count) @@ -787,7 +962,7 @@ ico_load_thumbnail_image (GFile *file, D(("*** %s: Microsoft icon file, containing %i icon(s)\n", gimp_file_get_utf8_name (file), icon_count)); - info = ico_read_info (fp, icon_count, error); + info = ico_read_info (fp, icon_count, file_offset, error); if (! info) { fclose (fp); @@ -821,7 +996,7 @@ ico_load_thumbnail_image (GFile *file, image = gimp_image_new (w, h, GIMP_RGB); buf = g_new (guchar, w*h*4); - ico_load_layer (fp, image, match, buf, w*h*4, info+match); + ico_load_layer (fp, image, match, buf, w*h*4, file_offset, info+match); g_free (buf); *width = w; diff --git a/plug-ins/file-ico/ico-load.h b/plug-ins/file-ico/ico-load.h index 5c97cfdf67..88f9f68d61 100644 --- a/plug-ins/file-ico/ico-load.h +++ b/plug-ins/file-ico/ico-load.h @@ -23,10 +23,17 @@ GimpImage * ico_load_image (GFile *file, + gint32 *file_offset, + GError **error); +GimpImage * ani_load_image (GFile *file, + gboolean load_thumb, + gint *width, + gint *height, GError **error); GimpImage * ico_load_thumbnail_image (GFile *file, gint *width, gint *height, + gint32 file_offset, GError **error); gint ico_get_bit_from_data (const guint8 *data, diff --git a/plug-ins/file-ico/ico-save.c b/plug-ins/file-ico/ico-save.c index 4993166732..915c61b373 100644 --- a/plug-ins/file-ico/ico-save.c +++ b/plug-ins/file-ico/ico-save.c @@ -82,12 +82,15 @@ static gboolean ico_save_init (GimpImage *image, GError **error); static GimpPDBStatusType shared_save_image (GFile *file, + FILE *fp_ani, GimpImage *image, gint32 run_mode, gint *n_hot_spot_x, gint32 **hot_spot_x, gint *n_hot_spot_y, gint32 **hot_spot_y, + gint32 file_offset, + gint icon_index, GError **error, IcoSaveInfo *info); @@ -302,8 +305,10 @@ ico_save_init (GimpImage *image, static gboolean -ico_save_dialog (GimpImage *image, - IcoSaveInfo *info) +ico_save_dialog (GimpImage *image, + IcoSaveInfo *info, + AniFileHeader *ani_header, + AniSaveInfo *ani_info) { GtkWidget *dialog; GList *iter; @@ -312,7 +317,7 @@ ico_save_dialog (GimpImage *image, gimp_ui_init (PLUG_IN_BINARY); - dialog = ico_dialog_new (info); + dialog = ico_dialog_new (info, ani_header, ani_info); for (iter = info->layers, i = 0; iter; iter = g_list_next (iter), i++) @@ -1174,9 +1179,9 @@ ico_save_image (GFile *file, info.is_cursor = FALSE; - return shared_save_image (file, image, run_mode, + return shared_save_image (file, NULL, image, run_mode, 0, NULL, 0, NULL, - error, &info); + 0, 0, error, &info); } GimpPDBStatusType @@ -1196,49 +1201,122 @@ cur_save_image (GFile *file, info.is_cursor = TRUE; - return shared_save_image (file, image, run_mode, + return shared_save_image (file, NULL, image, run_mode, n_hot_spot_x, hot_spot_x, n_hot_spot_y, hot_spot_y, - error, &info); + 0, 0, error, &info); } +/* Ported from James Huang's ani.c code, under the GPL v3 license */ GimpPDBStatusType -shared_save_image (GFile *file, - GimpImage *image, - gint32 run_mode, - gint *n_hot_spot_x, - gint32 **hot_spot_x, - gint *n_hot_spot_y, - gint32 **hot_spot_y, - GError **error, - IcoSaveInfo *info) +ani_save_image (GFile *file, + GimpImage *image, + gint32 run_mode, + gint *n_hot_spot_x, + gint32 **hot_spot_x, + gint *n_hot_spot_y, + gint32 **hot_spot_y, + AniFileHeader *header, + AniSaveInfo *ani_info, + GError **error) { - FILE *fp; - GList *iter; - gint width; - gint height; - IcoFileHeader header; - IcoFileEntry *entries; - gboolean saved; - gint i; - GimpParasite *parasite = NULL; - gchar *str; + FILE *fp; + gint32 i; + gchar *str; + GimpParasite *parasite = NULL; + gchar id[5]; + guint32 size; + gint32 offset, ofs_size_riff, ofs_size_list, ofs_size_icon; + gint32 ofs_size_info = 0; + IcoSaveInfo info; - if (! ico_save_init (image, run_mode, info, - n_hot_spot_x ? *n_hot_spot_x : 0, - hot_spot_x ? *hot_spot_x : NULL, - n_hot_spot_y ? *n_hot_spot_y : 0, - hot_spot_y ? *hot_spot_y : NULL, - error)) + if (! ico_save_init (image, run_mode, &info, + *n_hot_spot_x, *hot_spot_x, + *n_hot_spot_y, *hot_spot_y, + error)) { return GIMP_PDB_EXECUTION_ERROR; } + /* Save individual frames as .cur so we can retain + * the hotspot information + */ + info.is_cursor = TRUE; + + /* Default header values */ + header->bSizeOf = sizeof (*header); + header->frames = info.num_icons; + header->steps = info.num_icons; + header->x = 0; + header->y = 0; + if (info.depths[0] == 24) + { + header->bpp = 4; + header->planes = 1; + } + else + { + header->bpp = 0; + header->planes = 0; + } + header->flags = 1; + + /* Load metadata from parasite */ + parasite = gimp_image_get_parasite (image, "ani-header"); + if (parasite) + { + gchar *parasite_data; + guint32 parasite_size; + gint jif_rate; + + parasite_data = (gchar *) gimp_parasite_get_data (parasite, ¶site_size); + parasite_data = g_strndup (parasite_data, parasite_size); + + if (sscanf (parasite_data, "%i", &jif_rate) == 1) + { + header->jif_rate = jif_rate; + } + + gimp_parasite_free (parasite); + g_free (parasite_data); + } + + parasite = gimp_image_get_parasite (image, "ani-info-inam"); + if (parasite) + { + guint32 parasite_size; + gchar *inam = NULL; + + inam = (gchar *) gimp_parasite_get_data (parasite, ¶site_size); + ani_info->inam = g_strndup (inam, parasite_size); + + gimp_parasite_free (parasite); + } + + parasite = gimp_image_get_parasite (image, "ani-info-iart"); + if (parasite) + { + guint32 parasite_size; + gchar *iart = NULL; + + iart = (gchar *) gimp_parasite_get_data (parasite, ¶site_size); + ani_info->iart = g_strndup (iart, parasite_size); + + gimp_parasite_free (parasite); + } + if (run_mode == GIMP_RUN_INTERACTIVE) { - /* Allow user to override default values */ - if ( !ico_save_dialog (image, info)) + if (! ico_save_dialog (image, &info, + header, ani_info)) return GIMP_PDB_CANCEL; + + for (i = 1; i < info.num_icons; i++) + { + info.depths[i] = info.depths[0]; + info.default_depths[i] = info.default_depths[0]; + info.compress[i] = info.compress[0]; + } } gimp_progress_init_printf (_("Exporting '%s'"), @@ -1246,6 +1324,212 @@ shared_save_image (GFile *file, fp = g_fopen (g_file_peek_path (file), "wb"); + if (! fp) + { + g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), + _("Could not open '%s' for writing: %s"), + gimp_file_get_utf8_name (file), g_strerror (errno)); + return GIMP_PDB_EXECUTION_ERROR; + } + + /* Writing the .ani header data */ + strcpy (id, "RIFF"); + size = 0; + fwrite (id, 4, 1, fp); + ofs_size_riff = ftell (fp); + fwrite (&size, sizeof (size), 1, fp); + + strcpy (id, "ACON"); + fwrite (id, 4, 1, fp); + + if ((ani_info->inam && strlen (ani_info->inam) > 0) || + (ani_info->iart && strlen (ani_info->iart) > 0)) + { + gint32 string_size; + + strcpy (id, "LIST"); + fwrite (id, 4, 1, fp); + ofs_size_info = ftell (fp); + fwrite (&size, sizeof (size), 1, fp); + + strcpy (id, "INFO"); + fwrite (id, 4, 1, fp); + if (ani_info->inam && strlen (ani_info->inam) > 0) /* Cursor name */ + { + strcpy (id, "INAM"); + fwrite (id, 4, 1, fp); + string_size = strlen (ani_info->inam) + 1; + fwrite (&string_size, 4, 1, fp); + fwrite (ani_info->inam, string_size, 1, fp); + } + if (ani_info->iart && strlen (ani_info->iart) > 0) /* Author name */ + { + strcpy (id, "IART"); + fwrite (id, 4, 1, fp); + string_size = strlen (ani_info->iart) + 1; + fwrite (&string_size, 4, 1, fp); + fwrite (ani_info->iart, string_size, 1, fp); + } + + /* Go back and update info list size */ + fseek (fp, 0L, SEEK_END); + size = ftell (fp) - ofs_size_info - 4; + fseek (fp, ofs_size_info, SEEK_SET); + fwrite (&size, sizeof (size), 1, fp); + fseek (fp, 0L, SEEK_END); + } + + strcpy (id, "anih"); + size = sizeof (*header); + fwrite (id, 4, 1, fp); + fwrite (&size, sizeof (size), 1, fp); + fwrite (header, sizeof (*header), 1, fp); + + strcpy (id, "LIST"); + fwrite (id, 4, 1, fp); + ofs_size_list = ftell (fp); + fwrite (&size, sizeof (size), 1, fp); + + strcpy (id, "fram"); + fwrite (id, 4, 1, fp); + + strcpy (id, "icon"); + for (i = 0; i < info.num_icons; i++ ) + { + GimpPDBStatusType status; + fwrite (id, 4, 1, fp); + ofs_size_icon = ftell (fp); + fwrite (&size, sizeof (size), 1, fp); + offset = ftell (fp); + status = shared_save_image (file, fp, image, run_mode, + n_hot_spot_x, hot_spot_x, + n_hot_spot_y, hot_spot_y, + offset, i, error, &info); + + if (status != GIMP_PDB_SUCCESS) + { + ico_save_info_free (&info); + g_free (ani_info->inam); + g_free (ani_info->iart); + fclose (fp); + return GIMP_PDB_EXECUTION_ERROR; + } + fseek (fp, 0L, SEEK_END); + size = ftell (fp) - offset; + fseek (fp, ofs_size_icon, SEEK_SET); + fwrite (&size, sizeof (size), 1, fp); + fseek (fp, 0L, SEEK_END); + + gimp_progress_update ((gdouble) i / (gdouble) info.num_icons); + } + ico_save_info_free (&info); + + fseek (fp, 0L, SEEK_END); + size = ftell (fp); + fseek (fp, ofs_size_riff, SEEK_SET); + fwrite (&size, sizeof (size), 1, fp); + + size -= ofs_size_list; + fseek (fp, ofs_size_list, SEEK_SET); + fwrite (&size, sizeof (size), 1, fp); + fclose (fp); + + /* Update metadata if needed */ + str = g_strdup_printf ("%d", header->jif_rate); + parasite = gimp_parasite_new ("ani-header", + GIMP_PARASITE_PERSISTENT, + strlen (str) + 1, (gpointer) str); + g_free (str); + gimp_image_attach_parasite (image, parasite); + gimp_parasite_free (parasite); + + if (ani_info->inam && strlen (ani_info->inam) > 0) + { + str = g_strdup_printf ("%s", ani_info->inam); + parasite = gimp_parasite_new ("ani-info-inam", + GIMP_PARASITE_PERSISTENT, + strlen (ani_info->inam) + 1, (gpointer) str); + g_free (str); + gimp_image_attach_parasite (image, parasite); + gimp_parasite_free (parasite); + } + if (ani_info->iart && strlen (ani_info->iart) > 0) + { + str = g_strdup_printf ("%s", ani_info->iart); + parasite = gimp_parasite_new ("ani-info-iart", + GIMP_PARASITE_PERSISTENT, + strlen (ani_info->iart) + 1, (gpointer) str); + g_free (str); + gimp_image_attach_parasite (image, parasite); + gimp_parasite_free (parasite); + } + + gimp_progress_update (1.0); + + return GIMP_PDB_SUCCESS; +} + +GimpPDBStatusType +shared_save_image (GFile *file, + FILE *fp_ani, + GimpImage *image, + gint32 run_mode, + gint *n_hot_spot_x, + gint32 **hot_spot_x, + gint *n_hot_spot_y, + gint32 **hot_spot_y, + gint32 file_offset, + gint icon_index, + GError **error, + IcoSaveInfo *info) +{ + FILE *fp; + GList *iter; + gint width; + gint height; + IcoFileHeader header; + IcoFileEntry *entries; + gboolean saved; + gint i; + gint num_icons; + GimpParasite *parasite = NULL; + gchar *str; + + if (! fp_ani && + ! ico_save_init (image, run_mode, info, + n_hot_spot_x ? *n_hot_spot_x : 0, + hot_spot_x ? *hot_spot_x : NULL, + n_hot_spot_y ? *n_hot_spot_y : 0, + hot_spot_y ? *hot_spot_y : NULL, + error)) + { + return GIMP_PDB_EXECUTION_ERROR; + } + + if (run_mode == GIMP_RUN_INTERACTIVE && ! fp_ani) + { + /* Allow user to override default values */ + if ( !ico_save_dialog (image, info, + NULL, NULL)) + return GIMP_PDB_CANCEL; + } + + num_icons = (fp_ani) ? 1 : info->num_icons; + + if (! fp_ani) + gimp_progress_init_printf (_("Exporting '%s'"), + gimp_file_get_utf8_name (file)); + + /* If saving an .ani file, we append the next icon frame. */ + if (! fp_ani) + { + fp = g_fopen (g_file_peek_path (file), "wb"); + } + else + { + fp = fp_ani; + } + if (! fp) { g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), @@ -1258,7 +1542,7 @@ shared_save_image (GFile *file, header.resource_type = 1; if (info->is_cursor) header.resource_type = 2; - header.icon_count = info->num_icons; + header.icon_count = num_icons; if (! ico_write_int16 (fp, &header.reserved, 1) || ! ico_write_int16 (fp, &header.resource_type, 1) || ! ico_write_int16 (fp, &header.icon_count, 1)) @@ -1268,8 +1552,8 @@ shared_save_image (GFile *file, return GIMP_PDB_EXECUTION_ERROR; } - entries = g_new0 (IcoFileEntry, info->num_icons); - if (fwrite (entries, sizeof (IcoFileEntry), info->num_icons, fp) <= 0) + entries = g_new0 (IcoFileEntry, num_icons); + if (fwrite (entries, sizeof (IcoFileEntry), num_icons, fp) <= 0) { ico_save_info_free (info); g_free (entries); @@ -1281,7 +1565,12 @@ shared_save_image (GFile *file, iter; iter = g_list_next (iter), i++) { - gimp_progress_update ((gdouble)i / (gdouble)info->num_icons); + if (! fp_ani) + gimp_progress_update ((gdouble)i / (gdouble)info->num_icons); + + /* If saving .ani file, jump to the correct frame */ + if (fp_ani) + iter = g_list_nth (info->layers, icon_index); width = gimp_drawable_get_width (iter->data); height = gimp_drawable_get_height (iter->data); @@ -1305,10 +1594,12 @@ shared_save_image (GFile *file, /* .cur file reuses these fields for cursor offsets */ if (info->is_cursor) { - entries[i].planes = info->hot_spot_x[i]; - entries[i].bpp = info->hot_spot_y[i]; + gint hot_spot_index = icon_index ? icon_index : i; + + entries[i].planes = info->hot_spot_x[hot_spot_index]; + entries[i].bpp = info->hot_spot_y[hot_spot_index]; } - entries[i].offset = ftell (fp); + entries[i].offset = ftell (fp) - file_offset; if (info->compress[i]) saved = ico_write_png (fp, iter->data, info->depths[i]); @@ -1322,10 +1613,13 @@ shared_save_image (GFile *file, return GIMP_PDB_EXECUTION_ERROR; } - entries[i].size = ftell (fp) - entries[i].offset; + entries[i].size = ftell (fp) - file_offset - entries[i].offset; + + if (fp_ani) + break; } - for (i = 0; i < info->num_icons; i++) + for (i = 0; i < num_icons; i++) { entries[i].planes = GUINT16_TO_LE (entries[i].planes); entries[i].bpp = GUINT16_TO_LE (entries[i].bpp); @@ -1333,15 +1627,16 @@ shared_save_image (GFile *file, entries[i].offset = GUINT32_TO_LE (entries[i].offset); } - if (fseek (fp, sizeof(IcoFileHeader), SEEK_SET) < 0 - || fwrite (entries, sizeof (IcoFileEntry), info->num_icons, fp) <= 0) + if (fseek (fp, sizeof (IcoFileHeader) + file_offset, SEEK_SET) < 0 || + fwrite (entries, sizeof (IcoFileEntry), num_icons, fp) <= 0) { ico_save_info_free (info); fclose (fp); return GIMP_PDB_EXECUTION_ERROR; } - gimp_progress_update (1.0); + if (! fp_ani) + gimp_progress_update (1.0); /* Updating parasite hot spots if needed */ if (info->is_cursor) @@ -1360,23 +1655,32 @@ shared_save_image (GFile *file, } } - if (hot_spot_x) + if (! fp_ani) { - *hot_spot_x = info->hot_spot_x; - info->hot_spot_x = NULL; + if (hot_spot_x) + { + *hot_spot_x = info->hot_spot_x; + info->hot_spot_x = NULL; + } + if (hot_spot_y) + { + *hot_spot_y = info->hot_spot_y; + info->hot_spot_y = NULL; + } + if (n_hot_spot_x) + *n_hot_spot_x = num_icons; + if (n_hot_spot_y) + *n_hot_spot_y = num_icons; } - if (hot_spot_y) - { - *hot_spot_y = info->hot_spot_y; - info->hot_spot_y = NULL; - } - if (n_hot_spot_x) - *n_hot_spot_x = info->num_icons; - if (n_hot_spot_y) - *n_hot_spot_y = info->num_icons; - ico_save_info_free (info); - fclose (fp); + /* If saving .ani file, don't clear until + * all icons are saved in ani_save_image () + */ + if (! file_offset) + { + ico_save_info_free (info); + fclose (fp); + } g_free (entries); return GIMP_PDB_SUCCESS; diff --git a/plug-ins/file-ico/ico-save.h b/plug-ins/file-ico/ico-save.h index 1e1ed82dc9..ff9b3d7a30 100644 --- a/plug-ins/file-ico/ico-save.h +++ b/plug-ins/file-ico/ico-save.h @@ -36,6 +36,17 @@ GimpPDBStatusType cur_save_image (GFile *file, gint32 **hot_spot_y, GError **error); +GimpPDBStatusType ani_save_image (GFile *file, + GimpImage *image, + gint32 run_mode, + gint *n_hot_spot_x, + gint32 **hot_spot_x, + gint *n_hot_spot_y, + gint32 **hot_spot_y, + AniFileHeader *header, + AniSaveInfo *ani_info, + GError **error); + gboolean ico_cmap_contains_black (const guchar *cmap, gint num_colors); diff --git a/plug-ins/file-ico/ico.c b/plug-ins/file-ico/ico.c index 1ac292760d..0b4efda25d 100644 --- a/plug-ins/file-ico/ico.c +++ b/plug-ins/file-ico/ico.c @@ -20,8 +20,11 @@ #include "config.h" +#include #include +#include + #include #include @@ -33,11 +36,14 @@ #include "libgimp/stdplugins-intl.h" -#define LOAD_PROC "file-ico-load" -#define LOAD_CUR_PROC "file-cur-load" -#define LOAD_THUMB_PROC "file-ico-load-thumb" -#define SAVE_PROC "file-ico-save" -#define SAVE_CUR_PROC "file-cur-save" +#define LOAD_PROC "file-ico-load" +#define LOAD_CUR_PROC "file-cur-load" +#define LOAD_ANI_PROC "file-ani-load" +#define LOAD_THUMB_PROC "file-ico-load-thumb" +#define LOAD_ANI_THUMB_PROC "file-ani-load-thumb" +#define SAVE_PROC "file-ico-save" +#define SAVE_CUR_PROC "file-cur-save" +#define SAVE_ANI_PROC "file-ani-save" typedef struct _Ico Ico; @@ -68,11 +74,21 @@ static GimpValueArray * ico_load (GimpProcedure *procedure, GFile *file, const GimpValueArray *args, gpointer run_data); +static GimpValueArray * ani_load (GimpProcedure *procedure, + GimpRunMode run_mode, + GFile *file, + const GimpValueArray *args, + gpointer run_data); static GimpValueArray * ico_load_thumb (GimpProcedure *procedure, GFile *file, gint size, const GimpValueArray *args, gpointer run_data); +static GimpValueArray * ani_load_thumb (GimpProcedure *procedure, + GFile *file, + gint size, + const GimpValueArray *args, + gpointer run_data); static GimpValueArray * ico_save (GimpProcedure *procedure, GimpRunMode run_mode, GimpImage *image, @@ -89,6 +105,14 @@ static GimpValueArray * cur_save (GimpProcedure *procedure, GFile *file, const GimpValueArray *args, gpointer run_data); +static GimpValueArray * ani_save (GimpProcedure *procedure, + GimpRunMode run_mode, + GimpImage *image, + gint n_drawables, + GimpDrawable **drawables, + GFile *file, + const GimpValueArray *args, + gpointer run_data); G_DEFINE_TYPE (Ico, ico, GIMP_TYPE_PLUG_IN) @@ -118,10 +142,13 @@ ico_query_procedures (GimpPlugIn *plug_in) GList *list = NULL; list = g_list_append (list, g_strdup (LOAD_THUMB_PROC)); + list = g_list_append (list, g_strdup (LOAD_ANI_THUMB_PROC)); list = g_list_append (list, g_strdup (LOAD_PROC)); list = g_list_append (list, g_strdup (LOAD_CUR_PROC)); + list = g_list_append (list, g_strdup (LOAD_ANI_PROC)); list = g_list_append (list, g_strdup (SAVE_PROC)); list = g_list_append (list, g_strdup (SAVE_CUR_PROC)); + list = g_list_append (list, g_strdup (SAVE_ANI_PROC)); return list; } @@ -190,6 +217,36 @@ ico_create_procedure (GimpPlugIn *plug_in, gimp_load_procedure_set_thumbnail_loader (GIMP_LOAD_PROCEDURE (procedure), LOAD_THUMB_PROC); } + else if (! strcmp (name, LOAD_ANI_PROC)) + { + procedure = gimp_load_procedure_new (plug_in, name, + GIMP_PDB_PROC_TYPE_PLUGIN, + ani_load, NULL, NULL); + + gimp_procedure_set_menu_label (procedure, _("Microsoft Windows animated cursor")); + gimp_procedure_set_icon_name (procedure, GIMP_ICON_BRUSH); + + gimp_procedure_set_documentation (procedure, + _("Loads files of Windows ANI file format"), + "Loads files of Windows ANI file format", + name); + gimp_procedure_set_attribution (procedure, + "Christian Kreibich , " + "James Huang, Alex S.", + "Christian Kreibich , " + "James Huang, Alex S.", + "2007-2022"); + + gimp_file_procedure_set_mime_types (GIMP_FILE_PROCEDURE (procedure), + "application/x-navi-animation"); + gimp_file_procedure_set_extensions (GIMP_FILE_PROCEDURE (procedure), + "ani"); + gimp_file_procedure_set_magics (GIMP_FILE_PROCEDURE (procedure), + "0,string,RIFF"); + + gimp_load_procedure_set_thumbnail_loader (GIMP_LOAD_PROCEDURE (procedure), + LOAD_ANI_THUMB_PROC); + } else if (! strcmp (name, LOAD_THUMB_PROC)) { procedure = gimp_thumbnail_procedure_new (plug_in, name, @@ -205,6 +262,24 @@ ico_create_procedure (GimpPlugIn *plug_in, "Sven Neumann ", "2005"); } + else if (! strcmp (name, LOAD_ANI_THUMB_PROC)) + { + procedure = gimp_thumbnail_procedure_new (plug_in, name, + GIMP_PDB_PROC_TYPE_PLUGIN, + ani_load_thumb, NULL, NULL); + + gimp_procedure_set_documentation (procedure, + _("Loads a preview from a Windows ANI files"), + "", + name); + gimp_procedure_set_attribution (procedure, + "Dom Lachowicz, Sven Neumann, James Huang, " + "Alex S.", + "Dom Lachowicz, " + "Sven Neumann , " + "James Huang, Alex S.", + "2007-2022"); + } else if (! strcmp (name, SAVE_PROC)) { procedure = gimp_save_procedure_new (plug_in, name, @@ -277,6 +352,72 @@ ico_create_procedure (GimpPlugIn *plug_in, "Y coordinates of hot spot (one per layer)", G_PARAM_READWRITE); } + else if (! strcmp (name, SAVE_ANI_PROC)) + { + procedure = gimp_save_procedure_new (plug_in, name, + GIMP_PDB_PROC_TYPE_PLUGIN, + ani_save, NULL, NULL); + + gimp_procedure_set_image_types (procedure, "*"); + + gimp_procedure_set_menu_label (procedure, _("Microsoft Windows animated cursor")); + gimp_procedure_set_icon_name (procedure, GIMP_ICON_BRUSH); + + gimp_procedure_set_documentation (procedure, + _("Saves files in Windows ANI file format"), + _("Saves files in Windows ANI file format"), + name); + gimp_procedure_set_attribution (procedure, + "Christian Kreibich , " + "James Huang, Alex S.", + "Christian Kreibich , " + "James Huang, Alex S.", + "2007-2022"); + + gimp_file_procedure_set_mime_types (GIMP_FILE_PROCEDURE (procedure), + "application/x-navi-animation"); + gimp_file_procedure_set_extensions (GIMP_FILE_PROCEDURE (procedure), + "ani"); + + GIMP_PROC_ARG_STRING (procedure, "cursor-name", + "Cursor Name", + _("Cursor Name (Optional)"), + NULL, + G_PARAM_READWRITE); + + GIMP_PROC_ARG_STRING (procedure, "author-name", + "Cursor Author", + _("Cursor Author (Optional)"), + NULL, + G_PARAM_READWRITE); + + GIMP_PROC_ARG_INT (procedure, "default-delay", + "Default delay", + "Default delay between frames " + "in jiffies (1/60 of a second)", + 0, G_MAXINT, 8, + G_PARAM_READWRITE); + + GIMP_PROC_ARG_INT (procedure, "n-hot-spot-x", + "Number of hot spot's X coordinates", + "Number of hot spot's X coordinates", + 0, G_MAXINT, 0, + G_PARAM_READWRITE); + GIMP_PROC_ARG_INT32_ARRAY (procedure, "hot-spot-x", + "Hot spot X", + "X coordinates of hot spot (one per layer)", + G_PARAM_READWRITE); + + GIMP_PROC_ARG_INT (procedure, "n-hot-spot-y", + "Number of hot spot's Y coordinates", + "Number of hot spot's Y coordinates", + 0, G_MAXINT, 0, + G_PARAM_READWRITE); + GIMP_PROC_ARG_INT32_ARRAY (procedure, "hot-spot-y", + "Hot spot Y", + "Y coordinates of hot spot (one per layer)", + G_PARAM_READWRITE); + } return procedure; } @@ -294,7 +435,37 @@ ico_load (GimpProcedure *procedure, gegl_init (NULL, NULL); - image = ico_load_image (file, &error); + image = ico_load_image (file, NULL, &error); + + if (! image) + return gimp_procedure_new_return_values (procedure, + GIMP_PDB_EXECUTION_ERROR, + error); + + return_vals = gimp_procedure_new_return_values (procedure, + GIMP_PDB_SUCCESS, + NULL); + + GIMP_VALUES_SET_IMAGE (return_vals, 1, image); + + return return_vals; +} + +static GimpValueArray * +ani_load (GimpProcedure *procedure, + GimpRunMode run_mode, + GFile *file, + const GimpValueArray *args, + gpointer run_data) +{ + GimpValueArray *return_vals; + GimpImage *image; + GError *error = NULL; + + gegl_init (NULL, NULL); + + image = ani_load_image (file, FALSE, + NULL, NULL, &error); if (! image) return gimp_procedure_new_return_values (procedure, @@ -329,7 +500,46 @@ ico_load_thumb (GimpProcedure *procedure, height = size; image = ico_load_thumbnail_image (file, - &width, &height, &error); + &width, &height, 0, &error); + + if (image) + return gimp_procedure_new_return_values (procedure, + GIMP_PDB_EXECUTION_ERROR, + error); + + return_vals = gimp_procedure_new_return_values (procedure, + GIMP_PDB_SUCCESS, + NULL); + + GIMP_VALUES_SET_IMAGE (return_vals, 1, image); + GIMP_VALUES_SET_INT (return_vals, 2, width); + GIMP_VALUES_SET_INT (return_vals, 3, height); + + gimp_value_array_truncate (return_vals, 4); + + return return_vals; +} + +static GimpValueArray * +ani_load_thumb (GimpProcedure *procedure, + GFile *file, + gint size, + const GimpValueArray *args, + gpointer run_data) +{ + GimpValueArray *return_vals; + gint width; + gint height; + GimpImage *image; + GError *error = NULL; + + gegl_init (NULL, NULL); + + width = size; + height = size; + + image = ani_load_image (file, TRUE, + &width, &height, &error); if (image) return gimp_procedure_new_return_values (procedure, @@ -426,6 +636,85 @@ cur_save (GimpProcedure *procedure, return gimp_procedure_new_return_values (procedure, status, error); } +static GimpValueArray * +ani_save (GimpProcedure *procedure, + GimpRunMode run_mode, + GimpImage *image, + gint n_drawables, + GimpDrawable **drawables, + GFile *file, + const GimpValueArray *args, + gpointer run_data) +{ + GimpProcedureConfig *config; + GimpPDBStatusType status; + GError *error = NULL; + gchar *inam = NULL; + gchar *iart = NULL; + gint jif_rate = 0; + gint32 *hot_spot_x = NULL; + gint32 *hot_spot_y = NULL; + gint n_hot_spot_x = 0; + gint n_hot_spot_y = 0; + AniFileHeader header; + AniSaveInfo ani_info; + + gegl_init (NULL, NULL); + + config = gimp_procedure_create_config (procedure); + gimp_procedure_config_begin_run (config, image, run_mode, args); + + g_object_get (config, + "cursor-name", &inam, + "author-name", &iart, + "default-delay", &jif_rate, + "n-hot-spot-x", &n_hot_spot_x, + "n-hot-spot-y", &n_hot_spot_y, + "hot-spot-x", &hot_spot_x, + "hot-spot-y", &hot_spot_y, + NULL); + + /* Jiffies (1/60th of a second) used if rate chunk not present. */ + header.jif_rate = jif_rate; + ani_info.inam = inam; + ani_info.iart = iart; + + status = ani_save_image (file, image, run_mode, + &n_hot_spot_x, &hot_spot_x, + &n_hot_spot_y, &hot_spot_y, + &header, &ani_info, &error); + + if (status == GIMP_PDB_SUCCESS) + { + /* XXX: seems libgimpconfig is not able to serialize + * GimpInt32Array args yet anyway. Still leave this here for now, + * as reminder of missing feature when we see the warnings. + */ + g_object_set (config, + "cursor-name", NULL, + "author-name", NULL, + "default-delay", header.jif_rate, + "n-hot-spot-x", n_hot_spot_x, + "n-hot-spot-y", n_hot_spot_y, + /*"hot-spot-x", hot_spot_x,*/ + /*"hot-spot-y", hot_spot_y,*/ + NULL); + g_free (hot_spot_x); + g_free (hot_spot_y); + + g_free (inam); + g_free (iart); + g_free (ani_info.inam); + g_free (ani_info.iart); + memset (&ani_info, 0, sizeof (AniSaveInfo)); + } + + gimp_procedure_config_end_run (config, status); + g_object_unref (config); + + return gimp_procedure_new_return_values (procedure, status, error); +} + gint ico_rowstride (gint width, gint bpp) diff --git a/plug-ins/file-ico/ico.h b/plug-ins/file-ico/ico.h index 07465d853b..dd8a40d400 100644 --- a/plug-ins/file-ico/ico.h +++ b/plug-ins/file-ico/ico.h @@ -97,6 +97,22 @@ typedef struct _IcoSaveInfo gint *hot_spot_y; } IcoSaveInfo; +typedef struct _AniFileHeader +{ + guint32 bSizeOf; /* Number of bytes in AniFileHeader (36 bytes) */ + guint32 frames; /* Number of unique icons in this cursor */ + guint32 steps; /* Number of Blits before the animation cycles */ + guint32 x, y; /* Reserved, must be zero. */ + guint32 bpp, planes; /* Reserved, must be zero. */ + guint32 jif_rate; /* Default Jiffies (1/60th of a second) if rate chunk's not present. */ + guint32 flags; /* Animation Flag */ +} AniFileHeader; + +typedef struct _AniSaveInfo +{ + gchar *inam; /* Cursor name metadata */ + gchar *iart; /* Author name metadata */ +} AniSaveInfo; /* Miscellaneous helper functions below: */