summaryrefslogtreecommitdiffstats
path: root/plug-ins/common/file-xmc.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 03:13:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 03:13:10 +0000
commit3c57dd931145d43f2b0aef96c4d178135956bf91 (patch)
tree3de698981e9f0cc2c4f9569b19a5f3595e741f6b /plug-ins/common/file-xmc.c
parentInitial commit. (diff)
downloadgimp-3c57dd931145d43f2b0aef96c4d178135956bf91.tar.xz
gimp-3c57dd931145d43f2b0aef96c4d178135956bf91.zip
Adding upstream version 2.10.36.upstream/2.10.36
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'plug-ins/common/file-xmc.c')
-rw-r--r--plug-ins/common/file-xmc.c2492
1 files changed, 2492 insertions, 0 deletions
diff --git a/plug-ins/common/file-xmc.c b/plug-ins/common/file-xmc.c
new file mode 100644
index 0000000..a5c41f7
--- /dev/null
+++ b/plug-ins/common/file-xmc.c
@@ -0,0 +1,2492 @@
+/*
+ * X11 Mouse Cursor (XMC) plug-in for GIMP
+ *
+ * Copyright 2008-2009 Takeshi Matsuyama <tksmashiw@gmail.com>
+ *
+ * Special thanks: Alexia Death, Sven Neumann, Martin Nordholts
+ * and all community members.
+ */
+
+/*
+ * 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/>.
+ */
+
+/*
+ * Todo: if drawable->bpp != 4 in save_image for GIMP-2.8?
+ * Todo: support for "gimp-metadata" parasite.
+ * "xmc-copyright" and "xmc-license" may be deprecated in future?
+ */
+
+/*
+ * This plug-in use these four parasites.
+ * "hot-spot" common with file-xbm plug-in
+ * "xmc-copyright" original, store contents of type1 comment chunk of Xcursor
+ * "xmc-license" original, store contents of type2 comment chunk of Xcursor
+ * "gimp-comment" common, store contents of type3 comment chunk of Xcursor
+ */
+
+/* *** Caution: Size vs Dimension ***
+ *
+ * In this file, "size" and "dimension" are used in definitely
+ * different contexts. "Size" means nominal size of Xcursor which is
+ * used to determine which frame depends on which animation sequence
+ * and which sequence is really used. (for more detail, please read
+ * Xcursor(3).) On the other hand, "Dimension" simply means width
+ * and/or height.
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+
+#include <glib/gstdio.h>
+#include <glib/gprintf.h>
+
+#include <libgimp/gimp.h>
+#include <libgimp/gimpui.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xcursor/Xcursor.h>
+
+#include "libgimp/stdplugins-intl.h"
+
+/* For debug */
+/* #define XMC_DEBUG */
+#ifdef XMC_DEBUG
+# define DM_XMC(...) g_fprintf(stderr, __VA_ARGS__)
+#else
+# define DM_XMC(...)
+#endif
+
+/*
+ * Constants...
+ */
+
+#define LOAD_PROC "file-xmc-load"
+#define LOAD_THUMB_PROC "file-xmc-load-thumb"
+#define SAVE_PROC "file-xmc-save"
+
+#define PLUG_IN_BINARY "file-xmc"
+#define PLUG_IN_ROLE "gimp-file-xmc"
+
+/* We use "xmc" as the file extension of X cursor for convenience */
+#define XCURSOR_EXTENSION "xmc"
+#define XCURSOR_MIME_TYPE "image/x-xcursor"
+
+/* The maximum dimension of Xcursor which is fully supported in any
+ * environments. This is defined on line 59 of xcursorint.h in
+ * libXcursor source code. Make sure this is about dimensions (width
+ * and height) not about nominal size despite of it's name.
+ *
+ * As of 2018, this macro still exists in libXCursor codebase, but I am
+ * unsure how far this restriction is enforced since this is a very low
+ * max dimension for today's displays. Therefore our code will not
+ * enforce this value anymore, but only warn about possible
+ * incompatibilities when using higher values.
+ */
+#define MAX_BITMAP_CURSOR_SIZE 64
+
+/* The maximum dimension of each frame of X cursor we want to save
+ * should be MAX_BITMAP_CURSOR_SIZE but about loading, xhot(& yhot) of
+ * each frame varies from 0 to MAX_BITMAP_CURSOR_SIZE-1, so we need to
+ * set the maximum dimension of image no less than
+ * MAX_BITMAP_CURSOR_SIZE * 2( -1 to be precise) to remain hotspots on
+ * the same coordinates.
+ *
+ * We use four times value (256 for saving, 512 for loading) as a
+ * limitation because some cursors generated by CursorXP/FX to X11
+ * Mouse Theme Converter is very large.
+ *
+ * The biggest cursor I found is "watch" of OuterLimits which size is
+ * 213x208. If you found bigger one, please tell me ;-)
+ */
+#define MAX_LOAD_DIMENSION 512
+#define MAX_SAVE_DIMENSION 256
+
+/* The maximum number of different nominal sizes in one cursor this
+ * plug-in can treat. This is based on the number of cursor size which
+ * gnome-appearance-properties supports.(12,16,24,32,36,40,48,64)
+ * ref. capplets/common/gnome-theme-info.c in source of
+ * gnome-control-center
+ */
+#define MAX_SIZE_NUM 8
+
+/* cursor delay is guint32 defined in Xcursor.h */
+#define CURSOR_MAX_DELAY 100000000
+#define CURSOR_DEFAULT_DELAY 50
+#define CURSOR_MINIMUM_DELAY 5
+
+#define div_255(x) (((x) + 0x80 + (((x) + 0x80) >> 8)) >> 8)
+#define READ32(f, e) read32 ((f), (e)); if (*(e)) return -1;
+#define DISPLAY_DIGIT(x) ((x) > 100) ? 3 : ((x) > 10) ? 2 : 1
+
+/*
+ * Structures...
+ */
+
+typedef struct
+{
+ gboolean crop;
+ gint size;
+ gboolean size_replace;
+ gint32 delay;
+ gboolean delay_replace;
+} XmcSaveVals;
+
+/*
+ * 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 load_image (const gchar *filename,
+ GError **error);
+
+static gint32 load_thumbnail (const gchar *filename,
+ gint32 thumb_size,
+ gint32 *width,
+ gint32 *height,
+ gint32 *num_layers,
+ GError **error);
+
+static guint32 read32 (FILE *f,
+ GError **error);
+
+static gboolean save_image (const gchar *filename,
+ gint32 image_ID,
+ gint32 drawable_ID,
+ gint32 orig_image_ID,
+ GError **error);
+
+static gboolean save_dialog (const gint32 image_ID,
+ GeglRectangle *hotspotRange);
+
+static void comment_entry_callback (GtkWidget *widget,
+ gchar **commentp);
+
+static void text_view_callback (GtkTextBuffer *buffer,
+ gchar **commentp);
+
+static gboolean load_default_hotspot (const gint32 image_ID,
+ GeglRectangle *hotspotRange);
+
+static inline guint32 separate_alpha (guint32 pixel);
+
+static inline guint32 premultiply_alpha (guint32 pixel);
+
+static XcursorComments *set_cursor_comments (void);
+
+static void load_comments (const gint32 image_ID);
+
+static gboolean set_comment_to_pname (const gint32 image_ID,
+ const gchar *content,
+ const gchar *pname);
+
+static gchar *get_comment_from_pname (const gint32 image_ID,
+ const gchar *pname);
+
+static gboolean set_hotspot_to_parasite (gint32 image_ID);
+
+static gboolean get_hotspot_from_parasite (gint32 image_ID);
+
+static void set_size_and_delay (const gchar *framename,
+ guint32 *sizep,
+ guint32 *delayp,
+ GRegex *re,
+ gboolean *size_warnp);
+
+static gchar *make_framename (guint32 size,
+ guint32 delay,
+ guint indent,
+ GError **errorp);
+
+static void get_cropped_region (GeglRectangle *retrun_rgn,
+ GeglBuffer *buffer);
+
+static inline gboolean pix_is_opaque (guint32 pix);
+
+static GeglRectangle * get_intersection_of_frames (gint32 image_ID);
+
+static gboolean pix_in_region (gint32 x,
+ gint32 y,
+ GeglRectangle *xmcrp);
+
+static void find_hotspots_and_dimensions (XcursorImages *xcIs,
+ gint32 *xhot,
+ gint32 *yhot,
+ gint32 *width,
+ gint32 *height);
+
+/*
+ * Globals...
+ */
+
+const GimpPlugInInfo PLUG_IN_INFO =
+{
+ NULL,
+ NULL,
+ query,
+ run
+};
+
+static XmcSaveVals xmcvals =
+{
+ /* saved in pdb after this plug-in's process has gone. */
+ FALSE, /* crop */
+ 32, /* size */
+ FALSE, /* size_replace */
+ CURSOR_DEFAULT_DELAY, /* delay */
+ FALSE /* delay_replace */
+};
+
+static struct
+{
+ /* saved as parasites of original image after this plug-in's process has gone.*/
+ gint32 x; /* hotspot x */
+ gint32 y; /* hotspot y */
+ gchar *comments[3]; /* copyright, license, other */
+} xmcparas = {0,};
+
+/* parasites correspond to XcursorComment type */
+static const gchar *parasiteName[3] = { "xmc-copyright",
+ "xmc-license",
+ "gimp-comment" };
+
+/*
+ * 'main()' - Main entry - just call gimp_main()...
+ */
+
+MAIN ()
+
+
+/*
+ * 'query()' - Respond to a plug-in query...
+ */
+
+static void
+query (void)
+{
+ static const GimpParamDef load_args[] =
+ {
+ { GIMP_PDB_INT32, "run-mode", "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }" },
+ { GIMP_PDB_STRING, "filename", "The name of the file to load" },
+ { GIMP_PDB_STRING, "raw-filename", "The name of the file to load" }
+ };
+ static const GimpParamDef load_return_vals[] =
+ {
+ { GIMP_PDB_IMAGE, "image", "Output image" }
+ };
+
+ static const GimpParamDef thumb_args[] =
+ {
+ { GIMP_PDB_STRING, "filename", "The name of the file to load" },
+ { GIMP_PDB_INT32, "thumb-size", "Preferred thumbnail size" }
+ };
+ static const GimpParamDef thumb_return_vals[] =
+ {
+ { GIMP_PDB_IMAGE, "image", "Thumbnail image" },
+ { GIMP_PDB_INT32, "image-width", "The width of image" },
+ { GIMP_PDB_INT32, "image-height", "The height of image" },
+ { GIMP_PDB_INT32, "image-type", "The color type of image"},
+ { GIMP_PDB_INT32, "image-num-layers", "The number of layeres" }
+ };
+
+ static const GimpParamDef save_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", "Drawable to export" },
+ { GIMP_PDB_STRING, "filename", "The name of the file to export the image in" },
+ { GIMP_PDB_STRING, "raw-filename", "The name entered" },
+ /* following elements are XMC specific options */
+ { GIMP_PDB_INT32, "x_hot", "X-coordinate of hot spot" },
+ { GIMP_PDB_INT32, "y_hot", "Y-coordinate of hot spot\n"
+ "Use (-1, -1) to keep original hot spot."},
+ { GIMP_PDB_INT32, "crop", "Auto-crop or not" },
+ { GIMP_PDB_INT32, "size", "Default nominal size" },
+ { GIMP_PDB_INT32, "size_replace", "Replace existent size or not." },
+ { GIMP_PDB_INT32, "delay", "Default delay" },
+ { GIMP_PDB_INT32, "delay_replace","Replace existent delay or not."},
+ { GIMP_PDB_STRING, "copyright", "Copyright information." },
+ { GIMP_PDB_STRING, "license", "License information." },
+ { GIMP_PDB_STRING, "other", "Other comment.(taken from "
+ "\"gimp-comment\" parasite)" }
+ };
+
+ gimp_install_procedure (LOAD_PROC,
+ "Loads files of X11 Mouse Cursor file format",
+ "This plug-in loads X11 Mouse Cursor (XMC) files.",
+ "Takeshi Matsuyama <tksmashiw@gmail.com>",
+ "Takeshi Matsuyama",
+ "26 May 2009",
+ N_("X11 Mouse Cursor"),
+ NULL,
+ GIMP_PLUGIN,
+ G_N_ELEMENTS (load_args),
+ G_N_ELEMENTS (load_return_vals),
+ load_args,
+ load_return_vals);
+
+ gimp_register_file_handler_mime (LOAD_PROC, XCURSOR_MIME_TYPE);
+ gimp_register_magic_load_handler (LOAD_PROC,
+ XCURSOR_EXTENSION,
+ "",
+ "0,string,Xcur");
+
+ gimp_install_procedure (LOAD_THUMB_PROC,
+ "Loads only first frame of X11 Mouse Cursor's "
+ "animation sequence which nominal size is the closest "
+ "of thumb-size to be used as a thumbnail",
+ "",
+ "Takeshi Matsuyama <tksmashiw@gmail.com>",
+ "Takeshi Matsuyama",
+ "26 May 2009",
+ NULL,
+ NULL,
+ GIMP_PLUGIN,
+ G_N_ELEMENTS (thumb_args),
+ G_N_ELEMENTS (thumb_return_vals),
+ thumb_args,
+ thumb_return_vals);
+
+ gimp_register_thumbnail_loader (LOAD_PROC, LOAD_THUMB_PROC);
+
+ gimp_install_procedure (SAVE_PROC,
+ "Exports files of X11 cursor file",
+ "This plug-in exports X11 Mouse Cursor (XMC) files",
+ "Takeshi Matsuyama <tksmashiw@gmail.com>",
+ "Takeshi Matsuyama",
+ "26 May 2009",
+ N_("X11 Mouse Cursor"),
+ "RGBA",
+ GIMP_PLUGIN,
+ G_N_ELEMENTS (save_args), 0,
+ save_args, NULL);
+
+ gimp_register_file_handler_mime (SAVE_PROC, XCURSOR_MIME_TYPE);
+ gimp_register_save_handler (SAVE_PROC, XCURSOR_EXTENSION, "");
+}
+
+/*
+ * 'run()' - Run the plug-in...
+ */
+
+static void
+run (const gchar *name,
+ gint nparams,
+ const GimpParam *param,
+ gint *nreturn_vals,
+ GimpParam **return_vals)
+{
+ static GimpParam values[6];
+ GimpRunMode run_mode;
+ GimpPDBStatusType status = GIMP_PDB_SUCCESS;
+ gint32 image_ID;
+ gint32 drawable_ID;
+ gint32 orig_image_ID;
+ GimpExportReturn export = GIMP_EXPORT_CANCEL;
+ GeglRectangle *hotspotRange = NULL;
+ gint32 width, height;
+ gint32 num_layers;
+ GError *error = NULL;
+ gint i;
+
+ INIT_I18N ();
+ gegl_init (NULL, NULL);
+
+ DM_XMC ("run: start.\n");
+ *nreturn_vals = 1;
+ *return_vals = values;
+
+ values[0].type = GIMP_PDB_STATUS;
+ values[0].data.d_status = GIMP_PDB_EXECUTION_ERROR;
+
+ if (strcmp (name, LOAD_PROC) == 0)
+ {
+ DM_XMC ("Starting to load file.\tparam.data=%s\n",
+ param[1].data.d_string);
+ image_ID = load_image (param[1].data.d_string, &error);
+
+ if (image_ID != -1)
+ {
+ *nreturn_vals = 2;
+ values[1].type = GIMP_PDB_IMAGE;
+ values[1].data.d_image = image_ID;
+ DM_XMC ("LOAD_PROC successfully load image. image_ID=%i\n", image_ID);
+ }
+ else
+ {
+ status = GIMP_PDB_EXECUTION_ERROR;
+ }
+ }
+ else if (strcmp (name, LOAD_THUMB_PROC) == 0)
+ {
+ DM_XMC ("Starting to load thumbnail.\tfilename=%s\tthumb-size=%d\n",
+ param[0].data.d_string, param[1].data.d_int32);
+ image_ID = load_thumbnail (param[0].data.d_string,
+ param[1].data.d_int32,
+ &width,
+ &height,
+ &num_layers,
+ &error);
+
+ if (image_ID != -1)
+ {
+ *nreturn_vals = 6;
+ values[1].type = GIMP_PDB_IMAGE;
+ values[1].data.d_image = image_ID;
+ values[2].type = GIMP_PDB_INT32;
+ values[2].data.d_int32 = width; /* width */
+ values[3].type = GIMP_PDB_INT32;
+ values[3].data.d_int32 = height; /* height */
+ /* This will not work on GIMP 2.6, but not harmful. */
+ values[4].type = GIMP_PDB_INT32;
+ values[4].data.d_int32 = GIMP_RGBA_IMAGE; /* type */
+ values[5].type = GIMP_PDB_INT32;
+ values[5].data.d_int32 = num_layers; /* num_layers */
+
+ DM_XMC ("LOAD_THUMB_PROC successfully load image. image_ID=%i\n", image_ID);
+ }
+ else
+ {
+ status = GIMP_PDB_EXECUTION_ERROR;
+ }
+ }
+ else if (strcmp (name, SAVE_PROC) == 0)
+ {
+ DM_XMC ("run: export %s\n", name);
+ run_mode = param[0].data.d_int32;
+ image_ID = orig_image_ID = param[1].data.d_int32;
+ drawable_ID = param[2].data.d_int32;
+ hotspotRange = get_intersection_of_frames (image_ID);
+
+ if (! hotspotRange)
+ {
+ g_set_error (&error, 0, 0,
+ _("Cannot set the hot spot!\n"
+ "You must arrange layers so that all of them have an intersection."));
+ *nreturn_vals = 2;
+ values[1].type = GIMP_PDB_STRING;
+ values[1].data.d_string = error->message;
+ values[0].data.d_status = GIMP_PDB_EXECUTION_ERROR;
+
+ return;
+ }
+
+ /* eventually export the image */
+ switch (run_mode)
+ {
+ case GIMP_RUN_INTERACTIVE:
+ case GIMP_RUN_WITH_LAST_VALS:
+ gimp_ui_init (PLUG_IN_BINARY, FALSE);
+
+ export = gimp_export_image (&image_ID, &drawable_ID, "XMC",
+ GIMP_EXPORT_CAN_HANDLE_RGB |
+ GIMP_EXPORT_CAN_HANDLE_ALPHA |
+ GIMP_EXPORT_CAN_HANDLE_LAYERS |
+ GIMP_EXPORT_NEEDS_ALPHA);
+
+ if (export == GIMP_EXPORT_CANCEL)
+ {
+ *nreturn_vals = 1;
+ values[0].data.d_status = GIMP_PDB_CANCEL;
+
+ return;
+ }
+ break;
+
+ default:
+ break;
+ }
+ switch (run_mode)
+ {
+ case GIMP_RUN_INTERACTIVE:
+ /*
+ * Possibly retrieve data...
+ */
+ gimp_get_data (SAVE_PROC, &xmcvals);
+ load_comments (image_ID);
+
+ load_default_hotspot (image_ID, hotspotRange);
+
+ if (! save_dialog (image_ID, hotspotRange))
+ status = GIMP_PDB_CANCEL;
+ break;
+
+ case GIMP_RUN_NONINTERACTIVE:
+ /*
+ * Make sure all the arguments are there!
+ */
+ if (nparams != 15)
+ {
+ status = GIMP_PDB_CALLING_ERROR;
+ }
+ else
+ {
+ if (pix_in_region (param[5].data.d_int32, param[6].data.d_int32,
+ hotspotRange))
+ { /* if passed hotspot is acceptable, use that ones. */
+ xmcparas.x = param[5].data.d_int32;
+ xmcparas.y = param[6].data.d_int32;
+ }
+ else
+ {
+ load_default_hotspot (image_ID, hotspotRange);
+ /* you can purposely choose non acceptable values for hotspot
+ to use cursor's original values. */
+ }
+ xmcvals.crop = param[7].data.d_int32;
+ xmcvals.size = param[8].data.d_int32;
+ xmcvals.size_replace = param[9].data.d_int32;
+ /* load delay */
+ if (param[10].data.d_int32 < CURSOR_MINIMUM_DELAY)
+ {
+ xmcvals.delay = CURSOR_DEFAULT_DELAY;
+ }
+ else
+ {
+ xmcvals.delay = param[10].data.d_int32;
+ }
+ xmcvals.delay_replace = param[11].data.d_int32;
+ load_comments (image_ID);
+ for (i = 0; i < 3; ++i)
+ {
+ if (param[i + 12].data.d_string &&
+ g_utf8_validate (param[i + 12].data.d_string, -1, NULL))
+ {
+ xmcparas.comments[i] = g_strdup (param[i + 12].data.d_string);
+ }
+ }
+ }
+ break;
+
+ case GIMP_RUN_WITH_LAST_VALS:
+ /* Possibly retrieve data... */
+ gimp_get_data (SAVE_PROC, &xmcvals);
+ load_comments (image_ID);
+ load_default_hotspot (image_ID, hotspotRange);
+ break;
+
+ default:
+ break;
+ }
+ if (status == GIMP_PDB_SUCCESS)
+ {
+ if (save_image (param[3].data.d_string, image_ID,
+ drawable_ID, orig_image_ID, &error))
+ {
+ gimp_set_data (SAVE_PROC, &xmcvals, sizeof (XmcSaveVals));
+ }
+ else
+ {
+ status = GIMP_PDB_EXECUTION_ERROR;
+ }
+ }
+
+ if (export == GIMP_EXPORT_EXPORT)
+ gimp_image_delete (image_ID);
+
+ g_free (hotspotRange);
+
+ for (i = 0; i < 3 ; ++i)
+ {
+ g_free (xmcparas.comments[i]);
+ xmcparas.comments[i] = NULL;
+ }
+ }
+ else
+ {
+ DM_XMC ("name=%s\n", name);
+ status = GIMP_PDB_CALLING_ERROR;
+ }
+
+ if (status != GIMP_PDB_SUCCESS && error)
+ {
+ *nreturn_vals = 2;
+ values[1].type = GIMP_PDB_STRING;
+ values[1].data.d_string = error->message;
+ }
+
+ values[0].data.d_status = status;
+ DM_XMC ("run: finish\n");
+}
+
+
+/*
+ * 'load_image()' - Load a X cursor image into a new image window.
+ */
+
+static gint32
+load_image (const gchar *filename,
+ GError **error)
+{
+ FILE *fp;
+ gint32 image_ID;
+ gint32 layer_ID;
+ GeglBuffer *buffer;
+ XcursorComments *commentsp; /* pointer to comments */
+ XcursorImages *imagesp; /* pointer to images*/
+ guint32 delay; /* use guint32 instead CARD32(in X11/Xmd.h) */
+ gchar *framename; /* name of layer */
+ guint32 *tmppixel; /* pixel data (guchar * bpp = guint32) */
+ gint img_width;
+ gint img_height;
+ gint i, j;
+
+ gimp_progress_init_printf (_("Opening '%s'"),
+ gimp_filename_to_utf8 (filename));
+
+ /* Open the file and check it is a valid X cursor */
+
+ fp = g_fopen (filename, "rb");
+
+ if (fp == NULL)
+ {
+ g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
+ _("Could not open '%s' for reading: %s"),
+ gimp_filename_to_utf8 (filename), g_strerror (errno));
+ return -1;
+ }
+
+ if (! XcursorFileLoad (fp, &commentsp, &imagesp))
+ {
+ g_set_error (error, 0, 0, _("'%s' is not a valid X cursor."),
+ gimp_filename_to_utf8 (filename));
+ fclose (fp);
+ return -1;
+ }
+
+ /* check dimension is valid. */
+
+ for (i = 0; i < imagesp->nimage; i++)
+ {
+ if (imagesp->images[i]->width > MAX_LOAD_DIMENSION)
+ {
+ g_set_error (error, 0, 0,
+ _("Frame %d of '%s' is too wide for an X cursor."),
+ i + 1, gimp_filename_to_utf8 (filename));
+ fclose (fp);
+ return -1;
+ }
+ if (imagesp->images[i]->height > MAX_LOAD_DIMENSION)
+ {
+ g_set_error (error, 0, 0,
+ _("Frame %d of '%s' is too high for an X cursor."),
+ i + 1, gimp_filename_to_utf8 (filename));
+ fclose (fp);
+ return -1;
+ }
+ }
+
+ find_hotspots_and_dimensions (imagesp,
+ &xmcparas.x, &xmcparas.y,
+ &img_width, &img_height);
+
+ DM_XMC ("xhot=%i,\tyhot=%i,\timg_width=%i,\timg_height=%i\n",
+ xmcparas.x, xmcparas.y, img_width, img_height);
+
+ image_ID = gimp_image_new (img_width, img_height, GIMP_RGB);
+
+ gimp_image_set_filename (image_ID, filename);
+
+ if (! set_hotspot_to_parasite (image_ID))
+ {
+ fclose (fp);
+ return -1;
+ }
+
+ /* Temporary buffer */
+ tmppixel = g_new (guint32, img_width * img_height);
+
+ /* load each frame to each layer one by one */
+ for (i = 0; i < imagesp->nimage; i++)
+ {
+ gint width = imagesp->images[i]->width;
+ gint height = imagesp->images[i]->height;
+
+ delay = imagesp->images[i]->delay;
+
+ if (delay < CURSOR_MINIMUM_DELAY)
+ {
+ delay = CURSOR_DEFAULT_DELAY;
+ }
+
+ DM_XMC ("images[%i]->delay=%i\twidth=%d\theight=%d\n",
+ i ,delay, imagesp->images[i]->width, imagesp->images[i]->height);
+
+ framename = make_framename (imagesp->images[i]->size, delay,
+ DISPLAY_DIGIT (imagesp->nimage), error);
+ if (! framename)
+ {
+ fclose (fp);
+ return -1;
+ }
+
+ layer_ID = gimp_layer_new (image_ID, framename, width, height,
+ GIMP_RGBA_IMAGE,
+ 100,
+ gimp_image_get_default_new_layer_mode (image_ID));
+ gimp_image_insert_layer (image_ID, layer_ID, -1, 0);
+
+ /* Adjust layer position to let hotspot sit on the same point. */
+ gimp_item_transform_translate (layer_ID,
+ xmcparas.x - imagesp->images[i]->xhot,
+ xmcparas.y - imagesp->images[i]->yhot);
+
+ g_free (framename);
+
+ /* Get the buffer for our load... */
+
+ buffer = gimp_drawable_get_buffer (layer_ID);
+
+ /* set color to each pixel */
+ for (j = 0; j < width * height; j++)
+ {
+ tmppixel[j] = separate_alpha (imagesp->images[i]->pixels[j]);
+ }
+
+ /* set pixel */
+ gegl_buffer_set (buffer, GEGL_RECTANGLE (0, 0, width, height), 0,
+ NULL, tmppixel, GEGL_AUTO_ROWSTRIDE);
+
+ gimp_progress_update ((i + 1) / imagesp->nimage);
+
+ g_object_unref (buffer);
+ }
+
+ g_free (tmppixel);
+
+ gimp_progress_update (1.0);
+
+ /* Comment parsing */
+
+ if (commentsp)
+ {
+ for (i = 0; i < commentsp->ncomment; ++i)
+ {
+ DM_XMC ("comment type=%d\tcomment=%s\n",
+ commentsp->comments[i]->comment_type,
+ commentsp->comments[i]->comment);
+ if (! set_comment_to_pname (image_ID,
+ commentsp->comments[i]->comment,
+ parasiteName[commentsp->comments[i]->comment_type -1]))
+ {
+ DM_XMC ("Failed to write %ith comment.\n", i);
+ fclose (fp);
+ return -1;
+ }
+ }
+ }
+
+ DM_XMC ("Comment parsing done.\n");
+ XcursorImagesDestroy (imagesp);
+ XcursorCommentsDestroy (commentsp);
+ fclose (fp);
+
+ gimp_progress_end ();
+
+ return image_ID;
+}
+
+/*
+ * load_thumbnail
+ */
+
+static gint32
+load_thumbnail (const gchar *filename,
+ gint32 thumb_size,
+ gint32 *thumb_width,
+ gint32 *thumb_height,
+ gint32 *thumb_num_layers,
+ GError **error)
+{
+ /* Return only one frame for thumbnail.
+ * We select first frame of an animation sequence which nominal size is the
+ * closest of thumb_size.
+ */
+
+ XcursorImages *xcIs = NULL; /* use to find the dimensions of thumbnail */
+ XcursorImage *xcI; /* temporary pointer to XcursorImage */
+ guint32 *positions; /* array of the offsets of image chunks */
+ guint32 size; /* nominal size */
+ guint32 diff; /* difference between thumb_size and current size */
+ guint32 min_diff = XCURSOR_IMAGE_MAX_SIZE; /* minimum value of diff */
+ guint32 type; /* chunk type */
+ FILE *fp = NULL;
+ gint32 image_ID = -1;
+ gint32 layer_ID;
+ GeglBuffer *buffer;
+ guint32 *tmppixel; /* pixel data (guchar * bpp = guint32) */
+ guint32 ntoc = 0; /* the number of table of contents */
+ gint sel_num = -1; /* the index of selected image chunk */
+ gint width;
+ gint height;
+ gint i;
+
+ g_return_val_if_fail (thumb_width, -1);
+ g_return_val_if_fail (thumb_height, -1);
+ g_return_val_if_fail (thumb_num_layers, -1);
+
+ *thumb_width = 0;
+ *thumb_height = 0;
+ *thumb_num_layers = 0;
+
+ fp = g_fopen (filename, "rb");
+
+ if (fp == NULL)
+ {
+ g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
+ _("Could not open '%s' for reading: %s"),
+ gimp_filename_to_utf8 (filename), g_strerror (errno));
+ return -1;
+ }
+
+ /* From this line, we make a XcursorImages struct so that we can find out the
+ * width and height of entire image.
+ * We can use XcursorFileLoadImages (fp, thumb_size) from libXcursor instead
+ * of this ugly code but XcursorFileLoadImages loads all pixel data of the
+ * image chunks on memory thus we should not use it.
+ */
+
+ /* find which image chunk is preferred to load. */
+
+ /* skip magic, headersize, version */
+ fseek (fp, 12, SEEK_SET);
+ /* read the number of chunks */
+ ntoc = READ32 (fp, error)
+ if (ntoc > (G_MAXUINT32 / sizeof (guint32)))
+ {
+ g_set_error (error, 0, 0,
+ "'%s' seems to have an incorrect toc size.",
+ gimp_filename_to_utf8 (filename));
+ fclose (fp);
+ return -1;
+ }
+ positions = g_malloc (ntoc * sizeof (guint32));
+
+ /* enter list of toc(table of contents) */
+ for (; ntoc > 0; --ntoc)
+ {
+ /* read entry type */
+ type = READ32 (fp, error)
+ if (type != XCURSOR_IMAGE_TYPE)
+ {
+ /* not a image */
+
+ /* skip rest of this content */
+ fseek (fp, 8, SEEK_CUR);
+ }
+ else
+ {
+ /* this content is image */
+
+ size = READ32 (fp, error)
+ positions[*thumb_num_layers] = READ32 (fp, error)
+ /* is this image is more preferred than selected before? */
+ diff = MAX (thumb_size, size) - MIN (thumb_size, size);
+ if (diff < min_diff)
+ {/* the image size is closer than current selected image */
+ min_diff = diff;
+ sel_num = *thumb_num_layers;
+ }
+ ++*thumb_num_layers;
+ }
+ }
+
+ if (sel_num < 0)
+ {
+ g_set_error (error, 0, 0,
+ _("there is no image chunk in \"%s\"."),
+ gimp_filename_to_utf8 (filename));
+ fclose (fp);
+ return -1;
+ }
+
+ /* get width and height of entire image */
+
+ /* Let's make XcursorImages */
+ xcIs = XcursorImagesCreate (*thumb_num_layers);
+ xcIs->nimage = *thumb_num_layers;
+ for (i = 0; i < xcIs->nimage; ++i)
+ {
+ /* make XcursorImage with no pixel buffer */
+ xcI = XcursorImageCreate (0, 0);
+ /* go to the image chunk header */
+ fseek (fp, positions[i], SEEK_SET);
+ /* skip chunk header */
+ fseek (fp, 16, SEEK_CUR);
+ /* read properties of this image to determine entire image dimensions */
+ xcI->width = READ32 (fp, error)
+ xcI->height = READ32 (fp, error)
+ xcI->xhot = READ32 (fp, error)
+ xcI->yhot = READ32 (fp, error)
+
+ xcIs->images[i] = xcI;
+ }
+
+ DM_XMC ("selected size is %i or %i\n",
+ thumb_size - min_diff, thumb_size + min_diff);
+
+ /* get entire image dimensions */
+ find_hotspots_and_dimensions (xcIs, NULL, NULL, thumb_width, thumb_height);
+
+ DM_XMC ("width=%i\theight=%i\tnum-layers=%i\n",
+ *thumb_width, *thumb_height, xcIs->nimage);
+
+ /* dimension check */
+ if (*thumb_width > MAX_LOAD_DIMENSION)
+ {
+ g_set_error (error, 0, 0,
+ _("'%s' is too wide for an X cursor."),
+ gimp_filename_to_utf8 (filename));
+ fclose (fp);
+ return -1;
+ }
+
+ if (*thumb_height > MAX_LOAD_DIMENSION)
+ {
+ g_set_error (error, 0, 0,
+ _("'%s' is too high for an X cursor."),
+ gimp_filename_to_utf8 (filename));
+ fclose (fp);
+ return -1;
+ }
+
+ /* create new image! */
+
+ width = xcIs->images[sel_num]->width;
+ height = xcIs->images[sel_num]->height;
+
+ image_ID = gimp_image_new (width, height, GIMP_RGB);
+
+ layer_ID = gimp_layer_new (image_ID, NULL, width, height,
+ GIMP_RGBA_IMAGE,
+ 100,
+ gimp_image_get_default_new_layer_mode (image_ID));
+
+ gimp_image_insert_layer (image_ID, layer_ID, -1, 0);
+
+ /*
+ * Get the drawable and set the pixel region for our load...
+ */
+
+ buffer = gimp_drawable_get_buffer (layer_ID);
+
+ /* Temporary buffer */
+ tmppixel = g_new (guint32, width * height);
+
+ /* copy the chunk data to tmppixel */
+ fseek (fp, positions[sel_num], SEEK_SET);
+ fseek (fp, 36, SEEK_CUR); /* skip chunk header(16bytes), xhot, yhot, width, height, delay */
+
+ for (i = 0; i < width * height; i++)
+ {
+ tmppixel[i] = READ32 (fp, error)
+ /* get back separate alpha */
+ tmppixel[i] = separate_alpha (tmppixel[i]);
+ }
+
+ /* set pixel */
+ gegl_buffer_set (buffer, GEGL_RECTANGLE (0, 0, width, height), 0,
+ NULL, tmppixel, GEGL_AUTO_ROWSTRIDE);
+
+ /* free tmppixel */
+ g_free(tmppixel);
+ g_free (positions);
+ fclose (fp);
+
+ g_object_unref (buffer);
+
+ return image_ID;
+}
+
+/* read guint32 value from f despite of host's byte order. */
+static guint32
+read32 (FILE *f,
+ GError **error)
+{
+ guchar p[4];
+ guint32 ret;
+
+ if (fread (p, 1, 4, f) != 4)
+ {
+ g_set_error (error, 0, 0, _("A read error occurred."));
+ return 0;
+ }
+
+#if G_BYTE_ORDER == G_LITTLE_ENDIAN
+ ret = p[0] + (p[1]<<8) + (p[2]<<16) + (p[3]<<24);
+#elif G_BYTE_ORDER == G_BIG_ENDIAN
+ ret = p[3] + (p[2]<<8) + (p[1]<<16) + (p[0]<<24);
+#elif G_BYTE_ORDER == G_PDP_ENDIAN
+ ret = p[2] + (p[3]<<8) + (p[0]<<16) + (p[1]<<24);
+#else
+ g_return_val_if_rearched ();
+#endif
+
+ return ret;
+}
+
+/* 'save_dialog ()'
+ */
+static gboolean
+save_dialog (const gint32 image_ID,
+ GeglRectangle *hotspotRange)
+{
+ GtkWidget *dialog;
+ GtkWidget *frame;
+ GtkWidget *table;
+ GtkWidget *box;
+ GtkAdjustment *adjustment;
+ GtkWidget *alignment;
+ GtkWidget *tmpwidget;
+ GtkWidget *label;
+ GtkTextBuffer *textbuffer;
+ GValue val = G_VALUE_INIT;
+ gint x1, x2, y1, y2;
+ gboolean run;
+
+ g_value_init (&val, G_TYPE_DOUBLE);
+ dialog = gimp_export_dialog_new (_("X11 Mouse Cursor"),
+ PLUG_IN_BINARY, SAVE_PROC);
+
+ /*
+ * parameter settings
+ */
+ frame = gimp_frame_new (_("XMC Options"));
+ gtk_container_set_border_width (GTK_CONTAINER (frame), 12);
+ gtk_box_pack_start (GTK_BOX (gimp_export_dialog_get_content_area (dialog)),
+ frame, TRUE, TRUE, 0);
+ gtk_widget_show (frame);
+
+ table = gtk_table_new (9, 3, FALSE);
+ gtk_widget_show (table);
+ gtk_table_set_col_spacings (GTK_TABLE (table), 6);
+ gtk_table_set_row_spacings (GTK_TABLE (table), 6);
+ gtk_container_set_border_width (GTK_CONTAINER (table), 12);
+ gtk_container_add (GTK_CONTAINER (frame), table);
+
+ /*
+ * Hotspot
+ */
+ /* label "Hot spot _X:" + spinbox */
+ x1 = hotspotRange->x;
+ x2 = hotspotRange->width + hotspotRange->x - 1;
+
+ adjustment = (GtkAdjustment *)
+ gtk_adjustment_new (xmcparas.x, x1, x2, 1, 5, 0);
+ tmpwidget = gimp_spin_button_new (adjustment, 1.0, 0);
+ gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (tmpwidget), TRUE);
+ g_value_set_double (&val, 1.0);
+ g_object_set_property (G_OBJECT (tmpwidget), "xalign", &val);/* align right*/
+ gimp_table_attach_aligned (GTK_TABLE (table), 0, 0,
+ _("Hot spot _X:"), 0, 0.5, tmpwidget, 1, TRUE);
+ gtk_widget_show (tmpwidget);
+
+ g_signal_connect (adjustment, "value-changed",
+ G_CALLBACK (gimp_int_adjustment_update),
+ &xmcparas.x);
+
+ gimp_help_set_help_data (tmpwidget,
+ _("Enter the X coordinate of the hot spot. "
+ "The origin is top left corner."),
+ NULL);
+
+ /* label "Y:" + spinbox */
+ y1 = hotspotRange->y;
+ y2 = hotspotRange->height + hotspotRange->y - 1;
+
+ adjustment = (GtkAdjustment *)
+ gtk_adjustment_new (xmcparas.y, y1, y2, 1, 5, 0);
+ tmpwidget = gimp_spin_button_new (adjustment, 1.0, 0);
+ gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (tmpwidget), TRUE);
+ g_value_set_double (&val, 1.0);
+ g_object_set_property (G_OBJECT (tmpwidget), "xalign", &val);/* align right*/
+ gimp_table_attach_aligned (GTK_TABLE (table), 1, 0,
+ "_Y:", 1.0, 0.5, tmpwidget, 1, TRUE);
+ gtk_widget_show (tmpwidget);
+
+ g_signal_connect (adjustment, "value-changed",
+ G_CALLBACK (gimp_int_adjustment_update),
+ &xmcparas.y);
+
+ gimp_help_set_help_data (tmpwidget,
+ _("Enter the Y coordinate of the hot spot. "
+ "The origin is top left corner."),
+ NULL);
+
+ /*
+ * Auto-crop
+ */
+ /* check button */
+ tmpwidget =
+ gtk_check_button_new_with_mnemonic (_("_Auto-Crop all frames."));
+ gtk_table_attach (GTK_TABLE (table),
+ tmpwidget, 0, 3, 1, 2, GTK_FILL, 0, 0, 10);
+ gtk_widget_show (tmpwidget);
+
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (tmpwidget),
+ xmcvals.crop);
+ gtk_widget_show (tmpwidget);
+ g_signal_connect (tmpwidget, "toggled",
+ G_CALLBACK (gimp_toggle_button_update),
+ &xmcvals.crop);
+ /* tooltip */
+ gimp_help_set_help_data (tmpwidget,
+ _("Remove the empty borders of all frames.\n"
+ "This reduces the file size and may fix "
+ "the problem that some large cursors disorder "
+ "the screen.\n"
+ "Uncheck if you plan to edit the exported "
+ "cursor using other programs."),
+ NULL);
+
+ /*
+ * size
+ */
+ tmpwidget =
+ gimp_int_combo_box_new ("12px", 12, "16px", 16,
+ "24px", 24, "32px", 32,
+ "36px", 36, "40px", 40,
+ "48px", 48, "64px", 64, NULL);
+ gimp_int_combo_box_connect (GIMP_INT_COMBO_BOX (tmpwidget),
+ 32,
+ G_CALLBACK (gimp_int_combo_box_get_active),
+ &xmcvals.size);
+ gtk_widget_show (tmpwidget);
+ /* tooltip */
+ gimp_help_set_help_data (tmpwidget,
+ _("Choose the nominal size of frames.\n"
+ "If you don't have plans to make multi-sized "
+ "cursor, or you have no idea, leave it \"32px\".\n"
+ "Nominal size has no relation with the actual "
+ "size (width or height).\n"
+ "It is only used to determine which frame depends "
+ "on which animation sequence, and which sequence "
+ "is used based on the value of "
+ "\"gtk-cursor-theme-size\"."),
+ NULL);
+
+ gimp_table_attach_aligned (GTK_TABLE (table), 0, 2,
+ _("_Size:"), 0, 0.5, tmpwidget, 3, TRUE);
+ /* Replace size ? */
+ tmpwidget =
+ gimp_int_radio_group_new (FALSE, NULL, G_CALLBACK (gimp_radio_button_update),
+ &xmcvals.size_replace, xmcvals.size_replace,
+ _("_Use this value only for a frame which size "
+ "is not specified."),
+ FALSE, NULL,
+ _("_Replace the size of all frames even if it "
+ "is specified."),
+ TRUE, NULL,
+ NULL);
+ alignment = gtk_alignment_new (0.5, 0.5, 1.0, 1.0);
+ gtk_widget_show (alignment);
+ gtk_table_attach (GTK_TABLE (table), alignment, 0, 3, 3, 4, 0, 0, 0, 0);
+ gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 0, 6, 20, 0); /*padding left*/
+ gtk_container_add (GTK_CONTAINER (alignment), tmpwidget);
+ gtk_widget_show (tmpwidget);
+
+ /*
+ * delay
+ */
+ /* spin button */
+ box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
+ gimp_table_attach_aligned (GTK_TABLE (table), 0, 4, _("_Delay:"),
+ 0, 0.5, box, 3, TRUE);
+ gtk_widget_show (box);
+
+ gimp_help_set_help_data (box,
+ _("Enter time span in milliseconds in which "
+ "each frame is rendered."),
+ NULL);
+
+ adjustment = (GtkAdjustment *)
+ gtk_adjustment_new (xmcvals.delay, CURSOR_MINIMUM_DELAY,
+ CURSOR_MAX_DELAY, 1, 5, 0);
+ tmpwidget = gimp_spin_button_new (adjustment, 1.0, 0);
+ gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (tmpwidget), TRUE);
+ g_value_set_double (&val, 1.0);
+ g_object_set_property (G_OBJECT (tmpwidget), "xalign", &val);/* align right*/
+ gtk_box_pack_start (GTK_BOX (box), tmpwidget, TRUE, TRUE, 0);
+ gtk_widget_show (tmpwidget);
+
+ g_signal_connect (adjustment, "value-changed",
+ G_CALLBACK (gimp_int_adjustment_update),
+ &xmcvals.delay);
+
+ /* appended "ms" */
+ tmpwidget = gtk_label_new ("ms");
+ gtk_label_set_xalign (GTK_LABEL (tmpwidget), 0.0); /*align left*/
+ gtk_box_pack_start (GTK_BOX (box), tmpwidget, TRUE, TRUE, 0);
+ gtk_widget_show (tmpwidget);
+
+ /* Replace delay? */
+ tmpwidget =
+ gimp_int_radio_group_new (FALSE, NULL, G_CALLBACK (gimp_radio_button_update),
+ &xmcvals.delay_replace, xmcvals.delay_replace,
+ _("_Use this value only for a frame which delay "
+ "is not specified."),
+ FALSE, NULL,
+ _("_Replace the delay of all frames even if it "
+ "is specified."),
+ TRUE, NULL,
+ NULL);
+ alignment = gtk_alignment_new (0.5, 0.5, 1.0, 1.0);
+ gtk_widget_show (alignment);
+ gtk_table_attach (GTK_TABLE (table), alignment, 0, 3, 5, 6, 0, 0, 0, 0);
+ gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 0, 6, 20, 0); /*padding left*/
+ gtk_container_add (GTK_CONTAINER (alignment), tmpwidget);
+ gtk_widget_show (tmpwidget);
+
+ /*
+ * Copyright
+ */
+ tmpwidget = gtk_entry_new ();
+ /* Maximum length will be clamped to 65536 */
+ gtk_entry_set_max_length (GTK_ENTRY (tmpwidget), XCURSOR_COMMENT_MAX_LEN);
+
+ if (xmcparas.comments[0])
+ {
+ gtk_entry_set_text (GTK_ENTRY (tmpwidget),
+ gimp_any_to_utf8 (xmcparas.comments[0], - 1, NULL));
+ /* show warning if comment is over 65535 characters
+ * because gtk_entry can hold only that. */
+ if (strlen (gtk_entry_get_text (GTK_ENTRY (tmpwidget))) >= 65535)
+ g_message (_("The part of copyright information "
+ "that exceeded 65535 characters was removed."));
+ }
+
+ g_signal_connect (tmpwidget, "changed",
+ G_CALLBACK (comment_entry_callback),
+ xmcparas.comments);
+ gtk_widget_show (tmpwidget);
+ /* tooltip */
+ gimp_help_set_help_data (tmpwidget,
+ _("Enter copyright information."),
+ NULL);
+ gimp_table_attach_aligned (GTK_TABLE (table), 0, 6, _("_Copyright:"),
+ 0, 0.5, tmpwidget, 3, FALSE);
+ /*
+ * License
+ */
+ tmpwidget = gtk_entry_new ();
+ /* Maximum length will be clamped to 65536 */
+ gtk_entry_set_max_length (GTK_ENTRY (tmpwidget), XCURSOR_COMMENT_MAX_LEN);
+
+ if (xmcparas.comments[1])
+ {
+ gtk_entry_set_text (GTK_ENTRY (tmpwidget),
+ gimp_any_to_utf8 (xmcparas.comments[1], - 1, NULL));
+ /* show warning if comment is over 65535 characters
+ * because gtk_entry can hold only that. */
+ if (strlen (gtk_entry_get_text (GTK_ENTRY (tmpwidget))) >= 65535)
+ g_message (_("The part of license information "
+ "that exceeded 65535 characters was removed."));
+ }
+
+ g_signal_connect (tmpwidget, "changed",
+ G_CALLBACK (comment_entry_callback),
+ xmcparas.comments + 1);
+ gtk_widget_show (tmpwidget);
+ /* tooltip */
+ gimp_help_set_help_data (tmpwidget,
+ _("Enter license information."),
+ NULL);
+ gimp_table_attach_aligned (GTK_TABLE (table), 0, 7, _("_License:"),
+ 0, 0.5, tmpwidget, 3, FALSE);
+ /*
+ * Other
+ */
+ /* We use gtk_text_view for "Other" while "Copyright" & "License" is entered
+ * in gtk_entry because We want allow '\n' for "Other". */
+ label = gtk_label_new_with_mnemonic (_("_Other:"));
+ gtk_widget_show (label);
+ gtk_label_set_xalign (GTK_LABEL (label), 0.0); /*align top-left*/
+ gtk_label_set_yalign (GTK_LABEL (label), 0.0); /*align top-left*/
+ gtk_table_attach (GTK_TABLE (table), label, 0, 1, 8, 9, GTK_FILL, 0, 0, 0);
+ /* content of Other */
+ /* scrolled window */
+ box = gtk_scrolled_window_new (NULL, NULL);
+ gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (box),
+ GTK_SHADOW_IN);
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (box),
+ GTK_POLICY_AUTOMATIC,
+ GTK_POLICY_AUTOMATIC);
+ gtk_table_attach (GTK_TABLE (table), box, 1, 3, 8, 9, GTK_FILL, 0, 0, 0);
+ gtk_widget_show (box);
+ /* textbuffer */
+ textbuffer = gtk_text_buffer_new (NULL);
+ if (xmcparas.comments[2])
+ gtk_text_buffer_set_text (textbuffer,
+ gimp_any_to_utf8 (xmcparas.comments[2], -1, NULL),
+ -1);
+ g_signal_connect (textbuffer, "changed",
+ G_CALLBACK (text_view_callback),
+ xmcparas.comments + 2);
+ /* textview */
+ tmpwidget =
+ gtk_text_view_new_with_buffer (GTK_TEXT_BUFFER (textbuffer));
+ gtk_text_view_set_accepts_tab (GTK_TEXT_VIEW (tmpwidget), FALSE);
+ gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tmpwidget), GTK_WRAP_WORD);
+ g_object_unref (textbuffer);
+ gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tmpwidget), GTK_WRAP_WORD);
+ gtk_container_add (GTK_CONTAINER (box), tmpwidget);
+ gtk_widget_show (tmpwidget);
+ /* tooltip */
+ gimp_help_set_help_data (tmpwidget,
+ _("Enter other comment if you want."),
+ NULL);
+ gtk_label_set_mnemonic_widget (GTK_LABEL (label), tmpwidget);
+
+ /*
+ * all widget is prepared. Let's show dialog.
+ */
+ gtk_widget_show (dialog);
+
+ run = (gimp_dialog_run (GIMP_DIALOG (dialog)) == GTK_RESPONSE_OK);
+
+ gtk_widget_destroy (dialog);
+
+ return run;
+}
+
+/*
+ * callback function of gtk_entry for "copyright" and "license".
+ * "other" is processed by text_view_callback
+ */
+static void
+comment_entry_callback (GtkWidget *widget,
+ gchar **commentp)
+{
+ const gchar *text;
+
+ g_return_if_fail (commentp);
+
+ text = gtk_entry_get_text (GTK_ENTRY (widget));
+
+ /* This will not happen because sizeof(gtk_entry) < XCURSOR_COMMENT_MAX_LEN */
+ g_return_if_fail (strlen (text) <= XCURSOR_COMMENT_MAX_LEN);
+
+ g_free (*commentp);
+ *commentp = g_strdup (text);
+}
+
+static void
+text_view_callback (GtkTextBuffer *buffer,
+ gchar **commentp)
+{
+ GtkTextIter start_iter;
+ GtkTextIter end_iter;
+ gchar *text;
+
+ g_return_if_fail (commentp != NULL);
+
+ gtk_text_buffer_get_bounds (buffer, &start_iter, &end_iter);
+ text = gtk_text_buffer_get_text (buffer, &start_iter, &end_iter, FALSE);
+
+ if (strlen (text) > XCURSOR_COMMENT_MAX_LEN)
+ {
+ g_message (_("Comment is limited to %d characters."),
+ XCURSOR_COMMENT_MAX_LEN);
+
+ gtk_text_buffer_get_iter_at_offset (buffer, &start_iter,
+ XCURSOR_COMMENT_MAX_LEN - 1);
+ gtk_text_buffer_get_end_iter (buffer, &end_iter);
+
+ gtk_text_buffer_delete (buffer, &start_iter, &end_iter);
+ }
+ else
+ {
+ g_free (*commentp);
+ *commentp = g_strdup (text);
+ }
+}
+
+/**
+ * Set default hotspot based on hotspotRange.
+**/
+static gboolean
+load_default_hotspot (const gint32 image_ID,
+ GeglRectangle *hotspotRange)
+{
+
+ g_return_val_if_fail (hotspotRange, FALSE);
+
+ if ( /* if we cannot load hotspot correctly */
+ ! get_hotspot_from_parasite (image_ID) ||
+ /* ,or hostspot is out of range */
+ ! pix_in_region (xmcparas.x, xmcparas.y, hotspotRange))
+ {
+ /* then use top left point of hotspotRange as fallback. */
+ xmcparas.x = hotspotRange->x;
+ xmcparas.y = hotspotRange->y;
+ }
+
+ return TRUE;
+}
+
+/*
+ * 'save_image ()' - Save the specified image to X cursor file.
+ */
+
+static gboolean
+save_image (const gchar *filename,
+ gint32 image_ID,
+ gint32 drawable_ID,
+ gint32 orig_image_ID,
+ GError **error)
+{
+ FILE *fp; /* File pointer */
+ gboolean dimension_warn = FALSE; /* become TRUE if even one
+ * of the dimensions of the
+ * frames of the cursor is
+ * over
+ * MAX_BITMAP_CURSOR_SIZE */
+ gboolean size_warn = FALSE; /* become TRUE if even one
+ * of the nominal size of
+ * the frames is not
+ * supported by
+ * gnome-appearance-properties */
+ GRegex *re; /* used to get size and delay from
+ * framename */
+ XcursorComments *commentsp; /* pointer to comments */
+ XcursorImages *imagesp; /* pointer to images */
+ gint32 *layers; /* Array of layer */
+ gint32 *orig_layers; /* Array of layer of orig_image */
+ gint nlayers; /* Number of layers */
+ gchar *framename; /* framename of a layer */
+ GeglRectangle save_rgn; /* region to save */
+ gint layer_xoffset, layer_yoffset;
+ /* temporary buffer which store pixel data (guchar * bpp = guint32) */
+ guint32 pixelbuf[SQR (MAX_SAVE_DIMENSION)];
+ gint i, j; /* Looping vars */
+
+ /* This will be used in set_size_and_delay function later. To
+ * define this in that function is easy to read but place here to
+ * reduce overheads.
+ */
+ re = g_regex_new ("[(][ 0]*(\\d+)[ ]*(px|ms)[ ]*[)]",
+ G_REGEX_CASELESS | G_REGEX_OPTIMIZE,
+ 0,
+ NULL);
+
+ gimp_progress_init_printf (_("Saving '%s'"),
+ gimp_filename_to_utf8 (filename));
+
+ /*
+ * Open the file pointer.
+ */
+ DM_XMC ("Open the file pointer.\n");
+ fp = g_fopen (filename, "wb");
+ if (fp == NULL)
+ {
+ g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
+ _("Could not open '%s' for writing: %s"),
+ gimp_filename_to_utf8 (filename), g_strerror (errno));
+ return FALSE;
+ }
+
+ /* get layers */
+ orig_layers = gimp_image_get_layers (orig_image_ID, &nlayers);
+ layers = gimp_image_get_layers (image_ID, &nlayers);
+
+ /* create new XcursorImages. */
+ imagesp = XcursorImagesCreate (nlayers);
+ if (!imagesp)
+ {
+ DM_XMC ("Failed to XcursorImagesCreate!\n");
+ fclose (fp);
+ return FALSE;
+ }
+ imagesp->nimage = nlayers;
+
+ /* XcursorImages also have `name' member but it is not used as long as I know.
+ We leave it NULL here. */
+
+ /*
+ * Now we start to convert each layer to a XcurosrImage one by one.
+ */
+ for (i = 0; i < nlayers; i++)
+ {
+ GeglBuffer *buffer;
+ const Babl *format;
+ gint width;
+ gint height;
+
+ buffer = gimp_drawable_get_buffer (layers[nlayers - 1 - i]);
+
+ width = gegl_buffer_get_width (buffer);
+ height = gegl_buffer_get_height (buffer);
+
+ format = babl_format ("R'G'B'A u8");
+
+ /* get framename of this layer */
+ framename = gimp_item_get_name (layers[nlayers - 1 - i]);
+ /* get offset of this layer. */
+ gimp_drawable_offsets (layers[nlayers - 1 - i], &layer_xoffset, &layer_yoffset);
+
+ /*
+ * layer dimension check.
+ */
+ DM_XMC ("layer size check.\n");
+ /* We allow to save a cursor which dimensions are no more than
+ * MAX_SAVE_DIMENSION but after auto-cropping, we warn (only
+ * warn, don't stop) if dimension is over
+ * MAX_BITMAP_CURSOR_SIZE.
+ */
+ if (width > MAX_SAVE_DIMENSION)
+ {
+ g_set_error (error, 0, 0,
+ _("Frame '%s' is too wide. Please reduce to no more than %dpx."),
+ gimp_any_to_utf8 (framename, -1, NULL),
+ MAX_SAVE_DIMENSION);
+ fclose (fp);
+ return FALSE;
+ }
+
+ if (height > MAX_SAVE_DIMENSION)
+ {
+ g_set_error (error, 0, 0,
+ _("Frame '%s' is too high. Please reduce to no more than %dpx."),
+ gimp_any_to_utf8 (framename, -1, NULL),
+ MAX_SAVE_DIMENSION);
+ fclose (fp);
+ return FALSE;
+ }
+
+ if (height == 0 || width == 0)
+ {
+ g_set_error (error, 0, 0,
+ _("Width and/or height of frame '%s' is zero!"),
+ gimp_any_to_utf8 (framename, -1, NULL));
+ fclose (fp);
+ return FALSE;
+ }
+
+ if (xmcvals.crop) /* with auto-cropping */
+ {
+ /* get the region of auto-cropped area. */
+ DM_XMC ("get_cropped_region\n");
+ get_cropped_region (&save_rgn, buffer);
+
+ /* don't forget save_rgn's origin is not a entire image
+ * but a layer which we are doing on.
+ */
+
+ if (save_rgn.width == 0 || save_rgn.height == 0)
+ {/* perfectly transparent frames become 1x1px transparent pixel. */
+ DM_XMC ("get_cropped_region return 0.\n");
+ imagesp->images[i] = XcursorImageCreate (1, 1);
+ if (!imagesp->images[i])
+ {
+ DM_XMC ("Failed to XcursorImageCreate.\n");
+ fclose (fp);
+ return FALSE;
+ }
+ imagesp->images[i]->pixels[0] = 0x0;
+ imagesp->images[i]->xhot = 0;
+ imagesp->images[i]->yhot = 0;
+ set_size_and_delay (framename, &(imagesp->images[i]->size),
+ &(imagesp->images[i]->delay), re,
+ &size_warn);
+ continue;
+ }
+ /* OK save_rgn is not 0x0 */
+ /* is hotspot in save_rgn ? */
+ if (! pix_in_region (xmcparas.x - layer_xoffset,
+ xmcparas.y - layer_yoffset,
+ &save_rgn))
+ { /* if hotspot is not on save_rgn */
+ g_set_error (error, 0, 0,
+ _("Cannot export the cursor because the hot spot "
+ "is not on frame '%s'.\n"
+ "Try to change the hot spot position, "
+ "layer geometry or export without auto-crop."),
+ gimp_any_to_utf8 (framename, -1, NULL));
+ fclose (fp);
+ return FALSE;
+ }
+ }
+ else /* if without auto-cropping... */
+ {
+ /* set save_rgn for the case not to auto-crop */
+ save_rgn.width = width;
+ save_rgn.height = height;
+ save_rgn.x = 0;
+ save_rgn.y = 0;
+ }
+
+ /* We warn if the dimension of the layer is over MAX_BITMAP_CURSOR_SIZE. */
+ if (! dimension_warn)
+ {
+ if (save_rgn.width > MAX_BITMAP_CURSOR_SIZE ||
+ save_rgn.height > MAX_BITMAP_CURSOR_SIZE)
+ {
+ dimension_warn = TRUE;
+ /* actual warning is done after the cursor is successfully saved.*/
+ }
+ }
+ /*
+ * Create new XcursorImage.
+ */
+ DM_XMC ("create new xcursorimage.\twidth=%i\theight=%i\n",
+ save_rgn.width, save_rgn.height);
+ imagesp->images[i] = XcursorImageCreate (save_rgn.width, save_rgn.height);
+ /* Cursor width & height is automatically set by function */
+ /* XcursorImageCreate, so no need to set manually. */
+ if (!imagesp->images[i])
+ {
+ DM_XMC ("Failed to XcursorImageCreate.\n");
+ fclose (fp);
+ return FALSE;
+ }
+ /*
+ ** set images[i]'s xhot & yhot.
+ */
+ /* [Cropped layer's hotspot] =
+ [image's hotspot] - [layer's offset] - [save_rgn's offset]. */
+ DM_XMC ("xhot=%i\tsave_rgn->xoffset=%i\tlayer_xoffset=%i\n",
+ xmcparas.x, layer_xoffset, save_rgn.x);
+ DM_XMC ("yhot=%i\tsave_rgn->yoffset=%i\tlayer_yoffset=%i\n",
+ xmcparas.y, layer_yoffset, save_rgn.y);
+ imagesp->images[i]->xhot = xmcparas.x - layer_xoffset - save_rgn.x;
+ imagesp->images[i]->yhot = xmcparas.y - layer_yoffset - save_rgn.y;
+ DM_XMC ("images[%i]->xhot=%i\tyhot=%i\n", i,
+ imagesp->images[i]->xhot, imagesp->images[i]->yhot);
+
+ /*
+ * set images[i]->pixels
+ */
+ /* get image data to pixelbuf. */
+ gegl_buffer_get (buffer,
+ GEGL_RECTANGLE (save_rgn.x, save_rgn.y,
+ save_rgn.width, save_rgn.height), 1.0,
+ format, pixelbuf,
+ GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
+
+ /*convert pixel date to XcursorPixel. */
+ g_assert (save_rgn.width * save_rgn.height < SQR (MAX_SAVE_DIMENSION));
+ for (j = 0; j < save_rgn.width * save_rgn.height; j++)
+ {
+ imagesp->images[i]->pixels[j] = premultiply_alpha (pixelbuf[j]);
+ }
+
+ /*
+ * get back size & delay from framename.
+ */
+ set_size_and_delay (framename, &(imagesp->images[i]->size),
+ &(imagesp->images[i]->delay), re, &size_warn);
+
+ /*
+ * All property of this XcursorImage is loaded.
+ */
+
+ /* set the layer name of original image with the saved value */
+ g_free (framename);
+ framename = make_framename (imagesp->images[i]->size,
+ imagesp->images[i]->delay,
+ DISPLAY_DIGIT (imagesp->nimage),
+ error);
+ if (! framename)
+ {
+ fclose (fp);
+ return FALSE;
+ }
+
+ gimp_item_set_name (orig_layers[nlayers - 1 - i], framename);
+ g_free (framename);
+
+ g_object_unref (buffer);
+
+ gimp_progress_update ((i + 1) / imagesp->nimage);
+ }
+
+ gimp_progress_update (1.0);
+
+ /*
+ * comment parsing
+ */
+ commentsp = set_cursor_comments ();
+
+#ifdef XMC_DEBUG
+ DM_XMC ("imagesp->nimage=%i\tname=%s\n", imagesp->nimage, imagesp->name);
+ for (i = 0; i < imagesp->nimage; ++i)
+ {
+ DM_XMC ("\timages[%i]->size=%i\n\
+ \twidth=%i\n\
+ \theight=%i\n\
+ \txhot=%i\n\
+ \tyhot=%i\n\
+ \tdelay=%i\n\
+ \t*pixels=%p\n",
+ i,
+ imagesp->images[i]->size,
+ imagesp->images[i]->width,
+ imagesp->images[i]->height,
+ imagesp->images[i]->xhot,
+ imagesp->images[i]->yhot,
+ imagesp->images[i]->delay,
+ imagesp->images[i]->pixels);
+ }
+
+ if (commentsp)
+ {
+ for (i = 0; i < commentsp->ncomment; ++i)
+ {
+ DM_XMC ("comment type=%d\tcomment=%s\n",
+ commentsp->comments[i]->comment_type,
+ commentsp->comments[i]->comment);
+ }
+ }
+#endif
+
+ /*
+ * save cursor to file *fp.
+ */
+
+ if (commentsp)
+ {
+ if (! XcursorFileSave (fp, commentsp, imagesp))
+ {
+ DM_XMC ("Failed to XcursorFileSave.\t%p\t%p\t%p\n",
+ fp, commentsp, imagesp);
+ fclose (fp);
+ return FALSE;
+ }
+
+ }
+ else /* if no comments exist */
+ {
+ if (! XcursorFileSaveImages (fp, imagesp))
+ {
+ DM_XMC ("Failed to XcursorFileSaveImages.\t%p\t%p\n", fp, imagesp);
+ fclose (fp);
+ return FALSE;
+ }
+ }
+
+ /* actual warning about dimensions */
+ if (dimension_warn)
+ {
+ g_message (_("Your cursor was successfully exported but it contains one or "
+ "more frames whose width or height is more than %ipx, "
+ "a historical max dimension value for X bitmap cursors.\n"
+ "It might be unsupported by some environments."),
+ MAX_BITMAP_CURSOR_SIZE);
+ }
+ if (size_warn)
+ {
+ g_message (_("Your cursor was successfully exported but it contains one "
+ "or more frames whose nominal size is not supported by "
+ "GNOME settings.\n"
+ "You can satisfy it by checking \"Replace the size of all "
+ "frames...\" in the export dialog, or your cursor may not "
+ "appear in GNOME settings."));
+ }
+ /*
+ * Done with the file...
+ */
+ g_regex_unref (re);
+ DM_XMC ("fp=%p\n", fp);
+ fclose (fp);
+ DM_XMC ("%i frames written.\n", imagesp->nimage);
+ XcursorImagesDestroy (imagesp);
+ DM_XMC ("Xcursor destroyed.\n");
+ XcursorCommentsDestroy (commentsp); /* this is safe even if commentsp is NULL. */
+ gimp_progress_end ();
+
+ /* Save the comment back to the original image */
+ for (i = 0; i < 3; i++)
+ {
+ gimp_image_detach_parasite (orig_image_ID, parasiteName[i]);
+
+ if (xmcparas.comments[i])
+ {
+ if (! set_comment_to_pname (orig_image_ID,
+ xmcparas.comments[i], parasiteName[i]))
+ {
+ DM_XMC ("Failed to write back %ith comment to orig_image.\n", i);
+ }
+ }
+ }
+ /* Save hotspot back to the original image */
+ set_hotspot_to_parasite (orig_image_ID);
+
+ return TRUE;
+}
+
+static inline guint32
+separate_alpha (guint32 pixel)
+{
+ guint alpha, red, green, blue;
+ guint32 retval;
+
+#if G_BYTE_ORDER != G_LITTLE_ENDIAN
+ pixel = GUINT32_TO_LE (pixel);
+#endif
+
+ blue = pixel & 0xff;
+ green = (pixel>>8) & 0xff;
+ red = (pixel>>16) & 0xff;
+ alpha = (pixel>>24) & 0xff;
+
+ if (alpha == 0)
+ return 0;
+
+ /* resume separate alpha data. */
+ red = MIN (red * 255 / alpha, 255);
+ blue = MIN (blue * 255 / alpha, 255);
+ green = MIN (green * 255 / alpha, 255);
+
+ retval = red + (green<<8) + (blue<<16) + (alpha<<24);
+
+#if G_BYTE_ORDER != G_LITTLE_ENDIAN
+ pixel = GUINT32_FROM_LE (pixel);
+#endif
+
+ return retval;
+}
+
+static inline guint32
+premultiply_alpha (guint32 pixel)
+{
+ guint alpha, red, green, blue;
+ guint32 retval;
+
+#if G_BYTE_ORDER != G_LITTLE_ENDIAN
+ pixel = GUINT32_TO_LE (pixel);
+#endif
+
+ red = pixel & 0xff;
+ green = (pixel >> 8) & 0xff;
+ blue = (pixel >> 16) & 0xff;
+ alpha = (pixel >> 24) & 0xff;
+
+ /* premultiply alpha
+ (see "premultiply_data" function at line 154 of xcursorgen.c) */
+ red = div_255 (red * alpha);
+ green = div_255 (green * alpha);
+ blue = div_255 (blue * alpha);
+
+ retval = blue + (green << 8) + (red << 16) + (alpha << 24);
+
+#if G_BYTE_ORDER != G_LITTLE_ENDIAN
+ pixel = GUINT32_FROM_LE (pixel);
+#endif
+
+ return retval;
+}
+
+/* set comments to cursor from xmcparas.comments.
+ * don't forget to XcursorCommentsDestroy returned pointer later.
+ */
+static XcursorComments *
+set_cursor_comments (void)
+{
+ gint i;
+ guint gcomlen, arraylen;
+ GArray *xcCommentsArray;
+ XcursorComment *(xcCommentp[3]) = {NULL,};
+ XcursorComments *xcCommentsp;
+
+ xcCommentsArray = g_array_new (FALSE, FALSE, sizeof (XcursorComment *));
+
+ for (i = 0; i < 3; ++i)
+ {
+ if (xmcparas.comments[i])
+ {
+ gcomlen = strlen (xmcparas.comments[i]);
+ if (gcomlen > 0)
+ {
+ xcCommentp[i] = XcursorCommentCreate (i + 1, gcomlen);
+ /* first argument of XcursorCommentCreate is comment_type
+ defined in Xcursor.h as enumerator.
+ i + 1 is appropriate when we dispose parasiteName before MAIN(). */
+ if (!xcCommentp[i])
+ {
+ g_warning ("Cannot create xcCommentp[%i]\n", i);
+ return NULL;
+ }
+ else
+ {
+ g_stpcpy (xcCommentp[i]->comment, xmcparas.comments[i]);
+ g_array_append_val (xcCommentsArray, xcCommentp[i]);
+ }
+ }
+ }
+ }
+
+ arraylen = xcCommentsArray->len;
+
+ if (arraylen == 0)
+ return NULL;
+
+ xcCommentsp = XcursorCommentsCreate (arraylen);
+ xcCommentsp->ncomment = arraylen;
+
+ for (i = 0; i < arraylen; ++i)
+ {
+ xcCommentsp->comments[i] =
+ g_array_index (xcCommentsArray, XcursorComment* ,i);
+ }
+
+ return xcCommentsp;
+}
+
+/* Load xmcparas.comments from three parasites named as "xmc-copyright",
+ * "xmc-license","gimp-comment".
+ * This alignment sequence is depends on the definition of comment_type
+ * in Xcursor.h .
+ * Don't forget to g_free each element of xmcparas.comments later.
+ */
+static void
+load_comments (const gint32 image_ID)
+{
+ gint i;
+
+ g_return_if_fail (image_ID != -1);
+
+ for (i = 0; i < 3; ++i)
+ xmcparas.comments[i] = get_comment_from_pname (image_ID, parasiteName[i]);
+}
+
+/* Set content to a parasite named as pname. if parasite already
+ * exists, append the new one to the old one with "\n"
+ */
+static gboolean
+set_comment_to_pname (const gint32 image_ID,
+ const gchar *content,
+ const gchar *pname)
+{
+ gboolean ret = FALSE;
+ gchar *tmpstring, *joind;
+ GimpParasite *parasite;
+
+ g_return_val_if_fail (image_ID != -1, FALSE);
+ g_return_val_if_fail (content, FALSE);
+
+ parasite = gimp_image_get_parasite (image_ID, pname);
+ if (! parasite)
+ {
+ parasite = gimp_parasite_new (pname, GIMP_PARASITE_PERSISTENT,
+ strlen (content) + 1, content);
+ }
+ else
+ {
+ tmpstring = g_strndup (gimp_parasite_data (parasite),
+ gimp_parasite_data_size (parasite));
+ gimp_parasite_free (parasite);
+ joind = g_strjoin ("\n", tmpstring, content, NULL);
+ g_free (tmpstring);
+ parasite = gimp_parasite_new (pname, GIMP_PARASITE_PERSISTENT,
+ strlen (joind) + 1, joind);
+ g_free (joind);
+ }
+
+ if (parasite)
+ {
+ ret = gimp_image_attach_parasite (image_ID, parasite);
+ gimp_parasite_free (parasite);
+ }
+
+ return ret;
+}
+
+/* get back comment from parasite name, don't forget to call
+ * g_free(returned pointer) later
+ */
+static gchar *
+get_comment_from_pname (const gint32 image_ID,
+ const gchar *pname)
+{
+ gchar *string = NULL;
+ GimpParasite *parasite;
+ glong length;
+
+ g_return_val_if_fail (image_ID != -1, NULL);
+
+ parasite = gimp_image_get_parasite (image_ID, pname);
+ length = gimp_parasite_data_size (parasite);
+
+ if (parasite)
+ {
+ if (length > XCURSOR_COMMENT_MAX_LEN)
+ {
+ length = XCURSOR_COMMENT_MAX_LEN;
+ g_message (_("The parasite \"%s\" is too long for an X cursor "
+ "comment. It was cut off to fit."),
+ gimp_any_to_utf8 (pname, -1,NULL));
+ }
+
+ string = g_strndup (gimp_parasite_data (parasite), length);
+ gimp_parasite_free (parasite);
+ }
+
+ return string;
+}
+
+/* Set hotspot to "hot-spot" parasite which format is common with that
+ * of file-xbm.
+ */
+static gboolean
+set_hotspot_to_parasite (gint32 image_ID)
+{
+ gboolean ret = FALSE;
+ gchar *tmpstr;
+ GimpParasite *parasite;
+
+ g_return_val_if_fail (image_ID != -1, FALSE);
+
+ tmpstr = g_strdup_printf ("%d %d", xmcparas.x, xmcparas.y);
+ parasite = gimp_parasite_new ("hot-spot",
+ GIMP_PARASITE_PERSISTENT,
+ strlen (tmpstr) + 1,
+ tmpstr);
+ g_free (tmpstr);
+
+ if (parasite)
+ {
+ ret = gimp_image_attach_parasite (image_ID, parasite);
+ gimp_parasite_free (parasite);
+ }
+
+ return ret;
+}
+
+/* Get back xhot & yhot from "hot-spot" parasite.
+ * If succeed, hotspot coordinate is set to xmcparas.x, xmcparas.y and
+ * return TRUE.
+ * If "hot-spot" is not found or broken, return FALSE.
+ */
+static gboolean
+get_hotspot_from_parasite (gint32 image_ID)
+{
+ GimpParasite *parasite;
+
+ g_return_val_if_fail (image_ID != -1, FALSE);
+
+ DM_XMC ("function: getHotsopt\n");
+
+ parasite = gimp_image_get_parasite (image_ID, "hot-spot");
+ if (!parasite) /* cannot find a parasite named "hot-spot". */
+ {
+ return FALSE;
+ }
+
+ if (sscanf (gimp_parasite_data (parasite),
+ "%i %i", &xmcparas.x, &xmcparas.y) < 2)
+ { /*cannot load hotspot.(parasite is broken?) */
+ return FALSE;
+ }
+
+ /*OK, hotspot is set to *xhotp & *yhotp. */
+ return TRUE;
+}
+
+/* Set size to sizep, delay to delayp from drawable's framename.
+ */
+static void
+set_size_and_delay (const gchar *framename,
+ guint32 *sizep,
+ guint32 *delayp,
+ GRegex *re,
+ gboolean *size_warnp)
+{
+ guint32 size = 0;
+ guint32 delay = 0;
+ gchar *digits = NULL;
+ gchar *suffix = NULL;
+ GMatchInfo *info = NULL;
+
+ g_return_if_fail (framename);
+ g_return_if_fail (sizep);
+ g_return_if_fail (delayp);
+ g_return_if_fail (re);
+
+ DM_XMC ("function: set_size_and_delay\tframename=%s\n", framename);
+
+ /* re is defined at the start of save_image() as
+ [(] : open parenthesis
+ [ ]* : ignore zero or more spaces
+ (\\d+) : the number we want to get out
+ [ ]* : ignore zero or more spaces
+ (px|ms) : whether "px"(size) or "ms"(delay)
+ [ ]* : ignore zero or more spaces
+ [)] : close parenthesis
+ This is intended to match for the animation-play plug-in. */
+
+ g_regex_match (re, framename, 0, &info);
+
+ while (g_match_info_matches (info))
+ {
+ digits = g_match_info_fetch (info, 1);
+ suffix = g_match_info_fetch (info, 2);
+
+ if (g_ascii_strcasecmp (suffix, "px") == 0)
+ {
+ if (!size) /* substitute it only for the first time */
+ {
+ if (strlen (digits) > 8) /* too large number should be clamped */
+ {
+ g_message (_("Your cursor was successfully exported but it contains one or "
+ "more frames whose size is over 8 digits.\n"
+ "We clamped it to %dpx. You should check the exported cursor."),
+ MAX_BITMAP_CURSOR_SIZE);
+ size = MAX_BITMAP_CURSOR_SIZE;
+ }
+ else
+ {
+ size = atoi (digits);
+ }
+ }
+ }
+ else /* suffix is "ms" */
+ {
+ if (!delay) /* substitute it only for the first time */
+ {
+ if (strlen (digits) > 8) /* too large number should be clamped */
+ delay = CURSOR_MAX_DELAY;
+ else
+ delay = MIN (CURSOR_MAX_DELAY, atoi (digits));
+ }
+ }
+
+ g_free (digits);
+ g_free (suffix);
+
+ g_match_info_next (info, NULL);
+ }
+
+ g_match_info_free (info);
+
+ /* if size is not set, or size_replace is TRUE, set default size
+ * (which was chosen in save dialog) */
+ if (size == 0 || xmcvals.size_replace == TRUE)
+ {
+ size = xmcvals.size;
+ }
+ else if (! *size_warnp &&
+ size != 12 && size != 16 && size != 24 && size != 32 &&
+ size != 36 && size != 40 && size != 48 && size != 64 &&
+ size != 96)
+ { /* if the size is different from these values, we warn about it after
+ successfully saving because gnome-appearance-properties only support
+ them. */
+ *size_warnp = TRUE;
+ }
+
+ *sizep = size;
+
+ /* if delay is not set, or delay_replace is TRUE, set default delay
+ * (which was chosen in save dialog) */
+ if (delay == 0 || xmcvals.delay_replace == TRUE)
+ {
+ delay = xmcvals.delay;
+ }
+
+ *delayp = delay;
+
+ DM_XMC ("set_size_and_delay return\tsize=%i\tdelay=%i\n", size, delay);
+}
+
+/* Return framename as format: "([x]px)_[i] ([t]ms) (replace)"
+ * where [x] is nominal size, [t] is delay passed as argument respectively,
+ * and [i] is an index separately counted by [x].
+ * This format is compatible with "animation-play" plug-in.
+ * Don't forget to g_free returned framename later.
+ */
+static gchar *
+make_framename (guint32 size,
+ guint32 delay,
+ guint indent,
+ GError **errorp)
+{
+ static struct
+ {
+ guint32 size;
+ guint count;
+ } Counter[MAX_SIZE_NUM + 1] = {{0,}};
+
+ int i; /* loop index */
+
+ /* don't pass 0 for size. */
+ g_return_val_if_fail (size > 0, NULL);
+
+ /* "count" member of Counter's element means how many time corresponding
+ "size" is passed to this function. The size member of the last element
+ of Counter must be 0, so Counter can have MAX_SIZE_NUM elements at most.
+ This is not a smart way but rather simple than using dynamic method. */
+
+ for (i = 0; Counter[i].size != size; ++i)
+ {
+ if (Counter[i].size == 0) /* the end of Counter elements */
+ {
+ if (i > MAX_SIZE_NUM)
+ {
+ g_set_error (errorp, 0, 0,
+ /* translators: the %i is *always* 8 here */
+ _("Sorry, this plug-in cannot handle a cursor "
+ "which contains over %i different nominal sizes."),
+ MAX_SIZE_NUM);
+ return NULL;
+ }
+ else /* append new element which "size" is given value. */
+ {
+ Counter[i].size = size;
+ break;
+ }
+ }
+ }
+
+ Counter[i].count += 1;
+
+ return g_strdup_printf ("(%dpx)_%0*d (%dms) (replace)", size, indent,
+ Counter[i].count, delay);
+}
+
+/* Get the region which is maintained when auto-crop.
+ */
+static void
+get_cropped_region (GeglRectangle *return_rgn,
+ GeglBuffer *buffer)
+{
+ gint width = gegl_buffer_get_width (buffer);
+ gint height = gegl_buffer_get_height (buffer);
+ guint32 *buf = g_malloc (MAX (width, height) * sizeof (guint32));
+ const Babl *format = babl_format ("R'G'B'A u8");
+ guint i, j;
+
+ g_return_if_fail (GEGL_IS_BUFFER (buffer));
+
+ DM_XMC ("function:get_cropped_region\n");
+
+ DM_XMC ("getTrim:\tMAX=%i\tpr->w=%i\tpr->h=%i\n", sizeof (buf)/4, pr->w, pr->h);
+
+ /* find left border. */
+ for (i = 0; i < width; ++i)
+ {
+ DM_XMC ("i=%i width=%i\n", i, width);
+
+ gegl_buffer_get (buffer, GEGL_RECTANGLE (i, 0, 1, height), 1.0,
+ format, buf,
+ GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
+
+ for (j = 0; j < height; ++j)
+ {
+ if (pix_is_opaque (buf[j])) /* if a opaque pixel exist. */
+ {
+ return_rgn->x = i;
+ goto find_right;
+ }
+ }
+ }
+
+ /* pr has no opaque pixel. */
+ return_rgn->width = 0;
+ return;
+
+ /* find right border. */
+ find_right:
+ for (i = 0; i < width ; ++i)
+ {
+ DM_XMC ("width-1-i=%i height=%i\n", width - 1 - i, height);
+
+ gegl_buffer_get (buffer, GEGL_RECTANGLE (width - 1 - i, 0, 1, height), 1.0,
+ format, buf,
+ GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
+
+ for (j = 0; j < height; ++j)
+ {
+ if (pix_is_opaque (buf[j])) /* if a opaque pixel exist. */
+ {
+ return_rgn->width = width - i - return_rgn->x;
+ goto find_top;
+ }
+ }
+ }
+ g_return_if_reached ();
+
+ /* find top border. */
+ find_top:
+ for (j = 0; j < height; ++j)
+ {
+ DM_XMC ("j=%i width=%i\n", j, width);
+
+ gegl_buffer_get (buffer, GEGL_RECTANGLE (0, j, width, 1), 1.0,
+ format, buf,
+ GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
+
+ for (i = 0; i < width; ++i)
+ {
+ if (pix_is_opaque (buf[i])) /* if a opaque pixel exist. */
+ {
+ return_rgn->y = j;
+ goto find_bottom;
+ }
+ }
+ }
+
+ g_return_if_reached ();
+
+ /* find bottom border. */
+ find_bottom:
+ for (j = 0; j < height; ++j)
+ {
+ DM_XMC ("height-1-j=%i width=%i\n", height - 1 - j, width);
+
+ gegl_buffer_get (buffer, GEGL_RECTANGLE (0, height - 1 - j, width, 1), 1.0,
+ format, buf,
+ GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
+
+ for (i = 0; i < width; ++i)
+ {
+ if (pix_is_opaque (buf[i])) /* if a opaque pixel exist. */
+ {
+ return_rgn->height = height - j - return_rgn->y;
+ goto end_trim;
+ }
+ }
+ }
+
+ g_return_if_reached ();
+
+ end_trim:
+ DM_XMC ("width=%i\theight=%i\txoffset=%i\tyoffset=%i\n",
+ return_rgn->width, return_rgn->height,
+ return_rgn->x, return_rgn->y);
+
+ g_free (buf);
+}
+
+/* Return true if alpha of pix is not 0.
+ */
+static inline gboolean
+pix_is_opaque (guint32 pix)
+{
+#if G_BYTE_ORDER != G_LITTLE_ENDIAN
+ pix = GUINT32_TO_LE (pix);
+#endif
+
+ return ((pix >> 24) != 0);
+}
+
+/* Get the intersection of the all layers of the image specified by image_ID.
+ * if the intersection is empty return NULL.
+ * don't forget to g_free returned pointer later.
+ */
+static GeglRectangle *
+get_intersection_of_frames (gint32 image_ID)
+{
+ GeglRectangle *iregion;
+ gint i;
+ gint32 x1 = G_MININT32, x2 = G_MAXINT32;
+ gint32 y1 = G_MININT32, y2 = G_MAXINT32;
+ gint32 x_off, y_off;
+ gint nlayers;
+ gint *layers;
+
+ g_return_val_if_fail (image_ID != -1, FALSE);
+
+ layers = gimp_image_get_layers (image_ID, &nlayers);
+
+ for (i = 0; i < nlayers; ++i)
+ {
+ if (! gimp_drawable_offsets (layers[i], &x_off, &y_off))
+ return NULL;
+
+ x1 = MAX (x1, x_off);
+ y1 = MAX (y1, y_off);
+ x2 = MIN (x2, x_off + gimp_drawable_width (layers[i]) - 1);
+ y2 = MIN (y2, y_off + gimp_drawable_height (layers[i]) - 1);
+ }
+
+ if (x1 > x2 || y1 > y2)
+ return NULL;
+
+ /* OK intersection exists. */
+ iregion = g_new (GeglRectangle, 1);
+ iregion->x = x1;
+ iregion->y = y1;
+ iregion->width = x2 - x1 + 1;
+ iregion->height = y2 - y1 + 1;
+
+ return iregion;
+}
+
+/* If (x,y) is in xmcrp, return TRUE.
+ */
+static gboolean
+pix_in_region (gint32 x,
+ gint32 y,
+ GeglRectangle *xmcrp)
+{
+ g_return_val_if_fail (xmcrp, FALSE);
+
+ if (x < xmcrp->x || y < xmcrp->y ||
+ x >= xmcrp->x + xmcrp->width || y >= xmcrp->y + xmcrp->height)
+ return FALSE;
+ else
+ return TRUE;
+}
+
+/**
+ * Find out xhot, yhot, width and height of the Xcursor specified by xcIs.
+ * Use NULL for the value you don't want to return.
+**/
+static void
+find_hotspots_and_dimensions (XcursorImages *xcIs,
+ gint32 *xhotp,
+ gint32 *yhotp,
+ gint32 *widthp,
+ gint32 *heightp)
+{
+ gint32 dw, dh; /* the distance between hotspot and right(bottom) border */
+ gint32 max_xhot;
+ gint32 max_yhot; /* the maximum value of xhot(yhot) */
+ gint i;
+
+ g_return_if_fail (xcIs);
+
+ max_xhot = max_yhot = dw = dh = 0;
+
+ for (i = 0; i < xcIs->nimage; ++i)
+ {
+ /* xhot of entire image is the maximum value of xhot of all frames */
+ max_xhot = MAX (xcIs->images[i]->xhot, max_xhot);
+ /* same for yhot */
+ max_yhot = MAX (xcIs->images[i]->yhot, max_yhot);
+ /* the maximum distance between right border and xhot */
+ dw = MAX (dw, xcIs->images[i]->width - xcIs->images[i]->xhot);
+ /* the maximum distance between bottom border and yhot */
+ dh = MAX (dh, xcIs->images[i]->height - xcIs->images[i]->yhot);
+ }
+
+ if (xhotp)
+ *xhotp = max_xhot;
+ if (yhotp)
+ *yhotp = max_yhot;
+ if (widthp)
+ *widthp = dw + max_xhot;
+ if (heightp)
+ *heightp = dh + max_yhot;
+}