2319 lines
76 KiB
C
2319 lines
76 KiB
C
/*
|
|
* 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, ¶site_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;
|
|
}
|