mirror of https://github.com/GNOME/gimp.git
2390 lines
78 KiB
C
2390 lines
78 KiB
C
/*
|
|
* X11 Mouse Cursor (XMC) plug-in for GIMP
|
|
*
|
|
* Copyright 2008-2009 Takeshi Matsuyama <tksmashiw@gmail.com>
|
|
*
|
|
* Special thanks: Alexia Death, Sven Neumann, Martin Nordholts
|
|
* and all community members.
|
|
*/
|
|
|
|
/*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/*
|
|
* Todo: if drawable->bpp != 4 in save_image for GIMP-2.8?
|
|
* Todo: support for "gimp-metadata" parasite.
|
|
* "xmc-copyright" and "xmc-license" may be deprecated in future?
|
|
*/
|
|
|
|
/*
|
|
* This plug-in use these four parasites.
|
|
* "hot-spot" common with file-xbm plug-in
|
|
* "xmc-copyright" original, store contents of type1 comment chunk of Xcursor
|
|
* "xmc-license" original, store contents of type2 comment chunk of Xcursor
|
|
* "gimp-comment" common, store contents of type3 comment chunk of Xcursor
|
|
*/
|
|
|
|
/* *** Caution: Size vs Dimension ***
|
|
*
|
|
* In this file, "size" and "dimension" are used in definitely
|
|
* different contexts. "Size" means nominal size of Xcursor which is
|
|
* used to determine which frame depends on which animation sequence
|
|
* and which sequence is really used. (for more detail, please read
|
|
* Xcursor(3).) On the other hand, "Dimension" simply means width
|
|
* and/or height.
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
|
|
#include <glib/gstdio.h>
|
|
#include <glib/gprintf.h>
|
|
|
|
#include <libgimp/gimp.h>
|
|
#include <libgimp/gimpui.h>
|
|
|
|
#include <X11/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.
|
|
*/
|
|
#define MAX_BITMAP_CURSOR_SIZE 64
|
|
|
|
/* The maximum dimension of each frame of X cursor we want to save
|
|
* should be MAX_BITMAP_CURSOR_SIZE but about loading, xhot(& yhot) of
|
|
* each frame varies from 0 to MAX_BITMAP_CURSOR_SIZE-1, so we need to
|
|
* set the maximum dimension of image no less than
|
|
* MAX_BITMAP_CURSOR_SIZE * 2( -1 to be precise) to remain hotspots on
|
|
* the same coordinates.
|
|
*
|
|
* We use four times value (256 for saving, 512 for loading) as a
|
|
* limitation because some cursors generated by CursorXP/FX to X11
|
|
* Mouse Theme Converter is very large.
|
|
*
|
|
* The biggest cursor I found is "watch" of OuterLimits which size is
|
|
* 213x208. If you found bigger one, please tell me ;-)
|
|
*/
|
|
#define MAX_LOAD_DIMENSION 512
|
|
#define MAX_SAVE_DIMENSION 256
|
|
|
|
/* The maximum number of different nominal sizes in one cursor this
|
|
* plug-in can treat. This is based on the number of cursor size which
|
|
* gnome-appearance-properties supports.(12,16,24,32,36,40,48,64)
|
|
* ref. capplets/common/gnome-theme-info.c in source of
|
|
* gnome-control-center
|
|
*/
|
|
#define MAX_SIZE_NUM 8
|
|
|
|
/* cursor delay is guint32 defined in Xcursor.h */
|
|
#define CURSOR_MAX_DELAY 100000000
|
|
#define CURSOR_DEFAULT_DELAY 50
|
|
#define CURSOR_MINIMUM_DELAY 5
|
|
|
|
#define div_255(x) (((x) + 0x80 + (((x) + 0x80) >> 8)) >> 8)
|
|
#define READ32(f, e) read32 ((f), (e)); if (*(e)) return -1;
|
|
#define DISPLAY_DIGIT(x) ((x) > 100) ? 3 : ((x) > 10) ? 2 : 1
|
|
|
|
/*
|
|
* Structures...
|
|
*/
|
|
|
|
typedef struct
|
|
{
|
|
gboolean crop;
|
|
gint size;
|
|
gboolean size_replace;
|
|
gint32 delay;
|
|
gboolean delay_replace;
|
|
} XmcSaveVals;
|
|
|
|
/*
|
|
* Local functions...
|
|
*/
|
|
|
|
static void query (void);
|
|
|
|
static void run (const gchar *name,
|
|
gint nparams,
|
|
const GimpParam *param,
|
|
gint *nreturn_vals,
|
|
GimpParam **return_vals);
|
|
|
|
static gint32 load_image (const gchar *filename,
|
|
GError **error);
|
|
|
|
static gint32 load_thumbnail (const gchar *filename,
|
|
gint32 thumb_size,
|
|
gint32 *width,
|
|
gint32 *height,
|
|
gint32 *num_layers,
|
|
GError **error);
|
|
|
|
static guint32 read32 (FILE *f,
|
|
GError **error);
|
|
|
|
static gboolean save_image (const gchar *filename,
|
|
gint32 image_ID,
|
|
gint32 drawable_ID,
|
|
gint32 orig_image_ID,
|
|
GError **error);
|
|
|
|
static gboolean save_dialog (const gint32 image_ID,
|
|
GimpParamRegion *hotspotRange);
|
|
|
|
static void comment_entry_callback (GtkWidget *widget,
|
|
gchar **commentp);
|
|
|
|
static void text_view_callback (GtkTextBuffer *buffer,
|
|
gchar **commentp);
|
|
|
|
static gboolean load_default_hotspot (const gint32 image_ID,
|
|
GimpParamRegion *hotspotRange);
|
|
|
|
static inline guint32 separate_alpha (guint32 pixel);
|
|
|
|
static inline guint32 premultiply_alpha (guint32 pixel);
|
|
|
|
static XcursorComments *set_cursor_comments (void);
|
|
|
|
static void load_comments (const gint32 image_ID);
|
|
|
|
static gboolean set_comment_to_pname (const gint32 image_ID,
|
|
const gchar *content,
|
|
const gchar *pname);
|
|
|
|
static gchar *get_comment_from_pname (const gint32 image_ID,
|
|
const gchar *pname);
|
|
|
|
static gboolean set_hotspot_to_parasite (gint32 image_ID);
|
|
|
|
static gboolean get_hotspot_from_parasite (gint32 image_ID);
|
|
|
|
static void set_size_and_delay (const gchar *framename,
|
|
guint32 *sizep,
|
|
guint32 *delayp,
|
|
GRegex *re,
|
|
gboolean *size_warnp);
|
|
|
|
static gchar *make_framename (guint32 size,
|
|
guint32 delay,
|
|
guint indent,
|
|
GError **errorp);
|
|
|
|
static void get_cropped_region (GimpParamRegion *retrun_rgn,
|
|
GimpPixelRgn *pr);
|
|
|
|
static inline gboolean pix_is_opaque (guint32 pix);
|
|
|
|
static GimpParamRegion *get_intersection_of_frames (gint32 image_ID);
|
|
|
|
static gboolean pix_in_region (gint32 x,
|
|
gint32 y,
|
|
GimpParamRegion *xmcrp);
|
|
|
|
static void find_hotspots_and_dimensions (XcursorImages *xcIs,
|
|
gint32 *xhot,
|
|
gint32 *yhot,
|
|
gint32 *width,
|
|
gint32 *height);
|
|
|
|
/*
|
|
* Globals...
|
|
*/
|
|
|
|
const GimpPlugInInfo PLUG_IN_INFO =
|
|
{
|
|
NULL,
|
|
NULL,
|
|
query,
|
|
run
|
|
};
|
|
|
|
static XmcSaveVals xmcvals =
|
|
{ /* saved in pdb after this plug-in's process has gone. */
|
|
FALSE, /* crop */
|
|
32, /* size */
|
|
FALSE, /* size_replace */
|
|
CURSOR_DEFAULT_DELAY, /* delay */
|
|
FALSE /* delay_replace */
|
|
};
|
|
|
|
static struct
|
|
{ /* saved as parasites of original image after this plug-in's process has gone.*/
|
|
gint32 x; /* hotspot x */
|
|
gint32 y; /* hotspot y */
|
|
gchar *comments[3]; /* copyright, license, other */
|
|
} xmcparas = {0,};
|
|
|
|
/* parasites correspond to XcursorComment type */
|
|
static const gchar *
|
|
parasiteName[3] = {"xmc-copyright", "xmc-license", "gimp-comment"};
|
|
|
|
/*
|
|
* 'main()' - Main entry - just call gimp_main()...
|
|
*/
|
|
|
|
MAIN ()
|
|
|
|
|
|
/*
|
|
* 'query()' - Respond to a plug-in query...
|
|
*/
|
|
|
|
static void
|
|
query (void)
|
|
{
|
|
static const GimpParamDef load_args[] =
|
|
{
|
|
{ GIMP_PDB_INT32, "run-mode", "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }" },
|
|
{ GIMP_PDB_STRING, "filename", "The name of the file to load" },
|
|
{ GIMP_PDB_STRING, "raw-filename", "The name of the file to load" }
|
|
};
|
|
static const GimpParamDef load_return_vals[] =
|
|
{
|
|
{ GIMP_PDB_IMAGE, "image", "Output image" }
|
|
};
|
|
|
|
static const GimpParamDef thumb_args[] =
|
|
{
|
|
{ GIMP_PDB_STRING, "filename", "The name of the file to load" },
|
|
{ GIMP_PDB_INT32, "thumb-size", "Preferred thumbnail size" }
|
|
};
|
|
static const GimpParamDef thumb_return_vals[] =
|
|
{
|
|
{ GIMP_PDB_IMAGE, "image", "Thumbnail image" },
|
|
{ GIMP_PDB_INT32, "image-width", "The width of image" },
|
|
{ GIMP_PDB_INT32, "image-height", "The height of image" },
|
|
{ GIMP_PDB_INT32, "image-type", "The color type of image"},
|
|
{ GIMP_PDB_INT32, "image-num-layers", "The number of layeres" }
|
|
};
|
|
|
|
static const GimpParamDef save_args[] =
|
|
{
|
|
{ GIMP_PDB_INT32, "run-mode", "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }" },
|
|
{ GIMP_PDB_IMAGE, "image", "Input image" },
|
|
{ GIMP_PDB_DRAWABLE, "drawable", "Drawable to save" },
|
|
{ GIMP_PDB_STRING, "filename", "The name of the file to save the image in" },
|
|
{ GIMP_PDB_STRING, "raw-filename", "The name entered" },
|
|
/* following elements are XMC specific options */
|
|
{ GIMP_PDB_INT32, "x_hot", "X-coordinate of 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,
|
|
"Saves files of X11 cursor file",
|
|
"This plug-in saves 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;
|
|
GimpParamRegion *hotspotRange = NULL;
|
|
gint i;
|
|
gint32 width, height, num_layers;
|
|
GError *error = NULL;
|
|
|
|
INIT_I18N ();
|
|
|
|
DM_XMC("run: start.\n");
|
|
*nreturn_vals = 1;
|
|
*return_vals = values;
|
|
|
|
values[0].type = GIMP_PDB_STATUS;
|
|
values[0].data.d_status = GIMP_PDB_EXECUTION_ERROR;
|
|
|
|
if (strcmp (name, LOAD_PROC) == 0)
|
|
{
|
|
DM_XMC("Starting to load file.\tparam.data=%s\n",
|
|
param[1].data.d_string);
|
|
image_ID = load_image (param[1].data.d_string, &error);
|
|
|
|
if (image_ID != -1)
|
|
{
|
|
*nreturn_vals = 2;
|
|
values[1].type = GIMP_PDB_IMAGE;
|
|
values[1].data.d_image = image_ID;
|
|
DM_XMC("LOAD_PROC successfully load image. image_ID=%i\n",image_ID);
|
|
}
|
|
else
|
|
{
|
|
status = GIMP_PDB_EXECUTION_ERROR;
|
|
}
|
|
}
|
|
else if (strcmp (name, LOAD_THUMB_PROC) == 0)
|
|
{
|
|
DM_XMC("Starting to load thumbnail.\tfilename=%s\tthumb-size=%d\n",
|
|
param[0].data.d_string, param[1].data.d_int32);
|
|
image_ID = load_thumbnail (param[0].data.d_string,
|
|
param[1].data.d_int32,
|
|
&width,
|
|
&height,
|
|
&num_layers,
|
|
&error);
|
|
|
|
if (image_ID != -1)
|
|
{
|
|
*nreturn_vals = 6;
|
|
values[1].type = GIMP_PDB_IMAGE;
|
|
values[1].data.d_image = image_ID;
|
|
values[2].type = GIMP_PDB_INT32;
|
|
values[2].data.d_int32 = width; /* width */
|
|
values[3].type = GIMP_PDB_INT32;
|
|
values[3].data.d_int32 = height; /* height */
|
|
/* This will not work on GIMP 2.6, but not harmful. */
|
|
values[4].type = GIMP_PDB_INT32;
|
|
values[4].data.d_int32 = GIMP_RGBA_IMAGE; /* type */
|
|
values[5].type = GIMP_PDB_INT32;
|
|
values[5].data.d_int32 = num_layers; /* num_layers */
|
|
|
|
DM_XMC("LOAD_THUMB_PROC successfully load image. image_ID=%i\n",image_ID);
|
|
}
|
|
else
|
|
{
|
|
status = GIMP_PDB_EXECUTION_ERROR;
|
|
}
|
|
}
|
|
else if (strcmp (name, SAVE_PROC) == 0)
|
|
{
|
|
DM_XMC("run: save %s\n", name);
|
|
run_mode = param[0].data.d_int32;
|
|
image_ID = orig_image_ID = param[1].data.d_int32;
|
|
drawable_ID = param[2].data.d_int32;
|
|
hotspotRange = get_intersection_of_frames (image_ID);
|
|
|
|
if (! hotspotRange)
|
|
{
|
|
g_set_error (&error, 0, 0,
|
|
_("Cannot set the 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, NULL,
|
|
(GIMP_EXPORT_CAN_HANDLE_RGB |
|
|
GIMP_EXPORT_CAN_HANDLE_ALPHA |
|
|
GIMP_EXPORT_CAN_HANDLE_LAYERS |
|
|
GIMP_EXPORT_NEEDS_ALPHA));
|
|
if (export == GIMP_EXPORT_CANCEL)
|
|
{
|
|
*nreturn_vals = 1;
|
|
values[0].data.d_status = GIMP_PDB_CANCEL;
|
|
|
|
return;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
switch (run_mode)
|
|
{
|
|
case GIMP_RUN_INTERACTIVE:
|
|
/*
|
|
* Possibly retrieve data...
|
|
*/
|
|
gimp_get_data (SAVE_PROC, &xmcvals);
|
|
/* load xmcparas.comments from parasite. */
|
|
load_comments (image_ID);
|
|
|
|
load_default_hotspot (image_ID, hotspotRange);
|
|
|
|
if (! save_dialog (image_ID, hotspotRange))
|
|
status = GIMP_PDB_CANCEL;
|
|
break;
|
|
|
|
case GIMP_RUN_NONINTERACTIVE:
|
|
/*
|
|
* Make sure all the arguments are there!
|
|
*/
|
|
if (nparams != 15)
|
|
{
|
|
status = GIMP_PDB_CALLING_ERROR;
|
|
}
|
|
else
|
|
{
|
|
if (pix_in_region (param[5].data.d_int32, param[6].data.d_int32,
|
|
hotspotRange))
|
|
{ /* if passed hotspot is acceptable, use that ones. */
|
|
xmcparas.x = param[5].data.d_int32;
|
|
xmcparas.y = param[6].data.d_int32;
|
|
}
|
|
else
|
|
{
|
|
load_default_hotspot (image_ID, hotspotRange);
|
|
/* you can purposely choose non acceptable values for hotspot
|
|
to use cursor's original values. */
|
|
}
|
|
xmcvals.crop = param[7].data.d_int32;
|
|
xmcvals.size = param[8].data.d_int32;
|
|
xmcvals.size_replace = param[9].data.d_int32;
|
|
/* load delay */
|
|
if (param[10].data.d_int32 < CURSOR_MINIMUM_DELAY)
|
|
{
|
|
xmcvals.delay = CURSOR_DEFAULT_DELAY;
|
|
}
|
|
else
|
|
{
|
|
xmcvals.delay = param[10].data.d_int32;
|
|
}
|
|
xmcvals.delay_replace = param[11].data.d_int32;
|
|
/* load xmcparas.comments from parasites.*/
|
|
load_comments (image_ID);
|
|
for (i = 0; i < 3; ++i)
|
|
{
|
|
if (param[i + 12].data.d_string &&
|
|
g_utf8_validate (param[i + 12].data.d_string, -1, NULL))
|
|
{
|
|
xmcparas.comments[i] = g_strdup (param[i + 12].data.d_string);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case GIMP_RUN_WITH_LAST_VALS:
|
|
/*
|
|
* Possibly retrieve data...
|
|
*/
|
|
gimp_get_data (SAVE_PROC, &xmcvals);
|
|
/* load xmcparas.comments from parasite. */
|
|
load_comments (image_ID);
|
|
/* load hotspot from parasite */
|
|
load_default_hotspot (image_ID, hotspotRange);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
if (status == GIMP_PDB_SUCCESS)
|
|
{
|
|
if (save_image (param[3].data.d_string, image_ID,
|
|
drawable_ID, orig_image_ID, &error))
|
|
{
|
|
gimp_set_data (SAVE_PROC, &xmcvals, sizeof (XmcSaveVals));
|
|
}
|
|
else
|
|
{
|
|
status = GIMP_PDB_EXECUTION_ERROR;
|
|
}
|
|
}
|
|
|
|
if (export == GIMP_EXPORT_EXPORT)
|
|
gimp_image_delete (image_ID);
|
|
/* free hotspotRange */
|
|
g_free (hotspotRange);
|
|
/* free xmcparas.comments */
|
|
for (i = 0; i < 3 ; ++i)
|
|
{
|
|
g_free (xmcparas.comments[i]);
|
|
xmcparas.comments[i] = NULL;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DM_XMC("name=%s\n", name);
|
|
status = GIMP_PDB_CALLING_ERROR;
|
|
}
|
|
|
|
if (status != GIMP_PDB_SUCCESS && error)
|
|
{
|
|
*nreturn_vals = 2;
|
|
values[1].type = GIMP_PDB_STRING;
|
|
values[1].data.d_string = error->message;
|
|
}
|
|
|
|
values[0].data.d_status = status;
|
|
DM_XMC("run: finish\n");
|
|
}
|
|
|
|
|
|
/*
|
|
* 'load_image()' - Load a X cursor image into a new image window.
|
|
*/
|
|
|
|
static gint32
|
|
load_image (const gchar *filename, GError **error)
|
|
{
|
|
gint i, j; /* Looping var */
|
|
gint img_width, img_height; /* dimensions of the image */
|
|
FILE *fp; /* File pointer */
|
|
gint32 image_ID; /* Image */
|
|
gint32 layer_ID; /* Layer */
|
|
GimpDrawable *drawable; /* Drawable for layer */
|
|
GimpPixelRgn pixel_rgn; /* Pixel region for layer */
|
|
XcursorComments *commentsp; /* pointer to comments */
|
|
XcursorImages *imagesp; /* pointer to images*/
|
|
guint32 delay; /* use guint32 instead CARD32(defined in X11/Xmd.h)*/
|
|
gchar *framename; /* name of layer */
|
|
guint32 *tmppixel; /* pixel data (guchar * bpp = guint32) */
|
|
|
|
/*
|
|
* Open the file and check it is a valid X cursor
|
|
*/
|
|
|
|
fp = g_fopen (filename, "rb");
|
|
|
|
if (fp == NULL)
|
|
{
|
|
g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
|
|
_("Could not open '%s' for reading: %s"),
|
|
gimp_filename_to_utf8 (filename), g_strerror (errno));
|
|
return -1;
|
|
}
|
|
|
|
if (!XcursorFileLoad (fp, &commentsp, &imagesp))
|
|
{
|
|
g_set_error (error, 0, 0, _("'%s' is not a valid X cursor."),
|
|
gimp_filename_to_utf8 (filename));
|
|
return -1;
|
|
}
|
|
|
|
gimp_progress_init_printf (_("Opening '%s'"),
|
|
gimp_filename_to_utf8 (filename));
|
|
|
|
/*
|
|
** check dimension is valid.
|
|
*/
|
|
for (i = 0; i < imagesp->nimage; i++)
|
|
{
|
|
if (imagesp->images[i]->width > MAX_LOAD_DIMENSION)
|
|
{
|
|
g_set_error (error, 0, 0,
|
|
_("Frame %d of '%s' is too wide for an X cursor."),
|
|
i + 1, gimp_filename_to_utf8 (filename));
|
|
return -1;
|
|
}
|
|
if (imagesp->images[i]->height > MAX_LOAD_DIMENSION)
|
|
{
|
|
g_set_error (error, 0, 0,
|
|
_("Frame %d of '%s' is too high for an X cursor."),
|
|
i + 1, gimp_filename_to_utf8 (filename));
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
find_hotspots_and_dimensions (imagesp,
|
|
&xmcparas.x, &xmcparas.y,
|
|
&img_width, &img_height);
|
|
|
|
DM_XMC("xhot=%i,\tyhot=%i,\timg_width=%i,\timg_height=%i\n",
|
|
xmcparas.x, xmcparas.y, img_width, img_height);
|
|
|
|
/* create new image! */
|
|
image_ID = gimp_image_new (img_width, img_height, GIMP_RGB);
|
|
|
|
/* set filename */
|
|
gimp_image_set_filename (image_ID, filename);
|
|
|
|
if (! set_hotspot_to_parasite (image_ID))
|
|
return -1;
|
|
|
|
/* Temporary buffer */
|
|
tmppixel = g_new (guint32, img_width * img_height);
|
|
|
|
/* load each frame to each layer one by one */
|
|
for (i = 0; i < imagesp->nimage; i++)
|
|
{
|
|
delay = imagesp->images[i]->delay;
|
|
if (delay < CURSOR_MINIMUM_DELAY)
|
|
{
|
|
delay = CURSOR_DEFAULT_DELAY;
|
|
}
|
|
|
|
DM_XMC("images[%i]->delay=%i\twidth=%d\theight=%d\n"
|
|
,i ,delay, imagesp->images[i]->width, imagesp->images[i]->height);
|
|
|
|
framename = make_framename (imagesp->images[i]->size, delay,
|
|
DISPLAY_DIGIT(imagesp->nimage), error);
|
|
if (!framename)
|
|
return -1;
|
|
|
|
layer_ID = gimp_layer_new (image_ID, framename,
|
|
imagesp->images[i]->width,
|
|
imagesp->images[i]->height,
|
|
GIMP_RGBA_IMAGE, 100, GIMP_NORMAL_MODE);
|
|
gimp_image_insert_layer (image_ID, layer_ID, -1, 0);
|
|
|
|
/* Adjust layer position to let hotspot sit on the same point. */
|
|
gimp_layer_translate (layer_ID,
|
|
xmcparas.x - imagesp->images[i]->xhot,
|
|
xmcparas.y - imagesp->images[i]->yhot);
|
|
|
|
g_free (framename);
|
|
|
|
/*
|
|
* Get the drawable and set the pixel region for our load...
|
|
*/
|
|
|
|
drawable = gimp_drawable_get (layer_ID);
|
|
|
|
gimp_pixel_rgn_init (&pixel_rgn, drawable, 0, 0, drawable->width,
|
|
drawable->height, TRUE, FALSE);
|
|
|
|
/* set color to each pixel */
|
|
for (j = 0; j < drawable->width * drawable->height; j++)
|
|
{
|
|
tmppixel[j] = separate_alpha (imagesp->images[i]->pixels[j]) ;
|
|
}
|
|
/* set pixel */
|
|
gimp_pixel_rgn_set_rect (&pixel_rgn, (guchar *)tmppixel,
|
|
0, 0, drawable->width, drawable->height);
|
|
|
|
gimp_progress_update ( (i + 1) / imagesp->nimage);
|
|
|
|
gimp_drawable_flush (drawable);
|
|
gimp_drawable_detach (drawable);
|
|
}
|
|
gimp_progress_update (1.0);
|
|
/* free temporary buffer */
|
|
g_free(tmppixel);
|
|
|
|
/*
|
|
* Comment parsing
|
|
*/
|
|
|
|
if (commentsp)
|
|
{
|
|
for (i = 0; i < commentsp->ncomment; ++i)
|
|
{
|
|
DM_XMC ("comment type=%d\tcomment=%s\n",
|
|
commentsp->comments[i]->comment_type,
|
|
commentsp->comments[i]->comment);
|
|
if (! set_comment_to_pname (image_ID,
|
|
commentsp->comments[i]->comment,
|
|
parasiteName[commentsp->comments[i]->comment_type -1]))
|
|
{
|
|
DM_XMC ("Failed to write %ith comment.\n", i);
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
DM_XMC("Comment parsing done.\n");
|
|
XcursorImagesDestroy (imagesp);
|
|
XcursorCommentsDestroy (commentsp);
|
|
fclose (fp);
|
|
|
|
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. */
|
|
|
|
gint i; /* Looping var */
|
|
guint32 ntoc = 0; /* the number of table of contents */
|
|
gint sel_num = -1; /* the index of selected image chunk */
|
|
XcursorImages *xcIs = NULL; /* use to find the dimensions of thumbnail */
|
|
XcursorImage *xcI; /* temporary pointer to XcursorImage */
|
|
guint32 *positions; /* array of the offsets of image chunks */
|
|
guint32 size; /* nominal size */
|
|
guint32 diff; /* difference between thumb_size and current size */
|
|
guint32 min_diff = XCURSOR_IMAGE_MAX_SIZE; /* minimum value of diff */
|
|
guint32 type; /* chunk type */
|
|
FILE *fp = NULL; /* File pointer */
|
|
gint32 image_ID = -1; /* Image */
|
|
gint32 layer_ID; /* Layer */
|
|
GimpDrawable *drawable; /* Drawable for layer */
|
|
GimpPixelRgn pixel_rgn; /* Pixel region for layer */
|
|
guint32 *tmppixel; /* pixel data (guchar * bpp = guint32) */
|
|
|
|
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)
|
|
positions = g_malloc (ntoc * sizeof (guint32));
|
|
|
|
/* enter list of toc(table of contents) */
|
|
for (; ntoc > 0; --ntoc)
|
|
{
|
|
/* read entry type */
|
|
type = READ32 (fp, error)
|
|
if (type != XCURSOR_IMAGE_TYPE) /* not a image */
|
|
/* skip rest of this content */
|
|
fseek (fp, 8, SEEK_CUR);
|
|
|
|
else
|
|
{/* this content is image */
|
|
size = READ32 (fp, error)
|
|
positions[*thumb_num_layers] = READ32 (fp, error)
|
|
/* is this image is more preferred than selected before? */
|
|
diff = ABS(thumb_size - size);
|
|
if (diff < min_diff)
|
|
{/* the image size is closer than current selected image */
|
|
min_diff = diff;
|
|
sel_num = *thumb_num_layers;
|
|
}
|
|
++*thumb_num_layers;
|
|
}
|
|
}
|
|
|
|
if (sel_num < 0)
|
|
{
|
|
g_set_error (error, 0, 0,
|
|
_("there is no image chunk in \"%s\"."),
|
|
gimp_filename_to_utf8 (filename));
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* get width and height of entire image
|
|
*/
|
|
|
|
/* Let's make XcursorImages */
|
|
xcIs = XcursorImagesCreate (*thumb_num_layers);
|
|
xcIs->nimage = *thumb_num_layers;
|
|
for (i = 0; i < xcIs->nimage; ++i)
|
|
{
|
|
/* make XcursorImage with no pixel buffer */
|
|
xcI = XcursorImageCreate (0, 0);
|
|
/* go to the image chunk header */
|
|
fseek (fp, positions[i], SEEK_SET);
|
|
/* skip chunk header */
|
|
fseek (fp, 16, SEEK_CUR);
|
|
/* read properties of this image to determine entire image dimensions */
|
|
xcI->width = READ32 (fp, error)
|
|
xcI->height = READ32 (fp, error)
|
|
xcI->xhot = READ32 (fp, error)
|
|
xcI->yhot = READ32 (fp, error)
|
|
|
|
xcIs->images[i] = xcI;
|
|
}
|
|
|
|
DM_XMC("selected size is %i or %i\n",
|
|
thumb_size - min_diff, thumb_size + min_diff);
|
|
|
|
/* get entire image dimensions */
|
|
find_hotspots_and_dimensions (xcIs, NULL, NULL, thumb_width, thumb_height);
|
|
|
|
DM_XMC("width=%i\theight=%i\tnum-layers=%i\n",
|
|
*thumb_width, *thumb_height, xcIs->nimage);
|
|
|
|
/* dimension check */
|
|
if (*thumb_width > MAX_LOAD_DIMENSION)
|
|
{
|
|
g_set_error (error, 0, 0,
|
|
_("'%s' is too wide for an X cursor."),
|
|
gimp_filename_to_utf8 (filename));
|
|
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));
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* create new image!
|
|
*/
|
|
image_ID = gimp_image_new (xcIs->images[sel_num]->width,
|
|
xcIs->images[sel_num]->height, GIMP_RGB);
|
|
|
|
layer_ID = gimp_layer_new (image_ID, NULL,
|
|
xcIs->images[sel_num]->width,
|
|
xcIs->images[sel_num]->height,
|
|
GIMP_RGBA_IMAGE, 100,
|
|
GIMP_NORMAL_MODE);
|
|
|
|
gimp_image_insert_layer (image_ID, layer_ID, -1, 0);
|
|
|
|
/*
|
|
* Get the drawable and set the pixel region for our load...
|
|
*/
|
|
|
|
drawable = gimp_drawable_get (layer_ID);
|
|
|
|
gimp_pixel_rgn_init (&pixel_rgn, drawable, 0, 0,
|
|
xcIs->images[sel_num]->width,
|
|
xcIs->images[sel_num]->height,
|
|
TRUE, FALSE);
|
|
|
|
/* Temporary buffer */
|
|
tmppixel = g_new (guint32,
|
|
xcIs->images[sel_num]->width *
|
|
xcIs->images[sel_num]->height);
|
|
|
|
/* copy the chunk data to tmppixel */
|
|
fseek (fp, positions[sel_num], SEEK_SET);
|
|
fseek (fp, 36, SEEK_CUR); /* skip chunk header(16bytes), xhot, yhot, width, height, delay */
|
|
|
|
for (i = 0;
|
|
i < xcIs->images[sel_num]->width * xcIs->images[sel_num]->height;
|
|
i++)
|
|
{
|
|
tmppixel[i] = READ32 (fp, error)
|
|
/* get back separate alpha */
|
|
tmppixel[i] = separate_alpha (tmppixel[i]);
|
|
}
|
|
|
|
/* set pixel */
|
|
gimp_pixel_rgn_set_rect (&pixel_rgn, (guchar *)tmppixel, 0, 0,
|
|
drawable->width, drawable->height);
|
|
/* free tmppixel */
|
|
g_free(tmppixel);
|
|
g_free (positions);
|
|
fclose (fp);
|
|
|
|
gimp_drawable_flush (drawable);
|
|
gimp_drawable_detach (drawable);
|
|
|
|
return image_ID;
|
|
}
|
|
|
|
/* read guint32 value from f despite of host's byte order. */
|
|
static guint32
|
|
read32 (FILE *f, GError **error)
|
|
{
|
|
guchar p[4];
|
|
guint32 ret;
|
|
if (fread (p, 1, 4, f) != 4)
|
|
{
|
|
g_set_error (error, 0, 0, _("A read error occurred."));
|
|
return 0;
|
|
}
|
|
|
|
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
|
|
ret = p[0] + (p[1]<<8) + (p[2]<<16) + (p[3]<<24);
|
|
#elif G_BYTE_ORDER == G_BIG_ENDIAN
|
|
ret = p[3] + (p[2]<<8) + (p[1]<<16) + (p[0]<<24);
|
|
#elif G_BYTE_ORDER == G_PDP_ENDIAN
|
|
ret = p[2] + (p[3]<<8) + (p[0]<<16) + (p[1]<<24);
|
|
#else
|
|
g_return_val_if_rearched ();
|
|
#endif
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* 'save_dialog ()'
|
|
*/
|
|
|
|
static gboolean
|
|
save_dialog (const gint32 image_ID, GimpParamRegion *hotspotRange)
|
|
{
|
|
gint x1, x2, y1, y2;
|
|
GtkWidget *dialog;
|
|
GtkWidget *frame;
|
|
GtkWidget *table;
|
|
GtkWidget *box;
|
|
GtkObject *adjustment;
|
|
GtkWidget *alignment;
|
|
GtkWidget *tmpwidget;
|
|
GtkWidget *label;
|
|
GtkTextBuffer *textbuffer;
|
|
GValue val = {0,};
|
|
gboolean run;
|
|
|
|
g_value_init (&val, G_TYPE_DOUBLE);
|
|
dialog = gimp_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;
|
|
tmpwidget =
|
|
gimp_spin_button_new (&adjustment, xmcparas.x, x1, x2, 1, 5, 0, 0, 0);
|
|
gtk_widget_show (tmpwidget);
|
|
g_value_set_double (&val, 1.0);
|
|
g_object_set_property (G_OBJECT (tmpwidget), "xalign", &val);/* align right*/
|
|
g_signal_connect (adjustment, "value-changed",
|
|
G_CALLBACK (gimp_int_adjustment_update),
|
|
&xmcparas.x);
|
|
gimp_help_set_help_data (tmpwidget,
|
|
_("Enter the X coordinate of the hot spot. "
|
|
"The origin is top left corner."),
|
|
NULL);
|
|
gimp_table_attach_aligned (GTK_TABLE (table), 0, 0,
|
|
_("Hot spot _X:"), 0, 0.5, tmpwidget, 1, TRUE);
|
|
/* label "Y:" + spinbox */
|
|
y1 = hotspotRange->y;
|
|
y2 = hotspotRange->height + hotspotRange->y - 1;
|
|
tmpwidget =
|
|
gimp_spin_button_new (&adjustment, xmcparas.y, y1, y2, 1, 5, 0, 0, 0);
|
|
gtk_widget_show (tmpwidget);
|
|
g_value_set_double (&val, 1.0);
|
|
g_object_set_property (G_OBJECT (tmpwidget), "xalign", &val);/* align right*/
|
|
g_signal_connect (adjustment, "value-changed",
|
|
G_CALLBACK (gimp_int_adjustment_update),
|
|
&xmcparas.y);
|
|
/* tooltip */
|
|
gimp_help_set_help_data (tmpwidget,
|
|
_("Enter the Y coordinate of the hot spot. "
|
|
"The origin is top left corner."),
|
|
NULL);
|
|
gimp_table_attach_aligned (GTK_TABLE (table), 1, 0,
|
|
"_Y:", 1.0, 0.5, tmpwidget, 1, TRUE);
|
|
|
|
/*
|
|
* Auto-crop
|
|
*/
|
|
/* check button */
|
|
tmpwidget =
|
|
gtk_check_button_new_with_mnemonic (_("_Auto-Crop all frames."));
|
|
gtk_table_attach (GTK_TABLE (table),
|
|
tmpwidget, 0, 3, 1, 2, GTK_FILL, 0, 0, 10);
|
|
gtk_widget_show (tmpwidget);
|
|
|
|
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (tmpwidget),
|
|
xmcvals.crop);
|
|
gtk_widget_show (tmpwidget);
|
|
g_signal_connect (tmpwidget, "toggled",
|
|
G_CALLBACK (gimp_toggle_button_update),
|
|
&xmcvals.crop);
|
|
/* tooltip */
|
|
gimp_help_set_help_data (tmpwidget,
|
|
_("Remove the empty borders of all frames.\n"
|
|
"This 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);
|
|
gtk_widget_show (box);
|
|
tmpwidget = gimp_spin_button_new (&adjustment,
|
|
xmcvals.delay, CURSOR_MINIMUM_DELAY,
|
|
CURSOR_MAX_DELAY, 1, 5, 0, 1, 0);
|
|
gtk_widget_show (tmpwidget);
|
|
g_value_set_double (&val, 1.0);
|
|
g_object_set_property (G_OBJECT (tmpwidget), "xalign", &val);/* align right*/
|
|
gtk_box_pack_start (GTK_BOX (box), tmpwidget, TRUE, TRUE, 0);
|
|
g_signal_connect (adjustment, "value-changed",
|
|
G_CALLBACK (gimp_int_adjustment_update),
|
|
&xmcvals.delay);
|
|
/* appended "ms" */
|
|
tmpwidget = gtk_label_new ("ms");
|
|
gtk_misc_set_alignment (GTK_MISC (tmpwidget), 0, 0.5); /*align left*/
|
|
gtk_box_pack_start (GTK_BOX (box), tmpwidget, TRUE, TRUE, 0);
|
|
gtk_widget_show (tmpwidget);
|
|
/* tooltip */
|
|
gimp_help_set_help_data (box,
|
|
_("Enter time span in milliseconds in which "
|
|
"each frame is rendered."),
|
|
NULL);
|
|
gimp_table_attach_aligned (GTK_TABLE (table), 0, 4, _("_Delay:"), 0, 0.5, box, 3, TRUE);
|
|
/* Replace delay? */
|
|
tmpwidget =
|
|
gimp_int_radio_group_new (FALSE, NULL, G_CALLBACK (gimp_radio_button_update),
|
|
&xmcvals.delay_replace, xmcvals.delay_replace,
|
|
_("_Use this value only for a frame which delay "
|
|
"is not specified."),
|
|
FALSE, NULL,
|
|
_("_Replace the delay of all frames even if it "
|
|
"is specified."),
|
|
TRUE, NULL,
|
|
NULL);
|
|
alignment = gtk_alignment_new (0.5, 0.5, 1.0, 1.0);
|
|
gtk_widget_show (alignment);
|
|
gtk_table_attach (GTK_TABLE (table), alignment, 0, 3, 5, 6, 0, 0, 0, 0);
|
|
gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 0, 6, 20, 0); /*padding left*/
|
|
gtk_container_add (GTK_CONTAINER (alignment), tmpwidget);
|
|
gtk_widget_show (tmpwidget);
|
|
|
|
/*
|
|
* Copyright
|
|
*/
|
|
tmpwidget = gtk_entry_new();
|
|
/* Maximum length will be clamped to 65536 */
|
|
gtk_entry_set_max_length (GTK_ENTRY (tmpwidget), XCURSOR_COMMENT_MAX_LEN);
|
|
|
|
if (xmcparas.comments[0])
|
|
{
|
|
gtk_entry_set_text (GTK_ENTRY (tmpwidget),
|
|
gimp_any_to_utf8 (xmcparas.comments[0], - 1, NULL));
|
|
/* show warning if comment is over 65535 characters
|
|
* because gtk_entry can hold only that. */
|
|
if (strlen (gtk_entry_get_text (GTK_ENTRY (tmpwidget))) >= 65535)
|
|
g_message (_("The part of copyright information "
|
|
"that exceeded 65535 characters was removed."));
|
|
}
|
|
|
|
g_signal_connect (tmpwidget, "changed",
|
|
G_CALLBACK (comment_entry_callback),
|
|
xmcparas.comments);
|
|
gtk_widget_show (tmpwidget);
|
|
/* tooltip */
|
|
gimp_help_set_help_data (tmpwidget,
|
|
_("Enter copyright information."),
|
|
NULL);
|
|
gimp_table_attach_aligned (GTK_TABLE (table), 0, 6, _("_Copyright:"),
|
|
0, 0.5, tmpwidget, 3, FALSE);
|
|
/*
|
|
* License
|
|
*/
|
|
tmpwidget = gtk_entry_new();
|
|
/* Maximum length will be clamped to 65536 */
|
|
gtk_entry_set_max_length (GTK_ENTRY (tmpwidget), XCURSOR_COMMENT_MAX_LEN);
|
|
|
|
if (xmcparas.comments[1])
|
|
{
|
|
gtk_entry_set_text (GTK_ENTRY (tmpwidget),
|
|
gimp_any_to_utf8 (xmcparas.comments[1], - 1, NULL));
|
|
/* show warning if comment is over 65535 characters
|
|
* because gtk_entry can hold only that. */
|
|
if (strlen (gtk_entry_get_text (GTK_ENTRY (tmpwidget))) >= 65535)
|
|
g_message (_("The part of license information "
|
|
"that exceeded 65535 characters was removed."));
|
|
}
|
|
|
|
g_signal_connect (tmpwidget, "changed",
|
|
G_CALLBACK (comment_entry_callback),
|
|
xmcparas.comments + 1);
|
|
gtk_widget_show (tmpwidget);
|
|
/* tooltip */
|
|
gimp_help_set_help_data (tmpwidget,
|
|
_("Enter license information."),
|
|
NULL);
|
|
gimp_table_attach_aligned (GTK_TABLE (table), 0, 7, _("_License:"),
|
|
0, 0.5, tmpwidget, 3, FALSE);
|
|
/*
|
|
* Other
|
|
*/
|
|
/* We use gtk_text_view for "Other" while "Copyright" & "License" is entered
|
|
* in gtk_entry because We want allow '\n' for "Other". */
|
|
label = gtk_label_new_with_mnemonic (_("_Other:"));
|
|
gtk_widget_show (label);
|
|
gtk_misc_set_alignment (GTK_MISC (label), 0, 0); /*align top-left*/
|
|
gtk_table_attach (GTK_TABLE (table), label, 0, 1, 8, 9, GTK_FILL, 0, 0, 0);
|
|
/* content of Other */
|
|
/* scrolled window */
|
|
box = gtk_scrolled_window_new (NULL, NULL);
|
|
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (box),
|
|
GTK_SHADOW_IN);
|
|
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (box),
|
|
GTK_POLICY_AUTOMATIC,
|
|
GTK_POLICY_AUTOMATIC);
|
|
gtk_table_attach (GTK_TABLE (table), box, 1, 3, 8, 9, GTK_FILL, 0, 0, 0);
|
|
gtk_widget_show (box);
|
|
/* textbuffer */
|
|
textbuffer = gtk_text_buffer_new (NULL);
|
|
if (xmcparas.comments[2])
|
|
gtk_text_buffer_set_text (textbuffer,
|
|
gimp_any_to_utf8 (xmcparas.comments[2], -1, NULL),
|
|
-1);
|
|
g_signal_connect (textbuffer, "changed",
|
|
G_CALLBACK (text_view_callback),
|
|
xmcparas.comments + 2);
|
|
/* textview */
|
|
tmpwidget =
|
|
gtk_text_view_new_with_buffer (GTK_TEXT_BUFFER (textbuffer));
|
|
gtk_text_view_set_accepts_tab (GTK_TEXT_VIEW (tmpwidget), FALSE);
|
|
gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tmpwidget), GTK_WRAP_WORD);
|
|
g_object_unref (textbuffer);
|
|
gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tmpwidget), GTK_WRAP_WORD);
|
|
gtk_box_pack_start (GTK_BOX (box), tmpwidget, TRUE, TRUE, 0);
|
|
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,
|
|
GimpParamRegion *hotspotRange)
|
|
{
|
|
|
|
g_return_val_if_fail(hotspotRange, FALSE);
|
|
|
|
if ( /* if we cannot load hotspot correctly */
|
|
! get_hotspot_from_parasite (image_ID) ||
|
|
/* ,or hostspot is out of range */
|
|
! pix_in_region (xmcparas.x, xmcparas.y, hotspotRange))
|
|
{ /* then use top left point of hotspotRange as fallback. */
|
|
xmcparas.x = hotspotRange->x;
|
|
xmcparas.y = hotspotRange->y;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
/*
|
|
* 'save_image ()' - Save the specified image to X cursor file.
|
|
*/
|
|
|
|
static gboolean
|
|
save_image (const gchar *filename,
|
|
gint32 image_ID,
|
|
gint32 drawable_ID,
|
|
gint32 orig_image_ID,
|
|
GError **error)
|
|
{
|
|
gint i, j; /* Looping vars */
|
|
FILE *fp; /* File pointer */
|
|
gboolean dimension_warn = FALSE; /* become TRUE if even one of the
|
|
dimensions of the frames of the cursor is over MAX_BITMAP_CURSOR_SIZE */
|
|
gboolean size_warn = FALSE; /* become TRUE if even one of the nominal
|
|
size of the frames is not supported by gnome-appearance-properties */
|
|
GRegex *re; /* used to get size and delay from framename */
|
|
GimpDrawable *drawable; /* Drawable for layer */
|
|
GimpPixelRgn pixel_rgn; /* Pixel region for layer */
|
|
XcursorComments *commentsp; /* pointer to comments */
|
|
XcursorImages *imagesp; /* pointer to images */
|
|
gint32 *layers; /* Array of layer */
|
|
gint32 *orig_layers; /* Array of layer of orig_image */
|
|
gint nlayers; /* Number of layers */
|
|
gchar *framename; /* framename of a layer */
|
|
GimpParamRegion save_rgn; /* region to save */
|
|
gint layer_xoffset, layer_yoffset;
|
|
/* temporary buffer which store pixel data (guchar * bpp = guint32) */
|
|
guint32 pixelbuf[SQR(MAX_SAVE_DIMENSION)];
|
|
|
|
/* This will be used in set_size_and_delay fucntion later.
|
|
To define this in that function is easy to read but place here to
|
|
reduce overheads. */
|
|
re = g_regex_new ("[(][ ]*(\\d+)[ ]*(px|ms)[ ]*[)]",
|
|
G_REGEX_CASELESS | G_REGEX_OPTIMIZE,
|
|
0,
|
|
NULL);
|
|
|
|
/*
|
|
* Open the file pointer.
|
|
*/
|
|
DM_XMC ("Open the file pointer.\n");
|
|
fp = g_fopen (filename, "wb");
|
|
if (fp == NULL)
|
|
{
|
|
g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
|
|
_("Could not open '%s' for writing: %s"),
|
|
gimp_filename_to_utf8 (filename), g_strerror (errno));
|
|
return FALSE;
|
|
}
|
|
|
|
gimp_progress_init_printf (_("Saving '%s'"),
|
|
gimp_filename_to_utf8 (filename));
|
|
|
|
/* get layers */
|
|
orig_layers = gimp_image_get_layers (orig_image_ID, &nlayers);
|
|
layers = gimp_image_get_layers (image_ID, &nlayers);
|
|
|
|
/* create new XcursorImages. */
|
|
imagesp = XcursorImagesCreate(nlayers);
|
|
if (!imagesp)
|
|
{
|
|
DM_XMC("Failed to XcursorImagesCreate!\n");
|
|
return FALSE;
|
|
}
|
|
imagesp->nimage = nlayers;
|
|
|
|
/* XcursorImages also have `name' member but it is not used as long as I know.
|
|
We leave it NULL here. */
|
|
|
|
/*
|
|
* Now we start to convert each layer to a XcurosrImage one by one.
|
|
*/
|
|
for (i = 0; i < nlayers; i++)
|
|
{
|
|
drawable = gimp_drawable_get (layers[nlayers - 1 - i]);
|
|
/* this plugin only treat 8bit color depth RGBA image. */
|
|
if (drawable->bpp != 4)
|
|
{
|
|
g_set_error (error, 0, 0,
|
|
_("This plug-in can only handle RGBA image files with 8bit color depth."));
|
|
return FALSE;
|
|
}
|
|
/* 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 (drawable->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);
|
|
return FALSE;
|
|
}
|
|
if (drawable->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);
|
|
return FALSE;
|
|
}
|
|
if (drawable->height == 0 ||drawable->width == 0)
|
|
{
|
|
g_set_error (error, 0, 0,
|
|
_("Width and/or height of frame '%s' is zero!"),
|
|
gimp_any_to_utf8 (framename, -1, NULL));
|
|
return FALSE;
|
|
}
|
|
|
|
gimp_pixel_rgn_init (&pixel_rgn, drawable, 0, 0, drawable->width,
|
|
drawable->height, FALSE, FALSE);
|
|
|
|
if (xmcvals.crop) /* with auto-cropping */
|
|
{
|
|
/* get the region of auto-cropped area. */
|
|
DM_XMC("get_cropped_region\n");
|
|
get_cropped_region (&save_rgn, &pixel_rgn);
|
|
/* don't forget save_rgn's origin is not a entire image
|
|
but a layer which we are doing on.*/
|
|
|
|
if (save_rgn.width == 0 || save_rgn.height == 0)
|
|
{/* perfectly transparent frames become 1x1px transparent pixel. */
|
|
DM_XMC("get_cropped_region return 0.\n");
|
|
imagesp->images[i] = XcursorImageCreate (1, 1);
|
|
if (!imagesp->images[i])
|
|
{
|
|
DM_XMC ("Failed to XcursorImageCreate.\n");
|
|
return FALSE;
|
|
}
|
|
imagesp->images[i]->pixels[0] = 0x0;
|
|
imagesp->images[i]->xhot = 0;
|
|
imagesp->images[i]->yhot = 0;
|
|
set_size_and_delay (framename, &(imagesp->images[i]->size),
|
|
&(imagesp->images[i]->delay), re,
|
|
&size_warn);
|
|
continue;
|
|
}
|
|
/* OK save_rgn is not 0x0 */
|
|
/* is hotspot in save_rgn ? */
|
|
if (! pix_in_region (xmcparas.x - layer_xoffset,
|
|
xmcparas.y - layer_yoffset,
|
|
&save_rgn))
|
|
{ /* if hotspot is not on save_rgn */
|
|
g_set_error (error, 0, 0,
|
|
_("Cannot save the cursor because the hot spot "
|
|
"is not on frame '%s'.\n"
|
|
"Try to change the hot spot position, "
|
|
"layer geometry or save without auto-crop."),
|
|
gimp_any_to_utf8 (framename, -1, NULL));
|
|
return FALSE;
|
|
}
|
|
}
|
|
else /* if without auto-cropping... */
|
|
{
|
|
/* set save_rgn for the case not to auto-crop */
|
|
save_rgn.width = drawable->width;
|
|
save_rgn.height = drawable->height;
|
|
save_rgn.x= 0;
|
|
save_rgn.y= 0;
|
|
}
|
|
/* We warn if the dimension of the layer is over MAX_BITMAP_CURSOR_SIZE. */
|
|
if (! dimension_warn)
|
|
{
|
|
if (save_rgn.width > MAX_BITMAP_CURSOR_SIZE ||
|
|
save_rgn.height > MAX_BITMAP_CURSOR_SIZE)
|
|
{
|
|
dimension_warn = TRUE;
|
|
/* actual warning is done after the cursor is successfully saved.*/
|
|
}
|
|
}
|
|
/*
|
|
* Create new XcursorImage.
|
|
*/
|
|
DM_XMC("create new xcursorimage.\twidth=%i\theight=%i\n",
|
|
save_rgn.width, save_rgn.height);
|
|
imagesp->images[i] = XcursorImageCreate (save_rgn.width, save_rgn.height);
|
|
/* Cursor width & height is automatically set by function */
|
|
/* XcursorImageCreate, so no need to set manually. */
|
|
if (!imagesp->images[i])
|
|
{
|
|
DM_XMC ("Failed to XcursorImageCreate.\n");
|
|
return FALSE;
|
|
}
|
|
/*
|
|
** set images[i]'s xhot & yhot.
|
|
*/
|
|
/* [Cropped layer's hotspot] =
|
|
[image's hotspot] - [layer's offset] - [save_rgn's offset]. */
|
|
DM_XMC("xhot=%i\tsave_rgn->xoffset=%i\tlayer_xoffset=%i\n",
|
|
xmcparas.x, layer_xoffset, save_rgn.x);
|
|
DM_XMC("yhot=%i\tsave_rgn->yoffset=%i\tlayer_yoffset=%i\n",
|
|
xmcparas.y, layer_yoffset, save_rgn.y);
|
|
imagesp->images[i]->xhot = xmcparas.x - layer_xoffset - save_rgn.x;
|
|
imagesp->images[i]->yhot = xmcparas.y - layer_yoffset - save_rgn.y;
|
|
DM_XMC("images[%i]->xhot=%i\tyhot=%i\n", i,
|
|
imagesp->images[i]->xhot, imagesp->images[i]->yhot);
|
|
|
|
/*
|
|
* set images[i]->pixels
|
|
*/
|
|
/* get image data to pixelbuf. */
|
|
gimp_pixel_rgn_get_rect (&pixel_rgn, (guchar *) pixelbuf,
|
|
save_rgn.x, save_rgn.y,
|
|
save_rgn.width, save_rgn.height);
|
|
|
|
/*convert pixel date to XcursorPixel. */
|
|
g_assert (save_rgn.width * save_rgn.height < SQR(MAX_SAVE_DIMENSION));
|
|
for (j = 0; j < save_rgn.width * save_rgn.height; j++)
|
|
{
|
|
imagesp->images[i]->pixels[j] = premultiply_alpha (pixelbuf[j]);
|
|
}
|
|
|
|
/*
|
|
* get back size & delay from framename.
|
|
*/
|
|
set_size_and_delay (framename, &(imagesp->images[i]->size),
|
|
&(imagesp->images[i]->delay), re, &size_warn);
|
|
|
|
/*
|
|
* All property of this XcursorImage is loaded.
|
|
*/
|
|
|
|
/* set the layer name of original image with the saved value */
|
|
g_free (framename);
|
|
framename = make_framename (imagesp->images[i]->size,
|
|
imagesp->images[i]->delay,
|
|
DISPLAY_DIGIT(imagesp->nimage),
|
|
error);
|
|
if (!framename)
|
|
return FALSE;
|
|
|
|
gimp_item_set_name (orig_layers[nlayers - 1 - i], framename);
|
|
g_free (framename);
|
|
|
|
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);
|
|
return FALSE;
|
|
}
|
|
|
|
}
|
|
else /* if no comments exist */
|
|
{
|
|
if (! XcursorFileSaveImages (fp, imagesp))
|
|
{
|
|
DM_XMC("Failed to XcursorFileSaveImages.\t%p\t%p\n", fp, imagesp);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/* actual warning about dimensions */
|
|
if (dimension_warn)
|
|
{
|
|
g_message (_("Your cursor was successfully saved but it contains one or "
|
|
"more frames whose width or height is more than %ipx.\n"
|
|
"It will clutter the screen in some environments."),
|
|
MAX_BITMAP_CURSOR_SIZE);
|
|
}
|
|
if (size_warn)
|
|
{
|
|
g_message (_("Your cursor was successfully saved but it contains one "
|
|
"or more frames whose nominal size is not supported by "
|
|
"GNOME settings.\n"
|
|
"You can satisfy it by checking \"Replace the size of all "
|
|
"frames...\" in the save 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 = CLAMP0255(red * 255 / alpha);
|
|
blue = CLAMP0255(blue * 255 / alpha);
|
|
green = CLAMP0255(green * 255 / alpha);
|
|
retval = red + (green<<8) + (blue<<16) + (alpha<<24);
|
|
|
|
#if G_BYTE_ORDER != G_LITTLE_ENDIAN
|
|
pixel = GUINT32_FROM_LE(pixel);
|
|
#endif
|
|
|
|
return retval;
|
|
}
|
|
|
|
static inline guint32
|
|
premultiply_alpha (guint32 pixel)
|
|
{
|
|
guint alpha, red, green, blue;
|
|
guint32 retval;
|
|
|
|
#if G_BYTE_ORDER != G_LITTLE_ENDIAN
|
|
pixel = GUINT32_TO_LE(pixel);
|
|
#endif
|
|
|
|
red = pixel & 0xff;
|
|
green = (pixel>>8) & 0xff;
|
|
blue = (pixel>>16) & 0xff;
|
|
alpha = (pixel>>24) & 0xff;
|
|
|
|
/* premultiply alpha
|
|
(see "premultiply_data" function at line 154 of xcursorgen.c) */
|
|
red = div_255 (red * alpha);
|
|
green = div_255 (green * alpha);
|
|
blue = div_255 (blue * alpha);
|
|
retval = blue + (green<<8) + (red<<16) + (alpha<<24);
|
|
|
|
#if G_BYTE_ORDER != G_LITTLE_ENDIAN
|
|
pixel = GUINT32_FROM_LE(pixel);
|
|
#endif
|
|
|
|
return retval;
|
|
}
|
|
|
|
/* set comments to cursor from xmcparas.comments. */
|
|
/* don't forget to XcursorCommentsDestory returned pointer later. */
|
|
static XcursorComments *
|
|
set_cursor_comments (void)
|
|
{
|
|
gint i;
|
|
guint gcomlen, arraylen;
|
|
GArray *xcCommentsArray;
|
|
XcursorComment *(xcCommentp[3]) = {NULL,};
|
|
XcursorComments *xcCommentsp;
|
|
|
|
xcCommentsArray = g_array_new (FALSE, FALSE, sizeof (XcursorComment *));
|
|
|
|
for (i = 0; i < 3; ++i)
|
|
{
|
|
if (xmcparas.comments[i])
|
|
{
|
|
gcomlen = strlen (xmcparas.comments[i]);
|
|
if (gcomlen > 0)
|
|
{
|
|
xcCommentp[i] = XcursorCommentCreate (i + 1, gcomlen);
|
|
/* first argument of XcursorCommentCreate is comment_type
|
|
defined in Xcursor.h as enumerator.
|
|
i + 1 is appropriate when we dispose parasiteName before MAIN(). */
|
|
if (!xcCommentp[i])
|
|
{
|
|
g_warning ("Cannot create xcCommentp[%i]\n", i);
|
|
return NULL;
|
|
}
|
|
else
|
|
{
|
|
g_stpcpy (xcCommentp[i]->comment, xmcparas.comments[i]);
|
|
g_array_append_val(xcCommentsArray, xcCommentp[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
arraylen = xcCommentsArray->len;
|
|
|
|
if (arraylen == 0)
|
|
return NULL;
|
|
|
|
xcCommentsp = XcursorCommentsCreate (arraylen);
|
|
xcCommentsp->ncomment = arraylen;
|
|
|
|
for (i = 0; i < arraylen; ++i)
|
|
{
|
|
xcCommentsp->comments[i] =
|
|
g_array_index(xcCommentsArray, XcursorComment* ,i);
|
|
}
|
|
|
|
return xcCommentsp;
|
|
|
|
}
|
|
|
|
/*
|
|
* Load xmcparas.comments from three parasites named as "xmc-copyright",
|
|
* "xmc-license","gimp-comment".
|
|
* This alignment sequence is depends on the definition of comment_type
|
|
* in Xcursor.h .
|
|
* Don't forget to g_free each element of xmcparas.comments later.
|
|
*/
|
|
static void
|
|
load_comments (const gint32 image_ID)
|
|
{
|
|
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
|
|
* is already exist, append the new one to the old one with "\n"
|
|
**/
|
|
static gboolean
|
|
set_comment_to_pname (const gint32 image_ID,
|
|
const gchar *content,
|
|
const gchar *pname)
|
|
{
|
|
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 = NULL;
|
|
|
|
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 */
|
|
size = MAX_BITMAP_CURSOR_SIZE;
|
|
else
|
|
size = MIN (MAX_BITMAP_CURSOR_SIZE, atoi (digits));
|
|
}
|
|
}
|
|
else /* suffix is "ms" */
|
|
{
|
|
if (!delay) /* substitute it only for the first time */
|
|
{
|
|
if (strlen (digits) > 8) /* too large number should be clamped */
|
|
delay = CURSOR_MAX_DELAY;
|
|
else
|
|
delay = MIN (CURSOR_MAX_DELAY, atoi (digits));
|
|
}
|
|
}
|
|
|
|
g_free (digits);
|
|
g_free (suffix);
|
|
|
|
g_match_info_next (info, NULL);
|
|
}
|
|
|
|
g_match_info_free (info);
|
|
|
|
/* if size is not set, or size_replace is TRUE, set default size
|
|
* (which was chosen in save dialog) */
|
|
if (size == 0 || xmcvals.size_replace == TRUE)
|
|
{
|
|
size = xmcvals.size;
|
|
}
|
|
else if (! *size_warnp &&
|
|
size != 12 && size != 16 && size != 24 && size != 32 &&
|
|
size != 36 && size != 40 && size != 48 && size != 64)
|
|
{ /* if the size is different from these values, we warn about it after
|
|
successfully saving because gnome-appearance-properties only support
|
|
them. */
|
|
*size_warnp = TRUE;
|
|
}
|
|
|
|
*sizep = size;
|
|
|
|
/* if delay is not set, or delay_replace is TRUE, set default delay
|
|
* (which was chosen in save dialog) */
|
|
if (delay == 0 || xmcvals.delay_replace == TRUE)
|
|
{
|
|
delay = xmcvals.delay;
|
|
}
|
|
|
|
*delayp = delay;
|
|
|
|
DM_XMC("set_size_and_delay return\tsize=%i\tdelay=%i\n", size, delay);
|
|
}
|
|
|
|
/**
|
|
* Return 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 (GimpParamRegion *return_rgn,
|
|
GimpPixelRgn *pr)
|
|
{
|
|
guint i, j;
|
|
guint32 *buf = g_malloc (MAX (pr->w, pr->h) * sizeof (guint32));
|
|
|
|
g_return_if_fail (pr);
|
|
|
|
DM_XMC("function:get_cropped_region\n");
|
|
|
|
gimp_tile_cache_ntiles (MAX (pr->w / gimp_tile_width (),
|
|
pr->h / gimp_tile_height ()) + 1);
|
|
DM_XMC("getTrim:\tMAX=%i\tpr->w=%i\tpr->h=%i\n", sizeof(buf)/4, pr->w, pr->h);
|
|
|
|
/* find left border. */
|
|
for (i = 0 ;i < pr->w ; ++i)
|
|
{
|
|
DM_XMC("pr->x+i=%i\tpr->w=%i\n",pr->x + i, pr->w);
|
|
gimp_pixel_rgn_get_col (pr, (guchar *)buf, pr->x + i, pr->y, pr->h);
|
|
for (j = 0; j < pr->h; ++j)
|
|
{
|
|
if (pix_is_opaque (buf[j])) /* if a opaque pixel exist. */
|
|
{
|
|
return_rgn->x = pr->x + i;
|
|
goto find_right;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* pr has no opaque pixel. */
|
|
return_rgn->width = 0;
|
|
return;
|
|
|
|
/* find right border. */
|
|
find_right:
|
|
for (i = 0 ;i < pr->w ; ++i)
|
|
{
|
|
DM_XMC("pr->x+pr->w-1=%i\tpr->y+j=%i\tpr->h=%i\n",
|
|
pr->x + pr->w - 1 - i, pr->y, pr->h);
|
|
gimp_pixel_rgn_get_col (pr, (guchar *)buf, pr->x + pr->w - 1 - i, pr->y, pr->h);
|
|
for (j = 0; j < pr->h; ++j)
|
|
{
|
|
if (pix_is_opaque (buf[j])) /* if a opaque pixel exist. */
|
|
{
|
|
return_rgn->width = pr->x + pr->w - i - return_rgn->x;
|
|
goto find_top;
|
|
}
|
|
}
|
|
}
|
|
g_return_if_reached ();
|
|
|
|
/* find top border. */
|
|
find_top:
|
|
for (j = 0 ;j < pr->h ; ++j)
|
|
{
|
|
DM_XMC("pr->x=%i\tpr->y+j=%i\tpr->w=%i\n",pr->x, pr->y + j, pr->w);
|
|
|
|
gimp_pixel_rgn_get_row (pr, (guchar *) buf, pr->x, pr->y + j, pr->w);
|
|
|
|
for (i = 0; i < pr->w; ++i)
|
|
{
|
|
if (pix_is_opaque (buf[i])) /* if a opaque pixel exist. */
|
|
{
|
|
return_rgn->y = pr->y + j;
|
|
goto find_bottom;
|
|
}
|
|
}
|
|
}
|
|
|
|
g_return_if_reached ();
|
|
|
|
/* find bottom border. */
|
|
find_bottom:
|
|
for (j = 0 ;j < pr->h ; ++j)
|
|
{
|
|
DM_XMC ("pr->x=%i\tpr->y+pr->h-1-j=%i\tpr->w=%i\n",pr->x, pr->y + pr->h - 1 - j, pr->w);
|
|
gimp_pixel_rgn_get_row (pr, (guchar *) buf,
|
|
pr->x, pr->y + pr->h - 1 - j, pr->w);
|
|
|
|
for (i = 0; i < pr->w; ++i)
|
|
{
|
|
if (pix_is_opaque (buf[i])) /* if a opaque pixel exist. */
|
|
{
|
|
return_rgn->height = pr->y + pr->h - j - return_rgn->y;
|
|
goto end_trim;
|
|
}
|
|
}
|
|
}
|
|
|
|
g_return_if_reached ();
|
|
|
|
end_trim:
|
|
DM_XMC ("width=%i\theight=%i\txoffset=%i\tyoffset=%i\n",
|
|
return_rgn->width, return_rgn->height,
|
|
return_rgn->x, return_rgn->y);
|
|
|
|
g_free (buf);
|
|
}
|
|
|
|
/**
|
|
* Return 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 GimpParamRegion*
|
|
get_intersection_of_frames (gint32 image_ID)
|
|
{
|
|
GimpParamRegion *iregion;
|
|
gint i;
|
|
gint32 x1 = G_MININT32, x2 = G_MAXINT32;
|
|
gint32 y1 = G_MININT32, y2 = G_MAXINT32;
|
|
gint32 x_off, y_off;
|
|
gint nlayers;
|
|
gint *layers;
|
|
GimpDrawable *drawable;
|
|
|
|
g_return_val_if_fail (image_ID != -1, FALSE);
|
|
|
|
layers = gimp_image_get_layers (image_ID, &nlayers);
|
|
|
|
for (i = 0; i < nlayers; ++i)
|
|
{
|
|
drawable = gimp_drawable_get (layers[i]);
|
|
|
|
if (! gimp_drawable_offsets (layers[i], &x_off, &y_off))
|
|
return NULL;
|
|
|
|
x1 = MAX (x1, x_off);
|
|
y1 = MAX (y1, y_off);
|
|
x2 = MIN (x2, x_off + drawable->width - 1);
|
|
y2 = MIN (y2, y_off + drawable->height - 1);
|
|
}
|
|
|
|
if (x1 > x2 || y1 > y2)
|
|
return NULL;
|
|
|
|
/* OK intersection exists. */
|
|
iregion = g_new (GimpParamRegion, 1);
|
|
iregion->x = x1;
|
|
iregion->y = y1;
|
|
iregion->width = x2 - x1 + 1;
|
|
iregion->height = y2 - y1 + 1;
|
|
|
|
return iregion;
|
|
}
|
|
|
|
/**
|
|
* If (x,y) is in xmcrp, return TRUE.
|
|
**/
|
|
static gboolean
|
|
pix_in_region (gint32 x, gint32 y, GimpParamRegion *xmcrp)
|
|
{
|
|
g_return_val_if_fail (xmcrp, FALSE);
|
|
|
|
if (x < xmcrp->x || y < xmcrp->y ||
|
|
x >= xmcrp->x + xmcrp->width || y >= xmcrp->y + xmcrp->height)
|
|
return FALSE;
|
|
else
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* Find out xhot, yhot, width and height of the Xcursor specified by xcIs.
|
|
* Use NULL for the value you don't want to return.
|
|
**/
|
|
static void
|
|
find_hotspots_and_dimensions (XcursorImages *xcIs,
|
|
gint32 *xhotp, gint32 *yhotp,
|
|
gint32 *widthp, gint32 *heightp)
|
|
{
|
|
gint i; /* loop value */
|
|
gint32 dw, dh; /* the distance between hotspot and right(bottom) border */
|
|
gint32 max_xhot, max_yhot; /* the maximum value of xhot(yhot) */
|
|
|
|
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;
|
|
}
|