/* * X11 Mouse Cursor (XMC) plug-in for GIMP * * Copyright 2008-2009 Takeshi Matsuyama * * 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 . */ /* * 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 #include #include #include #include #include #include #include #include #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 ", "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 ", "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 ", "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; }