From 63c6a9268656f4224fd74ec7048647d7abd0d076 Mon Sep 17 00:00:00 2001 From: Alx Sa Date: Mon, 15 Sep 2025 14:16:00 +0000 Subject: [PATCH 1/7] plug-ins: Initial SVG export support --- plug-ins/common/file-svg.c | 966 +++++++++++++++++++++++++++++++++++++ 1 file changed, 966 insertions(+) diff --git a/plug-ins/common/file-svg.c b/plug-ins/common/file-svg.c index 8b2a0c76905..d2763166031 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,19 @@ typedef struct gint height; } SvgLoadVals; +typedef enum +{ + EXPORT_FORMAT_PNG, + EXPORT_FORMAT_JPEG +} EXPORT_FORMAT; + +typedef enum +{ + LAYER_ID_VECTOR, + LAYER_ID_GROUP, + LAYER_ID_TEXT, + LAYER_ID_RASTER +} LAYER_ID; typedef struct _Svg Svg; typedef struct _SvgClass SvgClass; @@ -97,6 +112,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 +127,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 +143,52 @@ 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 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_group_rec (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_raster (GimpLayer *layer, + gint raster_id, + GString *str, + GimpProcedureConfig *config, + 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 +224,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 +272,63 @@ 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, + NULL, NULL, NULL); + + gimp_procedure_add_string_argument (procedure, "title", + _("_Title"), + _("Optional for SVG"), + NULL, + G_PARAM_READWRITE); + + gimp_procedure_add_boolean_argument (procedure, "export-raster-layers", + _("Export r_aster layers"), + _("If enabled, embed or link raster layers " + "in SVG"), + FALSE, + G_PARAM_READWRITE); + + gimp_procedure_add_choice_argument (procedure, "raster-export-format", + _("E_xport format"), + _("What encoding format to use for raster layers"), + gimp_choice_new_with_values ("png", EXPORT_FORMAT_PNG, _("PNG"), NULL, + "jpeg", EXPORT_FORMAT_JPEG, _("JPEG"), NULL, + NULL), + "png", + G_PARAM_READWRITE); + } return procedure; } @@ -533,6 +662,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 +749,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 +1063,32 @@ load_dialog (GFile *file, return run ? GIMP_PDB_SUCCESS : GIMP_PDB_CANCEL; } +static gboolean +export_dialog (GimpImage *image, + GimpProcedure *procedure, + GObject *config) +{ + GtkWidget *dialog; + gboolean run; + + dialog = gimp_export_procedure_dialog_new (GIMP_EXPORT_PROCEDURE (procedure), + GIMP_PROCEDURE_CONFIG (config), + image); + + gimp_procedure_dialog_fill_frame (GIMP_PROCEDURE_DIALOG (dialog), + "raster-frame", "export-raster-layers", + FALSE, "raster-export-format"); + + gimp_procedure_dialog_fill (GIMP_PROCEDURE_DIALOG (dialog), "title", + "raster-frame", NULL); + + run = gimp_procedure_dialog_run (GIMP_PROCEDURE_DIALOG (dialog)); + + gtk_widget_destroy (dialog); + + return run; +} + #if LIBRSVG_CHECK_VERSION(2, 46, 0) static GimpUnit * svg_rsvg_to_gimp_unit (RsvgUnit unit) @@ -877,3 +1123,723 @@ 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) +{ + GList *drawables; + GList *list; + GString *str = g_string_new (NULL); + gint layer_ids[4]; + gchar *title; + gboolean export_rasters; + + g_object_get (config, + "title", &title, + "export-raster-layers", &export_rasters, + 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); + + drawables = gimp_image_list_layers (image); + for (list = g_list_reverse (drawables); list; list = list->next) + { + if (gimp_item_get_visible (GIMP_ITEM (list->data))) + { + if (GIMP_IS_VECTOR_LAYER (list->data)) + { + GimpVectorLayer *layer = GIMP_VECTOR_LAYER (list->data); + + svg_export_path (layer, layer_ids[LAYER_ID_VECTOR]++, str, " "); + } + else if (gimp_item_is_group (GIMP_ITEM (list->data))) + { + svg_export_group_header (GIMP_GROUP_LAYER (list->data), + layer_ids[LAYER_ID_GROUP]++, str, " "); + svg_export_group_rec (GIMP_GROUP_LAYER (list->data), layer_ids, + str, config, " ", error); + g_string_append_printf (str, " \n"); + } + else if (GIMP_IS_TEXT_LAYER (list->data)) + { + GimpTextLayer *layer = GIMP_TEXT_LAYER (list->data); + + svg_export_text (layer, layer_ids[LAYER_ID_TEXT]++, str, " "); + } + else if (export_rasters) + { + svg_export_raster (GIMP_LAYER (list->data), + layer_ids[LAYER_ID_RASTER]++, str, config, + " ", error); + } + } + } + + g_string_append (str, "\n"); + + return str; +} + +static void +svg_export_group_rec (GimpGroupLayer *group, + gint *layer_ids, + GString *str, + GimpProcedureConfig *config, + gchar *spacing, + GError **error) +{ + GimpItem **children; + gint32 n_layers; + gchar *extra_spacing; + gboolean export_rasters; + + g_object_get (config, + "export-raster-layers", &export_rasters, + NULL); + + extra_spacing = g_strdup_printf ("%s ", spacing); + + children = gimp_item_get_children (GIMP_ITEM (group)); + n_layers = gimp_core_object_array_get_length ((GObject **) children); + + for (gint i = n_layers - 1; i >= 0; i--) + { + if (gimp_item_get_visible (GIMP_ITEM (children[i]))) + { + if (GIMP_IS_VECTOR_LAYER (children[i])) + { + GimpVectorLayer *layer = GIMP_VECTOR_LAYER (children[i]); + + svg_export_path (layer, layer_ids[LAYER_ID_VECTOR]++, str, + extra_spacing); + } + else if (gimp_item_is_group (GIMP_ITEM (children[i]))) + { + svg_export_group_header (GIMP_GROUP_LAYER (children[i]), + layer_ids[LAYER_ID_GROUP]++, str, + extra_spacing); + svg_export_group_rec (GIMP_GROUP_LAYER (children[i]), layer_ids, + str, config, extra_spacing, error); + g_string_append_printf (str, "%s\n", extra_spacing); + } + else if (GIMP_IS_TEXT_LAYER (children[i])) + { + GimpTextLayer *layer = GIMP_TEXT_LAYER (children[i]); + + svg_export_text (layer, layer_ids[LAYER_ID_TEXT]++, str, + extra_spacing); + } + else if (export_rasters) + { + svg_export_raster (GIMP_LAYER (children[i]), + layer_ids[LAYER_ID_RASTER]++, str, config, + extra_spacing, error); + } + } + } + g_free (extra_spacing); + g_free (children); +} + +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_raster (GimpLayer *layer, + gint raster_id, + GString *str, + GimpProcedureConfig *config, + gchar *spacing, + GError **error) +{ + gint format_id; + 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 width; + gint height; + gboolean include_color_profile; + const gchar *mimetype; + + 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"; + } + else + { + temp_file = gimp_temp_file ("jpeg"); + mimetype = "image/jpeg"; + } + + width = gimp_drawable_get_width (GIMP_DRAWABLE (layer)); + height = gimp_drawable_get_height (GIMP_DRAWABLE (layer)); + + 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 (GIMP_DRAWABLE (layer), + temp_image); + gimp_image_insert_layer (temp_image, temp_layer, NULL, 0); + gimp_layer_set_offsets (temp_layer, 0, 0); + + 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, name, + spacing, x, + spacing, y, + spacing, width, + spacing, height, + spacing, mimetype, encoded); + g_free (encoded); + g_free (buf); + } + fclose (temp_fp); + + g_file_delete (temp_file, NULL, NULL); + g_object_unref (temp_file); +} + +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; +} -- GitLab From 5c316453135b13355531768a4e481475586ac9bb Mon Sep 17 00:00:00 2001 From: Alx Sa Date: Wed, 24 Sep 2025 04:39:58 +0000 Subject: [PATCH 2/7] plug-ins: Add link layer export to SVG export --- plug-ins/common/file-svg.c | 70 +++++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/plug-ins/common/file-svg.c b/plug-ins/common/file-svg.c index d2763166031..1fe61d331fa 100644 --- a/plug-ins/common/file-svg.c +++ b/plug-ins/common/file-svg.c @@ -65,6 +65,7 @@ typedef enum LAYER_ID_VECTOR, LAYER_ID_GROUP, LAYER_ID_TEXT, + LAYER_ID_LINK, LAYER_ID_RASTER } LAYER_ID; @@ -178,6 +179,10 @@ static void svg_export_text (GimpTextLayer *layer, 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 (GimpLayer *layer, gint raster_id, GString *str, @@ -1133,7 +1138,7 @@ svg_export_file (GimpImage *image, GList *drawables; GList *list; GString *str = g_string_new (NULL); - gint layer_ids[4]; + gint layer_ids[5] = { 0 }; gchar *title; gboolean export_rasters; @@ -1168,7 +1173,7 @@ svg_export_file (GimpImage *image, { if (gimp_item_get_visible (GIMP_ITEM (list->data))) { - if (GIMP_IS_VECTOR_LAYER (list->data)) + if (gimp_item_is_vector_layer (GIMP_ITEM (list->data))) { GimpVectorLayer *layer = GIMP_VECTOR_LAYER (list->data); @@ -1182,12 +1187,19 @@ svg_export_file (GimpImage *image, str, config, " ", error); g_string_append_printf (str, " \n"); } - else if (GIMP_IS_TEXT_LAYER (list->data)) + else if (gimp_item_is_text_layer (GIMP_ITEM (list->data))) { GimpTextLayer *layer = GIMP_TEXT_LAYER (list->data); svg_export_text (layer, layer_ids[LAYER_ID_TEXT]++, str, " "); } + else if (gimp_item_is_link_layer (GIMP_ITEM (list->data))) + { + GimpLinkLayer *layer = GIMP_LINK_LAYER (list->data); + + svg_export_link_layer (layer, layer_ids[LAYER_ID_LINK]++, str, + " "); + } else if (export_rasters) { svg_export_raster (GIMP_LAYER (list->data), @@ -1228,7 +1240,7 @@ svg_export_group_rec (GimpGroupLayer *group, { if (gimp_item_get_visible (GIMP_ITEM (children[i]))) { - if (GIMP_IS_VECTOR_LAYER (children[i])) + if (gimp_item_is_vector_layer (GIMP_ITEM (children[i]))) { GimpVectorLayer *layer = GIMP_VECTOR_LAYER (children[i]); @@ -1244,13 +1256,20 @@ svg_export_group_rec (GimpGroupLayer *group, str, config, extra_spacing, error); g_string_append_printf (str, "%s\n", extra_spacing); } - else if (GIMP_IS_TEXT_LAYER (children[i])) + else if (gimp_item_is_text_layer (GIMP_ITEM (children[i]))) { GimpTextLayer *layer = GIMP_TEXT_LAYER (children[i]); svg_export_text (layer, layer_ids[LAYER_ID_TEXT]++, str, extra_spacing); } + else if (gimp_item_is_link_layer (GIMP_ITEM (children[i]))) + { + GimpLinkLayer *layer = GIMP_LINK_LAYER (children[i]); + + svg_export_link_layer (layer, layer_ids[LAYER_ID_LINK]++, str, + extra_spacing); + } else if (export_rasters) { svg_export_raster (GIMP_LAYER (children[i]), @@ -1652,6 +1671,46 @@ svg_export_text_lines (GimpTextLayer *layer, 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; + gint x; + gint y; + + width = gimp_drawable_get_width (GIMP_DRAWABLE (layer)); + height = gimp_drawable_get_height (GIMP_DRAWABLE (layer)); + 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, path); + g_free (name); + g_free (path); +} + static void svg_export_raster (GimpLayer *layer, gint raster_id, @@ -1789,6 +1848,7 @@ svg_export_raster (GimpLayer *layer, spacing, height, spacing, mimetype, encoded); g_free (encoded); + g_free (name); g_free (buf); } fclose (temp_fp); -- GitLab From d53210667a0686b2d2dff900a14198f38068478e Mon Sep 17 00:00:00 2001 From: Alx Sa Date: Wed, 24 Sep 2025 18:14:14 +0000 Subject: [PATCH 3/7] plug-ins: Add warning for non-standard links Per the specification, SVG reviewers are only required to support SVG, PNG, and JPEG links. This adds a warning if you link any other type. --- plug-ins/common/file-svg.c | 58 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/plug-ins/common/file-svg.c b/plug-ins/common/file-svg.c index 1fe61d331fa..95c6ad68242 100644 --- a/plug-ins/common/file-svg.c +++ b/plug-ins/common/file-svg.c @@ -147,6 +147,8 @@ static GimpPDBStatusType load_dialog (GFile *file, 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); @@ -1075,11 +1077,29 @@ export_dialog (GimpImage *image, { GtkWidget *dialog; gboolean run; + gboolean has_nonstandard_links = FALSE; dialog = gimp_export_procedure_dialog_new (GIMP_EXPORT_PROCEDURE (procedure), GIMP_PROCEDURE_CONFIG (config), image); + has_nonstandard_links = has_invalid_links (gimp_image_get_layers (image), + NULL); + if (has_nonstandard_links) + { + gimp_procedure_dialog_get_label (GIMP_PROCEDURE_DIALOG (dialog), + "link-warning", + _("The SVG format only requires viewers " + "to display image links for SVG, PNG, " + "and JPEG images.\nLink layers for " + "images in other formats may not " + "show up in all viewers."), + FALSE, FALSE); + + gimp_procedure_dialog_fill (GIMP_PROCEDURE_DIALOG (dialog), + "link-warning", NULL); + } + gimp_procedure_dialog_fill_frame (GIMP_PROCEDURE_DIALOG (dialog), "raster-frame", "export-raster-layers", FALSE, "raster-export-format"); @@ -1094,6 +1114,44 @@ export_dialog (GimpImage *image, 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) -- GitLab From ef8347cf1b70e85c98c0bdb94c23aa7c41a89933 Mon Sep 17 00:00:00 2001 From: Alx Sa Date: Fri, 26 Sep 2025 12:51:49 +0000 Subject: [PATCH 4/7] Add GimpHintBox with warning label --- plug-ins/common/file-svg.c | 43 +++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/plug-ins/common/file-svg.c b/plug-ins/common/file-svg.c index 95c6ad68242..4c465818cdb 100644 --- a/plug-ins/common/file-svg.c +++ b/plug-ins/common/file-svg.c @@ -1076,6 +1076,8 @@ export_dialog (GimpImage *image, GObject *config) { GtkWidget *dialog; + GtkWidget *box; + GtkWidget *hint = NULL; gboolean run; gboolean has_nonstandard_links = FALSE; @@ -1083,29 +1085,36 @@ export_dialog (GimpImage *image, GIMP_PROCEDURE_CONFIG (config), image); + gimp_procedure_dialog_fill_frame (GIMP_PROCEDURE_DIALOG (dialog), + "raster-frame", "export-raster-layers", + FALSE, "raster-export-format"); + + box = gimp_procedure_dialog_fill_box (GIMP_PROCEDURE_DIALOG (dialog), + "svg-box", "title", "raster-frame", + NULL); + has_nonstandard_links = has_invalid_links (gimp_image_get_layers (image), NULL); if (has_nonstandard_links) { - gimp_procedure_dialog_get_label (GIMP_PROCEDURE_DIALOG (dialog), - "link-warning", - _("The SVG format only requires viewers " - "to display image links for SVG, PNG, " - "and JPEG images.\nLink layers for " - "images in other formats may not " - "show up in all viewers."), - FALSE, FALSE); - - gimp_procedure_dialog_fill (GIMP_PROCEDURE_DIALOG (dialog), - "link-warning", NULL); + 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_frame (GIMP_PROCEDURE_DIALOG (dialog), - "raster-frame", "export-raster-layers", - FALSE, "raster-export-format"); - - gimp_procedure_dialog_fill (GIMP_PROCEDURE_DIALOG (dialog), "title", - "raster-frame", NULL); + gimp_procedure_dialog_fill (GIMP_PROCEDURE_DIALOG (dialog), "svg-box", NULL); run = gimp_procedure_dialog_run (GIMP_PROCEDURE_DIALOG (dialog)); -- GitLab From 72d839173c81dd356ed8c2f23948aa778ba33ce8 Mon Sep 17 00:00:00 2001 From: Alx Sa Date: Mon, 29 Sep 2025 21:15:21 +0000 Subject: [PATCH 5/7] plug-ins: Combine raster export parameters --- plug-ins/common/file-svg.c | 45 ++++++++++++++------------------------ 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/plug-ins/common/file-svg.c b/plug-ins/common/file-svg.c index 4c465818cdb..904df93b586 100644 --- a/plug-ins/common/file-svg.c +++ b/plug-ins/common/file-svg.c @@ -56,6 +56,7 @@ typedef struct typedef enum { + EXPORT_FORMAT_NONE, EXPORT_FORMAT_PNG, EXPORT_FORMAT_JPEG } EXPORT_FORMAT; @@ -320,18 +321,12 @@ svg_create_procedure (GimpPlugIn *plug_in, NULL, G_PARAM_READWRITE); - gimp_procedure_add_boolean_argument (procedure, "export-raster-layers", - _("Export r_aster layers"), - _("If enabled, embed or link raster layers " - "in SVG"), - FALSE, - G_PARAM_READWRITE); - gimp_procedure_add_choice_argument (procedure, "raster-export-format", - _("E_xport format"), - _("What encoding format to use for raster layers"), - gimp_choice_new_with_values ("png", EXPORT_FORMAT_PNG, _("PNG"), NULL, - "jpeg", EXPORT_FORMAT_JPEG, _("JPEG"), NULL, + _("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); @@ -1085,13 +1080,9 @@ export_dialog (GimpImage *image, GIMP_PROCEDURE_CONFIG (config), image); - gimp_procedure_dialog_fill_frame (GIMP_PROCEDURE_DIALOG (dialog), - "raster-frame", "export-raster-layers", - FALSE, "raster-export-format"); - box = gimp_procedure_dialog_fill_box (GIMP_PROCEDURE_DIALOG (dialog), - "svg-box", "title", "raster-frame", - NULL); + "svg-box", "title", + "raster-export-format", NULL); has_nonstandard_links = has_invalid_links (gimp_image_get_layers (image), NULL); @@ -1207,12 +1198,11 @@ svg_export_file (GimpImage *image, GString *str = g_string_new (NULL); gint layer_ids[5] = { 0 }; gchar *title; - gboolean export_rasters; + gint format_id; - g_object_get (config, - "title", &title, - "export-raster-layers", &export_rasters, - NULL); + g_object_get (config, "title", &title, NULL); + format_id = gimp_procedure_config_get_choice_id (GIMP_PROCEDURE_CONFIG (config), + "raster-export-format"); g_string_append_printf (str, "\n" @@ -1267,7 +1257,7 @@ svg_export_file (GimpImage *image, svg_export_link_layer (layer, layer_ids[LAYER_ID_LINK]++, str, " "); } - else if (export_rasters) + else if (format_id != EXPORT_FORMAT_NONE) { svg_export_raster (GIMP_LAYER (list->data), layer_ids[LAYER_ID_RASTER]++, str, config, @@ -1292,11 +1282,10 @@ svg_export_group_rec (GimpGroupLayer *group, GimpItem **children; gint32 n_layers; gchar *extra_spacing; - gboolean export_rasters; + gint format_id; - g_object_get (config, - "export-raster-layers", &export_rasters, - NULL); + format_id = gimp_procedure_config_get_choice_id (GIMP_PROCEDURE_CONFIG (config), + "raster-export-format"); extra_spacing = g_strdup_printf ("%s ", spacing); @@ -1337,7 +1326,7 @@ svg_export_group_rec (GimpGroupLayer *group, svg_export_link_layer (layer, layer_ids[LAYER_ID_LINK]++, str, extra_spacing); } - else if (export_rasters) + else if (format_id != EXPORT_FORMAT_NONE) { svg_export_raster (GIMP_LAYER (children[i]), layer_ids[LAYER_ID_RASTER]++, str, config, -- GitLab From f3f15b004168868a11d55cf8327f2e8ed8a29c68 Mon Sep 17 00:00:00 2001 From: Alx Sa Date: Fri, 10 Oct 2025 23:38:00 +0000 Subject: [PATCH 6/7] svg: Code updates * Condense duplicate layer code to one function * Use opacity attribute instead of semi-transparent raster --- plug-ins/common/file-svg.c | 163 ++++++++++++++++--------------------- 1 file changed, 70 insertions(+), 93 deletions(-) diff --git a/plug-ins/common/file-svg.c b/plug-ins/common/file-svg.c index 904df93b586..ae4ca7bd8ba 100644 --- a/plug-ins/common/file-svg.c +++ b/plug-ins/common/file-svg.c @@ -160,7 +160,8 @@ static GString * svg_export_file (GimpImage *image, GError **error); static void svg_export_image_size (GimpImage *image, GString *str); -static void svg_export_group_rec (GimpGroupLayer *group, +static void svg_export_layers (GimpItem **layers, + GimpGroupLayer *group, gint *layer_ids, GString *str, GimpProcedureConfig *config, @@ -1193,16 +1194,12 @@ svg_export_file (GimpImage *image, GimpProcedureConfig *config, GError **error) { - GList *drawables; - GList *list; - GString *str = g_string_new (NULL); - gint layer_ids[5] = { 0 }; - gchar *title; - gint format_id; + GimpLayer **layers; + GString *str = g_string_new (NULL); + gint layer_ids[5] = { 0 }; + gchar *title; g_object_get (config, "title", &title, NULL); - format_id = gimp_procedure_config_get_choice_id (GIMP_PROCEDURE_CONFIG (config), - "raster-export-format"); g_string_append_printf (str, "\n" @@ -1225,46 +1222,10 @@ svg_export_file (GimpImage *image, " %s\n", title); - drawables = gimp_image_list_layers (image); - for (list = g_list_reverse (drawables); list; list = list->next) - { - if (gimp_item_get_visible (GIMP_ITEM (list->data))) - { - if (gimp_item_is_vector_layer (GIMP_ITEM (list->data))) - { - GimpVectorLayer *layer = GIMP_VECTOR_LAYER (list->data); - - svg_export_path (layer, layer_ids[LAYER_ID_VECTOR]++, str, " "); - } - else if (gimp_item_is_group (GIMP_ITEM (list->data))) - { - svg_export_group_header (GIMP_GROUP_LAYER (list->data), - layer_ids[LAYER_ID_GROUP]++, str, " "); - svg_export_group_rec (GIMP_GROUP_LAYER (list->data), layer_ids, - str, config, " ", error); - g_string_append_printf (str, " \n"); - } - else if (gimp_item_is_text_layer (GIMP_ITEM (list->data))) - { - GimpTextLayer *layer = GIMP_TEXT_LAYER (list->data); - - svg_export_text (layer, layer_ids[LAYER_ID_TEXT]++, str, " "); - } - else if (gimp_item_is_link_layer (GIMP_ITEM (list->data))) - { - GimpLinkLayer *layer = GIMP_LINK_LAYER (list->data); - - svg_export_link_layer (layer, layer_ids[LAYER_ID_LINK]++, str, - " "); - } - else if (format_id != EXPORT_FORMAT_NONE) - { - svg_export_raster (GIMP_LAYER (list->data), - layer_ids[LAYER_ID_RASTER]++, str, config, - " ", error); - } - } - } + 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"); @@ -1272,70 +1233,76 @@ svg_export_file (GimpImage *image, } static void -svg_export_group_rec (GimpGroupLayer *group, - gint *layer_ids, - GString *str, - GimpProcedureConfig *config, - gchar *spacing, - GError **error) +svg_export_layers (GimpItem **layers, + GimpGroupLayer *group, + gint *layer_ids, + GString *str, + GimpProcedureConfig *config, + gchar *spacing, + GError **error) { - GimpItem **children; + 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); - children = gimp_item_get_children (GIMP_ITEM (group)); - n_layers = gimp_core_object_array_get_length ((GObject **) children); + n_layers = gimp_core_object_array_get_length ((GObject **) items); for (gint i = n_layers - 1; i >= 0; i--) { - if (gimp_item_get_visible (GIMP_ITEM (children[i]))) + if (gimp_item_get_visible (items[i])) { - if (gimp_item_is_vector_layer (GIMP_ITEM (children[i]))) + if (gimp_item_is_vector_layer (items[i])) { - GimpVectorLayer *layer = GIMP_VECTOR_LAYER (children[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 (GIMP_ITEM (children[i]))) + else if (gimp_item_is_group (items[i])) { - svg_export_group_header (GIMP_GROUP_LAYER (children[i]), + svg_export_group_header (GIMP_GROUP_LAYER (items[i]), layer_ids[LAYER_ID_GROUP]++, str, extra_spacing); - svg_export_group_rec (GIMP_GROUP_LAYER (children[i]), layer_ids, - str, config, extra_spacing, error); + 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 (GIMP_ITEM (children[i]))) + else if (gimp_item_is_text_layer (items[i])) { - GimpTextLayer *layer = GIMP_TEXT_LAYER (children[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 (GIMP_ITEM (children[i]))) + else if (gimp_item_is_link_layer (items[i])) { - GimpLinkLayer *layer = GIMP_LINK_LAYER (children[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_LAYER (children[i]), + svg_export_raster (GIMP_LAYER (items[i]), layer_ids[LAYER_ID_RASTER]++, str, config, extra_spacing, error); } } } g_free (extra_spacing); - g_free (children); + if (group) + g_free (items); } static void @@ -1733,16 +1700,18 @@ svg_export_link_layer (GimpLinkLayer *layer, GString *str, gchar *spacing) { - GFile *file; - gchar *name; - gchar *path; - gint width; - gint height; - gint x; - gint y; - - width = gimp_drawable_get_width (GIMP_DRAWABLE (layer)); - height = gimp_drawable_get_height (GIMP_DRAWABLE (layer)); + 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); @@ -1752,16 +1721,18 @@ svg_export_link_layer (GimpLinkLayer *layer, g_string_append_printf (str, "%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); @@ -1786,6 +1757,7 @@ svg_export_raster (GimpLayer *layer, gsize temp_size; gint width; gint height; + gdouble opacity; gboolean include_color_profile; const gchar *mimetype; @@ -1803,8 +1775,9 @@ svg_export_raster (GimpLayer *layer, mimetype = "image/jpeg"; } - width = gimp_drawable_get_width (GIMP_DRAWABLE (layer)); - height = gimp_drawable_get_height (GIMP_DRAWABLE (layer)); + 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; image = gimp_item_get_image (GIMP_ITEM (layer)); temp_image = gimp_image_new (width, height, @@ -1817,6 +1790,8 @@ svg_export_raster (GimpLayer *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)) @@ -1892,16 +1867,18 @@ svg_export_raster (GimpLayer *layer, g_string_append_printf (str, "%s\n", + "%s x=\"%d\"\n" + "%s y=\"%d\"\n" + "%s width=\"%d\"\n" + "%s height=\"%d\"\n" + "%s opacity=\"%f\"\n" + "%s href=\"data:%s;base64,%s\" />\n", spacing, name, spacing, x, spacing, y, spacing, width, spacing, height, + spacing, opacity, spacing, mimetype, encoded); g_free (encoded); g_free (name); -- GitLab From 38575c8c440121a16ca10568f35fb83b25e4562e Mon Sep 17 00:00:00 2001 From: Alx Sa Date: Sat, 11 Oct 2025 15:07:21 +0000 Subject: [PATCH 7/7] plug-ins: Add code for raster masks --- plug-ins/common/file-svg.c | 138 +++++++++++++++++++++++++++---------- 1 file changed, 100 insertions(+), 38 deletions(-) diff --git a/plug-ins/common/file-svg.c b/plug-ins/common/file-svg.c index ae4ca7bd8ba..d8c0f96e3fe 100644 --- a/plug-ins/common/file-svg.c +++ b/plug-ins/common/file-svg.c @@ -67,7 +67,8 @@ typedef enum LAYER_ID_GROUP, LAYER_ID_TEXT, LAYER_ID_LINK, - LAYER_ID_RASTER + LAYER_ID_RASTER, + LAYER_ID_MASK, } LAYER_ID; typedef struct _Svg Svg; @@ -187,10 +188,15 @@ static void svg_export_link_layer (GimpLinkLayer *layer, gint link_id, GString *str, gchar *spacing); -static void svg_export_raster (GimpLayer *layer, - gint raster_id, +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, - GimpProcedureConfig *config, gchar *spacing, GError **error); @@ -313,7 +319,8 @@ svg_create_procedure (GimpPlugIn *plug_in, GIMP_EXPORT_CAN_HANDLE_GRAY | GIMP_EXPORT_CAN_HANDLE_INDEXED | GIMP_EXPORT_CAN_HANDLE_ALPHA | - GIMP_EXPORT_CAN_HANDLE_LAYERS, + GIMP_EXPORT_CAN_HANDLE_LAYERS | + GIMP_EXPORT_CAN_HANDLE_LAYER_MASKS, NULL, NULL, NULL); gimp_procedure_add_string_argument (procedure, "title", @@ -1195,9 +1202,9 @@ svg_export_file (GimpImage *image, GError **error) { GimpLayer **layers; - GString *str = g_string_new (NULL); - gint layer_ids[5] = { 0 }; gchar *title; + GString *str = g_string_new (NULL); + gint layer_ids[6] = { 0 }; g_object_get (config, "title", &title, NULL); @@ -1294,15 +1301,17 @@ svg_export_layers (GimpItem **layers, } else if (format_id != EXPORT_FORMAT_NONE) { - svg_export_raster (GIMP_LAYER (items[i]), - layer_ids[LAYER_ID_RASTER]++, str, config, - extra_spacing, error); + 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 @@ -1739,30 +1748,36 @@ svg_export_link_layer (GimpLinkLayer *layer, } static void -svg_export_raster (GimpLayer *layer, - gint raster_id, +svg_export_raster (GimpDrawable *layer, + gint *layer_ids, GString *str, - GimpProcedureConfig *config, + gint format_id, gchar *spacing, GError **error) { - gint format_id; - 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 width; - gint height; - gdouble opacity; - gboolean include_color_profile; - const gchar *mimetype; - - format_id = gimp_procedure_config_get_choice_id (GIMP_PROCEDURE_CONFIG (config), - "raster-export-format"); + 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) { @@ -1774,10 +1789,14 @@ svg_export_raster (GimpLayer *layer, temp_file = gimp_temp_file ("jpeg"); mimetype = "image/jpeg"; } + raster_id = layer_ids[LAYER_ID_RASTER]++; - 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; + 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, @@ -1786,8 +1805,7 @@ svg_export_raster (GimpLayer *layer, gimp_image_set_palette (temp_image, gimp_image_get_palette (image)); - temp_layer = gimp_layer_new_from_drawable (GIMP_DRAWABLE (layer), - temp_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 */ @@ -1871,14 +1889,21 @@ svg_export_raster (GimpLayer *layer, "%s y=\"%d\"\n" "%s width=\"%d\"\n" "%s height=\"%d\"\n" - "%s opacity=\"%f\"\n" - "%s href=\"data:%s;base64,%s\" />\n", + "%s opacity=\"%f\"\n", spacing, name, spacing, x, spacing, y, spacing, width, spacing, height, - spacing, opacity, + 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); @@ -1890,6 +1915,43 @@ svg_export_raster (GimpLayer *layer, 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) -- GitLab