diff options
Diffstat (limited to 'plug-ins/common/file-xmc.c')
-rw-r--r-- | plug-ins/common/file-xmc.c | 2492 |
1 files changed, 2492 insertions, 0 deletions
diff --git a/plug-ins/common/file-xmc.c b/plug-ins/common/file-xmc.c new file mode 100644 index 0000000..a5c41f7 --- /dev/null +++ b/plug-ins/common/file-xmc.c @@ -0,0 +1,2492 @@ +/* + * X11 Mouse Cursor (XMC) plug-in for GIMP + * + * Copyright 2008-2009 Takeshi Matsuyama <tksmashiw@gmail.com> + * + * Special thanks: Alexia Death, Sven Neumann, Martin Nordholts + * and all community members. + */ + +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +/* + * Todo: if drawable->bpp != 4 in save_image for GIMP-2.8? + * Todo: support for "gimp-metadata" parasite. + * "xmc-copyright" and "xmc-license" may be deprecated in future? + */ + +/* + * This plug-in use these four parasites. + * "hot-spot" common with file-xbm plug-in + * "xmc-copyright" original, store contents of type1 comment chunk of Xcursor + * "xmc-license" original, store contents of type2 comment chunk of Xcursor + * "gimp-comment" common, store contents of type3 comment chunk of Xcursor + */ + +/* *** Caution: Size vs Dimension *** + * + * In this file, "size" and "dimension" are used in definitely + * different contexts. "Size" means nominal size of Xcursor which is + * used to determine which frame depends on which animation sequence + * and which sequence is really used. (for more detail, please read + * Xcursor(3).) On the other hand, "Dimension" simply means width + * and/or height. + */ + +#include "config.h" + +#include <stdlib.h> +#include <string.h> +#include <errno.h> + +#include <glib/gstdio.h> +#include <glib/gprintf.h> + +#include <libgimp/gimp.h> +#include <libgimp/gimpui.h> + +#include <X11/Xlib.h> +#include <X11/Xcursor/Xcursor.h> + +#include "libgimp/stdplugins-intl.h" + +/* For debug */ +/* #define XMC_DEBUG */ +#ifdef XMC_DEBUG +# define DM_XMC(...) g_fprintf(stderr, __VA_ARGS__) +#else +# define DM_XMC(...) +#endif + +/* + * Constants... + */ + +#define LOAD_PROC "file-xmc-load" +#define LOAD_THUMB_PROC "file-xmc-load-thumb" +#define SAVE_PROC "file-xmc-save" + +#define PLUG_IN_BINARY "file-xmc" +#define PLUG_IN_ROLE "gimp-file-xmc" + +/* We use "xmc" as the file extension of X cursor for convenience */ +#define XCURSOR_EXTENSION "xmc" +#define XCURSOR_MIME_TYPE "image/x-xcursor" + +/* The maximum dimension of Xcursor which is fully supported in any + * environments. This is defined on line 59 of xcursorint.h in + * libXcursor source code. Make sure this is about dimensions (width + * and height) not about nominal size despite of it's name. + * + * As of 2018, this macro still exists in libXCursor codebase, but I am + * unsure how far this restriction is enforced since this is a very low + * max dimension for today's displays. Therefore our code will not + * enforce this value anymore, but only warn about possible + * incompatibilities when using higher values. + */ +#define MAX_BITMAP_CURSOR_SIZE 64 + +/* The maximum dimension of each frame of X cursor we want to save + * should be MAX_BITMAP_CURSOR_SIZE but about loading, xhot(& yhot) of + * each frame varies from 0 to MAX_BITMAP_CURSOR_SIZE-1, so we need to + * set the maximum dimension of image no less than + * MAX_BITMAP_CURSOR_SIZE * 2( -1 to be precise) to remain hotspots on + * the same coordinates. + * + * We use four times value (256 for saving, 512 for loading) as a + * limitation because some cursors generated by CursorXP/FX to X11 + * Mouse Theme Converter is very large. + * + * The biggest cursor I found is "watch" of OuterLimits which size is + * 213x208. If you found bigger one, please tell me ;-) + */ +#define MAX_LOAD_DIMENSION 512 +#define MAX_SAVE_DIMENSION 256 + +/* The maximum number of different nominal sizes in one cursor this + * plug-in can treat. This is based on the number of cursor size which + * gnome-appearance-properties supports.(12,16,24,32,36,40,48,64) + * ref. capplets/common/gnome-theme-info.c in source of + * gnome-control-center + */ +#define MAX_SIZE_NUM 8 + +/* cursor delay is guint32 defined in Xcursor.h */ +#define CURSOR_MAX_DELAY 100000000 +#define CURSOR_DEFAULT_DELAY 50 +#define CURSOR_MINIMUM_DELAY 5 + +#define div_255(x) (((x) + 0x80 + (((x) + 0x80) >> 8)) >> 8) +#define READ32(f, e) read32 ((f), (e)); if (*(e)) return -1; +#define DISPLAY_DIGIT(x) ((x) > 100) ? 3 : ((x) > 10) ? 2 : 1 + +/* + * Structures... + */ + +typedef struct +{ + gboolean crop; + gint size; + gboolean size_replace; + gint32 delay; + gboolean delay_replace; +} XmcSaveVals; + +/* + * Local functions... + */ + +static void query (void); + +static void run (const gchar *name, + gint nparams, + const GimpParam *param, + gint *nreturn_vals, + GimpParam **return_vals); + +static gint32 load_image (const gchar *filename, + GError **error); + +static gint32 load_thumbnail (const gchar *filename, + gint32 thumb_size, + gint32 *width, + gint32 *height, + gint32 *num_layers, + GError **error); + +static guint32 read32 (FILE *f, + GError **error); + +static gboolean save_image (const gchar *filename, + gint32 image_ID, + gint32 drawable_ID, + gint32 orig_image_ID, + GError **error); + +static gboolean save_dialog (const gint32 image_ID, + GeglRectangle *hotspotRange); + +static void comment_entry_callback (GtkWidget *widget, + gchar **commentp); + +static void text_view_callback (GtkTextBuffer *buffer, + gchar **commentp); + +static gboolean load_default_hotspot (const gint32 image_ID, + GeglRectangle *hotspotRange); + +static inline guint32 separate_alpha (guint32 pixel); + +static inline guint32 premultiply_alpha (guint32 pixel); + +static XcursorComments *set_cursor_comments (void); + +static void load_comments (const gint32 image_ID); + +static gboolean set_comment_to_pname (const gint32 image_ID, + const gchar *content, + const gchar *pname); + +static gchar *get_comment_from_pname (const gint32 image_ID, + const gchar *pname); + +static gboolean set_hotspot_to_parasite (gint32 image_ID); + +static gboolean get_hotspot_from_parasite (gint32 image_ID); + +static void set_size_and_delay (const gchar *framename, + guint32 *sizep, + guint32 *delayp, + GRegex *re, + gboolean *size_warnp); + +static gchar *make_framename (guint32 size, + guint32 delay, + guint indent, + GError **errorp); + +static void get_cropped_region (GeglRectangle *retrun_rgn, + GeglBuffer *buffer); + +static inline gboolean pix_is_opaque (guint32 pix); + +static GeglRectangle * get_intersection_of_frames (gint32 image_ID); + +static gboolean pix_in_region (gint32 x, + gint32 y, + GeglRectangle *xmcrp); + +static void find_hotspots_and_dimensions (XcursorImages *xcIs, + gint32 *xhot, + gint32 *yhot, + gint32 *width, + gint32 *height); + +/* + * Globals... + */ + +const GimpPlugInInfo PLUG_IN_INFO = +{ + NULL, + NULL, + query, + run +}; + +static XmcSaveVals xmcvals = +{ + /* saved in pdb after this plug-in's process has gone. */ + FALSE, /* crop */ + 32, /* size */ + FALSE, /* size_replace */ + CURSOR_DEFAULT_DELAY, /* delay */ + FALSE /* delay_replace */ +}; + +static struct +{ + /* saved as parasites of original image after this plug-in's process has gone.*/ + gint32 x; /* hotspot x */ + gint32 y; /* hotspot y */ + gchar *comments[3]; /* copyright, license, other */ +} xmcparas = {0,}; + +/* parasites correspond to XcursorComment type */ +static const gchar *parasiteName[3] = { "xmc-copyright", + "xmc-license", + "gimp-comment" }; + +/* + * 'main()' - Main entry - just call gimp_main()... + */ + +MAIN () + + +/* + * 'query()' - Respond to a plug-in query... + */ + +static void +query (void) +{ + static const GimpParamDef load_args[] = + { + { GIMP_PDB_INT32, "run-mode", "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }" }, + { GIMP_PDB_STRING, "filename", "The name of the file to load" }, + { GIMP_PDB_STRING, "raw-filename", "The name of the file to load" } + }; + static const GimpParamDef load_return_vals[] = + { + { GIMP_PDB_IMAGE, "image", "Output image" } + }; + + static const GimpParamDef thumb_args[] = + { + { GIMP_PDB_STRING, "filename", "The name of the file to load" }, + { GIMP_PDB_INT32, "thumb-size", "Preferred thumbnail size" } + }; + static const GimpParamDef thumb_return_vals[] = + { + { GIMP_PDB_IMAGE, "image", "Thumbnail image" }, + { GIMP_PDB_INT32, "image-width", "The width of image" }, + { GIMP_PDB_INT32, "image-height", "The height of image" }, + { GIMP_PDB_INT32, "image-type", "The color type of image"}, + { GIMP_PDB_INT32, "image-num-layers", "The number of layeres" } + }; + + static const GimpParamDef save_args[] = + { + { GIMP_PDB_INT32, "run-mode", "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }" }, + { GIMP_PDB_IMAGE, "image", "Input image" }, + { GIMP_PDB_DRAWABLE, "drawable", "Drawable to export" }, + { GIMP_PDB_STRING, "filename", "The name of the file to export the image in" }, + { GIMP_PDB_STRING, "raw-filename", "The name entered" }, + /* following elements are XMC specific options */ + { GIMP_PDB_INT32, "x_hot", "X-coordinate of hot spot" }, + { GIMP_PDB_INT32, "y_hot", "Y-coordinate of hot spot\n" + "Use (-1, -1) to keep original hot spot."}, + { GIMP_PDB_INT32, "crop", "Auto-crop or not" }, + { GIMP_PDB_INT32, "size", "Default nominal size" }, + { GIMP_PDB_INT32, "size_replace", "Replace existent size or not." }, + { GIMP_PDB_INT32, "delay", "Default delay" }, + { GIMP_PDB_INT32, "delay_replace","Replace existent delay or not."}, + { GIMP_PDB_STRING, "copyright", "Copyright information." }, + { GIMP_PDB_STRING, "license", "License information." }, + { GIMP_PDB_STRING, "other", "Other comment.(taken from " + "\"gimp-comment\" parasite)" } + }; + + gimp_install_procedure (LOAD_PROC, + "Loads files of X11 Mouse Cursor file format", + "This plug-in loads X11 Mouse Cursor (XMC) files.", + "Takeshi Matsuyama <tksmashiw@gmail.com>", + "Takeshi Matsuyama", + "26 May 2009", + N_("X11 Mouse Cursor"), + NULL, + GIMP_PLUGIN, + G_N_ELEMENTS (load_args), + G_N_ELEMENTS (load_return_vals), + load_args, + load_return_vals); + + gimp_register_file_handler_mime (LOAD_PROC, XCURSOR_MIME_TYPE); + gimp_register_magic_load_handler (LOAD_PROC, + XCURSOR_EXTENSION, + "", + "0,string,Xcur"); + + gimp_install_procedure (LOAD_THUMB_PROC, + "Loads only first frame of X11 Mouse Cursor's " + "animation sequence which nominal size is the closest " + "of thumb-size to be used as a thumbnail", + "", + "Takeshi Matsuyama <tksmashiw@gmail.com>", + "Takeshi Matsuyama", + "26 May 2009", + NULL, + NULL, + GIMP_PLUGIN, + G_N_ELEMENTS (thumb_args), + G_N_ELEMENTS (thumb_return_vals), + thumb_args, + thumb_return_vals); + + gimp_register_thumbnail_loader (LOAD_PROC, LOAD_THUMB_PROC); + + gimp_install_procedure (SAVE_PROC, + "Exports files of X11 cursor file", + "This plug-in exports X11 Mouse Cursor (XMC) files", + "Takeshi Matsuyama <tksmashiw@gmail.com>", + "Takeshi Matsuyama", + "26 May 2009", + N_("X11 Mouse Cursor"), + "RGBA", + GIMP_PLUGIN, + G_N_ELEMENTS (save_args), 0, + save_args, NULL); + + gimp_register_file_handler_mime (SAVE_PROC, XCURSOR_MIME_TYPE); + gimp_register_save_handler (SAVE_PROC, XCURSOR_EXTENSION, ""); +} + +/* + * 'run()' - Run the plug-in... + */ + +static void +run (const gchar *name, + gint nparams, + const GimpParam *param, + gint *nreturn_vals, + GimpParam **return_vals) +{ + static GimpParam values[6]; + GimpRunMode run_mode; + GimpPDBStatusType status = GIMP_PDB_SUCCESS; + gint32 image_ID; + gint32 drawable_ID; + gint32 orig_image_ID; + GimpExportReturn export = GIMP_EXPORT_CANCEL; + GeglRectangle *hotspotRange = NULL; + gint32 width, height; + gint32 num_layers; + GError *error = NULL; + gint i; + + INIT_I18N (); + gegl_init (NULL, NULL); + + DM_XMC ("run: start.\n"); + *nreturn_vals = 1; + *return_vals = values; + + values[0].type = GIMP_PDB_STATUS; + values[0].data.d_status = GIMP_PDB_EXECUTION_ERROR; + + if (strcmp (name, LOAD_PROC) == 0) + { + DM_XMC ("Starting to load file.\tparam.data=%s\n", + param[1].data.d_string); + image_ID = load_image (param[1].data.d_string, &error); + + if (image_ID != -1) + { + *nreturn_vals = 2; + values[1].type = GIMP_PDB_IMAGE; + values[1].data.d_image = image_ID; + DM_XMC ("LOAD_PROC successfully load image. image_ID=%i\n", image_ID); + } + else + { + status = GIMP_PDB_EXECUTION_ERROR; + } + } + else if (strcmp (name, LOAD_THUMB_PROC) == 0) + { + DM_XMC ("Starting to load thumbnail.\tfilename=%s\tthumb-size=%d\n", + param[0].data.d_string, param[1].data.d_int32); + image_ID = load_thumbnail (param[0].data.d_string, + param[1].data.d_int32, + &width, + &height, + &num_layers, + &error); + + if (image_ID != -1) + { + *nreturn_vals = 6; + values[1].type = GIMP_PDB_IMAGE; + values[1].data.d_image = image_ID; + values[2].type = GIMP_PDB_INT32; + values[2].data.d_int32 = width; /* width */ + values[3].type = GIMP_PDB_INT32; + values[3].data.d_int32 = height; /* height */ + /* This will not work on GIMP 2.6, but not harmful. */ + values[4].type = GIMP_PDB_INT32; + values[4].data.d_int32 = GIMP_RGBA_IMAGE; /* type */ + values[5].type = GIMP_PDB_INT32; + values[5].data.d_int32 = num_layers; /* num_layers */ + + DM_XMC ("LOAD_THUMB_PROC successfully load image. image_ID=%i\n", image_ID); + } + else + { + status = GIMP_PDB_EXECUTION_ERROR; + } + } + else if (strcmp (name, SAVE_PROC) == 0) + { + DM_XMC ("run: export %s\n", name); + run_mode = param[0].data.d_int32; + image_ID = orig_image_ID = param[1].data.d_int32; + drawable_ID = param[2].data.d_int32; + hotspotRange = get_intersection_of_frames (image_ID); + + if (! hotspotRange) + { + g_set_error (&error, 0, 0, + _("Cannot set the hot spot!\n" + "You must arrange layers so that all of them have an intersection.")); + *nreturn_vals = 2; + values[1].type = GIMP_PDB_STRING; + values[1].data.d_string = error->message; + values[0].data.d_status = GIMP_PDB_EXECUTION_ERROR; + + return; + } + + /* eventually export the image */ + switch (run_mode) + { + case GIMP_RUN_INTERACTIVE: + case GIMP_RUN_WITH_LAST_VALS: + gimp_ui_init (PLUG_IN_BINARY, FALSE); + + export = gimp_export_image (&image_ID, &drawable_ID, "XMC", + GIMP_EXPORT_CAN_HANDLE_RGB | + GIMP_EXPORT_CAN_HANDLE_ALPHA | + GIMP_EXPORT_CAN_HANDLE_LAYERS | + GIMP_EXPORT_NEEDS_ALPHA); + + if (export == GIMP_EXPORT_CANCEL) + { + *nreturn_vals = 1; + values[0].data.d_status = GIMP_PDB_CANCEL; + + return; + } + break; + + default: + break; + } + switch (run_mode) + { + case GIMP_RUN_INTERACTIVE: + /* + * Possibly retrieve data... + */ + gimp_get_data (SAVE_PROC, &xmcvals); + load_comments (image_ID); + + load_default_hotspot (image_ID, hotspotRange); + + if (! save_dialog (image_ID, hotspotRange)) + status = GIMP_PDB_CANCEL; + break; + + case GIMP_RUN_NONINTERACTIVE: + /* + * Make sure all the arguments are there! + */ + if (nparams != 15) + { + status = GIMP_PDB_CALLING_ERROR; + } + else + { + if (pix_in_region (param[5].data.d_int32, param[6].data.d_int32, + hotspotRange)) + { /* if passed hotspot is acceptable, use that ones. */ + xmcparas.x = param[5].data.d_int32; + xmcparas.y = param[6].data.d_int32; + } + else + { + load_default_hotspot (image_ID, hotspotRange); + /* you can purposely choose non acceptable values for hotspot + to use cursor's original values. */ + } + xmcvals.crop = param[7].data.d_int32; + xmcvals.size = param[8].data.d_int32; + xmcvals.size_replace = param[9].data.d_int32; + /* load delay */ + if (param[10].data.d_int32 < CURSOR_MINIMUM_DELAY) + { + xmcvals.delay = CURSOR_DEFAULT_DELAY; + } + else + { + xmcvals.delay = param[10].data.d_int32; + } + xmcvals.delay_replace = param[11].data.d_int32; + load_comments (image_ID); + for (i = 0; i < 3; ++i) + { + if (param[i + 12].data.d_string && + g_utf8_validate (param[i + 12].data.d_string, -1, NULL)) + { + xmcparas.comments[i] = g_strdup (param[i + 12].data.d_string); + } + } + } + break; + + case GIMP_RUN_WITH_LAST_VALS: + /* Possibly retrieve data... */ + gimp_get_data (SAVE_PROC, &xmcvals); + load_comments (image_ID); + load_default_hotspot (image_ID, hotspotRange); + break; + + default: + break; + } + if (status == GIMP_PDB_SUCCESS) + { + if (save_image (param[3].data.d_string, image_ID, + drawable_ID, orig_image_ID, &error)) + { + gimp_set_data (SAVE_PROC, &xmcvals, sizeof (XmcSaveVals)); + } + else + { + status = GIMP_PDB_EXECUTION_ERROR; + } + } + + if (export == GIMP_EXPORT_EXPORT) + gimp_image_delete (image_ID); + + g_free (hotspotRange); + + for (i = 0; i < 3 ; ++i) + { + g_free (xmcparas.comments[i]); + xmcparas.comments[i] = NULL; + } + } + else + { + DM_XMC ("name=%s\n", name); + status = GIMP_PDB_CALLING_ERROR; + } + + if (status != GIMP_PDB_SUCCESS && error) + { + *nreturn_vals = 2; + values[1].type = GIMP_PDB_STRING; + values[1].data.d_string = error->message; + } + + values[0].data.d_status = status; + DM_XMC ("run: finish\n"); +} + + +/* + * 'load_image()' - Load a X cursor image into a new image window. + */ + +static gint32 +load_image (const gchar *filename, + GError **error) +{ + FILE *fp; + gint32 image_ID; + gint32 layer_ID; + GeglBuffer *buffer; + XcursorComments *commentsp; /* pointer to comments */ + XcursorImages *imagesp; /* pointer to images*/ + guint32 delay; /* use guint32 instead CARD32(in X11/Xmd.h) */ + gchar *framename; /* name of layer */ + guint32 *tmppixel; /* pixel data (guchar * bpp = guint32) */ + gint img_width; + gint img_height; + gint i, j; + + gimp_progress_init_printf (_("Opening '%s'"), + gimp_filename_to_utf8 (filename)); + + /* Open the file and check it is a valid X cursor */ + + fp = g_fopen (filename, "rb"); + + if (fp == NULL) + { + g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), + _("Could not open '%s' for reading: %s"), + gimp_filename_to_utf8 (filename), g_strerror (errno)); + return -1; + } + + if (! XcursorFileLoad (fp, &commentsp, &imagesp)) + { + g_set_error (error, 0, 0, _("'%s' is not a valid X cursor."), + gimp_filename_to_utf8 (filename)); + fclose (fp); + return -1; + } + + /* check dimension is valid. */ + + for (i = 0; i < imagesp->nimage; i++) + { + if (imagesp->images[i]->width > MAX_LOAD_DIMENSION) + { + g_set_error (error, 0, 0, + _("Frame %d of '%s' is too wide for an X cursor."), + i + 1, gimp_filename_to_utf8 (filename)); + fclose (fp); + return -1; + } + if (imagesp->images[i]->height > MAX_LOAD_DIMENSION) + { + g_set_error (error, 0, 0, + _("Frame %d of '%s' is too high for an X cursor."), + i + 1, gimp_filename_to_utf8 (filename)); + fclose (fp); + return -1; + } + } + + find_hotspots_and_dimensions (imagesp, + &xmcparas.x, &xmcparas.y, + &img_width, &img_height); + + DM_XMC ("xhot=%i,\tyhot=%i,\timg_width=%i,\timg_height=%i\n", + xmcparas.x, xmcparas.y, img_width, img_height); + + image_ID = gimp_image_new (img_width, img_height, GIMP_RGB); + + gimp_image_set_filename (image_ID, filename); + + if (! set_hotspot_to_parasite (image_ID)) + { + fclose (fp); + return -1; + } + + /* Temporary buffer */ + tmppixel = g_new (guint32, img_width * img_height); + + /* load each frame to each layer one by one */ + for (i = 0; i < imagesp->nimage; i++) + { + gint width = imagesp->images[i]->width; + gint height = imagesp->images[i]->height; + + delay = imagesp->images[i]->delay; + + if (delay < CURSOR_MINIMUM_DELAY) + { + delay = CURSOR_DEFAULT_DELAY; + } + + DM_XMC ("images[%i]->delay=%i\twidth=%d\theight=%d\n", + i ,delay, imagesp->images[i]->width, imagesp->images[i]->height); + + framename = make_framename (imagesp->images[i]->size, delay, + DISPLAY_DIGIT (imagesp->nimage), error); + if (! framename) + { + fclose (fp); + return -1; + } + + layer_ID = gimp_layer_new (image_ID, framename, width, height, + GIMP_RGBA_IMAGE, + 100, + gimp_image_get_default_new_layer_mode (image_ID)); + gimp_image_insert_layer (image_ID, layer_ID, -1, 0); + + /* Adjust layer position to let hotspot sit on the same point. */ + gimp_item_transform_translate (layer_ID, + xmcparas.x - imagesp->images[i]->xhot, + xmcparas.y - imagesp->images[i]->yhot); + + g_free (framename); + + /* Get the buffer for our load... */ + + buffer = gimp_drawable_get_buffer (layer_ID); + + /* set color to each pixel */ + for (j = 0; j < width * height; j++) + { + tmppixel[j] = separate_alpha (imagesp->images[i]->pixels[j]); + } + + /* set pixel */ + gegl_buffer_set (buffer, GEGL_RECTANGLE (0, 0, width, height), 0, + NULL, tmppixel, GEGL_AUTO_ROWSTRIDE); + + gimp_progress_update ((i + 1) / imagesp->nimage); + + g_object_unref (buffer); + } + + g_free (tmppixel); + + gimp_progress_update (1.0); + + /* Comment parsing */ + + if (commentsp) + { + for (i = 0; i < commentsp->ncomment; ++i) + { + DM_XMC ("comment type=%d\tcomment=%s\n", + commentsp->comments[i]->comment_type, + commentsp->comments[i]->comment); + if (! set_comment_to_pname (image_ID, + commentsp->comments[i]->comment, + parasiteName[commentsp->comments[i]->comment_type -1])) + { + DM_XMC ("Failed to write %ith comment.\n", i); + fclose (fp); + return -1; + } + } + } + + DM_XMC ("Comment parsing done.\n"); + XcursorImagesDestroy (imagesp); + XcursorCommentsDestroy (commentsp); + fclose (fp); + + gimp_progress_end (); + + return image_ID; +} + +/* + * load_thumbnail + */ + +static gint32 +load_thumbnail (const gchar *filename, + gint32 thumb_size, + gint32 *thumb_width, + gint32 *thumb_height, + gint32 *thumb_num_layers, + GError **error) +{ + /* Return only one frame for thumbnail. + * We select first frame of an animation sequence which nominal size is the + * closest of thumb_size. + */ + + XcursorImages *xcIs = NULL; /* use to find the dimensions of thumbnail */ + XcursorImage *xcI; /* temporary pointer to XcursorImage */ + guint32 *positions; /* array of the offsets of image chunks */ + guint32 size; /* nominal size */ + guint32 diff; /* difference between thumb_size and current size */ + guint32 min_diff = XCURSOR_IMAGE_MAX_SIZE; /* minimum value of diff */ + guint32 type; /* chunk type */ + FILE *fp = NULL; + gint32 image_ID = -1; + gint32 layer_ID; + GeglBuffer *buffer; + guint32 *tmppixel; /* pixel data (guchar * bpp = guint32) */ + guint32 ntoc = 0; /* the number of table of contents */ + gint sel_num = -1; /* the index of selected image chunk */ + gint width; + gint height; + gint i; + + g_return_val_if_fail (thumb_width, -1); + g_return_val_if_fail (thumb_height, -1); + g_return_val_if_fail (thumb_num_layers, -1); + + *thumb_width = 0; + *thumb_height = 0; + *thumb_num_layers = 0; + + fp = g_fopen (filename, "rb"); + + if (fp == NULL) + { + g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), + _("Could not open '%s' for reading: %s"), + gimp_filename_to_utf8 (filename), g_strerror (errno)); + return -1; + } + + /* From this line, we make a XcursorImages struct so that we can find out the + * width and height of entire image. + * We can use XcursorFileLoadImages (fp, thumb_size) from libXcursor instead + * of this ugly code but XcursorFileLoadImages loads all pixel data of the + * image chunks on memory thus we should not use it. + */ + + /* find which image chunk is preferred to load. */ + + /* skip magic, headersize, version */ + fseek (fp, 12, SEEK_SET); + /* read the number of chunks */ + ntoc = READ32 (fp, error) + if (ntoc > (G_MAXUINT32 / sizeof (guint32))) + { + g_set_error (error, 0, 0, + "'%s' seems to have an incorrect toc size.", + gimp_filename_to_utf8 (filename)); + fclose (fp); + return -1; + } + positions = g_malloc (ntoc * sizeof (guint32)); + + /* enter list of toc(table of contents) */ + for (; ntoc > 0; --ntoc) + { + /* read entry type */ + type = READ32 (fp, error) + if (type != XCURSOR_IMAGE_TYPE) + { + /* not a image */ + + /* skip rest of this content */ + fseek (fp, 8, SEEK_CUR); + } + else + { + /* this content is image */ + + size = READ32 (fp, error) + positions[*thumb_num_layers] = READ32 (fp, error) + /* is this image is more preferred than selected before? */ + diff = MAX (thumb_size, size) - MIN (thumb_size, size); + if (diff < min_diff) + {/* the image size is closer than current selected image */ + min_diff = diff; + sel_num = *thumb_num_layers; + } + ++*thumb_num_layers; + } + } + + if (sel_num < 0) + { + g_set_error (error, 0, 0, + _("there is no image chunk in \"%s\"."), + gimp_filename_to_utf8 (filename)); + fclose (fp); + return -1; + } + + /* get width and height of entire image */ + + /* Let's make XcursorImages */ + xcIs = XcursorImagesCreate (*thumb_num_layers); + xcIs->nimage = *thumb_num_layers; + for (i = 0; i < xcIs->nimage; ++i) + { + /* make XcursorImage with no pixel buffer */ + xcI = XcursorImageCreate (0, 0); + /* go to the image chunk header */ + fseek (fp, positions[i], SEEK_SET); + /* skip chunk header */ + fseek (fp, 16, SEEK_CUR); + /* read properties of this image to determine entire image dimensions */ + xcI->width = READ32 (fp, error) + xcI->height = READ32 (fp, error) + xcI->xhot = READ32 (fp, error) + xcI->yhot = READ32 (fp, error) + + xcIs->images[i] = xcI; + } + + DM_XMC ("selected size is %i or %i\n", + thumb_size - min_diff, thumb_size + min_diff); + + /* get entire image dimensions */ + find_hotspots_and_dimensions (xcIs, NULL, NULL, thumb_width, thumb_height); + + DM_XMC ("width=%i\theight=%i\tnum-layers=%i\n", + *thumb_width, *thumb_height, xcIs->nimage); + + /* dimension check */ + if (*thumb_width > MAX_LOAD_DIMENSION) + { + g_set_error (error, 0, 0, + _("'%s' is too wide for an X cursor."), + gimp_filename_to_utf8 (filename)); + fclose (fp); + return -1; + } + + if (*thumb_height > MAX_LOAD_DIMENSION) + { + g_set_error (error, 0, 0, + _("'%s' is too high for an X cursor."), + gimp_filename_to_utf8 (filename)); + fclose (fp); + return -1; + } + + /* create new image! */ + + width = xcIs->images[sel_num]->width; + height = xcIs->images[sel_num]->height; + + image_ID = gimp_image_new (width, height, GIMP_RGB); + + layer_ID = gimp_layer_new (image_ID, NULL, width, height, + GIMP_RGBA_IMAGE, + 100, + gimp_image_get_default_new_layer_mode (image_ID)); + + gimp_image_insert_layer (image_ID, layer_ID, -1, 0); + + /* + * Get the drawable and set the pixel region for our load... + */ + + buffer = gimp_drawable_get_buffer (layer_ID); + + /* Temporary buffer */ + tmppixel = g_new (guint32, width * height); + + /* copy the chunk data to tmppixel */ + fseek (fp, positions[sel_num], SEEK_SET); + fseek (fp, 36, SEEK_CUR); /* skip chunk header(16bytes), xhot, yhot, width, height, delay */ + + for (i = 0; i < width * height; i++) + { + tmppixel[i] = READ32 (fp, error) + /* get back separate alpha */ + tmppixel[i] = separate_alpha (tmppixel[i]); + } + + /* set pixel */ + gegl_buffer_set (buffer, GEGL_RECTANGLE (0, 0, width, height), 0, + NULL, tmppixel, GEGL_AUTO_ROWSTRIDE); + + /* free tmppixel */ + g_free(tmppixel); + g_free (positions); + fclose (fp); + + g_object_unref (buffer); + + return image_ID; +} + +/* read guint32 value from f despite of host's byte order. */ +static guint32 +read32 (FILE *f, + GError **error) +{ + guchar p[4]; + guint32 ret; + + if (fread (p, 1, 4, f) != 4) + { + g_set_error (error, 0, 0, _("A read error occurred.")); + return 0; + } + +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + ret = p[0] + (p[1]<<8) + (p[2]<<16) + (p[3]<<24); +#elif G_BYTE_ORDER == G_BIG_ENDIAN + ret = p[3] + (p[2]<<8) + (p[1]<<16) + (p[0]<<24); +#elif G_BYTE_ORDER == G_PDP_ENDIAN + ret = p[2] + (p[3]<<8) + (p[0]<<16) + (p[1]<<24); +#else + g_return_val_if_rearched (); +#endif + + return ret; +} + +/* 'save_dialog ()' + */ +static gboolean +save_dialog (const gint32 image_ID, + GeglRectangle *hotspotRange) +{ + GtkWidget *dialog; + GtkWidget *frame; + GtkWidget *table; + GtkWidget *box; + GtkAdjustment *adjustment; + GtkWidget *alignment; + GtkWidget *tmpwidget; + GtkWidget *label; + GtkTextBuffer *textbuffer; + GValue val = G_VALUE_INIT; + gint x1, x2, y1, y2; + gboolean run; + + g_value_init (&val, G_TYPE_DOUBLE); + dialog = gimp_export_dialog_new (_("X11 Mouse Cursor"), + PLUG_IN_BINARY, SAVE_PROC); + + /* + * parameter settings + */ + frame = gimp_frame_new (_("XMC Options")); + gtk_container_set_border_width (GTK_CONTAINER (frame), 12); + gtk_box_pack_start (GTK_BOX (gimp_export_dialog_get_content_area (dialog)), + frame, TRUE, TRUE, 0); + gtk_widget_show (frame); + + table = gtk_table_new (9, 3, FALSE); + gtk_widget_show (table); + gtk_table_set_col_spacings (GTK_TABLE (table), 6); + gtk_table_set_row_spacings (GTK_TABLE (table), 6); + gtk_container_set_border_width (GTK_CONTAINER (table), 12); + gtk_container_add (GTK_CONTAINER (frame), table); + + /* + * Hotspot + */ + /* label "Hot spot _X:" + spinbox */ + x1 = hotspotRange->x; + x2 = hotspotRange->width + hotspotRange->x - 1; + + adjustment = (GtkAdjustment *) + gtk_adjustment_new (xmcparas.x, x1, x2, 1, 5, 0); + tmpwidget = gimp_spin_button_new (adjustment, 1.0, 0); + gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (tmpwidget), TRUE); + g_value_set_double (&val, 1.0); + g_object_set_property (G_OBJECT (tmpwidget), "xalign", &val);/* align right*/ + gimp_table_attach_aligned (GTK_TABLE (table), 0, 0, + _("Hot spot _X:"), 0, 0.5, tmpwidget, 1, TRUE); + gtk_widget_show (tmpwidget); + + g_signal_connect (adjustment, "value-changed", + G_CALLBACK (gimp_int_adjustment_update), + &xmcparas.x); + + gimp_help_set_help_data (tmpwidget, + _("Enter the X coordinate of the hot spot. " + "The origin is top left corner."), + NULL); + + /* label "Y:" + spinbox */ + y1 = hotspotRange->y; + y2 = hotspotRange->height + hotspotRange->y - 1; + + adjustment = (GtkAdjustment *) + gtk_adjustment_new (xmcparas.y, y1, y2, 1, 5, 0); + tmpwidget = gimp_spin_button_new (adjustment, 1.0, 0); + gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (tmpwidget), TRUE); + g_value_set_double (&val, 1.0); + g_object_set_property (G_OBJECT (tmpwidget), "xalign", &val);/* align right*/ + gimp_table_attach_aligned (GTK_TABLE (table), 1, 0, + "_Y:", 1.0, 0.5, tmpwidget, 1, TRUE); + gtk_widget_show (tmpwidget); + + g_signal_connect (adjustment, "value-changed", + G_CALLBACK (gimp_int_adjustment_update), + &xmcparas.y); + + gimp_help_set_help_data (tmpwidget, + _("Enter the Y coordinate of the hot spot. " + "The origin is top left corner."), + NULL); + + /* + * Auto-crop + */ + /* check button */ + tmpwidget = + gtk_check_button_new_with_mnemonic (_("_Auto-Crop all frames.")); + gtk_table_attach (GTK_TABLE (table), + tmpwidget, 0, 3, 1, 2, GTK_FILL, 0, 0, 10); + gtk_widget_show (tmpwidget); + + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (tmpwidget), + xmcvals.crop); + gtk_widget_show (tmpwidget); + g_signal_connect (tmpwidget, "toggled", + G_CALLBACK (gimp_toggle_button_update), + &xmcvals.crop); + /* tooltip */ + gimp_help_set_help_data (tmpwidget, + _("Remove the empty borders of all frames.\n" + "This reduces the file size and may fix " + "the problem that some large cursors disorder " + "the screen.\n" + "Uncheck if you plan to edit the exported " + "cursor using other programs."), + NULL); + + /* + * size + */ + tmpwidget = + gimp_int_combo_box_new ("12px", 12, "16px", 16, + "24px", 24, "32px", 32, + "36px", 36, "40px", 40, + "48px", 48, "64px", 64, NULL); + gimp_int_combo_box_connect (GIMP_INT_COMBO_BOX (tmpwidget), + 32, + G_CALLBACK (gimp_int_combo_box_get_active), + &xmcvals.size); + gtk_widget_show (tmpwidget); + /* tooltip */ + gimp_help_set_help_data (tmpwidget, + _("Choose the nominal size of frames.\n" + "If you don't have plans to make multi-sized " + "cursor, or you have no idea, leave it \"32px\".\n" + "Nominal size has no relation with the actual " + "size (width or height).\n" + "It is only used to determine which frame depends " + "on which animation sequence, and which sequence " + "is used based on the value of " + "\"gtk-cursor-theme-size\"."), + NULL); + + gimp_table_attach_aligned (GTK_TABLE (table), 0, 2, + _("_Size:"), 0, 0.5, tmpwidget, 3, TRUE); + /* Replace size ? */ + tmpwidget = + gimp_int_radio_group_new (FALSE, NULL, G_CALLBACK (gimp_radio_button_update), + &xmcvals.size_replace, xmcvals.size_replace, + _("_Use this value only for a frame which size " + "is not specified."), + FALSE, NULL, + _("_Replace the size of all frames even if it " + "is specified."), + TRUE, NULL, + NULL); + alignment = gtk_alignment_new (0.5, 0.5, 1.0, 1.0); + gtk_widget_show (alignment); + gtk_table_attach (GTK_TABLE (table), alignment, 0, 3, 3, 4, 0, 0, 0, 0); + gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 0, 6, 20, 0); /*padding left*/ + gtk_container_add (GTK_CONTAINER (alignment), tmpwidget); + gtk_widget_show (tmpwidget); + + /* + * delay + */ + /* spin button */ + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gimp_table_attach_aligned (GTK_TABLE (table), 0, 4, _("_Delay:"), + 0, 0.5, box, 3, TRUE); + gtk_widget_show (box); + + gimp_help_set_help_data (box, + _("Enter time span in milliseconds in which " + "each frame is rendered."), + NULL); + + adjustment = (GtkAdjustment *) + gtk_adjustment_new (xmcvals.delay, CURSOR_MINIMUM_DELAY, + CURSOR_MAX_DELAY, 1, 5, 0); + tmpwidget = gimp_spin_button_new (adjustment, 1.0, 0); + gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (tmpwidget), TRUE); + g_value_set_double (&val, 1.0); + g_object_set_property (G_OBJECT (tmpwidget), "xalign", &val);/* align right*/ + gtk_box_pack_start (GTK_BOX (box), tmpwidget, TRUE, TRUE, 0); + gtk_widget_show (tmpwidget); + + g_signal_connect (adjustment, "value-changed", + G_CALLBACK (gimp_int_adjustment_update), + &xmcvals.delay); + + /* appended "ms" */ + tmpwidget = gtk_label_new ("ms"); + gtk_label_set_xalign (GTK_LABEL (tmpwidget), 0.0); /*align left*/ + gtk_box_pack_start (GTK_BOX (box), tmpwidget, TRUE, TRUE, 0); + gtk_widget_show (tmpwidget); + + /* Replace delay? */ + tmpwidget = + gimp_int_radio_group_new (FALSE, NULL, G_CALLBACK (gimp_radio_button_update), + &xmcvals.delay_replace, xmcvals.delay_replace, + _("_Use this value only for a frame which delay " + "is not specified."), + FALSE, NULL, + _("_Replace the delay of all frames even if it " + "is specified."), + TRUE, NULL, + NULL); + alignment = gtk_alignment_new (0.5, 0.5, 1.0, 1.0); + gtk_widget_show (alignment); + gtk_table_attach (GTK_TABLE (table), alignment, 0, 3, 5, 6, 0, 0, 0, 0); + gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 0, 6, 20, 0); /*padding left*/ + gtk_container_add (GTK_CONTAINER (alignment), tmpwidget); + gtk_widget_show (tmpwidget); + + /* + * Copyright + */ + tmpwidget = gtk_entry_new (); + /* Maximum length will be clamped to 65536 */ + gtk_entry_set_max_length (GTK_ENTRY (tmpwidget), XCURSOR_COMMENT_MAX_LEN); + + if (xmcparas.comments[0]) + { + gtk_entry_set_text (GTK_ENTRY (tmpwidget), + gimp_any_to_utf8 (xmcparas.comments[0], - 1, NULL)); + /* show warning if comment is over 65535 characters + * because gtk_entry can hold only that. */ + if (strlen (gtk_entry_get_text (GTK_ENTRY (tmpwidget))) >= 65535) + g_message (_("The part of copyright information " + "that exceeded 65535 characters was removed.")); + } + + g_signal_connect (tmpwidget, "changed", + G_CALLBACK (comment_entry_callback), + xmcparas.comments); + gtk_widget_show (tmpwidget); + /* tooltip */ + gimp_help_set_help_data (tmpwidget, + _("Enter copyright information."), + NULL); + gimp_table_attach_aligned (GTK_TABLE (table), 0, 6, _("_Copyright:"), + 0, 0.5, tmpwidget, 3, FALSE); + /* + * License + */ + tmpwidget = gtk_entry_new (); + /* Maximum length will be clamped to 65536 */ + gtk_entry_set_max_length (GTK_ENTRY (tmpwidget), XCURSOR_COMMENT_MAX_LEN); + + if (xmcparas.comments[1]) + { + gtk_entry_set_text (GTK_ENTRY (tmpwidget), + gimp_any_to_utf8 (xmcparas.comments[1], - 1, NULL)); + /* show warning if comment is over 65535 characters + * because gtk_entry can hold only that. */ + if (strlen (gtk_entry_get_text (GTK_ENTRY (tmpwidget))) >= 65535) + g_message (_("The part of license information " + "that exceeded 65535 characters was removed.")); + } + + g_signal_connect (tmpwidget, "changed", + G_CALLBACK (comment_entry_callback), + xmcparas.comments + 1); + gtk_widget_show (tmpwidget); + /* tooltip */ + gimp_help_set_help_data (tmpwidget, + _("Enter license information."), + NULL); + gimp_table_attach_aligned (GTK_TABLE (table), 0, 7, _("_License:"), + 0, 0.5, tmpwidget, 3, FALSE); + /* + * Other + */ + /* We use gtk_text_view for "Other" while "Copyright" & "License" is entered + * in gtk_entry because We want allow '\n' for "Other". */ + label = gtk_label_new_with_mnemonic (_("_Other:")); + gtk_widget_show (label); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); /*align top-left*/ + gtk_label_set_yalign (GTK_LABEL (label), 0.0); /*align top-left*/ + gtk_table_attach (GTK_TABLE (table), label, 0, 1, 8, 9, GTK_FILL, 0, 0, 0); + /* content of Other */ + /* scrolled window */ + box = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (box), + GTK_SHADOW_IN); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (box), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_AUTOMATIC); + gtk_table_attach (GTK_TABLE (table), box, 1, 3, 8, 9, GTK_FILL, 0, 0, 0); + gtk_widget_show (box); + /* textbuffer */ + textbuffer = gtk_text_buffer_new (NULL); + if (xmcparas.comments[2]) + gtk_text_buffer_set_text (textbuffer, + gimp_any_to_utf8 (xmcparas.comments[2], -1, NULL), + -1); + g_signal_connect (textbuffer, "changed", + G_CALLBACK (text_view_callback), + xmcparas.comments + 2); + /* textview */ + tmpwidget = + gtk_text_view_new_with_buffer (GTK_TEXT_BUFFER (textbuffer)); + gtk_text_view_set_accepts_tab (GTK_TEXT_VIEW (tmpwidget), FALSE); + gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tmpwidget), GTK_WRAP_WORD); + g_object_unref (textbuffer); + gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tmpwidget), GTK_WRAP_WORD); + gtk_container_add (GTK_CONTAINER (box), tmpwidget); + gtk_widget_show (tmpwidget); + /* tooltip */ + gimp_help_set_help_data (tmpwidget, + _("Enter other comment if you want."), + NULL); + gtk_label_set_mnemonic_widget (GTK_LABEL (label), tmpwidget); + + /* + * all widget is prepared. Let's show dialog. + */ + gtk_widget_show (dialog); + + run = (gimp_dialog_run (GIMP_DIALOG (dialog)) == GTK_RESPONSE_OK); + + gtk_widget_destroy (dialog); + + return run; +} + +/* + * callback function of gtk_entry for "copyright" and "license". + * "other" is processed by text_view_callback + */ +static void +comment_entry_callback (GtkWidget *widget, + gchar **commentp) +{ + const gchar *text; + + g_return_if_fail (commentp); + + text = gtk_entry_get_text (GTK_ENTRY (widget)); + + /* This will not happen because sizeof(gtk_entry) < XCURSOR_COMMENT_MAX_LEN */ + g_return_if_fail (strlen (text) <= XCURSOR_COMMENT_MAX_LEN); + + g_free (*commentp); + *commentp = g_strdup (text); +} + +static void +text_view_callback (GtkTextBuffer *buffer, + gchar **commentp) +{ + GtkTextIter start_iter; + GtkTextIter end_iter; + gchar *text; + + g_return_if_fail (commentp != NULL); + + gtk_text_buffer_get_bounds (buffer, &start_iter, &end_iter); + text = gtk_text_buffer_get_text (buffer, &start_iter, &end_iter, FALSE); + + if (strlen (text) > XCURSOR_COMMENT_MAX_LEN) + { + g_message (_("Comment is limited to %d characters."), + XCURSOR_COMMENT_MAX_LEN); + + gtk_text_buffer_get_iter_at_offset (buffer, &start_iter, + XCURSOR_COMMENT_MAX_LEN - 1); + gtk_text_buffer_get_end_iter (buffer, &end_iter); + + gtk_text_buffer_delete (buffer, &start_iter, &end_iter); + } + else + { + g_free (*commentp); + *commentp = g_strdup (text); + } +} + +/** + * Set default hotspot based on hotspotRange. +**/ +static gboolean +load_default_hotspot (const gint32 image_ID, + GeglRectangle *hotspotRange) +{ + + g_return_val_if_fail (hotspotRange, FALSE); + + if ( /* if we cannot load hotspot correctly */ + ! get_hotspot_from_parasite (image_ID) || + /* ,or hostspot is out of range */ + ! pix_in_region (xmcparas.x, xmcparas.y, hotspotRange)) + { + /* then use top left point of hotspotRange as fallback. */ + xmcparas.x = hotspotRange->x; + xmcparas.y = hotspotRange->y; + } + + return TRUE; +} + +/* + * 'save_image ()' - Save the specified image to X cursor file. + */ + +static gboolean +save_image (const gchar *filename, + gint32 image_ID, + gint32 drawable_ID, + gint32 orig_image_ID, + GError **error) +{ + FILE *fp; /* File pointer */ + gboolean dimension_warn = FALSE; /* become TRUE if even one + * of the dimensions of the + * frames of the cursor is + * over + * MAX_BITMAP_CURSOR_SIZE */ + gboolean size_warn = FALSE; /* become TRUE if even one + * of the nominal size of + * the frames is not + * supported by + * gnome-appearance-properties */ + GRegex *re; /* used to get size and delay from + * framename */ + XcursorComments *commentsp; /* pointer to comments */ + XcursorImages *imagesp; /* pointer to images */ + gint32 *layers; /* Array of layer */ + gint32 *orig_layers; /* Array of layer of orig_image */ + gint nlayers; /* Number of layers */ + gchar *framename; /* framename of a layer */ + GeglRectangle save_rgn; /* region to save */ + gint layer_xoffset, layer_yoffset; + /* temporary buffer which store pixel data (guchar * bpp = guint32) */ + guint32 pixelbuf[SQR (MAX_SAVE_DIMENSION)]; + gint i, j; /* Looping vars */ + + /* This will be used in set_size_and_delay function later. To + * define this in that function is easy to read but place here to + * reduce overheads. + */ + re = g_regex_new ("[(][ 0]*(\\d+)[ ]*(px|ms)[ ]*[)]", + G_REGEX_CASELESS | G_REGEX_OPTIMIZE, + 0, + NULL); + + gimp_progress_init_printf (_("Saving '%s'"), + gimp_filename_to_utf8 (filename)); + + /* + * Open the file pointer. + */ + DM_XMC ("Open the file pointer.\n"); + fp = g_fopen (filename, "wb"); + if (fp == NULL) + { + g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), + _("Could not open '%s' for writing: %s"), + gimp_filename_to_utf8 (filename), g_strerror (errno)); + return FALSE; + } + + /* get layers */ + orig_layers = gimp_image_get_layers (orig_image_ID, &nlayers); + layers = gimp_image_get_layers (image_ID, &nlayers); + + /* create new XcursorImages. */ + imagesp = XcursorImagesCreate (nlayers); + if (!imagesp) + { + DM_XMC ("Failed to XcursorImagesCreate!\n"); + fclose (fp); + return FALSE; + } + imagesp->nimage = nlayers; + + /* XcursorImages also have `name' member but it is not used as long as I know. + We leave it NULL here. */ + + /* + * Now we start to convert each layer to a XcurosrImage one by one. + */ + for (i = 0; i < nlayers; i++) + { + GeglBuffer *buffer; + const Babl *format; + gint width; + gint height; + + buffer = gimp_drawable_get_buffer (layers[nlayers - 1 - i]); + + width = gegl_buffer_get_width (buffer); + height = gegl_buffer_get_height (buffer); + + format = babl_format ("R'G'B'A u8"); + + /* get framename of this layer */ + framename = gimp_item_get_name (layers[nlayers - 1 - i]); + /* get offset of this layer. */ + gimp_drawable_offsets (layers[nlayers - 1 - i], &layer_xoffset, &layer_yoffset); + + /* + * layer dimension check. + */ + DM_XMC ("layer size check.\n"); + /* We allow to save a cursor which dimensions are no more than + * MAX_SAVE_DIMENSION but after auto-cropping, we warn (only + * warn, don't stop) if dimension is over + * MAX_BITMAP_CURSOR_SIZE. + */ + if (width > MAX_SAVE_DIMENSION) + { + g_set_error (error, 0, 0, + _("Frame '%s' is too wide. Please reduce to no more than %dpx."), + gimp_any_to_utf8 (framename, -1, NULL), + MAX_SAVE_DIMENSION); + fclose (fp); + return FALSE; + } + + if (height > MAX_SAVE_DIMENSION) + { + g_set_error (error, 0, 0, + _("Frame '%s' is too high. Please reduce to no more than %dpx."), + gimp_any_to_utf8 (framename, -1, NULL), + MAX_SAVE_DIMENSION); + fclose (fp); + return FALSE; + } + + if (height == 0 || width == 0) + { + g_set_error (error, 0, 0, + _("Width and/or height of frame '%s' is zero!"), + gimp_any_to_utf8 (framename, -1, NULL)); + fclose (fp); + return FALSE; + } + + if (xmcvals.crop) /* with auto-cropping */ + { + /* get the region of auto-cropped area. */ + DM_XMC ("get_cropped_region\n"); + get_cropped_region (&save_rgn, buffer); + + /* don't forget save_rgn's origin is not a entire image + * but a layer which we are doing on. + */ + + if (save_rgn.width == 0 || save_rgn.height == 0) + {/* perfectly transparent frames become 1x1px transparent pixel. */ + DM_XMC ("get_cropped_region return 0.\n"); + imagesp->images[i] = XcursorImageCreate (1, 1); + if (!imagesp->images[i]) + { + DM_XMC ("Failed to XcursorImageCreate.\n"); + fclose (fp); + return FALSE; + } + imagesp->images[i]->pixels[0] = 0x0; + imagesp->images[i]->xhot = 0; + imagesp->images[i]->yhot = 0; + set_size_and_delay (framename, &(imagesp->images[i]->size), + &(imagesp->images[i]->delay), re, + &size_warn); + continue; + } + /* OK save_rgn is not 0x0 */ + /* is hotspot in save_rgn ? */ + if (! pix_in_region (xmcparas.x - layer_xoffset, + xmcparas.y - layer_yoffset, + &save_rgn)) + { /* if hotspot is not on save_rgn */ + g_set_error (error, 0, 0, + _("Cannot export the cursor because the hot spot " + "is not on frame '%s'.\n" + "Try to change the hot spot position, " + "layer geometry or export without auto-crop."), + gimp_any_to_utf8 (framename, -1, NULL)); + fclose (fp); + return FALSE; + } + } + else /* if without auto-cropping... */ + { + /* set save_rgn for the case not to auto-crop */ + save_rgn.width = width; + save_rgn.height = height; + save_rgn.x = 0; + save_rgn.y = 0; + } + + /* We warn if the dimension of the layer is over MAX_BITMAP_CURSOR_SIZE. */ + if (! dimension_warn) + { + if (save_rgn.width > MAX_BITMAP_CURSOR_SIZE || + save_rgn.height > MAX_BITMAP_CURSOR_SIZE) + { + dimension_warn = TRUE; + /* actual warning is done after the cursor is successfully saved.*/ + } + } + /* + * Create new XcursorImage. + */ + DM_XMC ("create new xcursorimage.\twidth=%i\theight=%i\n", + save_rgn.width, save_rgn.height); + imagesp->images[i] = XcursorImageCreate (save_rgn.width, save_rgn.height); + /* Cursor width & height is automatically set by function */ + /* XcursorImageCreate, so no need to set manually. */ + if (!imagesp->images[i]) + { + DM_XMC ("Failed to XcursorImageCreate.\n"); + fclose (fp); + return FALSE; + } + /* + ** set images[i]'s xhot & yhot. + */ + /* [Cropped layer's hotspot] = + [image's hotspot] - [layer's offset] - [save_rgn's offset]. */ + DM_XMC ("xhot=%i\tsave_rgn->xoffset=%i\tlayer_xoffset=%i\n", + xmcparas.x, layer_xoffset, save_rgn.x); + DM_XMC ("yhot=%i\tsave_rgn->yoffset=%i\tlayer_yoffset=%i\n", + xmcparas.y, layer_yoffset, save_rgn.y); + imagesp->images[i]->xhot = xmcparas.x - layer_xoffset - save_rgn.x; + imagesp->images[i]->yhot = xmcparas.y - layer_yoffset - save_rgn.y; + DM_XMC ("images[%i]->xhot=%i\tyhot=%i\n", i, + imagesp->images[i]->xhot, imagesp->images[i]->yhot); + + /* + * set images[i]->pixels + */ + /* get image data to pixelbuf. */ + gegl_buffer_get (buffer, + GEGL_RECTANGLE (save_rgn.x, save_rgn.y, + save_rgn.width, save_rgn.height), 1.0, + format, pixelbuf, + GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); + + /*convert pixel date to XcursorPixel. */ + g_assert (save_rgn.width * save_rgn.height < SQR (MAX_SAVE_DIMENSION)); + for (j = 0; j < save_rgn.width * save_rgn.height; j++) + { + imagesp->images[i]->pixels[j] = premultiply_alpha (pixelbuf[j]); + } + + /* + * get back size & delay from framename. + */ + set_size_and_delay (framename, &(imagesp->images[i]->size), + &(imagesp->images[i]->delay), re, &size_warn); + + /* + * All property of this XcursorImage is loaded. + */ + + /* set the layer name of original image with the saved value */ + g_free (framename); + framename = make_framename (imagesp->images[i]->size, + imagesp->images[i]->delay, + DISPLAY_DIGIT (imagesp->nimage), + error); + if (! framename) + { + fclose (fp); + return FALSE; + } + + gimp_item_set_name (orig_layers[nlayers - 1 - i], framename); + g_free (framename); + + g_object_unref (buffer); + + gimp_progress_update ((i + 1) / imagesp->nimage); + } + + gimp_progress_update (1.0); + + /* + * comment parsing + */ + commentsp = set_cursor_comments (); + +#ifdef XMC_DEBUG + DM_XMC ("imagesp->nimage=%i\tname=%s\n", imagesp->nimage, imagesp->name); + for (i = 0; i < imagesp->nimage; ++i) + { + DM_XMC ("\timages[%i]->size=%i\n\ + \twidth=%i\n\ + \theight=%i\n\ + \txhot=%i\n\ + \tyhot=%i\n\ + \tdelay=%i\n\ + \t*pixels=%p\n", + i, + imagesp->images[i]->size, + imagesp->images[i]->width, + imagesp->images[i]->height, + imagesp->images[i]->xhot, + imagesp->images[i]->yhot, + imagesp->images[i]->delay, + imagesp->images[i]->pixels); + } + + if (commentsp) + { + for (i = 0; i < commentsp->ncomment; ++i) + { + DM_XMC ("comment type=%d\tcomment=%s\n", + commentsp->comments[i]->comment_type, + commentsp->comments[i]->comment); + } + } +#endif + + /* + * save cursor to file *fp. + */ + + if (commentsp) + { + if (! XcursorFileSave (fp, commentsp, imagesp)) + { + DM_XMC ("Failed to XcursorFileSave.\t%p\t%p\t%p\n", + fp, commentsp, imagesp); + fclose (fp); + return FALSE; + } + + } + else /* if no comments exist */ + { + if (! XcursorFileSaveImages (fp, imagesp)) + { + DM_XMC ("Failed to XcursorFileSaveImages.\t%p\t%p\n", fp, imagesp); + fclose (fp); + return FALSE; + } + } + + /* actual warning about dimensions */ + if (dimension_warn) + { + g_message (_("Your cursor was successfully exported but it contains one or " + "more frames whose width or height is more than %ipx, " + "a historical max dimension value for X bitmap cursors.\n" + "It might be unsupported by some environments."), + MAX_BITMAP_CURSOR_SIZE); + } + if (size_warn) + { + g_message (_("Your cursor was successfully exported but it contains one " + "or more frames whose nominal size is not supported by " + "GNOME settings.\n" + "You can satisfy it by checking \"Replace the size of all " + "frames...\" in the export dialog, or your cursor may not " + "appear in GNOME settings.")); + } + /* + * Done with the file... + */ + g_regex_unref (re); + DM_XMC ("fp=%p\n", fp); + fclose (fp); + DM_XMC ("%i frames written.\n", imagesp->nimage); + XcursorImagesDestroy (imagesp); + DM_XMC ("Xcursor destroyed.\n"); + XcursorCommentsDestroy (commentsp); /* this is safe even if commentsp is NULL. */ + gimp_progress_end (); + + /* Save the comment back to the original image */ + for (i = 0; i < 3; i++) + { + gimp_image_detach_parasite (orig_image_ID, parasiteName[i]); + + if (xmcparas.comments[i]) + { + if (! set_comment_to_pname (orig_image_ID, + xmcparas.comments[i], parasiteName[i])) + { + DM_XMC ("Failed to write back %ith comment to orig_image.\n", i); + } + } + } + /* Save hotspot back to the original image */ + set_hotspot_to_parasite (orig_image_ID); + + return TRUE; +} + +static inline guint32 +separate_alpha (guint32 pixel) +{ + guint alpha, red, green, blue; + guint32 retval; + +#if G_BYTE_ORDER != G_LITTLE_ENDIAN + pixel = GUINT32_TO_LE (pixel); +#endif + + blue = pixel & 0xff; + green = (pixel>>8) & 0xff; + red = (pixel>>16) & 0xff; + alpha = (pixel>>24) & 0xff; + + if (alpha == 0) + return 0; + + /* resume separate alpha data. */ + red = MIN (red * 255 / alpha, 255); + blue = MIN (blue * 255 / alpha, 255); + green = MIN (green * 255 / alpha, 255); + + retval = red + (green<<8) + (blue<<16) + (alpha<<24); + +#if G_BYTE_ORDER != G_LITTLE_ENDIAN + pixel = GUINT32_FROM_LE (pixel); +#endif + + return retval; +} + +static inline guint32 +premultiply_alpha (guint32 pixel) +{ + guint alpha, red, green, blue; + guint32 retval; + +#if G_BYTE_ORDER != G_LITTLE_ENDIAN + pixel = GUINT32_TO_LE (pixel); +#endif + + red = pixel & 0xff; + green = (pixel >> 8) & 0xff; + blue = (pixel >> 16) & 0xff; + alpha = (pixel >> 24) & 0xff; + + /* premultiply alpha + (see "premultiply_data" function at line 154 of xcursorgen.c) */ + red = div_255 (red * alpha); + green = div_255 (green * alpha); + blue = div_255 (blue * alpha); + + retval = blue + (green << 8) + (red << 16) + (alpha << 24); + +#if G_BYTE_ORDER != G_LITTLE_ENDIAN + pixel = GUINT32_FROM_LE (pixel); +#endif + + return retval; +} + +/* set comments to cursor from xmcparas.comments. + * don't forget to XcursorCommentsDestroy returned pointer later. + */ +static XcursorComments * +set_cursor_comments (void) +{ + gint i; + guint gcomlen, arraylen; + GArray *xcCommentsArray; + XcursorComment *(xcCommentp[3]) = {NULL,}; + XcursorComments *xcCommentsp; + + xcCommentsArray = g_array_new (FALSE, FALSE, sizeof (XcursorComment *)); + + for (i = 0; i < 3; ++i) + { + if (xmcparas.comments[i]) + { + gcomlen = strlen (xmcparas.comments[i]); + if (gcomlen > 0) + { + xcCommentp[i] = XcursorCommentCreate (i + 1, gcomlen); + /* first argument of XcursorCommentCreate is comment_type + defined in Xcursor.h as enumerator. + i + 1 is appropriate when we dispose parasiteName before MAIN(). */ + if (!xcCommentp[i]) + { + g_warning ("Cannot create xcCommentp[%i]\n", i); + return NULL; + } + else + { + g_stpcpy (xcCommentp[i]->comment, xmcparas.comments[i]); + g_array_append_val (xcCommentsArray, xcCommentp[i]); + } + } + } + } + + arraylen = xcCommentsArray->len; + + if (arraylen == 0) + return NULL; + + xcCommentsp = XcursorCommentsCreate (arraylen); + xcCommentsp->ncomment = arraylen; + + for (i = 0; i < arraylen; ++i) + { + xcCommentsp->comments[i] = + g_array_index (xcCommentsArray, XcursorComment* ,i); + } + + return xcCommentsp; +} + +/* Load xmcparas.comments from three parasites named as "xmc-copyright", + * "xmc-license","gimp-comment". + * This alignment sequence is depends on the definition of comment_type + * in Xcursor.h . + * Don't forget to g_free each element of xmcparas.comments later. + */ +static void +load_comments (const gint32 image_ID) +{ + gint i; + + g_return_if_fail (image_ID != -1); + + for (i = 0; i < 3; ++i) + xmcparas.comments[i] = get_comment_from_pname (image_ID, parasiteName[i]); +} + +/* Set content to a parasite named as pname. if parasite already + * exists, append the new one to the old one with "\n" + */ +static gboolean +set_comment_to_pname (const gint32 image_ID, + const gchar *content, + const gchar *pname) +{ + gboolean ret = FALSE; + gchar *tmpstring, *joind; + GimpParasite *parasite; + + g_return_val_if_fail (image_ID != -1, FALSE); + g_return_val_if_fail (content, FALSE); + + parasite = gimp_image_get_parasite (image_ID, pname); + if (! parasite) + { + parasite = gimp_parasite_new (pname, GIMP_PARASITE_PERSISTENT, + strlen (content) + 1, content); + } + else + { + tmpstring = g_strndup (gimp_parasite_data (parasite), + gimp_parasite_data_size (parasite)); + gimp_parasite_free (parasite); + joind = g_strjoin ("\n", tmpstring, content, NULL); + g_free (tmpstring); + parasite = gimp_parasite_new (pname, GIMP_PARASITE_PERSISTENT, + strlen (joind) + 1, joind); + g_free (joind); + } + + if (parasite) + { + ret = gimp_image_attach_parasite (image_ID, parasite); + gimp_parasite_free (parasite); + } + + return ret; +} + +/* get back comment from parasite name, don't forget to call + * g_free(returned pointer) later + */ +static gchar * +get_comment_from_pname (const gint32 image_ID, + const gchar *pname) +{ + gchar *string = NULL; + GimpParasite *parasite; + glong length; + + g_return_val_if_fail (image_ID != -1, NULL); + + parasite = gimp_image_get_parasite (image_ID, pname); + length = gimp_parasite_data_size (parasite); + + if (parasite) + { + if (length > XCURSOR_COMMENT_MAX_LEN) + { + length = XCURSOR_COMMENT_MAX_LEN; + g_message (_("The parasite \"%s\" is too long for an X cursor " + "comment. It was cut off to fit."), + gimp_any_to_utf8 (pname, -1,NULL)); + } + + string = g_strndup (gimp_parasite_data (parasite), length); + gimp_parasite_free (parasite); + } + + return string; +} + +/* Set hotspot to "hot-spot" parasite which format is common with that + * of file-xbm. + */ +static gboolean +set_hotspot_to_parasite (gint32 image_ID) +{ + gboolean ret = FALSE; + gchar *tmpstr; + GimpParasite *parasite; + + g_return_val_if_fail (image_ID != -1, FALSE); + + tmpstr = g_strdup_printf ("%d %d", xmcparas.x, xmcparas.y); + parasite = gimp_parasite_new ("hot-spot", + GIMP_PARASITE_PERSISTENT, + strlen (tmpstr) + 1, + tmpstr); + g_free (tmpstr); + + if (parasite) + { + ret = gimp_image_attach_parasite (image_ID, parasite); + gimp_parasite_free (parasite); + } + + return ret; +} + +/* Get back xhot & yhot from "hot-spot" parasite. + * If succeed, hotspot coordinate is set to xmcparas.x, xmcparas.y and + * return TRUE. + * If "hot-spot" is not found or broken, return FALSE. + */ +static gboolean +get_hotspot_from_parasite (gint32 image_ID) +{ + GimpParasite *parasite; + + g_return_val_if_fail (image_ID != -1, FALSE); + + DM_XMC ("function: getHotsopt\n"); + + parasite = gimp_image_get_parasite (image_ID, "hot-spot"); + if (!parasite) /* cannot find a parasite named "hot-spot". */ + { + return FALSE; + } + + if (sscanf (gimp_parasite_data (parasite), + "%i %i", &xmcparas.x, &xmcparas.y) < 2) + { /*cannot load hotspot.(parasite is broken?) */ + return FALSE; + } + + /*OK, hotspot is set to *xhotp & *yhotp. */ + return TRUE; +} + +/* Set size to sizep, delay to delayp from drawable's framename. + */ +static void +set_size_and_delay (const gchar *framename, + guint32 *sizep, + guint32 *delayp, + GRegex *re, + gboolean *size_warnp) +{ + guint32 size = 0; + guint32 delay = 0; + gchar *digits = NULL; + gchar *suffix = NULL; + GMatchInfo *info = NULL; + + g_return_if_fail (framename); + g_return_if_fail (sizep); + g_return_if_fail (delayp); + g_return_if_fail (re); + + DM_XMC ("function: set_size_and_delay\tframename=%s\n", framename); + + /* re is defined at the start of save_image() as + [(] : open parenthesis + [ ]* : ignore zero or more spaces + (\\d+) : the number we want to get out + [ ]* : ignore zero or more spaces + (px|ms) : whether "px"(size) or "ms"(delay) + [ ]* : ignore zero or more spaces + [)] : close parenthesis + This is intended to match for the animation-play plug-in. */ + + g_regex_match (re, framename, 0, &info); + + while (g_match_info_matches (info)) + { + digits = g_match_info_fetch (info, 1); + suffix = g_match_info_fetch (info, 2); + + if (g_ascii_strcasecmp (suffix, "px") == 0) + { + if (!size) /* substitute it only for the first time */ + { + if (strlen (digits) > 8) /* too large number should be clamped */ + { + g_message (_("Your cursor was successfully exported but it contains one or " + "more frames whose size is over 8 digits.\n" + "We clamped it to %dpx. You should check the exported cursor."), + MAX_BITMAP_CURSOR_SIZE); + size = MAX_BITMAP_CURSOR_SIZE; + } + else + { + size = atoi (digits); + } + } + } + else /* suffix is "ms" */ + { + if (!delay) /* substitute it only for the first time */ + { + if (strlen (digits) > 8) /* too large number should be clamped */ + delay = CURSOR_MAX_DELAY; + else + delay = MIN (CURSOR_MAX_DELAY, atoi (digits)); + } + } + + g_free (digits); + g_free (suffix); + + g_match_info_next (info, NULL); + } + + g_match_info_free (info); + + /* if size is not set, or size_replace is TRUE, set default size + * (which was chosen in save dialog) */ + if (size == 0 || xmcvals.size_replace == TRUE) + { + size = xmcvals.size; + } + else if (! *size_warnp && + size != 12 && size != 16 && size != 24 && size != 32 && + size != 36 && size != 40 && size != 48 && size != 64 && + size != 96) + { /* if the size is different from these values, we warn about it after + successfully saving because gnome-appearance-properties only support + them. */ + *size_warnp = TRUE; + } + + *sizep = size; + + /* if delay is not set, or delay_replace is TRUE, set default delay + * (which was chosen in save dialog) */ + if (delay == 0 || xmcvals.delay_replace == TRUE) + { + delay = xmcvals.delay; + } + + *delayp = delay; + + DM_XMC ("set_size_and_delay return\tsize=%i\tdelay=%i\n", size, delay); +} + +/* Return framename as format: "([x]px)_[i] ([t]ms) (replace)" + * where [x] is nominal size, [t] is delay passed as argument respectively, + * and [i] is an index separately counted by [x]. + * This format is compatible with "animation-play" plug-in. + * Don't forget to g_free returned framename later. + */ +static gchar * +make_framename (guint32 size, + guint32 delay, + guint indent, + GError **errorp) +{ + static struct + { + guint32 size; + guint count; + } Counter[MAX_SIZE_NUM + 1] = {{0,}}; + + int i; /* loop index */ + + /* don't pass 0 for size. */ + g_return_val_if_fail (size > 0, NULL); + + /* "count" member of Counter's element means how many time corresponding + "size" is passed to this function. The size member of the last element + of Counter must be 0, so Counter can have MAX_SIZE_NUM elements at most. + This is not a smart way but rather simple than using dynamic method. */ + + for (i = 0; Counter[i].size != size; ++i) + { + if (Counter[i].size == 0) /* the end of Counter elements */ + { + if (i > MAX_SIZE_NUM) + { + g_set_error (errorp, 0, 0, + /* translators: the %i is *always* 8 here */ + _("Sorry, this plug-in cannot handle a cursor " + "which contains over %i different nominal sizes."), + MAX_SIZE_NUM); + return NULL; + } + else /* append new element which "size" is given value. */ + { + Counter[i].size = size; + break; + } + } + } + + Counter[i].count += 1; + + return g_strdup_printf ("(%dpx)_%0*d (%dms) (replace)", size, indent, + Counter[i].count, delay); +} + +/* Get the region which is maintained when auto-crop. + */ +static void +get_cropped_region (GeglRectangle *return_rgn, + GeglBuffer *buffer) +{ + gint width = gegl_buffer_get_width (buffer); + gint height = gegl_buffer_get_height (buffer); + guint32 *buf = g_malloc (MAX (width, height) * sizeof (guint32)); + const Babl *format = babl_format ("R'G'B'A u8"); + guint i, j; + + g_return_if_fail (GEGL_IS_BUFFER (buffer)); + + DM_XMC ("function:get_cropped_region\n"); + + DM_XMC ("getTrim:\tMAX=%i\tpr->w=%i\tpr->h=%i\n", sizeof (buf)/4, pr->w, pr->h); + + /* find left border. */ + for (i = 0; i < width; ++i) + { + DM_XMC ("i=%i width=%i\n", i, width); + + gegl_buffer_get (buffer, GEGL_RECTANGLE (i, 0, 1, height), 1.0, + format, buf, + GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); + + for (j = 0; j < height; ++j) + { + if (pix_is_opaque (buf[j])) /* if a opaque pixel exist. */ + { + return_rgn->x = i; + goto find_right; + } + } + } + + /* pr has no opaque pixel. */ + return_rgn->width = 0; + return; + + /* find right border. */ + find_right: + for (i = 0; i < width ; ++i) + { + DM_XMC ("width-1-i=%i height=%i\n", width - 1 - i, height); + + gegl_buffer_get (buffer, GEGL_RECTANGLE (width - 1 - i, 0, 1, height), 1.0, + format, buf, + GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); + + for (j = 0; j < height; ++j) + { + if (pix_is_opaque (buf[j])) /* if a opaque pixel exist. */ + { + return_rgn->width = width - i - return_rgn->x; + goto find_top; + } + } + } + g_return_if_reached (); + + /* find top border. */ + find_top: + for (j = 0; j < height; ++j) + { + DM_XMC ("j=%i width=%i\n", j, width); + + gegl_buffer_get (buffer, GEGL_RECTANGLE (0, j, width, 1), 1.0, + format, buf, + GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); + + for (i = 0; i < width; ++i) + { + if (pix_is_opaque (buf[i])) /* if a opaque pixel exist. */ + { + return_rgn->y = j; + goto find_bottom; + } + } + } + + g_return_if_reached (); + + /* find bottom border. */ + find_bottom: + for (j = 0; j < height; ++j) + { + DM_XMC ("height-1-j=%i width=%i\n", height - 1 - j, width); + + gegl_buffer_get (buffer, GEGL_RECTANGLE (0, height - 1 - j, width, 1), 1.0, + format, buf, + GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); + + for (i = 0; i < width; ++i) + { + if (pix_is_opaque (buf[i])) /* if a opaque pixel exist. */ + { + return_rgn->height = height - j - return_rgn->y; + goto end_trim; + } + } + } + + g_return_if_reached (); + + end_trim: + DM_XMC ("width=%i\theight=%i\txoffset=%i\tyoffset=%i\n", + return_rgn->width, return_rgn->height, + return_rgn->x, return_rgn->y); + + g_free (buf); +} + +/* Return true if alpha of pix is not 0. + */ +static inline gboolean +pix_is_opaque (guint32 pix) +{ +#if G_BYTE_ORDER != G_LITTLE_ENDIAN + pix = GUINT32_TO_LE (pix); +#endif + + return ((pix >> 24) != 0); +} + +/* Get the intersection of the all layers of the image specified by image_ID. + * if the intersection is empty return NULL. + * don't forget to g_free returned pointer later. + */ +static GeglRectangle * +get_intersection_of_frames (gint32 image_ID) +{ + GeglRectangle *iregion; + gint i; + gint32 x1 = G_MININT32, x2 = G_MAXINT32; + gint32 y1 = G_MININT32, y2 = G_MAXINT32; + gint32 x_off, y_off; + gint nlayers; + gint *layers; + + g_return_val_if_fail (image_ID != -1, FALSE); + + layers = gimp_image_get_layers (image_ID, &nlayers); + + for (i = 0; i < nlayers; ++i) + { + if (! gimp_drawable_offsets (layers[i], &x_off, &y_off)) + return NULL; + + x1 = MAX (x1, x_off); + y1 = MAX (y1, y_off); + x2 = MIN (x2, x_off + gimp_drawable_width (layers[i]) - 1); + y2 = MIN (y2, y_off + gimp_drawable_height (layers[i]) - 1); + } + + if (x1 > x2 || y1 > y2) + return NULL; + + /* OK intersection exists. */ + iregion = g_new (GeglRectangle, 1); + iregion->x = x1; + iregion->y = y1; + iregion->width = x2 - x1 + 1; + iregion->height = y2 - y1 + 1; + + return iregion; +} + +/* If (x,y) is in xmcrp, return TRUE. + */ +static gboolean +pix_in_region (gint32 x, + gint32 y, + GeglRectangle *xmcrp) +{ + g_return_val_if_fail (xmcrp, FALSE); + + if (x < xmcrp->x || y < xmcrp->y || + x >= xmcrp->x + xmcrp->width || y >= xmcrp->y + xmcrp->height) + return FALSE; + else + return TRUE; +} + +/** + * Find out xhot, yhot, width and height of the Xcursor specified by xcIs. + * Use NULL for the value you don't want to return. +**/ +static void +find_hotspots_and_dimensions (XcursorImages *xcIs, + gint32 *xhotp, + gint32 *yhotp, + gint32 *widthp, + gint32 *heightp) +{ + gint32 dw, dh; /* the distance between hotspot and right(bottom) border */ + gint32 max_xhot; + gint32 max_yhot; /* the maximum value of xhot(yhot) */ + gint i; + + g_return_if_fail (xcIs); + + max_xhot = max_yhot = dw = dh = 0; + + for (i = 0; i < xcIs->nimage; ++i) + { + /* xhot of entire image is the maximum value of xhot of all frames */ + max_xhot = MAX (xcIs->images[i]->xhot, max_xhot); + /* same for yhot */ + max_yhot = MAX (xcIs->images[i]->yhot, max_yhot); + /* the maximum distance between right border and xhot */ + dw = MAX (dw, xcIs->images[i]->width - xcIs->images[i]->xhot); + /* the maximum distance between bottom border and yhot */ + dh = MAX (dh, xcIs->images[i]->height - xcIs->images[i]->yhot); + } + + if (xhotp) + *xhotp = max_xhot; + if (yhotp) + *yhotp = max_yhot; + if (widthp) + *widthp = dw + max_xhot; + if (heightp) + *heightp = dh + max_yhot; +} |