[gimp] app, devel-docs: add a new "Vectors Structure" in the XCF format.



commit 81969a065149726a38e9a76aec8698689ae08e3b
Author: Jehan <jehan girinstud io>
Date:   Thu Oct 20 00:11:34 2022 +0200

    app, devel-docs: add a new "Vectors Structure" in the XCF format.
    
    Instead of storing vectors as properties, they have their own structure, which
    make them able to store and load all the usual and common properties of other
    items. In other words, it makes XCF now able to store locks, color tags and
    several selected paths.

 app/core/gimpimage.c              |  29 +++
 app/xcf/xcf-load.c                | 425 ++++++++++++++++++++++++++++++++++++--
 app/xcf/xcf-private.h             |   2 +
 app/xcf/xcf-save.c                | 288 ++++++++++++++++++++++++--
 app/xcf/xcf.c                     |   1 +
 devel-docs/specifications/xcf.txt |  84 +++++++-
 6 files changed, 791 insertions(+), 38 deletions(-)
---
diff --git a/app/core/gimpimage.c b/app/core/gimpimage.c
index 8f42942f22..f22395f57a 100644
--- a/app/core/gimpimage.c
+++ b/app/core/gimpimage.c
@@ -2966,6 +2966,34 @@ gimp_image_get_xcf_version (GimpImage    *image,
     }
   g_list_free (items);
 
+  if (g_list_length (gimp_image_get_selected_vectors (image)) > 1)
+    {
+      ADD_REASON (g_strdup_printf (_("Multiple path selection was "
+                                     "added in %s"), "GIMP 3.0.0"));
+      version = MAX (18, version);
+    }
+
+  items = gimp_image_get_vectors_list (image);
+  for (list = items; list; list = g_list_next (list))
+    {
+      GimpVectors *vectors = GIMP_VECTORS (list->data);
+
+      if (gimp_item_get_color_tag (GIMP_ITEM (vectors)) != GIMP_COLOR_TAG_NONE)
+        {
+          ADD_REASON (g_strdup_printf (_("Storing color tags in path was "
+                                         "added in %s"), "GIMP 3.0.0"));
+          version = MAX (18, version);
+        }
+      if (gimp_item_get_lock_content (GIMP_ITEM (list->data)) ||
+          gimp_item_get_lock_position (GIMP_ITEM (list->data)))
+        {
+          ADD_REASON (g_strdup_printf (_("Storing locks in path was "
+                                         "added in %s"), "GIMP 3.0.0"));
+          version = MAX (18, version);
+        }
+    }
+  g_list_free (items);
+
   /* version 6 for new metadata has been dropped since they are
    * saved through parasites, which is compatible with older versions.
    */
@@ -3086,6 +3114,7 @@ gimp_image_get_xcf_version (GimpImage    *image,
     case 15:
     case 16:
     case 17:
+    case 18:
       if (gimp_version)   *gimp_version   = 300;
       if (version_string) *version_string = "GIMP 3.0";
       break;
diff --git a/app/xcf/xcf-load.c b/app/xcf/xcf-load.c
index aaccf0ea46..0df8182203 100644
--- a/app/xcf/xcf-load.c
+++ b/app/xcf/xcf-load.c
@@ -105,6 +105,9 @@ static gboolean        xcf_check_layer_props  (XcfInfo       *info,
 static gboolean        xcf_load_channel_props (XcfInfo       *info,
                                                GimpImage     *image,
                                                GimpChannel  **channel);
+static gboolean        xcf_load_vectors_props (XcfInfo       *info,
+                                               GimpImage     *image,
+                                               GimpVectors  **vectors);
 static gboolean        xcf_load_prop          (XcfInfo       *info,
                                                PropType      *prop_type,
                                                guint32       *prop_size);
@@ -113,6 +116,8 @@ static GimpLayer     * xcf_load_layer         (XcfInfo       *info,
                                                GList        **item_path);
 static GimpChannel   * xcf_load_channel       (XcfInfo       *info,
                                                GimpImage     *image);
+static GimpVectors   * xcf_load_vectors       (XcfInfo       *info,
+                                               GimpImage     *image);
 static GimpLayerMask * xcf_load_layer_mask    (XcfInfo       *info,
                                                GimpImage     *image);
 static gboolean        xcf_load_buffer        (XcfInfo       *info,
@@ -138,9 +143,9 @@ static gboolean        xcf_load_old_paths     (XcfInfo       *info,
                                                GimpImage     *image);
 static gboolean        xcf_load_old_path      (XcfInfo       *info,
                                                GimpImage     *image);
-static gboolean        xcf_load_vectors       (XcfInfo       *info,
+static gboolean        xcf_load_old_vectors   (XcfInfo       *info,
                                                GimpImage     *image);
-static gboolean        xcf_load_vector        (XcfInfo       *info,
+static gboolean        xcf_load_old_vector    (XcfInfo       *info,
                                                GimpImage     *image);
 
 static gboolean        xcf_skip_unknown_prop  (XcfInfo       *info,
@@ -176,6 +181,7 @@ xcf_load_image (Gimp     *gimp,
   gint                num_successful_elements = 0;
   gint                n_broken_layers         = 0;
   gint                n_broken_channels       = 0;
+  gint                n_broken_vectors        = 0;
   GList              *broken_paths            = NULL;
   GList              *group_layers            = NULL;
   GList              *syms;
@@ -775,6 +781,67 @@ xcf_load_image (Gimp     *gimp,
       floating_sel_attach (info->floating_sel, info->floating_sel_drawable);
     }
 
+  if (info->file_version >= 18)
+    {
+      while (TRUE)
+        {
+          GimpVectors *vectors;
+
+          /* read in the offset of the next path */
+          xcf_read_offset (info, &offset, 1);
+
+          /* if the offset is 0 then we are at the end
+           *  of the path list.
+           */
+          if (offset == 0)
+            break;
+
+          /* save the current position as it is where the
+           *  next channel offset is stored.
+           */
+          saved_pos = info->cp;
+
+          if (offset < saved_pos)
+            {
+              GIMP_LOG (XCF, "Invalid path offset: %" G_GOFFSET_FORMAT
+                        " at offset: % "G_GOFFSET_FORMAT, offset, saved_pos);
+              goto error;
+            }
+
+          /* seek to the path offset */
+          if (! xcf_seek_pos (info, offset, NULL))
+            goto error;
+
+          /* read in the path */
+          vectors = xcf_load_vectors (info, image);
+          if (! vectors)
+            {
+              n_broken_vectors++;
+              GIMP_LOG (XCF, "Failed to load path.");
+
+              if (! xcf_seek_pos (info, saved_pos, NULL))
+                goto error;
+
+              continue;
+            }
+
+          num_successful_elements++;
+
+          xcf_progress_update (info);
+
+          gimp_image_add_vectors (image, vectors,
+                                  NULL, /* can't be a tree */
+                                  gimp_container_get_n_children (gimp_image_get_vectors (image)),
+                                  FALSE);
+
+          /* restore the saved position so we'll be ready to
+           *  read the next offset.
+           */
+          if (! xcf_seek_pos (info, saved_pos, NULL))
+            goto error;
+        }
+    }
+
   if (info->selected_layers)
     {
       gimp_image_set_selected_layers (image, info->selected_layers);
@@ -784,6 +851,9 @@ xcf_load_image (Gimp     *gimp,
   if (info->selected_channels)
     gimp_image_set_selected_channels (image, info->selected_channels);
 
+  if (info->selected_vectors)
+    gimp_image_set_selected_vectors (image, info->selected_vectors);
+
   /* We don't have linked items concept anymore. We transform formerly
    * linked items into stored sets of named items instead.
    */
@@ -854,7 +924,7 @@ xcf_load_image (Gimp     *gimp,
   if (info->tattoo_state > 0)
     gimp_image_set_tattoo_state (image, info->tattoo_state);
 
-  if (n_broken_layers > 0 || n_broken_channels > 0)
+  if (n_broken_layers > 0 || n_broken_channels > 0 || n_broken_vectors > 0)
     goto error;
 
   gimp_image_undo_enable (image);
@@ -1267,6 +1337,12 @@ xcf_load_image_props (XcfInfo   *info,
           {
             goffset base = info->cp;
 
+            if (info->file_version >= 18)
+              gimp_message (info->gimp, G_OBJECT (info->progress),
+                            GIMP_MESSAGE_WARNING,
+                            "XCF %d file should not contain PROP_PATHS image properties",
+                            info->file_version);
+
             if (! xcf_load_old_paths (info, image))
               xcf_seek_pos (info, base + prop_size, NULL);
           }
@@ -1326,7 +1402,13 @@ xcf_load_image_props (XcfInfo   *info,
           {
             goffset base = info->cp;
 
-            if (xcf_load_vectors (info, image))
+            if (info->file_version >= 18)
+              gimp_message (info->gimp, G_OBJECT (info->progress),
+                            GIMP_MESSAGE_WARNING,
+                            "XCF %d file should not contain PROP_VECTORS image properties",
+                            info->file_version);
+
+            if (xcf_load_old_vectors (info, image))
               {
                 if (base + prop_size != info->cp)
                   {
@@ -1339,7 +1421,7 @@ xcf_load_image_props (XcfInfo   *info,
             else
               {
                 /* skip silently since we don't understand the format and
-                 * xcf_load_vectors already explained what was wrong
+                 * xcf_load_old_vectors already explained what was wrong
                  */
                 xcf_seek_pos (info, base + prop_size, NULL);
               }
@@ -2189,6 +2271,165 @@ xcf_load_channel_props (XcfInfo      *info,
   return FALSE;
 }
 
+static gboolean
+xcf_load_vectors_props (XcfInfo      *info,
+                        GimpImage    *image,
+                        GimpVectors **vectors)
+{
+  PropType prop_type;
+  guint32  prop_size;
+
+  while (TRUE)
+    {
+      if (! xcf_load_prop (info, &prop_type, &prop_size))
+        return FALSE;
+
+      switch (prop_type)
+        {
+        case PROP_END:
+          return TRUE;
+
+        case PROP_SELECTED_PATH:
+          info->selected_vectors = g_list_prepend (info->selected_vectors, *vectors);
+          break;
+
+        case PROP_VISIBLE:
+          {
+            gboolean visible;
+
+            xcf_read_int32 (info, (guint32 *) &visible, 1);
+
+            gimp_item_set_visible (GIMP_ITEM (*vectors), visible, FALSE);
+          }
+          break;
+
+        case PROP_COLOR_TAG:
+          {
+            GimpColorTag color_tag;
+
+            xcf_read_int32 (info, (guint32 *) &color_tag, 1);
+
+            gimp_item_set_color_tag (GIMP_ITEM (*vectors), color_tag, FALSE);
+          }
+          break;
+
+        case PROP_LOCK_CONTENT:
+          {
+            gboolean lock_content;
+
+            xcf_read_int32 (info, (guint32 *) &lock_content, 1);
+
+            if (gimp_item_can_lock_content (GIMP_ITEM (*vectors)))
+              gimp_item_set_lock_content (GIMP_ITEM (*vectors),
+                                          lock_content, FALSE);
+          }
+          break;
+
+        case PROP_LOCK_POSITION:
+          {
+            gboolean lock_position;
+
+            xcf_read_int32 (info, (guint32 *) &lock_position, 1);
+
+            if (gimp_item_can_lock_position (GIMP_ITEM (*vectors)))
+              gimp_item_set_lock_position (GIMP_ITEM (*vectors),
+                                           lock_position, FALSE);
+          }
+          break;
+
+        case PROP_LOCK_VISIBILITY:
+          {
+            gboolean lock_visibility;
+
+            xcf_read_int32 (info, (guint32 *) &lock_visibility, 1);
+
+            if (gimp_item_can_lock_visibility (GIMP_ITEM (*vectors)))
+              gimp_item_set_lock_visibility (GIMP_ITEM (*vectors),
+                                             lock_visibility, FALSE);
+          }
+          break;
+
+        case PROP_TATTOO:
+          {
+            GimpTattoo tattoo;
+
+            xcf_read_int32 (info, (guint32 *) &tattoo, 1);
+
+            gimp_item_set_tattoo (GIMP_ITEM (*vectors), tattoo);
+          }
+          break;
+
+        case PROP_PARASITES:
+          {
+            goffset base = info->cp;
+
+            while ((info->cp - base) < prop_size)
+              {
+                GimpParasite *p     = xcf_load_parasite (info);
+                GError       *error = NULL;
+
+                if (! p)
+                  return FALSE;
+
+                if (! gimp_item_parasite_validate (GIMP_ITEM (*vectors), p,
+                                                    &error))
+                  {
+                    gimp_message (info->gimp, G_OBJECT (info->progress),
+                                  GIMP_MESSAGE_WARNING,
+                                  "Warning, invalid path parasite in XCF file: %s",
+                                  error->message);
+                    g_clear_error (&error);
+                  }
+                else
+                  {
+                    gimp_item_parasite_attach (GIMP_ITEM (*vectors), p, FALSE);
+                  }
+
+                gimp_parasite_free (p);
+              }
+
+            if (info->cp - base != prop_size)
+              gimp_message_literal (info->gimp, G_OBJECT (info->progress),
+                                    GIMP_MESSAGE_WARNING,
+                                    "Error while loading a path's parasites");
+          }
+          break;
+
+#if 0
+        case PROP_ITEM_SET_ITEM:
+            {
+              GimpItemList *set;
+              guint32       n;
+
+              xcf_read_int32 (info, &n, 1);
+              set = g_list_nth_data (info->vectors_sets, n);
+              if (set == NULL)
+                g_printerr ("xcf: unknown path set: %d (skipping)\n", n);
+              else if (! g_type_is_a (G_TYPE_FROM_INSTANCE (*vectors),
+                                      gimp_item_list_get_item_type (set)))
+                g_printerr ("xcf: path '%s' cannot be added to item set '%s' with item type %s (skipping)\n",
+                            gimp_object_get_name (*vectors), gimp_object_get_name (set),
+                            g_type_name (gimp_item_list_get_item_type (set)));
+              else
+                gimp_item_list_add (set, GIMP_ITEM (*vectors));
+            }
+          break;
+#endif
+
+        default:
+#ifdef GIMP_UNSTABLE
+          g_printerr ("unexpected/unknown path property: %d (skipping)\n",
+                      prop_type);
+#endif
+          if (! xcf_skip_unknown_prop (info, prop_size))
+            return FALSE;
+          break;
+        }
+    }
+
+  return FALSE;
+}
+
 static gboolean
 xcf_load_prop (XcfInfo  *info,
                PropType *prop_type,
@@ -2552,6 +2793,162 @@ xcf_load_channel (XcfInfo   *info,
   return NULL;
 }
 
+/* The new path structure since XCF 18. */
+static GimpVectors *
+xcf_load_vectors (XcfInfo   *info,
+                  GimpImage *image)
+{
+  GimpVectors *vectors = NULL;
+  gchar       *name;
+  guint32      version;
+  guint32      plength;
+  guint32      num_strokes;
+  goffset      base;
+  gint         i;
+
+  /* read in the path name. */
+  xcf_read_string (info, &name,   1);
+
+  GIMP_LOG (XCF, "Path name='%s'", name);
+
+  /* create a new path */
+  vectors = gimp_vectors_new (image, name);
+  g_free (name);
+  if (! vectors)
+    return NULL;
+
+  /* Read the path's payload size. */
+  xcf_read_int32 (info, (guint32 *) &plength, 1);
+  base = info->cp;
+
+  /* read in the path properties */
+  if (! xcf_load_vectors_props (info, image, &vectors))
+    goto error;
+
+  GIMP_LOG (XCF, "path props loaded");
+
+  xcf_progress_update (info);
+
+  xcf_read_int32 (info, (guint32 *) &version, 1);
+
+  if (version != 1)
+    {
+      gimp_message (info->gimp, G_OBJECT (info->progress),
+                    GIMP_MESSAGE_WARNING,
+                    "Unknown vectors version: %d (skipping)", version);
+      goto error;
+    }
+
+  /* Read the number of strokes. */
+  xcf_read_int32  (info, &num_strokes, 1);
+
+  for (i = 0; i < num_strokes; i++)
+    {
+      guint32      stroke_type_id;
+      guint32      closed;
+      guint32      num_axes;
+      guint32      num_control_points;
+      guint32      type;
+      gfloat       coords[13] = GIMP_COORDS_DEFAULT_VALUES;
+      GimpStroke  *stroke;
+      gint         j;
+
+      GimpValueArray *control_points;
+      GValue          value  = G_VALUE_INIT;
+      GimpAnchor      anchor = { { 0, } };
+      GType           stroke_type;
+
+      g_value_init (&value, GIMP_TYPE_ANCHOR);
+
+      xcf_read_int32 (info, &stroke_type_id,     1);
+      xcf_read_int32 (info, &closed,             1);
+      xcf_read_int32 (info, &num_axes,           1);
+      xcf_read_int32 (info, &num_control_points, 1);
+
+#ifdef GIMP_XCF_PATH_DEBUG
+      g_printerr ("stroke_type: %d, closed: %d, num_axes %d, len %d\n",
+                  stroke_type_id, closed, num_axes, num_control_points);
+#endif
+
+      switch (stroke_type_id)
+        {
+        case XCF_STROKETYPE_BEZIER_STROKE:
+          stroke_type = GIMP_TYPE_BEZIER_STROKE;
+          break;
+
+        default:
+          g_printerr ("skipping unknown stroke type\n");
+          xcf_seek_pos (info,
+                        info->cp + 4 * num_axes * num_control_points,
+                        NULL);
+          continue;
+        }
+
+      if (num_axes < 2 || num_axes > 6)
+        {
+          g_printerr ("bad number of axes in stroke description\n");
+          goto error;
+        }
+
+      control_points = gimp_value_array_new (num_control_points);
+
+      anchor.selected = FALSE;
+
+      for (j = 0; j < num_control_points; j++)
+        {
+          xcf_read_int32 (info, &type,  1);
+          xcf_read_float (info, coords, num_axes);
+
+          anchor.type              = type;
+          anchor.position.x        = coords[0];
+          anchor.position.y        = coords[1];
+          anchor.position.pressure = coords[2];
+          anchor.position.xtilt    = coords[3];
+          anchor.position.ytilt    = coords[4];
+          anchor.position.wheel    = coords[5];
+
+          g_value_set_boxed (&value, &anchor);
+          gimp_value_array_append (control_points, &value);
+
+#ifdef GIMP_XCF_PATH_DEBUG
+          g_printerr ("Anchor: %d, (%f, %f, %f, %f, %f, %f)\n", type,
+                      coords[0], coords[1], coords[2], coords[3],
+                      coords[4], coords[5]);
+#endif
+        }
+
+      g_value_unset (&value);
+
+      stroke = g_object_new (stroke_type,
+                             "closed",         closed,
+                             "control-points", control_points,
+                             NULL);
+
+      gimp_vectors_stroke_add (vectors, stroke);
+
+      g_object_unref (stroke);
+      gimp_value_array_unref (control_points);
+    }
+
+
+  if (plength != info->cp - base)
+    {
+      gimp_message (info->gimp, G_OBJECT (info->progress),
+                    GIMP_MESSAGE_WARNING,
+                    "Path payload size does not match stored size (skipping)");
+      goto error;
+    }
+
+  return vectors;
+
+error:
+
+  xcf_seek_pos (info, base + plength, NULL);
+  g_clear_object (&vectors);
+
+  return NULL;
+}
+
 static GimpLayerMask *
 xcf_load_layer_mask (XcfInfo   *info,
                      GimpImage *image)
@@ -3174,6 +3571,7 @@ xcf_load_parasite (XcfInfo *info)
   return parasite;
 }
 
+/* Old paths are the PROP_PATHS property, even older than PROP_VECTORS. */
 static gboolean
 xcf_load_old_paths (XcfInfo   *info,
                     GimpImage *image)
@@ -3302,9 +3700,10 @@ xcf_load_old_path (XcfInfo   *info,
   return TRUE;
 }
 
+/* Old vectors are the PROP_VECTORS property up to all GIMP 2.10 versions. */
 static gboolean
-xcf_load_vectors (XcfInfo   *info,
-                  GimpImage *image)
+xcf_load_old_vectors (XcfInfo   *info,
+                      GimpImage *image)
 {
   guint32      version;
   guint32      active_index;
@@ -3312,7 +3711,7 @@ xcf_load_vectors (XcfInfo   *info,
   GimpVectors *active_vectors;
 
 #ifdef GIMP_XCF_PATH_DEBUG
-  g_printerr ("xcf_load_vectors\n");
+  g_printerr ("xcf_load_old_vectors\n");
 #endif
 
   xcf_read_int32 (info, &version, 1);
@@ -3333,7 +3732,7 @@ xcf_load_vectors (XcfInfo   *info,
 #endif
 
   while (num_paths-- > 0)
-    if (! xcf_load_vector (info, image))
+    if (! xcf_load_old_vector (info, image))
       return FALSE;
 
   /* FIXME tree */
@@ -3345,14 +3744,14 @@ xcf_load_vectors (XcfInfo   *info,
     gimp_image_set_active_vectors (image, active_vectors);
 
 #ifdef GIMP_XCF_PATH_DEBUG
-  g_printerr ("xcf_load_vectors: loaded %d bytes\n", info->cp - base);
+  g_printerr ("xcf_load_old_vectors: loaded %d bytes\n", info->cp - base);
 #endif
   return TRUE;
 }
 
 static gboolean
-xcf_load_vector (XcfInfo   *info,
-                 GimpImage *image)
+xcf_load_old_vector (XcfInfo   *info,
+                     GimpImage *image)
 {
   gchar       *name;
   GimpTattoo   tattoo = 0;
@@ -3364,7 +3763,7 @@ xcf_load_vector (XcfInfo   *info,
   gint         i;
 
 #ifdef GIMP_XCF_PATH_DEBUG
-  g_printerr ("xcf_load_vector\n");
+  g_printerr ("xcf_load_old_vector\n");
 #endif
 
   xcf_read_string (info, &name,          1);
diff --git a/app/xcf/xcf-private.h b/app/xcf/xcf-private.h
index 7b8001d98e..941caf11ba 100644
--- a/app/xcf/xcf-private.h
+++ b/app/xcf/xcf-private.h
@@ -68,6 +68,7 @@ typedef enum
   PROP_ITEM_SET           = 40,
   PROP_ITEM_SET_ITEM      = 41,
   PROP_LOCK_VISIBILITY    = 42,
+  PROP_SELECTED_PATH      = 43,
 } PropType;
 
 typedef enum
@@ -110,6 +111,7 @@ struct _XcfInfo
   GimpTattoo          tattoo_state;
   GList              *selected_layers;
   GList              *selected_channels;
+  GList              *selected_vectors;
 
   /* Old deprecated "linked" concept which we keep in the XcfInfo
    * probably forever to transform these tags into named stored item
diff --git a/app/xcf/xcf-save.c b/app/xcf/xcf-save.c
index 851df3502b..deae6d4ce7 100644
--- a/app/xcf/xcf-save.c
+++ b/app/xcf/xcf-save.c
@@ -85,6 +85,10 @@ static gboolean xcf_save_channel_props (XcfInfo           *info,
                                         GimpImage         *image,
                                         GimpChannel       *channel,
                                         GError           **error);
+static gboolean xcf_save_path_props    (XcfInfo           *info,
+                                        GimpImage         *image,
+                                        GimpVectors       *vectors,
+                                        GError           **error);
 static gboolean xcf_save_prop          (XcfInfo           *info,
                                         GimpImage         *image,
                                         PropType           prop_type,
@@ -98,6 +102,10 @@ static gboolean xcf_save_channel       (XcfInfo           *info,
                                         GimpImage         *image,
                                         GimpChannel       *channel,
                                         GError           **error);
+static gboolean xcf_save_path          (XcfInfo           *info,
+                                        GimpImage         *image,
+                                        GimpVectors       *vectors,
+                                        GError           **error);
 static gboolean xcf_save_buffer        (XcfInfo           *info,
                                         GeglBuffer        *buffer,
                                         GError           **error);
@@ -129,7 +137,7 @@ static gboolean xcf_save_parasite_list (XcfInfo           *info,
 static gboolean xcf_save_old_paths     (XcfInfo           *info,
                                         GimpImage         *image,
                                         GError           **error);
-static gboolean xcf_save_vectors       (XcfInfo           *info,
+static gboolean xcf_save_old_vectors   (XcfInfo           *info,
                                         GimpImage         *image,
                                         GError           **error);
 
@@ -224,16 +232,19 @@ xcf_save_image (XcfInfo    *info,
 {
   GList   *all_layers;
   GList   *all_channels;
+  GList   *all_paths = NULL;
   GList   *list;
   goffset  saved_pos;
   goffset  offset;
   guint32  value;
   guint    n_layers;
   guint    n_channels;
+  guint    n_paths  = 0;
   guint    progress = 0;
   guint    max_progress;
   gchar    version_tag[16];
-  GError  *tmp_error = NULL;
+  gboolean write_paths = FALSE;
+  GError  *tmp_error   = NULL;
 
   /* write out the tag information for the image */
   if (info->file_version > 0)
@@ -264,6 +275,9 @@ xcf_save_image (XcfInfo    *info,
       xcf_write_int32_check_error (info, &value, 1);
     }
 
+  if (info->file_version >= 18)
+    write_paths = TRUE;
+
   /* determine the number of layers and channels in the image */
   all_layers   = gimp_image_get_layer_list (image);
   all_channels = gimp_image_get_channel_list (image);
@@ -277,7 +291,13 @@ xcf_save_image (XcfInfo    *info,
   n_layers   = (guint) g_list_length (all_layers);
   n_channels = (guint) g_list_length (all_channels);
 
-  max_progress = 1 + n_layers + n_channels;
+  if (write_paths)
+    {
+      all_paths = gimp_image_get_vectors_list (image);
+      n_paths   = (guint) g_list_length (all_paths);
+    }
+
+  max_progress = 1 + n_layers + n_channels + n_paths;
 
   /* write the property information for the image */
   xcf_check_error (xcf_save_image_props (info, image, error));
@@ -288,7 +308,9 @@ xcf_save_image (XcfInfo    *info,
   saved_pos = info->cp;
 
   /* write an empty offset table */
-  xcf_write_zero_offset_check_error (info, n_layers + n_channels + 2);
+  xcf_write_zero_offset_check_error (info,
+                                     n_layers + n_channels + 2 +
+                                     (write_paths ? n_paths + 1 : 0));
 
   /* 'offset' is where we will write the next layer or channel */
   offset = info->cp;
@@ -344,12 +366,44 @@ xcf_save_image (XcfInfo    *info,
       xcf_progress_update (info);
     }
 
+  if (write_paths)
+    {
+      /* skip a '0' in the offset table to indicate the end of the channel
+       * offsets
+       */
+      saved_pos += info->bytes_per_offset;
+
+      for (list = all_paths; list; list = g_list_next (list))
+        {
+          GimpVectors *vectors = list->data;
+
+          /* seek back to the next slot in the offset table and write the
+           * offset of the channel
+           */
+          xcf_check_error (xcf_seek_pos (info, saved_pos, error));
+          xcf_write_offset_check_error (info, &offset, 1);
+
+          /* remember the next slot in the offset table */
+          saved_pos = info->cp;
+
+          /* seek to the channel offset and save the channel */
+          xcf_check_error (xcf_seek_pos (info, offset, error));
+          xcf_check_error (xcf_save_path (info, image, vectors, error));
+
+          /* the next channels's offset is after the channel we just wrote */
+          offset = info->cp;
+
+          xcf_progress_update (info);
+        }
+    }
+
   /* there is already a '0' at the end of the offset table to indicate
    * the end of the channel offsets
    */
 
   g_list_free (all_layers);
   g_list_free (all_channels);
+  g_list_free (all_paths);
 
   return ! g_output_stream_is_closed (info->output);
 }
@@ -408,7 +462,8 @@ xcf_save_image_props (XcfInfo    *info,
   if (unit < gimp_unit_get_number_of_built_in_units ())
     xcf_check_error (xcf_save_prop (info, image, PROP_UNIT, error, unit));
 
-  if (gimp_container_get_n_children (gimp_image_get_vectors (image)) > 0)
+  if (gimp_container_get_n_children (gimp_image_get_vectors (image)) > 0 &&
+      info->file_version < 18)
     {
       if (gimp_vectors_compat_is_compatible (image))
         xcf_check_error (xcf_save_prop (info, image, PROP_PATHS, error));
@@ -714,6 +769,60 @@ xcf_save_channel_props (XcfInfo      *info,
   return TRUE;
 }
 
+static gboolean
+xcf_save_path_props (XcfInfo      *info,
+                     GimpImage    *image,
+                     GimpVectors  *vectors,
+                     GError      **error)
+{
+  GimpParasiteList *parasites;
+
+  if (g_list_find (gimp_image_get_selected_vectors (image), vectors))
+    xcf_check_error (xcf_save_prop (info, image, PROP_SELECTED_PATH, error));
+
+  xcf_check_error (xcf_save_prop (info, image, PROP_VISIBLE, error,
+                                  gimp_item_get_visible (GIMP_ITEM (vectors))));
+  xcf_check_error (xcf_save_prop (info, image, PROP_COLOR_TAG, error,
+                                  gimp_item_get_color_tag (GIMP_ITEM (vectors))));
+  xcf_check_error (xcf_save_prop (info, image, PROP_LOCK_CONTENT, error,
+                                  gimp_item_get_lock_content (GIMP_ITEM (vectors))));
+  xcf_check_error (xcf_save_prop (info, image, PROP_LOCK_POSITION, error,
+                                  gimp_item_get_lock_position (GIMP_ITEM (vectors))));
+
+  xcf_check_error (xcf_save_prop (info, image, PROP_TATTOO, error,
+                                  gimp_item_get_tattoo (GIMP_ITEM (vectors))));
+
+  parasites = gimp_item_get_parasites (GIMP_ITEM (vectors));
+
+  if (gimp_parasite_list_length (parasites) > 0)
+    {
+      xcf_check_error (xcf_save_prop (info, image, PROP_PARASITES, error,
+                                      parasites));
+    }
+
+#if 0
+  for (iter = info->vectors_sets; iter; iter = iter->next)
+    {
+      GimpItemList *set = iter->data;
+
+      if (! gimp_item_list_is_pattern (set, NULL))
+        {
+          GList *items = gimp_item_list_get_items (set, NULL);
+
+          if (g_list_find (items, GIMP_ITEM (vectors)))
+            xcf_check_error (xcf_save_prop (info, image, PROP_ITEM_SET_ITEM, error,
+                                            g_list_position (info->layer_sets, iter)));
+
+          g_list_free (items);
+        }
+    }
+#endif
+
+  xcf_check_error (xcf_save_prop (info, image, PROP_END, error));
+
+  return TRUE;
+}
+
 static gboolean
 xcf_save_prop (XcfInfo    *info,
                GimpImage  *image,
@@ -753,6 +862,7 @@ xcf_save_prop (XcfInfo    *info,
 
     case PROP_ACTIVE_LAYER:
     case PROP_ACTIVE_CHANNEL:
+    case PROP_SELECTED_PATH:
     case PROP_SELECTION:
     case PROP_GROUP_ITEM:
       size = 0;
@@ -1352,7 +1462,7 @@ xcf_save_prop (XcfInfo    *info,
 
         base = info->cp;
 
-        xcf_check_error (xcf_save_vectors (info, image, error));
+        xcf_check_error (xcf_save_old_vectors (info, image, error));
 
         size = info->cp - base;
 
@@ -2124,12 +2234,13 @@ xcf_save_parasite_list (XcfInfo           *info,
   return TRUE;
 }
 
+/* This is the oldest way to save paths. */
 static gboolean
 xcf_save_old_paths (XcfInfo    *info,
                     GimpImage  *image,
                     GError    **error)
 {
-  GimpVectors *active_vectors;
+  GimpVectors *active_vectors = NULL;
   guint32      num_paths;
   guint32      active_index = 0;
   GList       *list;
@@ -2145,7 +2256,16 @@ xcf_save_old_paths (XcfInfo    *info,
 
   num_paths = gimp_container_get_n_children (gimp_image_get_vectors (image));
 
-  active_vectors = gimp_image_get_active_vectors (image);
+  if (gimp_image_get_selected_vectors (image))
+    {
+      active_vectors = gimp_image_get_selected_vectors (image)->data;
+      /* Having more than 1 selected vectors should not have happened in this
+       * code path but let's not break saving, only produce a critical.
+       */
+      if (g_list_length (gimp_image_get_selected_vectors (image)) > 1)
+        g_critical ("%s: this code path should not happen with multiple paths selected",
+                    G_STRFUNC);
+    }
 
   if (active_vectors)
     active_index = gimp_container_get_child_index (gimp_image_get_vectors (image),
@@ -2233,14 +2353,18 @@ xcf_save_old_paths (XcfInfo    *info,
   return TRUE;
 }
 
+/* This is an older way to save paths, though more recent than
+ * xcf_save_old_paths(). It used to be the normal path storing format until all
+ * 2.10 versions. It changed with GIMP 3.0.
+ */
 static gboolean
-xcf_save_vectors (XcfInfo    *info,
-                  GimpImage  *image,
-                  GError    **error)
+xcf_save_old_vectors (XcfInfo    *info,
+                      GimpImage  *image,
+                      GError    **error)
 {
-  GimpVectors *active_vectors;
-  guint32      version      = 1;
-  guint32      active_index = 0;
+  GimpVectors *active_vectors = NULL;
+  guint32      version        = 1;
+  guint32      active_index   = 0;
   guint32      num_paths;
   GList       *list;
   GList       *stroke_list;
@@ -2255,7 +2379,16 @@ xcf_save_vectors (XcfInfo    *info,
    * then each path:-
    */
 
-  active_vectors = gimp_image_get_active_vectors (image);
+  if (gimp_image_get_selected_vectors (image))
+    {
+      active_vectors = gimp_image_get_selected_vectors (image)->data;
+      /* Having more than 1 selected vectors should not have happened in this
+       * code path but let's not break saving, only produce a critical.
+       */
+      if (g_list_length (gimp_image_get_selected_vectors (image)) > 1)
+        g_critical ("%s: this code path should not happen with multiple paths selected",
+                    G_STRFUNC);
+    }
 
   if (active_vectors)
     active_index = gimp_container_get_child_index (gimp_image_get_vectors (image),
@@ -2388,3 +2521,128 @@ xcf_save_vectors (XcfInfo    *info,
 
   return TRUE;
 }
+
+static gboolean
+xcf_save_path (XcfInfo      *info,
+               GimpImage    *image,
+               GimpVectors  *vectors,
+               GError      **error)
+{
+  const gchar *string;
+  GList       *stroke_list;
+  GError      *tmp_error = NULL;
+  /* Version of the path format is always 1 for now. */
+  guint32      version   = 1;
+  guint32      num_strokes;
+  guint32      size;
+  goffset      base;
+  goffset      pos;
+
+  /* write out the path name */
+  string = gimp_object_get_name (vectors);
+  xcf_write_string_check_error (info, (gchar **) &string, 1);
+
+  /* Payload size */
+  size = 0;
+  pos = info->cp;
+  xcf_write_int32_check_error (info, &size, 1);
+  base = info->cp;
+
+  /* write out the path properties */
+  xcf_save_path_props (info, image, vectors, error);
+
+  /* Path version */
+  xcf_write_int32_check_error (info, &version, 1);
+
+  /* Write out the number of strokes. */
+  num_strokes = g_queue_get_length (vectors->strokes);
+  xcf_write_int32_check_error  (info, &num_strokes, 1);
+
+  for (stroke_list = g_list_first (vectors->strokes->head);
+       stroke_list;
+       stroke_list = g_list_next (stroke_list))
+    {
+      GimpStroke *stroke = stroke_list->data;
+      guint32     stroke_type;
+      guint32     closed;
+      guint32     num_axes;
+      GArray     *control_points;
+      gint        i;
+
+      guint32     type;
+      gfloat      coords[6];
+
+      /*
+       * stroke_type (gint)
+       * closed (gint)
+       * num_axes (gint)
+       * num_control_points (gint)
+       *
+       * then each control point.
+       */
+
+      if (GIMP_IS_BEZIER_STROKE (stroke))
+        {
+          stroke_type = XCF_STROKETYPE_BEZIER_STROKE;
+          num_axes = 2;   /* hardcoded, might be increased later */
+        }
+      else
+        {
+          g_printerr ("Skipping unknown stroke type!\n");
+          continue;
+        }
+
+      control_points = gimp_stroke_control_points_get (stroke,
+                                                       (gint32 *) &closed);
+
+      /* Stroke type. */
+      xcf_write_int32_check_error (info, &stroke_type,         1);
+      /* close path or not? */
+      xcf_write_int32_check_error (info, &closed,              1);
+      /* Number of floats given for each point. */
+      xcf_write_int32_check_error (info, &num_axes,            1);
+      /* Number of control points. */
+      xcf_write_int32_check_error (info, &control_points->len, 1);
+
+      for (i = 0; i < control_points->len; i++)
+        {
+          GimpAnchor *anchor;
+
+          anchor = & (g_array_index (control_points, GimpAnchor, i));
+
+          type      = anchor->type;
+          coords[0] = anchor->position.x;
+          coords[1] = anchor->position.y;
+          coords[2] = anchor->position.pressure;
+          coords[3] = anchor->position.xtilt;
+          coords[4] = anchor->position.ytilt;
+          coords[5] = anchor->position.wheel;
+
+          /*
+           * type (gint)
+           *
+           * the first num_axis elements of:
+           * [0] x (gfloat)
+           * [1] y (gfloat)
+           * [2] pressure (gfloat)
+           * [3] xtilt (gfloat)
+           * [4] ytilt (gfloat)
+           * [5] wheel (gfloat)
+           */
+
+          xcf_write_int32_check_error (info, &type,  1);
+          xcf_write_float_check_error (info, coords, num_axes);
+        }
+
+      g_array_free (control_points, TRUE);
+    }
+
+  /* go back to the saved position and write the length */
+  size = info->cp - base;
+  xcf_check_error (xcf_seek_pos (info, pos, error));
+  xcf_write_int32_check_error (info, &size, 1);
+
+  xcf_check_error (xcf_seek_pos (info, base + size, error));
+
+  return TRUE;
+}
diff --git a/app/xcf/xcf.c b/app/xcf/xcf.c
index 5509e54a8a..6164f55734 100644
--- a/app/xcf/xcf.c
+++ b/app/xcf/xcf.c
@@ -86,6 +86,7 @@ static GimpXcfLoaderFunc * const xcf_loaders[] =
   xcf_load_image,   /* version 15 */
   xcf_load_image,   /* version 16 */
   xcf_load_image,   /* version 17 */
+  xcf_load_image,   /* version 18 */
 };
 
 
diff --git a/devel-docs/specifications/xcf.txt b/devel-docs/specifications/xcf.txt
index 4900bc7c7d..b606ef6df3 100644
--- a/devel-docs/specifications/xcf.txt
+++ b/devel-docs/specifications/xcf.txt
@@ -199,6 +199,11 @@ Since GIMP 3.0.0, released on TODO.
 - New PROP_LOCK_VISIBILITY on layers and channels.
 - PROP_LOCK_POSITION and PROP_LOCK_ALPHA can now be set on layer groups.
 
+Vection 18:
+Since GIMP 3.0.0, released on TODO.
+- New Vectors Structure for storing paths with properties.
+
+
 1. BASIC CONCEPTS
 =================
 
@@ -506,8 +511,9 @@ names which will be ignored by other plug-ins.
 A list of known parasites and their data formats can be found in the
 file devel-doc/parasites.txt of the GIMP source tree.
 
-The PROP_PARASITE property stores the parasites of the image, layers
-and channels and the PROP_VECTORS property those of the paths.
+The PROP_PARASITE property stores the parasites of the image, layers, channels
+and vectors structures. The PROP_VECTORS stores those of paths when using this
+image property instead of proper vectors structure.
 
 The number of parasites there is not directly encoded; the list ends when
 the total length of the parasite data read equals the property payload length.
@@ -602,6 +608,11 @@ If all paths are continuous sequences of Bezier strokes, then GIMP uses
 the PROP_PATHS property, otherwise PROP_VECTORS. PROP_PATHS is for old
 files from GIMP up to version 1.2.
 
+If more than 2 paths are selected or if a path have a color tag, a content lock
+or a position lock, GIMP will use vectors structures, which bumps the XCF to at
+least version 18. If the XCF is already 18 or later, GIMP will always use
+vectors structure.
+
 
 2. GENERAL PROPERTIES
 =====================
@@ -728,8 +739,9 @@ PROP_TATTOO (internal GIMP state)
   uint32  4          Four bytes of payload
   uint32  tattoo     Nonzero unsigned integer identifier
 
-  PROP_TATTOO is an unique identifier for the denoted image, channel or layer.
-  It appears in the property list of layers, channels, and the image.
+  PROP_TATTOO is an unique identifier for the denoted image, channel, layer or
+  path.
+  It appears in the property list of layers, channels, vectors and the image.
 
 PROP_VISIBLE (essential)
   uint32  8          Type identification
@@ -737,8 +749,10 @@ PROP_VISIBLE (essential)
   uint32  visible    1 if the layer/channel is visible; 0 if not
 
   PROP_VISIBLE specifies the visibility of a layer or channel.
-  It appears in the property list for layers and channels.
-  For the visibility of a path see the PROP_VECTORS property.
+  It appears in the property list for layers, channels and paths.
+
+  For the visibility of a path stored with the older PROP_VECTORS property, see
+  this property's description.
 
   When reading old XCF files that lack this property, assume that
   layers are visible and channels are not.
@@ -827,6 +841,10 @@ The image structure always starts at offset 0 in the XCF file.
   | pointer cptr       Pointer to the channel structure.
   `--
   pointer   0           Zero marks the end of the array of channel pointers.
+  ,------------------  Repeat once for each path, in no particular order:
+  | pointer cptr       Pointer to the vectors structure.
+  `--
+  pointer   0           Zero marks the end of the array of vectors pointers.
 
 The last 4 characters of the initial 13-character identification string are
 a version indicator. The version will be higher than 3 if the correct
@@ -960,7 +978,7 @@ PROP_PATHS
 
   This format is used to save path data if all paths in the image are
   continuous sequences of Bezier strokes. Otherwise GIMP stores the paths in
-  PROP_VECTORS.
+  PROP_VECTORS or in Vectors Structure.
 
   Note: the attribute 'linked' was formerly erroneously called 'locked'
   (but meant 'linked' anyway).
@@ -1526,8 +1544,54 @@ PROP_TEXT_LAYER_FLAGS
   The actual text (and other parameters such as font and color) is a
   parasite rather than a property.
 
+6. THE VECTORS STRUCTURE
+========================
+
+Vectors structures are pointed to from the master image structure.
+
+  string    name       Name of the path
+  uint32    plength    Total length of the following payload in bytes
+  property-list        Vectors properties
+  uint32    1          Version tag; so far always 1
+  uint32    k          Number of strokes in the path
+  ,-------------------- Repeat k times:
+  | uint32   1          The stroke is a Bezier stroke
+  | uint32   closed     1 if path is closed; 0 otherwise
+  | uint32   nf         Number of floats given for each point;
+  |                     must be >= 2 and <= 6.
+  | uint32   np         Number of control points for this stroke
+  | ,------------------ Repeat np times:
+  | | uint32 type       Type of the first point; one of
+  | |                     0: Anchor
+  | |                     1: Bezier control point
+  | | float  x          X coordinate
+  | | float  y          Y coordinate
+  | | float  pressure   Only if nf >= 3; otherwise defaults to 1.0
+  | | float  xtilt      Only if nf >= 4; otherwise defaults to 0.5
+  | | float  ytilt      Only if nf >= 5; otherwise defaults to 0.5
+  | | float  wheel      Only if nf == 6; otherwise defaults to 0.5
+  | `--
+  `--
+
+Vectors properties
+------------------
+
+The following properties are found only in the property list of
+vectors structures. Additionally the list can also contain the
+properties: PROP_COLOR_TAG, PROP_END, PROP_LOCK_CONTENT, PROP_LOCK_POSITION,
+PROP_PARASITES, PROP_TATTOO and PROP_VISIBLE, defined in chapter 2.
+
+PROP_SELECTED_PATH (editing state)
+  uint32  43     Type identification
+  uint32  0      PROP_SELECTED_PATH has no payload
+
+  The presence of PROP_SELECTED_PATH indicates that the path is currently
+  selected.
+  It appears in the property list of any currently selected path.
+  Any number of paths (or none) can have this property at any time.
+
 
-6. THE HIERARCHY STRUCTURE
+7. THE HIERARCHY STRUCTURE
 ==========================
 
 A hierarchy contains data for a rectangular array of pixels.
@@ -1581,7 +1645,7 @@ hierarchy structure (except for the dummy levels).
 Ceil(x) is the smallest integer not smaller than x.
 
 
-7. TILE DATA ORGANIZATION
+8. TILE DATA ORGANIZATION
 =========================
 
 The format of the data blocks pointed to by the tile pointers in the
@@ -1683,7 +1747,7 @@ TODO: If each tile has a maximum of 64 pixels (resulting in a maximum of 64
 bytes for each color in this tile), do values>64 and long runs apply at all?
 
 
-8. MISCELLANEOUS
+9. MISCELLANEOUS
 ================
 
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]