gimp/plug-ins/common/file-xmc.c

2400 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"
/* save without file extension "xmc" */
#define SAVE_PROC2 "file-xmc-save2"
#define PLUG_IN_BINARY "file-xmc"
/* We use "xmc" as the file extension of X cursor for convenience */
#define XCURSOR_EXTENSION "xmc"
#define XCURSOR_MIME_TYPE "image/x-xcursor"
/* The maximum dimension of Xcursor which is fully supported in any
* environments. This is defined on line 59 of xcursorint.h in
* libXcursor source code. Make sure this is about dimensions(width
* and height) not about nominal size despite of it's name.
*/
#define MAX_BITMAP_CURSOR_SIZE 64
/* The maximum dimension of each frame of X cursor we want to save
* should be MAX_BITMAP_CURSOR_SIZE but about loading, xhot(& yhot) of
* each frame varies from 0 to MAX_BITMAP_CURSOR_SIZE-1, so we need to
* set the maximum dimension of image no less than
* MAX_BITMAP_CURSOR_SIZE * 2( -1 to be precise) to remain hotspots on
* the same coordinates.
*
* We use four times value (256 for saving, 512 for loading) as a
* limitation because some cursors generated by CursorXP/FX to X11
* Mouse Theme Converter is very large.
*
* The biggest cursor I found is "watch" of OuterLimits which size is
* 213x208. If you found bigger one, please tell me ;-)
*/
#define MAX_LOAD_DIMENSION 512
#define MAX_SAVE_DIMENSION 256
/* The maximum number of different nominal sizes in one cursor this
* plug-in can treat. This is based on the number of cursor size which
* gnome-appearance-properties supports.(12,16,24,32,36,40,48,64)
* ref. capplets/common/gnome-theme-info.c in source of
* gnome-control-center
*/
#define MAX_SIZE_NUM 8
/* cursor delay is guint32 defined in Xcursor.h */
#define CURSOR_MAX_DELAY 100000000
#define CURSOR_DEFAULT_DELAY 50
#define CURSOR_MINIMUM_DELAY 5
#define div_255(x) (((x) + 0x80 + (((x) + 0x80) >> 8)) >> 8)
#define READ32(f, e) read32 ((f), (e)); if (*(e)) return -1;
#define DISPLAY_DIGIT(x) ((x) > 100) ? 3 : ((x) > 10) ? 2 : 1
/*
* Structures...
*/
typedef struct
{
gboolean crop;
gint size;
gboolean size_replace;
gint32 delay;
gboolean delay_replace;
} XmcSaveVals;
/*
* Local functions...
*/
static void query (void);
static void run (const gchar *name,
gint nparams,
const GimpParam *param,
gint *nreturn_vals,
GimpParam **return_vals);
static gint32 load_image (const gchar *filename,
GError **error);
static gint32 load_thumbnail (const gchar *filename,
gint32 thumb_size,
gint32 *width,
gint32 *height,
gint32 *num_layers,
GError **error);
static guint32 read32 (FILE *f,
GError **error);
static gboolean save_image (const gchar *filename,
gint32 image_ID,
gint32 drawable_ID,
gint32 orig_image_ID,
GError **error);
static gboolean save_dialog (const gint32 image_ID,
GimpParamRegion *hotspotRange);
static void comment_entry_callback (GtkWidget *widget,
gchar **commentp);
static void text_view_callback (GtkTextBuffer *buffer,
gchar **commentp);
static gboolean load_default_hotspot (const gint32 image_ID,
GimpParamRegion *hotspotRange);
static inline guint32 separate_alpha (guint32 pixel);
static inline guint32 premultiply_alpha (guint32 pixel);
static XcursorComments *set_cursor_comments (void);
static void load_comments (const gint32 image_ID);
static gboolean set_comment_to_pname (const gint32 image_ID,
const gchar *content,
const gchar *pname);
static gchar *get_comment_from_pname (const gint32 image_ID,
const gchar *pname);
static gboolean set_hotspot_to_parasite (gint32 image_ID);
static gboolean get_hotspot_from_parasite (gint32 image_ID);
static void set_size_and_delay (const gchar *framename,
guint32 *sizep,
guint32 *delayp,
GRegex *re,
gboolean *size_warnp);
static gchar *make_framename (guint32 size,
guint32 delay,
guint indent,
GError **errorp);
static void get_cropped_region (GimpParamRegion *retrun_rgn,
GimpPixelRgn *pr);
static inline gboolean pix_is_opaque (guint32 pix);
static GimpParamRegion *get_intersection_of_frames (gint32 image_ID);
static gboolean pix_in_region (gint32 x,
gint32 y,
GimpParamRegion *xmcrp);
static void find_hotspots_and_dimensions (XcursorImages *xcIs,
gint32 *xhot,
gint32 *yhot,
gint32 *width,
gint32 *height);
/*
* Globals...
*/
const GimpPlugInInfo PLUG_IN_INFO =
{
NULL,
NULL,
query,
run
};
static XmcSaveVals xmcvals =
{ /* saved in pdb after this plug-in's process has gone. */
FALSE, /* crop */
32, /* size */
FALSE, /* size_replace */
CURSOR_DEFAULT_DELAY, /* delay */
FALSE /* delay_replace */
};
static struct
{ /* saved as parasites of original image after this plug-in's process has gone.*/
gint32 x; /* hotspot x */
gint32 y; /* hotspot y */
gchar *comments[3]; /* copyright, license, other */
} xmcparas = {0,};
/* parasites correspond to XcursorComment type */
static const gchar *
parasiteName[3] = {"xmc-copyright", "xmc-license", "gimp-comment"};
/*
* 'main()' - Main entry - just call gimp_main()...
*/
MAIN ()
/*
* 'query()' - Respond to a plug-in query...
*/
static void
query (void)
{
static const GimpParamDef load_args[] =
{
{ GIMP_PDB_INT32, "run-mode", "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }" },
{ GIMP_PDB_STRING, "filename", "The name of the file to load" },
{ GIMP_PDB_STRING, "raw-filename", "The name of the file to load" }
};
static const GimpParamDef load_return_vals[] =
{
{ GIMP_PDB_IMAGE, "image", "Output image" }
};
static const GimpParamDef thumb_args[] =
{
{ GIMP_PDB_STRING, "filename", "The name of the file to load" },
{ GIMP_PDB_INT32, "thumb-size", "Preferred thumbnail size" }
};
static const GimpParamDef thumb_return_vals[] =
{
{ GIMP_PDB_IMAGE, "image", "Thumbnail image" },
{ GIMP_PDB_INT32, "image-width", "The width of image" },
{ GIMP_PDB_INT32, "image-height", "The height of image" },
{ GIMP_PDB_INT32, "image-type", "The color type of image"},
{ GIMP_PDB_INT32, "image-num-layers", "The number of layeres" }
};
static const GimpParamDef save_args[] =
{
{ GIMP_PDB_INT32, "run-mode", "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }" },
{ GIMP_PDB_IMAGE, "image", "Input image" },
{ GIMP_PDB_DRAWABLE, "drawable", "Drawable to save" },
{ GIMP_PDB_STRING, "filename", "The name of the file to save the image in" },
{ GIMP_PDB_STRING, "raw-filename", "The name entered" },
/* following elements are XMC specific options */
{ GIMP_PDB_INT32, "x_hot", "X-coordinate of 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);
#define GIMP_INSTALL_SAVE_PROC(mProc, mAdd) \
gimp_install_procedure ((mProc),\
"Saves files of X11 cursor file",\
"This plug-in saves X11 Mouse Cursor (XMC) files"\
#mAdd,\
"Takeshi Matsuyama <tksmashiw@gmail.com>",\
"Takeshi Matsuyama",\
"26 May 2009",\
N_("X11 Mouse Cursor"),\
"RGBA",\
GIMP_PLUGIN,\
G_N_ELEMENTS (save_args), 0,\
save_args, NULL)
GIMP_INSTALL_SAVE_PROC(SAVE_PROC, .);
GIMP_INSTALL_SAVE_PROC(SAVE_PROC2, \nwithout file extension.);
gimp_register_file_handler_mime (SAVE_PROC, XCURSOR_MIME_TYPE);
gimp_register_save_handler (SAVE_PROC, XCURSOR_EXTENSION, "");
gimp_register_file_handler_mime (SAVE_PROC2, XCURSOR_MIME_TYPE);
}
/*
* 'run()' - Run the plug-in...
*/
static void
run (const gchar *name,
gint nparams,
const GimpParam *param,
gint *nreturn_vals,
GimpParam **return_vals)
{
static GimpParam values[2];
GimpRunMode run_mode;
GimpPDBStatusType status = GIMP_PDB_SUCCESS;
gint32 image_ID;
gint32 drawable_ID;
gint32 orig_image_ID;
GimpExportReturn export = GIMP_EXPORT_CANCEL;
GimpParamRegion *hotspotRange = NULL;
gint i;
gint32 width, height, num_layers;
GError *error = NULL;
INIT_I18N ();
DM_XMC("run: start.\n");
*nreturn_vals = 1;
*return_vals = values;
values[0].type = GIMP_PDB_STATUS;
values[0].data.d_status = GIMP_PDB_EXECUTION_ERROR;
if (strcmp (name, LOAD_PROC) == 0)
{
DM_XMC("Starting to load file.\tparam.data=%s\n",
param[1].data.d_string);
image_ID = load_image (param[1].data.d_string, &error);
if (image_ID != -1)
{
*nreturn_vals = 2;
values[1].type = GIMP_PDB_IMAGE;
values[1].data.d_image = image_ID;
DM_XMC("LOAD_PROC successfully load image. image_ID=%i\n",image_ID);
}
else
{
status = GIMP_PDB_EXECUTION_ERROR;
}
}
else if (strcmp (name, LOAD_THUMB_PROC) == 0)
{
DM_XMC("Starting to load thumbnail.\tfilename=%s\tthumb-size=%d\n",
param[0].data.d_string, param[1].data.d_int32);
image_ID = load_thumbnail (param[0].data.d_string,
param[1].data.d_int32,
&width,
&height,
&num_layers,
&error);
if (image_ID != -1)
{
*nreturn_vals = 6;
values[1].type = GIMP_PDB_IMAGE;
values[1].data.d_image = image_ID;
values[2].type = GIMP_PDB_INT32;
values[2].data.d_int32 = width; /* width */
values[3].type = GIMP_PDB_INT32;
values[3].data.d_int32 = height; /* height */
/* This will not work on GIMP 2.6, but not harmful. */
values[4].type = GIMP_PDB_INT32;
values[4].data.d_int32 = GIMP_RGBA_IMAGE; /* type */
values[5].type = GIMP_PDB_INT32;
values[5].data.d_int32 = num_layers; /* num_layers */
DM_XMC("LOAD_THUMB_PROC successfully load image. image_ID=%i\n",image_ID);
}
else
{
status = GIMP_PDB_EXECUTION_ERROR;
}
}
else if (strcmp (name, SAVE_PROC) == 0 || strcmp (name, SAVE_PROC2) == 0)
{
DM_XMC("run: save %s\n", name);
run_mode = param[0].data.d_int32;
image_ID = orig_image_ID = param[1].data.d_int32;
drawable_ID = param[2].data.d_int32;
hotspotRange = get_intersection_of_frames (image_ID);
if (! hotspotRange)
{
g_set_error (&error, 0, 0,
_("Cannot set the 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) */
/* initialize image here, thus avoiding compiler warnings */
image_ID = -1;
/*
* Open the file and check it is a valid X cursor
*/
fp = g_fopen (filename, "rb");
if (fp == NULL)
{
g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
_("Could not open '%s' for reading: %s"),
gimp_filename_to_utf8 (filename), g_strerror (errno));
return -1;
}
if (!XcursorFileLoad (fp, &commentsp, &imagesp))
{
g_set_error (error, 0, 0, _("'%s' is not a valid X cursor."),
gimp_filename_to_utf8 (filename));
return -1;
}
gimp_progress_init_printf (_("Opening '%s'"),
gimp_filename_to_utf8 (filename));
/*
** check dimension is valid.
*/
for (i = 0; i < imagesp->nimage; i++)
{
if (imagesp->images[i]->width > MAX_LOAD_DIMENSION)
{
g_set_error (error, 0, 0,
_("The width of frame %d of '%s' is too big for X cursor."),
i + 1, gimp_filename_to_utf8 (filename));
return -1;
}
if (imagesp->images[i]->height > MAX_LOAD_DIMENSION)
{
g_set_error (error, 0, 0,
_("The height of frame %d of '%s' is too big for X cursor."),
i + 1, gimp_filename_to_utf8 (filename));
return -1;
}
}
find_hotspots_and_dimensions (imagesp,
&xmcparas.x, &xmcparas.y,
&img_width, &img_height);
DM_XMC("xhot=%i,\tyhot=%i,\timg_width=%i,\timg_height=%i\n",
xmcparas.x, xmcparas.y, img_width, img_height);
/* create new image! */
image_ID = gimp_image_new (img_width, img_height, GIMP_RGB);
/* set filename */
gimp_image_set_filename (image_ID, filename);
if (! set_hotspot_to_parasite (image_ID))
return -1;
/* Temporary buffer */
tmppixel = g_new (guint32, img_width * img_height);
/* load each frame to each layer one by one */
for (i = 0; i < imagesp->nimage; i++)
{
delay = imagesp->images[i]->delay;
if (delay < CURSOR_MINIMUM_DELAY)
{
delay = CURSOR_DEFAULT_DELAY;
}
DM_XMC("images[%i]->delay=%i\twidth=%d\theight=%d\n"
,i ,delay, imagesp->images[i]->width, imagesp->images[i]->height);
framename = make_framename (imagesp->images[i]->size, delay,
DISPLAY_DIGIT(imagesp->nimage), error);
if (!framename)
return -1;
layer_ID = gimp_layer_new (image_ID, framename,
imagesp->images[i]->width,
imagesp->images[i]->height,
GIMP_RGBA_IMAGE, 100, GIMP_NORMAL_MODE);
gimp_image_add_layer (image_ID, layer_ID, 0);
/* Adjust layer position to let hotspot sit on the same point. */
gimp_layer_translate (layer_ID,
xmcparas.x - imagesp->images[i]->xhot,
xmcparas.y - imagesp->images[i]->yhot);
g_free (framename);
/*
* Get the drawable and set the pixel region for our load...
*/
drawable = gimp_drawable_get (layer_ID);
gimp_pixel_rgn_init (&pixel_rgn, drawable, 0, 0, drawable->width,
drawable->height, TRUE, FALSE);
/* set color to each pixel */
for (j = 0; j < drawable->width * drawable->height; j++)
{
tmppixel[j] = separate_alpha (imagesp->images[i]->pixels[j]) ;
}
/* set pixel */
gimp_pixel_rgn_set_rect (&pixel_rgn, (guchar *)tmppixel,
0, 0, drawable->width, drawable->height);
gimp_progress_update ( (i + 1) / imagesp->nimage);
}
/* free temporary buffer */
g_free(tmppixel);
/*
* Comment parsing
*/
if (commentsp)
{
for (i = 0; i < commentsp->ncomment; ++i)
{
DM_XMC ("comment type=%d\tcomment=%s\n",
commentsp->comments[i]->comment_type,
commentsp->comments[i]->comment);
if (! set_comment_to_pname (image_ID,
commentsp->comments[i]->comment,
parasiteName[commentsp->comments[i]->comment_type -1]))
{
DM_XMC ("Failed to write %ith comment.\n", i);
return -1;
}
}
}
DM_XMC("Comment parsing done.\n");
XcursorImagesDestroy (imagesp);
XcursorCommentsDestroy (commentsp);
fclose (fp);
/*
* Update the display...
*/
gimp_progress_end ();
gimp_drawable_flush (drawable);
gimp_drawable_detach (drawable);
return image_ID;
}
/*
* load_thumbnail
*/
static gint32
load_thumbnail (const gchar *filename, gint32 thumb_size,
gint32 *thumb_width, gint32 *thumb_height,
gint32 *thumb_num_layers, GError **error)
{
/* Return only one frame for thumbnail.
* We select first frame of an animation sequence which nominal size is the
* closest of thumb_size. */
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,
_("The width of '%s' is too big for X cursor."),
gimp_filename_to_utf8 (filename));
return -1;
}
if (*thumb_height > MAX_LOAD_DIMENSION)
{
g_set_error (error, 0, 0,
_("The height of '%s' is too big for X cursor."),
gimp_filename_to_utf8 (filename));
return -1;
}
/*
* create new image!
*/
image_ID = gimp_image_new (xcIs->images[sel_num]->width,
xcIs->images[sel_num]->height, GIMP_RGB);
layer_ID = gimp_layer_new (image_ID, NULL,
xcIs->images[sel_num]->width,
xcIs->images[sel_num]->height,
GIMP_RGBA_IMAGE, 100,
GIMP_NORMAL_MODE);
gimp_image_add_layer (image_ID, layer_ID, 0);
/*
* Get the drawable and set the pixel region for our load...
*/
drawable = gimp_drawable_get (layer_ID);
gimp_pixel_rgn_init (&pixel_rgn, drawable, 0, 0,
xcIs->images[sel_num]->width,
xcIs->images[sel_num]->height,
TRUE, FALSE);
/* Temporary buffer */
tmppixel = g_new (guint32,
xcIs->images[sel_num]->width *
xcIs->images[sel_num]->height);
/* copy the chunk data to tmppixel */
fseek (fp, positions[sel_num], SEEK_SET);
fseek (fp, 36, SEEK_CUR); /* skip chunk header(16bytes), xhot, yhot, width, height, delay */
for (i = 0;
i < xcIs->images[sel_num]->width * xcIs->images[sel_num]->height;
i++)
{
tmppixel[i] = READ32 (fp, error)
/* get back separate alpha */
tmppixel[i] = separate_alpha (tmppixel[i]);
}
/* set pixel */
gimp_pixel_rgn_set_rect (&pixel_rgn, (guchar *)tmppixel, 0, 0,
drawable->width, drawable->height);
/* free tmppixel */
g_free(tmppixel);
g_free (positions);
fclose (fp);
gimp_drawable_flush (drawable);
gimp_drawable_detach (drawable);
return image_ID;
}
/* read guint32 value from f despite of host's byte order. */
static guint32
read32 (FILE *f, GError **error)
{
guchar p[4];
guint32 ret;
if (fread (p, 1, 4, f) != 4)
{
g_set_error (error, 0, 0, _("A read error occurred."));
return 0;
}
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
ret = p[0] + (p[1]<<8) + (p[2]<<16) + (p[3]<<24);
#elif G_BYTE_ORDER == G_BIG_ENDIAN
ret = p[3] + (p[2]<<8) + (p[1]<<16) + (p[0]<<24);
#elif G_BYTE_ORDER == G_PDP_ENDIAN
ret = p[2] + (p[3]<<8) + (p[0]<<16) + (p[1]<<24);
#else
g_return_val_if_rearched ();
#endif
return ret;
}
/*
* 'save_dialog ()'
*/
static gboolean
save_dialog (const gint32 image_ID, GimpParamRegion *hotspotRange)
{
gint x1, x2, y1, y2;
GtkWidget *dialog;
GtkWidget *frame;
GtkWidget *table;
GtkWidget *box;
GtkObject *adjustment;
GtkWidget *alignment;
GtkWidget *tmpwidget;
GtkWidget *label;
GtkTextBuffer *textbuffer;
GValue val = {0,};
gboolean run;
g_value_init (&val, G_TYPE_DOUBLE);
dialog = gimp_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_hbox_new (FALSE, 6);
gtk_widget_show (box);
tmpwidget = gimp_spin_button_new (&adjustment,
xmcvals.delay, CURSOR_MINIMUM_DELAY,
CURSOR_MAX_DELAY, 1, 5, 0, 1, 0);
gtk_widget_show (tmpwidget);
g_value_set_double (&val, 1.0);
g_object_set_property (G_OBJECT (tmpwidget), "xalign", &val);/* align right*/
gtk_box_pack_start (GTK_BOX (box), tmpwidget, TRUE, TRUE, 0);
g_signal_connect (adjustment, "value-changed",
G_CALLBACK (gimp_int_adjustment_update),
&xmcvals.delay);
/* appended "ms" */
tmpwidget = gtk_label_new ("ms");
gtk_misc_set_alignment (GTK_MISC (tmpwidget), 0, 0.5); /*align left*/
gtk_box_pack_start (GTK_BOX (box), tmpwidget, TRUE, TRUE, 0);
gtk_widget_show (tmpwidget);
/* tooltip */
gimp_help_set_help_data (box,
_("Enter time span in milliseconds in which "
"each frame is rendered."),
NULL);
gimp_table_attach_aligned (GTK_TABLE (table), 0, 4, _("_Delay:"), 0, 0.5, box, 3, TRUE);
/* Replace delay? */
tmpwidget =
gimp_int_radio_group_new (FALSE, NULL, G_CALLBACK (gimp_radio_button_update),
&xmcvals.delay_replace, xmcvals.delay_replace,
_("_Use this value only for a frame which delay "
"is not specified."),
FALSE, NULL,
_("_Replace the delay of all frames even if it "
"is specified."),
TRUE, NULL,
NULL);
alignment = gtk_alignment_new (0.5, 0.5, 1.0, 1.0);
gtk_widget_show (alignment);
gtk_table_attach (GTK_TABLE (table), alignment, 0, 3, 5, 6, 0, 0, 0, 0);
gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 0, 6, 20, 0); /*padding left*/
gtk_container_add (GTK_CONTAINER (alignment), tmpwidget);
gtk_widget_show (tmpwidget);
/*
* Copyright
*/
tmpwidget = gtk_entry_new();
/* Maximum length will be clamped to 65536 */
gtk_entry_set_max_length (GTK_ENTRY (tmpwidget), XCURSOR_COMMENT_MAX_LEN);
if (xmcparas.comments[0])
{
gtk_entry_set_text (GTK_ENTRY (tmpwidget),
gimp_any_to_utf8 (xmcparas.comments[0], - 1, NULL));
/* show warning if comment is over 65535 characters
* because gtk_entry can hold only that. */
if (strlen (gtk_entry_get_text (GTK_ENTRY (tmpwidget))) >= 65535)
g_message (_("The part of copyright information "
"that exceeded 65535 characters was removed."));
}
g_signal_connect (tmpwidget, "changed",
G_CALLBACK (comment_entry_callback),
xmcparas.comments);
gtk_widget_show (tmpwidget);
/* tooltip */
gimp_help_set_help_data (tmpwidget,
_("Enter copyright information."),
NULL);
gimp_table_attach_aligned (GTK_TABLE (table), 0, 6, _("_Copyright:"),
0, 0.5, tmpwidget, 3, FALSE);
/*
* License
*/
tmpwidget = gtk_entry_new();
/* Maximum length will be clamped to 65536 */
gtk_entry_set_max_length (GTK_ENTRY (tmpwidget), XCURSOR_COMMENT_MAX_LEN);
if (xmcparas.comments[1])
{
gtk_entry_set_text (GTK_ENTRY (tmpwidget),
gimp_any_to_utf8 (xmcparas.comments[1], - 1, NULL));
/* show warning if comment is over 65535 characters
* because gtk_entry can hold only that. */
if (strlen (gtk_entry_get_text (GTK_ENTRY (tmpwidget))) >= 65535)
g_message (_("The part of license information "
"that exceeded 65535 characters was removed."));
}
g_signal_connect (tmpwidget, "changed",
G_CALLBACK (comment_entry_callback),
xmcparas.comments + 1);
gtk_widget_show (tmpwidget);
/* tooltip */
gimp_help_set_help_data (tmpwidget,
_("Enter license information."),
NULL);
gimp_table_attach_aligned (GTK_TABLE (table), 0, 7, _("_License:"),
0, 0.5, tmpwidget, 3, FALSE);
/*
* Other
*/
/* We use gtk_text_view for "Other" while "Copyright" & "License" is entered
* in gtk_entry because We want allow '\n' for "Other". */
label = gtk_label_new_with_mnemonic (_("_Other:"));
gtk_widget_show (label);
gtk_misc_set_alignment (GTK_MISC (label), 0, 0); /*align top-left*/
gtk_table_attach (GTK_TABLE (table), label, 0, 1, 8, 9, GTK_FILL, 0, 0, 0);
/* content of Other */
/* scrolled window */
box = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (box),
GTK_SHADOW_IN);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (box),
GTK_POLICY_AUTOMATIC,
GTK_POLICY_AUTOMATIC);
gtk_table_attach (GTK_TABLE (table), box, 1, 3, 8, 9, GTK_FILL, 0, 0, 0);
gtk_widget_show (box);
/* textbuffer */
textbuffer = gtk_text_buffer_new (NULL);
if (xmcparas.comments[2])
gtk_text_buffer_set_text (textbuffer,
gimp_any_to_utf8 (xmcparas.comments[2], -1, NULL),
-1);
g_signal_connect (textbuffer, "changed",
G_CALLBACK (text_view_callback),
xmcparas.comments + 2);
/* textview */
tmpwidget =
gtk_text_view_new_with_buffer (GTK_TEXT_BUFFER (textbuffer));
gtk_text_view_set_accepts_tab (GTK_TEXT_VIEW (tmpwidget), FALSE);
gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tmpwidget), GTK_WRAP_WORD);
g_object_unref (textbuffer);
gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tmpwidget), GTK_WRAP_WORD);
gtk_container_add (GTK_CONTAINER (box), tmpwidget);
gtk_widget_show (tmpwidget);
/* tooltip */
gimp_help_set_help_data (tmpwidget,
_("Enter other comment if you want."),
NULL);
gtk_label_set_mnemonic_widget (GTK_LABEL (label), tmpwidget);
/*
* all widget is prepared. Let's show dialog.
*/
gtk_widget_show (dialog);
run = (gimp_dialog_run (GIMP_DIALOG (dialog)) == GTK_RESPONSE_OK);
gtk_widget_destroy (dialog);
return run;
}
/*
* callback function of gtk_entry for "copyright" and "license".
* "other" is processed by text_view_callback
*/
static void
comment_entry_callback (GtkWidget *widget, gchar **commentp)
{
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 format with 8bit color depth."));
return FALSE;
}
/* get framename of this layer */
framename = gimp_drawable_get_name (layers[nlayers - 1 - i]);
/* get offset of this layer. */
gimp_drawable_offsets (layers[nlayers - 1 - i], &layer_xoffset, &layer_yoffset);
/*
* layer dimension check.
*/
DM_XMC("layer size check.\n");
/* We allow to save a cursor which dimensions are no more than
* MAX_SAVE_DIMENSION but after auto-cropping, we warn (only warn, don't
* stop) if dimension is over MAX_BITMAP_CURSOR_SIZE. */
if (drawable->width > MAX_SAVE_DIMENSION)
{
g_set_error (error, 0, 0,
_("Width of '%s' is too large. Please reduce more than %dpx."),
gimp_any_to_utf8 (framename, -1, NULL), MAX_SAVE_DIMENSION);
return FALSE;
}
if (drawable->height > MAX_SAVE_DIMENSION)
{
g_set_error (error, 0, 0,
_("Height of '%s' is too large. Please reduce more than %dpx."),
gimp_any_to_utf8 (framename, -1, NULL), MAX_SAVE_DIMENSION);
return FALSE;
}
if (drawable->height == 0 ||drawable->width == 0)
{
g_set_error (error, 0, 0,
_("The size of '%s' is zero!"),
gimp_any_to_utf8 (framename, -1, NULL));
return FALSE;
}
gimp_pixel_rgn_init (&pixel_rgn, drawable, 0, 0, drawable->width,
drawable->height, FALSE, FALSE);
if (xmcvals.crop) /* with auto-cropping */
{
/* get the region of auto-cropped area. */
DM_XMC("get_cropped_region\n");
get_cropped_region (&save_rgn, &pixel_rgn);
/* don't forget save_rgn's origin is not a entire image
but a layer which we are doing on.*/
if (save_rgn.width == 0 || save_rgn.height == 0)
{/* perfectly transparent frames become 1x1px transparent pixel. */
DM_XMC("get_cropped_region return 0.\n");
imagesp->images[i] = XcursorImageCreate (1, 1);
if (!imagesp->images[i])
{
DM_XMC ("Failed to XcursorImageCreate.\n");
return FALSE;
}
imagesp->images[i]->pixels[0] = 0x0;
imagesp->images[i]->xhot = 0;
imagesp->images[i]->yhot = 0;
set_size_and_delay (framename, &(imagesp->images[i]->size),
&(imagesp->images[i]->delay), re,
&size_warn);
continue;
}
/* OK save_rgn is not 0x0 */
/* is hotspot in save_rgn ? */
if (! pix_in_region (xmcparas.x - layer_xoffset,
xmcparas.y - layer_yoffset,
&save_rgn))
{ /* if hotspot is not on save_rgn */
g_set_error (error, 0, 0,
_("Cannot save the cursor because the hot spot is not on '%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_drawable_set_name (orig_layers[nlayers - 1 - i], framename);
g_free (framename);
gimp_progress_update ((i + 1) / imagesp->nimage);
}
/*
* comment parsing
*/
commentsp = set_cursor_comments ();
#ifdef XMC_DEBUG
DM_XMC("imagesp->nimage=%i\tname=%s\n",imagesp->nimage,imagesp->name);
for (i = 0; i < imagesp->nimage; ++i)
{
DM_XMC("\timages[%i]->size=%i\n\
\twidth=%i\n\
\theight=%i\n\
\txhot=%i\n\
\tyhot=%i\n\
\tdelay=%i\n\
\t*pixels=%p\n",
i,
imagesp->images[i]->size,
imagesp->images[i]->width,
imagesp->images[i]->height,
imagesp->images[i]->xhot,
imagesp->images[i]->yhot,
imagesp->images[i]->delay,
imagesp->images[i]->pixels);
}
if (commentsp)
{
for (i = 0; i < commentsp->ncomment; ++i)
{
DM_XMC ("comment type=%d\tcomment=%s\n",
commentsp->comments[i]->comment_type,
commentsp->comments[i]->comment);
}
}
#endif
/*
* save cursor to file *fp.
*/
if (commentsp)
{
if (! XcursorFileSave (fp, commentsp, imagesp))
{
DM_XMC("Failed to XcursorFileSave.\t%p\t%p\t%p\n",
fp, commentsp, imagesp);
return FALSE;
}
}
else /* if no comments exist */
{
if (! XcursorFileSaveImages (fp, imagesp))
{
DM_XMC("Failed to XcursorFileSaveImages.\t%p\t%p\n", fp, imagesp);
return FALSE;
}
}
/* actual warning about dimensions */
if (dimension_warn)
{
g_message (_("Your cursor was successfully saved but it contains one "
"or more frames which width or height is more than %ipx.\n"
"It will clutter the screen in some environments."),
MAX_BITMAP_CURSOR_SIZE);
}
if (size_warn)
{
g_message (_("Your cursor was successfully saved but it contains one "
"or more frames which nominal size is not "
"supported by gnome-appearance-properties.\n"
"You can satisfy it by checking \"Replace the size of all "
"frame...\" in save dialog, or Your cursor may not appear "
"in gnome-appearance-properties."));
}
/*
* Done with the file...
*/
g_regex_unref(re);
DM_XMC("fp=%p\n",fp);
fclose (fp);
DM_XMC("%i frames written.\n", imagesp->nimage);
XcursorImagesDestroy (imagesp);
DM_XMC("Xcursor destroyed.\n");
XcursorCommentsDestroy(commentsp); /* this is safe even if commentsp is NULL. */
gimp_progress_end ();
/* Save the comment back to the original image */
for (i = 0; i < 3; i++)
{
gimp_image_parasite_detach (orig_image_ID, parasiteName[i]);
if (xmcparas.comments[i])
{
if (! set_comment_to_pname (orig_image_ID,
xmcparas.comments[i], parasiteName[i]))
{
DM_XMC ("Failed to write back %ith comment to orig_image.\n", i);
}
}
}
/* Save hotspot back to the original image */
set_hotspot_to_parasite (orig_image_ID);
return TRUE;
}
static inline guint32
separate_alpha (guint32 pixel)
{
guint alpha, red, green, blue;
guint32 retval;
#if G_BYTE_ORDER != G_LITTLE_ENDIAN
pixel = GUINT32_TO_LE(pixel);
#endif
blue = pixel & 0xff;
green = (pixel>>8) & 0xff;
red = (pixel>>16) & 0xff;
alpha = (pixel>>24) & 0xff;
if (alpha == 0)
return 0;
/* resume separate alpha data. */
red = CLAMP0255(red * 255 / alpha);
blue = CLAMP0255(blue * 255 / alpha);
green = CLAMP0255(green * 255 / alpha);
retval = red + (green<<8) + (blue<<16) + (alpha<<24);
#if G_BYTE_ORDER != G_LITTLE_ENDIAN
pixel = GUINT32_FROM_LE(pixel);
#endif
return retval;
}
static inline guint32
premultiply_alpha (guint32 pixel)
{
guint alpha, red, green, blue;
guint32 retval;
#if G_BYTE_ORDER != G_LITTLE_ENDIAN
pixel = GUINT32_TO_LE(pixel);
#endif
red = pixel & 0xff;
green = (pixel>>8) & 0xff;
blue = (pixel>>16) & 0xff;
alpha = (pixel>>24) & 0xff;
/* premultiply alpha
(see "premultiply_data" function at line 154 of xcursorgen.c) */
red = div_255 (red * alpha);
green = div_255 (green * alpha);
blue = div_255 (blue * alpha);
retval = blue + (green<<8) + (red<<16) + (alpha<<24);
#if G_BYTE_ORDER != G_LITTLE_ENDIAN
pixel = GUINT32_FROM_LE(pixel);
#endif
return retval;
}
/* set comments to cursor from xmcparas.comments. */
/* don't forget to XcursorCommentsDestory returned pointer later. */
static XcursorComments *
set_cursor_comments (void)
{
gint i;
guint gcomlen, arraylen;
GArray *xcCommentsArray;
XcursorComment *(xcCommentp[3]) = {NULL,};
XcursorComments *xcCommentsp;
xcCommentsArray = g_array_new (FALSE, FALSE, sizeof (XcursorComment *));
for (i = 0; i < 3; ++i)
{
if (xmcparas.comments[i])
{
gcomlen = strlen (xmcparas.comments[i]);
if (gcomlen > 0)
{
xcCommentp[i] = XcursorCommentCreate (i + 1, gcomlen);
/* first argument of XcursorCommentCreate is comment_type
defined in Xcursor.h as enumerator.
i + 1 is appropriate when we dispose parasiteName before MAIN(). */
if (!xcCommentp[i])
{
g_warning ("Cannot create xcCommentp[%i]\n", i);
return NULL;
}
else
{
g_stpcpy (xcCommentp[i]->comment, xmcparas.comments[i]);
g_array_append_val(xcCommentsArray, xcCommentp[i]);
}
}
}
}
arraylen = xcCommentsArray->len;
if (arraylen == 0)
return NULL;
xcCommentsp = XcursorCommentsCreate (arraylen);
xcCommentsp->ncomment = arraylen;
for (i = 0; i < arraylen; ++i)
{
xcCommentsp->comments[i] =
g_array_index(xcCommentsArray, XcursorComment* ,i);
}
return xcCommentsp;
}
/*
* Load xmcparas.comments from three parasites named as "xmc-copyright",
* "xmc-license","gimp-comment".
* This alignment sequence is depends on the definition of comment_type
* in Xcursor.h .
* Don't forget to g_free each element of xmcparas.comments later.
*/
static void
load_comments (const gint32 image_ID)
{
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_parasite_find (image_ID, pname);
if (! parasite)
{
parasite = gimp_parasite_new (pname, GIMP_PARASITE_PERSISTENT,
strlen (content) + 1, content);
}
else
{
tmpstring = g_strndup (gimp_parasite_data (parasite),
gimp_parasite_data_size (parasite));
gimp_parasite_free (parasite);
joind = g_strjoin ("\n", tmpstring, content, NULL);
g_free (tmpstring);
parasite = gimp_parasite_new (pname, GIMP_PARASITE_PERSISTENT,
strlen (joind) + 1, joind);
g_free (joind);
}
if (parasite)
{
ret = gimp_image_parasite_attach (image_ID, parasite);
gimp_parasite_free (parasite);
}
return ret;
}
/**
* get back comment from parasite name
* don't forget to call g_free(returned pointer) later
**/
static gchar *
get_comment_from_pname (const gint32 image_ID,
const gchar *pname)
{
gchar *string = NULL;
GimpParasite *parasite;
glong length;
g_return_val_if_fail (image_ID != -1, NULL);
parasite = gimp_image_parasite_find (image_ID, pname);
length = gimp_parasite_data_size (parasite);
if (parasite)
{
if (length > XCURSOR_COMMENT_MAX_LEN)
{
length = XCURSOR_COMMENT_MAX_LEN;
g_message (_("The parasite \"%s\" is too long for X cursor.\n"
"The overflowed string was dropped."),
gimp_any_to_utf8 (pname, -1,NULL));
}
string = g_strndup (gimp_parasite_data (parasite), length);
gimp_parasite_free (parasite);
}
return string;
}
/**
* Set hotspot to "hot-spot" parasite which format is common with that
* of file-xbm.
**/
static gboolean
set_hotspot_to_parasite (gint32 image_ID)
{
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_parasite_attach (image_ID, parasite);
gimp_parasite_free (parasite);
}
return ret;
}
/**
* Get back xhot & yhot from "hot-spot" parasite.
* If succeed, hotspot coordinate is set to xmcparas.x, xmcparas.y and
* return TRUE.
* If "hot-spot" is not found or broken, return FALSE.
**/
static gboolean
get_hotspot_from_parasite (gint32 image_ID)
{
GimpParasite *parasite = NULL;
g_return_val_if_fail (image_ID != -1, FALSE);
DM_XMC("function: getHotsopt\n");
parasite = gimp_image_parasite_find (image_ID, "hot-spot");
if (!parasite) /* cannot find a parasite named "hot-spot". */
{
return FALSE;
}
if (sscanf (gimp_parasite_data (parasite),
"%i %i", &xmcparas.x, &xmcparas.y) < 2)
{ /*cannot load hotspot.(parasite is broken?) */
return FALSE;
}
/*OK, hotspot is set to *xhotp & *yhotp. */
return TRUE;
}
/**
* Set size to sizep, delay to delayp from drawable's framename.
**/
static void
set_size_and_delay (const gchar *framename, guint32 *sizep, guint32 *delayp,
GRegex *re, gboolean *size_warnp)
{
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)
{ /* the number of different nominal size is over MAX_SIZE_NUM! */
g_set_error (errorp, 0, 0,
_("Sorry, this plug-in cannot handle a cursor "
"which contains over %i different nominal sizes."),
MAX_SIZE_NUM);
return NULL;
}
else /* append new element which "size" is given value. */
{
Counter[i].size = size;
break;
}
}
}
Counter[i].count += 1;
return g_strdup_printf ("(%dpx)_%0*d (%dms) (replace)", size, indent,
Counter[i].count, delay);
}
/**
* Get the region which is maintained when auto-crop.
**/
static void
get_cropped_region (GimpParamRegion *return_rgn,
GimpPixelRgn *pr)
{
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;
}