[gimp] plug-ins: add X11 Mouse Cursor plug-in



commit 27cec2c3f2fcb3a3aa728283bac52bf534c670f9
Author: Takeshi Matsuyama <tksmashiw gmail com>
Date:   Wed May 27 12:04:35 2009 +0900

    plug-ins: add X11 Mouse Cursor plug-in
    
    This commit adds a plug-in to read and write X11 mouse cursor files.
---
 INSTALL                        |    5 +
 configure.ac                   |   24 +
 plug-ins/common/.gitignore     |    1 +
 plug-ins/common/Makefile.am    |   19 +
 plug-ins/common/file-xmc.c     | 2320 ++++++++++++++++++++++++++++++++++++++++
 plug-ins/common/plugin-defs.pl |    1 +
 6 files changed, 2370 insertions(+), 0 deletions(-)

diff --git a/INSTALL b/INSTALL
index 3dff3ed..86a9744 100644
--- a/INSTALL
+++ b/INSTALL
@@ -253,6 +253,11 @@ These are:
   --without-script-fu.  If for some reason you don't want to build the
      Script-Fu plug-in, you can use --without-script-fu.
 
+  --without-xmc.  The X11 Mouse Cursor(XMC) plug-in needs libXcursor
+     and configure checks for its presense. If for some reason you
+     don't want to build the XMC plug-in even though the library is
+     installed, use --without-xmc to disable it explicitly.
+
 
 The `make' command builds several things:
  - A bunch of public libraries in the directories starting with 'libgimp'.
diff --git a/configure.ac b/configure.ac
index c2ee7ee..bbb3b3e 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1431,6 +1431,29 @@ AC_SUBST(LCMS)
 AM_CONDITIONAL(HAVE_LCMS, test "x$have_lcms" = xyes)
 
 
+######################
+# Check for libXcursor
+######################
+
+AC_ARG_WITH(xmc,[  --without-xmc           build without Xcursor support])
+
+have_xmc=no
+if test "x$with_xmc" != xno; then
+  have_xmc=yes
+  PKG_CHECK_MODULES(XMC, xcursor,
+    FILE_XMC='file-xmc$(EXEEXT)',
+    have_xmc="no (libXcursor not found)")
+else
+   have_xmc="no (libXcursor support disabled)"
+fi
+
+if test "x$have_xmc" = xyes; then
+  MIME_TYPES="$MIME_TYPES;image/x-xcursor"
+fi
+
+AC_SUBST(FILE_XMC)
+
+
 ################
 # Check for alsa
 ################
@@ -2104,6 +2127,7 @@ Optional Plug-Ins:
   Windows ICO          $have_libpng
   WMF:                 $have_libwmf
   XJT:                 $have_xjt
+  X11 Mouse Cursor:    $have_xmc
   XPM:                 $have_libxpm
 
 Plug-In Features:
diff --git a/plug-ins/common/.gitignore b/plug-ins/common/.gitignore
index 639b5c0..38fb925 100644
--- a/plug-ins/common/.gitignore
+++ b/plug-ins/common/.gitignore
@@ -80,6 +80,7 @@
 /file-tiff-save
 /file-wmf
 /file-xbm
+/file-xmc
 /file-xpm
 /file-xwd
 /film
diff --git a/plug-ins/common/Makefile.am b/plug-ins/common/Makefile.am
index a9afec1..6659997 100644
--- a/plug-ins/common/Makefile.am
+++ b/plug-ins/common/Makefile.am
@@ -115,6 +115,7 @@ libexec_PROGRAMS = \
 	$(FILE_TIFF_SAVE) \
 	$(FILE_WMF) \
 	file-xbm \
+	$(FILE_XMC) \
 	$(FILE_XPM) \
 	file-xwd \
 	film \
@@ -194,6 +195,7 @@ EXTRA_PROGRAMS = \
 	file-tiff-load \
 	file-tiff-save \
 	file-wmf \
+	file-xmc \
 	file-xpm \
 	lcms \
 	mail \
@@ -1437,6 +1439,23 @@ file_xbm_LDADD = \
 	$(RT_LIBS)		\
 	$(INTLLIBS)
 
+file_xmc_SOURCES = \
+	file-xmc.c
+
+file_xmc_LDADD = \
+	$(libgimpui)		\
+	$(libgimpwidgets)	\
+	$(libgimpmodule)	\
+	$(libgimp)		\
+	$(libgimpmath)		\
+	$(libgimpconfig)	\
+	$(libgimpcolor)		\
+	$(libgimpbase)		\
+	$(GTK_LIBS)		\
+	$(XMC_LIBS)		\
+	$(RT_LIBS)		\
+	$(INTLLIBS)
+
 file_xpm_SOURCES = \
 	file-xpm.c
 
diff --git a/plug-ins/common/file-xmc.c b/plug-ins/common/file-xmc.c
new file mode 100644
index 0000000..4b95f5d
--- /dev/null
+++ b/plug-ins/common/file-xmc.c
@@ -0,0 +1,2320 @@
+/*
+ *   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 <http://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/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"
+/* save without file extension "xmc" */
+#define SAVE_PROC2             "file-xmc-save2"
+
+#define PLUG_IN_BINARY         "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.
+ */
+#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,
+                                            GimpParamRegion  *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,
+                                            GimpParamRegion  *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        (GimpParamRegion   *retrun_rgn,
+                                            GimpPixelRgn      *pr);
+
+static inline gboolean pix_is_opaque       (guint32            pix);
+
+static GimpParamRegion *get_intersection_of_frames (gint32     image_ID);
+
+static gboolean   pix_in_region            (gint32             x,
+                                            gint32             y,
+                                            GimpParamRegion   *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 save"             },
+      { GIMP_PDB_STRING,   "filename",     "The name of the file to save 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 hotspot"       },
+      { GIMP_PDB_INT32,    "y_hot",        "Y-coordinate of hotspot\n"
+                                           "Use (-1, -1) to keep original hotspot."},
+      { 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);
+
+#define GIMP_INSTALL_SAVE_PROC(mProc, mAdd) \
+        gimp_install_procedure ((mProc),\
+        "Saves files of X11 cursor file",\
+        "This plug-in saves X11 Mouse Cursor (XMC) files"\
+        #mAdd,\
+        "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_INSTALL_SAVE_PROC(SAVE_PROC, .);
+  GIMP_INSTALL_SAVE_PROC(SAVE_PROC2, \nwithout file extension.);
+
+  gimp_register_file_handler_mime (SAVE_PROC, XCURSOR_MIME_TYPE);
+  gimp_register_save_handler (SAVE_PROC, XCURSOR_EXTENSION, "");
+  gimp_register_file_handler_mime (SAVE_PROC2, XCURSOR_MIME_TYPE);
+}
+
+/*
+ * '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[2];
+  GimpRunMode       run_mode;
+  GimpPDBStatusType status = GIMP_PDB_SUCCESS;
+  gint32            image_ID;
+  gint32            drawable_ID;
+  gint32            orig_image_ID;
+  GimpExportReturn  export = GIMP_EXPORT_CANCEL;
+  GimpParamRegion  *hotspotRange = NULL;
+  gint              i;
+  gint32            width, height, num_layers;
+  GError           *error = NULL;
+
+  INIT_I18N ();
+
+  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 || strcmp (name, SAVE_PROC2) == 0)
+    {
+      DM_XMC("run: save %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 hotspot!\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 xmcparas.comments from parasite. */
+          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 xmcparas.comments from parasites.*/
+              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 xmcparas.comments from parasite. */
+          load_comments (image_ID);
+          /* load hotspot from parasite */
+          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);
+      /* free hotspotRange */
+      g_free (hotspotRange);
+      /* free xmcparas.comments */
+      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)
+{
+  gint i, j;                    /* Looping var */
+  gint img_width, img_height;   /* dimensions of the image */
+  FILE *fp;                     /* File pointer */
+  gint32 image_ID;              /* Image */
+  gint32 layer_ID;              /* Layer */
+  GimpDrawable *drawable;       /* Drawable for layer */
+  GimpPixelRgn pixel_rgn;       /* Pixel region for layer */
+  XcursorComments  *commentsp;  /* pointer to comments */
+  XcursorImages    *imagesp;    /* pointer to images*/
+  guint32 delay;                /* use guint32 instead CARD32(defined in X11/Xmd.h)*/
+  gchar *framename;             /* name of layer */
+  guint32 *tmppixel;            /* pixel data (guchar * bpp = guint32) */
+
+  /* initialize image here, thus avoiding compiler warnings */
+
+  image_ID = -1;
+
+  /*
+   * 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));
+      return -1;
+    }
+
+  gimp_progress_init_printf (_("Opening '%s'"),
+                             gimp_filename_to_utf8 (filename));
+
+  /*
+  ** 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,
+                       _("The width of frame %d of '%s' is too big for X cursor."),
+                       i + 1, gimp_filename_to_utf8 (filename));
+          return -1;
+        }
+      if (imagesp->images[i]->height > MAX_LOAD_DIMENSION)
+        {
+          g_set_error (error, 0, 0,
+                       _("The height of frame %d of '%s' is too big for X cursor."),
+                       i + 1, gimp_filename_to_utf8 (filename));
+          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);
+
+  /* create new image! */
+  image_ID = gimp_image_new (img_width, img_height, GIMP_RGB);
+
+  /* set filename */
+  gimp_image_set_filename (image_ID, filename);
+
+  if (! set_hotspot_to_parasite (image_ID))
+    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++)
+    {
+      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)
+        return -1;
+
+      layer_ID = gimp_layer_new (image_ID, framename, imagesp->images[i]->width,
+              imagesp->images[i]->height, GIMP_RGBA_IMAGE, 100, GIMP_NORMAL_MODE);
+      gimp_image_add_layer (image_ID, layer_ID, 0);
+
+      /* Adjust layer position to let hotspot sit on the same point. */
+      gimp_layer_translate (layer_ID,
+                            xmcparas.x - imagesp->images[i]->xhot,
+                            xmcparas.y - imagesp->images[i]->yhot);
+
+      g_free (framename);
+
+      /*
+       * Get the drawable and set the pixel region for our load...
+       */
+
+      drawable = gimp_drawable_get (layer_ID);
+
+      gimp_pixel_rgn_init (&pixel_rgn, drawable, 0, 0, drawable->width,
+                           drawable->height, TRUE, FALSE);
+
+      /* set color to each pixel */
+      for (j = 0; j < drawable->width * drawable->height; j++)
+        {
+          tmppixel[j] = separate_alpha (imagesp->images[i]->pixels[j]) ;
+        }
+      /* set pixel */
+      gimp_pixel_rgn_set_rect (&pixel_rgn, (guchar *)tmppixel,
+                               0, 0, drawable->width, drawable->height);
+
+      gimp_progress_update ( (i + 1) / imagesp->nimage);
+    }
+  /* free temporary buffer */
+  g_free(tmppixel);
+
+  /*
+   * 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);
+              return -1;
+            }
+        }
+    }
+  DM_XMC("Comment parsing done.\n");
+  XcursorImagesDestroy (imagesp);
+  XcursorCommentsDestroy (commentsp);
+  fclose (fp);
+
+  /*
+   * Update the display...
+   */
+
+  gimp_progress_end ();
+  gimp_drawable_flush (drawable);
+  gimp_drawable_detach (drawable);
+
+  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. */
+
+  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);
+
+  gint i;                       /* Looping var */
+  guint32 ntoc = 0;             /* the number of table of contents */
+  gint sel_num = -1;            /* the index of selected image chunk */
+  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;              /* File pointer */
+  gint32 image_ID = -1;         /* Image */
+  gint32 layer_ID;              /* Layer */
+  GimpDrawable *drawable;       /* Drawable for layer */
+  GimpPixelRgn pixel_rgn;       /* Pixel region for layer */
+  guint32 *tmppixel;            /* pixel data (guchar * bpp = guint32) */
+
+  *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)
+  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 = ABS(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));
+      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,
+                   _("The width of '%s' is too big for X cursor."),
+                   gimp_filename_to_utf8 (filename));
+      return -1;
+    }
+  if (*thumb_height > MAX_LOAD_DIMENSION)
+    {
+      g_set_error (error, 0, 0,
+                   _("The height of '%s' is too big for X cursor."),
+                   gimp_filename_to_utf8 (filename));
+      return -1;
+    }
+
+  /*
+   *  create new image!
+   */
+  image_ID = gimp_image_new (xcIs->images[sel_num]->width,
+                             xcIs->images[sel_num]->height, GIMP_RGB);
+
+  layer_ID = gimp_layer_new (image_ID, NULL,
+                             xcIs->images[sel_num]->width,
+                             xcIs->images[sel_num]->height,
+                             GIMP_RGBA_IMAGE, 100,
+                             GIMP_NORMAL_MODE);
+
+  gimp_image_add_layer (image_ID, layer_ID, 0);
+
+  /*
+   * Get the drawable and set the pixel region for our load...
+   */
+
+  drawable = gimp_drawable_get (layer_ID);
+
+  gimp_pixel_rgn_init (&pixel_rgn, drawable, 0, 0,
+                       xcIs->images[sel_num]->width,
+                       xcIs->images[sel_num]->height,
+                       TRUE, FALSE);
+
+  /* Temporary buffer */
+  tmppixel = g_new (guint32,
+                    xcIs->images[sel_num]->width * xcIs->images[sel_num]->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 < xcIs->images[sel_num]->width * xcIs->images[sel_num]->height; i++)
+    {
+      tmppixel[i] = READ32 (fp, error)
+      /* get back separate alpha */
+      tmppixel[i] = separate_alpha (tmppixel[i]);
+    }
+
+  /* set pixel */
+  gimp_pixel_rgn_set_rect (&pixel_rgn, (guchar *)tmppixel, 0, 0,
+                           drawable->width, drawable->height);
+  /* free tmppixel */
+  g_free(tmppixel);
+  g_free (positions);
+  fclose (fp);
+
+  gimp_drawable_flush (drawable);
+  gimp_drawable_detach (drawable);
+
+  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, GimpParamRegion *hotspotRange)
+{
+  gint x1, x2, y1, y2;
+  GtkWidget      *dialog;
+  GtkWidget      *frame;
+  GtkWidget      *table;
+  GtkWidget      *box;
+  GtkObject      *adjustment;
+  GtkWidget      *alignment;
+  GtkWidget      *tmpwidget;
+  GtkWidget      *label;
+  GtkTextBuffer  *textbuffer;
+  GValue          val = {0,};
+  gboolean        run;
+
+  g_value_init (&val, G_TYPE_DOUBLE);
+  dialog = gimp_dialog_new (_("Save as X11 Mouse Cursor"), /* title */
+                            PLUG_IN_BINARY,                 /* role */
+                            NULL, 0,                /* parent flags */
+                            gimp_standard_help_func,   /* help func */
+                            SAVE_PROC,                   /* help id */
+
+                            GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
+                            GTK_STOCK_SAVE,   GTK_RESPONSE_OK,
+
+                            NULL);
+  gtk_dialog_set_alternative_button_order (GTK_DIALOG (dialog),
+                                           GTK_RESPONSE_OK,
+                                           GTK_RESPONSE_CANCEL,
+                                           -1);
+
+  gimp_window_set_transient (GTK_WINDOW (dialog));
+
+  /*
+   * parameter settings
+   */
+  frame = gimp_frame_new (_("XMC Options"));
+  gtk_container_set_border_width (GTK_CONTAINER (frame), 12);
+  gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dialog)->vbox),
+                      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 "Hotspot  _X:" + spinbox */
+  x1 = hotspotRange->x;
+  x2 = hotspotRange->width + hotspotRange->x - 1;
+  tmpwidget =
+    gimp_spin_button_new (&adjustment, xmcparas.x, x1, x2, 1, 5, 0, 0, 0);
+  gtk_widget_show (tmpwidget);
+  g_value_set_double (&val, 1.0);
+  g_object_set_property (G_OBJECT (tmpwidget), "xalign", &val);/* align right*/
+  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 hotspot. The origin is top left corner."),
+                         NULL);
+  gimp_table_attach_aligned (GTK_TABLE (table), 0, 0, _("Hotspot _X:"), 0, 0.5, tmpwidget, 1, TRUE);
+  /* label "Y:" + spinbox */
+  y1 = hotspotRange->y;
+  y2 = hotspotRange->height + hotspotRange->y - 1;
+  tmpwidget =
+    gimp_spin_button_new (&adjustment, xmcparas.y, y1, y2, 1, 5, 0, 0, 0);
+  gtk_widget_show (tmpwidget);
+  g_value_set_double (&val, 1.0);
+  g_object_set_property (G_OBJECT (tmpwidget), "xalign", &val);/* align right*/
+  g_signal_connect (adjustment, "value-changed",
+                    G_CALLBACK (gimp_int_adjustment_update),
+                    &xmcparas.y);
+  /* tooltip */
+  gimp_help_set_help_data (tmpwidget,
+                         _("Enter the Y coordinate of the hotspot. The origin is top left corner."),
+                         NULL);
+  gimp_table_attach_aligned (GTK_TABLE (table), 1, 0, "_Y:", 1.0, 0.5, tmpwidget, 1, TRUE);
+
+  /*
+   *  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 may fix the problem that some large cursors disorder the "
+                          "screen as well as reduces the file size.\n"
+                          "Uncheck if you plan to edit the exported cursor by 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_hbox_new (FALSE, 6);
+  gtk_widget_show (box);
+  tmpwidget = gimp_spin_button_new (&adjustment, xmcvals.delay, CURSOR_MINIMUM_DELAY,
+                                    CURSOR_MAX_DELAY, 1, 5, 0, 1, 0);
+  gtk_widget_show (tmpwidget);
+  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);
+  g_signal_connect (adjustment, "value-changed",
+                    G_CALLBACK (gimp_int_adjustment_update),
+                    &xmcvals.delay);
+  /* appended "ms" */
+  tmpwidget = gtk_label_new ("ms");
+  gtk_misc_set_alignment (GTK_MISC (tmpwidget), 0, 0.5); /*align left*/
+  gtk_box_pack_start (GTK_BOX (box), tmpwidget, TRUE, TRUE, 0);
+  gtk_widget_show (tmpwidget);
+  /* tooltip */
+  gimp_help_set_help_data (box,
+                        _("Enter time span in milliseconds in which each frame is rendered."),
+                        NULL);
+  gimp_table_attach_aligned (GTK_TABLE (table), 0, 4, _("_Delay:"), 0, 0.5, box, 3, TRUE);
+  /* 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_misc_set_alignment (GTK_MISC (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)
+{
+  g_return_if_fail (commentp);
+
+  const gchar *text;
+
+  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);
+  return;
+}
+
+static void
+text_view_callback (GtkTextBuffer *buffer, gchar **commentp)
+{
+  g_return_if_fail (commentp);
+
+  GtkTextIter   start_iter;
+  GtkTextIter   end_iter;
+  gchar        *text;
+
+  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);
+    }
+  return;
+}
+
+/**
+ * Set default hotspot based on hotspotRange.
+**/
+static gboolean
+load_default_hotspot (const gint32 image_ID, GimpParamRegion *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)
+{
+  gint i, j;                    /* Looping vars */
+  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 */
+  GimpDrawable *drawable;       /* Drawable for layer */
+  GimpPixelRgn pixel_rgn;       /* Pixel region for layer */
+  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 */
+  GimpParamRegion 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)];
+
+  /* This will be used in set_size_and_delay fucntion later.
+     To define this in that function is easy to read but place here to
+     reduce overheads. */
+  re = g_regex_new ("[(][ ]*(\\d+)[ ]*(px|ms)[ ]*[)]",
+                    G_REGEX_CASELESS | G_REGEX_OPTIMIZE,
+                    0,
+                    NULL);
+
+  /*
+   * 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;
+    }
+
+  gimp_progress_init_printf (_("Saving '%s'"),
+                             gimp_filename_to_utf8 (filename));
+
+  /* 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");
+      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++)
+    {
+      drawable = gimp_drawable_get (layers[nlayers - 1 - i]);
+      /* this plugin only treat 8bit color depth RGBA image. */
+      if (drawable->bpp != 4)
+        {
+          g_set_error (error, 0, 0,
+          _("This plug-in can only handle RGBA image format with 8bit color depth."));
+          return FALSE;
+        }
+      /* get framename of this layer */
+      framename = gimp_drawable_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 (drawable->width > MAX_SAVE_DIMENSION)
+        {
+          g_set_error (error, 0, 0,
+                       _("Width of '%s' is too large. Please reduce more than %dpx)"),
+                       gimp_any_to_utf8 (framename, -1, NULL), MAX_SAVE_DIMENSION);
+          return FALSE;
+        }
+      if (drawable->height > MAX_SAVE_DIMENSION)
+        {
+          g_set_error (error, 0, 0,
+                       _("Height of '%s' is too large. Please reduce more than %dpx)"),
+                       gimp_any_to_utf8 (framename, -1, NULL), MAX_SAVE_DIMENSION);
+          return FALSE;
+        }
+      if (drawable->height == 0 ||drawable->width == 0)
+        {
+          g_set_error (error, 0, 0,
+                       _("The size of '%s' is zero!"),
+                       gimp_any_to_utf8 (framename, -1, NULL));
+          return FALSE;
+        }
+
+      gimp_pixel_rgn_init (&pixel_rgn, drawable, 0, 0, drawable->width,
+                            drawable->height, FALSE, 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, &pixel_rgn);
+          /* 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");
+                  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 save the cursor because the hotspot is not on '%s'.\n"
+                             "Try to change the hotspot position, layer geometry or "
+                             "save without auto-crop."),
+                           gimp_any_to_utf8 (framename, -1, NULL));
+              return FALSE;
+            }
+        }
+      else /* if without auto-cropping... */
+        {
+          /* set save_rgn for the case not to auto-crop */
+          save_rgn.width = drawable->width;
+          save_rgn.height = drawable->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");
+          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. */
+      gimp_pixel_rgn_get_rect (&pixel_rgn, (guchar *) pixelbuf,
+                               save_rgn.x, save_rgn.y,
+                               save_rgn.width, save_rgn.height);
+
+      /*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)
+        return FALSE;
+
+      gimp_drawable_set_name (orig_layers[nlayers - 1 - i], framename);
+      g_free (framename);
+
+      gimp_progress_update ((i + 1) / imagesp->nimage);
+    }
+
+  /*
+   * 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);
+          return FALSE;
+        }
+
+    }
+  else /* if no comments exist */
+    {
+      if (! XcursorFileSaveImages (fp, imagesp))
+        {
+          DM_XMC("Failed to XcursorFileSaveImages.\t%p\t%p\n", fp, imagesp);
+          return FALSE;
+        }
+    }
+
+  /* actual warning about dimensions */
+  if (dimension_warn)
+    {
+      g_message (_("Your cursor was successfully saved but it contains one "
+                   "or more frames which width or height is more than %ipx.\n"
+                   "It will clutter the screen in some environments."),
+                   MAX_BITMAP_CURSOR_SIZE);
+    }
+  if (size_warn)
+    {
+      g_message (_("Your cursor was successfully saved but it contains one "
+                   "or more frames which nominal size is not "
+                   "supported by gnome-appearance-properties.\n"
+                   "You can satisfy it by checking \"Replace the size of all "
+                   "frame...\" in save dialog, or Your cursor may not appear "
+                   "in gnome-appearance-properties."));
+    }
+  /*
+   * 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_parasite_detach (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 = CLAMP0255(red * 255 / alpha);
+  blue = CLAMP0255(blue * 255 / alpha);
+  green = CLAMP0255(green * 255 / alpha);
+  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 XcursorCommentsDestory 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)
+{
+  g_return_if_fail (image_ID != -1);
+
+  gint i;
+
+  for (i = 0; i < 3; ++i)
+    {
+      xmcparas.comments[i] = get_comment_from_pname (image_ID, parasiteName[i]);
+    }
+  return;
+}
+
+/**
+ * Set content to a parasite named as pname. if parasite
+ * is already exist, 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)
+{
+  g_return_val_if_fail (image_ID != -1, FALSE);
+  g_return_val_if_fail (content, FALSE);
+
+  gboolean ret = FALSE;
+  gchar *tmpstring, *joind;
+  GimpParasite *parasite;
+
+  parasite = gimp_image_parasite_find (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_parasite_attach (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)
+{
+  g_return_val_if_fail (image_ID != -1, NULL);
+
+  gchar* string = NULL;
+  GimpParasite *parasite;
+  glong length;
+
+  parasite = gimp_image_parasite_find (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 X cursor.\n"
+                       "The overflowed string was dropped."),
+                     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)
+{
+  g_return_val_if_fail (image_ID != -1, FALSE);
+
+  gboolean ret = FALSE;
+  gchar *tmpstr;
+  GimpParasite *parasite;
+
+  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_parasite_attach (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)
+{
+  g_return_val_if_fail (image_ID != -1, FALSE);
+
+  GimpParasite *parasite = NULL;
+
+  DM_XMC("function: getHotsopt\n");
+
+  parasite = gimp_image_parasite_find (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)
+{
+  g_return_if_fail (framename);
+  g_return_if_fail (sizep);
+  g_return_if_fail (delayp);
+  g_return_if_fail (re);
+
+  guint32 size = 0;
+  guint32 delay = 0;
+  gchar *digits = NULL;
+  gchar *suffix = NULL;
+  GMatchInfo *info = NULL;
+
+  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 */
+                size = MAX_BITMAP_CURSOR_SIZE;
+              else
+                size = MIN (MAX_BITMAP_CURSOR_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)
+    { /* 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;
+}
+
+/**
+ * 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)
+{
+
+  /* don't pass 0 for size. */
+  g_return_val_if_fail (size > 0, NULL);
+
+  int i;  /* loop index */
+  static struct {guint32 size; guint count;} Counter[MAX_SIZE_NUM + 1] = {{0,}};
+  /* "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)
+            { /* the number of different nominal size is over MAX_SIZE_NUM! */
+              g_set_error (errorp, 0, 0,
+                           _("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 (GimpParamRegion *return_rgn, GimpPixelRgn *pr)
+{
+  g_return_if_fail (pr);
+
+  guint i, j;
+  guint32 *buf = g_malloc (MAX(pr->w, pr->h) * sizeof (guint32));
+
+  DM_XMC("function:get_cropped_region\n");
+
+  gimp_tile_cache_ntiles (MAX (pr->w / gimp_tile_width (), pr->h /  gimp_tile_height ()) + 1);
+  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 < pr->w ; ++i)
+    {
+      DM_XMC("pr->x+i=%i\tpr->w=%i\n",pr->x + i, pr->w);
+      gimp_pixel_rgn_get_col (pr, (guchar *)buf, pr->x + i, pr->y, pr->h);
+      for (j = 0; j < pr->h; ++j)
+        {
+          if (pix_is_opaque (buf[j])) /* if a opaque pixel exist. */
+            {
+              return_rgn->x = pr->x + i;
+              goto find_right;
+            }
+        }
+    }
+  /* pr has no opaque pixel. */
+   return_rgn->width = 0;
+   return;
+
+  /* find right border. */
+  find_right:
+  for (i = 0 ;i < pr->w ; ++i)
+    {
+      DM_XMC("pr->x+pr->w-1=%i\tpr->y+j=%i\tpr->h=%i\n",
+              pr->x + pr->w - 1 - i, pr->y, pr->h);
+      gimp_pixel_rgn_get_col (pr, (guchar *)buf, pr->x + pr->w - 1 - i, pr->y, pr->h);
+      for (j = 0; j < pr->h; ++j)
+        {
+          if (pix_is_opaque (buf[j])) /* if a opaque pixel exist. */
+            {
+              return_rgn->width = pr->x + pr->w - i - return_rgn->x;
+              goto find_top;
+            }
+        }
+    }
+  g_return_if_reached ();
+
+  /* find top border. */
+  find_top:
+  for (j = 0 ;j < pr->h ; ++j)
+    {
+      DM_XMC("pr->x=%i\tpr->y+j=%i\tpr->w=%i\n",pr->x, pr->y + j, pr->w);
+      gimp_pixel_rgn_get_row (pr, (guchar *)buf, pr->x, pr->y + j, pr->w);
+      for (i = 0; i < pr->w; ++i)
+        {
+          if (pix_is_opaque (buf[i])) /* if a opaque pixel exist. */
+            {
+              return_rgn->y = pr->y + j;
+              goto find_bottom;
+            }
+        }
+    }
+  g_return_if_reached ();
+
+  /* find bottom border. */
+  find_bottom:
+  for (j = 0 ;j < pr->h ; ++j)
+    {
+      DM_XMC ("pr->x=%i\tpr->y+pr->h-1-j=%i\tpr->w=%i\n",pr->x, pr->y + pr->h - 1 - j, pr->w);
+      gimp_pixel_rgn_get_row (pr, (guchar *)buf, pr->x, pr->y + pr->h - 1 - j, pr->w);
+      for (i = 0; i < pr->w; ++i)
+        {
+          if (pix_is_opaque (buf[i])) /* if a opaque pixel exist. */
+            {
+              return_rgn->height = pr->y + pr->h  - 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;
+}
+/**
+ * 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
+  if ((pix>>24) != 0)
+    return TRUE;
+  else
+    return FALSE;
+}
+
+/**
+ * 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 GimpParamRegion*
+get_intersection_of_frames (gint32 image_ID)
+{
+  g_return_val_if_fail (image_ID != -1, FALSE);
+
+  GimpParamRegion *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;
+  GimpDrawable *drawable;
+
+  layers = gimp_image_get_layers (image_ID, &nlayers);
+  for (i = 0; i < nlayers; ++i)
+    {
+      drawable = gimp_drawable_get (layers[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 + drawable->width - 1);
+      y2 = MIN (y2, y_off + drawable->height - 1);
+    }
+  if (x1 > x2 || y1 > y2)
+    return NULL;
+  /* OK intersection exists. */
+  iregion = g_new (GimpParamRegion, 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, GimpParamRegion *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)
+{
+  g_return_if_fail (xcIs);
+
+  gint i;                    /* loop value */
+  gint32 dw, dh;             /* the distance between hotspot and right(bottom) border */
+  gint32 max_xhot, max_yhot; /* the maximum value of xhot(yhot) */
+
+  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;
+  return;
+}
diff --git a/plug-ins/common/plugin-defs.pl b/plug-ins/common/plugin-defs.pl
index 783ccc1..04854c7 100644
--- a/plug-ins/common/plugin-defs.pl
+++ b/plug-ins/common/plugin-defs.pl
@@ -77,6 +77,7 @@
     'file-tiff-save' => { ui => 1, optional => 1, libs => 'TIFF_LIBS' },
     'file-wmf' => { ui => 1, optional => 1, libs => 'WMF_LIBS', cflags => 'WMF_CFLAGS' },
     'file-xbm' => { ui => 1 },
+    'file-xmc' => { ui => 1, optional => 1, libs => 'XMC_LIBS' },
     'file-xpm' => { ui => 1, optional => 1, libs => 'XPM_LIBS' },
     'file-xwd' => {  ui => 1 },
     'film' => { ui => 1 },



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]