/*
 *   X11 Mouse Cursor (XMC) plug-in for PIKA
 *
 *   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 PIKA-2.8?
 * Todo: support for "pika-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
 * "pika-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 <libpika/pika.h>
#include <libpika/pikaui.h>

#include <X11/Xlib.h>
#include <X11/Xcursor/Xcursor.h>

#include "libpika/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           "pika-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 NULL;
#define DISPLAY_DIGIT(x) ((x) > 100) ? 3 : ((x) > 10) ? 2 : 1


typedef struct _Xmc      Xmc;
typedef struct _XmcClass XmcClass;

struct _Xmc
{
  PikaPlugIn      parent_instance;
};

struct _XmcClass
{
  PikaPlugInClass parent_class;
};


#define XMC_TYPE  (xmc_get_type ())
#define XMC(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), XMC_TYPE, Xmc))

GType                   xmc_get_type                 (void) G_GNUC_CONST;

static GList          * xmc_query_procedures         (PikaPlugIn            *plug_in);
static PikaProcedure  * xmc_create_procedure         (PikaPlugIn            *plug_in,
                                                      const gchar           *name);

static PikaValueArray * xmc_load                     (PikaProcedure         *procedure,
                                                      PikaRunMode            run_mode,
                                                      GFile                 *file,
                                                      PikaMetadata          *metadata,
                                                      PikaMetadataLoadFlags *flags,
                                                      PikaProcedureConfig   *config,
                                                      gpointer               run_data);
static PikaValueArray * xmc_load_thumb               (PikaProcedure         *procedure,
                                                      GFile                 *file,
                                                      gint                   size,
                                                      PikaProcedureConfig   *config,
                                                      gpointer               run_data);
static PikaValueArray * xmc_save                     (PikaProcedure         *procedure,
                                                      PikaRunMode            run_mode,
                                                      PikaImage             *image,
                                                      gint                   n_drawables,
                                                      PikaDrawable         **drawables,
                                                      GFile                 *file,
                                                      PikaMetadata          *metadata,
                                                      PikaProcedureConfig   *config,
                                                      gpointer               run_data);

static PikaImage      * load_image                   (GFile                 *file,
                                                      GError               **error);

static PikaImage      * load_thumbnail               (GFile                 *file,
                                                      gint32                 thumb_size,
                                                      gint32                *width,
                                                      gint32                *height,
                                                      gint32                *num_layers,
                                                      GError               **error);

static guint32          read32                       (FILE                  *f,
                                                      GError               **error);

static gboolean         save_image                   (GFile                 *file,
                                                      PikaImage             *image,
                                                      gint                   n_drawables,
                                                      PikaDrawable         **drawables,
                                                      PikaImage             *orig_image,
                                                      GObject               *config,
                                                      GError               **error);

static gboolean         save_dialog                  (PikaProcedure         *procedure,
                                                      GObject               *config,
                                                      PikaImage             *image,
                                                      GeglRectangle         *hotspot_range);

static void             load_default_hotspot         (PikaImage             *image,
                                                      GeglRectangle         *hotspot_range,
                                                      GObject               *config);

static inline guint32   separate_alpha               (guint32                pixel);

static inline guint32   premultiply_alpha            (guint32                pixel);

static XcursorComments *set_cursor_comments          (GObject               *config);

static gboolean         set_hotspot_to_parasite      (PikaImage       *image,
                                                      gint             hot_spot_x,
                                                      gint             hot_spot_y);

static gboolean         get_hotspot_from_parasite    (PikaImage       *image,
                                                      gint            *hot_spot_x,
                                                      gint            *hot_spot_y);

static void             set_size_and_delay           (GObject         *config,
                                                      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   (PikaImage        *image);

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);


G_DEFINE_TYPE (Xmc, xmc, PIKA_TYPE_PLUG_IN)

PIKA_MAIN (XMC_TYPE)
DEFINE_STD_SET_I18N


static void
xmc_class_init (XmcClass *klass)
{
  PikaPlugInClass *plug_in_class = PIKA_PLUG_IN_CLASS (klass);

  plug_in_class->query_procedures = xmc_query_procedures;
  plug_in_class->create_procedure = xmc_create_procedure;
  plug_in_class->set_i18n         = STD_SET_I18N;
}

static void
xmc_init (Xmc *xmc)
{
}

static GList *
xmc_query_procedures (PikaPlugIn *plug_in)
{
  GList *list = NULL;

  list = g_list_append (list, g_strdup (LOAD_THUMB_PROC));
  list = g_list_append (list, g_strdup (LOAD_PROC));
  list = g_list_append (list, g_strdup (SAVE_PROC));

  return list;
}

static PikaProcedure *
xmc_create_procedure (PikaPlugIn  *plug_in,
                      const gchar *name)
{
  PikaProcedure *procedure = NULL;

  if (! strcmp (name, LOAD_PROC))
    {
      procedure = pika_load_procedure_new (plug_in, name,
                                           PIKA_PDB_PROC_TYPE_PLUGIN,
                                           xmc_load, NULL, NULL);

      pika_procedure_set_menu_label (procedure, _("X11 Mouse Cursor"));

      pika_procedure_set_documentation (procedure,
                                        "Loads files of X11 Mouse Cursor "
                                        "file format",
                                        "This plug-in loads X11 Mouse Cursor "
                                        "(XMC) files.",
                                        name);
      pika_procedure_set_attribution (procedure,
                                      "Takeshi Matsuyama <tksmashiw@gmail.com>",
                                      "Takeshi Matsuyama",
                                      "26 May 2009");

      pika_file_procedure_set_mime_types (PIKA_FILE_PROCEDURE (procedure),
                                          XCURSOR_MIME_TYPE);
      pika_file_procedure_set_extensions (PIKA_FILE_PROCEDURE (procedure),
                                          XCURSOR_EXTENSION);
      pika_file_procedure_set_magics (PIKA_FILE_PROCEDURE (procedure),
                                      "0,string,Xcur");

      pika_load_procedure_set_thumbnail_loader (PIKA_LOAD_PROCEDURE (procedure),
                                                LOAD_THUMB_PROC);
    }
  else if (! strcmp (name, LOAD_THUMB_PROC))
    {
      procedure = pika_thumbnail_procedure_new (plug_in, name,
                                                PIKA_PDB_PROC_TYPE_PLUGIN,
                                                xmc_load_thumb, NULL, NULL);

      pika_procedure_set_documentation (procedure,
                                        "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",
                                        "",
                                        name);
      pika_procedure_set_attribution (procedure,
                                      "Takeshi Matsuyama <tksmashiw@gmail.com>",
                                      "Takeshi Matsuyama",
                                      "26 May 2009");
    }
  else if (! strcmp (name, SAVE_PROC))
    {
      procedure = pika_save_procedure_new (plug_in, name,
                                           PIKA_PDB_PROC_TYPE_PLUGIN,
                                           FALSE, xmc_save, NULL, NULL);

      pika_procedure_set_image_types (procedure, "RGBA");

      pika_procedure_set_menu_label (procedure, _("X11 Mouse Cursor"));

      pika_procedure_set_documentation (procedure,
                                        "Exports files of X11 cursor file",
                                        "This plug-in exports X11 Mouse Cursor "
                                        "(XMC) files",
                                        name);
      pika_procedure_set_attribution (procedure,
                                      "Takeshi Matsuyama <tksmashiw@gmail.com>",
                                      "Takeshi Matsuyama",
                                      "26 May 2009");

      pika_file_procedure_set_mime_types (PIKA_FILE_PROCEDURE (procedure),
                                          XCURSOR_MIME_TYPE);
      pika_file_procedure_set_extensions (PIKA_FILE_PROCEDURE (procedure),
                                          XCURSOR_EXTENSION);

      PIKA_PROC_ARG_INT (procedure, "hot-spot-x",
                         "Hot spot X",
                         "X-coordinate of hot spot "
                         "(use -1, -1 to keep original hot spot",
                         -1, PIKA_MAX_IMAGE_SIZE, -1,
                         G_PARAM_READWRITE);

      PIKA_PROC_ARG_INT (procedure, "hot-spot-y",
                         "Hot spot Y",
                         "Y-coordinate of hot spot "
                         "(use -1, -1 to keep original hot spot",
                         -1, PIKA_MAX_IMAGE_SIZE, -1,
                         G_PARAM_READWRITE);

      PIKA_PROC_ARG_BOOLEAN (procedure, "crop",
                             "Crop",
                             "Auto-crop or not",
                             FALSE,
                             G_PARAM_READWRITE);

      PIKA_PROC_ARG_INT (procedure, "size",
                         "Size",
                         "Default nominal size",
                         1, G_MAXINT, 32,
                         G_PARAM_READWRITE);

      PIKA_PROC_ARG_BOOLEAN (procedure, "size-replace",
                             "Size replace",
                             "Replace existent size or not",
                             FALSE,
                             G_PARAM_READWRITE);

      PIKA_PROC_ARG_INT (procedure, "delay",
                         "Delay",
                         "Default delay",
                         CURSOR_MINIMUM_DELAY, G_MAXINT, CURSOR_DEFAULT_DELAY,
                         G_PARAM_READWRITE);

      PIKA_PROC_ARG_BOOLEAN (procedure, "delay-replace",
                             "Delay replace",
                             "Replace existent delay or not",
                             FALSE,
                             G_PARAM_READWRITE);

      PIKA_PROC_ARG_STRING (procedure, "xmc-copyright",
                            "Copyright",
                            "Copyright information",
                            NULL,
                            G_PARAM_READWRITE);

      pika_procedure_set_argument_sync (procedure, "xmc-copyright",
                                        PIKA_ARGUMENT_SYNC_PARASITE);

      PIKA_PROC_ARG_STRING (procedure, "xmc-license",
                            "License",
                            "License information",
                            NULL,
                            G_PARAM_READWRITE);

      pika_procedure_set_argument_sync (procedure, "xmc-license",
                                        PIKA_ARGUMENT_SYNC_PARASITE);

      PIKA_PROC_ARG_STRING (procedure, "pika-comment",
                            "Other",
                            "Other comment (taken from 'pika-comment' parasite)",
                            pika_get_default_comment (),
                            G_PARAM_READWRITE);

      pika_procedure_set_argument_sync (procedure, "pika-comment",
                                        PIKA_ARGUMENT_SYNC_PARASITE);
    }

  return procedure;
}

static PikaValueArray *
xmc_load (PikaProcedure         *procedure,
          PikaRunMode            run_mode,
          GFile                 *file,
          PikaMetadata          *metadata,
          PikaMetadataLoadFlags *flags,
          PikaProcedureConfig   *config,
          gpointer               run_data)
{
  PikaValueArray *return_vals;
  PikaImage      *image;
  GError         *error = NULL;

  gegl_init (NULL, NULL);

  image = load_image (file, &error);

  if (! image)
    return pika_procedure_new_return_values (procedure,
                                             PIKA_PDB_EXECUTION_ERROR,
                                             error);

  return_vals = pika_procedure_new_return_values (procedure,
                                                  PIKA_PDB_SUCCESS,
                                                  NULL);

  PIKA_VALUES_SET_IMAGE (return_vals, 1, image);

  return return_vals;
}

static PikaValueArray *
xmc_load_thumb (PikaProcedure       *procedure,
                GFile               *file,
                gint                 size,
                PikaProcedureConfig *config,
                gpointer             run_data)
{
  PikaValueArray *return_vals;
  gint            width;
  gint            height;
  gint            num_layers;
  PikaImage      *image;
  GError         *error = NULL;

  gegl_init (NULL, NULL);

  image = load_thumbnail (file, size,
                          &width,
                          &height,
                          &num_layers,
                          &error);

  if (! image)
    return pika_procedure_new_return_values (procedure,
                                             PIKA_PDB_EXECUTION_ERROR,
                                             error);

  return_vals = pika_procedure_new_return_values (procedure,
                                                  PIKA_PDB_SUCCESS,
                                                  NULL);

  PIKA_VALUES_SET_IMAGE (return_vals, 1, image);
  PIKA_VALUES_SET_INT   (return_vals, 2, width);
  PIKA_VALUES_SET_INT   (return_vals, 3, height);
  PIKA_VALUES_SET_ENUM  (return_vals, 4, PIKA_RGBA_IMAGE);
  PIKA_VALUES_SET_INT   (return_vals, 5, num_layers);

  return return_vals;
}

static PikaValueArray *
xmc_save (PikaProcedure        *procedure,
          PikaRunMode           run_mode,
          PikaImage            *image,
          gint                  n_drawables,
          PikaDrawable        **drawables,
          GFile                *file,
          PikaMetadata         *metadata,
          PikaProcedureConfig  *config,
          gpointer              run_data)
{
  PikaPDBStatusType  status = PIKA_PDB_SUCCESS;
  PikaExportReturn   export = PIKA_EXPORT_CANCEL;
  PikaImage         *orig_image;
  GeglRectangle     *hotspot_range;
  gint               hot_spot_x;
  gint               hot_spot_y;
  GError            *error = NULL;

  gegl_init (NULL, NULL);

  orig_image = image;

  hotspot_range = get_intersection_of_frames (image);

  if (! hotspot_range)
    {
      g_set_error (&error, 0, 0,
                   _("Cannot set the hot spot!\n"
                     "You must arrange layers so that all of them have "
                     "an intersection."));

      return pika_procedure_new_return_values (procedure,
                                               PIKA_PDB_EXECUTION_ERROR,
                                               error);
    }

  switch (run_mode)
    {
    case PIKA_RUN_INTERACTIVE:
    case PIKA_RUN_WITH_LAST_VALS:
      pika_ui_init (PLUG_IN_BINARY);

      export = pika_export_image (&image, &n_drawables, &drawables, "XMC",
                                  PIKA_EXPORT_CAN_HANDLE_RGB    |
                                  PIKA_EXPORT_CAN_HANDLE_ALPHA  |
                                  PIKA_EXPORT_CAN_HANDLE_LAYERS |
                                  PIKA_EXPORT_NEEDS_ALPHA);

      if (export == PIKA_EXPORT_CANCEL)
        return pika_procedure_new_return_values (procedure,
                                                 PIKA_PDB_CANCEL,
                                                 NULL);
      break;

    default:
      break;
    }

  switch (run_mode)
    {
    case PIKA_RUN_INTERACTIVE:
      load_default_hotspot (image, hotspot_range, G_OBJECT (config));

      if (! save_dialog (procedure, G_OBJECT (config), image, hotspot_range))
        return pika_procedure_new_return_values (procedure,
                                                 PIKA_PDB_CANCEL,
                                                 NULL);
      break;

    case PIKA_RUN_NONINTERACTIVE:
      g_object_get (config,
                    "hot-spot-x", &hot_spot_x,
                    "hot-spot-y", &hot_spot_y,
                    NULL);

      if (! pix_in_region (hot_spot_x, hot_spot_y, hotspot_range))
        {
          /* you can purposely choose non acceptable values for
           * hotspot to use cursor's original values.
           */
          load_default_hotspot (image, hotspot_range, G_OBJECT (config));
        }
      break;

    case PIKA_RUN_WITH_LAST_VALS:
      load_default_hotspot (image, hotspot_range, G_OBJECT (config));
      break;

    default:
      break;
    }

  if (! save_image (file, image, n_drawables, drawables,
                    orig_image, G_OBJECT (config), &error))
    {
      status = PIKA_PDB_EXECUTION_ERROR;
    }

  if (export == PIKA_EXPORT_EXPORT)
    {
      pika_image_delete (image);
      g_free (drawables);
    }

  g_free (hotspot_range);

  return pika_procedure_new_return_values (procedure, status, error);
}

/*
 * 'load_image()' - Load a X cursor image into a new image window.
 */

static PikaImage *
load_image (GFile   *file,
            GError **error)
{
  FILE            *fp;
  PikaImage       *image;
  PikaLayer       *layer;
  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             hot_spot_x;
  gint             hot_spot_y;
  gint             i, j;

  pika_progress_init_printf (_("Opening '%s'"),
                             pika_file_get_utf8_name (file));

  /* Open the file and check it is a valid X cursor */

  fp = g_fopen (g_file_peek_path (file), "rb");

  if (fp == NULL)
    {
      g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
                   _("Could not open '%s' for reading: %s"),
                   pika_file_get_utf8_name (file), g_strerror (errno));
      return NULL;
    }

  if (! XcursorFileLoad (fp, &commentsp, &imagesp))
    {
      g_set_error (error, 0, 0, _("'%s' is not a valid X cursor."),
                   pika_file_get_utf8_name (file));
      fclose (fp);
      return NULL;
    }

  /* 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, pika_file_get_utf8_name (file));
          fclose (fp);
          return NULL;
        }
      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, pika_file_get_utf8_name (file));
          fclose (fp);
          return NULL;
        }
    }

  find_hotspots_and_dimensions (imagesp,
                                &hot_spot_x, &hot_spot_y,
                                &img_width, &img_height);

  DM_XMC ("xhot=%i,\tyhot=%i,\timg_width=%i,\timg_height=%i\n",
          hot_spot_x, hot_spot_y, img_width, img_height);

  image = pika_image_new (img_width, img_height, PIKA_RGB);

  if (! set_hotspot_to_parasite (image, hot_spot_x, hot_spot_y))
    {
      fclose (fp);
      return NULL;
    }

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

      layer = pika_layer_new (image, framename, width, height,
                              PIKA_RGBA_IMAGE,
                              100,
                              pika_image_get_default_new_layer_mode (image));
      pika_image_insert_layer (image, layer, NULL, 0);

      /* Adjust layer position to let hotspot sit on the same point. */
      pika_item_transform_translate (PIKA_ITEM (layer),
                                     hot_spot_x - imagesp->images[i]->xhot,
                                     hot_spot_y - imagesp->images[i]->yhot);

      g_free (framename);

      /* Get the buffer for our load... */

      buffer = pika_drawable_get_buffer (PIKA_DRAWABLE (layer));

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

      pika_progress_update ((i + 1) / imagesp->nimage);

      g_object_unref (buffer);
    }

  g_free (tmppixel);

  pika_progress_update (1.0);

  /* Comment parsing */

  if (commentsp)
    {
      PikaParasite *parasite;
      gchar        *comments[3] = { NULL, };

      for (i = 0; i < commentsp->ncomment; ++i)
        {
          gint type = commentsp->comments[i]->comment_type -1;

          DM_XMC ("comment type=%d\tcomment=%s\n",
                  commentsp->comments[i]->comment_type,
                  commentsp->comments[i]->comment);

          if (comments[type])
            {
              gchar *tmp = g_strjoin ("\n",
                                      comments[type],
                                      commentsp->comments[i]->comment,
                                      NULL);
              g_free (comments[type]);
              comments[type] = tmp;
            }
          else
            {
              comments[type] = g_strdup (commentsp->comments[i]->comment);
            }
        }

      XcursorCommentsDestroy (commentsp);

      if (comments[0])
        {
          parasite = pika_parasite_new ("xmc-copyright",
                                        PIKA_PARASITE_PERSISTENT,
                                        strlen (comments[0]) + 1,
                                        (gpointer) comments[0]);
          pika_image_attach_parasite (image, parasite);
          pika_parasite_free (parasite);

          g_free (comments[0]);
        }

      if (comments[1])
        {
          parasite = pika_parasite_new ("xmc-license",
                                        PIKA_PARASITE_PERSISTENT,
                                        strlen (comments[1]) + 1,
                                        (gpointer) comments[1]);
          pika_image_attach_parasite (image, parasite);
          pika_parasite_free (parasite);

          g_free (comments[1]);
        }

      if (comments[2])
        {
          parasite = pika_parasite_new ("pika-comment",
                                        PIKA_PARASITE_PERSISTENT,
                                        strlen (comments[2]) + 1,
                                        (gpointer) comments[2]);
          pika_image_attach_parasite (image, parasite);
          pika_parasite_free (parasite);

          g_free (comments[2]);
        }
    }

  DM_XMC ("Comment parsing done.\n");
  XcursorImagesDestroy (imagesp);
  fclose (fp);

  pika_progress_end ();

  return image;
}

/*
 * load_thumbnail
 */

static PikaImage *
load_thumbnail (GFile   *file,
                gint32   thumb_size,
                gint32  *img_width,
                gint32  *img_height,
                gint32  *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;
  PikaImage     *image = NULL;
  PikaLayer     *layer;
  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 (img_width, NULL);
  g_return_val_if_fail (img_height, NULL);
  g_return_val_if_fail (num_layers, NULL);

  *img_width  = 0;
  *img_height = 0;
  *num_layers = 0;

  fp = g_fopen (g_file_peek_path (file), "rb");

  if (! fp)
    {
      g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
                   _("Could not open '%s' for reading: %s"),
                   pika_file_get_utf8_name (file), g_strerror (errno));
      return NULL;
    }

  /* 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.",
                   pika_file_get_utf8_name (file));
      fclose (fp);
      return NULL;
    }
  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[*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 = *num_layers;
            }
          ++*num_layers;
        }
    }

  if (sel_num < 0)
    {
      g_set_error (error, 0, 0,
                   _("there is no image chunk in \"%s\"."),
                   pika_file_get_utf8_name (file));
      fclose (fp);
      return NULL;
    }

  /* get width and height of entire image */

  /* Let's make XcursorImages */
  xcIs = XcursorImagesCreate (*num_layers);
  xcIs->nimage = *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, img_width, img_height);

  DM_XMC ("width=%i\theight=%i\tnum-layers=%i\n",
          *img_width, *img_height, xcIs->nimage);

  /* dimension check */
  if (*img_width > MAX_LOAD_DIMENSION)
    {
      g_set_error (error, 0, 0,
                   _("'%s' is too wide for an X cursor."),
                   pika_file_get_utf8_name (file));
      fclose (fp);
      return NULL;
    }

  if (*img_height > MAX_LOAD_DIMENSION)
    {
      g_set_error (error, 0, 0,
                   _("'%s' is too high for an X cursor."),
                   pika_file_get_utf8_name (file));
      fclose (fp);
      return NULL;
    }

  /*  create new image! */

  width  = xcIs->images[sel_num]->width;
  height = xcIs->images[sel_num]->height;

  image = pika_image_new (width, height, PIKA_RGB);

  layer = pika_layer_new (image, NULL, width, height,
                          PIKA_RGBA_IMAGE,
                          100,
                          pika_image_get_default_new_layer_mode (image));

  pika_image_insert_layer (image, layer, NULL, 0);

  /*
   * Get the drawable and set the pixel region for our load...
   */

  buffer = pika_drawable_get_buffer (PIKA_DRAWABLE (layer));

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

/* 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 (PikaProcedure *procedure,
             GObject       *config,
             PikaImage     *image,
             GeglRectangle *hotspot_range)
{
  GtkWidget     *dialog;
  GtkWidget     *grid;
  GtkWidget     *box;
  GtkWidget     *button;
  GtkListStore  *store;
  GtkWidget     *combo;
  GtkWidget     *label;
  GtkWidget     *entry;
  GtkTextBuffer *textbuffer;
  GtkWidget     *view;
  gint           row;
  gboolean       run;

  dialog = pika_procedure_dialog_new (procedure,
                                      PIKA_PROCEDURE_CONFIG (config),
                                      _("Export Image as X11 Mouse Cursor"));

  grid = gtk_grid_new ();
  gtk_grid_set_row_spacing (GTK_GRID (grid), 6);
  gtk_grid_set_column_spacing (GTK_GRID (grid), 6);
  gtk_container_set_border_width (GTK_CONTAINER (grid), 12);
  gtk_box_pack_start (GTK_BOX (pika_export_dialog_get_content_area (dialog)),
                      grid, TRUE, TRUE, 0);
  gtk_widget_show (grid);

  row = 0;

  /* Hotspot */
  button = pika_prop_spin_button_new (config, "hot-spot-x",
                                      1, 10, 0);
  gtk_spin_button_set_range (GTK_SPIN_BUTTON (button),
                             hotspot_range->x,
                             hotspot_range->x + hotspot_range->width - 1);
  pika_grid_attach_aligned (GTK_GRID (grid), 0, row++,
                            _("Hot spot _X:"), 0.0, 0.5,
                            button, 1);

  pika_help_set_help_data (button,
                           _("Enter the X coordinate of the hot spot. "
                             "The origin is top left corner."),
                           NULL);

  button = pika_prop_spin_button_new (config, "hot-spot-y",
                                      1, 10, 0);
  gtk_spin_button_set_range (GTK_SPIN_BUTTON (button),
                             hotspot_range->y,
                             hotspot_range->y + hotspot_range->height - 1);
  pika_grid_attach_aligned (GTK_GRID (grid), 0, row++,
                            _("Hot spot _Y:"), 0.0, 0.5,
                            button, 1);

  pika_help_set_help_data (button,
                           _("Enter the Y coordinate of the hot spot. "
                             "The origin is top left corner."),
                           NULL);

  /* Auto-crop */
  button = pika_prop_check_button_new (config, "crop",
                                       _("_Auto-Crop all frames"));
  gtk_grid_attach (GTK_GRID (grid), button, 0, row++, 2, 1);

  pika_help_set_help_data (button,
                           _("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 */
  store = pika_int_store_new ("12px", 12, "16px", 16,
                              "24px", 24, "32px", 32,
                              "36px", 36, "40px", 40,
                              "48px", 48, "64px", 64, NULL);

  combo = pika_prop_int_combo_box_new (config, "size",
                                       PIKA_INT_STORE (store));
  g_object_unref (store);

  pika_grid_attach_aligned (GTK_GRID (grid), 0, row++,
                            _("_Size where\nunspecified:"), 0.0, 0.5,
                            combo, 1);

  pika_help_set_help_data (combo,
                           _("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);

  /* Replace size */
  button = pika_prop_check_button_new (config, "size-replace",
                                       _("Use size entered above for "
                                         "all frames"));
  gtk_grid_attach (GTK_GRID (grid), button, 1, row++, 2, 1);

  /* Delay */
  box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
  pika_grid_attach_aligned (GTK_GRID (grid), 0, row++,
                            _("_Delay where\nunspecified:"), 0, 0.5,
                            box, 3);
  gtk_widget_show (box);

  pika_help_set_help_data (box,
                           _("Enter time span in milliseconds in which "
                             "each frame is rendered."),
                           NULL);

  button = pika_prop_spin_button_new (config, "delay",
                                      1, 10, 0);
  gtk_box_pack_start (GTK_BOX (box), button, FALSE, FALSE, 0);

  label = gtk_label_new ("ms");
  gtk_box_pack_start (GTK_BOX (box), label, FALSE, FALSE, 0);
  gtk_widget_show (label);

  /* Replace delay */
  button = pika_prop_check_button_new (config, "delay-replace",
                                       _("Use delay entered above for "
                                         "all frames"));
  gtk_grid_attach (GTK_GRID (grid), button, 1, row++, 2, 1);

  /* Copyright */
  entry = pika_prop_entry_new (config, "xmc-copyright",
                               XCURSOR_COMMENT_MAX_LEN);
  pika_grid_attach_aligned (GTK_GRID (grid), 0, row++, _("_Copyright:"),
                            0, 0.5, entry, 3);

  pika_help_set_help_data (entry,
                           _("Enter copyright information."),
                           NULL);

  /* License */
  entry = pika_prop_entry_new (config, "xmc-license",
                               XCURSOR_COMMENT_MAX_LEN);
  pika_grid_attach_aligned (GTK_GRID (grid), 0, row++, _("_License:"),
                            0, 0.5, entry, 3);

  pika_help_set_help_data (entry,
                           _("Enter license information."),
                           NULL);

  /* Other */
  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);
  pika_grid_attach_aligned (GTK_GRID (grid), 0, row++, _("_Other:"),
                            0, 0.5, box, 3);

  textbuffer = pika_prop_text_buffer_new (config, "pika-comment",
                                          XCURSOR_COMMENT_MAX_LEN);
  view = gtk_text_view_new_with_buffer (GTK_TEXT_BUFFER (textbuffer));
  gtk_text_view_set_accepts_tab (GTK_TEXT_VIEW (view), FALSE);
  gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (view), GTK_WRAP_WORD);
  g_object_unref (textbuffer);

  gtk_container_add (GTK_CONTAINER (box), view);
  gtk_widget_show (view);

  pika_help_set_help_data (view,
                           _("Enter other comment if you want."),
                           NULL);

  gtk_widget_show (dialog);

  run = pika_procedure_dialog_run (PIKA_PROCEDURE_DIALOG (dialog));

  gtk_widget_destroy (dialog);

  return run;
}

/**
 * Set default hotspot based on hotspot_range.
**/
static void
load_default_hotspot (PikaImage     *image,
                      GeglRectangle *hotspot_range,
                      GObject       *config)
{
  gint hot_spot_x;
  gint hot_spot_y;

  g_return_if_fail (hotspot_range);

  g_object_get (config,
                "hot-spot-x", &hot_spot_x,
                "hot-spot-y", &hot_spot_y,
                NULL);

  if (/* if we cannot load hotspot correctly */
      ! get_hotspot_from_parasite (image, &hot_spot_x, &hot_spot_y) ||
      /* or hotspot is out of range */
      ! pix_in_region (hot_spot_x, hot_spot_y, hotspot_range))
    {
      /* then use top left point of hotspot_range as fallback. */
      hot_spot_x = hotspot_range->x;
      hot_spot_y = hotspot_range->y;
    }

  g_object_set (config,
                "hot-spot-x", hot_spot_x,
                "hot-spot-y", hot_spot_y,
                NULL);
}

/*
 * 'save_image ()' - Save the specified image to X cursor file.
 */

static gboolean
save_image (GFile         *file,
            PikaImage     *image,
            gint           n_drawables,
            PikaDrawable **drawables,
            PikaImage     *orig_image,
            GObject       *config,
            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 */
  GList           *layers;                 /* Array of layer */
  GList           *orig_layers;            /* Array of layer of orig_image */
  GList           *list;
  GList           *orig_list;
  gchar           *framename;              /* framename of a layer */
  GeglRectangle    save_rgn = { 0 };       /* region to save */
  gint             layer_xoffset, layer_yoffset;
  /* temporary buffer which store pixel data (guchar * bpp = guint32) */
  guint32          pixelbuf[SQR (MAX_SAVE_DIMENSION)];
  gint             config_hot_spot_x;
  gint             config_hot_spot_y;
  gboolean         config_crop;
  gboolean         config_size_replace;
  gint             i, j;                   /* Looping vars */

  g_object_get (config,
                "hot-spot-x",   &config_hot_spot_x,
                "hot-spot-y",   &config_hot_spot_y,
                "crop",         &config_crop,
                "size-replace", &config_size_replace,
                NULL);

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

  pika_progress_init_printf (_("Saving '%s'"),
                             pika_file_get_utf8_name (file));

  /*
   * Open the file pointer.
   */
  DM_XMC ("Open the file pointer.\n");

  fp = g_fopen (g_file_peek_path (file), "wb");

  if (! fp)
    {
      g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
                   _("Could not open '%s' for writing: %s"),
                   pika_file_get_utf8_name (file), g_strerror (errno));
      return FALSE;
    }

  /* get layers, in bottom-to-top order */
  orig_layers = pika_image_list_layers (orig_image);
  layers      = pika_image_list_layers (image);

  orig_layers = g_list_reverse (orig_layers);
  layers      = g_list_reverse (layers);

  /* create new XcursorImages. */
  imagesp = XcursorImagesCreate (g_list_length (layers));
  if (!imagesp)
    {
      DM_XMC ("Failed to XcursorImagesCreate!\n");
      fclose (fp);
      return FALSE;
    }
  imagesp->nimage = g_list_length (layers);

  /* 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 (list = layers, orig_list = orig_layers, i = 0;
       list && orig_list;
       list = g_list_next (layers), orig_list = g_list_next (orig_list), i++)
    {
      PikaDrawable *drawable = list->data;
      GeglBuffer   *buffer;
      const Babl   *format;
      gint          width;
      gint          height;

      buffer = pika_drawable_get_buffer (drawable);

      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 = pika_item_get_name (PIKA_ITEM (drawable));
      /* get offset of this layer. */
      pika_drawable_get_offsets (drawable, &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."),
                       pika_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."),
                       pika_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!"),
                       pika_any_to_utf8 (framename, -1, NULL));
          fclose (fp);
          return FALSE;
        }

      if (config_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 (config,
                                  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 (config_hot_spot_x - layer_xoffset,
                               config_hot_spot_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."),
                           pika_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",
              config_hot_spot_x, layer_xoffset, save_rgn.x);
      DM_XMC ("yhot=%i\tsave_rgn->yoffset=%i\tlayer_yoffset=%i\n",
              config_hot_spot_y, layer_yoffset, save_rgn.y);
      imagesp->images[i]->xhot = config_hot_spot_x - layer_xoffset - save_rgn.x;
      imagesp->images[i]->yhot = config_hot_spot_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 (config,
                          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;
        }

      pika_item_set_name (orig_list->data, framename);
      g_free (framename);

      g_object_unref (buffer);

      pika_progress_update ((i + 1) / imagesp->nimage);
    }

  g_list_free (list);
  g_list_free (orig_list);

  pika_progress_update (1.0);

  /*
   * comment parsing
   */
  commentsp = set_cursor_comments (config);

#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. */
  pika_progress_end ();

  /* Save hotspot back to the original image */
  set_hotspot_to_parasite (orig_image, config_hot_spot_x, config_hot_spot_y);

  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 (GObject *config)
{
  GArray          *xcCommentsArray;
  XcursorComment  *(xcCommentp[3]) = { NULL, };
  XcursorComments *xcCommentsp;
  gchar           *comments[3] = { NULL, };
  guint            arraylen;
  gint             i;

  g_object_get (config,
                "xmc-copyright", comments,
                "xmc-license",   comments + 1,
                "pika-comment",  comments + 2,
                NULL);

  xcCommentsArray = g_array_new (FALSE, FALSE, sizeof (XcursorComment *));

  for (i = 0; i < 3; ++i)
    {
      if (comments[i])
        {
          gint gcomlen = strlen (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, comments[i]);
                  g_array_append_val (xcCommentsArray, xcCommentp[i]);
                }
            }

          g_free (comments[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;
}

/* Set hotspot to "hot-spot" parasite which format is common with that
 * of file-xbm.
 */
static gboolean
set_hotspot_to_parasite (PikaImage *image,
                         gint       hot_spot_x,
                         gint       hot_spot_y)
{
  gboolean      ret = FALSE;
  gchar        *tmpstr;
  PikaParasite *parasite;

  g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);

  tmpstr = g_strdup_printf ("%d %d", hot_spot_x, hot_spot_y);
  parasite = pika_parasite_new ("hot-spot",
                                PIKA_PARASITE_PERSISTENT,
                                strlen (tmpstr) + 1,
                                tmpstr);
  g_free (tmpstr);

  if (parasite)
    {
      ret = pika_image_attach_parasite (image, parasite);
      pika_parasite_free (parasite);
    }

  return ret;
}

/* Get back xhot & yhot from "hot-spot" parasite.
 * If succeed, hotspot coordinate is returned and
 * return TRUE.
 * If "hot-spot" is not found or broken, return FALSE.
 */
static gboolean
get_hotspot_from_parasite (PikaImage *image,
                           gint      *hot_spot_x,
                           gint      *hot_spot_y)
{
  PikaParasite *parasite;

  g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);

  DM_XMC ("function: getHotsopt\n");

  parasite = pika_image_get_parasite (image, "hot-spot");

  if (parasite)
    {
      gchar   *parasite_data;
      guint32  parasite_size;
      gint     x, y;

      parasite_data = (gchar *) pika_parasite_get_data (parasite, &parasite_size);
      parasite_data = g_strndup (parasite_data, parasite_size);

      if (sscanf (parasite_data, "%i %i", &x, &y) == 2)
        {
          *hot_spot_x = x;
          *hot_spot_y = y;

          g_free (parasite_data);

          return TRUE;
        }
      g_free (parasite_data);
    }

  return FALSE;
}

/* Set size to sizep, delay to delayp from drawable's framename.
 */
static void
set_size_and_delay (GObject     *config,
                    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;
  gint        config_size;
  gboolean    config_size_replace;
  gint        config_delay;
  gboolean    config_delay_replace;

  g_return_if_fail (framename);
  g_return_if_fail (sizep);
  g_return_if_fail (delayp);
  g_return_if_fail (re);

  g_object_get (config,
                "size",          &config_size,
                "size-replace",  &config_size_replace,
                "delay",         &config_delay,
                "delay-replace", &config_delay_replace,
                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 */
                {
                  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 || config_size_replace == TRUE)
    {
      size = config_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 ||  config_delay_replace == TRUE)
    {
      delay = config_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.
 * if the intersection is empty return NULL.
 * don't forget to g_free returned pointer later.
 */
static GeglRectangle *
get_intersection_of_frames (PikaImage *image)
{
  GeglRectangle *iregion;
  gint32         x1 = G_MININT32, x2 = G_MAXINT32;
  gint32         y1 = G_MININT32, y2 = G_MAXINT32;
  gint32         x_off, y_off;
  GList         *layers;
  GList         *list;

  g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);

  layers = pika_image_list_layers (image);

  for (list = layers; list; list = g_list_next (list))
    {
      PikaDrawable *drawable = list->data;

      if (! pika_drawable_get_offsets (drawable, &x_off, &y_off))
        {
          g_list_free (layers);
          return NULL;
        }

      x1 = MAX (x1, x_off);
      y1 = MAX (y1, y_off);
      x2 = MIN (x2, x_off + pika_drawable_get_width  (drawable) - 1);
      y2 = MIN (y2, y_off + pika_drawable_get_height (drawable) - 1);
    }

  g_list_free (layers);

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