Gimp/plug-ins/common/file-svg.c

2312 lines
83 KiB
C

/* GIMP - The GNU Image Manipulation Program
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
*
* 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/>.
*/
/* SVG loader plug-in
* (C) Copyright 2003 Dom Lachowicz <cinamod@hotmail.com>
*
* Largely rewritten in September 2003 by Sven Neumann <sven@gimp.org>
*/
#include "config.h"
#include <stdlib.h>
#include <string.h>
#include <glib/gstdio.h>
#include <librsvg/rsvg.h>
#include "libgimp/gimp.h"
#include "libgimp/gimpui.h"
#include "libgimp/stdplugins-intl.h"
#define LOAD_PROC "file-svg-load"
#define EXPORT_PROC "file-svg-export"
#define PLUG_IN_BINARY "file-svg"
#define PLUG_IN_ROLE "gimp-file-svg"
#define SVG_VERSION "2.5.0"
#define SVG_DEFAULT_RESOLUTION 90.0
#define SVG_DEFAULT_SIZE 500
#define SVG_PREVIEW_SIZE 128
#define EPSILON 1e-6
typedef struct
{
gdouble resolution;
gint width;
gint height;
} SvgLoadVals;
typedef enum
{
EXPORT_FORMAT_NONE,
EXPORT_FORMAT_PNG,
EXPORT_FORMAT_JPEG
} EXPORT_FORMAT;
typedef enum
{
LAYER_ID_VECTOR,
LAYER_ID_GROUP,
LAYER_ID_TEXT,
LAYER_ID_LINK,
LAYER_ID_RASTER,
LAYER_ID_MASK,
} LAYER_ID;
typedef struct _Svg Svg;
typedef struct _SvgClass SvgClass;
struct _Svg
{
GimpPlugIn parent_instance;
};
struct _SvgClass
{
GimpPlugInClass parent_class;
};
#define SVG_TYPE (svg_get_type ())
#define SVG(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SVG_TYPE, Svg))
GType svg_get_type (void) G_GNUC_CONST;
static GList * svg_query_procedures (GimpPlugIn *plug_in);
static GimpProcedure * svg_create_procedure (GimpPlugIn *plug_in,
const gchar *name);
static gboolean svg_extract (GimpProcedure *procedure,
GimpRunMode run_mode,
GFile *file,
GimpMetadata *metadata,
GimpProcedureConfig *config,
GimpVectorLoadData *extracted_dimensions,
gpointer *data_for_run,
GDestroyNotify *data_for_run_destroy,
gpointer extract_data,
GError **error);
static GimpValueArray * svg_load (GimpProcedure *procedure,
GimpRunMode run_mode,
GFile *file,
gint width,
gint height,
GimpVectorLoadData extracted_data,
GimpMetadata *metadata,
GimpMetadataLoadFlags *flags,
GimpProcedureConfig *config,
gpointer data_from_extract,
gpointer run_data);
static GimpValueArray * svg_export (GimpProcedure *procedure,
GimpRunMode run_mode,
GimpImage *image,
GFile *file,
GimpExportOptions *options,
GimpMetadata *metadata,
GimpProcedureConfig *config,
gpointer run_data);
static GimpImage * load_image (GFile *file,
RsvgHandle *handle,
gint width,
gint height,
gdouble resolution,
GError **error);
static gboolean export_image (GFile *file,
GimpProcedureConfig *config,
GimpImage *image,
GError **error);
static GdkPixbuf * load_rsvg_pixbuf (RsvgHandle *handle,
gint width,
gint height,
gdouble resolution,
GError **error);
static GimpPDBStatusType load_dialog (GFile *file,
RsvgHandle *handle,
GimpProcedure *procedure,
GimpProcedureConfig *config,
GimpVectorLoadData extracted_data,
GError **error);
static gboolean export_dialog (GimpImage *image,
GimpProcedure *procedure,
GObject *config);
static gboolean has_invalid_links (GimpLayer **layers,
GimpGroupLayer *group);
static gboolean svg_extract_dimensions (RsvgHandle *handle,
GimpVectorLoadData *extracted_dimensions);
/* Taken from /app/path/gimppath-export.c */
static GString * svg_export_file (GimpImage *image,
GimpProcedureConfig *config,
GError **error);
static void svg_export_image_size (GimpImage *image,
GString *str);
static void svg_export_defs (GimpItem **layers,
GimpGroupLayer *group,
GString *str,
GimpProcedureConfig *config,
GList **exported_res,
GError **error);
static void svg_export_def (GimpVectorLayer *layer,
GString *str,
GimpProcedureConfig *config,
GList **exported_res,
GError **error);
static gchar * svg_get_def_string (GimpVectorLayer *layer,
const gchar *type,
GimpProcedureConfig *config,
GList **exported_res,
GError **error);
static void svg_export_layers (GimpItem **layers,
GimpGroupLayer *group,
gint *layer_ids,
GString *str,
GimpProcedureConfig *config,
gchar *spacing,
GError **error);
static void svg_export_group_header (GimpGroupLayer *group,
gint group_id,
GString *str,
gchar *spacing);
static void svg_export_path (GimpVectorLayer *layer,
gint vector_id,
GString *str,
gchar *spacing);
static gchar * svg_export_path_data (GimpPath *paths);
static void svg_export_text (GimpTextLayer *layer,
gint text_id,
GString *str,
gchar *spacing);
static void svg_export_text_lines (GimpTextLayer *layer,
GString *str,
gchar *text);
static void svg_export_link_layer (GimpLinkLayer *layer,
gint link_id,
GString *str,
gchar *spacing);
static void svg_export_raster (GimpDrawable *layer,
gint *layer_ids,
GString *str,
gint format_id,
gchar *spacing,
GError **error);
static void svg_export_layer_mask (GimpLayerMask *mask,
gint *layer_ids,
GString *str,
gchar *spacing,
GError **error);
static gchar * svg_get_color_string (GimpVectorLayer *layer,
const gchar *type);
static gchar * svg_get_hex_color (GeglColor *color);
static gboolean svg_drawable_has_transparency (GimpDrawable *drawable);
#if LIBRSVG_CHECK_VERSION(2, 46, 0)
static GimpUnit * svg_rsvg_to_gimp_unit (RsvgUnit unit);
static void svg_destroy_surface (guchar *pixels,
cairo_surface_t *surface);
#endif
G_DEFINE_TYPE (Svg, svg, GIMP_TYPE_PLUG_IN)
GIMP_MAIN (SVG_TYPE)
DEFINE_STD_SET_I18N
static void
svg_class_init (SvgClass *klass)
{
GimpPlugInClass *plug_in_class = GIMP_PLUG_IN_CLASS (klass);
plug_in_class->query_procedures = svg_query_procedures;
plug_in_class->create_procedure = svg_create_procedure;
plug_in_class->set_i18n = STD_SET_I18N;
}
static void
svg_init (Svg *svg)
{
}
static GList *
svg_query_procedures (GimpPlugIn *plug_in)
{
GList *list = NULL;
list = g_list_append (list, g_strdup (LOAD_PROC));
list = g_list_append (list, g_strdup (EXPORT_PROC));
return list;
}
static GimpProcedure *
svg_create_procedure (GimpPlugIn *plug_in,
const gchar *name)
{
GimpProcedure *procedure = NULL;
if (! strcmp (name, LOAD_PROC))
{
procedure = gimp_vector_load_procedure_new (plug_in, name,
GIMP_PDB_PROC_TYPE_PLUGIN,
svg_extract, NULL, NULL,
svg_load, NULL, NULL);
gimp_procedure_set_menu_label (procedure, _("SVG image"));
gimp_procedure_set_documentation (procedure,
_("Loads files in SVG file format"),
/* TODO: let's localize after we
* add support for vector and/or
* text and link layers:
* _("Loads SVG (Scalable Vector Graphic) files")
*/
"Renders SVG files to raster graphics "
"using librsvg.",
name);
gimp_procedure_set_attribution (procedure,
"Dom Lachowicz, Sven Neumann",
"Dom Lachowicz <cinamod@hotmail.com>",
SVG_VERSION);
gimp_file_procedure_set_format_name (GIMP_FILE_PROCEDURE (procedure),
_("SVG"));
gimp_file_procedure_set_mime_types (GIMP_FILE_PROCEDURE (procedure),
"image/svg+xml");
gimp_file_procedure_set_extensions (GIMP_FILE_PROCEDURE (procedure),
"svg");
gimp_file_procedure_set_magics (GIMP_FILE_PROCEDURE (procedure),
"0,string,<?xml,0,string,<svg");
gimp_procedure_add_choice_argument (procedure, "paths",
_("_Paths"),
_("Whether and how to import paths so that they can be used with the path tool"),
gimp_choice_new_with_values ("no-import", 0, _("Don't import paths"), NULL,
"import", 1, _("Import paths individually"), NULL,
"import-merged", 2, _("Merge imported paths"), NULL,
NULL),
"no-import", G_PARAM_READWRITE);
}
else if (! strcmp (name, EXPORT_PROC))
{
procedure = gimp_export_procedure_new (plug_in, name,
GIMP_PDB_PROC_TYPE_PLUGIN,
FALSE, svg_export, NULL, NULL);
gimp_procedure_set_image_types (procedure, "*");
gimp_procedure_set_menu_label (procedure, _("Scalable Vector Graphic"));
gimp_procedure_set_documentation (procedure,
_("Exports files in SVG file format"),
_("Exports files in SVG file format "
"(Scalable Vector Graphic)"),
name);
gimp_procedure_set_attribution (procedure,
"Alex S.",
"Alex S.",
"2025");
gimp_file_procedure_set_format_name (GIMP_FILE_PROCEDURE (procedure),
"SVG");
gimp_file_procedure_set_mime_types (GIMP_FILE_PROCEDURE (procedure),
"image/svg+xml");
gimp_file_procedure_set_extensions (GIMP_FILE_PROCEDURE (procedure),
"svg");
gimp_export_procedure_set_capabilities (GIMP_EXPORT_PROCEDURE (procedure),
GIMP_EXPORT_CAN_HANDLE_RGB |
GIMP_EXPORT_CAN_HANDLE_GRAY |
GIMP_EXPORT_CAN_HANDLE_INDEXED |
GIMP_EXPORT_CAN_HANDLE_ALPHA |
GIMP_EXPORT_CAN_HANDLE_LAYERS |
GIMP_EXPORT_CAN_HANDLE_LAYER_MASKS,
NULL, NULL, NULL);
gimp_procedure_add_string_argument (procedure, "title",
_("_Title"),
_("Optional <title> for SVG"),
NULL,
G_PARAM_READWRITE);
gimp_procedure_add_choice_argument (procedure, "raster-export-format",
_("Em_bed raster layers as"),
NULL,
gimp_choice_new_with_values ("png", EXPORT_FORMAT_PNG, _("PNG (always)"), NULL,
"jpeg", EXPORT_FORMAT_JPEG, _("JPEG (for opaque images, PNG otherwise)"), NULL,
"none", EXPORT_FORMAT_NONE, _("Do not export raster layers"), NULL,
NULL),
"png",
G_PARAM_READWRITE);
}
return procedure;
}
static gboolean
svg_extract (GimpProcedure *procedure,
GimpRunMode run_mode,
GFile *file,
GimpMetadata *metadata,
GimpProcedureConfig *config,
GimpVectorLoadData *extracted_dimensions,
gpointer *data_for_run,
GDestroyNotify *data_for_run_destroy,
gpointer extract_data,
GError **error)
{
RsvgHandle *handle;
g_return_val_if_fail (extracted_dimensions != NULL, FALSE);
g_return_val_if_fail (data_for_run_destroy != NULL && *data_for_run_destroy == NULL, FALSE);
g_return_val_if_fail (data_for_run != NULL && *data_for_run == NULL, FALSE);
g_return_val_if_fail (error != NULL && *error == NULL, FALSE);
handle = rsvg_handle_new_from_gfile_sync (file, RSVG_HANDLE_FLAGS_NONE, NULL, error);
if (! handle)
{
if (run_mode == GIMP_RUN_INTERACTIVE)
{
GtkWidget *dialog;
GtkWidget *main_hbox;
GtkWidget *label;
gchar *text;
gboolean retry;
gimp_ui_init (PLUG_IN_BINARY);
/* We need to ask explicitly before using the "unlimited" size
* option (XML_PARSE_HUGE in libxml) because it is considered
* unsafe, possibly consumming too much memory with malicious XML
* files.
*/
dialog = gimp_dialog_new (_("Disable safety size limits?"),
PLUG_IN_ROLE,
NULL, 0,
gimp_standard_help_func, LOAD_PROC,
_("_No"), GTK_RESPONSE_NO,
_("_Yes"), GTK_RESPONSE_YES,
NULL);
gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_NO);
gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE);
gimp_window_set_transient (GTK_WINDOW (dialog));
main_hbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12);
gtk_container_set_border_width (GTK_CONTAINER (main_hbox), 12);
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
main_hbox, TRUE, TRUE, 0);
gtk_widget_show (main_hbox);
/* Unfortunately the error returned by librsvg is unclear. While
* libxml explicitly returns a "parser error : internal error:
* Huge input lookup", librsvg does not seem to relay this error.
* It sends a further parsing error, false positive provoked by
* the huge input error.
* If we were able to single out the huge data error, we could
* just directly return from the plug-in as a failure run in other
* cases. Instead of this, we need to ask each and every time, in
* case it might be the huge data error.
*/
label = gtk_label_new (_("A parsing error occurred.\n"
"Disabling safety limits may help. "
"Malicious SVG files may use this to consume too much memory."));
gtk_label_set_justify (GTK_LABEL (label), GTK_JUSTIFY_CENTER);
gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
gtk_label_set_line_wrap_mode (GTK_LABEL (label), PANGO_WRAP_WORD);
gtk_label_set_max_width_chars (GTK_LABEL (label), 80);
gtk_box_pack_start (GTK_BOX (main_hbox), label, FALSE, FALSE, 0);
gtk_widget_show (label);
label = gtk_label_new (NULL);
text = g_strdup_printf ("<b>%s</b>",
_("For security reasons, this should only be used for trusted input!"));
gtk_label_set_markup (GTK_LABEL (label), text);
g_free (text);
gtk_label_set_justify (GTK_LABEL (label), GTK_JUSTIFY_CENTER);
gtk_box_pack_start (GTK_BOX (main_hbox), label, FALSE, FALSE, 0);
gtk_widget_show (label);
label = gtk_label_new (_("Retry without limits preventing to parse huge data?"));
gtk_label_set_justify (GTK_LABEL (label), GTK_JUSTIFY_CENTER);
gtk_box_pack_start (GTK_BOX (main_hbox), label, FALSE, FALSE, 0);
gtk_widget_show (label);
gtk_widget_show (dialog);
retry = (gimp_dialog_run (GIMP_DIALOG (dialog)) == GTK_RESPONSE_YES);
gtk_widget_destroy (dialog);
if (retry)
{
g_clear_error (error);
handle = rsvg_handle_new_from_gfile_sync (file, RSVG_HANDLE_FLAG_UNLIMITED, NULL, error);
}
}
if (! handle)
return FALSE;
}
if (! svg_extract_dimensions (handle, extracted_dimensions))
{
g_object_unref (handle);
return FALSE;
}
*data_for_run = handle;
*data_for_run_destroy = g_object_unref;
return TRUE;
}
static gboolean
svg_extract_dimensions (RsvgHandle *handle,
GimpVectorLoadData *extracted_dimensions)
{
#if LIBRSVG_CHECK_VERSION(2, 46, 0)
gboolean out_has_width;
RsvgLength out_width;
gboolean out_has_height;
RsvgLength out_height;
gboolean out_has_viewbox;
RsvgRectangle out_viewbox;
#endif
#if ! LIBRSVG_CHECK_VERSION(2, 52, 0)
RsvgDimensionData dim;
#endif
/* SVG has no pixel density information */
extracted_dimensions->exact_density = TRUE;
extracted_dimensions->correct_ratio = TRUE;
#if LIBRSVG_CHECK_VERSION(2, 46, 0)
rsvg_handle_get_intrinsic_dimensions (handle,
&out_has_width, &out_width,
&out_has_height, &out_height,
&out_has_viewbox, &out_viewbox);
if (out_has_width && out_has_height)
{
/* Starting in librsvg 2.54, rsvg_handle_get_intrinsic_dimensions ()
* always returns TRUE for width/height. If width/height default to 100%,
* we should check the viewport dimensions */
gdouble viewbox_width = 0;
gdouble viewbox_height = 0;
if (out_has_viewbox)
{
viewbox_width = out_viewbox.width - out_viewbox.x;
viewbox_height = out_viewbox.height - out_viewbox.y;
}
/* "width" and "height" present */
if (svg_rsvg_to_gimp_unit (out_width.unit) != NULL &&
svg_rsvg_to_gimp_unit (out_height.unit) != NULL)
{
/* Best case scenario: we know all these units. */
extracted_dimensions->width = out_width.length;
extracted_dimensions->width_unit = svg_rsvg_to_gimp_unit (out_width.unit);
extracted_dimensions->height = out_height.length;
extracted_dimensions->height_unit = svg_rsvg_to_gimp_unit (out_height.unit);
if ((out_width.length == 1.0 && out_width.unit == RSVG_UNIT_PERCENT) &&
(out_height.length == 1.0 && out_height.unit == RSVG_UNIT_PERCENT))
{
extracted_dimensions->width = viewbox_width;
extracted_dimensions->height = viewbox_height;
extracted_dimensions->exact_width = FALSE;
extracted_dimensions->exact_height = FALSE;
}
else
{
extracted_dimensions->exact_width = TRUE;
extracted_dimensions->exact_height = TRUE;
}
}
else
{
/* Ideally we let GIMP handle the transformation, but for cases where
* we don't know how, we let rsvg make a computation to pixel for us.
*/
#if LIBRSVG_CHECK_VERSION(2, 52, 0)
if (rsvg_handle_get_intrinsic_size_in_pixels (handle,
&extracted_dimensions->width,
&extracted_dimensions->height))
{
extracted_dimensions->width_unit = gimp_unit_pixel ();
extracted_dimensions->height_unit = gimp_unit_pixel ();
extracted_dimensions->exact_width = FALSE;
extracted_dimensions->exact_height = FALSE;
}
else
{
/* Honestly at this point, setting 300 PPI is random. */
rsvg_handle_set_dpi (handle, 300.0);
if (rsvg_handle_get_intrinsic_size_in_pixels (handle,
&extracted_dimensions->width,
&extracted_dimensions->height))
{
extracted_dimensions->width_unit = gimp_unit_percent ();
extracted_dimensions->height_unit = gimp_unit_percent ();
}
else if (out_width.unit == out_height.unit)
{
/* Odd case which should likely never happen as we have width,
* height and a (fake) pixel density. Just in case though.
*/
extracted_dimensions->width = out_width.length;
extracted_dimensions->height = out_height.length;
extracted_dimensions->width_unit = gimp_unit_percent ();
extracted_dimensions->height_unit = gimp_unit_percent ();
}
else
{
/* Should even less happen! */
return FALSE;
}
}
#else
rsvg_handle_get_dimensions (handle, &dim);
extracted_dimensions->width = (gdouble) dim.width;
extracted_dimensions->height = (gdouble) dim.height;
extracted_dimensions->exact_width = FALSE;
extracted_dimensions->exact_height = FALSE;
extracted_dimensions->correct_ratio = TRUE;
#endif
}
}
else if (out_has_viewbox)
{
/* Only a viewbox, so dimensions have a ratio, but no units. */
extracted_dimensions->width = out_width.length;
extracted_dimensions->width_unit = gimp_unit_percent ();
extracted_dimensions->exact_width = FALSE;
extracted_dimensions->height = out_height.length;
extracted_dimensions->height_unit = gimp_unit_percent ();
extracted_dimensions->exact_height = FALSE;
extracted_dimensions->correct_ratio = TRUE;
}
else
{
/* Neither width nor viewbox. Nothing can be determined. */
return FALSE;
}
#else
rsvg_handle_get_dimensions (handle, &dim);
extracted_dimensions->width = (gdouble) dim.width;
extracted_dimensions->height = (gdouble) dim.height;
extracted_dimensions->exact_width = FALSE;
extracted_dimensions->exact_height = FALSE;
extracted_dimensions->correct_ratio = TRUE;
#endif
return TRUE;
}
static GimpValueArray *
svg_load (GimpProcedure *procedure,
GimpRunMode run_mode,
GFile *file,
gint width,
gint height,
GimpVectorLoadData extracted_data,
GimpMetadata *metadata,
GimpMetadataLoadFlags *flags,
GimpProcedureConfig *config,
gpointer data_from_extract,
gpointer run_data)
{
GimpValueArray *return_vals;
GimpImage *image;
GError *error = NULL;
GimpPDBStatusType status;
gchar *import_paths;
gdouble resolution;
gegl_init (NULL, NULL);
if (run_mode == GIMP_RUN_INTERACTIVE)
{
status = load_dialog (file, RSVG_HANDLE (data_from_extract),
procedure, config, extracted_data, &error);
if (status != GIMP_PDB_SUCCESS)
return gimp_procedure_new_return_values (procedure, status, error);
}
g_object_get (config,
"width", &width,
"height", &height,
"pixel-density", &resolution,
NULL);
image = load_image (file, RSVG_HANDLE (data_from_extract),
width, height, resolution, &error);
if (! image)
return gimp_procedure_new_return_values (procedure,
GIMP_PDB_EXECUTION_ERROR,
error);
g_object_get (config, "paths", &import_paths, NULL);
if (g_strcmp0 (import_paths, "no-import") != 0)
{
GimpPath **paths = NULL;
gimp_image_import_paths_from_file (image, file,
g_strcmp0 (import_paths, "import-merged") == 0,
TRUE, &paths);
g_free (paths);
}
g_free (import_paths);
return_vals = gimp_procedure_new_return_values (procedure,
GIMP_PDB_SUCCESS,
NULL);
GIMP_VALUES_SET_IMAGE (return_vals, 1, image);
return return_vals;
}
static GimpValueArray *
svg_export (GimpProcedure *procedure,
GimpRunMode run_mode,
GimpImage *image,
GFile *file,
GimpExportOptions *options,
GimpMetadata *metadata,
GimpProcedureConfig *config,
gpointer run_data)
{
GimpPDBStatusType status = GIMP_PDB_SUCCESS;
GimpExportReturn export = GIMP_EXPORT_IGNORE;
GError *error = NULL;
gegl_init (NULL, NULL);
if (run_mode == GIMP_RUN_INTERACTIVE)
{
gimp_ui_init (PLUG_IN_BINARY);
if (! export_dialog (image, procedure, G_OBJECT (config)))
status = GIMP_PDB_CANCEL;
}
if (status == GIMP_PDB_SUCCESS)
{
export = gimp_export_options_get_image (options, &image);
if (! export_image (file, config, image, &error))
status = GIMP_PDB_EXECUTION_ERROR;
if (export == GIMP_EXPORT_EXPORT)
gimp_image_delete (image);
}
return gimp_procedure_new_return_values (procedure, status, error);
}
static GimpImage *
load_image (GFile *file,
RsvgHandle *handle,
gint width,
gint height,
gdouble resolution,
GError **load_error)
{
GimpImage *image;
GimpLayer *layer;
GdkPixbuf *pixbuf;
GError *error = NULL;
pixbuf = load_rsvg_pixbuf (handle, width, height, resolution, &error);
if (! pixbuf)
{
/* Do not rely on librsvg setting GError on failure! */
g_set_error (load_error,
error ? error->domain : 0, error ? error->code : 0,
_("Could not open '%s' for reading: %s"),
gimp_file_get_utf8_name (file),
error ? error->message : _("Unknown reason"));
g_clear_error (&error);
return NULL;
}
gimp_progress_init (_("Rendering SVG"));
width = gdk_pixbuf_get_width (pixbuf);
height = gdk_pixbuf_get_height (pixbuf);
image = gimp_image_new (width, height, GIMP_RGB);
gimp_image_undo_disable (image);
gimp_image_set_resolution (image, resolution, resolution);
layer = gimp_layer_new_from_pixbuf (image, _("Rendered SVG"), pixbuf,
100,
gimp_image_get_default_new_layer_mode (image),
0.0, 1.0);
gimp_image_insert_layer (image, layer, NULL, 0);
gimp_image_undo_enable (image);
return image;
}
static gboolean
export_image (GFile *file,
GimpProcedureConfig *config,
GimpImage *image,
GError **error)
{
GOutputStream *output;
GString *string;
gimp_progress_init_printf (_("Exporting '%s'"),
gimp_file_get_utf8_name (file));
output = G_OUTPUT_STREAM (g_file_replace (file,
NULL, FALSE, G_FILE_CREATE_NONE,
NULL, error));
if (! output)
{
g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
_("Writing to file '%s' failed: %s"),
gimp_file_get_utf8_name (file), g_strerror (errno));
return FALSE;
}
string = svg_export_file (image, config, error);
if (! g_output_stream_write_all (output, string->str, string->len,
NULL, NULL, error))
{
GCancellable *cancellable = g_cancellable_new ();
if (error)
g_clear_error (error);
g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
_("Writing to file '%s' failed: %s"),
gimp_file_get_utf8_name (file), g_strerror (errno));
g_string_free (string, TRUE);
/* Cancel the overwrite initiated by g_file_replace(). */
g_cancellable_cancel (cancellable);
g_output_stream_close (output, cancellable, NULL);
g_object_unref (cancellable);
g_object_unref (output);
return FALSE;
}
g_string_free (string, TRUE);
g_object_unref (output);
return TRUE;
}
#if ! LIBRSVG_CHECK_VERSION(2, 46, 0)
/* XXX Why we keep old deprecated implementation next to newer librsvg API is
* because librsvg uses Rust since version 2.41.0. There are 2 raised problems:
* 1. Rust is still not available or well tested on every architecture/OS out
* there yet so unless we decide to have SVG support as optional again, it
* would make GIMP non-available on these.
* 2. There are some technical-ideological position against Rust for bundling
* and linking dependencies statically.
* While the second point may or may not be a problem we want to take into
* account (I guess that it mostly depends on the amount of additional
* maintenance work it would imply), the first is definitely enough of a reason
* to keep an old version requirement.
* See also report #6821.
*/
static void
load_set_size_callback (gint *width,
gint *height,
gpointer data)
{
SvgLoadVals *vals = data;
if (*width < 1 || *height < 1)
{
*width = SVG_DEFAULT_SIZE;
*height = SVG_DEFAULT_SIZE;
}
if (!vals->width || !vals->height)
return;
/* either both arguments negative or none */
if ((vals->width * vals->height) < 0)
return;
if (vals->width > 0)
{
*width = vals->width;
*height = vals->height;
}
else
{
gdouble w = *width;
gdouble h = *height;
gdouble aspect = (gdouble) vals->width / (gdouble) vals->height;
if (aspect > (w / h))
{
*height = abs (vals->height);
*width = (gdouble) abs (vals->width) * (w / h) + 0.5;
}
else
{
*width = abs (vals->width);
*height = (gdouble) abs (vals->height) / (w / h) + 0.5;
}
vals->width = *width;
vals->height = *height;
}
}
#endif /* ! LIBRSVG_CHECK_VERSION(2, 46, 0) */
/* This function renders a pixbuf from an SVG file according to vals. */
static GdkPixbuf *
load_rsvg_pixbuf (RsvgHandle *handle,
gint width,
gint height,
gdouble resolution,
GError **error)
{
GdkPixbuf *pixbuf = NULL;
#if LIBRSVG_CHECK_VERSION(2, 46, 0)
cairo_surface_t *surface = NULL;
cairo_t *cr = NULL;
RsvgRectangle viewport = { 0, };
guchar *src;
gint y;
/* Aspect-ratio breaking is only implemented with newer librsvg API. */
GimpVectorLoadData dimensions;
gdouble x_scale = 1.0;
gdouble y_scale = 1.0;
gdouble aspect_width = width;
gdouble aspect_height = height;
#else
SvgLoadVals vals;
#endif
g_return_val_if_fail (handle, NULL);
rsvg_handle_set_dpi (handle, resolution);
#if LIBRSVG_CHECK_VERSION(2, 46, 0)
if (svg_extract_dimensions (handle, &dimensions))
{
if (fabs ((gdouble) dimensions.width / dimensions.height - (gdouble) width / height) < EPSILON)
{
rsvg_handle_set_dpi (handle, resolution);
}
else if ((gdouble) dimensions.width / dimensions.height > (gdouble) width / height)
{
aspect_height = (gdouble) width * dimensions.height / dimensions.width;
/* XXX The X/Y resolution doesn't change a thing regarding
* rendering unfortunately so we still have to scale when the
* aspect ratio is not kept. I still set these, just in case.
*/
rsvg_handle_set_dpi_x_y (handle, resolution, aspect_height / height * resolution);
y_scale = height / aspect_height;
}
else
{
aspect_width = (gdouble) height * dimensions.width / dimensions.height;
rsvg_handle_set_dpi_x_y (handle, aspect_width / width * resolution, resolution);
x_scale = width / aspect_width;
}
}
surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
cr = cairo_create (surface);
viewport.width = (double) aspect_width;
viewport.height = (double) aspect_height;
if (x_scale != 1.0 || y_scale != 1.0)
cairo_scale (cr, x_scale, y_scale);
rsvg_handle_render_document (handle, cr, &viewport, NULL);
pixbuf = gdk_pixbuf_new_from_data (cairo_image_surface_get_data (surface),
GDK_COLORSPACE_RGB, TRUE, 8, width, height,
cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, width),
(GdkPixbufDestroyNotify) svg_destroy_surface, surface);
cairo_destroy (cr);
/* un-premultiply the data */
src = gdk_pixbuf_get_pixels (pixbuf);
for (y = 0; y < height; y++)
{
guchar *s = src;
gint w = width;
while (w--)
{
GIMP_CAIRO_ARGB32_GET_PIXEL (s, s[0], s[1], s[2], s[3]);
s += 4;
}
src += gdk_pixbuf_get_rowstride (pixbuf);
}
#else
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
vals.resolution = resolution;
vals.width = width;
vals.height = height;
rsvg_handle_set_size_callback (handle, load_set_size_callback, (gpointer) &vals, NULL);
pixbuf = rsvg_handle_get_pixbuf (handle);
#pragma GCC diagnostic pop
#endif
return pixbuf;
}
/* User interface */
static GimpSizeEntry *size = NULL;
static GtkAdjustment *xadj = NULL;
static GtkAdjustment *yadj = NULL;
static GtkWidget *constrain = NULL;
static gdouble ratio_x = 1.0;
static gdouble ratio_y = 1.0;
static gint svg_width = 0;
static gint svg_height = 0;
static void load_dialog_set_ratio (gdouble x,
gdouble y);
static void
load_dialog_size_callback (GtkWidget *widget,
gpointer data)
{
if (gimp_chain_button_get_active (GIMP_CHAIN_BUTTON (constrain)))
{
gdouble x = gimp_size_entry_get_refval (size, 0) / (gdouble) svg_width;
gdouble y = gimp_size_entry_get_refval (size, 1) / (gdouble) svg_height;
if (x != ratio_x)
{
load_dialog_set_ratio (x, x);
}
else if (y != ratio_y)
{
load_dialog_set_ratio (y, y);
}
}
}
static void
load_dialog_ratio_callback (GtkAdjustment *adj,
gpointer data)
{
gdouble x = gtk_adjustment_get_value (xadj);
gdouble y = gtk_adjustment_get_value (yadj);
if (gimp_chain_button_get_active (GIMP_CHAIN_BUTTON (constrain)))
{
if (x != ratio_x)
y = x;
else
x = y;
}
load_dialog_set_ratio (x, y);
}
static void
load_dialog_set_ratio (gdouble x,
gdouble y)
{
ratio_x = x;
ratio_y = y;
g_signal_handlers_block_by_func (size, load_dialog_size_callback, NULL);
gimp_size_entry_set_refval (size, 0, svg_width * x);
gimp_size_entry_set_refval (size, 1, svg_height * y);
g_signal_handlers_unblock_by_func (size, load_dialog_size_callback, NULL);
g_signal_handlers_block_by_func (xadj, load_dialog_ratio_callback, NULL);
g_signal_handlers_block_by_func (yadj, load_dialog_ratio_callback, NULL);
gtk_adjustment_set_value (xadj, x);
gtk_adjustment_set_value (yadj, y);
g_signal_handlers_unblock_by_func (xadj, load_dialog_ratio_callback, NULL);
g_signal_handlers_unblock_by_func (yadj, load_dialog_ratio_callback, NULL);
}
static GimpPDBStatusType
load_dialog (GFile *file,
RsvgHandle *handle,
GimpProcedure *procedure,
GimpProcedureConfig *config,
GimpVectorLoadData extracted_data,
GError **load_error)
{
GtkWidget *dialog;
gboolean run;
gimp_ui_init (PLUG_IN_BINARY);
dialog = gimp_vector_load_procedure_dialog_new (GIMP_VECTOR_LOAD_PROCEDURE (procedure),
GIMP_PROCEDURE_CONFIG (config),
&extracted_data, file);
gimp_procedure_dialog_fill (GIMP_PROCEDURE_DIALOG (dialog), NULL);
run = gimp_procedure_dialog_run (GIMP_PROCEDURE_DIALOG (dialog));
gtk_widget_destroy (dialog);
return run ? GIMP_PDB_SUCCESS : GIMP_PDB_CANCEL;
}
static gboolean
export_dialog (GimpImage *image,
GimpProcedure *procedure,
GObject *config)
{
GtkWidget *dialog;
GtkWidget *box;
GtkWidget *hint = NULL;
gboolean run;
gboolean has_nonstandard_links = FALSE;
dialog = gimp_export_procedure_dialog_new (GIMP_EXPORT_PROCEDURE (procedure),
GIMP_PROCEDURE_CONFIG (config),
image);
box = gimp_procedure_dialog_fill_box (GIMP_PROCEDURE_DIALOG (dialog),
"svg-box", "title",
"raster-export-format", NULL);
has_nonstandard_links = has_invalid_links (gimp_image_get_layers (image),
NULL);
if (has_nonstandard_links)
{
hint =
g_object_new (GIMP_TYPE_HINT_BOX,
"icon-name", GIMP_ICON_DIALOG_WARNING,
"hint", _("The SVG format only requires viewers to "
"display image links for SVG, PNG, and "
"JPEG images.\n"
"Your image links external image files "
"in other formats which may not show up "
"in all viewers."),
NULL);
gtk_widget_set_visible (hint, TRUE);
gtk_widget_set_margin_bottom (hint, 12);
gtk_box_pack_start (GTK_BOX (box), hint, FALSE, FALSE, 0);
gtk_box_reorder_child (GTK_BOX (box), hint, 0);
}
gimp_procedure_dialog_fill (GIMP_PROCEDURE_DIALOG (dialog), "svg-box", NULL);
run = gimp_procedure_dialog_run (GIMP_PROCEDURE_DIALOG (dialog));
gtk_widget_destroy (dialog);
return run;
}
static gboolean
has_invalid_links (GimpLayer **layers,
GimpGroupLayer *group)
{
GimpItem **temp_layers = (GimpItem **) layers;
gint32 n_layers;
if (layers == NULL)
temp_layers = gimp_item_get_children (GIMP_ITEM (group));
n_layers = gimp_core_object_array_get_length ((GObject **) temp_layers);
for (gint i = 0; i < n_layers; i++)
{
if (gimp_item_is_link_layer (temp_layers[i]))
{
gchar *mimetype =
gimp_link_layer_get_mime_type (GIMP_LINK_LAYER (temp_layers[i]));
/* The SVG specification only requires viewers to support
SVG, PNG, and JPEG links. If the linked layer is not one of
those types, we'll notify the user */
if (g_strcmp0 (mimetype, "image/svg+xml") != 0 &&
g_strcmp0 (mimetype, "image/png") != 0 &&
g_strcmp0 (mimetype, "image/jpeg") != 0)
return TRUE;
}
else if (gimp_item_is_group (GIMP_ITEM (temp_layers[i])))
{
if (has_invalid_links (NULL, GIMP_GROUP_LAYER (temp_layers[i])))
return TRUE;
}
}
return FALSE;
}
#if LIBRSVG_CHECK_VERSION(2, 46, 0)
static GimpUnit *
svg_rsvg_to_gimp_unit (RsvgUnit unit)
{
switch (unit)
{
case RSVG_UNIT_PERCENT:
return gimp_unit_percent ();
case RSVG_UNIT_PX:
return gimp_unit_pixel ();
case RSVG_UNIT_IN:
return gimp_unit_inch ();
case RSVG_UNIT_MM:
return gimp_unit_mm ();
case RSVG_UNIT_PT:
return gimp_unit_point ();
case RSVG_UNIT_PC:
return gimp_unit_pica ();
case RSVG_UNIT_CM:
case RSVG_UNIT_EM:
case RSVG_UNIT_EX:
/*case RSVG_UNIT_CH:*/
default:
return NULL;
}
}
static void
svg_destroy_surface (guchar *pixels,
cairo_surface_t *surface)
{
cairo_surface_destroy (surface);
}
#endif
/* Taken from app/path/gimppath-export.c */
static GString *
svg_export_file (GimpImage *image,
GimpProcedureConfig *config,
GError **error)
{
GimpLayer **layers;
gchar *title = NULL;
GString *str = g_string_new (NULL);
GList *exported_res = NULL;
gint layer_ids[6] = { 0 };
g_object_get (config, "title", &title, NULL);
g_string_append_printf (str,
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
"<svg xmlns=\"http://www.w3.org/2000/svg\"\n"
" xmlns:svg=\"http://www.w3.org/2000/svg\"\n"
" version=\"1.1\"\n");
g_string_append (str, " ");
svg_export_image_size (image, str);
g_string_append_c (str, '\n');
g_string_append_printf (str,
" viewBox=\"0 0 %d %d\">\n",
gimp_image_get_width (image),
gimp_image_get_height (image));
/* Optional SVG title */
if (title && strlen (title))
g_string_append_printf (str,
" <title>%s</title>\n",
title);
layers = gimp_image_get_layers (image);
g_string_append (str, " <defs>\n");
svg_export_defs ((GimpItem **) layers, NULL, str, config, &exported_res, error);
g_list_free (exported_res);
g_string_append (str, " </defs>\n");
svg_export_layers ((GimpItem **) layers, NULL, layer_ids, str, config, "", error);
g_free (layers);
g_string_append (str, "</svg>\n");
g_free (title);
return str;
}
static void
svg_export_defs (GimpItem **layers,
GimpGroupLayer *group,
GString *str,
GimpProcedureConfig *config,
GList **exported_res,
GError **error)
{
GimpItem **items;
gint32 n_layers;
if (group)
items = gimp_item_get_children (GIMP_ITEM (group));
else
items = layers;
n_layers = gimp_core_object_array_get_length ((GObject **) items);
for (gint i = n_layers - 1; i >= 0; i--)
{
if (gimp_item_get_visible (items[i]))
{
if (gimp_item_is_group (items[i]))
svg_export_defs (NULL, GIMP_GROUP_LAYER (items[i]), str, config, exported_res, error);
else if (GIMP_IS_VECTOR_LAYER (items[i]) &&
! gimp_rasterizable_is_rasterized (GIMP_RASTERIZABLE (items[i])))
svg_export_def (GIMP_VECTOR_LAYER (items[i]), str, config, exported_res, error);
}
gimp_progress_update ((gdouble) (n_layers - i) / n_layers);
if (*error)
break;
}
if (group)
g_free (items);
gimp_progress_update (1.0);
}
static void
svg_export_def (GimpVectorLayer *layer,
GString *str,
GimpProcedureConfig *config,
GList **exported_res,
GError **error)
{
gchar *fill_string;
gchar *stroke_string;
fill_string = svg_get_def_string (layer, "fill", config, exported_res, error);
stroke_string = svg_get_def_string (layer, "stroke", config, exported_res, error);
g_string_append_printf (str,
"%s%s",
fill_string ? fill_string : "",
stroke_string ? stroke_string : "");
g_free (fill_string);
g_free (stroke_string);
}
static gchar *
svg_get_def_string (GimpVectorLayer *layer,
const gchar *type,
GimpProcedureConfig *config,
GList **exported_res,
GError **error)
{
GimpPattern *pattern = NULL;
gboolean enabled;
if (g_strcmp0 ("fill", type) == 0)
{
enabled = gimp_vector_layer_get_enable_fill (layer);
if (enabled)
pattern = gimp_vector_layer_get_fill_pattern (layer);
}
else
{
enabled = gimp_vector_layer_get_enable_stroke (layer);
if (enabled)
pattern = gimp_vector_layer_get_stroke_pattern (layer);
}
if (enabled && pattern && ! g_list_find (*exported_res, pattern))
{
gchar *def_string = NULL;
GFile *temp_file = NULL;
FILE *temp_fp;
gsize temp_size;
GimpProcedure *procedure;
GimpValueArray *return_vals;
GimpImage *temp_image;
GimpLayer *temp_layer;
const gchar *mimetype;
gint width;
gint height;
gint format_id;
GeglBuffer *src_buffer;
GeglBuffer *dst_buffer;
gint res_id;
res_id = gimp_resource_get_id (GIMP_RESOURCE (pattern));
/* XXX Should we try to output in high-bit depth when possible? */
src_buffer = gimp_pattern_get_buffer (pattern, 0, 0, babl_format ("R'G'B' u8"));
width = gegl_buffer_get_width (src_buffer);
height = gegl_buffer_get_height (src_buffer);
temp_image = gimp_image_new (width, height, GIMP_RGB);
temp_layer = gimp_layer_new (temp_image, NULL, width, height, GIMP_RGBA_IMAGE,
100.0, GIMP_LAYER_MODE_NORMAL);
gimp_image_insert_layer (temp_image, temp_layer, NULL, 0);
dst_buffer = gimp_drawable_get_buffer (GIMP_DRAWABLE (temp_layer));
gegl_buffer_copy (src_buffer, NULL, GEGL_ABYSS_NONE, dst_buffer, NULL);
g_object_unref (src_buffer);
g_object_unref (dst_buffer);
format_id = gimp_procedure_config_get_choice_id (GIMP_PROCEDURE_CONFIG (config),
"raster-export-format");
if (format_id == EXPORT_FORMAT_PNG)
{
temp_file = gimp_temp_file ("png");
mimetype = "image/png";
procedure = gimp_pdb_lookup_procedure (gimp_get_pdb (), "file-png-export");
return_vals = gimp_procedure_run (procedure,
"run-mode", GIMP_RUN_NONINTERACTIVE,
"image", temp_image,
"file", temp_file,
"interlaced", FALSE,
"compression", 9,
"bkgd", FALSE,
"offs", FALSE,
"phys", FALSE,
"time", FALSE,
"save-transparent", FALSE,
"optimize-palette", FALSE,
"include-color-profile", FALSE,
NULL);
}
else
{
temp_file = gimp_temp_file ("jpeg");
mimetype = "image/jpeg";
procedure = gimp_pdb_lookup_procedure (gimp_get_pdb (), "file-jpeg-export");
return_vals = gimp_procedure_run (procedure,
"run-mode", GIMP_RUN_NONINTERACTIVE,
"image", temp_image,
"file", temp_file,
"quality", 0.9f,
"cmyk", FALSE,
"include-color-profile", FALSE,
NULL);
}
gimp_image_delete (temp_image);
if (GIMP_VALUES_GET_ENUM (return_vals, 0) != GIMP_PDB_SUCCESS)
{
if (! error)
{
if (G_VALUE_HOLDS_STRING (gimp_value_array_index (return_vals, 1)))
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
"SVG: Unable to export pattern image data: %s",
GIMP_VALUES_GET_STRING (return_vals, 1));
else
g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
"SVG: Unable to export pattern image data.");
}
g_object_unref (temp_file);
return NULL;
}
temp_fp = g_fopen (g_file_peek_path (temp_file), "rb");
fseek (temp_fp, 0L, SEEK_END);
temp_size = ftell (temp_fp);
fseek (temp_fp, 0L, SEEK_SET);
if (temp_size > 0)
{
guchar *buf;
gchar *encoded;
buf = g_malloc0 (temp_size);
fread (buf, 1, temp_size, temp_fp);
encoded = g_base64_encode ((const guchar *) buf, temp_size + 1);
def_string = g_strdup_printf (" <pattern id=\"Pattern%d\"\n"
" patternUnits=\"userSpaceOnUse\"\n"
" width=\"%d\"\n"
" height=\"%d\">\n"
" <image href=\"data:%s;base64,%s\" />\n"
" </pattern>\n",
res_id, width, height, mimetype, encoded);
g_free (encoded);
g_free (buf);
}
fclose (temp_fp);
g_file_delete (temp_file, NULL, NULL);
g_object_unref (temp_file);
*exported_res = g_list_prepend (*exported_res, pattern);
return def_string;
}
return NULL;
}
static void
svg_export_layers (GimpItem **layers,
GimpGroupLayer *group,
gint *layer_ids,
GString *str,
GimpProcedureConfig *config,
gchar *spacing,
GError **error)
{
GimpItem **items;
gint32 n_layers;
gchar *extra_spacing;
gint format_id;
if (group)
items = gimp_item_get_children (GIMP_ITEM (group));
else
items = layers;
format_id = gimp_procedure_config_get_choice_id (GIMP_PROCEDURE_CONFIG (config),
"raster-export-format");
extra_spacing = g_strdup_printf ("%s ", spacing);
n_layers = gimp_core_object_array_get_length ((GObject **) items);
for (gint i = n_layers - 1; i >= 0; i--)
{
if (gimp_item_get_visible (items[i]))
{
if (gimp_item_is_group (items[i]))
{
svg_export_group_header (GIMP_GROUP_LAYER (items[i]),
layer_ids[LAYER_ID_GROUP]++, str,
extra_spacing);
svg_export_layers (NULL, GIMP_GROUP_LAYER (items[i]), layer_ids,
str, config, extra_spacing, error);
g_string_append_printf (str, "%s</g>\n", extra_spacing);
}
else if (GIMP_IS_RASTERIZABLE (items[i]) &&
! gimp_rasterizable_is_rasterized (GIMP_RASTERIZABLE (items[i])))
{
if (GIMP_IS_VECTOR_LAYER (items[i]))
{
GimpVectorLayer *layer = GIMP_VECTOR_LAYER (items[i]);
svg_export_path (layer, layer_ids[LAYER_ID_VECTOR]++, str,
extra_spacing);
}
else if (GIMP_IS_TEXT_LAYER (items[i]))
{
GimpTextLayer *layer = GIMP_TEXT_LAYER (items[i]);
svg_export_text (layer, layer_ids[LAYER_ID_TEXT]++, str,
extra_spacing);
}
else if (GIMP_IS_LINK_LAYER (items[i]))
{
GimpLinkLayer *layer = GIMP_LINK_LAYER (items[i]);
svg_export_link_layer (layer, layer_ids[LAYER_ID_LINK]++, str,
extra_spacing);
}
}
else if (format_id != EXPORT_FORMAT_NONE)
{
svg_export_raster (GIMP_DRAWABLE (items[i]), layer_ids, str,
format_id, extra_spacing, error);
}
}
gimp_progress_update ((gdouble) (n_layers - i) / n_layers);
}
g_free (extra_spacing);
if (group)
g_free (items);
gimp_progress_update (1.0);
}
static void
svg_export_group_header (GimpGroupLayer *group,
gint group_id,
GString *str,
gchar *spacing)
{
gchar *name;
gdouble opacity;
name = g_strdup_printf ("group%d", group_id);
opacity = gimp_layer_get_opacity (GIMP_LAYER (group)) / 100.0f;
g_string_append_printf (str, "%s<g id=\"%s\" opacity=\"%f\">\n",
spacing, name, opacity);
g_free (name);
}
static void
svg_export_image_size (GimpImage *image,
GString *str)
{
GimpUnit *unit;
const gchar *abbrev;
gchar wbuf[G_ASCII_DTOSTR_BUF_SIZE];
gchar hbuf[G_ASCII_DTOSTR_BUF_SIZE];
gdouble xres;
gdouble yres;
gdouble w, h;
gimp_image_get_resolution (image, &xres, &yres);
w = (gdouble) gimp_image_get_width (image) / xres;
h = (gdouble) gimp_image_get_height (image) / yres;
/* FIXME: should probably use the display unit here */
unit = gimp_image_get_unit (image);
switch (gimp_unit_get_id (unit))
{
case GIMP_UNIT_INCH: abbrev = "in"; break;
case GIMP_UNIT_MM: abbrev = "mm"; break;
case GIMP_UNIT_POINT: abbrev = "pt"; break;
case GIMP_UNIT_PICA: abbrev = "pc"; break;
default: abbrev = "cm";
unit = gimp_unit_mm ();
w /= 10.0;
h /= 10.0;
break;
}
g_ascii_formatd (wbuf, sizeof (wbuf), "%g", w * gimp_unit_get_factor (unit));
g_ascii_formatd (hbuf, sizeof (hbuf), "%g", h * gimp_unit_get_factor (unit));
g_string_append_printf (str,
"width=\"%s%s\" height=\"%s%s\"",
wbuf, abbrev, hbuf, abbrev);
}
static void
svg_export_path (GimpVectorLayer *layer,
gint vector_id,
GString *str,
gchar *spacing)
{
GimpPath *path;
gchar *name;
gchar *data;
gdouble opacity;
gchar *fill_string;
gchar *stroke_string;
gdouble stroke_width;
const gchar *stroke_capstyle;
const gchar *stroke_joinstyle;
gdouble stroke_miter_limit;
gdouble stroke_dash_offset;
gsize num_dashes;
gdouble *stroke_dash_pattern;
path = gimp_vector_layer_get_path (layer);
if (! path)
return;
name = g_strdup_printf ("vector%d", vector_id);
data = svg_export_path_data (path);
opacity = gimp_layer_get_opacity (GIMP_LAYER (layer)) / 100.0f;
/* Vector Layer Properties */
fill_string = svg_get_color_string (layer, "fill");
stroke_string = svg_get_color_string (layer, "stroke");
stroke_width = gimp_vector_layer_get_stroke_width (layer);
stroke_miter_limit = gimp_vector_layer_get_stroke_miter_limit (layer);
stroke_dash_offset = gimp_vector_layer_get_stroke_dash_offset (layer);
gimp_vector_layer_get_stroke_dash_pattern (layer, &num_dashes,
&stroke_dash_pattern);
switch (gimp_vector_layer_get_stroke_cap_style (layer))
{
case GIMP_CAP_ROUND:
stroke_capstyle = "round";
break;
case GIMP_CAP_SQUARE:
stroke_capstyle = "square";
break;
default:
stroke_capstyle = "butt";
break;
}
switch (gimp_vector_layer_get_stroke_join_style (layer))
{
case GIMP_JOIN_ROUND:
stroke_joinstyle = "round";
break;
case GIMP_JOIN_BEVEL:
stroke_joinstyle = "bevel";
break;
default:
stroke_joinstyle = "miter";
break;
}
g_string_append_printf (str,
"%s<path id=\"%s\"\n"
"%s %s\n"
"%s %s\n"
"%s opacity=\"%f\"\n"
"%s stroke-width=\"%f\"\n"
"%s stroke-linecap=\"%s\"\n"
"%s stroke-linejoin=\"%s\"\n"
"%s stroke-miterlimit=\"%f\"\n",
spacing, name,
spacing, fill_string,
spacing, stroke_string,
spacing, opacity,
spacing, stroke_width,
spacing, stroke_capstyle,
spacing, stroke_joinstyle,
spacing, stroke_miter_limit);
if (num_dashes > 0)
{
for (gint i = 0; i < num_dashes; i++)
stroke_dash_pattern[i] *= stroke_width;
g_string_append_printf (str,
"%s stroke-dashoffset=\"%f\"\n"
"%s stroke-dasharray=\"%f",
spacing, stroke_dash_offset,
spacing, stroke_dash_pattern[0]);
for (gint i = 0; i < num_dashes; i++)
g_string_append_printf (str, " %f", stroke_dash_pattern[i]);
g_string_append_printf (str, "\"\n");
}
g_string_append_printf (str,
"%s d=\"%s\" />\n",
spacing, data);
g_free (fill_string);
g_free (stroke_string);
g_free (name);
g_free (data);
}
#define NEWLINE "\n "
static gchar *
svg_export_path_data (GimpPath *path)
{
GString *str;
gsize num_strokes;
gint *strokes;
gint s;
gchar x_string[G_ASCII_DTOSTR_BUF_SIZE];
gchar y_string[G_ASCII_DTOSTR_BUF_SIZE];
gboolean closed = FALSE;
str = g_string_new (NULL);
strokes = gimp_path_get_strokes (path, &num_strokes);
for (s = 0; s < num_strokes; s++)
{
GimpPathStrokeType type;
gdouble *control_points;
gsize num_points;
gsize num_indexes;
type = gimp_path_stroke_get_points (path, strokes[s], &num_points,
&control_points, &closed);
if (type == GIMP_PATH_STROKE_TYPE_BEZIER)
{
num_indexes = num_points / 2;
if (num_indexes >= 3)
{
g_ascii_formatd (x_string, G_ASCII_DTOSTR_BUF_SIZE,
"%.2f", control_points[2]);
g_ascii_formatd (y_string, G_ASCII_DTOSTR_BUF_SIZE,
"%.2f", control_points[3]);
if (s > 0)
g_string_append_printf (str, NEWLINE);
g_string_append_printf (str, "M %s,%s", x_string, y_string);
}
if (num_indexes > 3)
g_string_append_printf (str, NEWLINE "C");
for (gint i = 2; i < (num_indexes + (closed ? 2 : - 1)); i++)
{
gint index = (i % num_indexes) * 2;
if (i > 2 && i % 3 == 2)
g_string_append_printf (str, NEWLINE " ");
g_ascii_formatd (x_string, G_ASCII_DTOSTR_BUF_SIZE,
"%.2f", control_points[index]);
g_ascii_formatd (y_string, G_ASCII_DTOSTR_BUF_SIZE,
"%.2f", control_points[index + 1]);
g_string_append_printf (str, " %s,%s", x_string, y_string);
}
if (closed && num_points > 3)
g_string_append_printf (str, " Z");
}
}
return g_strchomp (g_string_free (str, FALSE));
}
static void
svg_export_text (GimpTextLayer *layer,
gint text_id,
GString *str,
gchar *spacing)
{
gchar *name;
gint x = 0;
gint y = 0;
gdouble x_res = 0;
gdouble y_res = 0;
gdouble opacity;
gchar *hex_color;
gdouble font_size;
GimpUnit *unit;
GimpFont *gimp_font;
PangoFontDescription *font_description;
PangoFontDescription *font_description2;
PangoFontMap *fontmap;
PangoFont *font;
PangoContext *context;
name = g_strdup_printf ("text%d", text_id);
gimp_drawable_get_offsets (GIMP_DRAWABLE (layer), &x, &y);
gimp_image_get_resolution (gimp_item_get_image (GIMP_ITEM (layer)),
&x_res, &y_res);
opacity = gimp_layer_get_opacity (GIMP_LAYER (layer)) / 100.0f;
hex_color = svg_get_hex_color (gimp_text_layer_get_color (layer));
g_string_append_printf (str,
"%s<text id=\"%s\"\n"
"%s x=\"%d\"\n"
"%s y=\"%d\"\n"
"%s opacity=\"%f\"\n"
"%s fill=\"%s\"\n",
spacing, name,
spacing, x,
spacing, y,
spacing, opacity,
spacing, hex_color);
g_free (hex_color);
g_free (name);
/* Font style */
font_size = gimp_text_layer_get_font_size (layer, &unit);
font_size = gimp_units_to_pixels (font_size, unit, y_res);
fontmap = pango_cairo_font_map_new_for_font_type (CAIRO_FONT_TYPE_FT);
context = pango_font_map_create_context (fontmap);
gimp_font = gimp_text_layer_get_font (layer);
font_description = gimp_font_get_pango_font_description (gimp_font);
font = pango_font_map_load_font (fontmap, context,
font_description);
font_description2 = pango_font_describe (font);
g_string_append_printf (str,
"%s style=\"font-size:%fpx; "
"font-family:%s; font-weight:%d; "
"letter-spacing:%fpx;\">",
spacing, font_size,
pango_font_description_get_family (font_description2),
pango_font_description_get_weight (font_description2),
gimp_text_layer_get_letter_spacing (layer));
g_object_unref (font);
pango_font_description_free (font_description);
pango_font_description_free (font_description2);
g_object_unref (context);
g_object_unref (fontmap);
/* Text */
if (gimp_text_layer_get_markup (layer))
{
GString *markup;
markup = g_string_new (gimp_text_layer_get_markup (layer));
g_string_replace (markup, "<markup>", "", 1);
g_string_replace (markup, "</markup>", "", 1);
g_string_replace (markup, "<b>",
"<tspan style=\"font-weight: bold\">", 0);
g_string_replace (markup, "</b>", "</tspan>", 0);
g_string_replace (markup, "<i>",
"<tspan style=\"font-style: italic\">", 0);
g_string_replace (markup, "</i>", "</tspan>", 0);
g_string_replace (markup, "<u>",
"<tspan style=\"text-decoration: underline\">", 0);
g_string_replace (markup, "</u>", "</tspan>", 0);
g_string_replace (markup, "<s>",
"<tspan style=\"text-decoration: linethrough\">", 0);
g_string_replace (markup, "</s>", "</tspan>", 0);
g_string_replace (markup, "<span foreground=\"", "<tspan style=\"fill:", 0);
g_string_replace (markup, "<span font=\"", "<tspan style=\"font-family:", 0);
g_string_replace (markup, "<span ", "<tspan ", 0);
g_string_replace (markup, "</span>", "</tspan>", 0);
svg_export_text_lines (layer, str, markup->str);
g_string_free (markup, TRUE);
}
else
{
svg_export_text_lines (layer, str, gimp_text_layer_get_text (layer));
}
g_string_append_printf (str, "</text>\n");
}
static void
svg_export_text_lines (GimpTextLayer *layer,
GString *str,
gchar *text)
{
gint x = 0;
gint y = 0;
gdouble x_res = 0;
gdouble y_res = 0;
gdouble font_size;
GimpUnit *unit;
gdouble line_spacing;
gchar **lines;
guint count;
gimp_drawable_get_offsets (GIMP_DRAWABLE (layer), &x, &y);
gimp_image_get_resolution (gimp_item_get_image (GIMP_ITEM (layer)),
&x_res, &y_res);
line_spacing = gimp_text_layer_get_line_spacing (layer);
font_size = gimp_text_layer_get_font_size (layer, &unit);
font_size = gimp_units_to_pixels (font_size, unit, y_res);
lines = g_strsplit (text, "\n", -1);
count = g_strv_length (lines);
for (gint i = 0; i < count; i++)
{
g_string_append_printf (str, "<tspan x=\"%d\" y=\"%d\">"
"%s</tspan>",
x, y, lines[i]);
y += font_size + line_spacing;
}
g_strfreev (lines);
}
static void
svg_export_link_layer (GimpLinkLayer *layer,
gint link_id,
GString *str,
gchar *spacing)
{
GFile *file;
gchar *name;
gchar *path;
gint width;
gint height;
gdouble opacity;
gint x;
gint y;
width = gimp_drawable_get_width (GIMP_DRAWABLE (layer));
height = gimp_drawable_get_height (GIMP_DRAWABLE (layer));
opacity = gimp_layer_get_opacity (GIMP_LAYER (layer)) / 100.0f;
gimp_drawable_get_offsets (GIMP_DRAWABLE (layer), &x, &y);
name = g_strdup_printf ("link%d", link_id);
file = gimp_link_layer_get_file (layer);
path = g_file_get_path (file);
g_string_append_printf (str,
"%s<image id=\"%s\"\n"
"%s x=\"%d\"\n"
"%s y=\"%d\"\n"
"%s width=\"%d\"\n"
"%s height=\"%d\"\n"
"%s opacity=\"%f\"\n"
"%s href=\"%s\" />\n",
spacing, name,
spacing, x,
spacing, y,
spacing, width,
spacing, height,
spacing, opacity,
spacing, path);
g_free (name);
g_free (path);
}
static void
svg_export_raster (GimpDrawable *layer,
gint *layer_ids,
GString *str,
gint format_id,
gchar *spacing,
GError **error)
{
GimpProcedure *procedure;
GimpValueArray *return_vals = NULL;
GimpImage *image;
GimpImage *temp_image;
GimpLayer *temp_layer;
GFile *temp_file = NULL;
FILE *temp_fp;
gsize temp_size;
gint raster_id;
gint width;
gint height;
gdouble opacity;
gboolean include_color_profile;
gboolean has_layer_mask;
const gchar *mimetype;
has_layer_mask = FALSE;
if (GIMP_IS_LAYER (layer) && gimp_layer_get_mask (GIMP_LAYER (layer)))
{
svg_export_layer_mask (gimp_layer_get_mask (GIMP_LAYER (layer)),
layer_ids, str, spacing, error);
has_layer_mask = TRUE;
}
if (format_id == EXPORT_FORMAT_JPEG && svg_drawable_has_transparency (layer))
format_id = EXPORT_FORMAT_PNG;
if (format_id == EXPORT_FORMAT_PNG)
{
temp_file = gimp_temp_file ("png");
mimetype = "image/png";
}
else
{
temp_file = gimp_temp_file ("jpeg");
mimetype = "image/jpeg";
}
raster_id = layer_ids[LAYER_ID_RASTER]++;
width = gimp_drawable_get_width (layer);
height = gimp_drawable_get_height (layer);
if (GIMP_IS_LAYER (layer))
opacity = gimp_layer_get_opacity (GIMP_LAYER (layer)) / 100.0f;
else
opacity = gimp_channel_get_opacity (GIMP_CHANNEL (layer)) / 100.0f;
image = gimp_item_get_image (GIMP_ITEM (layer));
temp_image = gimp_image_new (width, height,
gimp_image_get_base_type (image));
if (gimp_image_get_base_type (image) == GIMP_INDEXED)
gimp_image_set_palette (temp_image,
gimp_image_get_palette (image));
temp_layer = gimp_layer_new_from_drawable (layer, temp_image);
gimp_image_insert_layer (temp_image, temp_layer, NULL, 0);
gimp_layer_set_offsets (temp_layer, 0, 0);
/* Set layer to full opacity and use opacity attribute instead */
gimp_layer_set_opacity (temp_layer, 100.0f);
include_color_profile = FALSE;
if (gimp_image_get_color_profile (image))
{
GimpColorProfile *profile;
profile = gimp_image_get_color_profile (image);
gimp_image_set_color_profile (temp_image, profile);
include_color_profile = TRUE;
}
if (format_id == EXPORT_FORMAT_PNG)
{
procedure = gimp_pdb_lookup_procedure (gimp_get_pdb (), "file-png-export");
return_vals = gimp_procedure_run (procedure,
"run-mode", GIMP_RUN_NONINTERACTIVE,
"image", temp_image,
"file", temp_file,
"interlaced", FALSE,
"compression", 9,
"bkgd", FALSE,
"offs", FALSE,
"phys", FALSE,
"time", FALSE,
"save-transparent", FALSE,
"optimize-palette", FALSE,
"include-color-profile", include_color_profile,
NULL);
}
else
{
procedure = gimp_pdb_lookup_procedure (gimp_get_pdb (), "file-jpeg-export");
return_vals = gimp_procedure_run (procedure,
"run-mode", GIMP_RUN_NONINTERACTIVE,
"image", temp_image,
"file", temp_file,
"quality", 0.9f,
"cmyk", FALSE,
"include-color-profile", include_color_profile,
NULL);
}
gimp_image_delete (temp_image);
if (GIMP_VALUES_GET_ENUM (return_vals, 0) != GIMP_PDB_SUCCESS)
{
if (! error)
g_set_error (error, G_FILE_ERROR, 0,
"SVG: Unable to export raster layer.");
return;
}
temp_fp = g_fopen (g_file_peek_path (temp_file), "rb");
fseek (temp_fp, 0L, SEEK_END);
temp_size = ftell (temp_fp);
fseek (temp_fp, 0L, SEEK_SET);
if (temp_size > 0)
{
guchar *buf;
gchar *encoded;
gchar *name;
gint x;
gint y;
buf = g_malloc0 (temp_size);
fread (buf, 1, temp_size, temp_fp);
encoded = g_base64_encode ((const guchar *) buf, temp_size + 1);
name = g_strdup_printf ("raster%d", raster_id);
gimp_drawable_get_offsets (GIMP_DRAWABLE (layer), &x, &y);
g_string_append_printf (str,
"%s<image id=\"%s\"\n"
"%s x=\"%d\"\n"
"%s y=\"%d\"\n"
"%s width=\"%d\"\n"
"%s height=\"%d\"\n"
"%s opacity=\"%f\"\n",
spacing, name,
spacing, x,
spacing, y,
spacing, width,
spacing, height,
spacing, opacity);
if (has_layer_mask)
g_string_append_printf (str,
"%s mask=\"url(#mask%d)\"\n",
spacing, layer_ids[LAYER_ID_MASK]);
g_string_append_printf (str,
"%s href=\"data:%s;base64,%s\" />\n",
spacing, mimetype, encoded);
g_free (encoded);
g_free (name);
g_free (buf);
}
fclose (temp_fp);
g_file_delete (temp_file, NULL, NULL);
g_object_unref (temp_file);
}
static void
svg_export_layer_mask (GimpLayerMask *mask,
gint *layer_ids,
GString *str,
gchar *spacing,
GError **error)
{
gchar *name;
gchar *extra_spacing;
gint mask_id;
gint x = 0;
gint y = 0;
mask_id = layer_ids[LAYER_ID_MASK]++;
name = g_strdup_printf ("mask%d", mask_id);
gimp_drawable_get_offsets (GIMP_DRAWABLE (mask), &x, &y);
g_string_append_printf (str,
"%s<mask id=\"%s\"\n"
"%s x=\"%d\"\n"
"%s y=\"%d\"\n"
"%s mask-type=\"luminance\">\n",
spacing, name,
spacing, x,
spacing, y,
spacing);
g_free (name);
extra_spacing = g_strdup_printf ("%s ", spacing);
svg_export_raster (GIMP_DRAWABLE (mask), layer_ids, str, EXPORT_FORMAT_PNG,
extra_spacing, error);
g_free (extra_spacing);
g_string_append_printf (str, "</mask>\n");
}
static gchar *
svg_get_color_string (GimpVectorLayer *layer,
const gchar *type)
{
GeglColor *color = NULL;
GimpPattern *pattern = NULL;
gboolean enabled = TRUE;
if (g_strcmp0 ("fill", type) == 0)
{
color = gimp_vector_layer_get_fill_color (layer);
if (color == NULL)
pattern = gimp_vector_layer_get_fill_pattern (layer);
enabled = gimp_vector_layer_get_enable_fill (layer);
}
else
{
color = gimp_vector_layer_get_stroke_color (layer);
if (color == NULL)
pattern = gimp_vector_layer_get_stroke_pattern (layer);
enabled = gimp_vector_layer_get_enable_stroke (layer);
}
if (enabled)
{
if (color)
{
gchar *hex_color;
gchar *color_string;
hex_color = svg_get_hex_color (color);
color_string = g_strdup_printf ("%s=\"%s\"",
type, hex_color);
g_free (hex_color);
g_clear_object (&color);
return color_string;
}
else if (pattern)
{
gchar *color_string;
gint res_id = gimp_resource_get_id (GIMP_RESOURCE (pattern));
color_string = g_strdup_printf ("%s=\"url(#Pattern%d)\"",
type, res_id);
return color_string;
}
}
g_clear_object (&color);
return g_strdup_printf ("%s=\"none\"", type);
}
static gchar *
svg_get_hex_color (GeglColor *color)
{
guchar rgb[3] = { 0, 0, 0 };
gchar *hex_color = NULL;
gegl_color_get_pixel (color, babl_format ("R'G'B' u8"), rgb);
hex_color = g_strdup_printf ("#%02X%02X%02X", rgb[0], rgb[1], rgb[2]);
return hex_color;
}
static gboolean
svg_drawable_has_transparency (GimpDrawable *drawable)
{
if (gimp_drawable_has_alpha (drawable))
{
GeglBuffer *buffer;
GeglBufferIterator *iter;
buffer = gimp_drawable_get_buffer (drawable);
iter = gegl_buffer_iterator_new (buffer, NULL, 0, babl_format ("A float"),
GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 1);
while (gegl_buffer_iterator_next (iter))
{
gfloat *d = iter->items[0].data;
gint i;
for (i = 0; i < iter->length; i++)
{
if (*d != 1.0)
{
g_object_unref (buffer);
return TRUE;
}
d++;
}
}
g_object_unref (buffer);
}
return FALSE;
}