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

2319 lines
76 KiB
C
Raw Normal View History

2023-09-26 00:35:21 +02:00
/*
* X11 Mouse Cursor (XMC) plug-in for PIKA
*
* Copyright 2008-2009 Takeshi Matsuyama <tksmashiw@gmail.com>
*
* Special thanks: Alexia Death, Sven Neumann, Martin Nordholts
* and all community members.
*/
/*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* Todo: if drawable->bpp != 4 in save_image for PIKA-2.8?
* Todo: support for "pika-metadata" parasite.
* "xmc-copyright" and "xmc-license" may be deprecated in future?
*/
/*
* This plug-in use these four parasites.
* "hot-spot" common with file-xbm plug-in
* "xmc-copyright" original, store contents of type1 comment chunk of Xcursor
* "xmc-license" original, store contents of type2 comment chunk of Xcursor
* "pika-comment" common, store contents of type3 comment chunk of Xcursor
*/
/* *** Caution: Size vs Dimension ***
*
* In this file, "size" and "dimension" are used in definitely
* different contexts. "Size" means nominal size of Xcursor which is
* used to determine which frame depends on which animation sequence
* and which sequence is really used. (for more detail, please read
* Xcursor(3).) On the other hand, "Dimension" simply means width
* and/or height.
*/
#include "config.h"
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <glib/gstdio.h>
#include <glib/gprintf.h>
#include <libpika/pika.h>
#include <libpika/pikaui.h>
#include <X11/Xlib.h>
#include <X11/Xcursor/Xcursor.h>
#include "libpika/stdplugins-intl.h"
/* For debug */
/* #define XMC_DEBUG */
#ifdef XMC_DEBUG
# define DM_XMC(...) g_fprintf(stderr, __VA_ARGS__)
#else
# define DM_XMC(...)
#endif
/*
* Constants...
*/
#define LOAD_PROC "file-xmc-load"
#define LOAD_THUMB_PROC "file-xmc-load-thumb"
#define SAVE_PROC "file-xmc-save"
#define PLUG_IN_BINARY "file-xmc"
#define PLUG_IN_ROLE "pika-file-xmc"
/* We use "xmc" as the file extension of X cursor for convenience */
#define XCURSOR_EXTENSION "xmc"
#define XCURSOR_MIME_TYPE "image/x-xcursor"
/* The maximum dimension of Xcursor which is fully supported in any
* environments. This is defined on line 59 of xcursorint.h in
* libXcursor source code. Make sure this is about dimensions (width
* and height) not about nominal size despite of it's name.
*
* As of 2018, this macro still exists in libXCursor codebase, but I am
* unsure how far this restriction is enforced since this is a very low
* max dimension for today's displays. Therefore our code will not
* enforce this value anymore, but only warn about possible
* incompatibilities when using higher values.
*/
#define MAX_BITMAP_CURSOR_SIZE 64
/* The maximum dimension of each frame of X cursor we want to save
* should be MAX_BITMAP_CURSOR_SIZE but about loading, xhot(& yhot) of
* each frame varies from 0 to MAX_BITMAP_CURSOR_SIZE-1, so we need to
* set the maximum dimension of image no less than
* MAX_BITMAP_CURSOR_SIZE * 2( -1 to be precise) to remain hotspots on
* the same coordinates.
*
* We use four times value (256 for saving, 512 for loading) as a
* limitation because some cursors generated by CursorXP/FX to X11
* Mouse Theme Converter is very large.
*
* The biggest cursor I found is "watch" of OuterLimits which size is
* 213x208. If you found bigger one, please tell me ;-)
*/
#define MAX_LOAD_DIMENSION 512
#define MAX_SAVE_DIMENSION 256
/* The maximum number of different nominal sizes in one cursor this
* plug-in can treat. This is based on the number of cursor size which
* gnome-appearance-properties supports.(12,16,24,32,36,40,48,64)
* ref. capplets/common/gnome-theme-info.c in source of
* gnome-control-center
*/
#define MAX_SIZE_NUM 8
/* cursor delay is guint32 defined in Xcursor.h */
#define CURSOR_MAX_DELAY 100000000
#define CURSOR_DEFAULT_DELAY 50
#define CURSOR_MINIMUM_DELAY 5
#define div_255(x) (((x) + 0x80 + (((x) + 0x80) >> 8)) >> 8)
#define READ32(f, e) read32 ((f), (e)); if (*(e)) return NULL;
#define DISPLAY_DIGIT(x) ((x) > 100) ? 3 : ((x) > 10) ? 2 : 1
typedef struct _Xmc Xmc;
typedef struct _XmcClass XmcClass;
struct _Xmc
{
PikaPlugIn parent_instance;
};
struct _XmcClass
{
PikaPlugInClass parent_class;
};
#define XMC_TYPE (xmc_get_type ())
#define XMC (obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), XMC_TYPE, Xmc))
GType xmc_get_type (void) G_GNUC_CONST;
static GList * xmc_query_procedures (PikaPlugIn *plug_in);
static PikaProcedure * xmc_create_procedure (PikaPlugIn *plug_in,
const gchar *name);
static PikaValueArray * xmc_load (PikaProcedure *procedure,
PikaRunMode run_mode,
GFile *file,
const PikaValueArray *args,
gpointer run_data);
static PikaValueArray * xmc_load_thumb (PikaProcedure *procedure,
GFile *file,
gint size,
const PikaValueArray *args,
gpointer run_data);
static PikaValueArray * xmc_save (PikaProcedure *procedure,
PikaRunMode run_mode,
PikaImage *image,
gint n_drawables,
PikaDrawable **drawables,
GFile *file,
const PikaValueArray *args,
gpointer run_data);
static PikaImage * load_image (GFile *file,
GError **error);
static PikaImage * load_thumbnail (GFile *file,
gint32 thumb_size,
gint32 *width,
gint32 *height,
gint32 *num_layers,
GError **error);
static guint32 read32 (FILE *f,
GError **error);
static gboolean save_image (GFile *file,
PikaImage *image,
gint n_drawables,
PikaDrawable **drawables,
PikaImage *orig_image,
GObject *config,
GError **error);
static gboolean save_dialog (PikaProcedure *procedure,
GObject *config,
PikaImage *image,
GeglRectangle *hotspot_range);
static void load_default_hotspot (PikaImage *image,
GeglRectangle *hotspot_range,
GObject *config);
static inline guint32 separate_alpha (guint32 pixel);
static inline guint32 premultiply_alpha (guint32 pixel);
static XcursorComments *set_cursor_comments (GObject *config);
static gboolean set_hotspot_to_parasite (PikaImage *image,
gint hot_spot_x,
gint hot_spot_y);
static gboolean get_hotspot_from_parasite (PikaImage *image,
gint *hot_spot_x,
gint *hot_spot_y);
static void set_size_and_delay (GObject *config,
const gchar *framename,
guint32 *sizep,
guint32 *delayp,
GRegex *re,
gboolean *size_warnp);
static gchar * make_framename (guint32 size,
guint32 delay,
guint indent,
GError **errorp);
static void get_cropped_region (GeglRectangle *retrun_rgn,
GeglBuffer *buffer);
static inline gboolean pix_is_opaque (guint32 pix);
static GeglRectangle * get_intersection_of_frames (PikaImage *image);
static gboolean pix_in_region (gint32 x,
gint32 y,
GeglRectangle *xmcrp);
static void find_hotspots_and_dimensions
(XcursorImages *xcIs,
gint32 *xhot,
gint32 *yhot,
gint32 *width,
gint32 *height);
G_DEFINE_TYPE (Xmc, xmc, PIKA_TYPE_PLUG_IN)
PIKA_MAIN (XMC_TYPE)
DEFINE_STD_SET_I18N
static void
xmc_class_init (XmcClass *klass)
{
PikaPlugInClass *plug_in_class = PIKA_PLUG_IN_CLASS (klass);
plug_in_class->query_procedures = xmc_query_procedures;
plug_in_class->create_procedure = xmc_create_procedure;
plug_in_class->set_i18n = STD_SET_I18N;
}
static void
xmc_init (Xmc *xmc)
{
}
static GList *
xmc_query_procedures (PikaPlugIn *plug_in)
{
GList *list = NULL;
list = g_list_append (list, g_strdup (LOAD_THUMB_PROC));
list = g_list_append (list, g_strdup (LOAD_PROC));
list = g_list_append (list, g_strdup (SAVE_PROC));
return list;
}
static PikaProcedure *
xmc_create_procedure (PikaPlugIn *plug_in,
const gchar *name)
{
PikaProcedure *procedure = NULL;
if (! strcmp (name, LOAD_PROC))
{
procedure = pika_load_procedure_new (plug_in, name,
PIKA_PDB_PROC_TYPE_PLUGIN,
xmc_load, NULL, NULL);
pika_procedure_set_menu_label (procedure, _("X11 Mouse Cursor"));
pika_procedure_set_documentation (procedure,
"Loads files of X11 Mouse Cursor "
"file format",
"This plug-in loads X11 Mouse Cursor "
"(XMC) files.",
name);
pika_procedure_set_attribution (procedure,
"Takeshi Matsuyama <tksmashiw@gmail.com>",
"Takeshi Matsuyama",
"26 May 2009");
pika_file_procedure_set_mime_types (PIKA_FILE_PROCEDURE (procedure),
XCURSOR_MIME_TYPE);
pika_file_procedure_set_extensions (PIKA_FILE_PROCEDURE (procedure),
XCURSOR_EXTENSION);
pika_file_procedure_set_magics (PIKA_FILE_PROCEDURE (procedure),
"0,string,Xcur");
pika_load_procedure_set_thumbnail_loader (PIKA_LOAD_PROCEDURE (procedure),
LOAD_THUMB_PROC);
}
else if (! strcmp (name, LOAD_THUMB_PROC))
{
procedure = pika_thumbnail_procedure_new (plug_in, name,
PIKA_PDB_PROC_TYPE_PLUGIN,
xmc_load_thumb, NULL, NULL);
pika_procedure_set_documentation (procedure,
"Loads only first frame of X11 Mouse "
"Cursor's animation sequence which "
"nominal size is the closest of "
"thumb-size to be used as a thumbnail",
"",
name);
pika_procedure_set_attribution (procedure,
"Takeshi Matsuyama <tksmashiw@gmail.com>",
"Takeshi Matsuyama",
"26 May 2009");
}
else if (! strcmp (name, SAVE_PROC))
{
procedure = pika_save_procedure_new (plug_in, name,
PIKA_PDB_PROC_TYPE_PLUGIN,
xmc_save, NULL, NULL);
pika_procedure_set_image_types (procedure, "RGBA");
pika_procedure_set_menu_label (procedure, _("X11 Mouse Cursor"));
pika_procedure_set_documentation (procedure,
"Exports files of X11 cursor file",
"This plug-in exports X11 Mouse Cursor "
"(XMC) files",
name);
pika_procedure_set_attribution (procedure,
"Takeshi Matsuyama <tksmashiw@gmail.com>",
"Takeshi Matsuyama",
"26 May 2009");
pika_file_procedure_set_mime_types (PIKA_FILE_PROCEDURE (procedure),
XCURSOR_MIME_TYPE);
pika_file_procedure_set_extensions (PIKA_FILE_PROCEDURE (procedure),
XCURSOR_EXTENSION);
PIKA_PROC_ARG_INT (procedure, "hot-spot-x",
"Hot spot X",
"X-coordinate of hot spot "
"(use -1, -1 to keep original hot spot",
-1, PIKA_MAX_IMAGE_SIZE, -1,
G_PARAM_READWRITE);
PIKA_PROC_ARG_INT (procedure, "hot-spot-y",
"Hot spot Y",
"Y-coordinate of hot spot "
"(use -1, -1 to keep original hot spot",
-1, PIKA_MAX_IMAGE_SIZE, -1,
G_PARAM_READWRITE);
PIKA_PROC_ARG_BOOLEAN (procedure, "crop",
"Crop",
"Auto-crop or not",
FALSE,
G_PARAM_READWRITE);
PIKA_PROC_ARG_INT (procedure, "size",
"Size",
"Default nominal size",
1, G_MAXINT, 32,
G_PARAM_READWRITE);
PIKA_PROC_ARG_BOOLEAN (procedure, "size-replace",
"Size replace",
"Replace existent size or not",
FALSE,
G_PARAM_READWRITE);
PIKA_PROC_ARG_INT (procedure, "delay",
"Delay",
"Default delay",
CURSOR_MINIMUM_DELAY, G_MAXINT, CURSOR_DEFAULT_DELAY,
G_PARAM_READWRITE);
PIKA_PROC_ARG_BOOLEAN (procedure, "delay-replace",
"Delay replace",
"Replace existent delay or not",
FALSE,
G_PARAM_READWRITE);
PIKA_PROC_ARG_STRING (procedure, "xmc-copyright",
"Copyright",
"Copyright information",
NULL,
G_PARAM_READWRITE);
pika_procedure_set_argument_sync (procedure, "xmc-copyright",
PIKA_ARGUMENT_SYNC_PARASITE);
PIKA_PROC_ARG_STRING (procedure, "xmc-license",
"License",
"License information",
NULL,
G_PARAM_READWRITE);
pika_procedure_set_argument_sync (procedure, "xmc-license",
PIKA_ARGUMENT_SYNC_PARASITE);
PIKA_PROC_ARG_STRING (procedure, "pika-comment",
"Other",
"Other comment (taken from 'pika-comment' parasite)",
pika_get_default_comment (),
G_PARAM_READWRITE);
pika_procedure_set_argument_sync (procedure, "pika-comment",
PIKA_ARGUMENT_SYNC_PARASITE);
}
return procedure;
}
static PikaValueArray *
xmc_load (PikaProcedure *procedure,
PikaRunMode run_mode,
GFile *file,
const PikaValueArray *args,
gpointer run_data)
{
PikaValueArray *return_vals;
PikaImage *image;
GError *error = NULL;
gegl_init (NULL, NULL);
image = load_image (file, &error);
if (! image)
return pika_procedure_new_return_values (procedure,
PIKA_PDB_EXECUTION_ERROR,
error);
return_vals = pika_procedure_new_return_values (procedure,
PIKA_PDB_SUCCESS,
NULL);
PIKA_VALUES_SET_IMAGE (return_vals, 1, image);
return return_vals;
}
static PikaValueArray *
xmc_load_thumb (PikaProcedure *procedure,
GFile *file,
gint size,
const PikaValueArray *args,
gpointer run_data)
{
PikaValueArray *return_vals;
gint width;
gint height;
gint num_layers;
PikaImage *image;
GError *error = NULL;
gegl_init (NULL, NULL);
image = load_thumbnail (file, size,
&width,
&height,
&num_layers,
&error);
if (! image)
return pika_procedure_new_return_values (procedure,
PIKA_PDB_EXECUTION_ERROR,
error);
return_vals = pika_procedure_new_return_values (procedure,
PIKA_PDB_SUCCESS,
NULL);
PIKA_VALUES_SET_IMAGE (return_vals, 1, image);
PIKA_VALUES_SET_INT (return_vals, 2, width);
PIKA_VALUES_SET_INT (return_vals, 3, height);
PIKA_VALUES_SET_ENUM (return_vals, 4, PIKA_RGBA_IMAGE);
PIKA_VALUES_SET_INT (return_vals, 5, num_layers);
return return_vals;
}
static PikaValueArray *
xmc_save (PikaProcedure *procedure,
PikaRunMode run_mode,
PikaImage *image,
gint n_drawables,
PikaDrawable **drawables,
GFile *file,
const PikaValueArray *args,
gpointer run_data)
{
PikaProcedureConfig *config;
PikaPDBStatusType status = PIKA_PDB_SUCCESS;
PikaExportReturn export = PIKA_EXPORT_CANCEL;
PikaImage *orig_image;
GeglRectangle *hotspot_range;
gint hot_spot_x;
gint hot_spot_y;
GError *error = NULL;
gegl_init (NULL, NULL);
orig_image = image;
hotspot_range = get_intersection_of_frames (image);
if (! hotspot_range)
{
g_set_error (&error, 0, 0,
_("Cannot set the hot spot!\n"
"You must arrange layers so that all of them have "
"an intersection."));
return pika_procedure_new_return_values (procedure,
PIKA_PDB_EXECUTION_ERROR,
error);
}
config = pika_procedure_create_config (procedure);
pika_procedure_config_begin_export (config, image, run_mode, args, NULL);
switch (run_mode)
{
case PIKA_RUN_INTERACTIVE:
case PIKA_RUN_WITH_LAST_VALS:
pika_ui_init (PLUG_IN_BINARY);
export = pika_export_image (&image, &n_drawables, &drawables, "XMC",
PIKA_EXPORT_CAN_HANDLE_RGB |
PIKA_EXPORT_CAN_HANDLE_ALPHA |
PIKA_EXPORT_CAN_HANDLE_LAYERS |
PIKA_EXPORT_NEEDS_ALPHA);
if (export == PIKA_EXPORT_CANCEL)
return pika_procedure_new_return_values (procedure,
PIKA_PDB_CANCEL,
NULL);
break;
default:
break;
}
switch (run_mode)
{
case PIKA_RUN_INTERACTIVE:
load_default_hotspot (image, hotspot_range, G_OBJECT (config));
if (! save_dialog (procedure, G_OBJECT (config), image, hotspot_range))
return pika_procedure_new_return_values (procedure,
PIKA_PDB_CANCEL,
NULL);
break;
case PIKA_RUN_NONINTERACTIVE:
g_object_get (config,
"hot-spot-x", &hot_spot_x,
"hot-spot-y", &hot_spot_y,
NULL);
if (! pix_in_region (hot_spot_x, hot_spot_y, hotspot_range))
{
/* you can purposely choose non acceptable values for
* hotspot to use cursor's original values.
*/
load_default_hotspot (image, hotspot_range, G_OBJECT (config));
}
break;
case PIKA_RUN_WITH_LAST_VALS:
load_default_hotspot (image, hotspot_range, G_OBJECT (config));
break;
default:
break;
}
if (! save_image (file, image, n_drawables, drawables,
orig_image, G_OBJECT (config), &error))
{
status = PIKA_PDB_EXECUTION_ERROR;
}
if (export == PIKA_EXPORT_EXPORT)
{
pika_image_delete (image);
g_free (drawables);
}
g_free (hotspot_range);
pika_procedure_config_end_export (config, image, file, status);
g_object_unref (config);
return pika_procedure_new_return_values (procedure, status, error);
}
/*
* 'load_image()' - Load a X cursor image into a new image window.
*/
static PikaImage *
load_image (GFile *file,
GError **error)
{
FILE *fp;
PikaImage *image;
PikaLayer *layer;
GeglBuffer *buffer;
XcursorComments *commentsp; /* pointer to comments */
XcursorImages *imagesp; /* pointer to images*/
guint32 delay; /* use guint32 instead CARD32(in X11/Xmd.h) */
gchar *framename; /* name of layer */
guint32 *tmppixel; /* pixel data (guchar * bpp = guint32) */
gint img_width;
gint img_height;
gint hot_spot_x;
gint hot_spot_y;
gint i, j;
pika_progress_init_printf (_("Opening '%s'"),
pika_file_get_utf8_name (file));
/* Open the file and check it is a valid X cursor */
fp = g_fopen (g_file_peek_path (file), "rb");
if (fp == NULL)
{
g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
_("Could not open '%s' for reading: %s"),
pika_file_get_utf8_name (file), g_strerror (errno));
return NULL;
}
if (! XcursorFileLoad (fp, &commentsp, &imagesp))
{
g_set_error (error, 0, 0, _("'%s' is not a valid X cursor."),
pika_file_get_utf8_name (file));
fclose (fp);
return NULL;
}
/* check dimension is valid. */
for (i = 0; i < imagesp->nimage; i++)
{
if (imagesp->images[i]->width > MAX_LOAD_DIMENSION)
{
g_set_error (error, 0, 0,
_("Frame %d of '%s' is too wide for an X cursor."),
i + 1, pika_file_get_utf8_name (file));
fclose (fp);
return NULL;
}
if (imagesp->images[i]->height > MAX_LOAD_DIMENSION)
{
g_set_error (error, 0, 0,
_("Frame %d of '%s' is too high for an X cursor."),
i + 1, pika_file_get_utf8_name (file));
fclose (fp);
return NULL;
}
}
find_hotspots_and_dimensions (imagesp,
&hot_spot_x, &hot_spot_y,
&img_width, &img_height);
DM_XMC ("xhot=%i,\tyhot=%i,\timg_width=%i,\timg_height=%i\n",
hot_spot_x, hot_spot_y, img_width, img_height);
image = pika_image_new (img_width, img_height, PIKA_RGB);
if (! set_hotspot_to_parasite (image, hot_spot_x, hot_spot_y))
{
fclose (fp);
return NULL;
}
/* Temporary buffer */
tmppixel = g_new (guint32, img_width * img_height);
/* load each frame to each layer one by one */
for (i = 0; i < imagesp->nimage; i++)
{
gint width = imagesp->images[i]->width;
gint height = imagesp->images[i]->height;
delay = imagesp->images[i]->delay;
if (delay < CURSOR_MINIMUM_DELAY)
{
delay = CURSOR_DEFAULT_DELAY;
}
DM_XMC ("images[%i]->delay=%i\twidth=%d\theight=%d\n",
i ,delay, imagesp->images[i]->width, imagesp->images[i]->height);
framename = make_framename (imagesp->images[i]->size, delay,
DISPLAY_DIGIT (imagesp->nimage), error);
if (! framename)
{
fclose (fp);
return NULL;
}
layer = pika_layer_new (image, framename, width, height,
PIKA_RGBA_IMAGE,
100,
pika_image_get_default_new_layer_mode (image));
pika_image_insert_layer (image, layer, NULL, 0);
/* Adjust layer position to let hotspot sit on the same point. */
pika_item_transform_translate (PIKA_ITEM (layer),
hot_spot_x - imagesp->images[i]->xhot,
hot_spot_y - imagesp->images[i]->yhot);
g_free (framename);
/* Get the buffer for our load... */
buffer = pika_drawable_get_buffer (PIKA_DRAWABLE (layer));
/* set color to each pixel */
for (j = 0; j < width * height; j++)
{
tmppixel[j] = separate_alpha (imagesp->images[i]->pixels[j]);
}
/* set pixel */
gegl_buffer_set (buffer, GEGL_RECTANGLE (0, 0, width, height), 0,
NULL, tmppixel, GEGL_AUTO_ROWSTRIDE);
pika_progress_update ((i + 1) / imagesp->nimage);
g_object_unref (buffer);
}
g_free (tmppixel);
pika_progress_update (1.0);
/* Comment parsing */
if (commentsp)
{
PikaParasite *parasite;
gchar *comments[3] = { NULL, };
for (i = 0; i < commentsp->ncomment; ++i)
{
gint type = commentsp->comments[i]->comment_type -1;
DM_XMC ("comment type=%d\tcomment=%s\n",
commentsp->comments[i]->comment_type,
commentsp->comments[i]->comment);
if (comments[type])
{
gchar *tmp = g_strjoin ("\n",
comments[type],
commentsp->comments[i]->comment,
NULL);
g_free (comments[type]);
comments[type] = tmp;
}
else
{
comments[type] = g_strdup (commentsp->comments[i]->comment);
}
}
XcursorCommentsDestroy (commentsp);
if (comments[0])
{
parasite = pika_parasite_new ("xmc-copyright",
PIKA_PARASITE_PERSISTENT,
strlen (comments[0]) + 1,
(gpointer) comments[0]);
pika_image_attach_parasite (image, parasite);
pika_parasite_free (parasite);
g_free (comments[0]);
}
if (comments[1])
{
parasite = pika_parasite_new ("xmc-license",
PIKA_PARASITE_PERSISTENT,
strlen (comments[1]) + 1,
(gpointer) comments[1]);
pika_image_attach_parasite (image, parasite);
pika_parasite_free (parasite);
g_free (comments[1]);
}
if (comments[2])
{
parasite = pika_parasite_new ("pika-comment",
PIKA_PARASITE_PERSISTENT,
strlen (comments[2]) + 1,
(gpointer) comments[2]);
pika_image_attach_parasite (image, parasite);
pika_parasite_free (parasite);
g_free (comments[2]);
}
}
DM_XMC ("Comment parsing done.\n");
XcursorImagesDestroy (imagesp);
fclose (fp);
pika_progress_end ();
return image;
}
/*
* load_thumbnail
*/
static PikaImage *
load_thumbnail (GFile *file,
gint32 thumb_size,
gint32 *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;
PikaImage *image = NULL;
PikaLayer *layer;
GeglBuffer *buffer;
guint32 *tmppixel; /* pixel data (guchar * bpp = guint32) */
guint32 ntoc = 0; /* the number of table of contents */
gint sel_num = -1; /* the index of selected image chunk */
gint width;
gint height;
gint i;
g_return_val_if_fail (thumb_width, NULL);
g_return_val_if_fail (thumb_height, NULL);
g_return_val_if_fail (thumb_num_layers, NULL);
*thumb_width = 0;
*thumb_height = 0;
*thumb_num_layers = 0;
fp = g_fopen (g_file_peek_path (file), "rb");
if (! fp)
{
g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
_("Could not open '%s' for reading: %s"),
pika_file_get_utf8_name (file), g_strerror (errno));
return NULL;
}
/* From this line, we make a XcursorImages struct so that we can find out the
* width and height of entire image.
* We can use XcursorFileLoadImages (fp, thumb_size) from libXcursor instead
* of this ugly code but XcursorFileLoadImages loads all pixel data of the
* image chunks on memory thus we should not use it.
*/
/* find which image chunk is preferred to load. */
/* skip magic, headersize, version */
fseek (fp, 12, SEEK_SET);
/* read the number of chunks */
ntoc = READ32 (fp, error)
if (ntoc > (G_MAXUINT32 / sizeof (guint32)))
{
g_set_error (error, 0, 0,
"'%s' seems to have an incorrect toc size.",
pika_file_get_utf8_name (file));
fclose (fp);
return NULL;
}
positions = g_malloc (ntoc * sizeof (guint32));
/* enter list of toc(table of contents) */
for (; ntoc > 0; --ntoc)
{
/* read entry type */
type = READ32 (fp, error)
if (type != XCURSOR_IMAGE_TYPE)
{
/* not a image */
/* skip rest of this content */
fseek (fp, 8, SEEK_CUR);
}
else
{
/* this content is image */
size = READ32 (fp, error)
positions[*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\"."),
pika_file_get_utf8_name (file));
fclose (fp);
return NULL;
}
/* 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."),
pika_file_get_utf8_name (file));
fclose (fp);
return NULL;
}
if (*thumb_height > MAX_LOAD_DIMENSION)
{
g_set_error (error, 0, 0,
_("'%s' is too high for an X cursor."),
pika_file_get_utf8_name (file));
fclose (fp);
return NULL;
}
/* create new image! */
width = xcIs->images[sel_num]->width;
height = xcIs->images[sel_num]->height;
image = pika_image_new (width, height, PIKA_RGB);
layer = pika_layer_new (image, NULL, width, height,
PIKA_RGBA_IMAGE,
100,
pika_image_get_default_new_layer_mode (image));
pika_image_insert_layer (image, layer, NULL, 0);
/*
* Get the drawable and set the pixel region for our load...
*/
buffer = pika_drawable_get_buffer (PIKA_DRAWABLE (layer));
/* Temporary buffer */
tmppixel = g_new (guint32, width * height);
/* copy the chunk data to tmppixel */
fseek (fp, positions[sel_num], SEEK_SET);
fseek (fp, 36, SEEK_CUR); /* skip chunk header(16bytes), xhot, yhot, width, height, delay */
for (i = 0; i < width * height; i++)
{
tmppixel[i] = READ32 (fp, error)
/* get back separate alpha */
tmppixel[i] = separate_alpha (tmppixel[i]);
}
/* set pixel */
gegl_buffer_set (buffer, GEGL_RECTANGLE (0, 0, width, height), 0,
NULL, tmppixel, GEGL_AUTO_ROWSTRIDE);
/* free tmppixel */
g_free(tmppixel);
g_free (positions);
fclose (fp);
g_object_unref (buffer);
return image;
}
/* read guint32 value from f despite of host's byte order. */
static guint32
read32 (FILE *f,
GError **error)
{
guchar p[4];
guint32 ret;
if (fread (p, 1, 4, f) != 4)
{
g_set_error (error, 0, 0, _("A read error occurred."));
return 0;
}
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
ret = p[0] + (p[1]<<8) + (p[2]<<16) + (p[3]<<24);
#elif G_BYTE_ORDER == G_BIG_ENDIAN
ret = p[3] + (p[2]<<8) + (p[1]<<16) + (p[0]<<24);
#elif G_BYTE_ORDER == G_PDP_ENDIAN
ret = p[2] + (p[3]<<8) + (p[0]<<16) + (p[1]<<24);
#else
g_return_val_if_rearched ();
#endif
return ret;
}
/* 'save_dialog ()'
*/
static gboolean
save_dialog (PikaProcedure *procedure,
GObject *config,
PikaImage *image,
GeglRectangle *hotspot_range)
{
GtkWidget *dialog;
GtkWidget *grid;
GtkWidget *box;
GtkWidget *button;
GtkListStore *store;
GtkWidget *combo;
GtkWidget *label;
GtkWidget *entry;
GtkTextBuffer *textbuffer;
GtkWidget *view;
gint row;
gboolean run;
dialog = pika_procedure_dialog_new (procedure,
PIKA_PROCEDURE_CONFIG (config),
_("Export Image as X11 Mouse Cursor"));
grid = gtk_grid_new ();
gtk_grid_set_row_spacing (GTK_GRID (grid), 6);
gtk_grid_set_column_spacing (GTK_GRID (grid), 6);
gtk_container_set_border_width (GTK_CONTAINER (grid), 12);
gtk_box_pack_start (GTK_BOX (pika_export_dialog_get_content_area (dialog)),
grid, TRUE, TRUE, 0);
gtk_widget_show (grid);
row = 0;
/* Hotspot */
button = pika_prop_spin_button_new (config, "hot-spot-x",
1, 10, 0);
gtk_spin_button_set_range (GTK_SPIN_BUTTON (button),
hotspot_range->x,
hotspot_range->x + hotspot_range->width - 1);
pika_grid_attach_aligned (GTK_GRID (grid), 0, row++,
_("Hot spot _X:"), 0.0, 0.5,
button, 1);
pika_help_set_help_data (button,
_("Enter the X coordinate of the hot spot. "
"The origin is top left corner."),
NULL);
button = pika_prop_spin_button_new (config, "hot-spot-y",
1, 10, 0);
gtk_spin_button_set_range (GTK_SPIN_BUTTON (button),
hotspot_range->y,
hotspot_range->y + hotspot_range->height - 1);
pika_grid_attach_aligned (GTK_GRID (grid), 0, row++,
_("Hot spot _Y:"), 0.0, 0.5,
button, 1);
pika_help_set_help_data (button,
_("Enter the Y coordinate of the hot spot. "
"The origin is top left corner."),
NULL);
/* Auto-crop */
button = pika_prop_check_button_new (config, "crop",
_("_Auto-Crop all frames"));
gtk_grid_attach (GTK_GRID (grid), button, 0, row++, 2, 1);
pika_help_set_help_data (button,
_("Remove the empty borders of all frames.\n"
"This reduces the file size and may fix "
"the problem that some large cursors disorder "
"the screen.\n"
"Uncheck if you plan to edit the exported "
"cursor using other programs."),
NULL);
/* Size */
store = pika_int_store_new ("12px", 12, "16px", 16,
"24px", 24, "32px", 32,
"36px", 36, "40px", 40,
"48px", 48, "64px", 64, NULL);
combo = pika_prop_int_combo_box_new (config, "size",
PIKA_INT_STORE (store));
g_object_unref (store);
pika_grid_attach_aligned (GTK_GRID (grid), 0, row++,
_("_Size where\nunspecified:"), 0.0, 0.5,
combo, 1);
pika_help_set_help_data (combo,
_("Choose the nominal size of frames.\n"
"If you don't have plans to make multi-sized "
"cursor, or you have no idea, leave it \"32px\".\n"
"Nominal size has no relation with the actual "
"size (width or height).\n"
"It is only used to determine which frame depends "
"on which animation sequence, and which sequence "
"is used based on the value of "
"\"gtk-cursor-theme-size\"."),
NULL);
/* Replace size */
button = pika_prop_check_button_new (config, "size-replace",
_("Use size entered above for "
"all frames"));
gtk_grid_attach (GTK_GRID (grid), button, 1, row++, 2, 1);
/* Delay */
box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
pika_grid_attach_aligned (GTK_GRID (grid), 0, row++,
_("_Delay where\nunspecified:"), 0, 0.5,
box, 3);
gtk_widget_show (box);
pika_help_set_help_data (box,
_("Enter time span in milliseconds in which "
"each frame is rendered."),
NULL);
button = pika_prop_spin_button_new (config, "delay",
1, 10, 0);
gtk_box_pack_start (GTK_BOX (box), button, FALSE, FALSE, 0);
label = gtk_label_new ("ms");
gtk_box_pack_start (GTK_BOX (box), label, FALSE, FALSE, 0);
gtk_widget_show (label);
/* Replace delay */
button = pika_prop_check_button_new (config, "delay-replace",
_("Use delay entered above for "
"all frames"));
gtk_grid_attach (GTK_GRID (grid), button, 1, row++, 2, 1);
/* Copyright */
entry = pika_prop_entry_new (config, "xmc-copyright",
XCURSOR_COMMENT_MAX_LEN);
pika_grid_attach_aligned (GTK_GRID (grid), 0, row++, _("_Copyright:"),
0, 0.5, entry, 3);
pika_help_set_help_data (entry,
_("Enter copyright information."),
NULL);
/* License */
entry = pika_prop_entry_new (config, "xmc-license",
XCURSOR_COMMENT_MAX_LEN);
pika_grid_attach_aligned (GTK_GRID (grid), 0, row++, _("_License:"),
0, 0.5, entry, 3);
pika_help_set_help_data (entry,
_("Enter license information."),
NULL);
/* Other */
box = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (box),
GTK_SHADOW_IN);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (box),
GTK_POLICY_AUTOMATIC,
GTK_POLICY_AUTOMATIC);
pika_grid_attach_aligned (GTK_GRID (grid), 0, row++, _("_Other:"),
0, 0.5, box, 3);
textbuffer = pika_prop_text_buffer_new (config, "pika-comment",
XCURSOR_COMMENT_MAX_LEN);
view = gtk_text_view_new_with_buffer (GTK_TEXT_BUFFER (textbuffer));
gtk_text_view_set_accepts_tab (GTK_TEXT_VIEW (view), FALSE);
gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (view), GTK_WRAP_WORD);
g_object_unref (textbuffer);
gtk_container_add (GTK_CONTAINER (box), view);
gtk_widget_show (view);
pika_help_set_help_data (view,
_("Enter other comment if you want."),
NULL);
gtk_widget_show (dialog);
run = pika_procedure_dialog_run (PIKA_PROCEDURE_DIALOG (dialog));
gtk_widget_destroy (dialog);
return run;
}
/**
* Set default hotspot based on hotspot_range.
**/
static void
load_default_hotspot (PikaImage *image,
GeglRectangle *hotspot_range,
GObject *config)
{
gint hot_spot_x;
gint hot_spot_y;
g_return_if_fail (hotspot_range);
g_object_get (config,
"hot-spot-x", &hot_spot_x,
"hot-spot-y", &hot_spot_y,
NULL);
if (/* if we cannot load hotspot correctly */
! get_hotspot_from_parasite (image, &hot_spot_x, &hot_spot_y) ||
/* or hotspot is out of range */
! pix_in_region (hot_spot_x, hot_spot_y, hotspot_range))
{
/* then use top left point of hotspot_range as fallback. */
hot_spot_x = hotspot_range->x;
hot_spot_y = hotspot_range->y;
}
g_object_set (config,
"hot-spot-x", hot_spot_x,
"hot-spot-y", hot_spot_y,
NULL);
}
/*
* 'save_image ()' - Save the specified image to X cursor file.
*/
static gboolean
save_image (GFile *file,
PikaImage *image,
gint n_drawables,
PikaDrawable **drawables,
PikaImage *orig_image,
GObject *config,
GError **error)
{
FILE *fp; /* File pointer */
gboolean dimension_warn = FALSE; /* become TRUE if even one
* of the dimensions of the
* frames of the cursor is
* over
* MAX_BITMAP_CURSOR_SIZE */
gboolean size_warn = FALSE; /* become TRUE if even one
* of the nominal size of
* the frames is not
* supported by
* gnome-appearance-properties */
GRegex *re; /* used to get size and delay from
* framename */
XcursorComments *commentsp; /* pointer to comments */
XcursorImages *imagesp; /* pointer to images */
GList *layers; /* Array of layer */
GList *orig_layers; /* Array of layer of orig_image */
GList *list;
GList *orig_list;
gchar *framename; /* framename of a layer */
GeglRectangle save_rgn = { 0 }; /* region to save */
gint layer_xoffset, layer_yoffset;
/* temporary buffer which store pixel data (guchar * bpp = guint32) */
guint32 pixelbuf[SQR (MAX_SAVE_DIMENSION)];
gint config_hot_spot_x;
gint config_hot_spot_y;
gboolean config_crop;
gboolean config_size_replace;
gint i, j; /* Looping vars */
g_object_get (config,
"hot-spot-x", &config_hot_spot_x,
"hot-spot-y", &config_hot_spot_y,
"crop", &config_crop,
"size-replace", &config_size_replace,
NULL);
/* This will be used in set_size_and_delay function later. To
* define this in that function is easy to read but place here to
* reduce overheads.
*/
re = g_regex_new ("[(][ 0]*(\\d+)[ ]*(px|ms)[ ]*[)]",
G_REGEX_CASELESS | G_REGEX_OPTIMIZE,
0,
NULL);
pika_progress_init_printf (_("Saving '%s'"),
pika_file_get_utf8_name (file));
/*
* Open the file pointer.
*/
DM_XMC ("Open the file pointer.\n");
fp = g_fopen (g_file_peek_path (file), "wb");
if (! fp)
{
g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
_("Could not open '%s' for writing: %s"),
pika_file_get_utf8_name (file), g_strerror (errno));
return FALSE;
}
/* get layers, in bottom-to-top order */
orig_layers = pika_image_list_layers (orig_image);
layers = pika_image_list_layers (image);
orig_layers = g_list_reverse (orig_layers);
layers = g_list_reverse (layers);
/* create new XcursorImages. */
imagesp = XcursorImagesCreate (g_list_length (layers));
if (!imagesp)
{
DM_XMC ("Failed to XcursorImagesCreate!\n");
fclose (fp);
return FALSE;
}
imagesp->nimage = g_list_length (layers);
/* XcursorImages also have `name' member but it is not used as long as I know.
We leave it NULL here. */
/*
* Now we start to convert each layer to a XcurosrImage one by one.
*/
for (list = layers, orig_list = orig_layers, i = 0;
list && orig_list;
list = g_list_next (layers), orig_list = g_list_next (orig_list), i++)
{
PikaDrawable *drawable = list->data;
GeglBuffer *buffer;
const Babl *format;
gint width;
gint height;
buffer = pika_drawable_get_buffer (drawable);
width = gegl_buffer_get_width (buffer);
height = gegl_buffer_get_height (buffer);
format = babl_format ("R'G'B'A u8");
/* get framename of this layer */
framename = pika_item_get_name (PIKA_ITEM (drawable));
/* get offset of this layer. */
pika_drawable_get_offsets (drawable, &layer_xoffset, &layer_yoffset);
/*
* layer dimension check.
*/
DM_XMC ("layer size check.\n");
/* We allow to save a cursor which dimensions are no more than
* MAX_SAVE_DIMENSION but after auto-cropping, we warn (only
* warn, don't stop) if dimension is over
* MAX_BITMAP_CURSOR_SIZE.
*/
if (width > MAX_SAVE_DIMENSION)
{
g_set_error (error, 0, 0,
_("Frame '%s' is too wide. Please reduce to no more than %dpx."),
pika_any_to_utf8 (framename, -1, NULL),
MAX_SAVE_DIMENSION);
fclose (fp);
return FALSE;
}
if (height > MAX_SAVE_DIMENSION)
{
g_set_error (error, 0, 0,
_("Frame '%s' is too high. Please reduce to no more than %dpx."),
pika_any_to_utf8 (framename, -1, NULL),
MAX_SAVE_DIMENSION);
fclose (fp);
return FALSE;
}
if (height == 0 || width == 0)
{
g_set_error (error, 0, 0,
_("Width and/or height of frame '%s' is zero!"),
pika_any_to_utf8 (framename, -1, NULL));
fclose (fp);
return FALSE;
}
if (config_crop) /* with auto-cropping */
{
/* get the region of auto-cropped area. */
DM_XMC ("get_cropped_region\n");
get_cropped_region (&save_rgn, buffer);
/* don't forget save_rgn's origin is not a entire image
* but a layer which we are doing on.
*/
if (save_rgn.width == 0 || save_rgn.height == 0)
{/* perfectly transparent frames become 1x1px transparent pixel. */
DM_XMC ("get_cropped_region return 0.\n");
imagesp->images[i] = XcursorImageCreate (1, 1);
if (!imagesp->images[i])
{
DM_XMC ("Failed to XcursorImageCreate.\n");
fclose (fp);
return FALSE;
}
imagesp->images[i]->pixels[0] = 0x0;
imagesp->images[i]->xhot = 0;
imagesp->images[i]->yhot = 0;
set_size_and_delay (config,
framename, &(imagesp->images[i]->size),
&(imagesp->images[i]->delay), re,
&size_warn);
continue;
}
/* OK save_rgn is not 0x0 */
/* is hotspot in save_rgn ? */
if (! pix_in_region (config_hot_spot_x - layer_xoffset,
config_hot_spot_y - layer_yoffset,
&save_rgn))
{ /* if hotspot is not on save_rgn */
g_set_error (error, 0, 0,
_("Cannot export the cursor because the hot spot "
"is not on frame '%s'.\n"
"Try to change the hot spot position, "
"layer geometry or export without auto-crop."),
pika_any_to_utf8 (framename, -1, NULL));
fclose (fp);
return FALSE;
}
}
else /* if without auto-cropping... */
{
/* set save_rgn for the case not to auto-crop */
save_rgn.width = width;
save_rgn.height = height;
save_rgn.x = 0;
save_rgn.y = 0;
}
/* We warn if the dimension of the layer is over MAX_BITMAP_CURSOR_SIZE. */
if (! dimension_warn)
{
if (save_rgn.width > MAX_BITMAP_CURSOR_SIZE ||
save_rgn.height > MAX_BITMAP_CURSOR_SIZE)
{
dimension_warn = TRUE;
/* actual warning is done after the cursor is successfully saved.*/
}
}
/*
* Create new XcursorImage.
*/
DM_XMC ("create new xcursorimage.\twidth=%i\theight=%i\n",
save_rgn.width, save_rgn.height);
imagesp->images[i] = XcursorImageCreate (save_rgn.width, save_rgn.height);
/* Cursor width & height is automatically set by function */
/* XcursorImageCreate, so no need to set manually. */
if (!imagesp->images[i])
{
DM_XMC ("Failed to XcursorImageCreate.\n");
fclose (fp);
return FALSE;
}
/*
** set images[i]'s xhot & yhot.
*/
/* [Cropped layer's hotspot] =
[image's hotspot] - [layer's offset] - [save_rgn's offset]. */
DM_XMC ("xhot=%i\tsave_rgn->xoffset=%i\tlayer_xoffset=%i\n",
config_hot_spot_x, layer_xoffset, save_rgn.x);
DM_XMC ("yhot=%i\tsave_rgn->yoffset=%i\tlayer_yoffset=%i\n",
config_hot_spot_y, layer_yoffset, save_rgn.y);
imagesp->images[i]->xhot = config_hot_spot_x - layer_xoffset - save_rgn.x;
imagesp->images[i]->yhot = config_hot_spot_y - layer_yoffset - save_rgn.y;
DM_XMC ("images[%i]->xhot=%i\tyhot=%i\n", i,
imagesp->images[i]->xhot, imagesp->images[i]->yhot);
/*
* set images[i]->pixels
*/
/* get image data to pixelbuf. */
gegl_buffer_get (buffer,
GEGL_RECTANGLE (save_rgn.x, save_rgn.y,
save_rgn.width, save_rgn.height), 1.0,
format, pixelbuf,
GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
/*convert pixel date to XcursorPixel. */
g_assert (save_rgn.width * save_rgn.height < SQR (MAX_SAVE_DIMENSION));
for (j = 0; j < save_rgn.width * save_rgn.height; j++)
{
imagesp->images[i]->pixels[j] = premultiply_alpha (pixelbuf[j]);
}
/*
* get back size & delay from framename.
*/
set_size_and_delay (config,
framename, &(imagesp->images[i]->size),
&(imagesp->images[i]->delay), re, &size_warn);
/*
* All property of this XcursorImage is loaded.
*/
/* set the layer name of original image with the saved value */
g_free (framename);
framename = make_framename (imagesp->images[i]->size,
imagesp->images[i]->delay,
DISPLAY_DIGIT (imagesp->nimage),
error);
if (! framename)
{
fclose (fp);
return FALSE;
}
pika_item_set_name (orig_list->data, framename);
g_free (framename);
g_object_unref (buffer);
pika_progress_update ((i + 1) / imagesp->nimage);
}
g_list_free (list);
g_list_free (orig_list);
pika_progress_update (1.0);
/*
* comment parsing
*/
commentsp = set_cursor_comments (config);
#ifdef XMC_DEBUG
DM_XMC ("imagesp->nimage=%i\tname=%s\n", imagesp->nimage, imagesp->name);
for (i = 0; i < imagesp->nimage; ++i)
{
DM_XMC ("\timages[%i]->size=%i\n\
\twidth=%i\n\
\theight=%i\n\
\txhot=%i\n\
\tyhot=%i\n\
\tdelay=%i\n\
\t*pixels=%p\n",
i,
imagesp->images[i]->size,
imagesp->images[i]->width,
imagesp->images[i]->height,
imagesp->images[i]->xhot,
imagesp->images[i]->yhot,
imagesp->images[i]->delay,
imagesp->images[i]->pixels);
}
if (commentsp)
{
for (i = 0; i < commentsp->ncomment; ++i)
{
DM_XMC ("comment type=%d\tcomment=%s\n",
commentsp->comments[i]->comment_type,
commentsp->comments[i]->comment);
}
}
#endif
/*
* save cursor to file *fp.
*/
if (commentsp)
{
if (! XcursorFileSave (fp, commentsp, imagesp))
{
DM_XMC ("Failed to XcursorFileSave.\t%p\t%p\t%p\n",
fp, commentsp, imagesp);
fclose (fp);
return FALSE;
}
}
else /* if no comments exist */
{
if (! XcursorFileSaveImages (fp, imagesp))
{
DM_XMC ("Failed to XcursorFileSaveImages.\t%p\t%p\n", fp, imagesp);
fclose (fp);
return FALSE;
}
}
/* actual warning about dimensions */
if (dimension_warn)
{
g_message (_("Your cursor was successfully exported but it contains one or "
"more frames whose width or height is more than %ipx, "
"a historical max dimension value for X bitmap cursors.\n"
"It might be unsupported by some environments."),
MAX_BITMAP_CURSOR_SIZE);
}
if (size_warn)
{
g_message (_("Your cursor was successfully exported but it contains one "
"or more frames whose nominal size is not supported by "
"GNOME settings.\n"
"You can satisfy it by checking \"Replace the size of all "
"frames...\" in the export dialog, or your cursor may not "
"appear in GNOME settings."));
}
/*
* Done with the file...
*/
g_regex_unref (re);
DM_XMC ("fp=%p\n", fp);
fclose (fp);
DM_XMC ("%i frames written.\n", imagesp->nimage);
XcursorImagesDestroy (imagesp);
DM_XMC ("Xcursor destroyed.\n");
XcursorCommentsDestroy (commentsp); /* this is safe even if commentsp is NULL. */
pika_progress_end ();
/* Save hotspot back to the original image */
set_hotspot_to_parasite (orig_image, config_hot_spot_x, config_hot_spot_y);
return TRUE;
}
static inline guint32
separate_alpha (guint32 pixel)
{
guint alpha, red, green, blue;
guint32 retval;
#if G_BYTE_ORDER != G_LITTLE_ENDIAN
pixel = GUINT32_TO_LE (pixel);
#endif
blue = pixel & 0xff;
green = (pixel>>8) & 0xff;
red = (pixel>>16) & 0xff;
alpha = (pixel>>24) & 0xff;
if (alpha == 0)
return 0;
/* resume separate alpha data. */
red = MIN (red * 255 / alpha, 255);
blue = MIN (blue * 255 / alpha, 255);
green = MIN (green * 255 / alpha, 255);
retval = red + (green<<8) + (blue<<16) + (alpha<<24);
#if G_BYTE_ORDER != G_LITTLE_ENDIAN
pixel = GUINT32_FROM_LE (pixel);
#endif
return retval;
}
static inline guint32
premultiply_alpha (guint32 pixel)
{
guint alpha, red, green, blue;
guint32 retval;
#if G_BYTE_ORDER != G_LITTLE_ENDIAN
pixel = GUINT32_TO_LE (pixel);
#endif
red = pixel & 0xff;
green = (pixel >> 8) & 0xff;
blue = (pixel >> 16) & 0xff;
alpha = (pixel >> 24) & 0xff;
/* premultiply alpha
(see "premultiply_data" function at line 154 of xcursorgen.c) */
red = div_255 (red * alpha);
green = div_255 (green * alpha);
blue = div_255 (blue * alpha);
retval = blue + (green << 8) + (red << 16) + (alpha << 24);
#if G_BYTE_ORDER != G_LITTLE_ENDIAN
pixel = GUINT32_FROM_LE (pixel);
#endif
return retval;
}
/* set comments to cursor from xmcparas.comments.
* don't forget to XcursorCommentsDestroy returned pointer later.
*/
static XcursorComments *
set_cursor_comments (GObject *config)
{
GArray *xcCommentsArray;
XcursorComment *(xcCommentp[3]) = { NULL, };
XcursorComments *xcCommentsp;
gchar *comments[3] = { NULL, };
guint arraylen;
gint i;
g_object_get (config,
"xmc-copyright", comments,
"xmc-license", comments + 1,
"pika-comment", comments + 2,
NULL);
xcCommentsArray = g_array_new (FALSE, FALSE, sizeof (XcursorComment *));
for (i = 0; i < 3; ++i)
{
if (comments[i])
{
gint gcomlen = strlen (comments[i]);
if (gcomlen > 0)
{
xcCommentp[i] = XcursorCommentCreate (i + 1, gcomlen);
/* first argument of XcursorCommentCreate is comment_type
defined in Xcursor.h as enumerator.
i + 1 is appropriate when we dispose parasiteName before MAIN(). */
if (! xcCommentp[i])
{
g_warning ("Cannot create xcCommentp[%i]\n", i);
return NULL;
}
else
{
g_stpcpy (xcCommentp[i]->comment, comments[i]);
g_array_append_val (xcCommentsArray, xcCommentp[i]);
}
}
g_free (comments[i]);
}
}
arraylen = xcCommentsArray->len;
if (arraylen == 0)
return NULL;
xcCommentsp = XcursorCommentsCreate (arraylen);
xcCommentsp->ncomment = arraylen;
for (i = 0; i < arraylen; ++i)
{
xcCommentsp->comments[i] =
g_array_index (xcCommentsArray, XcursorComment* ,i);
}
return xcCommentsp;
}
/* Set hotspot to "hot-spot" parasite which format is common with that
* of file-xbm.
*/
static gboolean
set_hotspot_to_parasite (PikaImage *image,
gint hot_spot_x,
gint hot_spot_y)
{
gboolean ret = FALSE;
gchar *tmpstr;
PikaParasite *parasite;
g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);
tmpstr = g_strdup_printf ("%d %d", hot_spot_x, hot_spot_y);
parasite = pika_parasite_new ("hot-spot",
PIKA_PARASITE_PERSISTENT,
strlen (tmpstr) + 1,
tmpstr);
g_free (tmpstr);
if (parasite)
{
ret = pika_image_attach_parasite (image, parasite);
pika_parasite_free (parasite);
}
return ret;
}
/* Get back xhot & yhot from "hot-spot" parasite.
* If succeed, hotspot coordinate is returned and
* return TRUE.
* If "hot-spot" is not found or broken, return FALSE.
*/
static gboolean
get_hotspot_from_parasite (PikaImage *image,
gint *hot_spot_x,
gint *hot_spot_y)
{
PikaParasite *parasite;
g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);
DM_XMC ("function: getHotsopt\n");
parasite = pika_image_get_parasite (image, "hot-spot");
if (parasite)
{
gchar *parasite_data;
guint32 parasite_size;
gint x, y;
parasite_data = (gchar *) pika_parasite_get_data (parasite, &parasite_size);
parasite_data = g_strndup (parasite_data, parasite_size);
if (sscanf (parasite_data, "%i %i", &x, &y) == 2)
{
*hot_spot_x = x;
*hot_spot_y = y;
g_free (parasite_data);
return TRUE;
}
g_free (parasite_data);
}
return FALSE;
}
/* Set size to sizep, delay to delayp from drawable's framename.
*/
static void
set_size_and_delay (GObject *config,
const gchar *framename,
guint32 *sizep,
guint32 *delayp,
GRegex *re,
gboolean *size_warnp)
{
guint32 size = 0;
guint32 delay = 0;
gchar *digits = NULL;
gchar *suffix = NULL;
GMatchInfo *info = NULL;
gint config_size;
gboolean config_size_replace;
gint config_delay;
gboolean config_delay_replace;
g_return_if_fail (framename);
g_return_if_fail (sizep);
g_return_if_fail (delayp);
g_return_if_fail (re);
g_object_get (config,
"size", &config_size,
"size-replace", &config_size_replace,
"delay", &config_delay,
"delay-replace", &config_delay_replace,
NULL);
DM_XMC ("function: set_size_and_delay\tframename=%s\n", framename);
/* re is defined at the start of save_image() as
[(] : open parenthesis
[ ]* : ignore zero or more spaces
(\\d+) : the number we want to get out
[ ]* : ignore zero or more spaces
(px|ms) : whether "px"(size) or "ms"(delay)
[ ]* : ignore zero or more spaces
[)] : close parenthesis
This is intended to match for the animation-play plug-in. */
g_regex_match (re, framename, 0, &info);
while (g_match_info_matches (info))
{
digits = g_match_info_fetch (info, 1);
suffix = g_match_info_fetch (info, 2);
if (g_ascii_strcasecmp (suffix, "px") == 0)
{
if (!size) /* substitute it only for the first time */
{
if (strlen (digits) > 8) /* too large number should be clamped */
{
g_message (_("Your cursor was successfully exported but it contains one or "
"more frames whose size is over 8 digits.\n"
"We clamped it to %dpx. You should check the exported cursor."),
MAX_BITMAP_CURSOR_SIZE);
size = MAX_BITMAP_CURSOR_SIZE;
}
else
{
size = atoi (digits);
}
}
}
else /* suffix is "ms" */
{
if (!delay) /* substitute it only for the first time */
{
if (strlen (digits) > 8) /* too large number should be clamped */
delay = CURSOR_MAX_DELAY;
else
delay = MIN (CURSOR_MAX_DELAY, atoi (digits));
}
}
g_free (digits);
g_free (suffix);
g_match_info_next (info, NULL);
}
g_match_info_free (info);
/* if size is not set, or size_replace is TRUE, set default size
* (which was chosen in save dialog) */
if (size == 0 || config_size_replace == TRUE)
{
size = config_size;
}
else if (! *size_warnp &&
size != 12 && size != 16 && size != 24 && size != 32 &&
size != 36 && size != 40 && size != 48 && size != 64 &&
size != 96)
{ /* if the size is different from these values, we warn about it after
successfully saving because gnome-appearance-properties only support
them. */
*size_warnp = TRUE;
}
*sizep = size;
/* if delay is not set, or delay_replace is TRUE, set default delay
* (which was chosen in save dialog) */
if (delay == 0 || config_delay_replace == TRUE)
{
delay = config_delay;
}
*delayp = delay;
DM_XMC ("set_size_and_delay return\tsize=%i\tdelay=%i\n", size, delay);
}
/* Return framename as format: "([x]px)_[i] ([t]ms) (replace)"
* where [x] is nominal size, [t] is delay passed as argument respectively,
* and [i] is an index separately counted by [x].
* This format is compatible with "animation-play" plug-in.
* Don't forget to g_free returned framename later.
*/
static gchar *
make_framename (guint32 size,
guint32 delay,
guint indent,
GError **errorp)
{
static struct
{
guint32 size;
guint count;
} Counter[MAX_SIZE_NUM + 1] = {{0,}};
int i; /* loop index */
/* don't pass 0 for size. */
g_return_val_if_fail (size > 0, NULL);
/* "count" member of Counter's element means how many time corresponding
"size" is passed to this function. The size member of the last element
of Counter must be 0, so Counter can have MAX_SIZE_NUM elements at most.
This is not a smart way but rather simple than using dynamic method. */
for (i = 0; Counter[i].size != size; ++i)
{
if (Counter[i].size == 0) /* the end of Counter elements */
{
if (i > MAX_SIZE_NUM)
{
g_set_error (errorp, 0, 0,
/* translators: the %i is *always* 8 here */
_("Sorry, this plug-in cannot handle a cursor "
"which contains over %i different nominal sizes."),
MAX_SIZE_NUM);
return NULL;
}
else /* append new element which "size" is given value. */
{
Counter[i].size = size;
break;
}
}
}
Counter[i].count += 1;
return g_strdup_printf ("(%dpx)_%0*d (%dms) (replace)", size, indent,
Counter[i].count, delay);
}
/* Get the region which is maintained when auto-crop.
*/
static void
get_cropped_region (GeglRectangle *return_rgn,
GeglBuffer *buffer)
{
gint width = gegl_buffer_get_width (buffer);
gint height = gegl_buffer_get_height (buffer);
guint32 *buf = g_malloc (MAX (width, height) * sizeof (guint32));
const Babl *format = babl_format ("R'G'B'A u8");
guint i, j;
g_return_if_fail (GEGL_IS_BUFFER (buffer));
DM_XMC ("function:get_cropped_region\n");
DM_XMC ("getTrim:\tMAX=%i\tpr->w=%i\tpr->h=%i\n", sizeof (buf)/4, pr->w, pr->h);
/* find left border. */
for (i = 0; i < width; ++i)
{
DM_XMC ("i=%i width=%i\n", i, width);
gegl_buffer_get (buffer, GEGL_RECTANGLE (i, 0, 1, height), 1.0,
format, buf,
GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
for (j = 0; j < height; ++j)
{
if (pix_is_opaque (buf[j])) /* if a opaque pixel exist. */
{
return_rgn->x = i;
goto find_right;
}
}
}
/* pr has no opaque pixel. */
return_rgn->width = 0;
return;
/* find right border. */
find_right:
for (i = 0; i < width ; ++i)
{
DM_XMC ("width-1-i=%i height=%i\n", width - 1 - i, height);
gegl_buffer_get (buffer, GEGL_RECTANGLE (width - 1 - i, 0, 1, height), 1.0,
format, buf,
GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
for (j = 0; j < height; ++j)
{
if (pix_is_opaque (buf[j])) /* if a opaque pixel exist. */
{
return_rgn->width = width - i - return_rgn->x;
goto find_top;
}
}
}
g_return_if_reached ();
/* find top border. */
find_top:
for (j = 0; j < height; ++j)
{
DM_XMC ("j=%i width=%i\n", j, width);
gegl_buffer_get (buffer, GEGL_RECTANGLE (0, j, width, 1), 1.0,
format, buf,
GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
for (i = 0; i < width; ++i)
{
if (pix_is_opaque (buf[i])) /* if a opaque pixel exist. */
{
return_rgn->y = j;
goto find_bottom;
}
}
}
g_return_if_reached ();
/* find bottom border. */
find_bottom:
for (j = 0; j < height; ++j)
{
DM_XMC ("height-1-j=%i width=%i\n", height - 1 - j, width);
gegl_buffer_get (buffer, GEGL_RECTANGLE (0, height - 1 - j, width, 1), 1.0,
format, buf,
GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
for (i = 0; i < width; ++i)
{
if (pix_is_opaque (buf[i])) /* if a opaque pixel exist. */
{
return_rgn->height = height - j - return_rgn->y;
goto end_trim;
}
}
}
g_return_if_reached ();
end_trim:
DM_XMC ("width=%i\theight=%i\txoffset=%i\tyoffset=%i\n",
return_rgn->width, return_rgn->height,
return_rgn->x, return_rgn->y);
g_free (buf);
}
/* Return true if alpha of pix is not 0.
*/
static inline gboolean
pix_is_opaque (guint32 pix)
{
#if G_BYTE_ORDER != G_LITTLE_ENDIAN
pix = GUINT32_TO_LE (pix);
#endif
return ((pix >> 24) != 0);
}
/* Get the intersection of the all layers of the image specified by image.
* if the intersection is empty return NULL.
* don't forget to g_free returned pointer later.
*/
static GeglRectangle *
get_intersection_of_frames (PikaImage *image)
{
GeglRectangle *iregion;
gint32 x1 = G_MININT32, x2 = G_MAXINT32;
gint32 y1 = G_MININT32, y2 = G_MAXINT32;
gint32 x_off, y_off;
GList *layers;
GList *list;
g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);
layers = pika_image_list_layers (image);
for (list = layers; list; list = g_list_next (list))
{
PikaDrawable *drawable = list->data;
if (! pika_drawable_get_offsets (drawable, &x_off, &y_off))
{
g_list_free (layers);
return NULL;
}
x1 = MAX (x1, x_off);
y1 = MAX (y1, y_off);
x2 = MIN (x2, x_off + pika_drawable_get_width (drawable) - 1);
y2 = MIN (y2, y_off + pika_drawable_get_height (drawable) - 1);
}
g_list_free (layers);
if (x1 > x2 || y1 > y2)
return NULL;
/* OK intersection exists. */
iregion = g_new (GeglRectangle, 1);
iregion->x = x1;
iregion->y = y1;
iregion->width = x2 - x1 + 1;
iregion->height = y2 - y1 + 1;
return iregion;
}
/* If (x,y) is in xmcrp, return TRUE.
*/
static gboolean
pix_in_region (gint32 x,
gint32 y,
GeglRectangle *xmcrp)
{
g_return_val_if_fail (xmcrp, FALSE);
if (x < xmcrp->x || y < xmcrp->y ||
x >= xmcrp->x + xmcrp->width || y >= xmcrp->y + xmcrp->height)
return FALSE;
else
return TRUE;
}
/**
* Find out xhot, yhot, width and height of the Xcursor specified by xcIs.
* Use NULL for the value you don't want to return.
**/
static void
find_hotspots_and_dimensions (XcursorImages *xcIs,
gint32 *xhotp,
gint32 *yhotp,
gint32 *widthp,
gint32 *heightp)
{
gint32 dw, dh; /* the distance between hotspot and right(bottom) border */
gint32 max_xhot;
gint32 max_yhot; /* the maximum value of xhot(yhot) */
gint i;
g_return_if_fail (xcIs);
max_xhot = max_yhot = dw = dh = 0;
for (i = 0; i < xcIs->nimage; ++i)
{
/* xhot of entire image is the maximum value of xhot of all frames */
max_xhot = MAX (xcIs->images[i]->xhot, max_xhot);
/* same for yhot */
max_yhot = MAX (xcIs->images[i]->yhot, max_yhot);
/* the maximum distance between right border and xhot */
dw = MAX (dw, xcIs->images[i]->width - xcIs->images[i]->xhot);
/* the maximum distance between bottom border and yhot */
dh = MAX (dh, xcIs->images[i]->height - xcIs->images[i]->yhot);
}
if (xhotp)
*xhotp = max_xhot;
if (yhotp)
*yhotp = max_yhot;
if (widthp)
*widthp = dw + max_xhot;
if (heightp)
*heightp = dh + max_yhot;
}