From b726afc8d7e50cdac46b9595043031f4bb80f80a Mon Sep 17 00:00:00 2001 From: Alx Sa Date: Sat, 11 Oct 2025 15:21:28 +0000 Subject: [PATCH] plug-ins: Initial SVG export support This patch adds support for initial SVG export. It currently exports vector, text, and link layers natively. Raster layers and masks for raster layers are optionally embedded as either PNG or JPEG base64 encoded data. --- plug-ins/common/file-svg.c | 1121 ++++++++++++++++++++++++++++++++++++ 1 file changed, 1121 insertions(+) diff --git a/plug-ins/common/file-svg.c b/plug-ins/common/file-svg.c index 8b2a0c7690..d8c0f96e3f 100644 --- a/plug-ins/common/file-svg.c +++ b/plug-ins/common/file-svg.c @@ -26,6 +26,7 @@ #include #include +#include #include #include "libgimp/gimp.h" @@ -35,6 +36,7 @@ #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" @@ -52,6 +54,22 @@ typedef struct 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; @@ -97,6 +115,14 @@ static GimpValueArray * svg_load (GimpProcedure *procedure, 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, @@ -104,6 +130,11 @@ static GimpImage * load_image (GFile *file, 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, @@ -115,9 +146,64 @@ static GimpPDBStatusType load_dialog (GFile *file, 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_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); + #if LIBRSVG_CHECK_VERSION(2, 46, 0) static GimpUnit * svg_rsvg_to_gimp_unit (RsvgUnit unit); static void svg_destroy_surface (guchar *pixels, @@ -153,6 +239,7 @@ 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; } @@ -200,6 +287,58 @@ svg_create_procedure (GimpPlugIn *plug_in, 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, + _("Export vector layers in SVG file format"), + _("Export vector layers 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 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"), NULL, + "jpeg", EXPORT_FORMAT_JPEG, _("JPEG"), NULL, + "none", EXPORT_FORMAT_NONE, _("Do not export raster layers"), NULL, + NULL), + "png", + G_PARAM_READWRITE); + } return procedure; } @@ -533,6 +672,44 @@ svg_load (GimpProcedure *procedure, 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, @@ -582,6 +759,59 @@ load_image (GFile *file, 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: @@ -843,6 +1073,93 @@ load_dialog (GFile *file, 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) @@ -877,3 +1194,807 @@ svg_destroy_surface (guchar *pixels, 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; + GString *str = g_string_new (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\n", + title); + + layers = gimp_image_get_layers (image); + svg_export_layers ((GimpItem **) layers, NULL, layer_ids, str, config, "", + error); + g_free (layers); + + g_string_append (str, "\n"); + + return str; +} + +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_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_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\n", extra_spacing); + } + else if (gimp_item_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_item_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\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 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", + 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, "", "", 1); + g_string_replace (markup, "", "", 1); + + g_string_replace (markup, "", + "", 0); + g_string_replace (markup, "", "", 0); + + g_string_replace (markup, "", + "", 0); + g_string_replace (markup, "", "", 0); + + g_string_replace (markup, "", + "", 0); + g_string_replace (markup, "", "", 0); + + g_string_replace (markup, "", + "", 0); + g_string_replace (markup, "", "", 0); + + g_string_replace (markup, "", "", 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, "\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, "" + "%s", + 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\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_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\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\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, "\n"); +} + +static gchar * +svg_get_color_string (GimpVectorLayer *layer, + const gchar *type) +{ + GeglColor *color = NULL; + gboolean enabled = TRUE; + + if (g_strcmp0 ("fill", type) == 0) + { + color = gimp_vector_layer_get_fill_color (layer); + enabled = gimp_vector_layer_get_enable_fill (layer); + } + else + { + color = gimp_vector_layer_get_stroke_color (layer); + enabled = gimp_vector_layer_get_enable_stroke (layer); + } + + if (enabled && 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); + + return color_string; + } + + 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; +}