/*
 *   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;
}
