[gimp/symmetry: 2/6] Bug 648776 - mirror symmetries.



commit b5e4a1db88ff4b56ac2441f89f7fd8ab8b1a79de
Author: Jehan <jehan girinstud io>
Date:   Mon Apr 29 10:53:36 2013 +0900

    Bug 648776 - mirror symmetries.
    
    You can now set any paint tool to mirror painting relatively
    horizontal/vertical axis or a central point (any combination of these 3
    symmetries).
    This has been implemented as a new multi-stroke core, where every stroke
    is actually handled as a multi-stroke (default of size 1).
    This is also the first usage of custom guides for symmetry guiding.
    Current version has to be activated in the playground.

 app/actions/dialogs-actions.c      |    6 +
 app/config/gimpguiconfig.c         |   14 +
 app/config/gimpguiconfig.h         |    1 +
 app/config/gimprc-blurbs.h         |    3 +
 app/core/Makefile.am               |    6 +
 app/core/core-types.h              |    5 +
 app/core/gimpbrush-boundary.c      |    2 +-
 app/core/gimpbrush.c               |   64 +++-
 app/core/gimpbrush.h               |    2 +
 app/core/gimpbrushcache.c          |  121 +++++--
 app/core/gimpbrushcache.h          |   10 +-
 app/core/gimpimage-private.h       |    3 +
 app/core/gimpimage-symmetry.c      |  192 ++++++++++
 app/core/gimpimage-symmetry.h      |   38 ++
 app/core/gimpimage.c               |   60 +++-
 app/core/gimpsymmetry-mirror.c     |  701 ++++++++++++++++++++++++++++++++++++
 app/core/gimpsymmetry-mirror.h     |   67 ++++
 app/core/gimpsymmetry.c            |  451 +++++++++++++++++++++++
 app/core/gimpsymmetry.h            |   94 +++++
 app/dialogs/dialogs-constructors.c |   12 +
 app/dialogs/dialogs-constructors.h |    4 +
 app/dialogs/dialogs.c              |    4 +
 app/dialogs/preferences-dialog.c   |    3 +
 app/display/display-enums.c        |   31 ++
 app/display/display-enums.h        |   10 +
 app/paint/gimpairbrush.c           |   46 ++-
 app/paint/gimpairbrush.h           |    2 +
 app/paint/gimpbrushcore.c          |   53 ++--
 app/paint/gimpbrushcore.h          |    8 +-
 app/paint/gimpclone.c              |   26 ++-
 app/paint/gimpconvolve.c           |  143 +++++---
 app/paint/gimpdodgeburn.c          |  105 ++++---
 app/paint/gimperaser.c             |   90 +++--
 app/paint/gimpheal.c               |   27 ++-
 app/paint/gimpink.c                |  263 +++++++++-----
 app/paint/gimpink.h                |    6 +-
 app/paint/gimpinkundo.c            |   32 ++-
 app/paint/gimpinkundo.h            |    2 +-
 app/paint/gimpmybrush.c            |  267 ++++++++++++++
 app/paint/gimpmybrushcore.c        |  228 +++++++++---
 app/paint/gimppaintbrush.c         |  163 +++++----
 app/paint/gimppaintbrush.h         |    2 +-
 app/paint/gimppaintcore-loops.c    |   25 +-
 app/paint/gimppaintcore.c          |   42 ++-
 app/paint/gimppaintcore.h          |   10 +-
 app/paint/gimpperspectiveclone.c   |   69 +++--
 app/paint/gimpsmudge.c             |  373 +++++++++++--------
 app/paint/gimpsmudge.h             |    2 +-
 app/paint/gimpsourcecore.c         |  163 +++++----
 app/paint/gimpsourcecore.h         |    3 +-
 app/pdb/image-guides-cmds.c        |    1 +
 app/widgets/Makefile.am            |    2 +
 app/widgets/gimphelp-ids.h         |    1 +
 app/widgets/gimpsymmetryeditor.c   |  506 ++++++++++++++++++++++++++
 app/widgets/gimpsymmetryeditor.h   |   58 +++
 app/widgets/widgets-types.h        |    1 +
 app/xcf/xcf-load.c                 |   37 ++
 app/xcf/xcf-save.c                 |   37 ++-
 icons/Color/16/gimp-symmetry.png   |  Bin 0 -> 4828 bytes
 icons/Color/24/gimp-symmetry.png   |  Bin 0 -> 1352 bytes
 icons/Color/24/gimp-symmetry.xcf   |  Bin 0 -> 2456 bytes
 icons/Color/Makefile.am            |    2 +
 libgimpwidgets/gimpicons.h         |    1 +
 menus/dialogs-menuitems.xml        |    1 +
 64 files changed, 3965 insertions(+), 736 deletions(-)
---
diff --git a/app/actions/dialogs-actions.c b/app/actions/dialogs-actions.c
index a0fbf45..4d197c1 100644
--- a/app/actions/dialogs-actions.c
+++ b/app/actions/dialogs-actions.c
@@ -61,6 +61,12 @@ const GimpStringActionEntry dialogs_dockable_actions[] =
     "gimp-device-status",
     GIMP_HELP_DEVICE_STATUS_DIALOG },
 
+  { "dialogs-symmetry", GIMP_STOCK_SYMMETRY,
+    NC_("dialogs-action", "_Symmetry painting"), NULL,
+    NC_("dialogs-action", "Open the symmetry dialog"),
+    "gimp-symmetry-editor",
+    GIMP_HELP_SYMMETRY_DIALOG },
+
   { "dialogs-layers", GIMP_STOCK_LAYERS,
     NC_("dialogs-action", "_Layers"), "<primary>L",
     NC_("dialogs-action", "Open the layers dialog"),
diff --git a/app/config/gimpguiconfig.c b/app/config/gimpguiconfig.c
index ef08362..3ec5369 100644
--- a/app/config/gimpguiconfig.c
+++ b/app/config/gimpguiconfig.c
@@ -83,6 +83,7 @@ enum
   PROP_PLAYGROUND_NPD_TOOL,
   PROP_PLAYGROUND_HANDLE_TRANSFORM_TOOL,
   PROP_PLAYGROUND_SEAMLESS_CLONE_TOOL,
+  PROP_PLAYGROUND_SYMMETRY,
 
   PROP_HIDE_DOCKS,
   PROP_SINGLE_WINDOW_MODE,
@@ -301,6 +302,13 @@ gimp_gui_config_class_init (GimpGuiConfigClass *klass)
                                     GIMP_PARAM_STATIC_STRINGS |
                                     GIMP_CONFIG_PARAM_RESTART);
   GIMP_CONFIG_INSTALL_PROP_BOOLEAN (object_class,
+                                    PROP_PLAYGROUND_SYMMETRY,
+                                    "playground-symmetry",
+                                    PLAYGROUND_SYMMETRY_BLURB,
+                                    FALSE,
+                                    GIMP_PARAM_STATIC_STRINGS |
+                                    GIMP_CONFIG_PARAM_RESTART);
+  GIMP_CONFIG_INSTALL_PROP_BOOLEAN (object_class,
                                     PROP_PLAYGROUND_SEAMLESS_CLONE_TOOL,
                                     "playground-seamless-clone-tool",
                                     PLAYGROUND_SEAMLESS_CLONE_TOOL_BLURB,
@@ -528,6 +536,9 @@ gimp_gui_config_set_property (GObject      *object,
     case PROP_PLAYGROUND_HANDLE_TRANSFORM_TOOL:
       gui_config->playground_handle_transform_tool = g_value_get_boolean (value);
       break;
+    case PROP_PLAYGROUND_SYMMETRY:
+      gui_config->playground_symmetry = g_value_get_boolean (value);
+      break;
     case PROP_PLAYGROUND_SEAMLESS_CLONE_TOOL:
       gui_config->playground_seamless_clone_tool = g_value_get_boolean (value);
       break;
@@ -678,6 +689,9 @@ gimp_gui_config_get_property (GObject    *object,
     case PROP_PLAYGROUND_HANDLE_TRANSFORM_TOOL:
       g_value_set_boolean (value, gui_config->playground_handle_transform_tool);
       break;
+    case PROP_PLAYGROUND_SYMMETRY:
+      g_value_set_boolean (value, gui_config->playground_symmetry);
+      break;
     case PROP_PLAYGROUND_SEAMLESS_CLONE_TOOL:
       g_value_set_boolean (value, gui_config->playground_seamless_clone_tool);
       break;
diff --git a/app/config/gimpguiconfig.h b/app/config/gimpguiconfig.h
index d487d0e..309c2fe 100644
--- a/app/config/gimpguiconfig.h
+++ b/app/config/gimpguiconfig.h
@@ -83,6 +83,7 @@ struct _GimpGuiConfig
   gboolean             playground_npd_tool;
   gboolean             playground_handle_transform_tool;
   gboolean             playground_seamless_clone_tool;
+  gboolean             playground_symmetry;
 
   /* saved in sessionrc */
   gboolean             hide_docks;
diff --git a/app/config/gimprc-blurbs.h b/app/config/gimprc-blurbs.h
index e4ae186..8424599 100644
--- a/app/config/gimprc-blurbs.h
+++ b/app/config/gimprc-blurbs.h
@@ -390,6 +390,9 @@ _("Enable the N-Point Deformation tool.")
 #define PLAYGROUND_HANDLE_TRANSFORM_TOOL_BLURB \
 _("Enable the Handle Transform tool.")
 
+#define PLAYGROUND_SYMMETRY_BLURB \
+_("Enable symmetry on painting.")
+
 #define PLAYGROUND_MYBRUSH_TOOL_BLURB \
 _("Enable the MyPaint Brush tool.")
 
diff --git a/app/core/Makefile.am b/app/core/Makefile.am
index c4ce10f..f8bbf2f 100644
--- a/app/core/Makefile.am
+++ b/app/core/Makefile.am
@@ -259,6 +259,8 @@ libappcore_a_sources = \
        gimpimage-scale.h                       \
        gimpimage-snap.c                        \
        gimpimage-snap.h                        \
+       gimpimage-symmetry.c                    \
+       gimpimage-symmetry.h                    \
        gimpimage-undo.c                        \
        gimpimage-undo.h                        \
        gimpimage-undo-push.c                   \
@@ -367,6 +369,10 @@ libappcore_a_sources = \
        gimpstrokeoptions.h                     \
        gimpsubprogress.c                       \
        gimpsubprogress.h                       \
+       gimpsymmetry.c                  \
+       gimpsymmetry.h                  \
+       gimpsymmetry-mirror.c           \
+       gimpsymmetry-mirror.h   \
        gimptag.c                               \
        gimptag.h                               \
        gimptagcache.c                          \
diff --git a/app/core/core-types.h b/app/core/core-types.h
index 6ab8f05..aa9121c 100644
--- a/app/core/core-types.h
+++ b/app/core/core-types.h
@@ -175,6 +175,11 @@ typedef struct _GimpUndoStack         GimpUndoStack;
 typedef struct _GimpUndoAccumulator   GimpUndoAccumulator;
 
 
+/* Symmetry transformations */
+
+typedef struct _GimpSymmetry        GimpSymmetry;
+typedef struct _GimpMirror          GimpMirror;
+
 /*  misc objects  */
 
 typedef struct _GimpBuffer          GimpBuffer;
diff --git a/app/core/gimpbrush-boundary.c b/app/core/gimpbrush-boundary.c
index c1573a5..a9efbec 100644
--- a/app/core/gimpbrush-boundary.c
+++ b/app/core/gimpbrush-boundary.c
@@ -39,7 +39,7 @@ gimp_brush_transform_boundary_exact (GimpBrush *brush,
 {
   const GimpTempBuf *mask;
 
-  mask = gimp_brush_transform_mask (brush,
+  mask = gimp_brush_transform_mask (brush, NULL,
                                     scale, aspect_ratio, angle, hardness);
 
   if (mask)
diff --git a/app/core/gimpbrush.c b/app/core/gimpbrush.c
index e57f0b4..c8d987c 100644
--- a/app/core/gimpbrush.c
+++ b/app/core/gimpbrush.c
@@ -313,12 +313,12 @@ gimp_brush_get_new_preview (GimpViewable *viewable,
             {
                GimpBrushGenerated *gen_brush = GIMP_BRUSH_GENERATED (brush);
 
-               mask_buf = gimp_brush_transform_mask (brush, scale,
+               mask_buf = gimp_brush_transform_mask (brush, NULL, scale,
                                                      0.0, 0.0,
                                                      gimp_brush_generated_get_hardness (gen_brush));
             }
           else
-            mask_buf = gimp_brush_transform_mask (brush, scale,
+            mask_buf = gimp_brush_transform_mask (brush, NULL, scale,
                                                   0.0, 0.0, 1.0);
 
           if (! mask_buf)
@@ -332,7 +332,7 @@ gimp_brush_get_new_preview (GimpViewable *viewable,
             }
 
           if (pixmap_buf)
-            pixmap_buf = gimp_brush_transform_pixmap (brush, scale,
+            pixmap_buf = gimp_brush_transform_pixmap (brush, NULL, scale,
                                                       0.0, 0.0, 1.0);
 
           mask_width  = gimp_temp_buf_get_width  (mask_buf);
@@ -611,6 +611,7 @@ gimp_brush_transform_size (GimpBrush     *brush,
 
 const GimpTempBuf *
 gimp_brush_transform_mask (GimpBrush *brush,
+                           GeglNode  *op,
                            gdouble    scale,
                            gdouble    aspect_ratio,
                            gdouble    angle,
@@ -628,7 +629,7 @@ gimp_brush_transform_mask (GimpBrush *brush,
                              &width, &height);
 
   mask = gimp_brush_cache_get (brush->priv->mask_cache,
-                               width, height,
+                               op, width, height,
                                scale, aspect_ratio, angle, hardness);
 
   if (! mask)
@@ -649,9 +650,31 @@ gimp_brush_transform_mask (GimpBrush *brush,
                                                                hardness);
         }
 
+      if (op)
+        {
+          GeglNode    *graph, *source, *target;
+          GeglBuffer  *buffer = gimp_temp_buf_create_buffer ((GimpTempBuf *) mask);
+
+          graph    = gegl_node_new ();
+          source   = gegl_node_new_child (graph,
+                                          "operation", "gegl:buffer-source",
+                                          "buffer", buffer,
+                                          NULL);
+          gegl_node_add_child (graph, op);
+          target  = gegl_node_new_child (graph,
+                                         "operation", "gegl:write-buffer",
+                                         "buffer", buffer,
+                                         NULL);
+
+          gegl_node_link_many (source, op, target, NULL);
+          gegl_node_process (target);
+
+          g_object_unref (graph);
+          g_object_unref (buffer);
+        }
       gimp_brush_cache_add (brush->priv->mask_cache,
                             (gpointer) mask,
-                            width, height,
+                            op, width, height,
                             scale, aspect_ratio, angle, hardness);
     }
 
@@ -660,6 +683,7 @@ gimp_brush_transform_mask (GimpBrush *brush,
 
 const GimpTempBuf *
 gimp_brush_transform_pixmap (GimpBrush *brush,
+                             GeglNode  *op,
                              gdouble    scale,
                              gdouble    aspect_ratio,
                              gdouble    angle,
@@ -678,7 +702,7 @@ gimp_brush_transform_pixmap (GimpBrush *brush,
                              &width, &height);
 
   pixmap = gimp_brush_cache_get (brush->priv->pixmap_cache,
-                                 width, height,
+                                 op, width, height,
                                  scale, aspect_ratio, angle, hardness);
 
   if (! pixmap)
@@ -699,9 +723,31 @@ gimp_brush_transform_pixmap (GimpBrush *brush,
                                                                    hardness);
         }
 
+      if (op)
+        {
+          GeglNode    *graph, *source, *target;
+          GeglBuffer  *buffer = gimp_temp_buf_create_buffer ((GimpTempBuf *) pixmap);
+
+          graph    = gegl_node_new ();
+          source   = gegl_node_new_child (graph,
+                                          "operation", "gegl:buffer-source",
+                                          "buffer", buffer,
+                                          NULL);
+          gegl_node_add_child (graph, op);
+          target  = gegl_node_new_child (graph,
+                                         "operation", "gegl:write-buffer",
+                                         "buffer", buffer,
+                                         NULL);
+
+          gegl_node_link_many (source, op, target, NULL);
+          gegl_node_process (target);
+
+          g_object_unref (graph);
+          g_object_unref (buffer);
+        }
       gimp_brush_cache_add (brush->priv->pixmap_cache,
                             (gpointer) pixmap,
-                            width, height,
+                            op, width, height,
                             scale, aspect_ratio, angle, hardness);
     }
 
@@ -729,7 +775,7 @@ gimp_brush_transform_boundary (GimpBrush *brush,
                              width, height);
 
   boundary = gimp_brush_cache_get (brush->priv->boundary_cache,
-                                   *width, *height,
+                                   NULL, *width, *height,
                                    scale, aspect_ratio, angle, hardness);
 
   if (! boundary)
@@ -751,7 +797,7 @@ gimp_brush_transform_boundary (GimpBrush *brush,
       if (boundary)
         gimp_brush_cache_add (brush->priv->boundary_cache,
                               (gpointer) boundary,
-                              *width, *height,
+                              NULL, *width, *height,
                               scale, aspect_ratio, angle, hardness);
     }
 
diff --git a/app/core/gimpbrush.h b/app/core/gimpbrush.h
index 53e2762..42390d1 100644
--- a/app/core/gimpbrush.h
+++ b/app/core/gimpbrush.h
@@ -109,11 +109,13 @@ void                   gimp_brush_transform_size     (GimpBrush        *brush,
                                                       gint             *width,
                                                       gint             *height);
 const GimpTempBuf    * gimp_brush_transform_mask     (GimpBrush        *brush,
+                                                      GeglNode         *op,
                                                       gdouble           scale,
                                                       gdouble           aspect_ratio,
                                                       gdouble           angle,
                                                       gdouble           hardness);
 const GimpTempBuf    * gimp_brush_transform_pixmap   (GimpBrush        *brush,
+                                                      GeglNode         *op,
                                                       gdouble           scale,
                                                       gdouble           aspect_ratio,
                                                       gdouble           angle,
diff --git a/app/core/gimpbrushcache.c b/app/core/gimpbrushcache.c
index bbaf348..11c74b8 100644
--- a/app/core/gimpbrushcache.c
+++ b/app/core/gimpbrushcache.c
@@ -29,6 +29,7 @@
 #include "gimp-log.h"
 #include "gimp-intl.h"
 
+#define MAX_CACHED_DATA 20
 
 enum
 {
@@ -36,6 +37,20 @@ enum
   PROP_DATA_DESTROY
 };
 
+typedef struct _GimpBrushCacheUnit GimpBrushCacheUnit;
+
+struct _GimpBrushCacheUnit
+{
+  gpointer        data;
+
+  gint            width;
+  gint            height;
+  gdouble         scale;
+  gdouble         aspect_ratio;
+  gdouble         angle;
+  gdouble         hardness;
+  GeglNode       *op;
+};
 
 static void   gimp_brush_cache_constructed  (GObject      *object);
 static void   gimp_brush_cache_finalize     (GObject      *object);
@@ -91,10 +106,18 @@ gimp_brush_cache_finalize (GObject *object)
 {
   GimpBrushCache *cache = GIMP_BRUSH_CACHE (object);
 
-  if (cache->last_data)
+  if (cache->cached_units)
     {
-      cache->data_destroy (cache->last_data);
-      cache->last_data = NULL;
+      GList *iter;
+
+      for (iter = cache->cached_units; iter; iter = g_list_next (iter))
+        {
+          GimpBrushCacheUnit *unit = iter->data;
+
+          cache->data_destroy (unit->data);
+        }
+      g_list_free_full (cache->cached_units, g_free);
+      cache->cached_units = NULL;
     }
 
   G_OBJECT_CLASS (parent_class)->finalize (object);
@@ -167,15 +190,24 @@ gimp_brush_cache_clear (GimpBrushCache *cache)
 {
   g_return_if_fail (GIMP_IS_BRUSH_CACHE (cache));
 
-  if (cache->last_data)
+  if (cache->cached_units)
     {
-      cache->data_destroy (cache->last_data);
-      cache->last_data = NULL;
+      GList *iter;
+
+      for (iter = cache->cached_units; iter; iter = g_list_next (iter))
+        {
+          GimpBrushCacheUnit *unit = iter->data;
+
+          cache->data_destroy (unit->data);
+        }
+      g_list_free_full (cache->cached_units, g_free);
+      cache->cached_units = NULL;
     }
 }
 
 gconstpointer
 gimp_brush_cache_get (GimpBrushCache *cache,
+                      GeglNode       *op,
                       gint            width,
                       gint            height,
                       gdouble         scale,
@@ -183,20 +215,34 @@ gimp_brush_cache_get (GimpBrushCache *cache,
                       gdouble         angle,
                       gdouble         hardness)
 {
+  GList *iter;
+
   g_return_val_if_fail (GIMP_IS_BRUSH_CACHE (cache), NULL);
 
-  if (cache->last_data                         &&
-      cache->last_width        == width        &&
-      cache->last_height       == height       &&
-      cache->last_scale        == scale        &&
-      cache->last_aspect_ratio == aspect_ratio &&
-      cache->last_angle        == angle        &&
-      cache->last_hardness     == hardness)
+  for (iter = cache->cached_units; iter; iter = g_list_next (iter))
     {
-      if (gimp_log_flags & GIMP_LOG_BRUSH_CACHE)
-        g_printerr ("%c", cache->debug_hit);
-
-      return (gconstpointer) cache->last_data;
+      GimpBrushCacheUnit *unit = iter->data;
+
+      if (unit->data                         &&
+          unit->width        == width        &&
+          unit->height       == height       &&
+          unit->scale        == scale        &&
+          unit->aspect_ratio == aspect_ratio &&
+          unit->angle        == angle        &&
+          unit->hardness     == hardness     &&
+          unit->op           == op)
+        {
+          if (gimp_log_flags & GIMP_LOG_BRUSH_CACHE)
+            g_printerr ("%c", cache->debug_hit);
+
+          /* Make the returned cached brush first in the list. */
+          cache->cached_units = g_list_remove_link (cache->cached_units, iter);
+          iter->next = cache->cached_units;
+          if (cache->cached_units)
+            cache->cached_units->prev = iter;
+          cache->cached_units = iter;
+          return (gconstpointer) unit->data;
+        }
     }
 
   if (gimp_log_flags & GIMP_LOG_BRUSH_CACHE)
@@ -208,6 +254,7 @@ gimp_brush_cache_get (GimpBrushCache *cache,
 void
 gimp_brush_cache_add (GimpBrushCache *cache,
                       gpointer        data,
+                      GeglNode       *op,
                       gint            width,
                       gint            height,
                       gdouble         scale,
@@ -215,20 +262,38 @@ gimp_brush_cache_add (GimpBrushCache *cache,
                       gdouble         angle,
                       gdouble         hardness)
 {
+  GList *iter;
+  GimpBrushCacheUnit *unit;
+
   g_return_if_fail (GIMP_IS_BRUSH_CACHE (cache));
   g_return_if_fail (data != NULL);
 
-  if (data == cache->last_data)
-    return;
+  for (iter = cache->cached_units; iter; iter = g_list_next (iter))
+    {
+      unit = iter->data;
+      if (data == unit->data)
+        return;
+    }
+
+  if (g_list_length (cache->cached_units) > MAX_CACHED_DATA &&
+      (iter = g_list_last (cache->cached_units)))
+    {
+      unit = iter->data;
+
+      cache->data_destroy (unit->data);
+      cache->cached_units = g_list_delete_link (cache->cached_units, iter);
+    }
+
+  unit = g_new (GimpBrushCacheUnit, 1);
 
-  if (cache->last_data)
-    cache->data_destroy (cache->last_data);
+  unit->data          = data;
+  unit->width         = width;
+  unit->height        = height;
+  unit->scale         = scale;
+  unit->aspect_ratio  = aspect_ratio;
+  unit->angle         = angle;
+  unit->hardness      = hardness;
+  unit->op            = op;
 
-  cache->last_data         = data;
-  cache->last_width        = width;
-  cache->last_height       = height;
-  cache->last_scale        = scale;
-  cache->last_aspect_ratio = aspect_ratio;
-  cache->last_angle        = angle;
-  cache->last_hardness     = hardness;
+  cache->cached_units = g_list_prepend (cache->cached_units, unit);
 }
diff --git a/app/core/gimpbrushcache.h b/app/core/gimpbrushcache.h
index 5c879cc..6fa48ec 100644
--- a/app/core/gimpbrushcache.h
+++ b/app/core/gimpbrushcache.h
@@ -41,13 +41,7 @@ struct _GimpBrushCache
 
   GDestroyNotify  data_destroy;
 
-  gpointer        last_data;
-  gint            last_width;
-  gint            last_height;
-  gdouble         last_scale;
-  gdouble         last_aspect_ratio;
-  gdouble         last_angle;
-  gdouble         last_hardness;
+  GList          *cached_units;
 
   gchar           debug_hit;
   gchar           debug_miss;
@@ -68,6 +62,7 @@ GimpBrushCache * gimp_brush_cache_new      (GDestroyNotify  data_destory,
 void             gimp_brush_cache_clear    (GimpBrushCache *cache);
 
 gconstpointer    gimp_brush_cache_get      (GimpBrushCache *cache,
+                                            GeglNode       *op,
                                             gint            width,
                                             gint            height,
                                             gdouble         scale,
@@ -76,6 +71,7 @@ gconstpointer    gimp_brush_cache_get      (GimpBrushCache *cache,
                                             gdouble         hardness);
 void             gimp_brush_cache_add      (GimpBrushCache *cache,
                                             gpointer        data,
+                                            GeglNode       *op,
                                             gint            width,
                                             gint            height,
                                             gdouble         scale,
diff --git a/app/core/gimpimage-private.h b/app/core/gimpimage-private.h
index f2a9dfa..5fc38eb 100644
--- a/app/core/gimpimage-private.h
+++ b/app/core/gimpimage-private.h
@@ -87,6 +87,9 @@ struct _GimpImagePrivate
   GeglNode          *graph;                 /*  GEGL projection graph        */
   GeglNode          *visible_mask;          /*  component visibility node    */
 
+  GList             *symmetries;            /*  Painting symmetries          */
+  GimpSymmetry      *selected_symmetry;     /*  Selected symmetry            */
+
   GList             *guides;                /*  guides                       */
   GimpGrid          *grid;                  /*  grid                         */
   GList             *sample_points;         /*  color sample points          */
diff --git a/app/core/gimpimage-symmetry.c b/app/core/gimpimage-symmetry.c
new file mode 100644
index 0000000..f974689
--- /dev/null
+++ b/app/core/gimpimage-symmetry.c
@@ -0,0 +1,192 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimpimage-symmetry.c
+ * Copyright (C) 2015 Jehan <jehan gimp org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <gegl.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+
+#include "core-types.h"
+
+#include "gimpsymmetry.h"
+#include "gimpimage.h"
+#include "gimpimage-private.h"
+#include "gimpimage-symmetry.h"
+#include "gimpsymmetry-mirror.h"
+
+/**
+ * gimp_image_symmetry_list:
+ *
+ * Returns a list of #GType of all existing symmetries.
+ **/
+GList *
+gimp_image_symmetry_list (void)
+{
+  GList *list = NULL;
+
+  list = g_list_prepend (list, GINT_TO_POINTER (GIMP_TYPE_MIRROR));
+  return list;
+}
+
+/**
+ * gimp_image_symmetry_new:
+ * @image: the #GimpImage
+ * @type:  the #GType of the symmetry
+ *
+ * Creates a new #GimpSymmetry of @type attached to @image.
+ *
+ * Returns: the new #GimpSymmetry.
+ **/
+GimpSymmetry *
+gimp_image_symmetry_new (GimpImage *image,
+                         GType      type)
+{
+  GimpSymmetry *sym = NULL;
+
+  if (type != G_TYPE_NONE)
+    {
+      sym = g_object_new (type,
+                          "image", image,
+                          NULL);
+      sym->type = type;
+    }
+
+  return sym;
+}
+
+/**
+ * gimp_image_symmetry_add:
+ * @image: the #GimpImage
+ * @type:  the #GType of the symmetry
+ *
+ * Add a symmetry of type @type to @image and make it the
+ * selected transformation.
+ **/
+void
+gimp_image_symmetry_add (GimpImage    *image,
+                         GimpSymmetry *sym)
+{
+  GimpImagePrivate *private;
+
+  g_return_if_fail (GIMP_IS_IMAGE (image));
+  g_return_if_fail (GIMP_IS_SYMMETRY (sym));
+
+  private = GIMP_IMAGE_GET_PRIVATE (image);
+
+  private->symmetries = g_list_prepend (private->symmetries,
+                                        sym);
+}
+
+/**
+ * gimp_image_symmetry_remove:
+ * @image:   the #GimpImage
+ * @sym: the #GimpSymmetry
+ *
+ * Remove @sym from the list of symmetries of @image.
+ * If it was the selected transformation, unselect it first.
+ **/
+void
+gimp_image_symmetry_remove (GimpImage    *image,
+                            GimpSymmetry *sym)
+{
+  GimpImagePrivate *private;
+
+  g_return_if_fail (GIMP_IS_SYMMETRY (sym));
+  g_return_if_fail (GIMP_IS_IMAGE (image));
+
+  private = GIMP_IMAGE_GET_PRIVATE (image);
+
+  if (private->selected_symmetry == sym)
+    gimp_image_symmetry_select (image, G_TYPE_NONE);
+  private->symmetries = g_list_remove (private->symmetries,
+                                       sym);
+  g_object_unref (sym);
+}
+
+/**
+ * gimp_image_symmetry_get:
+ * @image: the #GimpImage
+ *
+ * Returns: the list of #GimpSymmetry set on @image.
+ * The returned list belongs to @image and should not be freed.
+ **/
+GList *
+gimp_image_symmetry_get (GimpImage *image)
+{
+  GimpImagePrivate *private;
+
+  g_return_val_if_fail (GIMP_IS_IMAGE (image), FALSE);
+
+  private = GIMP_IMAGE_GET_PRIVATE (image);
+
+  return private->symmetries;
+}
+
+/**
+ * gimp_image_symmetry_select:
+ * @image: the #GimpImage
+ * @type:  the #GType of the symmetry
+ *
+ * Select the symmetry of type @type.
+ * Using the GType allows to select a transformation without
+ * knowing whether one of the same @type was already created.
+ *
+ * Returns TRUE on success, FALSE if no such symmetry was found.
+ **/
+gboolean
+gimp_image_symmetry_select (GimpImage *image,
+                            GType      type)
+{
+  g_return_val_if_fail (GIMP_IS_IMAGE (image), FALSE);
+
+  g_object_set (image,
+                "symmetry", type,
+                NULL);
+
+  return TRUE;
+}
+
+/**
+ * gimp_image_symmetry_selected:
+ * @image: the #GimpImage
+ *
+ * Returns the #GimpSymmetry transformation selected on @image.
+ **/
+GimpSymmetry *
+gimp_image_symmetry_selected (GimpImage *image)
+{
+  static GimpImage    *last_image = NULL;
+  static GimpSymmetry *identity = NULL;
+  GimpImagePrivate    *private;
+
+  g_return_val_if_fail (GIMP_IS_IMAGE (image), FALSE);
+
+  if (last_image != image)
+    {
+      if (identity)
+        g_object_unref (identity);
+      identity = gimp_image_symmetry_new (image,
+                                          GIMP_TYPE_SYMMETRY);
+    }
+
+  private = GIMP_IMAGE_GET_PRIVATE (image);
+
+  return private->selected_symmetry ? private->selected_symmetry : identity;
+}
diff --git a/app/core/gimpimage-symmetry.h b/app/core/gimpimage-symmetry.h
new file mode 100644
index 0000000..109bd53
--- /dev/null
+++ b/app/core/gimpimage-symmetry.h
@@ -0,0 +1,38 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimpimage-symmetry.h
+ * Copyright (C) 2015 Jehan <jehan gimp org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __GIMP_IMAGE_SYMMETRY_H__
+#define __GIMP_IMAGE_SYMMETRY_H__
+
+GList        * gimp_image_symmetry_list      (void);
+
+GimpSymmetry * gimp_image_symmetry_new       (GimpImage    *image,
+                                              GType         type);
+void           gimp_image_symmetry_add       (GimpImage    *image,
+                                              GimpSymmetry *sym);
+void           gimp_image_symmetry_remove    (GimpImage    *image,
+                                              GimpSymmetry *sym);
+GList        * gimp_image_symmetry_get       (GimpImage    *image);
+
+gboolean       gimp_image_symmetry_select    (GimpImage    *image,
+                                              GType         type);
+GimpSymmetry * gimp_image_symmetry_selected  (GimpImage    *image);
+
+#endif  /*  __GIMP_IMAGE_SYMMETRY_H__  */
diff --git a/app/core/gimpimage.c b/app/core/gimpimage.c
index c0a8e4d..5900715 100644
--- a/app/core/gimpimage.c
+++ b/app/core/gimpimage.c
@@ -56,6 +56,7 @@
 #include "gimpimage-preview.h"
 #include "gimpimage-private.h"
 #include "gimpimage-quick-mask.h"
+#include "gimpimage-symmetry.h"
 #include "gimpimage-undo.h"
 #include "gimpimage-undo-push.h"
 #include "gimpitemtree.h"
@@ -69,6 +70,7 @@
 #include "gimpprojection.h"
 #include "gimpsamplepoint.h"
 #include "gimpselection.h"
+#include "gimpsymmetry.h"
 #include "gimptempbuf.h"
 #include "gimptemplate.h"
 #include "gimpundostack.h"
@@ -130,7 +132,8 @@ enum
   PROP_BASE_TYPE,
   PROP_PRECISION,
   PROP_METADATA,
-  PROP_BUFFER
+  PROP_BUFFER,
+  PROP_SYMMETRY
 };
 
 
@@ -624,6 +627,11 @@ gimp_image_class_init (GimpImageClass *klass)
 
   g_object_class_override_property (object_class, PROP_BUFFER, "buffer");
 
+  g_object_class_install_property (object_class, PROP_SYMMETRY,
+                                   g_param_spec_int ("symmetry",
+                                                     NULL, _("Symmetry"),
+                                                     G_TYPE_NONE, G_MAXINT, G_TYPE_NONE,
+                                                     GIMP_PARAM_READWRITE));
   g_type_class_add_private (klass, sizeof (GimpImagePrivate));
 }
 
@@ -696,6 +704,9 @@ gimp_image_init (GimpImage *image)
 
   private->projection          = gimp_projection_new (GIMP_PROJECTABLE (image));
 
+  private->symmetries          = NULL;
+  private->selected_symmetry   = NULL;
+
   private->guides              = NULL;
   private->grid                = NULL;
   private->sample_points       = NULL;
@@ -859,6 +870,42 @@ gimp_image_set_property (GObject      *object,
       break;
     case PROP_METADATA:
     case PROP_BUFFER:
+      break;
+    case PROP_SYMMETRY:
+      {
+        GType type = g_value_get_int (value);
+
+        if (private->selected_symmetry)
+          g_object_set (private->selected_symmetry,
+                        "active", FALSE,
+                        NULL);
+        private->selected_symmetry = NULL;
+
+        if (type != G_TYPE_NONE)
+          {
+            GList *iter;
+
+            for (iter = private->symmetries; iter; iter = g_list_next (iter))
+              {
+                GimpSymmetry *sym = iter->data;
+                if (g_type_is_a (sym->type, type))
+                  private->selected_symmetry = iter->data;
+              }
+
+            if (private->selected_symmetry == NULL)
+              {
+                GimpSymmetry *sym;
+
+                sym = gimp_image_symmetry_new (image, type);
+                gimp_image_symmetry_add (image, sym);
+                private->selected_symmetry = sym;
+              }
+            g_object_set (private->selected_symmetry,
+                          "active", TRUE,
+                          NULL);
+          }
+      }
+      break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
       break;
@@ -900,6 +947,11 @@ gimp_image_get_property (GObject    *object,
     case PROP_BUFFER:
       g_value_set_object (value, gimp_image_get_buffer (GIMP_PICKABLE (image)));
       break;
+    case PROP_SYMMETRY:
+      g_value_set_int (value,
+                       private->selected_symmetry ?
+                       private->selected_symmetry->type : G_TYPE_NONE);
+      break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
       break;
@@ -1050,6 +1102,12 @@ gimp_image_finalize (GObject *object)
       private->guides = NULL;
     }
 
+  if (private->symmetries)
+    {
+      g_list_free_full (private->symmetries, g_object_unref);
+      private->symmetries = NULL;
+    }
+
   if (private->grid)
     {
       g_object_unref (private->grid);
diff --git a/app/core/gimpsymmetry-mirror.c b/app/core/gimpsymmetry-mirror.c
new file mode 100644
index 0000000..878f760
--- /dev/null
+++ b/app/core/gimpsymmetry-mirror.c
@@ -0,0 +1,701 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimpsymmetry-mirror.c
+ * Copyright (C) 2015 Jehan <jehan gimp org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include <cairo.h>
+#include <gegl.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+
+#include "libgimpconfig/gimpconfig.h"
+
+#include "core-types.h"
+
+#include "gimp.h"
+#include "gimp-cairo.h"
+#include "gimpbrush.h"
+#include "gimpguide.h"
+#include "gimpimage.h"
+#include "gimpimage-guides.h"
+#include "gimpimage-symmetry.h"
+#include "gimpitem.h"
+#include "gimpsymmetry-mirror.h"
+
+#include "gimp-intl.h"
+
+enum
+{
+  PROP_0,
+
+  PROP_HORIZONTAL_SYMMETRY,
+  PROP_VERTICAL_SYMMETRY,
+  PROP_POINT_SYMMETRY,
+  PROP_DISABLE_TRANSFORMATION,
+  PROP_HORIZONTAL_POSITION,
+  PROP_VERTICAL_POSITION
+};
+
+/* Local function prototypes */
+
+static void       gimp_mirror_finalize                (GObject             *object);
+static void       gimp_mirror_set_property            (GObject             *object,
+                                                       guint                property_id,
+                                                       const GValue        *value,
+                                                       GParamSpec          *pspec);
+static void       gimp_mirror_get_property            (GObject             *object,
+                                                       guint                property_id,
+                                                       GValue              *value,
+                                                       GParamSpec          *pspec);
+
+static void       gimp_mirror_update_strokes          (GimpSymmetry        *mirror,
+                                                       GimpDrawable        *drawable,
+                                                       GimpCoords          *origin);
+static void       gimp_mirror_prepare_operations      (GimpMirror          *mirror,
+                                                       gint                 paint_width,
+                                                       gint                 paint_height);
+static GeglNode * gimp_mirror_get_operation           (GimpSymmetry        *mirror,
+                                                       gint                 stroke,
+                                                       gint                 paint_width,
+                                                       gint                 paint_height);
+static void       gimp_mirror_reset                   (GimpMirror          *mirror);
+static void       gimp_mirror_add_guide               (GimpMirror          *mirror,
+                                                       GimpOrientationType  orientation);
+static void       gimp_mirror_remove_guide            (GimpMirror          *mirror,
+                                                       GimpOrientationType  orientation);
+static void       gimp_mirror_guide_removed_cb        (GObject             *object,
+                                                       GimpMirror          *mirror);
+static void       gimp_mirror_guide_position_cb       (GObject             *object,
+                                                       GParamSpec          *pspec,
+                                                       GimpMirror          *mirror);
+static GParamSpec ** gimp_mirror_get_settings         (GimpSymmetry        *sym,
+                                                       gint                *n_settings);
+static void       gimp_mirror_active_changed          (GimpSymmetry        *sym);
+static void       gimp_mirror_set_horizontal_symmetry (GimpMirror          *mirror,
+                                                       gboolean             active);
+static void       gimp_mirror_set_vertical_symmetry   (GimpMirror          *mirror,
+                                                       gboolean             active);
+static void       gimp_mirror_set_point_symmetry      (GimpMirror          *mirror,
+                                                       gboolean             active);
+
+G_DEFINE_TYPE (GimpMirror, gimp_mirror, GIMP_TYPE_SYMMETRY)
+
+#define parent_class gimp_mirror_parent_class
+
+static void
+gimp_mirror_class_init (GimpMirrorClass *klass)
+{
+  GObjectClass      *object_class   = G_OBJECT_CLASS (klass);
+  GimpSymmetryClass *symmetry_class = GIMP_SYMMETRY_CLASS (klass);
+
+  object_class->finalize            = gimp_mirror_finalize;
+  object_class->set_property        = gimp_mirror_set_property;
+  object_class->get_property        = gimp_mirror_get_property;
+
+  symmetry_class->label             = _("Mirror");
+  symmetry_class->update_strokes    = gimp_mirror_update_strokes;
+  symmetry_class->get_operation     = gimp_mirror_get_operation;
+  symmetry_class->get_settings      = gimp_mirror_get_settings;
+  symmetry_class->active_changed    = gimp_mirror_active_changed;
+
+  /* Properties for user settings */
+  GIMP_CONFIG_INSTALL_PROP_BOOLEAN (object_class, PROP_HORIZONTAL_SYMMETRY,
+                                    "horizontal-symmetry",
+                                    _("Horizontal Mirror"),
+                                    FALSE,
+                                    GIMP_PARAM_STATIC_STRINGS);
+  GIMP_CONFIG_INSTALL_PROP_BOOLEAN (object_class, PROP_VERTICAL_SYMMETRY,
+                                    "vertical-symmetry",
+                                    _("Vertical Mirror"),
+                                    FALSE,
+                                    GIMP_PARAM_STATIC_STRINGS);
+  GIMP_CONFIG_INSTALL_PROP_BOOLEAN (object_class, PROP_POINT_SYMMETRY,
+                                    "point-symmetry",
+                                    _("Central Symmetry"),
+                                    FALSE,
+                                    GIMP_PARAM_STATIC_STRINGS);
+  GIMP_CONFIG_INSTALL_PROP_BOOLEAN (object_class, PROP_DISABLE_TRANSFORMATION,
+                                    "disable-transformation",
+                                    _("Disable Brush Transformation (faster)"),
+                                    FALSE,
+                                    GIMP_PARAM_STATIC_STRINGS);
+
+  /* Properties for XCF serialization only */
+  GIMP_CONFIG_INSTALL_PROP_DOUBLE (object_class, PROP_HORIZONTAL_POSITION,
+                                   "horizontal-position",
+                                   _("Horizontal guide position"),
+                                   0.0, G_MAXDOUBLE, 0.0,
+                                   GIMP_PARAM_STATIC_STRINGS);
+  GIMP_CONFIG_INSTALL_PROP_DOUBLE (object_class, PROP_VERTICAL_POSITION,
+                                   "vertical-position",
+                                   _("Vertical guide position"),
+                                   0.0, G_MAXDOUBLE, 0.0,
+                                   GIMP_PARAM_STATIC_STRINGS);
+}
+
+static void
+gimp_mirror_init (GimpMirror *mirror)
+{
+}
+
+static void
+gimp_mirror_finalize (GObject *object)
+{
+  GimpMirror *mirror = GIMP_MIRROR (object);
+
+  if (mirror->horizontal_guide)
+    g_object_unref (mirror->horizontal_guide);
+  mirror->horizontal_guide = NULL;
+
+  if (mirror->vertical_guide)
+    g_object_unref (mirror->vertical_guide);
+  mirror->vertical_guide = NULL;
+
+  if (mirror->horizontal_op)
+    g_object_unref (mirror->horizontal_op);
+  mirror->horizontal_op = NULL;
+
+  if (mirror->vertical_op)
+    g_object_unref (mirror->vertical_op);
+  mirror->vertical_op = NULL;
+
+  if (mirror->central_op)
+    g_object_unref (mirror->central_op);
+  mirror->central_op = NULL;
+
+  G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static void
+gimp_mirror_set_property (GObject      *object,
+                          guint         property_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+  GimpMirror *mirror = GIMP_MIRROR (object);
+
+  switch (property_id)
+    {
+    case PROP_HORIZONTAL_SYMMETRY:
+      gimp_mirror_set_horizontal_symmetry (mirror,
+                                           g_value_get_boolean (value));
+      break;
+    case PROP_VERTICAL_SYMMETRY:
+      gimp_mirror_set_vertical_symmetry (mirror,
+                                         g_value_get_boolean (value));
+      break;
+    case PROP_POINT_SYMMETRY:
+      gimp_mirror_set_point_symmetry (mirror,
+                                      g_value_get_boolean (value));
+      break;
+    case PROP_DISABLE_TRANSFORMATION:
+      mirror->disable_transformation = g_value_get_boolean (value);
+      break;
+    case PROP_HORIZONTAL_POSITION:
+      mirror->horizontal_position = g_value_get_double (value);
+      if (mirror->horizontal_guide)
+        gimp_guide_set_position (mirror->horizontal_guide,
+                                 mirror->horizontal_position);
+      break;
+    case PROP_VERTICAL_POSITION:
+      mirror->vertical_position = g_value_get_double (value);
+      if (mirror->vertical_guide)
+        gimp_guide_set_position (mirror->vertical_guide,
+                                 mirror->vertical_position);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+gimp_mirror_get_property (GObject    *object,
+                          guint       property_id,
+                          GValue     *value,
+                          GParamSpec *pspec)
+{
+  GimpMirror *mirror = GIMP_MIRROR (object);
+
+  switch (property_id)
+    {
+    case PROP_HORIZONTAL_SYMMETRY:
+      g_value_set_boolean (value, mirror->horizontal_mirror);
+      break;
+    case PROP_VERTICAL_SYMMETRY:
+      g_value_set_boolean (value, mirror->vertical_mirror);
+      break;
+    case PROP_POINT_SYMMETRY:
+      g_value_set_boolean (value, mirror->point_symmetry);
+      break;
+    case PROP_DISABLE_TRANSFORMATION:
+      g_value_set_boolean (value, mirror->disable_transformation);
+      break;
+    case PROP_HORIZONTAL_POSITION:
+      g_value_set_double (value, mirror->horizontal_position);
+      break;
+    case PROP_VERTICAL_POSITION:
+      g_value_set_double (value, mirror->vertical_position);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+gimp_mirror_update_strokes (GimpSymmetry *sym,
+                            GimpDrawable *drawable,
+                            GimpCoords   *origin)
+{
+  GList      *strokes = NULL;
+  GimpMirror *mirror  = GIMP_MIRROR (sym);
+  GimpCoords *coords;
+
+  g_list_free_full (sym->strokes, g_free);
+  strokes = g_list_prepend (strokes,
+                            g_memdup (origin, sizeof (GimpCoords)));
+
+  if (mirror->horizontal_mirror)
+    {
+      coords = g_memdup (origin, sizeof (GimpCoords));
+      coords->y = 2.0 * mirror->horizontal_position - origin->y;
+      strokes = g_list_prepend (strokes, coords);
+    }
+  if (mirror->vertical_mirror)
+    {
+      coords = g_memdup (origin, sizeof (GimpCoords));
+      coords->x = 2.0 * mirror->vertical_position - origin->x;
+      strokes = g_list_prepend (strokes, coords);
+    }
+  if (mirror->point_symmetry)
+    {
+      coords = g_memdup (origin, sizeof (GimpCoords));
+      coords->x = 2.0 * mirror->vertical_position - origin->x;
+      coords->y = 2.0 * mirror->horizontal_position - origin->y;
+      strokes = g_list_prepend (strokes, coords);
+    }
+  sym->strokes = g_list_reverse (strokes);
+
+  g_signal_emit_by_name (sym, "strokes-updated", sym->image);
+}
+
+static void gimp_mirror_prepare_operations (GimpMirror *mirror,
+                                            gint        paint_width,
+                                            gint        paint_height)
+{
+  if (paint_width == mirror->last_paint_width &&
+      paint_height == mirror->last_paint_height)
+    return;
+
+  mirror->last_paint_width  = paint_width;
+  mirror->last_paint_height = paint_height;
+
+  if (mirror->horizontal_op)
+    g_object_unref (mirror->horizontal_op);
+
+  mirror->horizontal_op = gegl_node_new_child (NULL,
+                                               "operation", "gegl:reflect",
+                                               "origin-x", 0.0,
+                                               "origin-y",
+                                               (gdouble) paint_height / 2.0,
+                                               "x",
+                                               1.0,
+                                               "y",
+                                               0.0,
+                                               NULL);
+
+  if (mirror->vertical_op)
+    g_object_unref (mirror->vertical_op);
+
+  mirror->vertical_op = gegl_node_new_child (NULL,
+                                             "operation", "gegl:reflect",
+                                             "origin-x",
+                                             (gdouble) paint_width / 2.0,
+                                             "origin-y", 0.0,
+                                             "x",
+                                             0.0,
+                                             "y",
+                                             1.0,
+                                             NULL);
+
+  if (mirror->central_op)
+    g_object_unref (mirror->central_op);
+
+  mirror->central_op = gegl_node_new_child (NULL,
+                                            "operation", "gegl:rotate",
+                                            "origin-x",
+                                            (gdouble) paint_width / 2.0,
+                                            "origin-y",
+                                            (gdouble) paint_height / 2.0,
+                                            "degrees",
+                                            180.0,
+                                            NULL);
+}
+
+static GeglNode *
+gimp_mirror_get_operation (GimpSymmetry *sym,
+                           gint          stroke,
+                           gint          paint_width,
+                           gint          paint_height)
+{
+  GimpMirror *mirror  = GIMP_MIRROR (sym);
+  GeglNode   *op;
+
+  g_return_val_if_fail (stroke >= 0 &&
+                        stroke < g_list_length (sym->strokes), NULL);
+
+  gimp_mirror_prepare_operations (mirror, paint_width, paint_height);
+
+  if (mirror->disable_transformation || stroke == 0 ||
+      paint_width == 0 || paint_height == 0)
+    op = NULL;
+  else if (stroke == 1 && mirror->horizontal_mirror)
+    op = g_object_ref (mirror->horizontal_op);
+  else if ((stroke == 2 && mirror->horizontal_mirror &&
+            mirror->vertical_mirror) ||
+           (stroke == 1 && mirror->vertical_mirror &&
+            !  mirror->horizontal_mirror))
+    op = g_object_ref (mirror->vertical_op);
+  else
+    op = g_object_ref (mirror->central_op);
+
+  return op;
+}
+
+static void
+gimp_mirror_reset (GimpMirror *mirror)
+{
+  GimpSymmetry *sym;
+
+  g_return_if_fail (GIMP_IS_MIRROR (mirror));
+
+  sym = GIMP_SYMMETRY (mirror);
+
+  if (sym->origin)
+    {
+      gimp_symmetry_set_origin (sym, sym->drawable,
+                                sym->origin);
+    }
+}
+
+static void
+gimp_mirror_add_guide (GimpMirror          *mirror,
+                       GimpOrientationType  orientation)
+{
+  static GimpRGB  normal_fg = { 1.0, 1.0, 1.0, 1.0 };
+  static GimpRGB  normal_bg = { 0.0, 1.0, 0.0, 1.0 };
+  static GimpRGB  active_fg = { 0.0, 1.0, 0.0, 1.0 };
+  static GimpRGB  active_bg = { 1.0, 0.0, 0.0, 1.0 };
+  GimpSymmetry   *sym;
+  GimpImage      *image;
+  Gimp           *gimp;
+  GimpGuide      *guide;
+  gint            position;
+
+  g_return_if_fail (GIMP_IS_MIRROR (mirror));
+
+  sym   = GIMP_SYMMETRY (mirror);
+  image = sym->image;
+  gimp  = GIMP (image->gimp);
+
+  guide = gimp_guide_custom_new (orientation,
+                                 gimp->next_guide_ID++,
+                                 &normal_fg, &normal_bg,
+                                 &active_fg, &active_bg,
+                                 1.0);
+
+  if (orientation == GIMP_ORIENTATION_HORIZONTAL)
+    {
+      mirror->horizontal_guide = guide;
+
+      /* Mirror guide position at first activation is at canvas middle. */
+      if (mirror->horizontal_position < 1.0)
+        mirror->horizontal_position = (gdouble) gimp_image_get_height (image) / 2.0;
+      position = (gint) mirror->horizontal_position;
+    }
+  else
+    {
+      mirror->vertical_guide = guide;
+
+      /* Mirror guide position at first activation is at canvas middle. */
+      if (mirror->vertical_position < 1.0)
+        mirror->vertical_position = (gdouble) gimp_image_get_width (image) / 2.0;
+      position = (gint) mirror->vertical_position;
+    }
+  g_signal_connect (G_OBJECT (guide), "removed",
+                    G_CALLBACK (gimp_mirror_guide_removed_cb),
+                    mirror);
+
+  gimp_image_add_guide (image, guide,
+                        (gint) position);
+
+  g_signal_connect (G_OBJECT (guide), "notify::position",
+                    G_CALLBACK (gimp_mirror_guide_position_cb),
+                    mirror);
+}
+
+static void
+gimp_mirror_remove_guide (GimpMirror          *mirror,
+                          GimpOrientationType  orientation)
+{
+  GimpSymmetry *sym;
+  GimpImage    *image;
+  GimpGuide    *guide;
+
+  g_return_if_fail (GIMP_IS_MIRROR (mirror));
+
+  sym   = GIMP_SYMMETRY (mirror);
+  image = sym->image;
+  guide = (orientation == GIMP_ORIENTATION_HORIZONTAL) ?
+    mirror->horizontal_guide : mirror->vertical_guide;
+
+  g_signal_handlers_disconnect_by_func (G_OBJECT (guide),
+                                        gimp_mirror_guide_removed_cb,
+                                        mirror);
+  g_signal_handlers_disconnect_by_func (G_OBJECT (guide),
+                                        gimp_mirror_guide_position_cb,
+                                        mirror);
+  gimp_image_remove_guide (image, guide, FALSE);
+  g_object_unref (guide);
+
+  if (orientation == GIMP_ORIENTATION_HORIZONTAL)
+    mirror->horizontal_guide = NULL;
+  else
+    mirror->vertical_guide = NULL;
+}
+
+static void
+gimp_mirror_guide_removed_cb (GObject    *object,
+                              GimpMirror *mirror)
+{
+  GimpSymmetry *symmetry = GIMP_SYMMETRY (mirror);
+
+  g_signal_handlers_disconnect_by_func (object,
+                                        gimp_mirror_guide_removed_cb,
+                                        mirror);
+  g_signal_handlers_disconnect_by_func (object,
+                                        gimp_mirror_guide_position_cb,
+                                        mirror);
+  if (GIMP_GUIDE (object) == mirror->horizontal_guide)
+    {
+      g_object_unref (mirror->horizontal_guide);
+      mirror->horizontal_guide    = NULL;
+
+      mirror->horizontal_mirror   = FALSE;
+      mirror->point_symmetry      = FALSE;
+      mirror->horizontal_position = 0.0;
+
+      if (mirror->vertical_guide &&
+          ! mirror->vertical_mirror)
+        {
+          g_signal_handlers_disconnect_by_func (G_OBJECT (mirror->vertical_guide),
+                                                gimp_mirror_guide_removed_cb,
+                                                mirror);
+          g_signal_handlers_disconnect_by_func (G_OBJECT (mirror->vertical_guide),
+                                                gimp_mirror_guide_position_cb,
+                                                mirror);
+          gimp_image_remove_guide (symmetry->image,
+                                   mirror->vertical_guide,
+                                   FALSE);
+          g_clear_object (&mirror->vertical_guide);
+        }
+    }
+  else if (GIMP_GUIDE (object) == mirror->vertical_guide)
+    {
+      g_object_unref (mirror->vertical_guide);
+      mirror->vertical_guide    = NULL;
+
+      mirror->vertical_mirror   = FALSE;
+      mirror->point_symmetry    = FALSE;
+      mirror->vertical_position = 0.0;
+
+      if (mirror->horizontal_guide &&
+          ! mirror->horizontal_mirror)
+        {
+          g_signal_handlers_disconnect_by_func (G_OBJECT (mirror->horizontal_guide),
+                                                gimp_mirror_guide_removed_cb,
+                                                mirror);
+          g_signal_handlers_disconnect_by_func (G_OBJECT (mirror->horizontal_guide),
+                                                gimp_mirror_guide_position_cb,
+                                                mirror);
+          gimp_image_remove_guide (symmetry->image,
+                                   mirror->horizontal_guide,
+                                   FALSE);
+          g_clear_object (&mirror->horizontal_guide);
+        }
+    }
+
+  if (mirror->horizontal_guide == NULL &&
+      mirror->vertical_guide   == NULL)
+    {
+      gimp_image_symmetry_remove (symmetry->image,
+                                  GIMP_SYMMETRY (mirror));
+    }
+  else
+    {
+      gimp_mirror_reset (mirror);
+      g_signal_emit_by_name (mirror, "update-ui",
+                             GIMP_SYMMETRY (mirror)->image);
+    }
+}
+
+static void
+gimp_mirror_guide_position_cb (GObject    *object,
+                               GParamSpec *pspec,
+                               GimpMirror *mirror)
+{
+  GimpGuide *guide;
+
+  guide = GIMP_GUIDE (object);
+
+  if (guide == mirror->horizontal_guide)
+    {
+      mirror->horizontal_position = (gdouble) gimp_guide_get_position (guide);
+    }
+  else if (guide == mirror->vertical_guide)
+    {
+      mirror->vertical_position = (gdouble) gimp_guide_get_position (guide);
+    }
+}
+
+static GParamSpec **
+gimp_mirror_get_settings (GimpSymmetry *sym,
+                          gint         *n_settings)
+{
+  GParamSpec **pspecs;
+
+  *n_settings = 5;
+  pspecs = g_new (GParamSpec*, 5);
+
+  pspecs[0] = g_object_class_find_property (G_OBJECT_GET_CLASS (sym),
+                                            "horizontal-symmetry");
+  pspecs[1] = g_object_class_find_property (G_OBJECT_GET_CLASS (sym),
+                                            "vertical-symmetry");
+  pspecs[2] = g_object_class_find_property (G_OBJECT_GET_CLASS (sym),
+                                            "point-symmetry");
+  pspecs[3] = NULL;
+  pspecs[4] = g_object_class_find_property (G_OBJECT_GET_CLASS (sym),
+                                            "disable-transformation");
+
+  return pspecs;
+}
+
+static void
+gimp_mirror_active_changed (GimpSymmetry *sym)
+{
+  GimpMirror *mirror = GIMP_MIRROR (sym);
+
+  if (sym->active)
+    {
+      if ((mirror->horizontal_mirror || mirror->point_symmetry) &&
+          ! mirror->horizontal_guide)
+        gimp_mirror_add_guide (mirror, GIMP_ORIENTATION_HORIZONTAL);
+      if ((mirror->vertical_mirror || mirror->point_symmetry) &&
+          ! mirror->vertical_guide)
+        gimp_mirror_add_guide (mirror, GIMP_ORIENTATION_VERTICAL);
+    }
+  else
+    {
+      if (mirror->horizontal_guide)
+        gimp_mirror_remove_guide (mirror, GIMP_ORIENTATION_HORIZONTAL);
+      if (mirror->vertical_guide)
+        gimp_mirror_remove_guide (mirror, GIMP_ORIENTATION_VERTICAL);
+    }
+}
+
+static void
+gimp_mirror_set_horizontal_symmetry (GimpMirror *mirror,
+                                     gboolean    active)
+{
+  g_return_if_fail (GIMP_IS_MIRROR (mirror));
+
+  if (active == mirror->horizontal_mirror)
+    return;
+
+  mirror->horizontal_mirror = active;
+
+  if (active)
+    {
+      if (! mirror->horizontal_guide)
+        gimp_mirror_add_guide (mirror, GIMP_ORIENTATION_HORIZONTAL);
+    }
+  else if (! mirror->point_symmetry)
+    gimp_mirror_remove_guide (mirror, GIMP_ORIENTATION_HORIZONTAL);
+
+  gimp_mirror_reset (mirror);
+}
+
+static void
+gimp_mirror_set_vertical_symmetry (GimpMirror *mirror,
+                                   gboolean    active)
+{
+  g_return_if_fail (GIMP_IS_MIRROR (mirror));
+
+  if (active == mirror->vertical_mirror)
+    return;
+
+  mirror->vertical_mirror = active;
+
+  if (active)
+    {
+      if (! mirror->vertical_guide)
+        gimp_mirror_add_guide (mirror, GIMP_ORIENTATION_VERTICAL);
+    }
+  else if (! mirror->point_symmetry)
+    gimp_mirror_remove_guide (mirror, GIMP_ORIENTATION_VERTICAL);
+
+  gimp_mirror_reset (mirror);
+}
+
+static void
+gimp_mirror_set_point_symmetry (GimpMirror *mirror,
+                                gboolean    active)
+{
+  g_return_if_fail (GIMP_IS_MIRROR (mirror));
+
+  if (active == mirror->point_symmetry)
+    return;
+
+  mirror->point_symmetry = active;
+
+  if (active)
+    {
+      /* Show the horizontal guide unless already shown */
+      if (! mirror->horizontal_guide)
+        gimp_mirror_add_guide (mirror, GIMP_ORIENTATION_HORIZONTAL);
+
+      /* Show the vertical guide unless already shown */
+      if (! mirror->vertical_guide)
+        gimp_mirror_add_guide (mirror, GIMP_ORIENTATION_VERTICAL);
+    }
+  else
+    {
+      /* Remove the horizontal guide unless needed by horizontal mirror */
+      if (! mirror->horizontal_mirror)
+        gimp_mirror_remove_guide (mirror, GIMP_ORIENTATION_HORIZONTAL);
+      /* Remove the vertical guide unless needed by vertical mirror */
+      if (! mirror->vertical_mirror)
+        gimp_mirror_remove_guide (mirror, GIMP_ORIENTATION_VERTICAL);
+    }
+
+  gimp_mirror_reset (mirror);
+}
diff --git a/app/core/gimpsymmetry-mirror.h b/app/core/gimpsymmetry-mirror.h
new file mode 100644
index 0000000..6d83de0
--- /dev/null
+++ b/app/core/gimpsymmetry-mirror.h
@@ -0,0 +1,67 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimpsymmetry-mirror.h
+ * Copyright (C) 2015 Jehan <jehan gimp org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __GIMP_MIRROR_H__
+#define __GIMP_MIRROR_H__
+
+
+#include "gimpsymmetry.h"
+
+
+#define GIMP_TYPE_MIRROR            (gimp_mirror_get_type ())
+#define GIMP_MIRROR(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIMP_TYPE_MIRROR, GimpMirror))
+#define GIMP_MIRROR_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), GIMP_TYPE_MIRROR, GimpMirrorClass))
+#define GIMP_IS_MIRROR(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIMP_TYPE_MIRROR))
+#define GIMP_IS_MIRROR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIMP_TYPE_MIRROR))
+#define GIMP_MIRROR_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), GIMP_TYPE_MIRROR, GimpMirrorClass))
+
+typedef struct _GimpMirrorClass GimpMirrorClass;
+
+struct _GimpMirror
+{
+  GimpSymmetry     parent_instance;
+
+  gboolean         horizontal_mirror;
+  gboolean         vertical_mirror;
+  gboolean         point_symmetry;
+  gboolean         disable_transformation;
+
+  gdouble          horizontal_position;
+  gdouble          vertical_position;
+  GimpGuide       *horizontal_guide;
+  GimpGuide       *vertical_guide;
+
+  /* Cached data */
+  gint             last_paint_width;
+  gint             last_paint_height;
+  GeglNode        *horizontal_op;
+  GeglNode        *vertical_op;
+  GeglNode        *central_op;
+};
+
+struct _GimpMirrorClass
+{
+  GimpSymmetryClass  parent_class;
+};
+
+
+GType        gimp_mirror_get_type                (void) G_GNUC_CONST;
+
+#endif  /*  __GIMP_MIRROR_H__  */
diff --git a/app/core/gimpsymmetry.c b/app/core/gimpsymmetry.c
new file mode 100644
index 0000000..5724707
--- /dev/null
+++ b/app/core/gimpsymmetry.c
@@ -0,0 +1,451 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimpsymmetry.c
+ * Copyright (C) 2015 Jehan <jehan gimp org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include <gegl.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+
+#include "libgimpbase/gimpbase.h"
+#include "libgimpconfig/gimpconfig.h"
+
+#include "core-types.h"
+
+#include "gimpdrawable.h"
+#include "gimpimage.h"
+#include "gimpimage-symmetry.h"
+#include "gimpitem.h"
+#include "gimpsymmetry.h"
+
+#include "gimp-intl.h"
+
+enum
+{
+  STROKES_UPDATED,
+  UPDATE_UI,
+  ACTIVE_CHANGED,
+  LAST_SIGNAL
+};
+
+enum
+{
+  PROP_0,
+  PROP_IMAGE,
+  PROP_ACTIVE,
+};
+
+/* Local function prototypes */
+
+static void gimp_symmetry_finalize        (GObject      *object);
+static void gimp_symmetry_set_property    (GObject      *object,
+                                           guint         property_id,
+                                           const GValue *value,
+                                           GParamSpec   *pspec);
+static void gimp_symmetry_get_property    (GObject      *object,
+                                           guint         property_id,
+                                           GValue       *value,
+                                           GParamSpec   *pspec);
+
+static void
+        gimp_symmetry_real_update_strokes (GimpSymmetry *sym,
+                                           GimpDrawable *drawable,
+                                           GimpCoords   *origin);
+static GeglNode *
+            gimp_symmetry_real_get_op     (GimpSymmetry *sym,
+                                           gint          stroke,
+                                           gint          paint_width,
+                                           gint          paint_height);
+static GParamSpec **
+          gimp_symmetry_real_get_settings (GimpSymmetry *sym,
+                                           gint         *n_properties);
+
+G_DEFINE_TYPE_WITH_CODE (GimpSymmetry, gimp_symmetry, GIMP_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (GIMP_TYPE_CONFIG, NULL))
+
+
+#define parent_class gimp_symmetry_parent_class
+
+static guint gimp_symmetry_signals[LAST_SIGNAL] = { 0 };
+
+static void
+gimp_symmetry_class_init (GimpSymmetryClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  /* This signal should likely be emitted at the end of update_strokes()
+   * if stroke coordinates were changed. */
+  gimp_symmetry_signals[STROKES_UPDATED] =
+    g_signal_new ("strokes-updated",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_FIRST,
+                  0,
+                  NULL, NULL,
+                  NULL,
+                  G_TYPE_NONE,
+                  1, GIMP_TYPE_IMAGE);
+  /* This signal should be emitted when you request a change in
+   * the settings UI. For instance adding some settings (therefore having a
+   * dynamic UI), or changing scale min/max extremes, etc. */
+  gimp_symmetry_signals[UPDATE_UI] =
+    g_signal_new ("update-ui",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_FIRST,
+                  0,
+                  NULL, NULL,
+                  NULL,
+                  G_TYPE_NONE,
+                  1, GIMP_TYPE_IMAGE);
+
+  gimp_symmetry_signals[ACTIVE_CHANGED] =
+    g_signal_new ("active-changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_FIRST,
+                  G_STRUCT_OFFSET (GimpSymmetryClass, active_changed),
+                  NULL, NULL,
+                  NULL,
+                  G_TYPE_NONE, 0);
+
+  object_class->finalize     = gimp_symmetry_finalize;
+  object_class->set_property = gimp_symmetry_set_property;
+  object_class->get_property = gimp_symmetry_get_property;
+
+  klass->label               = _("None");
+  klass->update_strokes      = gimp_symmetry_real_update_strokes;
+  klass->get_operation       = gimp_symmetry_real_get_op;
+  klass->get_settings        = gimp_symmetry_real_get_settings;
+  klass->active_changed      = NULL;
+
+  g_object_class_install_property (object_class, PROP_IMAGE,
+                                   g_param_spec_object ("image",
+                                                        NULL, NULL,
+                                                        GIMP_TYPE_IMAGE,
+                                                        GIMP_PARAM_READWRITE |
+                                                        G_PARAM_CONSTRUCT_ONLY));
+  GIMP_CONFIG_INSTALL_PROP_BOOLEAN (object_class, PROP_ACTIVE,
+                                    "active",
+                                    _("Activate symmetry painting"),
+                                    FALSE,
+                                    GIMP_PARAM_STATIC_STRINGS);
+}
+
+
+static void
+gimp_symmetry_init (GimpSymmetry *sym)
+{
+  sym->type     = G_TYPE_NONE;
+}
+
+static void
+gimp_symmetry_finalize (GObject *object)
+{
+  GimpSymmetry *sym = GIMP_SYMMETRY (object);
+
+  if (sym->drawable)
+    g_object_unref (sym->drawable);
+
+  g_free (sym->origin);
+  sym->origin = NULL;
+
+  g_list_free_full (sym->strokes, g_free);
+  sym->strokes = NULL;
+
+  G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static void
+gimp_symmetry_set_property (GObject      *object,
+                            guint         property_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  GimpSymmetry *sym = GIMP_SYMMETRY (object);
+
+  switch (property_id)
+    {
+    case PROP_IMAGE:
+      sym->image = g_value_get_object (value);
+      break;
+    case PROP_ACTIVE:
+      sym->active = g_value_get_boolean (value);
+      g_signal_emit (sym, gimp_symmetry_signals[ACTIVE_CHANGED], 0,
+                     sym->active);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+gimp_symmetry_get_property (GObject    *object,
+                            guint       property_id,
+                            GValue     *value,
+                            GParamSpec *pspec)
+{
+  GimpSymmetry *sym = GIMP_SYMMETRY (object);
+
+  switch (property_id)
+    {
+    case PROP_IMAGE:
+      g_value_set_object (value, sym->image);
+      break;
+    case PROP_ACTIVE:
+      g_value_set_boolean (value, sym->active);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+gimp_symmetry_real_update_strokes (GimpSymmetry *sym,
+                                   GimpDrawable *drawable,
+                                   GimpCoords   *origin)
+{
+  /* The basic symmetry just uses the origin as is. */
+  sym->strokes = g_list_prepend (sym->strokes,
+                                     g_memdup (origin, sizeof (GimpCoords)));
+}
+
+static GeglNode *
+gimp_symmetry_real_get_op (GimpSymmetry *sym,
+                           gint          stroke,
+                           gint          paint_width,
+                           gint          paint_height)
+{
+  /* The basic symmetry just returns NULL, since no transformation of the
+   * brush painting happen. */
+  return NULL;
+}
+
+static GParamSpec **
+gimp_symmetry_real_get_settings (GimpSymmetry *sym,
+                                 gint         *n_properties)
+{
+  *n_properties = 0;
+
+  return NULL;
+}
+
+/***** Public Functions *****/
+
+/**
+ * gimp_symmetry_set_origin:
+ * @sym:      the #GimpSymmetry
+ * @drawable: the #GimpDrawable where painting will happen
+ * @origin:   new base coordinates.
+ *
+ * Set the symmetry to new origin coordinates and drawable.
+ **/
+void
+gimp_symmetry_set_origin (GimpSymmetry *sym,
+                          GimpDrawable *drawable,
+                          GimpCoords   *origin)
+{
+  g_return_if_fail (GIMP_IS_SYMMETRY (sym));
+  g_return_if_fail (GIMP_IS_DRAWABLE (drawable));
+  g_return_if_fail (gimp_item_get_image (GIMP_ITEM (drawable)) == sym->image);
+
+  if (drawable != sym->drawable)
+    {
+      if (sym->drawable)
+        g_object_unref (sym->drawable);
+      sym->drawable = g_object_ref (drawable);
+    }
+
+  if (origin != sym->origin)
+    {
+      g_free (sym->origin);
+      sym->origin = g_memdup (origin, sizeof (GimpCoords));
+    }
+
+  g_list_free_full (sym->strokes, g_free);
+  sym->strokes = NULL;
+
+  GIMP_SYMMETRY_GET_CLASS (sym)->update_strokes (sym,
+                                                 drawable,
+                                                 origin);
+}
+
+/**
+ * gimp_symmetry_get_origin:
+ * @sym: the #GimpSymmetry
+ *
+ * Returns: the origin stroke coordinates.
+ * The returned value is owned by the #GimpSymmetry and must not be freed.
+ **/
+GimpCoords *
+gimp_symmetry_get_origin (GimpSymmetry *sym)
+{
+  g_return_val_if_fail (GIMP_IS_SYMMETRY (sym), NULL);
+
+  return sym->origin;
+}
+
+/**
+ * gimp_symmetry_get_size:
+ * @sym: the #GimpSymmetry
+ *
+ * Returns: the total number of strokes.
+ **/
+gint
+gimp_symmetry_get_size (GimpSymmetry *sym)
+{
+  g_return_val_if_fail (GIMP_IS_SYMMETRY (sym), 0);
+
+  return g_list_length (sym->strokes);
+}
+
+/**
+ * gimp_symmetry_get_coords:
+ * @sym:    the #GimpSymmetry
+ * @stroke: the stroke number
+ *
+ * Returns: the coordinates of the stroke number @stroke.
+ * The returned value is owned by the #GimpSymmetry and must not be freed.
+ **/
+GimpCoords *
+gimp_symmetry_get_coords (GimpSymmetry *sym,
+                          gint          stroke)
+{
+  g_return_val_if_fail (GIMP_IS_SYMMETRY (sym), NULL);
+
+  return g_list_nth_data (sym->strokes, stroke);
+}
+
+/**
+ * gimp_symmetry_get_operation:
+ * @sym:          the #GimpSymmetry
+ * @stroke:       the stroke number
+ * @paint_width:  the width of the painting area
+ * @paint_height: the height of the painting area
+ *
+ * Returns: the operation to apply to the paint buffer for stroke number @stroke.
+ * NULL means to copy the original stroke as-is.
+ **/
+GeglNode *
+gimp_symmetry_get_operation (GimpSymmetry *sym,
+                             gint          stroke,
+                             gint          paint_width,
+                             gint          paint_height)
+{
+  g_return_val_if_fail (GIMP_IS_SYMMETRY (sym), NULL);
+
+  return GIMP_SYMMETRY_GET_CLASS (sym)->get_operation (sym,
+                                                       stroke,
+                                                       paint_width,
+                                                       paint_height);
+}
+
+/**
+ * gimp_symmetry_get_settings:
+ * @sym:     the #GimpSymmetry
+ * @n_properties: the number of properties in the returned array
+ *
+ * Returns: an array of the symmetry properties which are supposed to
+ * be settable by the user.
+ * The returned array must be freed by the caller.
+ **/
+GParamSpec **
+gimp_symmetry_get_settings (GimpSymmetry *sym,
+                            gint         *n_properties)
+{
+  g_return_val_if_fail (GIMP_IS_SYMMETRY (sym), NULL);
+
+  return GIMP_SYMMETRY_GET_CLASS (sym)->get_settings (sym,
+                                                      n_properties);
+}
+
+/*
+ * gimp_symmetry_parasite_name:
+ * @type: the #GimpSymmetry's #GType
+ *
+ * Returns: a newly allocated string.
+ */
+gchar *
+gimp_symmetry_parasite_name (GType type)
+{
+  GimpSymmetryClass *klass;
+
+  klass = g_type_class_ref (type);
+
+  return g_strconcat ("gimp-image-symmetry:", klass->label, NULL);
+}
+
+GimpParasite *
+gimp_symmetry_to_parasite (const GimpSymmetry *sym)
+{
+  GimpParasite *parasite;
+  gchar        *parasite_name;
+  gchar        *str;
+
+  g_return_val_if_fail (GIMP_IS_SYMMETRY (sym), NULL);
+
+  str = gimp_config_serialize_to_string (GIMP_CONFIG (sym), NULL);
+  g_return_val_if_fail (str != NULL, NULL);
+
+  parasite_name = gimp_symmetry_parasite_name (sym->type);
+  parasite = gimp_parasite_new (parasite_name,
+                                GIMP_PARASITE_PERSISTENT,
+                                strlen (str) + 1, str);
+  g_free (parasite_name);
+  g_free (str);
+
+  return parasite;
+}
+
+GimpSymmetry *
+gimp_symmetry_from_parasite (const GimpParasite *parasite,
+                             GimpImage          *image,
+                             GType               type)
+{
+  GimpSymmetry    *symmetry;
+  gchar           *parasite_name;
+  const gchar     *str;
+  GError          *error = NULL;
+
+  parasite_name = gimp_symmetry_parasite_name (type);
+
+  g_return_val_if_fail (parasite != NULL, NULL);
+  g_return_val_if_fail (strcmp (gimp_parasite_name (parasite),
+                                parasite_name) == 0,
+                        NULL);
+  g_free (parasite_name);
+
+  str = gimp_parasite_data (parasite);
+  g_return_val_if_fail (str != NULL, NULL);
+
+  symmetry = gimp_image_symmetry_new (image, type);
+
+  if (! gimp_config_deserialize_string (GIMP_CONFIG (symmetry),
+                                        str,
+                                        gimp_parasite_data_size (parasite),
+                                        NULL,
+                                        &error))
+    {
+      g_warning ("Failed to deserialize symmetry parasite: %s", error->message);
+      g_error_free (error);
+    }
+
+  return symmetry;
+}
diff --git a/app/core/gimpsymmetry.h b/app/core/gimpsymmetry.h
new file mode 100644
index 0000000..50c3d4e
--- /dev/null
+++ b/app/core/gimpsymmetry.h
@@ -0,0 +1,94 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimpsymmetry.h
+ * Copyright (C) 2015 Jehan <jehan gimp org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __GIMP_SYMMETRY_H__
+#define __GIMP_SYMMETRY_H__
+
+
+#include "gimpobject.h"
+
+
+#define GIMP_TYPE_SYMMETRY            (gimp_symmetry_get_type ())
+#define GIMP_SYMMETRY(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIMP_TYPE_SYMMETRY, GimpSymmetry))
+#define GIMP_SYMMETRY_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), GIMP_TYPE_SYMMETRY, 
GimpSymmetryClass))
+#define GIMP_IS_SYMMETRY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIMP_TYPE_SYMMETRY))
+#define GIMP_IS_SYMMETRY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIMP_TYPE_SYMMETRY))
+#define GIMP_SYMMETRY_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), GIMP_TYPE_SYMMETRY, 
GimpSymmetryClass))
+
+typedef struct _GimpSymmetryClass   GimpSymmetryClass;
+
+struct _GimpSymmetry
+{
+  GimpObject    parent_instance;
+
+  GimpImage    *image;
+  GimpDrawable *drawable;
+  GimpCoords   *origin;
+  gboolean      active;
+
+  GList        *strokes;
+
+  GType         type;
+};
+
+struct _GimpSymmetryClass
+{
+  GimpObjectClass  parent_class;
+
+  const gchar * label;
+
+  /* Virtual functions */
+  void       (* update_strokes)             (GimpSymmetry       *symmetry,
+                                             GimpDrawable       *drawable,
+                                             GimpCoords         *origin);
+  GeglNode * (* get_operation)              (GimpSymmetry       *symmetry,
+                                             gint                stroke,
+                                             gint                paint_width,
+                                             gint                paint_height);
+  GParamSpec **
+             (* get_settings)               (GimpSymmetry       *symmetry,
+                                             gint               *n_properties);
+  void       (* active_changed)             (GimpSymmetry       *symmetry);
+};
+
+
+GType          gimp_symmetry_get_type       (void) G_GNUC_CONST;
+
+void           gimp_symmetry_set_origin     (GimpSymmetry       *symmetry,
+                                             GimpDrawable       *drawable,
+                                             GimpCoords         *origin);
+
+GimpCoords   * gimp_symmetry_get_origin     (GimpSymmetry       *symmetry);
+gint           gimp_symmetry_get_size       (GimpSymmetry       *symmetry);
+GimpCoords   * gimp_symmetry_get_coords     (GimpSymmetry       *symmetry,
+                                             gint                stroke);
+GeglNode     * gimp_symmetry_get_operation  (GimpSymmetry       *symmetry,
+                                             gint                stroke,
+                                             gint                paint_width,
+                                             gint                paint_height);
+GParamSpec  ** gimp_symmetry_get_settings   (GimpSymmetry       *symmetry,
+                                             gint               *n_properties);
+
+gchar        * gimp_symmetry_parasite_name  (GType type);
+GimpParasite * gimp_symmetry_to_parasite    (const GimpSymmetry *symmetry);
+GimpSymmetry * gimp_symmetry_from_parasite  (const GimpParasite *parasite,
+                                             GimpImage          *image,
+                                             GType               type);
+#endif  /*  __GIMP_SYMMETRY_H__  */
diff --git a/app/dialogs/dialogs-constructors.c b/app/dialogs/dialogs-constructors.c
index bed8d83..8db0e64 100644
--- a/app/dialogs/dialogs-constructors.c
+++ b/app/dialogs/dialogs-constructors.c
@@ -53,6 +53,7 @@
 #include "widgets/gimppatternfactoryview.h"
 #include "widgets/gimpsamplepointeditor.h"
 #include "widgets/gimpselectioneditor.h"
+#include "widgets/gimpsymmetryeditor.h"
 #include "widgets/gimptemplateview.h"
 #include "widgets/gimptoolbox.h"
 #include "widgets/gimptooloptionseditor.h"
@@ -737,6 +738,17 @@ dialogs_selection_editor_new (GimpDialogFactory *factory,
 }
 
 GtkWidget *
+dialogs_symmetry_editor_new (GimpDialogFactory *factory,
+                             GimpContext       *context,
+                             GimpUIManager     *ui_manager,
+                             gint               view_size)
+{
+  return gimp_symmetry_editor_new (context->gimp,
+                                   gimp_context_get_image (context),
+                                   gimp_dialog_factory_get_menu_factory (factory));
+}
+
+GtkWidget *
 dialogs_undo_editor_new (GimpDialogFactory *factory,
                          GimpContext       *context,
                          GimpUIManager     *ui_manager,
diff --git a/app/dialogs/dialogs-constructors.h b/app/dialogs/dialogs-constructors.h
index 8f457a1..e25b3c0 100644
--- a/app/dialogs/dialogs-constructors.h
+++ b/app/dialogs/dialogs-constructors.h
@@ -244,6 +244,10 @@ GtkWidget * dialogs_selection_editor_new        (GimpDialogFactory *factory,
                                                  GimpContext       *context,
                                                  GimpUIManager     *ui_manager,
                                                  gint               view_size);
+GtkWidget * dialogs_symmetry_editor_new         (GimpDialogFactory *factory,
+                                                 GimpContext       *context,
+                                                 GimpUIManager     *ui_manager,
+                                                 gint               view_size);
 GtkWidget * dialogs_undo_editor_new             (GimpDialogFactory *factory,
                                                  GimpContext       *context,
                                                  GimpUIManager     *ui_manager,
diff --git a/app/dialogs/dialogs.c b/app/dialogs/dialogs.c
index a6bcf65..b262433 100644
--- a/app/dialogs/dialogs.c
+++ b/app/dialogs/dialogs.c
@@ -385,6 +385,10 @@ static const GimpDialogFactoryEntry entries[] =
             N_("Selection"), N_("Selection Editor"), GIMP_STOCK_SELECTION,
             GIMP_HELP_SELECTION_DIALOG,
             dialogs_selection_editor_new, 0, FALSE),
+  DOCKABLE ("gimp-symmetry-editor",
+            N_("Symmetry Painting"), NULL, GIMP_STOCK_SYMMETRY,
+            GIMP_HELP_SYMMETRY_DIALOG,
+            dialogs_symmetry_editor_new, 0, FALSE),
   DOCKABLE ("gimp-undo-history",
             N_("Undo"), N_("Undo History"), GIMP_STOCK_UNDO_HISTORY,
             GIMP_HELP_UNDO_DIALOG,
diff --git a/app/dialogs/preferences-dialog.c b/app/dialogs/preferences-dialog.c
index c839c83..9b866ae 100644
--- a/app/dialogs/preferences-dialog.c
+++ b/app/dialogs/preferences-dialog.c
@@ -1601,6 +1601,9 @@ prefs_dialog_new (Gimp       *gimp,
       button = prefs_check_button_add (object, "playground-seamless-clone-tool",
                                        _("_Seamless Clone tool"),
                                        GTK_BOX (vbox2));
+      button = prefs_check_button_add (object, "playground-symmetry",
+                                       _("_Symmetry Painting"),
+                                       GTK_BOX (vbox2));
     }
 
 
diff --git a/app/display/display-enums.c b/app/display/display-enums.c
index 70e0958..7eda7dd 100644
--- a/app/display/display-enums.c
+++ b/app/display/display-enums.c
@@ -228,6 +228,37 @@ gimp_zoom_focus_get_type (void)
   return type;
 }
 
+GType
+gimp_guide_style_get_type (void)
+{
+  static const GEnumValue values[] =
+  {
+    { GIMP_GUIDE_STYLE_NONE, "GIMP_GUIDE_STYLE_NONE", "none" },
+    { GIMP_GUIDE_STYLE_NORMAL, "GIMP_GUIDE_STYLE_NORMAL", "normal" },
+    { GIMP_GUIDE_STYLE_MIRROR, "GIMP_GUIDE_STYLE_MIRROR", "mirror" },
+    { 0, NULL, NULL }
+  };
+
+  static const GimpEnumDesc descs[] =
+  {
+    { GIMP_GUIDE_STYLE_NONE, "GIMP_GUIDE_STYLE_NONE", NULL },
+    { GIMP_GUIDE_STYLE_NORMAL, "GIMP_GUIDE_STYLE_NORMAL", NULL },
+    { GIMP_GUIDE_STYLE_MIRROR, "GIMP_GUIDE_STYLE_MIRROR", NULL },
+    { 0, NULL, NULL }
+  };
+
+  static GType type = 0;
+
+  if (G_UNLIKELY (! type))
+    {
+      type = g_enum_register_static ("GimpGuideStyle", values);
+      gimp_type_set_translation_context (type, "guide-style");
+      gimp_enum_set_value_descriptions (type, descs);
+    }
+
+  return type;
+}
+
 
 /* Generated data ends here */
 
diff --git a/app/display/display-enums.h b/app/display/display-enums.h
index 01ef767..5d164ed 100644
--- a/app/display/display-enums.h
+++ b/app/display/display-enums.h
@@ -117,5 +117,15 @@ typedef enum
 
 } GimpZoomFocus;
 
+#define GIMP_TYPE_GUIDE_STYLE (gimp_guide_style_get_type ())
+
+GType gimp_guide_style_get_type (void) G_GNUC_CONST;
+
+typedef enum
+{
+    GIMP_GUIDE_STYLE_NONE,
+    GIMP_GUIDE_STYLE_NORMAL,
+    GIMP_GUIDE_STYLE_MIRROR
+} GimpGuideStyle;
 
 #endif /* __DISPLAY_ENUMS_H__ */
diff --git a/app/paint/gimpairbrush.c b/app/paint/gimpairbrush.c
index 725b5a0..29dcc85 100644
--- a/app/paint/gimpairbrush.c
+++ b/app/paint/gimpairbrush.c
@@ -28,6 +28,7 @@
 #include "core/gimpdynamics.h"
 #include "core/gimpgradient.h"
 #include "core/gimpimage.h"
+#include "core/gimpsymmetry.h"
 
 #include "gimpairbrush.h"
 #include "gimpairbrushoptions.h"
@@ -40,13 +41,13 @@ static void       gimp_airbrush_finalize (GObject          *object);
 static void       gimp_airbrush_paint    (GimpPaintCore    *paint_core,
                                           GimpDrawable     *drawable,
                                           GimpPaintOptions *paint_options,
-                                          const GimpCoords *coords,
+                                          GimpSymmetry     *sym,
                                           GimpPaintState    paint_state,
                                           guint32           time);
 static void       gimp_airbrush_motion   (GimpPaintCore    *paint_core,
                                           GimpDrawable     *drawable,
                                           GimpPaintOptions *paint_options,
-                                          const GimpCoords *coords);
+                                          GimpSymmetry     *sym);
 static gboolean   gimp_airbrush_timeout  (gpointer          data);
 
 
@@ -82,6 +83,7 @@ static void
 gimp_airbrush_init (GimpAirbrush *airbrush)
 {
   airbrush->timeout_id = 0;
+  airbrush->sym    = NULL;
 }
 
 static void
@@ -95,6 +97,9 @@ gimp_airbrush_finalize (GObject *object)
       airbrush->timeout_id = 0;
     }
 
+  if (airbrush->sym)
+    g_object_unref (airbrush->sym);
+
   G_OBJECT_CLASS (parent_class)->finalize (object);
 }
 
@@ -102,7 +107,7 @@ static void
 gimp_airbrush_paint (GimpPaintCore    *paint_core,
                      GimpDrawable     *drawable,
                      GimpPaintOptions *paint_options,
-                     const GimpCoords *coords,
+                     GimpSymmetry     *sym,
                      GimpPaintState    paint_state,
                      guint32           time)
 {
@@ -121,7 +126,7 @@ gimp_airbrush_paint (GimpPaintCore    *paint_core,
 
       GIMP_PAINT_CORE_CLASS (parent_class)->paint (paint_core, drawable,
                                                    paint_options,
-                                                   coords,
+                                                   sym,
                                                    paint_state, time);
       break;
 
@@ -132,14 +137,15 @@ gimp_airbrush_paint (GimpPaintCore    *paint_core,
           airbrush->timeout_id = 0;
         }
 
-      gimp_airbrush_motion (paint_core, drawable, paint_options, coords);
+      gimp_airbrush_motion (paint_core, drawable, paint_options, sym);
 
       if ((options->rate != 0.0) && (!options->motion_only))
         {
-          GimpImage *image = gimp_item_get_image (GIMP_ITEM (drawable));
-          gdouble    fade_point;
-          gdouble    dynamic_rate;
-          gint       timeout;
+          GimpImage  *image = gimp_item_get_image (GIMP_ITEM (drawable));
+          gdouble     fade_point;
+          gdouble     dynamic_rate;
+          gint        timeout;
+          GimpCoords *coords;
 
           fade_point = gimp_paint_options_get_fade (paint_options, image,
                                                     paint_core->pixel_dist);
@@ -147,6 +153,13 @@ gimp_airbrush_paint (GimpPaintCore    *paint_core,
           airbrush->drawable      = drawable;
           airbrush->paint_options = paint_options;
 
+          if (airbrush->sym)
+            g_object_unref (airbrush->sym);
+          airbrush->sym = g_object_ref (sym);
+
+          /* Base our timeout on the original stroke. */
+          coords = gimp_symmetry_get_origin (sym);
+
           dynamic_rate = gimp_dynamics_get_linear_value (dynamics,
                                                          GIMP_DYNAMICS_OUTPUT_RATE,
                                                          coords,
@@ -170,7 +183,7 @@ gimp_airbrush_paint (GimpPaintCore    *paint_core,
 
       GIMP_PAINT_CORE_CLASS (parent_class)->paint (paint_core, drawable,
                                                    paint_options,
-                                                   coords,
+                                                   sym,
                                                    paint_state, time);
       break;
     }
@@ -180,7 +193,7 @@ static void
 gimp_airbrush_motion (GimpPaintCore    *paint_core,
                       GimpDrawable     *drawable,
                       GimpPaintOptions *paint_options,
-                      const GimpCoords *coords)
+                      GimpSymmetry     *sym)
 
 {
   GimpAirbrushOptions *options  = GIMP_AIRBRUSH_OPTIONS (paint_options);
@@ -188,10 +201,13 @@ gimp_airbrush_motion (GimpPaintCore    *paint_core,
   GimpImage           *image    = gimp_item_get_image (GIMP_ITEM (drawable));
   gdouble              opacity;
   gdouble              fade_point;
+  GimpCoords          *coords;
 
   fade_point = gimp_paint_options_get_fade (paint_options, image,
                                             paint_core->pixel_dist);
 
+  coords = gimp_symmetry_get_origin (sym);
+
   opacity = (options->flow / 100.0 *
              gimp_dynamics_get_linear_value (dynamics,
                                              GIMP_DYNAMICS_OUTPUT_FLOW,
@@ -199,21 +215,19 @@ gimp_airbrush_motion (GimpPaintCore    *paint_core,
                                              paint_options,
                                              fade_point));
 
-  _gimp_paintbrush_motion (paint_core, drawable, paint_options, coords, opacity);
+  _gimp_paintbrush_motion (paint_core, drawable, paint_options,
+                           sym, opacity);
 }
 
 static gboolean
 gimp_airbrush_timeout (gpointer data)
 {
   GimpAirbrush *airbrush = GIMP_AIRBRUSH (data);
-  GimpCoords    coords;
-
-  gimp_paint_core_get_current_coords (GIMP_PAINT_CORE (airbrush), &coords);
 
   gimp_airbrush_paint (GIMP_PAINT_CORE (airbrush),
                        airbrush->drawable,
                        airbrush->paint_options,
-                       &coords,
+                       airbrush->sym,
                        GIMP_PAINT_STATE_MOTION, 0);
 
   gimp_image_flush (gimp_item_get_image (GIMP_ITEM (airbrush->drawable)));
diff --git a/app/paint/gimpairbrush.h b/app/paint/gimpairbrush.h
index 9b27f8d..a3f9a8a 100644
--- a/app/paint/gimpairbrush.h
+++ b/app/paint/gimpairbrush.h
@@ -37,6 +37,8 @@ struct _GimpAirbrush
   GimpPaintbrush    parent_instance;
 
   guint             timeout_id;
+
+  GimpSymmetry     *sym;
   GimpDrawable     *drawable;
   GimpPaintOptions *paint_options;
 };
diff --git a/app/paint/gimpbrushcore.c b/app/paint/gimpbrushcore.c
index 9201643..ce1320e 100644
--- a/app/paint/gimpbrushcore.c
+++ b/app/paint/gimpbrushcore.c
@@ -84,7 +84,9 @@ static GeglBuffer * gimp_brush_core_get_paint_buffer(GimpPaintCore    *paint_cor
                                                      GimpPaintOptions *paint_options,
                                                      const GimpCoords *coords,
                                                      gint             *paint_buffer_x,
-                                                     gint             *paint_buffer_y);
+                                                     gint             *paint_buffer_y,
+                                                     gint             *paint_width,
+                                                     gint             *paint_height);
 
 static void      gimp_brush_core_real_set_brush     (GimpBrushCore    *core,
                                                      GimpBrush        *brush);
@@ -109,10 +111,12 @@ static const GimpTempBuf *
                                                      gdouble            y);
 static const GimpTempBuf *
                  gimp_brush_core_transform_mask     (GimpBrushCore     *core,
-                                                     GimpBrush         *brush);
+                                                     GimpBrush         *brush,
+                                                     GeglNode          *op);
 static const GimpTempBuf *
                  gimp_brush_core_transform_pixmap   (GimpBrushCore     *core,
-                                                     GimpBrush         *brush);
+                                                     GimpBrush         *brush,
+                                                     GeglNode          *op);
 
 static void      gimp_brush_core_invalidate_cache   (GimpBrush         *brush,
                                                      GimpBrushCore     *core);
@@ -811,7 +815,9 @@ gimp_brush_core_get_paint_buffer (GimpPaintCore    *paint_core,
                                   GimpPaintOptions *paint_options,
                                   const GimpCoords *coords,
                                   gint             *paint_buffer_x,
-                                  gint             *paint_buffer_y)
+                                  gint             *paint_buffer_y,
+                                  gint             *paint_width,
+                                  gint             *paint_height)
 {
   GimpBrushCore *core = GIMP_BRUSH_CORE (paint_core);
   gint           x, y;
@@ -819,18 +825,15 @@ gimp_brush_core_get_paint_buffer (GimpPaintCore    *paint_core,
   gint           drawable_width, drawable_height;
   gint           brush_width, brush_height;
 
-  if (GIMP_BRUSH_CORE_GET_CLASS (core)->handles_transforming_brush)
-    {
-      gimp_brush_core_eval_transform_dynamics (core,
-                                               drawable,
-                                               paint_options,
-                                               coords);
-    }
-
   gimp_brush_transform_size (core->brush,
                              core->scale, core->aspect_ratio, core->angle,
                              &brush_width, &brush_height);
 
+  if (paint_width)
+    *paint_width  = brush_width;
+  if (paint_height)
+    *paint_height = brush_height;
+
   /*  adjust the x and y coordinates to the upper left corner of the brush  */
   x = (gint) floor (coords->x) - (brush_width  / 2);
   y = (gint) floor (coords->y) - (brush_height / 2);
@@ -960,11 +963,12 @@ gimp_brush_core_paste_canvas (GimpBrushCore            *core,
                               GimpLayerModeEffects      paint_mode,
                               GimpBrushApplicationMode  brush_hardness,
                               gdouble                   dynamic_force,
-                              GimpPaintApplicationMode  mode)
+                              GimpPaintApplicationMode  mode,
+                              GeglNode                 *op)
 {
   const GimpTempBuf *brush_mask;
 
-  brush_mask = gimp_brush_core_get_brush_mask (core, coords,
+  brush_mask = gimp_brush_core_get_brush_mask (core, coords, op,
                                                brush_hardness,
                                                dynamic_force);
 
@@ -1003,11 +1007,12 @@ gimp_brush_core_replace_canvas (GimpBrushCore            *core,
                                 gdouble                   image_opacity,
                                 GimpBrushApplicationMode  brush_hardness,
                                 gdouble                   dynamic_force,
-                                GimpPaintApplicationMode  mode)
+                                GimpPaintApplicationMode  mode,
+                                GeglNode                 *op)
 {
   const GimpTempBuf *brush_mask;
 
-  brush_mask = gimp_brush_core_get_brush_mask (core, coords,
+  brush_mask = gimp_brush_core_get_brush_mask (core, coords, op,
                                                brush_hardness,
                                                dynamic_force);
 
@@ -1407,7 +1412,8 @@ gimp_brush_core_solidify_mask (GimpBrushCore     *core,
 
 static const GimpTempBuf *
 gimp_brush_core_transform_mask (GimpBrushCore *core,
-                                GimpBrush     *brush)
+                                GimpBrush     *brush,
+                                GeglNode      *op)
 {
   const GimpTempBuf *mask;
 
@@ -1415,6 +1421,7 @@ gimp_brush_core_transform_mask (GimpBrushCore *core,
     return NULL;
 
   mask = gimp_brush_transform_mask (brush,
+                                    op,
                                     core->scale,
                                     core->aspect_ratio,
                                     core->angle,
@@ -1432,7 +1439,8 @@ gimp_brush_core_transform_mask (GimpBrushCore *core,
 
 static const GimpTempBuf *
 gimp_brush_core_transform_pixmap (GimpBrushCore *core,
-                                  GimpBrush     *brush)
+                                  GimpBrush     *brush,
+                                  GeglNode      *op)
 {
   const GimpTempBuf *pixmap;
 
@@ -1440,6 +1448,7 @@ gimp_brush_core_transform_pixmap (GimpBrushCore *core,
     return NULL;
 
   pixmap = gimp_brush_transform_pixmap (brush,
+                                        op,
                                         core->scale,
                                         core->aspect_ratio,
                                         core->angle,
@@ -1457,12 +1466,13 @@ gimp_brush_core_transform_pixmap (GimpBrushCore *core,
 const GimpTempBuf *
 gimp_brush_core_get_brush_mask (GimpBrushCore            *core,
                                 const GimpCoords         *coords,
+                                GeglNode                 *op,
                                 GimpBrushApplicationMode  brush_hardness,
                                 gdouble                   dynamic_force)
 {
   const GimpTempBuf *mask;
 
-  mask = gimp_brush_core_transform_mask (core, core->brush);
+  mask = gimp_brush_core_transform_mask (core, core->brush, op);
 
   if (! mask)
     return NULL;
@@ -1591,6 +1601,7 @@ void
 gimp_brush_core_color_area_with_pixmap (GimpBrushCore            *core,
                                         GimpDrawable             *drawable,
                                         const GimpCoords         *coords,
+                                        GeglNode                 *op,
                                         GeglBuffer               *area,
                                         gint                      area_x,
                                         gint                      area_y,
@@ -1609,13 +1620,13 @@ gimp_brush_core_color_area_with_pixmap (GimpBrushCore            *core,
   g_return_if_fail (gimp_brush_get_pixmap (core->brush) != NULL);
 
   /*  scale the brushes  */
-  pixmap_mask = gimp_brush_core_transform_pixmap (core, core->brush);
+  pixmap_mask = gimp_brush_core_transform_pixmap (core, core->brush, op);
 
   if (! pixmap_mask)
     return;
 
   if (mode != GIMP_BRUSH_HARD)
-    brush_mask = gimp_brush_core_transform_mask (core, core->brush);
+    brush_mask = gimp_brush_core_transform_mask (core, core->brush, op);
   else
     brush_mask = NULL;
 
diff --git a/app/paint/gimpbrushcore.h b/app/paint/gimpbrushcore.h
index 1110a14..436db95 100644
--- a/app/paint/gimpbrushcore.h
+++ b/app/paint/gimpbrushcore.h
@@ -107,7 +107,8 @@ void   gimp_brush_core_paste_canvas   (GimpBrushCore            *core,
                                        GimpLayerModeEffects      paint_mode,
                                        GimpBrushApplicationMode  brush_hardness,
                                        gdouble                   dynamic_hardness,
-                                       GimpPaintApplicationMode  mode);
+                                       GimpPaintApplicationMode  mode,
+                                       GeglNode                 *op);
 void   gimp_brush_core_replace_canvas (GimpBrushCore            *core,
                                        GimpDrawable             *drawable,
                                        const GimpCoords         *coords,
@@ -115,12 +116,14 @@ void   gimp_brush_core_replace_canvas (GimpBrushCore            *core,
                                        gdouble                   image_opacity,
                                        GimpBrushApplicationMode  brush_hardness,
                                        gdouble                   dynamic_hardness,
-                                       GimpPaintApplicationMode  mode);
+                                       GimpPaintApplicationMode  mode,
+                                       GeglNode                 *op);
 
 void   gimp_brush_core_color_area_with_pixmap
                                       (GimpBrushCore            *core,
                                        GimpDrawable             *drawable,
                                        const GimpCoords         *coords,
+                                       GeglNode                 *op,
                                        GeglBuffer               *area,
                                        gint                      area_x,
                                        gint                      area_y,
@@ -129,6 +132,7 @@ void   gimp_brush_core_color_area_with_pixmap
 const GimpTempBuf * gimp_brush_core_get_brush_mask
                                       (GimpBrushCore            *core,
                                        const GimpCoords         *coords,
+                                       GeglNode                 *op,
                                        GimpBrushApplicationMode  brush_hardness,
                                        gdouble                   dynamic_hardness);
 
diff --git a/app/paint/gimpclone.c b/app/paint/gimpclone.c
index 6cc5f09..78a24b0 100644
--- a/app/paint/gimpclone.c
+++ b/app/paint/gimpclone.c
@@ -34,6 +34,7 @@
 #include "core/gimpimage.h"
 #include "core/gimppattern.h"
 #include "core/gimppickable.h"
+#include "core/gimpsymmetry.h"
 
 #include "gimpclone.h"
 #include "gimpcloneoptions.h"
@@ -51,6 +52,7 @@ static void       gimp_clone_motion     (GimpSourceCore    *source_core,
                                          GimpDrawable      *drawable,
                                          GimpPaintOptions  *paint_options,
                                          const GimpCoords  *coords,
+                                         GeglNode          *op,
                                          gdouble            opacity,
                                          GimpPickable      *src_pickable,
                                          GeglBuffer        *src_buffer,
@@ -137,6 +139,7 @@ gimp_clone_motion (GimpSourceCore   *source_core,
                    GimpDrawable     *drawable,
                    GimpPaintOptions *paint_options,
                    const GimpCoords *coords,
+                   GeglNode         *op,
                    gdouble           opacity,
                    GimpPickable     *src_pickable,
                    GeglBuffer       *src_buffer,
@@ -173,6 +176,26 @@ gimp_clone_motion (GimpSourceCore   *source_core,
                         GEGL_RECTANGLE (paint_area_offset_x,
                                         paint_area_offset_y,
                                         0, 0));
+      if (op)
+        {
+          GeglNode    *graph, *source, *target;
+
+          graph    = gegl_node_new ();
+          source   = gegl_node_new_child (graph,
+                                          "operation", "gegl:buffer-source",
+                                          "buffer", paint_buffer,
+                                          NULL);
+          gegl_node_add_child (graph, op);
+          target  = gegl_node_new_child (graph,
+                                         "operation", "gegl:write-buffer",
+                                         "buffer", paint_buffer,
+                                         NULL);
+
+          gegl_node_link_many (source, op, target, NULL);
+          gegl_node_process (target);
+
+          g_object_unref (graph);
+        }
     }
   else if (options->clone_type == GIMP_CLONE_PATTERN)
     {
@@ -223,7 +246,8 @@ gimp_clone_motion (GimpSourceCore   *source_core,
                                  */
                                 source_options->align_mode ==
                                 GIMP_SOURCE_ALIGN_FIXED ?
-                                GIMP_PAINT_INCREMENTAL : GIMP_PAINT_CONSTANT);
+                                GIMP_PAINT_INCREMENTAL : GIMP_PAINT_CONSTANT,
+                                NULL);
 }
 
 static gboolean
diff --git a/app/paint/gimpconvolve.c b/app/paint/gimpconvolve.c
index a6d3a13..15e4112 100644
--- a/app/paint/gimpconvolve.c
+++ b/app/paint/gimpconvolve.c
@@ -30,6 +30,7 @@
 #include "core/gimpdynamics.h"
 #include "core/gimpimage.h"
 #include "core/gimppickable.h"
+#include "core/gimpsymmetry.h"
 #include "core/gimptempbuf.h"
 
 #include "gimpconvolve.h"
@@ -48,13 +49,13 @@
 static void    gimp_convolve_paint            (GimpPaintCore    *paint_core,
                                                GimpDrawable     *drawable,
                                                GimpPaintOptions *paint_options,
-                                               const GimpCoords *coords,
+                                               GimpSymmetry     *sym,
                                                GimpPaintState    paint_state,
                                                guint32           time);
 static void    gimp_convolve_motion           (GimpPaintCore    *paint_core,
                                                GimpDrawable     *drawable,
                                                GimpPaintOptions *paint_options,
-                                               const GimpCoords *coords);
+                                               GimpSymmetry     *sym);
 
 static void    gimp_convolve_calculate_matrix (GimpConvolve     *convolve,
                                                GimpConvolveType  type,
@@ -102,14 +103,14 @@ static void
 gimp_convolve_paint (GimpPaintCore    *paint_core,
                      GimpDrawable     *drawable,
                      GimpPaintOptions *paint_options,
-                     const GimpCoords *coords,
+                     GimpSymmetry     *sym,
                      GimpPaintState    paint_state,
                      guint32           time)
 {
   switch (paint_state)
     {
     case GIMP_PAINT_STATE_MOTION:
-      gimp_convolve_motion (paint_core, drawable, paint_options, coords);
+      gimp_convolve_motion (paint_core, drawable, paint_options, sym);
       break;
 
     default:
@@ -121,7 +122,7 @@ static void
 gimp_convolve_motion (GimpPaintCore    *paint_core,
                       GimpDrawable     *drawable,
                       GimpPaintOptions *paint_options,
-                      const GimpCoords *coords)
+                      GimpSymmetry     *sym)
 {
   GimpConvolve        *convolve   = GIMP_CONVOLVE (paint_core);
   GimpBrushCore       *brush_core = GIMP_BRUSH_CORE (paint_core);
@@ -137,10 +138,16 @@ gimp_convolve_motion (GimpPaintCore    *paint_core,
   gdouble              fade_point;
   gdouble              opacity;
   gdouble              rate;
+  const GimpCoords    *coords;
+  GeglNode            *op;
+  gint                 paint_width, paint_height;
+  gint                 n_strokes;
+  gint                 i;
 
   fade_point = gimp_paint_options_get_fade (paint_options, image,
                                             paint_core->pixel_dist);
 
+  coords = gimp_symmetry_get_origin (sym);
   opacity = gimp_dynamics_get_linear_value (dynamics,
                                             GIMP_DYNAMICS_OUTPUT_OPACITY,
                                             coords,
@@ -149,61 +156,77 @@ gimp_convolve_motion (GimpPaintCore    *paint_core,
   if (opacity == 0.0)
     return;
 
-  paint_buffer = gimp_paint_core_get_paint_buffer (paint_core, drawable,
-                                                   paint_options, coords,
-                                                   &paint_buffer_x,
-                                                   &paint_buffer_y);
-  if (! paint_buffer)
-    return;
-
-  rate = (options->rate *
-          gimp_dynamics_get_linear_value (dynamics,
-                                          GIMP_DYNAMICS_OUTPUT_RATE,
-                                          coords,
-                                          paint_options,
-                                          fade_point));
-
-  gimp_convolve_calculate_matrix (convolve, options->type,
-                                  gimp_brush_get_width  (brush_core->brush) / 2,
-                                  gimp_brush_get_height (brush_core->brush) / 2,
-                                  rate);
-
-  /*  need a linear buffer for gimp_gegl_convolve()  */
-  temp_buf = gimp_temp_buf_new (gegl_buffer_get_width  (paint_buffer),
-                                gegl_buffer_get_height (paint_buffer),
-                                gegl_buffer_get_format (paint_buffer));
-  convolve_buffer = gimp_temp_buf_create_buffer (temp_buf);
-  gimp_temp_buf_unref (temp_buf);
-
-  gegl_buffer_copy (gimp_drawable_get_buffer (drawable),
-                    GEGL_RECTANGLE (paint_buffer_x,
-                                    paint_buffer_y,
-                                    gegl_buffer_get_width  (paint_buffer),
-                                    gegl_buffer_get_height (paint_buffer)),
-                    GEGL_ABYSS_NONE,
-                    convolve_buffer,
-                    GEGL_RECTANGLE (0, 0, 0, 0));
-
-  gimp_gegl_convolve (convolve_buffer,
-                      GEGL_RECTANGLE (0, 0,
-                                      gegl_buffer_get_width  (convolve_buffer),
-                                      gegl_buffer_get_height (convolve_buffer)),
-                      paint_buffer,
-                      GEGL_RECTANGLE (0, 0,
-                                      gegl_buffer_get_width  (paint_buffer),
-                                      gegl_buffer_get_height (paint_buffer)),
-                      convolve->matrix, 3, convolve->matrix_divisor,
-                      GIMP_NORMAL_CONVOL, TRUE);
-
-  g_object_unref (convolve_buffer);
-
-  gimp_brush_core_replace_canvas (brush_core, drawable,
-                                  coords,
-                                  MIN (opacity, GIMP_OPACITY_OPAQUE),
-                                  gimp_context_get_opacity (context),
-                                  gimp_paint_options_get_brush_mode (paint_options),
-                                  1.0,
-                                  GIMP_PAINT_INCREMENTAL);
+  gimp_brush_core_eval_transform_dynamics (GIMP_BRUSH_CORE (paint_core),
+                                           drawable,
+                                           paint_options,
+                                           coords);
+  n_strokes = gimp_symmetry_get_size (sym);
+  for (i = 0; i < n_strokes; i++)
+    {
+      coords = gimp_symmetry_get_coords (sym, i);
+
+      paint_buffer = gimp_paint_core_get_paint_buffer (paint_core, drawable,
+                                                       paint_options, coords,
+                                                       &paint_buffer_x,
+                                                       &paint_buffer_y,
+                                                       &paint_width,
+                                                       &paint_height);
+      if (! paint_buffer)
+        continue;
+
+      op = gimp_symmetry_get_operation (sym, i,
+                                        paint_width,
+                                        paint_height);
+
+      rate = (options->rate *
+              gimp_dynamics_get_linear_value (dynamics,
+                                              GIMP_DYNAMICS_OUTPUT_RATE,
+                                              coords,
+                                              paint_options,
+                                              fade_point));
+
+      gimp_convolve_calculate_matrix (convolve, options->type,
+                                      gimp_brush_get_width  (brush_core->brush) / 2,
+                                      gimp_brush_get_height (brush_core->brush) / 2,
+                                      rate);
+
+      /*  need a linear buffer for gimp_gegl_convolve()  */
+      temp_buf = gimp_temp_buf_new (gegl_buffer_get_width  (paint_buffer),
+                                    gegl_buffer_get_height (paint_buffer),
+                                    gegl_buffer_get_format (paint_buffer));
+      convolve_buffer = gimp_temp_buf_create_buffer (temp_buf);
+      gimp_temp_buf_unref (temp_buf);
+
+      gegl_buffer_copy (gimp_drawable_get_buffer (drawable),
+                        GEGL_RECTANGLE (paint_buffer_x,
+                                        paint_buffer_y,
+                                        gegl_buffer_get_width  (paint_buffer),
+                                        gegl_buffer_get_height (paint_buffer)),
+                        GEGL_ABYSS_NONE,
+                        convolve_buffer,
+                        GEGL_RECTANGLE (0, 0, 0, 0));
+
+      gimp_gegl_convolve (convolve_buffer,
+                          GEGL_RECTANGLE (0, 0,
+                                          gegl_buffer_get_width  (convolve_buffer),
+                                          gegl_buffer_get_height (convolve_buffer)),
+                          paint_buffer,
+                          GEGL_RECTANGLE (0, 0,
+                                          gegl_buffer_get_width  (paint_buffer),
+                                          gegl_buffer_get_height (paint_buffer)),
+                          convolve->matrix, 3, convolve->matrix_divisor,
+                          GIMP_NORMAL_CONVOL, TRUE);
+
+      g_object_unref (convolve_buffer);
+
+      gimp_brush_core_replace_canvas (brush_core, drawable,
+                                      coords,
+                                      MIN (opacity, GIMP_OPACITY_OPAQUE),
+                                      gimp_context_get_opacity (context),
+                                      gimp_paint_options_get_brush_mode (paint_options),
+                                      1.0,
+                                      GIMP_PAINT_INCREMENTAL, op);
+    }
 }
 
 static void
diff --git a/app/paint/gimpdodgeburn.c b/app/paint/gimpdodgeburn.c
index e87e79a..05fc9e0 100644
--- a/app/paint/gimpdodgeburn.c
+++ b/app/paint/gimpdodgeburn.c
@@ -31,6 +31,7 @@
 #include "core/gimpdrawable.h"
 #include "core/gimpdynamics.h"
 #include "core/gimpimage.h"
+#include "core/gimpsymmetry.h"
 
 #include "gimpdodgeburn.h"
 #include "gimpdodgeburnoptions.h"
@@ -41,13 +42,13 @@
 static void   gimp_dodge_burn_paint  (GimpPaintCore    *paint_core,
                                       GimpDrawable     *drawable,
                                       GimpPaintOptions *paint_options,
-                                      const GimpCoords *coords,
+                                      GimpSymmetry     *sym,
                                       GimpPaintState    paint_state,
                                       guint32           time);
 static void   gimp_dodge_burn_motion (GimpPaintCore    *paint_core,
                                       GimpDrawable     *drawable,
                                       GimpPaintOptions *paint_options,
-                                      const GimpCoords *coords);
+                                      GimpSymmetry     *sym);
 
 
 G_DEFINE_TYPE (GimpDodgeBurn, gimp_dodge_burn, GIMP_TYPE_BRUSH_CORE)
@@ -87,7 +88,7 @@ static void
 gimp_dodge_burn_paint (GimpPaintCore    *paint_core,
                        GimpDrawable     *drawable,
                        GimpPaintOptions *paint_options,
-                       const GimpCoords *coords,
+                       GimpSymmetry     *sym,
                        GimpPaintState    paint_state,
                        guint32           time)
 {
@@ -97,7 +98,7 @@ gimp_dodge_burn_paint (GimpPaintCore    *paint_core,
       break;
 
     case GIMP_PAINT_STATE_MOTION:
-      gimp_dodge_burn_motion (paint_core, drawable, paint_options, coords);
+      gimp_dodge_burn_motion (paint_core, drawable, paint_options, sym);
       break;
 
     case GIMP_PAINT_STATE_FINISH:
@@ -109,7 +110,7 @@ static void
 gimp_dodge_burn_motion (GimpPaintCore    *paint_core,
                         GimpDrawable     *drawable,
                         GimpPaintOptions *paint_options,
-                        const GimpCoords *coords)
+                        GimpSymmetry     *sym)
 {
   GimpDodgeBurnOptions *options   = GIMP_DODGE_BURN_OPTIONS (paint_options);
   GimpContext          *context   = GIMP_CONTEXT (paint_options);
@@ -121,10 +122,16 @@ gimp_dodge_burn_motion (GimpPaintCore    *paint_core,
   gdouble               fade_point;
   gdouble               opacity;
   gdouble               force;
+  const GimpCoords     *coords;
+  GeglNode             *op;
+  gint                  paint_width, paint_height;
+  gint                  n_strokes;
+  gint                  i;
 
   fade_point = gimp_paint_options_get_fade (paint_options, image,
                                             paint_core->pixel_dist);
 
+  coords = gimp_symmetry_get_origin (sym);
   opacity = gimp_dynamics_get_linear_value (dynamics,
                                             GIMP_DYNAMICS_OUTPUT_OPACITY,
                                             coords,
@@ -133,40 +140,56 @@ gimp_dodge_burn_motion (GimpPaintCore    *paint_core,
   if (opacity == 0.0)
     return;
 
-  paint_buffer = gimp_paint_core_get_paint_buffer (paint_core, drawable,
-                                                   paint_options, coords,
-                                                   &paint_buffer_x,
-                                                   &paint_buffer_y);
-  if (! paint_buffer)
-    return;
-
-  /*  DodgeBurn the region  */
-  gimp_gegl_dodgeburn (gimp_paint_core_get_orig_image (paint_core),
-                       GEGL_RECTANGLE (paint_buffer_x,
-                                       paint_buffer_y,
-                                       gegl_buffer_get_width  (paint_buffer),
-                                       gegl_buffer_get_height (paint_buffer)),
-                       paint_buffer,
-                       GEGL_RECTANGLE (0, 0, 0, 0),
-                       options->exposure / 100.0,
-                       options->type,
-                       options->mode);
-
-  if (gimp_dynamics_is_output_enabled (dynamics, GIMP_DYNAMICS_OUTPUT_FORCE))
-    force = gimp_dynamics_get_linear_value (dynamics,
-                                            GIMP_DYNAMICS_OUTPUT_FORCE,
-                                            coords,
-                                            paint_options,
-                                            fade_point);
-  else
-    force = paint_options->brush_force;
-
-  /* Replace the newly dodgedburned area (paint_area) to the image */
-  gimp_brush_core_replace_canvas (GIMP_BRUSH_CORE (paint_core), drawable,
-                                  coords,
-                                  MIN (opacity, GIMP_OPACITY_OPAQUE),
-                                  gimp_context_get_opacity (context),
-                                  gimp_paint_options_get_brush_mode (paint_options),
-                                  force,
-                                  GIMP_PAINT_CONSTANT);
+  gimp_brush_core_eval_transform_dynamics (GIMP_BRUSH_CORE (paint_core),
+                                           drawable,
+                                           paint_options,
+                                           coords);
+  n_strokes = gimp_symmetry_get_size (sym);
+  for (i = 0; i < n_strokes; i++)
+    {
+      coords = gimp_symmetry_get_coords (sym, i);
+
+      paint_buffer = gimp_paint_core_get_paint_buffer (paint_core, drawable,
+                                                       paint_options, coords,
+                                                       &paint_buffer_x,
+                                                       &paint_buffer_y,
+                                                       &paint_width,
+                                                       &paint_height);
+      if (! paint_buffer)
+        continue;
+
+      op = gimp_symmetry_get_operation (sym, i,
+                                        paint_width,
+                                        paint_height);
+
+      /*  DodgeBurn the region  */
+      gimp_gegl_dodgeburn (gimp_paint_core_get_orig_image (paint_core),
+                           GEGL_RECTANGLE (paint_buffer_x,
+                                           paint_buffer_y,
+                                           gegl_buffer_get_width  (paint_buffer),
+                                           gegl_buffer_get_height (paint_buffer)),
+                           paint_buffer,
+                           GEGL_RECTANGLE (0, 0, 0, 0),
+                           options->exposure / 100.0,
+                           options->type,
+                           options->mode);
+
+      if (gimp_dynamics_is_output_enabled (dynamics, GIMP_DYNAMICS_OUTPUT_FORCE))
+        force = gimp_dynamics_get_linear_value (dynamics,
+                                                GIMP_DYNAMICS_OUTPUT_FORCE,
+                                                coords,
+                                                paint_options,
+                                                fade_point);
+      else
+        force = paint_options->brush_force;
+
+      /* Replace the newly dodgedburned area (paint_area) to the image */
+      gimp_brush_core_replace_canvas (GIMP_BRUSH_CORE (paint_core), drawable,
+                                      coords,
+                                      MIN (opacity, GIMP_OPACITY_OPAQUE),
+                                      gimp_context_get_opacity (context),
+                                      gimp_paint_options_get_brush_mode (paint_options),
+                                      force,
+                                      GIMP_PAINT_CONSTANT, op);
+    }
 }
diff --git a/app/paint/gimperaser.c b/app/paint/gimperaser.c
index b3c6e46..175c598 100644
--- a/app/paint/gimperaser.c
+++ b/app/paint/gimperaser.c
@@ -29,6 +29,7 @@
 #include "core/gimpdrawable.h"
 #include "core/gimpdynamics.h"
 #include "core/gimpimage.h"
+#include "core/gimpsymmetry.h"
 
 #include "gimperaser.h"
 #include "gimperaseroptions.h"
@@ -39,13 +40,13 @@
 static void   gimp_eraser_paint  (GimpPaintCore    *paint_core,
                                   GimpDrawable     *drawable,
                                   GimpPaintOptions *paint_options,
-                                  const GimpCoords *coords,
+                                  GimpSymmetry     *sym,
                                   GimpPaintState    paint_state,
                                   guint32           time);
 static void   gimp_eraser_motion (GimpPaintCore    *paint_core,
                                   GimpDrawable     *drawable,
                                   GimpPaintOptions *paint_options,
-                                  const GimpCoords *coords);
+                                  GimpSymmetry     *sym);
 
 
 G_DEFINE_TYPE (GimpEraser, gimp_eraser, GIMP_TYPE_BRUSH_CORE)
@@ -83,7 +84,7 @@ static void
 gimp_eraser_paint (GimpPaintCore    *paint_core,
                    GimpDrawable     *drawable,
                    GimpPaintOptions *paint_options,
-                   const GimpCoords *coords,
+                   GimpSymmetry     *sym,
                    GimpPaintState    paint_state,
                    guint32           time)
 {
@@ -106,7 +107,7 @@ gimp_eraser_paint (GimpPaintCore    *paint_core,
         }
       break;
     case GIMP_PAINT_STATE_MOTION:
-      gimp_eraser_motion (paint_core, drawable, paint_options, coords);
+      gimp_eraser_motion (paint_core, drawable, paint_options, sym);
       break;
 
     default:
@@ -118,7 +119,7 @@ static void
 gimp_eraser_motion (GimpPaintCore    *paint_core,
                     GimpDrawable     *drawable,
                     GimpPaintOptions *paint_options,
-                    const GimpCoords *coords)
+                    GimpSymmetry     *sym)
 {
   GimpEraserOptions    *options  = GIMP_ERASER_OPTIONS (paint_options);
   GimpContext          *context  = GIMP_CONTEXT (paint_options);
@@ -133,10 +134,16 @@ gimp_eraser_motion (GimpPaintCore    *paint_core,
   GimpRGB               background;
   GeglColor            *color;
   gdouble               force;
+  const GimpCoords     *coords;
+  GeglNode             *op;
+  gint                  n_strokes;
+  gint                  paint_width, paint_height;
+  gint                  i;
 
   fade_point = gimp_paint_options_get_fade (paint_options, image,
                                             paint_core->pixel_dist);
 
+  coords = gimp_symmetry_get_origin (sym);
   opacity = gimp_dynamics_get_linear_value (dynamics,
                                             GIMP_DYNAMICS_OUTPUT_OPACITY,
                                             coords,
@@ -145,19 +152,9 @@ gimp_eraser_motion (GimpPaintCore    *paint_core,
   if (opacity == 0.0)
     return;
 
-  paint_buffer = gimp_paint_core_get_paint_buffer (paint_core, drawable,
-                                                   paint_options, coords,
-                                                   &paint_buffer_x,
-                                                   &paint_buffer_y);
-  if (! paint_buffer)
-    return;
-
   gimp_context_get_background (context, &background);
   color = gimp_gegl_color_new (&background);
 
-  gegl_buffer_set_color (paint_buffer, NULL, color);
-  g_object_unref (color);
-
   if (options->anti_erase)
     paint_mode = GIMP_ANTI_ERASE_MODE;
   else if (gimp_drawable_has_alpha (drawable))
@@ -165,21 +162,50 @@ gimp_eraser_motion (GimpPaintCore    *paint_core,
   else
     paint_mode = GIMP_NORMAL_MODE;
 
-  if (gimp_dynamics_is_output_enabled (dynamics, GIMP_DYNAMICS_OUTPUT_FORCE))
-    force = gimp_dynamics_get_linear_value (dynamics,
-                                            GIMP_DYNAMICS_OUTPUT_FORCE,
-                                            coords,
-                                            paint_options,
-                                            fade_point);
-  else
-    force = paint_options->brush_force;
-
-  gimp_brush_core_paste_canvas (GIMP_BRUSH_CORE (paint_core), drawable,
-                                coords,
-                                MIN (opacity, GIMP_OPACITY_OPAQUE),
-                                gimp_context_get_opacity (context),
-                                paint_mode,
-                                gimp_paint_options_get_brush_mode (paint_options),
-                                force,
-                                paint_options->application_mode);
+  gimp_brush_core_eval_transform_dynamics (GIMP_BRUSH_CORE (paint_core),
+                                           drawable,
+                                           paint_options,
+                                           coords);
+
+  n_strokes = gimp_symmetry_get_size (sym);
+  for (i = 0; i < n_strokes; i++)
+    {
+      coords = gimp_symmetry_get_coords (sym, i);
+
+      if (gimp_dynamics_is_output_enabled (dynamics, GIMP_DYNAMICS_OUTPUT_FORCE))
+        force = gimp_dynamics_get_linear_value (dynamics,
+                                                GIMP_DYNAMICS_OUTPUT_FORCE,
+                                                coords,
+                                                paint_options,
+                                                fade_point);
+      else
+        force = paint_options->brush_force;
+
+
+      paint_buffer = gimp_paint_core_get_paint_buffer (paint_core, drawable,
+                                                       paint_options, coords,
+                                                       &paint_buffer_x,
+                                                       &paint_buffer_y,
+                                                       &paint_width,
+                                                       &paint_height);
+      if (! paint_buffer)
+        continue;
+
+      op = gimp_symmetry_get_operation (sym, i,
+                                            paint_width,
+                                            paint_height);
+
+      gegl_buffer_set_color (paint_buffer, NULL, color);
+
+      gimp_brush_core_paste_canvas (GIMP_BRUSH_CORE (paint_core), drawable,
+                                    coords,
+                                    MIN (opacity, GIMP_OPACITY_OPAQUE),
+                                    gimp_context_get_opacity (context),
+                                    paint_mode,
+                                    gimp_paint_options_get_brush_mode (paint_options),
+                                    force,
+                                    paint_options->application_mode, op);
+    }
+
+  g_object_unref (color);
 }
diff --git a/app/paint/gimpheal.c b/app/paint/gimpheal.c
index 3be8200..2c06b94 100644
--- a/app/paint/gimpheal.c
+++ b/app/paint/gimpheal.c
@@ -74,6 +74,7 @@ static void         gimp_heal_motion             (GimpSourceCore   *source_core,
                                                   GimpDrawable     *drawable,
                                                   GimpPaintOptions *paint_options,
                                                   const GimpCoords *coords,
+                                                  GeglNode         *op,
                                                   gdouble           opacity,
                                                   GimpPickable     *src_pickable,
                                                   GeglBuffer       *src_buffer,
@@ -461,6 +462,7 @@ gimp_heal_motion (GimpSourceCore   *source_core,
                   GimpDrawable     *drawable,
                   GimpPaintOptions *paint_options,
                   const GimpCoords *coords,
+                  GeglNode         *op,
                   gdouble           opacity,
                   GimpPickable     *src_pickable,
                   GeglBuffer       *src_buffer,
@@ -500,7 +502,7 @@ gimp_heal_motion (GimpSourceCore   *source_core,
     force = paint_options->brush_force;
 
   mask_buf = gimp_brush_core_get_brush_mask (GIMP_BRUSH_CORE (source_core),
-                                             coords,
+                                             coords, op,
                                              GIMP_BRUSH_HARD,
                                              force);
 
@@ -549,6 +551,27 @@ gimp_heal_motion (GimpSourceCore   *source_core,
     mask_off_y = (y < 0) ? -y : 0;
   }
 
+  if (op)
+    {
+      GeglNode    *graph, *source, *target;
+
+      graph    = gegl_node_new ();
+      source   = gegl_node_new_child (graph,
+                                      "operation", "gegl:buffer-source",
+                                      "buffer", src_copy,
+                                      NULL);
+      gegl_node_add_child (graph, op);
+      target  = gegl_node_new_child (graph,
+                                     "operation", "gegl:write-buffer",
+                                     "buffer", src_copy,
+                                     NULL);
+
+      gegl_node_link_many (source, op, target, NULL);
+      gegl_node_process (target);
+
+      g_object_unref (graph);
+    }
+
   gimp_heal (src_copy,
              GEGL_RECTANGLE (0, 0,
                              gegl_buffer_get_width  (src_copy),
@@ -573,5 +596,5 @@ gimp_heal_motion (GimpSourceCore   *source_core,
                                   gimp_context_get_opacity (context),
                                   gimp_paint_options_get_brush_mode (paint_options),
                                   force,
-                                  GIMP_PAINT_INCREMENTAL);
+                                  GIMP_PAINT_INCREMENTAL, NULL);
 }
diff --git a/app/paint/gimpink.c b/app/paint/gimpink.c
index ee9cc4f..34a0941 100644
--- a/app/paint/gimpink.c
+++ b/app/paint/gimpink.c
@@ -32,6 +32,7 @@
 #include "core/gimpdrawable.h"
 #include "core/gimpimage.h"
 #include "core/gimpimage-undo.h"
+#include "core/gimpsymmetry.h"
 #include "core/gimptempbuf.h"
 
 #include "gimpinkoptions.h"
@@ -52,7 +53,7 @@ static void         gimp_ink_finalize         (GObject          *object);
 static void         gimp_ink_paint            (GimpPaintCore    *paint_core,
                                                GimpDrawable     *drawable,
                                                GimpPaintOptions *paint_options,
-                                               const GimpCoords *coords,
+                                               GimpSymmetry     *sym,
                                                GimpPaintState    paint_state,
                                                guint32           time);
 static GeglBuffer * gimp_ink_get_paint_buffer (GimpPaintCore    *paint_core,
@@ -60,7 +61,9 @@ static GeglBuffer * gimp_ink_get_paint_buffer (GimpPaintCore    *paint_core,
                                                GimpPaintOptions *paint_options,
                                                const GimpCoords *coords,
                                                gint             *paint_buffer_x,
-                                               gint             *paint_buffer_y);
+                                               gint             *paint_buffer_y,
+                                               gint             *paint_width,
+                                               gint             *paint_height);
 static GimpUndo   * gimp_ink_push_undo        (GimpPaintCore    *core,
                                                GimpImage        *image,
                                                const gchar      *undo_desc);
@@ -68,7 +71,7 @@ static GimpUndo   * gimp_ink_push_undo        (GimpPaintCore    *core,
 static void         gimp_ink_motion           (GimpPaintCore    *paint_core,
                                                GimpDrawable     *drawable,
                                                GimpPaintOptions *paint_options,
-                                               const GimpCoords *coords,
+                                               GimpSymmetry     *sym,
                                                guint32           time);
 
 static GimpBlob   * ink_pen_ellipse           (GimpInkOptions   *options,
@@ -124,16 +127,16 @@ gimp_ink_finalize (GObject *object)
 {
   GimpInk *ink = GIMP_INK (object);
 
-  if (ink->start_blob)
+  if (ink->start_blobs)
     {
-      g_free (ink->start_blob);
-      ink->start_blob = NULL;
+      g_list_free_full (ink->start_blobs, g_free);
+      ink->start_blobs = NULL;
     }
 
-  if (ink->last_blob)
+  if (ink->last_blobs)
     {
-      g_free (ink->last_blob);
-      ink->last_blob = NULL;
+      g_list_free_full (ink->last_blobs, g_free);
+      ink->last_blobs = NULL;
     }
 
   G_OBJECT_CLASS (parent_class)->finalize (object);
@@ -143,14 +146,16 @@ static void
 gimp_ink_paint (GimpPaintCore    *paint_core,
                 GimpDrawable     *drawable,
                 GimpPaintOptions *paint_options,
-                const GimpCoords *coords,
+                GimpSymmetry     *sym,
                 GimpPaintState    paint_state,
                 guint32           time)
 {
-  GimpInk *ink = GIMP_INK (paint_core);
-  GimpCoords last_coords;
+  GimpInk    *ink = GIMP_INK (paint_core);
+  GimpCoords *cur_coords;
+  GimpCoords  last_coords;
 
   gimp_paint_core_get_last_coords (paint_core, &last_coords);
+  cur_coords = gimp_symmetry_get_origin (sym);
 
   switch (paint_state)
     {
@@ -163,37 +168,48 @@ gimp_ink_paint (GimpPaintCore    *paint_core,
           gimp_palettes_add_color_history (context->gimp,
                                            &foreground);
 
-          if (coords->x == last_coords.x &&
-              coords->y == last_coords.y)
+          if (cur_coords->x == last_coords.x &&
+              cur_coords->y == last_coords.y)
             {
-              /*  start with new blobs if we're not interpolating  */
-
-              if (ink->start_blob)
+              if (ink->start_blobs)
                 {
-                  g_free (ink->start_blob);
-                  ink->start_blob = NULL;
+                  g_list_free_full (ink->start_blobs, g_free);
+                  ink->start_blobs = NULL;
                 }
 
-              if (ink->last_blob)
+              if (ink->last_blobs)
                 {
-                  g_free (ink->last_blob);
-                  ink->last_blob = NULL;
+                  g_list_free_full (ink->last_blobs, g_free);
+                  ink->last_blobs = NULL;
                 }
             }
-          else if (ink->last_blob)
+          else if (ink->last_blobs)
             {
-              /*  save the start blob of the line for undo otherwise  */
+              GimpBlob *last_blob;
+              GList    *iter;
+              gint      i;
 
-              if (ink->start_blob)
-                g_free (ink->start_blob);
+              if (ink->start_blobs)
+                {
+                  g_list_free_full (ink->start_blobs, g_free);
+                  ink->start_blobs = NULL;
+                }
 
-              ink->start_blob = gimp_blob_duplicate (ink->last_blob);
+              /*  save the start blobs of each stroke for undo otherwise  */
+              for (iter = ink->last_blobs, i = 0; iter; iter = g_list_next (iter), i++)
+                {
+                  last_blob = g_list_nth_data (ink->last_blobs, i);
+
+                  ink->start_blobs = g_list_prepend (ink->start_blobs,
+                                                     gimp_blob_duplicate (last_blob));
+                }
+              ink->start_blobs = g_list_reverse (ink->start_blobs);
             }
         }
       break;
 
     case GIMP_PAINT_STATE_MOTION:
-      gimp_ink_motion (paint_core, drawable, paint_options, coords, time);
+      gimp_ink_motion (paint_core, drawable, paint_options, sym, time);
       break;
 
     case GIMP_PAINT_STATE_FINISH:
@@ -207,7 +223,9 @@ gimp_ink_get_paint_buffer (GimpPaintCore    *paint_core,
                            GimpPaintOptions *paint_options,
                            const GimpCoords *coords,
                            gint             *paint_buffer_x,
-                           gint             *paint_buffer_y)
+                           gint             *paint_buffer_y,
+                           gint             *paint_width,
+                           gint             *paint_height)
 {
   GimpInk *ink = GIMP_INK (paint_core);
   gint     x, y;
@@ -225,6 +243,11 @@ gimp_ink_get_paint_buffer (GimpPaintCore    *paint_core,
   x2 = CLAMP ((x + width)  / SUBSAMPLE + 2, 0, dwidth);
   y2 = CLAMP ((y + height) / SUBSAMPLE + 2, 0, dheight);
 
+  if (paint_width)
+    *paint_width = width / SUBSAMPLE + 3;
+  if (paint_height)
+    *paint_height = height / SUBSAMPLE + 3;
+
   /*  configure the canvas buffer  */
   if ((x2 - x1) && (y2 - y1))
     {
@@ -271,93 +294,137 @@ static void
 gimp_ink_motion (GimpPaintCore    *paint_core,
                  GimpDrawable     *drawable,
                  GimpPaintOptions *paint_options,
-                 const GimpCoords *coords,
+                 GimpSymmetry     *sym,
                  guint32           time)
 {
-  GimpInk        *ink        = GIMP_INK (paint_core);
-  GimpInkOptions *options    = GIMP_INK_OPTIONS (paint_options);
-  GimpContext    *context    = GIMP_CONTEXT (paint_options);
-  GimpBlob       *blob_union = NULL;
-  GimpBlob       *blob_to_render;
+  GimpInk        *ink             = GIMP_INK (paint_core);
+  GimpInkOptions *options         = GIMP_INK_OPTIONS (paint_options);
+  GimpContext    *context         = GIMP_CONTEXT (paint_options);
+  GList          *blob_unions     = NULL;
+  GList          *blobs_to_render = NULL;
   GeglBuffer     *paint_buffer;
   gint            paint_buffer_x;
   gint            paint_buffer_y;
   GimpRGB         foreground;
   GeglColor      *color;
+  GimpBlob       *last_blob;
+  GimpCoords     *coords;
+  gint            n_strokes;
+  gint            i;
 
-  if (! ink->last_blob)
-    {
-      ink->last_blob = ink_pen_ellipse (options,
-                                        coords->x,
-                                        coords->y,
-                                        coords->pressure,
-                                        coords->xtilt,
-                                        coords->ytilt,
-                                        100);
+  n_strokes = gimp_symmetry_get_size (sym);
 
-      if (ink->start_blob)
-        g_free (ink->start_blob);
+  if (ink->last_blobs &&
+      g_list_length (ink->last_blobs) != n_strokes)
+    {
+      g_list_free_full (ink->last_blobs, g_free);
+      ink->last_blobs = NULL;
+    }
 
-      ink->start_blob = gimp_blob_duplicate (ink->last_blob);
+  if (! ink->last_blobs)
+    {
+      if (ink->start_blobs)
+        {
+          g_list_free_full (ink->start_blobs, g_free);
+          ink->start_blobs = NULL;
+        }
 
-      blob_to_render = ink->last_blob;
+      for (i = 0; i < n_strokes; i++)
+        {
+          coords = gimp_symmetry_get_coords (sym, i);
+
+          last_blob = ink_pen_ellipse (options,
+                                       coords->x,
+                                       coords->y,
+                                       coords->pressure,
+                                       coords->xtilt,
+                                       coords->ytilt,
+                                       100);
+
+          ink->last_blobs = g_list_prepend (ink->last_blobs,
+                                            last_blob);
+          ink->start_blobs = g_list_prepend (ink->start_blobs,
+                                             gimp_blob_duplicate (last_blob));
+          blobs_to_render = g_list_prepend (blobs_to_render, last_blob);
+        }
+      ink->start_blobs = g_list_reverse (ink->start_blobs);
+      ink->last_blobs = g_list_reverse (ink->last_blobs);
+      blobs_to_render = g_list_reverse (blobs_to_render);
     }
   else
     {
-      GimpBlob *blob = ink_pen_ellipse (options,
-                                        coords->x,
-                                        coords->y,
-                                        coords->pressure,
-                                        coords->xtilt,
-                                        coords->ytilt,
-                                        coords->velocity * 100);
-
-      blob_union = gimp_blob_convex_union (ink->last_blob, blob);
+      for (i = 0; i < n_strokes; i++)
+        {
+          GimpBlob *blob;
+          GimpBlob *blob_union = NULL;
+
+          coords = gimp_symmetry_get_coords (sym, i);
+          blob = ink_pen_ellipse (options,
+                                  coords->x,
+                                  coords->y,
+                                  coords->pressure,
+                                  coords->xtilt,
+                                  coords->ytilt,
+                                  coords->velocity * 100);
+
+          last_blob = g_list_nth_data (ink->last_blobs, i);
+          blob_union = gimp_blob_convex_union (last_blob, blob);
+
+          g_free (last_blob);
+          g_list_nth (ink->last_blobs, i)->data = blob;
+
+          blobs_to_render = g_list_prepend (blobs_to_render, blob_union);
+          blob_unions = g_list_prepend (blob_unions, blob_union);
+        }
+      blobs_to_render = g_list_reverse (blobs_to_render);
+    }
 
-      g_free (ink->last_blob);
-      ink->last_blob = blob;
+  /* Get the buffer */
+  for (i = 0; i < n_strokes; i++)
+    {
+      GimpBlob *blob_to_render = g_list_nth_data (blobs_to_render, i);
+
+      coords = gimp_symmetry_get_coords (sym, i);
+
+      ink->cur_blob = blob_to_render;
+      paint_buffer = gimp_paint_core_get_paint_buffer (paint_core, drawable,
+                                                       paint_options, coords,
+                                                       &paint_buffer_x,
+                                                       &paint_buffer_y,
+                                                       NULL, NULL);
+      ink->cur_blob = NULL;
+
+      if (! paint_buffer)
+        continue;
+
+      gimp_context_get_foreground (context, &foreground);
+      color = gimp_gegl_color_new (&foreground);
+
+      gegl_buffer_set_color (paint_buffer, NULL, color);
+      g_object_unref (color);
+
+      /*  draw the blob directly to the canvas_buffer  */
+      render_blob (paint_core->canvas_buffer,
+                   GEGL_RECTANGLE (paint_core->paint_buffer_x,
+                                   paint_core->paint_buffer_y,
+                                   gegl_buffer_get_width  (paint_core->paint_buffer),
+                                   gegl_buffer_get_height (paint_core->paint_buffer)),
+                   blob_to_render);
+
+      /*  draw the paint_area using the just rendered canvas_buffer as mask */
+      gimp_paint_core_paste (paint_core,
+                             NULL,
+                             paint_core->paint_buffer_x,
+                             paint_core->paint_buffer_y,
+                             drawable,
+                             GIMP_OPACITY_OPAQUE,
+                             gimp_context_get_opacity (context),
+                             gimp_context_get_paint_mode (context),
+                             GIMP_PAINT_CONSTANT);
 
-      blob_to_render = blob_union;
     }
 
-  /* Get the buffer */
-  ink->cur_blob = blob_to_render;
-  paint_buffer = gimp_paint_core_get_paint_buffer (paint_core, drawable,
-                                                   paint_options, coords,
-                                                   &paint_buffer_x,
-                                                   &paint_buffer_y);
-  ink->cur_blob = NULL;
-
-  if (! paint_buffer)
-    return;
-
-  gimp_context_get_foreground (context, &foreground);
-  color = gimp_gegl_color_new (&foreground);
-
-  gegl_buffer_set_color (paint_buffer, NULL, color);
-  g_object_unref (color);
-
-  /*  draw the blob directly to the canvas_buffer  */
-  render_blob (paint_core->canvas_buffer,
-               GEGL_RECTANGLE (paint_core->paint_buffer_x,
-                               paint_core->paint_buffer_y,
-                               gegl_buffer_get_width  (paint_core->paint_buffer),
-                               gegl_buffer_get_height (paint_core->paint_buffer)),
-               blob_to_render);
-
-  /*  draw the paint_area using the just rendered canvas_buffer as mask */
-  gimp_paint_core_paste (paint_core,
-                         NULL,
-                         paint_core->paint_buffer_x,
-                         paint_core->paint_buffer_y,
-                         drawable,
-                         GIMP_OPACITY_OPAQUE,
-                         gimp_context_get_opacity (context),
-                         gimp_context_get_paint_mode (context),
-                         GIMP_PAINT_CONSTANT);
-
-  if (blob_union)
-    g_free (blob_union);
+  g_list_free_full (blob_unions, g_free);
 }
 
 static GimpBlob *
diff --git a/app/paint/gimpink.h b/app/paint/gimpink.h
index 6cb185b..6c30f56 100644
--- a/app/paint/gimpink.h
+++ b/app/paint/gimpink.h
@@ -37,10 +37,10 @@ struct _GimpInk
 {
   GimpPaintCore  parent_instance;
 
-  GimpBlob      *start_blob;   /*  starting blob (for undo)       */
+  GList         *start_blobs;  /*  starting blobs per stroke (for undo) */
 
-  GimpBlob      *cur_blob;     /*  current blob                   */
-  GimpBlob      *last_blob;    /*  blob for last cursor position  */
+  GimpBlob      *cur_blob;     /*  current blob                         */
+  GList         *last_blobs;   /*  blobs for last stroke positions      */
 };
 
 struct _GimpInkClass
diff --git a/app/paint/gimpinkundo.c b/app/paint/gimpinkundo.c
index aede58f..b3aae31 100644
--- a/app/paint/gimpinkundo.c
+++ b/app/paint/gimpinkundo.c
@@ -58,6 +58,7 @@ gimp_ink_undo_class_init (GimpInkUndoClass *klass)
 static void
 gimp_ink_undo_init (GimpInkUndo *undo)
 {
+  undo->last_blobs = NULL;
 }
 
 static void
@@ -72,8 +73,20 @@ gimp_ink_undo_constructed (GObject *object)
 
   ink = GIMP_INK (GIMP_PAINT_CORE_UNDO (ink_undo)->paint_core);
 
-  if (ink->start_blob)
-    ink_undo->last_blob = gimp_blob_duplicate (ink->start_blob);
+  if (ink->start_blobs)
+    {
+      gint      i;
+      GimpBlob *blob;
+
+      for (i = 0; i < g_list_length (ink->start_blobs); i++)
+        {
+          blob = g_list_nth_data (ink->start_blobs, i);
+
+          ink_undo->last_blobs = g_list_prepend (ink_undo->last_blobs,
+                                                 gimp_blob_duplicate (blob));
+        }
+      ink_undo->last_blobs = g_list_reverse (ink_undo->last_blobs);
+    }
 }
 
 static void
@@ -88,12 +101,11 @@ gimp_ink_undo_pop (GimpUndo              *undo,
   if (GIMP_PAINT_CORE_UNDO (ink_undo)->paint_core)
     {
       GimpInk  *ink = GIMP_INK (GIMP_PAINT_CORE_UNDO (ink_undo)->paint_core);
-      GimpBlob *tmp_blob;
-
-      tmp_blob = ink->last_blob;
-      ink->last_blob = ink_undo->last_blob;
-      ink_undo->last_blob = tmp_blob;
+      GList    *tmp_blobs;
 
+      tmp_blobs = ink->last_blobs;
+      ink->last_blobs = ink_undo->last_blobs;
+      ink_undo->last_blobs = tmp_blobs;
     }
 }
 
@@ -103,10 +115,10 @@ gimp_ink_undo_free (GimpUndo     *undo,
 {
   GimpInkUndo *ink_undo = GIMP_INK_UNDO (undo);
 
-  if (ink_undo->last_blob)
+  if (ink_undo->last_blobs)
     {
-      g_free (ink_undo->last_blob);
-      ink_undo->last_blob = NULL;
+      g_list_free_full (ink_undo->last_blobs, g_free);
+      ink_undo->last_blobs = NULL;
     }
 
   GIMP_UNDO_CLASS (parent_class)->free (undo, undo_mode);
diff --git a/app/paint/gimpinkundo.h b/app/paint/gimpinkundo.h
index fbeecbf..e3a29cf 100644
--- a/app/paint/gimpinkundo.h
+++ b/app/paint/gimpinkundo.h
@@ -36,7 +36,7 @@ struct _GimpInkUndo
 {
   GimpPaintCoreUndo  parent_instance;
 
-  GimpBlob         *last_blob;
+  GList             *last_blobs;
 };
 
 struct _GimpInkUndoClass
diff --git a/app/paint/gimpmybrush.c b/app/paint/gimpmybrush.c
new file mode 100644
index 0000000..69aa5d9
--- /dev/null
+++ b/app/paint/gimpmybrush.c
@@ -0,0 +1,267 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#ifdef HAVE_LIBMYPAINT
+
+#include <string.h>
+
+#include <cairo.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <gegl.h>
+
+#include <mypaint-brush.h>
+#include <mypaint-tiled-surface.h>
+#include <mypaint-gegl-surface.h>
+
+#include "libgimpmath/gimpmath.h"
+#include "libgimpcolor/gimpcolor.h"
+
+#include "paint-types.h"
+
+#include "gegl/gimp-gegl-utils.h"
+
+#include "config/gimpguiconfig.h" /* playground */
+
+#include "core/gimp.h"
+#include "core/gimp-palettes.h"
+#include "core/gimpdrawable.h"
+#include "core/gimpimage.h"
+#include "core/gimpimage-undo.h"
+#include "core/gimptempbuf.h"
+
+#include "gimpmybrushoptions.h"
+#include "gimpmybrush.h"
+
+#include "gimp-intl.h"
+
+
+struct _GimpMybrushPrivate
+{
+  MyPaintGeglTiledSurface *surface;
+  MyPaintBrush            *brush;
+};
+
+
+/*  local function prototypes  */
+
+static void   gimp_mybrush_paint  (GimpPaintCore    *paint_core,
+                                   GimpDrawable     *drawable,
+                                   GimpPaintOptions *paint_options,
+                                   const GimpCoords *coords,
+                                   GimpPaintState    paint_state,
+                                   guint32           time);
+static void   gimp_mybrush_motion (GimpPaintCore    *paint_core,
+                                   GimpDrawable     *drawable,
+                                   GimpPaintOptions *paint_options,
+                                   const GimpCoords *coords,
+                                   guint32           time);
+
+
+G_DEFINE_TYPE (GimpMybrush, gimp_mybrush, GIMP_TYPE_PAINT_CORE)
+
+#define parent_class gimp_mybrush_parent_class
+
+
+void
+gimp_mybrush_register (Gimp                      *gimp,
+                       GimpPaintRegisterCallback  callback)
+{
+  if (GIMP_GUI_CONFIG (gimp->config)->playground_mybrush_tool)
+    (* callback) (gimp,
+                  GIMP_TYPE_MYBRUSH,
+                  GIMP_TYPE_MYBRUSH_OPTIONS,
+                  "gimp-mybrush",
+                  _("Mybrush"),
+                  "gimp-tool-mybrush");
+}
+
+static void
+gimp_mybrush_class_init (GimpMybrushClass *klass)
+{
+  GimpPaintCoreClass *paint_core_class = GIMP_PAINT_CORE_CLASS (klass);
+
+  paint_core_class->paint = gimp_mybrush_paint;
+
+  g_type_class_add_private (klass, sizeof (GimpMybrushPrivate));
+}
+
+static void
+gimp_mybrush_init (GimpMybrush *mybrush)
+{
+  mybrush->private = G_TYPE_INSTANCE_GET_PRIVATE (mybrush,
+                                                  GIMP_TYPE_MYBRUSH,
+                                                  GimpMybrushPrivate);
+}
+
+static void
+gimp_mybrush_paint (GimpPaintCore    *paint_core,
+                    GimpDrawable     *drawable,
+                    GimpPaintOptions *paint_options,
+                    const GimpCoords *coords,
+                    GimpPaintState    paint_state,
+                    guint32           time)
+{
+  GimpMybrush        *mybrush = GIMP_MYBRUSH (paint_core);
+  GimpMybrushOptions *options = GIMP_MYBRUSH_OPTIONS (paint_options);
+  GeglBuffer         *buffer;
+
+  switch (paint_state)
+    {
+    case GIMP_PAINT_STATE_INIT:
+        {
+          GimpContext *context = GIMP_CONTEXT (paint_options);
+          GimpRGB      foreground;
+
+          gimp_context_get_foreground (context, &foreground);
+          gimp_palettes_add_color_history (context->gimp,
+                                           &foreground);
+
+          mybrush->private->surface = mypaint_gegl_tiled_surface_new ();
+
+          buffer = mypaint_gegl_tiled_surface_get_buffer (mybrush->private->surface);
+          buffer = gegl_buffer_new (GEGL_RECTANGLE (0, 0,
+                                                    gimp_item_get_width (GIMP_ITEM (drawable)),
+                                                    gimp_item_get_height (GIMP_ITEM (drawable))),
+                                    gegl_buffer_get_format (buffer));
+          gegl_buffer_copy (gimp_drawable_get_buffer (drawable), NULL,
+                            GEGL_ABYSS_NONE,
+                            buffer, NULL);
+          mypaint_gegl_tiled_surface_set_buffer (mybrush->private->surface, buffer);
+          g_object_unref (buffer);
+
+          mybrush->private->brush = mypaint_brush_new ();
+          mypaint_brush_from_defaults (mybrush->private->brush);
+
+          if (options->mybrush)
+            {
+              gchar *string;
+              gsize  length;
+
+              if (g_file_get_contents (options->mybrush,
+                                       &string, &length, NULL))
+                {
+                  if (! mypaint_brush_from_string (mybrush->private->brush, string))
+                    g_printerr ("Failed to deserialize MyPaint brush\n");
+
+                  g_free (string);
+                }
+            }
+
+          mypaint_brush_new_stroke (mybrush->private->brush);
+        }
+      break;
+
+    case GIMP_PAINT_STATE_MOTION:
+      gimp_mybrush_motion (paint_core, drawable, paint_options, coords, time);
+      break;
+
+    case GIMP_PAINT_STATE_FINISH:
+      mypaint_surface_unref ((MyPaintSurface *) mybrush->private->surface);
+      mybrush->private->surface = NULL;
+
+      mypaint_brush_unref (mybrush->private->brush);
+      mybrush->private->brush = NULL;
+      break;
+    }
+}
+
+static void
+gimp_mybrush_motion (GimpPaintCore    *paint_core,
+                     GimpDrawable     *drawable,
+                     GimpPaintOptions *paint_options,
+                     const GimpCoords *coords,
+                     guint32           time)
+{
+  GimpMybrush        *mybrush = GIMP_MYBRUSH (paint_core);
+  GimpMybrushOptions *options = GIMP_MYBRUSH_OPTIONS (paint_options);
+  GimpContext        *context = GIMP_CONTEXT (paint_options);
+  GimpComponentMask   active_mask;
+  GimpRGB             fg;
+  GimpHSV             hsv;
+  MyPaintRectangle    rect;
+
+  active_mask = gimp_drawable_get_active_mask (drawable);
+
+  mypaint_brush_set_base_value (mybrush->private->brush,
+                                MYPAINT_BRUSH_SETTING_LOCK_ALPHA,
+                                (active_mask & GIMP_COMPONENT_MASK_ALPHA) ?
+                                FALSE : TRUE);
+
+  gimp_context_get_foreground (context, &fg);
+  gimp_rgb_to_hsv (&fg, &hsv);
+
+  mypaint_brush_set_base_value (mybrush->private->brush,
+                                MYPAINT_BRUSH_SETTING_COLOR_H,
+                                hsv.h);
+  mypaint_brush_set_base_value (mybrush->private->brush,
+                                MYPAINT_BRUSH_SETTING_COLOR_S,
+                                hsv.s);
+  mypaint_brush_set_base_value (mybrush->private->brush,
+                                MYPAINT_BRUSH_SETTING_COLOR_V,
+                                hsv.v);
+
+  mypaint_brush_set_base_value (mybrush->private->brush,
+                                MYPAINT_BRUSH_SETTING_OPAQUE,
+                                gimp_context_get_opacity (context));
+  mypaint_brush_set_base_value (mybrush->private->brush,
+                                MYPAINT_BRUSH_SETTING_RADIUS_LOGARITHMIC,
+                                options->radius);
+  mypaint_brush_set_base_value (mybrush->private->brush,
+                                MYPAINT_BRUSH_SETTING_HARDNESS,
+                                options->hardness);
+
+  mypaint_surface_begin_atomic ((MyPaintSurface *) mybrush->private->surface);
+
+  mypaint_brush_stroke_to (mybrush->private->brush,
+                           (MyPaintSurface *) mybrush->private->surface,
+                           coords->x,
+                           coords->y,
+                           coords->pressure,
+                           coords->xtilt,
+                           coords->ytilt,
+                           1);
+
+  mypaint_surface_end_atomic ((MyPaintSurface *) mybrush->private->surface,
+                              &rect);
+
+  g_printerr ("painted rect: %d %d %d %d\n",
+              rect.x, rect.y, rect.width, rect.height);
+
+  if (rect.width > 0 && rect.height > 0)
+    {
+      GeglBuffer *src;
+
+      src = mypaint_gegl_tiled_surface_get_buffer (mybrush->private->surface);
+
+      gegl_buffer_copy (src,
+                        (GeglRectangle *) &rect,
+                        GEGL_ABYSS_NONE,
+                        gimp_drawable_get_buffer (drawable),
+                        NULL);
+
+      paint_core->x1 = MIN (paint_core->x1, rect.x);
+      paint_core->y1 = MIN (paint_core->y1, rect.y);
+      paint_core->x2 = MAX (paint_core->x2, rect.x + rect.width);
+      paint_core->y2 = MAX (paint_core->y2, rect.y + rect.height);
+
+      gimp_drawable_update (drawable, rect.x, rect.y, rect.width, rect.height);
+    }
+}
+
+#endif
diff --git a/app/paint/gimpmybrushcore.c b/app/paint/gimpmybrushcore.c
index 8eb5964..45803ae 100644
--- a/app/paint/gimpmybrushcore.c
+++ b/app/paint/gimpmybrushcore.c
@@ -37,6 +37,7 @@
 #include "core/gimpdrawable.h"
 #include "core/gimperror.h"
 #include "core/gimpmybrush.h"
+#include "core/gimpsymmetry.h"
 
 #include "gimpmybrushcore.h"
 #include "gimpmybrushsurface.h"
@@ -49,7 +50,7 @@ struct _GimpMybrushCorePrivate
 {
   GimpMybrush             *mybrush;
   GimpMybrushSurface      *surface;
-  MyPaintBrush            *brush;
+  GList                   *brushes;
   gboolean                 synthetic;
   gint64                   last_time;
 };
@@ -57,6 +58,8 @@ struct _GimpMybrushCorePrivate
 
 /*  local function prototypes  */
 
+static void      gimp_mybrush_core_finalize    (GObject          *object);
+
 static gboolean  gimp_mybrush_core_start       (GimpPaintCore     *paint_core,
                                                 GimpDrawable      *drawable,
                                                 GimpPaintOptions  *paint_options,
@@ -69,13 +72,13 @@ static void      gimp_mybrush_core_interpolate (GimpPaintCore    *paint_core,
 static void      gimp_mybrush_core_paint       (GimpPaintCore     *paint_core,
                                                 GimpDrawable      *drawable,
                                                 GimpPaintOptions  *paint_options,
-                                                const GimpCoords  *coords,
+                                                GimpSymmetry      *sym,
                                                 GimpPaintState     paint_state,
                                                 guint32            time);
 static void      gimp_mybrush_core_motion      (GimpPaintCore     *paint_core,
                                                 GimpDrawable      *drawable,
                                                 GimpPaintOptions  *paint_options,
-                                                const GimpCoords  *coords,
+                                                GimpSymmetry      *sym,
                                                 guint32            time);
 
 
@@ -99,8 +102,11 @@ gimp_mybrush_core_register (Gimp                      *gimp,
 static void
 gimp_mybrush_core_class_init (GimpMybrushCoreClass *klass)
 {
+  GObjectClass       *object_class     = G_OBJECT_CLASS (klass);
   GimpPaintCoreClass *paint_core_class = GIMP_PAINT_CORE_CLASS (klass);
 
+  object_class->finalize        = gimp_mybrush_core_finalize;
+
   paint_core_class->start       = gimp_mybrush_core_start;
   paint_core_class->paint       = gimp_mybrush_core_paint;
   paint_core_class->interpolate = gimp_mybrush_core_interpolate;
@@ -116,6 +122,21 @@ gimp_mybrush_core_init (GimpMybrushCore *mybrush)
                                                   GimpMybrushCorePrivate);
 }
 
+static void
+gimp_mybrush_core_finalize (GObject *object)
+{
+  GimpMybrushCore *core = GIMP_MYBRUSH_CORE (object);
+
+  if (core->private->brushes)
+    {
+      g_list_free_full (core->private->brushes,
+                        (GDestroyNotify) mypaint_brush_unref);
+      core->private->brushes = NULL;
+    }
+
+  G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
 static gboolean
 gimp_mybrush_core_start (GimpPaintCore     *paint_core,
                          GimpDrawable      *drawable,
@@ -172,7 +193,7 @@ static void
 gimp_mybrush_core_paint (GimpPaintCore    *paint_core,
                          GimpDrawable     *drawable,
                          GimpPaintOptions *paint_options,
-                         const GimpCoords *coords,
+                         GimpSymmetry     *sym,
                          GimpPaintState    paint_state,
                          guint32           time)
 {
@@ -182,6 +203,8 @@ gimp_mybrush_core_paint (GimpPaintCore    *paint_core,
   const gchar        *brush_data;
   GimpRGB             fg;
   GimpHSV             hsv;
+  gint                n_strokes;
+  gint                i;
 
   switch (paint_state)
     {
@@ -192,53 +215,69 @@ gimp_mybrush_core_paint (GimpPaintCore    *paint_core,
       mybrush->private->surface = gimp_mypaint_surface_new (gimp_drawable_get_buffer (drawable),
                                                             gimp_drawable_get_active_mask (drawable));
 
-      mybrush->private->brush = mypaint_brush_new ();
-      mypaint_brush_from_defaults (mybrush->private->brush);
-      brush_data = gimp_mybrush_get_brush_json (mybrush->private->mybrush);
-      if (brush_data)
-        mypaint_brush_from_string (mybrush->private->brush, brush_data);
-
       gimp_rgb_to_hsv (&fg, &hsv);
 
-      mypaint_brush_set_base_value (mybrush->private->brush,
-                                    MYPAINT_BRUSH_SETTING_COLOR_H,
-                                    hsv.h);
-      mypaint_brush_set_base_value (mybrush->private->brush,
-                                    MYPAINT_BRUSH_SETTING_COLOR_S,
-                                    hsv.s);
-      mypaint_brush_set_base_value (mybrush->private->brush,
-                                    MYPAINT_BRUSH_SETTING_COLOR_V,
-                                    hsv.v);
-
-      mypaint_brush_set_base_value (mybrush->private->brush,
-                                    MYPAINT_BRUSH_SETTING_RADIUS_LOGARITHMIC,
-                                    options->radius);
-      mypaint_brush_set_base_value (mybrush->private->brush,
-                                    MYPAINT_BRUSH_SETTING_OPAQUE,
-                                    options->opaque * gimp_context_get_opacity (context));
-      mypaint_brush_set_base_value (mybrush->private->brush,
-                                    MYPAINT_BRUSH_SETTING_HARDNESS,
-                                    options->hardness);
-      mypaint_brush_set_base_value (mybrush->private->brush,
-                                    MYPAINT_BRUSH_SETTING_ERASER,
-                                    options->eraser ? 1.0f : 0.0f);
-
-      mypaint_brush_new_stroke (mybrush->private->brush);
+      if (mybrush->private->brushes)
+        {
+          g_list_free_full (mybrush->private->brushes,
+                            (GDestroyNotify) mypaint_brush_unref);
+          mybrush->private->brushes = NULL;
+        }
+
+      n_strokes = gimp_symmetry_get_size (sym);
+      for (i = 0; i < n_strokes; i++)
+        {
+          MyPaintBrush *brush = mypaint_brush_new ();
+
+          mypaint_brush_from_defaults (brush);
+          brush_data = gimp_mybrush_get_brush_json (mybrush->private->mybrush);
+          if (brush_data)
+            mypaint_brush_from_string (brush, brush_data);
+
+          mypaint_brush_set_base_value (brush,
+                                        MYPAINT_BRUSH_SETTING_COLOR_H,
+                                        hsv.h);
+          mypaint_brush_set_base_value (brush,
+                                        MYPAINT_BRUSH_SETTING_COLOR_S,
+                                        hsv.s);
+          mypaint_brush_set_base_value (brush,
+                                        MYPAINT_BRUSH_SETTING_COLOR_V,
+                                        hsv.v);
+
+          mypaint_brush_set_base_value (brush,
+                                        MYPAINT_BRUSH_SETTING_RADIUS_LOGARITHMIC,
+                                        options->radius);
+          mypaint_brush_set_base_value (brush,
+                                        MYPAINT_BRUSH_SETTING_OPAQUE,
+                                        options->opaque * gimp_context_get_opacity (context));
+          mypaint_brush_set_base_value (brush,
+                                        MYPAINT_BRUSH_SETTING_HARDNESS,
+                                        options->hardness);
+          mypaint_brush_set_base_value (brush,
+                                        MYPAINT_BRUSH_SETTING_ERASER,
+                                        options->eraser ? 1.0f : 0.0f);
+
+          mypaint_brush_new_stroke (brush);
+
+          mybrush->private->brushes = g_list_prepend (mybrush->private->brushes, brush);
+        }
+      mybrush->private->brushes = g_list_reverse (mybrush->private->brushes);
       mybrush->private->last_time = -1;
       mybrush->private->synthetic = FALSE;
       break;
 
     case GIMP_PAINT_STATE_MOTION:
       gimp_mybrush_core_motion (paint_core, drawable, paint_options,
-                                coords, time);
+                                sym, time);
       break;
 
     case GIMP_PAINT_STATE_FINISH:
       mypaint_surface_unref ((MyPaintSurface *) mybrush->private->surface);
       mybrush->private->surface = NULL;
 
-      mypaint_brush_unref (mybrush->private->brush);
-      mybrush->private->brush = NULL;
+      g_list_free_full (mybrush->private->brushes,
+                        (GDestroyNotify) mypaint_brush_unref);
+      mybrush->private->brushes = NULL;
       break;
     }
 }
@@ -247,27 +286,93 @@ static void
 gimp_mybrush_core_motion (GimpPaintCore    *paint_core,
                           GimpDrawable     *drawable,
                           GimpPaintOptions *paint_options,
-                          const GimpCoords *coords,
+                          GimpSymmetry     *sym,
                           guint32           time)
 {
   GimpMybrushCore  *mybrush = GIMP_MYBRUSH_CORE (paint_core);
+  GimpContext      *context = GIMP_CONTEXT (paint_options);
+  MyPaintBrush     *brush;
+  GimpCoords       *coords;
   MyPaintRectangle  rect;
   gdouble           pressure;
   gdouble           dt = 0.0;
+  gint              n_strokes;
+  gint              i;
+  GList            *iter;
+
+  n_strokes = gimp_symmetry_get_size (sym);
+
+  /* Number of strokes may change during a motion, depending on the type
+   * of symmetry. When that happens, we reset the brushes. */
+  if (g_list_length (mybrush->private->brushes) != n_strokes)
+    {
+      const gchar        *brush_data;
+      GimpMybrushOptions *options = GIMP_MYBRUSH_OPTIONS (paint_options);
+      GimpRGB             fg;
+      GimpHSV             hsv;
+
+      gimp_context_get_foreground (context, &fg);
+      gimp_rgb_to_hsv (&fg, &hsv);
+
+      g_list_free_full (mybrush->private->brushes,
+                        (GDestroyNotify) mypaint_brush_unref);
+      mybrush->private->brushes = NULL;
+      for (i = 0; i < n_strokes; i++)
+        {
+          brush = mypaint_brush_new ();
+
+          mypaint_brush_from_defaults (brush);
+          brush_data = gimp_mybrush_get_brush_json (mybrush->private->mybrush);
+          if (brush_data)
+            mypaint_brush_from_string (brush, brush_data);
+
+          mypaint_brush_set_base_value (brush,
+                                        MYPAINT_BRUSH_SETTING_COLOR_H,
+                                        hsv.h);
+          mypaint_brush_set_base_value (brush,
+                                        MYPAINT_BRUSH_SETTING_COLOR_S,
+                                        hsv.s);
+          mypaint_brush_set_base_value (brush,
+                                        MYPAINT_BRUSH_SETTING_COLOR_V,
+                                        hsv.v);
+
+          mypaint_brush_set_base_value (brush,
+                                        MYPAINT_BRUSH_SETTING_RADIUS_LOGARITHMIC,
+                                        options->radius);
+          mypaint_brush_set_base_value (brush,
+                                        MYPAINT_BRUSH_SETTING_OPAQUE,
+                                        options->opaque * gimp_context_get_opacity (context));
+          mypaint_brush_set_base_value (brush,
+                                        MYPAINT_BRUSH_SETTING_HARDNESS,
+                                        options->hardness);
+          mypaint_brush_set_base_value (brush,
+                                        MYPAINT_BRUSH_SETTING_ERASER,
+                                        options->eraser ? 1.0f : 0.0f);
+
+          mypaint_brush_new_stroke (brush);
+          mybrush->private->brushes = g_list_prepend (mybrush->private->brushes, brush);
+        }
+      mybrush->private->brushes = g_list_reverse (mybrush->private->brushes);
+    }
 
   mypaint_surface_begin_atomic ((MyPaintSurface *) mybrush->private->surface);
 
   if (mybrush->private->last_time < 0)
     {
-      /* First motion, so we need a zero pressure event to start the stroke */
-      mypaint_brush_stroke_to (mybrush->private->brush,
-                               (MyPaintSurface *) mybrush->private->surface,
-                               coords->x,
-                               coords->y,
-                               0.0f,
-                               coords->xtilt,
-                               coords->ytilt,
-                               1.0f /* Pretend the cursor hasn't moved in a while */);
+      /* First motion, so we need zero pressure events to start the strokes */
+      for (iter = mybrush->private->brushes, i = 0; iter ; iter = g_list_next (iter), i++)
+        {
+          brush  = iter->data;
+          coords = gimp_symmetry_get_coords (sym, i);
+          mypaint_brush_stroke_to (brush,
+                                   (MyPaintSurface *) mybrush->private->surface,
+                                   coords->x,
+                                   coords->y,
+                                   0.0f,
+                                   coords->xtilt,
+                                   coords->ytilt,
+                                   1.0f /* Pretend the cursor hasn't moved in a while */);
+        }
       dt = 0.015;
     }
   else if (mybrush->private->synthetic)
@@ -280,20 +385,25 @@ gimp_mybrush_core_motion (GimpPaintCore    *paint_core,
       dt = (time - mybrush->private->last_time) * 0.001;
     }
 
-  pressure = coords->pressure;
+  for (iter = mybrush->private->brushes, i = 0; iter ; iter = g_list_next (iter), i++)
+    {
+      brush  = iter->data;
+      coords = gimp_symmetry_get_coords (sym, i);
+      pressure = coords->pressure;
 
-  /* libmypaint expects non-extended devices to default to 0.5 pressure */
-  if (! coords->extended)
-    pressure = 0.5f;
+      /* libmypaint expects non-extended devices to default to 0.5 pressure */
+      if (! coords->extended)
+        pressure = 0.5f;
 
-  mypaint_brush_stroke_to (mybrush->private->brush,
-                           (MyPaintSurface *) mybrush->private->surface,
-                           coords->x,
-                           coords->y,
-                           pressure,
-                           coords->xtilt,
-                           coords->ytilt,
-                           dt);
+      mypaint_brush_stroke_to (brush,
+                               (MyPaintSurface *) mybrush->private->surface,
+                               coords->x,
+                               coords->y,
+                               pressure,
+                               coords->xtilt,
+                               coords->ytilt,
+                               dt);
+    }
 
   mybrush->private->last_time = time;
 
diff --git a/app/paint/gimppaintbrush.c b/app/paint/gimppaintbrush.c
index a41c278..f767657 100644
--- a/app/paint/gimppaintbrush.c
+++ b/app/paint/gimppaintbrush.c
@@ -36,6 +36,7 @@
 #include "core/gimpdynamics.h"
 #include "core/gimpgradient.h"
 #include "core/gimpimage.h"
+#include "core/gimpsymmetry.h"
 #include "core/gimptempbuf.h"
 
 #include "gimppaintbrush.h"
@@ -47,7 +48,7 @@
 static void   gimp_paintbrush_paint (GimpPaintCore    *paint_core,
                                      GimpDrawable     *drawable,
                                      GimpPaintOptions *paint_options,
-                                     const GimpCoords *coords,
+                                     GimpSymmetry     *sym,
                                      GimpPaintState    paint_state,
                                      guint32           time);
 
@@ -87,7 +88,7 @@ static void
 gimp_paintbrush_paint (GimpPaintCore    *paint_core,
                        GimpDrawable     *drawable,
                        GimpPaintOptions *paint_options,
-                       const GimpCoords *coords,
+                       GimpSymmetry     *sym,
                        GimpPaintState    paint_state,
                        guint32           time)
 {
@@ -116,8 +117,8 @@ gimp_paintbrush_paint (GimpPaintCore    *paint_core,
         }
       break;
     case GIMP_PAINT_STATE_MOTION:
-      _gimp_paintbrush_motion (paint_core, drawable, paint_options, coords,
-                               GIMP_OPACITY_OPAQUE);
+      _gimp_paintbrush_motion (paint_core, drawable, paint_options,
+                               sym, GIMP_OPACITY_OPAQUE);
       break;
 
     default:
@@ -129,7 +130,7 @@ void
 _gimp_paintbrush_motion (GimpPaintCore    *paint_core,
                          GimpDrawable     *drawable,
                          GimpPaintOptions *paint_options,
-                         const GimpCoords *coords,
+                         GimpSymmetry     *sym,
                          gdouble           opacity)
 {
   GimpBrushCore            *brush_core = GIMP_BRUSH_CORE (paint_core);
@@ -144,12 +145,18 @@ _gimp_paintbrush_motion (GimpPaintCore    *paint_core,
   gdouble                   fade_point;
   gdouble                   grad_point;
   gdouble                   force;
+  const GimpCoords         *coords;
+  GeglNode                 *op;
+  gint                      n_strokes;
+  gint                      i;
 
   image = gimp_item_get_image (GIMP_ITEM (drawable));
 
   fade_point = gimp_paint_options_get_fade (paint_options, image,
                                             paint_core->pixel_dist);
 
+  coords = gimp_symmetry_get_origin (sym);
+  /* Some settings are based on the original stroke. */
   opacity *= gimp_dynamics_get_linear_value (dynamics,
                                              GIMP_DYNAMICS_OUTPUT_OPACITY,
                                              coords,
@@ -158,13 +165,6 @@ _gimp_paintbrush_motion (GimpPaintCore    *paint_core,
   if (opacity == 0.0)
     return;
 
-  paint_buffer = gimp_paint_core_get_paint_buffer (paint_core, drawable,
-                                                   paint_options, coords,
-                                                   &paint_buffer_x,
-                                                   &paint_buffer_y);
-  if (! paint_buffer)
-    return;
-
   paint_appl_mode = paint_options->application_mode;
 
   grad_point = gimp_dynamics_get_linear_value (dynamics,
@@ -173,69 +173,98 @@ _gimp_paintbrush_motion (GimpPaintCore    *paint_core,
                                                paint_options,
                                                fade_point);
 
-  if (gimp_paint_options_get_gradient_color (paint_options, image,
-                                             grad_point,
-                                             paint_core->pixel_dist,
-                                             &gradient_color))
+
+  if (GIMP_BRUSH_CORE_GET_CLASS (brush_core)->handles_transforming_brush)
     {
-      /* optionally take the color from the current gradient */
+      gimp_brush_core_eval_transform_dynamics (brush_core,
+                                               drawable,
+                                               paint_options,
+                                               coords);
+    }
 
-      GeglColor *color;
+  n_strokes = gimp_symmetry_get_size (sym);
+  for (i = 0; i < n_strokes; i++)
+    {
+      gint paint_width, paint_height;
+
+      coords = gimp_symmetry_get_coords (sym, i);
+
+      paint_buffer = gimp_paint_core_get_paint_buffer (paint_core, drawable,
+                                                       paint_options, coords,
+                                                       &paint_buffer_x,
+                                                       &paint_buffer_y,
+                                                       &paint_width,
+                                                       &paint_height);
+      if (! paint_buffer)
+        continue;
+
+      op = gimp_symmetry_get_operation (sym, i,
+                                        paint_width,
+                                        paint_height);
+      if (gimp_paint_options_get_gradient_color (paint_options, image,
+                                                 grad_point,
+                                                 paint_core->pixel_dist,
+                                                 &gradient_color))
+        {
+          /* optionally take the color from the current gradient */
 
-      opacity *= gradient_color.a;
-      gimp_rgb_set_alpha (&gradient_color, GIMP_OPACITY_OPAQUE);
+          GeglColor *color;
 
-      color = gimp_gegl_color_new (&gradient_color);
+          opacity *= gradient_color.a;
+          gimp_rgb_set_alpha (&gradient_color, GIMP_OPACITY_OPAQUE);
 
-      gegl_buffer_set_color (paint_buffer, NULL, color);
-      g_object_unref (color);
+          color = gimp_gegl_color_new (&gradient_color);
 
-      paint_appl_mode = GIMP_PAINT_INCREMENTAL;
-    }
-  else if (brush_core->brush && gimp_brush_get_pixmap (brush_core->brush))
-    {
-      /* otherwise check if the brush has a pixmap and use that to
-       * color the area
-       */
-      gimp_brush_core_color_area_with_pixmap (brush_core, drawable,
-                                              coords,
-                                              paint_buffer,
-                                              paint_buffer_x,
-                                              paint_buffer_y,
-                                              gimp_paint_options_get_brush_mode (paint_options));
-
-      paint_appl_mode = GIMP_PAINT_INCREMENTAL;
-    }
-  else
-    {
-      /* otherwise fill the area with the foreground color */
+          gegl_buffer_set_color (paint_buffer, NULL, color);
+          g_object_unref (color);
 
-      GimpRGB    foreground;
-      GeglColor *color;
+          paint_appl_mode = GIMP_PAINT_INCREMENTAL;
+        }
+      else if (brush_core->brush && gimp_brush_get_pixmap (brush_core->brush))
+        {
+          /* otherwise check if the brush has a pixmap and use that to
+           * color the area
+           */
+          gimp_brush_core_color_area_with_pixmap (brush_core, drawable,
+                                                  coords, op,
+                                                  paint_buffer,
+                                                  paint_buffer_x,
+                                                  paint_buffer_y,
+                                                  gimp_paint_options_get_brush_mode (paint_options));
+
+          paint_appl_mode = GIMP_PAINT_INCREMENTAL;
+        }
+      else
+        {
+          /* otherwise fill the area with the foreground color */
 
-      gimp_context_get_foreground (context, &foreground);
-      color = gimp_gegl_color_new (&foreground);
+          GimpRGB    foreground;
+          GeglColor *color;
 
-      gegl_buffer_set_color (paint_buffer, NULL, color);
-      g_object_unref (color);
-    }
+          gimp_context_get_foreground (context, &foreground);
+          color = gimp_gegl_color_new (&foreground);
+
+          gegl_buffer_set_color (paint_buffer, NULL, color);
+          g_object_unref (color);
+        }
 
-  if (gimp_dynamics_is_output_enabled (dynamics, GIMP_DYNAMICS_OUTPUT_FORCE))
-    force = gimp_dynamics_get_linear_value (dynamics,
-                                            GIMP_DYNAMICS_OUTPUT_FORCE,
-                                            coords,
-                                            paint_options,
-                                            fade_point);
-  else
-    force = paint_options->brush_force;
-
-  /* finally, let the brush core paste the colored area on the canvas */
-  gimp_brush_core_paste_canvas (brush_core, drawable,
-                                coords,
-                                MIN (opacity, GIMP_OPACITY_OPAQUE),
-                                gimp_context_get_opacity (context),
-                                gimp_context_get_paint_mode (context),
-                                gimp_paint_options_get_brush_mode (paint_options),
-                                force,
-                                paint_appl_mode);
+      if (gimp_dynamics_is_output_enabled (dynamics, GIMP_DYNAMICS_OUTPUT_FORCE))
+        force = gimp_dynamics_get_linear_value (dynamics,
+                                                GIMP_DYNAMICS_OUTPUT_FORCE,
+                                                coords,
+                                                paint_options,
+                                                fade_point);
+      else
+        force = paint_options->brush_force;
+
+      /* finally, let the brush core paste the colored area on the canvas */
+      gimp_brush_core_paste_canvas (brush_core, drawable,
+                                    coords,
+                                    MIN (opacity, GIMP_OPACITY_OPAQUE),
+                                    gimp_context_get_opacity (context),
+                                    gimp_context_get_paint_mode (context),
+                                    gimp_paint_options_get_brush_mode (paint_options),
+                                    force,
+                                    paint_appl_mode, op);
+    }
 }
diff --git a/app/paint/gimppaintbrush.h b/app/paint/gimppaintbrush.h
index 12756fc..e60c590 100644
--- a/app/paint/gimppaintbrush.h
+++ b/app/paint/gimppaintbrush.h
@@ -54,7 +54,7 @@ GType   gimp_paintbrush_get_type (void) G_GNUC_CONST;
 void    _gimp_paintbrush_motion  (GimpPaintCore             *paint_core,
                                   GimpDrawable              *drawable,
                                   GimpPaintOptions          *paint_options,
-                                  const GimpCoords          *coords,
+                                  GimpSymmetry              *sym,
                                   gdouble                    opacity);
 
 
diff --git a/app/paint/gimppaintcore-loops.c b/app/paint/gimppaintcore-loops.c
index 0a35704..08345bd 100644
--- a/app/paint/gimppaintcore-loops.c
+++ b/app/paint/gimppaintcore-loops.c
@@ -39,14 +39,20 @@ combine_paint_mask_to_canvas_mask (const GimpTempBuf *paint_mask,
   GeglRectangle roi;
   GeglBufferIterator *iter;
 
-  const gint mask_stride       = gimp_temp_buf_get_width (paint_mask);
-  const gint mask_start_offset = mask_y_offset * mask_stride + mask_x_offset;
-  const Babl *mask_format      = gimp_temp_buf_get_format (paint_mask);
+  const gint   mask_stride       = gimp_temp_buf_get_width (paint_mask);
+  const gint   mask_start_offset = mask_y_offset * mask_stride + mask_x_offset;
+  const Babl  *mask_format       = gimp_temp_buf_get_format (paint_mask);
+  GimpTempBuf *modified_mask     = gimp_temp_buf_copy (paint_mask);
+  gint         width;
+  gint         height;
+
+  width  = gimp_temp_buf_get_width (modified_mask);
+  height = gimp_temp_buf_get_height (modified_mask);
 
   roi.x = x_offset;
   roi.y = y_offset;
-  roi.width  = gimp_temp_buf_get_width (paint_mask) - mask_x_offset;
-  roi.height = gimp_temp_buf_get_height (paint_mask) - mask_y_offset;
+  roi.width  = width - mask_x_offset;
+  roi.height = height - mask_y_offset;
 
   iter = gegl_buffer_iterator_new (canvas_buffer, &roi, 0,
                                    babl_format ("Y float"),
@@ -56,7 +62,7 @@ combine_paint_mask_to_canvas_mask (const GimpTempBuf *paint_mask,
     {
       if (mask_format == babl_format ("Y u8"))
         {
-          const guint8 *mask_data = (const guint8 *) gimp_temp_buf_get_data (paint_mask);
+          const guint8 *mask_data = (const guint8 *) gimp_temp_buf_get_data (modified_mask);
           mask_data += mask_start_offset;
 
           while (gegl_buffer_iterator_next (iter))
@@ -81,7 +87,7 @@ combine_paint_mask_to_canvas_mask (const GimpTempBuf *paint_mask,
         }
       else if (mask_format == babl_format ("Y float"))
         {
-          const gfloat *mask_data = (const gfloat *) gimp_temp_buf_get_data (paint_mask);
+          const gfloat *mask_data = (const gfloat *) gimp_temp_buf_get_data (modified_mask);
           mask_data += mask_start_offset;
 
           while (gegl_buffer_iterator_next (iter))
@@ -113,7 +119,7 @@ combine_paint_mask_to_canvas_mask (const GimpTempBuf *paint_mask,
     {
       if (mask_format == babl_format ("Y u8"))
         {
-          const guint8 *mask_data = (const guint8 *) gimp_temp_buf_get_data (paint_mask);
+          const guint8 *mask_data = (const guint8 *) gimp_temp_buf_get_data (modified_mask);
           mask_data += mask_start_offset;
 
           while (gegl_buffer_iterator_next (iter))
@@ -139,7 +145,7 @@ combine_paint_mask_to_canvas_mask (const GimpTempBuf *paint_mask,
         }
       else if (mask_format == babl_format ("Y float"))
         {
-          const gfloat *mask_data = (const gfloat *) gimp_temp_buf_get_data (paint_mask);
+          const gfloat *mask_data = (const gfloat *) gimp_temp_buf_get_data (modified_mask);
           mask_data += mask_start_offset;
 
           while (gegl_buffer_iterator_next (iter))
@@ -168,6 +174,7 @@ combine_paint_mask_to_canvas_mask (const GimpTempBuf *paint_mask,
           g_warning("Mask format not supported: %s", babl_get_name (mask_format));
         }
     }
+  gimp_temp_buf_unref (modified_mask);
 }
 
 void
diff --git a/app/paint/gimppaintcore.c b/app/paint/gimppaintcore.c
index ccca4b4..d88da5e 100644
--- a/app/paint/gimppaintcore.c
+++ b/app/paint/gimppaintcore.c
@@ -37,9 +37,12 @@
 #include "core/gimp-utils.h"
 #include "core/gimpchannel.h"
 #include "core/gimpimage.h"
+#include "core/gimpimage-guides.h"
+#include "core/gimpimage-symmetry.h"
 #include "core/gimpimage-undo.h"
 #include "core/gimppickable.h"
 #include "core/gimpprojection.h"
+#include "core/gimpsymmetry.h"
 #include "core/gimptempbuf.h"
 
 #include "gimppaintcore.h"
@@ -86,7 +89,7 @@ static gboolean  gimp_paint_core_real_pre_paint      (GimpPaintCore    *core,
 static void      gimp_paint_core_real_paint          (GimpPaintCore    *core,
                                                       GimpDrawable     *drawable,
                                                       GimpPaintOptions *options,
-                                                      const GimpCoords *coords,
+                                                      GimpSymmetry     *sym,
                                                       GimpPaintState    paint_state,
                                                       guint32           time);
 static void      gimp_paint_core_real_post_paint     (GimpPaintCore    *core,
@@ -104,7 +107,9 @@ static GeglBuffer *
                                                       GimpPaintOptions *options,
                                                       const GimpCoords *coords,
                                                       gint             *paint_buffer_x,
-                                                      gint             *paint_buffer_y);
+                                                      gint             *paint_buffer_y,
+                                                      gint             *paint_width,
+                                                      gint             *paint_height);
 static GimpUndo* gimp_paint_core_real_push_undo      (GimpPaintCore    *core,
                                                       GimpImage        *image,
                                                       const gchar      *undo_desc);
@@ -231,7 +236,7 @@ static void
 gimp_paint_core_real_paint (GimpPaintCore    *core,
                             GimpDrawable     *drawable,
                             GimpPaintOptions *paint_options,
-                            const GimpCoords *coords,
+                            GimpSymmetry     *sym,
                             GimpPaintState    paint_state,
                             guint32           time)
 {
@@ -264,7 +269,9 @@ gimp_paint_core_real_get_paint_buffer (GimpPaintCore    *core,
                                        GimpPaintOptions *paint_options,
                                        const GimpCoords *coords,
                                        gint             *paint_buffer_x,
-                                       gint             *paint_buffer_y)
+                                       gint             *paint_buffer_y,
+                                       gint             *paint_width,
+                                       gint             *paint_height)
 {
   return NULL;
 }
@@ -304,6 +311,12 @@ gimp_paint_core_paint (GimpPaintCore    *core,
                              paint_options,
                              paint_state, time))
     {
+      GimpSymmetry *sym;
+      GimpImage    *image;
+      GimpItem     *item;
+
+      item  = GIMP_ITEM (drawable);
+      image = gimp_item_get_image (item);
 
       if (paint_state == GIMP_PAINT_STATE_MOTION)
         {
@@ -312,10 +325,13 @@ gimp_paint_core_paint (GimpPaintCore    *core,
           core->last_paint.y = core->cur_coords.y;
         }
 
+      sym = g_object_ref (gimp_image_symmetry_selected (image));
+      gimp_symmetry_set_origin (sym, drawable, &core->cur_coords);
+
       core_class->paint (core, drawable,
                          paint_options,
-                         &core->cur_coords,
-                         paint_state, time);
+                         sym, paint_state, time);
+      g_object_unref (sym);
 
       core_class->post_paint (core, drawable,
                               paint_options,
@@ -749,7 +765,9 @@ gimp_paint_core_get_paint_buffer (GimpPaintCore    *core,
                                   GimpPaintOptions *paint_options,
                                   const GimpCoords *coords,
                                   gint             *paint_buffer_x,
-                                  gint             *paint_buffer_y)
+                                  gint             *paint_buffer_y,
+                                  gint             *paint_width,
+                                  gint             *paint_height)
 {
   GeglBuffer *paint_buffer;
 
@@ -766,7 +784,9 @@ gimp_paint_core_get_paint_buffer (GimpPaintCore    *core,
                                                         paint_options,
                                                         coords,
                                                         paint_buffer_x,
-                                                        paint_buffer_y);
+                                                        paint_buffer_y,
+                                                        paint_width,
+                                                        paint_height);
 
   core->paint_buffer_x = *paint_buffer_x;
   core->paint_buffer_y = *paint_buffer_y;
@@ -818,8 +838,9 @@ gimp_paint_core_paste (GimpPaintCore            *core,
            */
           if (paint_mask != NULL)
             {
-              GeglBuffer *paint_mask_buffer =
-                gimp_temp_buf_create_buffer ((GimpTempBuf *) paint_mask);
+              GimpTempBuf *modified_mask     = gimp_temp_buf_copy (paint_mask);
+              GeglBuffer  *paint_mask_buffer =
+                gimp_temp_buf_create_buffer ((GimpTempBuf *) modified_mask);
 
               gimp_gegl_combine_mask_weird (paint_mask_buffer,
                                             GEGL_RECTANGLE (paint_mask_offset_x,
@@ -833,6 +854,7 @@ gimp_paint_core_paste (GimpPaintCore            *core,
                                             GIMP_IS_AIRBRUSH (core));
 
               g_object_unref (paint_mask_buffer);
+              gimp_temp_buf_unref (modified_mask);
             }
 
           gimp_gegl_apply_mask (core->canvas_buffer,
diff --git a/app/paint/gimppaintcore.h b/app/paint/gimppaintcore.h
index b6b7d06..e880c83 100644
--- a/app/paint/gimppaintcore.h
+++ b/app/paint/gimppaintcore.h
@@ -93,7 +93,7 @@ struct _GimpPaintCoreClass
   void         (* paint)            (GimpPaintCore    *core,
                                      GimpDrawable     *drawable,
                                      GimpPaintOptions *paint_options,
-                                     const GimpCoords *coords,
+                                     GimpSymmetry     *sym,
                                      GimpPaintState    paint_state,
                                      guint32           time);
   void         (* post_paint)       (GimpPaintCore    *core,
@@ -112,7 +112,9 @@ struct _GimpPaintCoreClass
                                      GimpPaintOptions *paint_options,
                                      const GimpCoords *coords,
                                      gint             *paint_buffer_x,
-                                     gint             *paint_buffer_y);
+                                     gint             *paint_buffer_y,
+                                     gint             *paint_width,
+                                     gint             *paint_height);
 
   GimpUndo   * (* push_undo)        (GimpPaintCore    *core,
                                      GimpImage        *image,
@@ -168,7 +170,9 @@ GeglBuffer * gimp_paint_core_get_paint_buffer       (GimpPaintCore    *core,
                                                      GimpPaintOptions *options,
                                                      const GimpCoords *coords,
                                                      gint             *paint_buffer_x,
-                                                     gint             *paint_buffer_y);
+                                                     gint             *paint_buffer_y,
+                                                     gint             *paint_width,
+                                                     gint             *paint_height);
 
 GeglBuffer * gimp_paint_core_get_orig_image         (GimpPaintCore    *core);
 GeglBuffer * gimp_paint_core_get_orig_proj          (GimpPaintCore    *core);
diff --git a/app/paint/gimpperspectiveclone.c b/app/paint/gimpperspectiveclone.c
index 6459eac..fcd5452 100644
--- a/app/paint/gimpperspectiveclone.c
+++ b/app/paint/gimpperspectiveclone.c
@@ -38,6 +38,7 @@
 #include "core/gimpimage.h"
 #include "core/gimppattern.h"
 #include "core/gimppickable.h"
+#include "core/gimpsymmetry.h"
 
 #include "gimpperspectiveclone.h"
 #include "gimpperspectivecloneoptions.h"
@@ -48,7 +49,7 @@
 static void         gimp_perspective_clone_paint      (GimpPaintCore     *paint_core,
                                                        GimpDrawable      *drawable,
                                                        GimpPaintOptions  *paint_options,
-                                                       const GimpCoords  *coords,
+                                                       GimpSymmetry      *sym,
                                                        GimpPaintState     paint_state,
                                                        guint32            time);
 
@@ -120,7 +121,7 @@ static void
 gimp_perspective_clone_paint (GimpPaintCore    *paint_core,
                               GimpDrawable     *drawable,
                               GimpPaintOptions *paint_options,
-                              const GimpCoords *coords,
+                              GimpSymmetry     *sym,
                               GimpPaintState    paint_state,
                               guint32           time)
 {
@@ -129,6 +130,10 @@ gimp_perspective_clone_paint (GimpPaintCore    *paint_core,
   GimpContext          *context       = GIMP_CONTEXT (paint_options);
   GimpCloneOptions     *clone_options = GIMP_CLONE_OPTIONS (paint_options);
   GimpSourceOptions    *options       = GIMP_SOURCE_OPTIONS (paint_options);
+  const GimpCoords     *coords;
+
+  /* The source is based on the original stroke */
+  coords = gimp_symmetry_get_origin (sym);
 
   switch (paint_state)
     {
@@ -284,36 +289,44 @@ gimp_perspective_clone_paint (GimpPaintCore    *paint_core,
 
           gint dest_x;
           gint dest_y;
+          gint n_strokes;
+          gint i;
 
-          dest_x = coords->x;
-          dest_y = coords->y;
-
-          if (options->align_mode == GIMP_SOURCE_ALIGN_REGISTERED)
-            {
-              source_core->offset_x = 0;
-              source_core->offset_y = 0;
-            }
-          else if (options->align_mode == GIMP_SOURCE_ALIGN_FIXED)
+          n_strokes = gimp_symmetry_get_size (sym);
+          for (i = 0; i < n_strokes; i++)
             {
-              source_core->offset_x = source_core->src_x - dest_x;
-              source_core->offset_y = source_core->src_y - dest_y;
-            }
-          else if (source_core->first_stroke)
-            {
-              source_core->offset_x = source_core->src_x - dest_x;
-              source_core->offset_y = source_core->src_y - dest_y;
-
-              /* get destination coordinates in front view perspective */
-              gimp_matrix3_transform_point (&clone->transform_inv,
-                                            dest_x,
-                                            dest_y,
-                                            &clone->dest_x_fv,
-                                            &clone->dest_y_fv);
-
-              source_core->first_stroke = FALSE;
+              coords = gimp_symmetry_get_coords (sym, i);
+
+              dest_x = coords->x;
+              dest_y = coords->y;
+
+              if (options->align_mode == GIMP_SOURCE_ALIGN_REGISTERED)
+                {
+                  source_core->offset_x = 0;
+                  source_core->offset_y = 0;
+                }
+              else if (options->align_mode == GIMP_SOURCE_ALIGN_FIXED)
+                {
+                  source_core->offset_x = source_core->src_x - dest_x;
+                  source_core->offset_y = source_core->src_y - dest_y;
+                }
+              else if (source_core->first_stroke)
+                {
+                  source_core->offset_x = source_core->src_x - dest_x;
+                  source_core->offset_y = source_core->src_y - dest_y;
+
+                  /* get destination coordinates in front view perspective */
+                  gimp_matrix3_transform_point (&clone->transform_inv,
+                                                dest_x,
+                                                dest_y,
+                                                &clone->dest_x_fv,
+                                                &clone->dest_y_fv);
+
+                  source_core->first_stroke = FALSE;
+                }
             }
 
-          gimp_source_core_motion (source_core, drawable, paint_options, coords);
+          gimp_source_core_motion (source_core, drawable, paint_options, sym);
         }
       break;
 
diff --git a/app/paint/gimpsmudge.c b/app/paint/gimpsmudge.c
index 055042a..397132b 100644
--- a/app/paint/gimpsmudge.c
+++ b/app/paint/gimpsmudge.c
@@ -32,6 +32,7 @@
 #include "core/gimpdynamics.h"
 #include "core/gimpimage.h"
 #include "core/gimppickable.h"
+#include "core/gimpsymmetry.h"
 #include "core/gimptempbuf.h"
 
 #include "gimpsmudge.h"
@@ -45,20 +46,21 @@ static void       gimp_smudge_finalize     (GObject          *object);
 static void       gimp_smudge_paint        (GimpPaintCore    *paint_core,
                                             GimpDrawable     *drawable,
                                             GimpPaintOptions *paint_options,
-                                            const GimpCoords *coords,
+                                            GimpSymmetry     *sym,
                                             GimpPaintState    paint_state,
                                             guint32           time);
 static gboolean   gimp_smudge_start        (GimpPaintCore    *paint_core,
                                             GimpDrawable     *drawable,
                                             GimpPaintOptions *paint_options,
-                                            const GimpCoords *coords);
+                                            GimpSymmetry     *sym);
 static void       gimp_smudge_motion       (GimpPaintCore    *paint_core,
                                             GimpDrawable     *drawable,
                                             GimpPaintOptions *paint_options,
-                                            const GimpCoords *coords);
+                                            GimpSymmetry     *sym);
 
 static void       gimp_smudge_accumulator_coords (GimpPaintCore    *paint_core,
                                                   const GimpCoords *coords,
+                                                  gint              stroke,
                                                   gint             *x,
                                                   gint             *y);
 
@@ -110,10 +112,16 @@ gimp_smudge_finalize (GObject *object)
 {
   GimpSmudge *smudge = GIMP_SMUDGE (object);
 
-  if (smudge->accum_buffer)
+  if (smudge->accum_buffers)
     {
-      g_object_unref (smudge->accum_buffer);
-      smudge->accum_buffer = NULL;
+      GList *iter;
+
+      for (iter = smudge->accum_buffers; iter; iter = g_list_next (iter))
+        {
+          if (iter->data)
+            g_object_unref (iter->data);
+          smudge->accum_buffers = NULL;
+        }
     }
 
   G_OBJECT_CLASS (parent_class)->finalize (object);
@@ -123,7 +131,7 @@ static void
 gimp_smudge_paint (GimpPaintCore    *paint_core,
                    GimpDrawable     *drawable,
                    GimpPaintOptions *paint_options,
-                   const GimpCoords *coords,
+                   GimpSymmetry     *sym,
                    GimpPaintState    paint_state,
                    guint32           time)
 {
@@ -135,17 +143,23 @@ gimp_smudge_paint (GimpPaintCore    *paint_core,
       /* initialization fails if the user starts outside the drawable */
       if (! smudge->initialized)
         smudge->initialized = gimp_smudge_start (paint_core, drawable,
-                                                 paint_options, coords);
+                                                 paint_options, sym);
 
       if (smudge->initialized)
-        gimp_smudge_motion (paint_core, drawable, paint_options, coords);
+        gimp_smudge_motion (paint_core, drawable, paint_options, sym);
       break;
 
     case GIMP_PAINT_STATE_FINISH:
-      if (smudge->accum_buffer)
+      if (smudge->accum_buffers)
         {
-          g_object_unref (smudge->accum_buffer);
-          smudge->accum_buffer = NULL;
+          GList *iter;
+
+          for  (iter = smudge->accum_buffers; iter; iter = g_list_next (iter))
+            {
+              if (iter->data)
+                g_object_unref (iter->data);
+              smudge->accum_buffers = NULL;
+            }
         }
       smudge->initialized = FALSE;
       break;
@@ -159,71 +173,90 @@ static gboolean
 gimp_smudge_start (GimpPaintCore    *paint_core,
                    GimpDrawable     *drawable,
                    GimpPaintOptions *paint_options,
-                   const GimpCoords *coords)
+                   GimpSymmetry     *sym)
 {
   GimpSmudge *smudge = GIMP_SMUDGE (paint_core);
   GeglBuffer *paint_buffer;
+  GimpCoords *coords;
   gint        paint_buffer_x;
   gint        paint_buffer_y;
   gint        accum_size;
+  gint        n_strokes;
+  gint        i;
   gint        x, y;
 
-  paint_buffer = gimp_paint_core_get_paint_buffer (paint_core, drawable,
-                                                   paint_options, coords,
-                                                   &paint_buffer_x,
-                                                   &paint_buffer_y);
-  if (! paint_buffer)
-    return FALSE;
-
-  gimp_smudge_accumulator_size (paint_options, coords, &accum_size);
-
-  /*  Allocate the accumulation buffer */
-  smudge->accum_buffer = gegl_buffer_new (GEGL_RECTANGLE (0, 0,
-                                                          accum_size,
-                                                          accum_size),
-                                          babl_format ("RGBA float"));
-
-  /*  adjust the x and y coordinates to the upper left corner of the
-   *  accumulator
-   */
-  gimp_smudge_accumulator_coords (paint_core, coords, &x, &y);
+  coords = gimp_symmetry_get_origin (sym);
+  gimp_brush_core_eval_transform_dynamics (GIMP_BRUSH_CORE (paint_core),
+                                           drawable,
+                                           paint_options,
+                                           coords);
 
-  /*  If clipped, prefill the smudge buffer with the color at the
-   *  brush position.
-   */
-  if (x != paint_buffer_x ||
-      y != paint_buffer_y ||
-      accum_size != gegl_buffer_get_width  (paint_buffer) ||
-      accum_size != gegl_buffer_get_height (paint_buffer))
+  n_strokes = gimp_symmetry_get_size (sym);
+  for (i = 0; i < n_strokes; i++)
     {
-      GimpRGB    pixel;
-      GeglColor *color;
-
-      gimp_pickable_get_color_at (GIMP_PICKABLE (drawable),
-                                  CLAMP ((gint) coords->x,
-                                         0,
-                                         gimp_item_get_width (GIMP_ITEM (drawable)) - 1),
-                                  CLAMP ((gint) coords->y,
-                                         0,
-                                         gimp_item_get_height (GIMP_ITEM (drawable)) - 1),
-                                  &pixel);
-
-      color = gimp_gegl_color_new (&pixel);
-      gegl_buffer_set_color (smudge->accum_buffer, NULL, color);
-      g_object_unref (color);
-    }
+      GeglBuffer *accum_buffer;
+
+      coords = gimp_symmetry_get_coords (sym, i);
+      paint_buffer = gimp_paint_core_get_paint_buffer (paint_core, drawable,
+                                                       paint_options, coords,
+                                                       &paint_buffer_x,
+                                                       &paint_buffer_y,
+                                                       NULL, NULL);
+      if (! paint_buffer)
+        return FALSE;
+
+      gimp_smudge_accumulator_size (paint_options, coords, &accum_size);
+
+      /*  Allocate the accumulation buffer */
+      accum_buffer = gegl_buffer_new (GEGL_RECTANGLE (0, 0,
+                                                      accum_size,
+                                                      accum_size),
+                                      babl_format ("RGBA float"));
+      smudge->accum_buffers = g_list_prepend (smudge->accum_buffers,
+                                              accum_buffer);
+
+      /*  adjust the x and y coordinates to the upper left corner of the
+       *  accumulator
+       */
+      gimp_smudge_accumulator_coords (paint_core, coords, i, &x, &y);
+
+      /*  If clipped, prefill the smudge buffer with the color at the
+       *  brush position.
+       */
+      if (x != paint_buffer_x ||
+          y != paint_buffer_y ||
+          accum_size != gegl_buffer_get_width  (paint_buffer) ||
+          accum_size != gegl_buffer_get_height (paint_buffer))
+        {
+          GimpRGB    pixel;
+          GeglColor *color;
+
+          gimp_pickable_get_color_at (GIMP_PICKABLE (drawable),
+                                      CLAMP ((gint) coords->x,
+                                             0,
+                                             gimp_item_get_width (GIMP_ITEM (drawable)) - 1),
+                                      CLAMP ((gint) coords->y,
+                                             0,
+                                             gimp_item_get_height (GIMP_ITEM (drawable)) - 1),
+                                      &pixel);
+
+          color = gimp_gegl_color_new (&pixel);
+          gegl_buffer_set_color (accum_buffer, NULL, color);
+          g_object_unref (color);
+        }
 
-  /* copy the region under the original painthit. */
-  gegl_buffer_copy (gimp_drawable_get_buffer (drawable),
-                    GEGL_RECTANGLE (paint_buffer_x,
-                                    paint_buffer_y,
-                                    gegl_buffer_get_width  (paint_buffer),
-                                    gegl_buffer_get_height (paint_buffer)),
-                    GEGL_ABYSS_NONE,
-                    smudge->accum_buffer,
-                    GEGL_RECTANGLE (paint_buffer_x - x,
-                                    paint_buffer_y - y,
-                                    0, 0));
+      /* copy the region under the original painthit. */
+      gegl_buffer_copy (gimp_drawable_get_buffer (drawable),
+                        GEGL_RECTANGLE (paint_buffer_x,
+                                        paint_buffer_y,
+                                        gegl_buffer_get_width  (paint_buffer),
+                                        gegl_buffer_get_height (paint_buffer)),
+                        GEGL_ABYSS_NONE,
+                        accum_buffer,
+                        GEGL_RECTANGLE (paint_buffer_x - x,
+                                        paint_buffer_y - y,
+                                        0, 0));
+    }
 
   return TRUE;
 }
@@ -232,28 +265,35 @@ static void
 gimp_smudge_motion (GimpPaintCore    *paint_core,
                     GimpDrawable     *drawable,
                     GimpPaintOptions *paint_options,
-                    const GimpCoords *coords)
+                    GimpSymmetry     *sym)
 {
-  GimpSmudge        *smudge   = GIMP_SMUDGE (paint_core);
-  GimpSmudgeOptions *options  = GIMP_SMUDGE_OPTIONS (paint_options);
-  GimpContext       *context  = GIMP_CONTEXT (paint_options);
-  GimpDynamics      *dynamics = GIMP_BRUSH_CORE (paint_core)->dynamics;
-  GimpImage         *image    = gimp_item_get_image (GIMP_ITEM (drawable));
-  GeglBuffer        *paint_buffer;
-  gint               paint_buffer_x;
-  gint               paint_buffer_y;
-  gint               paint_buffer_width;
-  gint               paint_buffer_height;
-  gdouble            fade_point;
-  gdouble            opacity;
-  gdouble            rate;
-  gdouble            dynamic_rate;
-  gint               x, y;
-  gdouble            force;
+  GimpSmudge         *smudge   = GIMP_SMUDGE (paint_core);
+  GimpSmudgeOptions  *options  = GIMP_SMUDGE_OPTIONS (paint_options);
+  GimpContext        *context  = GIMP_CONTEXT (paint_options);
+  GimpDynamics       *dynamics = GIMP_BRUSH_CORE (paint_core)->dynamics;
+  GimpImage          *image    = gimp_item_get_image (GIMP_ITEM (drawable));
+  GeglBuffer         *paint_buffer;
+  gint                paint_buffer_x;
+  gint                paint_buffer_y;
+  gint                paint_buffer_width;
+  gint                paint_buffer_height;
+  gdouble             fade_point;
+  gdouble             opacity;
+  gdouble             rate;
+  gdouble             dynamic_rate;
+  gint                x, y;
+  gdouble             force;
+  GeglBuffer         *accum_buffer;
+  GimpCoords         *coords;
+  GeglNode           *op;
+  gint                paint_width, paint_height;
+  gint                n_strokes;
+  gint                i;
 
   fade_point = gimp_paint_options_get_fade (paint_options, image,
                                             paint_core->pixel_dist);
 
+  coords = gimp_symmetry_get_origin (sym);
   opacity = gimp_dynamics_get_linear_value (dynamics,
                                             GIMP_DYNAMICS_OUTPUT_OPACITY,
                                             coords,
@@ -262,90 +302,111 @@ gimp_smudge_motion (GimpPaintCore    *paint_core,
   if (opacity == 0.0)
     return;
 
-  paint_buffer = gimp_paint_core_get_paint_buffer (paint_core, drawable,
-                                                   paint_options, coords,
-                                                   &paint_buffer_x,
-                                                   &paint_buffer_y);
-  if (! paint_buffer)
-    return;
-
-  paint_buffer_width  = gegl_buffer_get_width  (paint_buffer);
-  paint_buffer_height = gegl_buffer_get_height (paint_buffer);
-
-  /*  Get the unclipped acumulator coordinates  */
-  gimp_smudge_accumulator_coords (paint_core, coords, &x, &y);
-
-  /* Enable dynamic rate */
-  dynamic_rate = gimp_dynamics_get_linear_value (dynamics,
-                                                 GIMP_DYNAMICS_OUTPUT_RATE,
-                                                 coords,
-                                                 paint_options,
-                                                 fade_point);
-
-  rate = (options->rate / 100.0) * dynamic_rate;
-
-  /*  Smudge uses the buffer Accum.
-   *  For each successive painthit Accum is built like this
-   *    Accum =  rate*Accum  + (1-rate)*I.
-   *  where I is the pixels under the current painthit.
-   *  Then the paint area (paint_area) is built as
-   *    (Accum,1) (if no alpha),
-   */
+  gimp_brush_core_eval_transform_dynamics (GIMP_BRUSH_CORE (paint_core),
+                                           drawable,
+                                           paint_options,
+                                           coords);
 
-  gimp_gegl_smudge_blend (smudge->accum_buffer,
-                          GEGL_RECTANGLE (paint_buffer_x - x,
-                                          paint_buffer_y - y,
-                                          paint_buffer_width,
-                                          paint_buffer_height),
-                          gimp_drawable_get_buffer (drawable),
-                          GEGL_RECTANGLE (paint_buffer_x,
-                                          paint_buffer_y,
-                                          paint_buffer_width,
-                                          paint_buffer_height),
-                          smudge->accum_buffer,
-                          GEGL_RECTANGLE (paint_buffer_x - x,
-                                          paint_buffer_y - y,
-                                          paint_buffer_width,
-                                          paint_buffer_height),
-                          rate);
-
-  gegl_buffer_copy (smudge->accum_buffer,
-                    GEGL_RECTANGLE (paint_buffer_x - x,
-                                    paint_buffer_y - y,
-                                    paint_buffer_width,
-                                    paint_buffer_height),
-                    GEGL_ABYSS_NONE,
-                    paint_buffer,
-                    GEGL_RECTANGLE (0, 0, 0, 0));
-
-  if (gimp_dynamics_is_output_enabled (dynamics, GIMP_DYNAMICS_OUTPUT_FORCE))
-    force = gimp_dynamics_get_linear_value (dynamics,
-                                            GIMP_DYNAMICS_OUTPUT_FORCE,
-                                            coords,
-                                            paint_options,
-                                            fade_point);
-  else
-    force = paint_options->brush_force;
-
-  gimp_brush_core_replace_canvas (GIMP_BRUSH_CORE (paint_core), drawable,
-                                  coords,
-                                  MIN (opacity, GIMP_OPACITY_OPAQUE),
-                                  gimp_context_get_opacity (context),
-                                  gimp_paint_options_get_brush_mode (paint_options),
-                                  force,
-                                  GIMP_PAINT_INCREMENTAL);
+  n_strokes = gimp_symmetry_get_size (sym);
+  for (i = 0; i < n_strokes; i++)
+    {
+      coords = gimp_symmetry_get_coords (sym, i);
+
+      paint_buffer = gimp_paint_core_get_paint_buffer (paint_core, drawable,
+                                                       paint_options, coords,
+                                                       &paint_buffer_x,
+                                                       &paint_buffer_y,
+                                                       &paint_width,
+                                                       &paint_height);
+      if (! paint_buffer)
+        continue;
+
+      op = gimp_symmetry_get_operation (sym, i,
+                                        paint_width,
+                                        paint_height);
+
+      paint_buffer_width  = gegl_buffer_get_width  (paint_buffer);
+      paint_buffer_height = gegl_buffer_get_height (paint_buffer);
+
+      /*  Get the unclipped acumulator coordinates  */
+      gimp_smudge_accumulator_coords (paint_core, coords, i, &x, &y);
+
+      /* Enable dynamic rate */
+      dynamic_rate = gimp_dynamics_get_linear_value (dynamics,
+                                                     GIMP_DYNAMICS_OUTPUT_RATE,
+                                                     coords,
+                                                     paint_options,
+                                                     fade_point);
+
+      rate = (options->rate / 100.0) * dynamic_rate;
+
+      /*  Smudge uses the buffer Accum.
+       *  For each successive painthit Accum is built like this
+       *    Accum =  rate*Accum  + (1-rate)*I.
+       *  where I is the pixels under the current painthit.
+       *  Then the paint area (paint_area) is built as
+       *    (Accum,1) (if no alpha),
+       */
+
+      accum_buffer = g_list_nth_data (smudge->accum_buffers, i);
+      gimp_gegl_smudge_blend (accum_buffer,
+                              GEGL_RECTANGLE (paint_buffer_x - x,
+                                              paint_buffer_y - y,
+                                              paint_buffer_width,
+                                              paint_buffer_height),
+                              gimp_drawable_get_buffer (drawable),
+                              GEGL_RECTANGLE (paint_buffer_x,
+                                              paint_buffer_y,
+                                              paint_buffer_width,
+                                              paint_buffer_height),
+                              accum_buffer,
+                              GEGL_RECTANGLE (paint_buffer_x - x,
+                                              paint_buffer_y - y,
+                                              paint_buffer_width,
+                                              paint_buffer_height),
+                              rate);
+
+      gegl_buffer_copy (accum_buffer,
+                        GEGL_RECTANGLE (paint_buffer_x - x,
+                                        paint_buffer_y - y,
+                                        paint_buffer_width,
+                                        paint_buffer_height),
+                        GEGL_ABYSS_NONE,
+                        paint_buffer,
+                        GEGL_RECTANGLE (0, 0, 0, 0));
+
+      if (gimp_dynamics_is_output_enabled (dynamics, GIMP_DYNAMICS_OUTPUT_FORCE))
+        force = gimp_dynamics_get_linear_value (dynamics,
+                                                GIMP_DYNAMICS_OUTPUT_FORCE,
+                                                coords,
+                                                paint_options,
+                                                fade_point);
+      else
+        force = paint_options->brush_force;
+
+      gimp_brush_core_replace_canvas (GIMP_BRUSH_CORE (paint_core), drawable,
+                                      coords,
+                                      MIN (opacity, GIMP_OPACITY_OPAQUE),
+                                      gimp_context_get_opacity (context),
+                                      gimp_paint_options_get_brush_mode (paint_options),
+                                      force,
+                                      GIMP_PAINT_INCREMENTAL, op);
+    }
 }
 
 static void
 gimp_smudge_accumulator_coords (GimpPaintCore    *paint_core,
                                 const GimpCoords *coords,
+                                gint              stroke,
                                 gint             *x,
                                 gint             *y)
 {
   GimpSmudge *smudge = GIMP_SMUDGE (paint_core);
+  GeglBuffer *accum_buffer;
 
-  *x = (gint) coords->x - gegl_buffer_get_width  (smudge->accum_buffer) / 2;
-  *y = (gint) coords->y - gegl_buffer_get_height (smudge->accum_buffer) / 2;
+  accum_buffer = g_list_nth_data (smudge->accum_buffers, stroke);
+  *x = (gint) coords->x - gegl_buffer_get_width  (accum_buffer) / 2;
+  *y = (gint) coords->y - gegl_buffer_get_height (accum_buffer) / 2;
 }
 
 static void
diff --git a/app/paint/gimpsmudge.h b/app/paint/gimpsmudge.h
index fe5d07e..881a0ed 100644
--- a/app/paint/gimpsmudge.h
+++ b/app/paint/gimpsmudge.h
@@ -37,7 +37,7 @@ struct _GimpSmudge
   GimpBrushCore  parent_instance;
 
   gboolean       initialized;
-  GeglBuffer    *accum_buffer;
+  GList         *accum_buffers;
 };
 
 struct _GimpSmudgeClass
diff --git a/app/paint/gimpsourcecore.c b/app/paint/gimpsourcecore.c
index cad949b..364c70e 100644
--- a/app/paint/gimpsourcecore.c
+++ b/app/paint/gimpsourcecore.c
@@ -32,6 +32,7 @@
 #include "core/gimperror.h"
 #include "core/gimpimage.h"
 #include "core/gimppickable.h"
+#include "core/gimpsymmetry.h"
 
 #include "gimpsourcecore.h"
 #include "gimpsourceoptions.h"
@@ -65,7 +66,7 @@ static gboolean gimp_source_core_start           (GimpPaintCore     *paint_core,
 static void     gimp_source_core_paint           (GimpPaintCore     *paint_core,
                                                   GimpDrawable      *drawable,
                                                   GimpPaintOptions  *paint_options,
-                                                  const GimpCoords  *coords,
+                                                  GimpSymmetry      *sym,
                                                   GimpPaintState     paint_state,
                                                   guint32            time);
 
@@ -73,7 +74,7 @@ static void     gimp_source_core_paint           (GimpPaintCore     *paint_core,
 static void     gimp_source_core_motion          (GimpSourceCore    *source_core,
                                                   GimpDrawable      *drawable,
                                                   GimpPaintOptions  *paint_options,
-                                                  const GimpCoords  *coords);
+                                                  GimpSymmetry      *sym);
 #endif
 
 static gboolean gimp_source_core_real_use_source (GimpSourceCore    *source_core,
@@ -253,12 +254,16 @@ static void
 gimp_source_core_paint (GimpPaintCore    *paint_core,
                         GimpDrawable     *drawable,
                         GimpPaintOptions *paint_options,
-                        const GimpCoords *coords,
+                        GimpSymmetry     *sym,
                         GimpPaintState    paint_state,
                         guint32           time)
 {
   GimpSourceCore    *source_core = GIMP_SOURCE_CORE (paint_core);
   GimpSourceOptions *options     = GIMP_SOURCE_OPTIONS (paint_options);
+  const GimpCoords  *coords;
+
+  /* The source is based on the original stroke */
+  coords = gimp_symmetry_get_origin (sym);
 
   switch (paint_state)
     {
@@ -322,7 +327,8 @@ gimp_source_core_paint (GimpPaintCore    *paint_core,
           source_core->src_x = dest_x + source_core->offset_x;
           source_core->src_y = dest_y + source_core->offset_y;
 
-          gimp_source_core_motion (source_core, drawable, paint_options, coords);
+          gimp_source_core_motion (source_core, drawable, paint_options,
+                                   sym);
         }
       break;
 
@@ -347,7 +353,7 @@ void
 gimp_source_core_motion (GimpSourceCore   *source_core,
                          GimpDrawable     *drawable,
                          GimpPaintOptions *paint_options,
-                         const GimpCoords *coords)
+                         GimpSymmetry     *sym)
 
 {
   GimpPaintCore     *paint_core   = GIMP_PAINT_CORE (source_core);
@@ -357,6 +363,8 @@ gimp_source_core_motion (GimpSourceCore   *source_core,
   GimpPickable      *src_pickable = NULL;
   GeglBuffer        *src_buffer   = NULL;
   GeglRectangle      src_rect;
+  gint               base_src_offset_x;
+  gint               base_src_offset_y;
   gint               src_offset_x;
   gint               src_offset_y;
   GeglBuffer        *paint_buffer;
@@ -368,20 +376,27 @@ gimp_source_core_motion (GimpSourceCore   *source_core,
   gint               paint_area_height;
   gdouble            fade_point;
   gdouble            opacity;
+  GeglNode          *op;
+  GimpCoords        *origin;
+  GimpCoords        *coords;
+  gint               n_strokes;
+  gint               i;
 
   fade_point = gimp_paint_options_get_fade (paint_options, image,
                                             paint_core->pixel_dist);
 
+  origin     = gimp_symmetry_get_origin (sym);
+  /* Some settings are based on the original stroke. */
   opacity = gimp_dynamics_get_linear_value (dynamics,
                                             GIMP_DYNAMICS_OUTPUT_OPACITY,
-                                            coords,
+                                            origin,
                                             paint_options,
                                             fade_point);
   if (opacity == 0.0)
     return;
 
-  src_offset_x = source_core->offset_x;
-  src_offset_y = source_core->offset_y;
+  base_src_offset_x = source_core->offset_x;
+  base_src_offset_y = source_core->offset_y;
 
   if (gimp_source_core_use_source (source_core, options))
     {
@@ -397,69 +412,91 @@ gimp_source_core_motion (GimpSourceCore   *source_core,
           gimp_item_get_offset (GIMP_ITEM (source_core->src_drawable),
                                 &off_x, &off_y);
 
-          src_offset_x += off_x;
-          src_offset_y += off_y;
+          base_src_offset_x += off_x;
+          base_src_offset_y += off_y;
         }
 
       gimp_pickable_flush (src_pickable);
     }
 
-  paint_buffer = gimp_paint_core_get_paint_buffer (paint_core, drawable,
-                                                   paint_options, coords,
-                                                   &paint_buffer_x,
-                                                   &paint_buffer_y);
-  if (! paint_buffer)
-    return;
+  gimp_brush_core_eval_transform_dynamics (GIMP_BRUSH_CORE (paint_core),
+                                           drawable,
+                                           paint_options,
+                                           origin);
 
-  paint_area_offset_x = 0;
-  paint_area_offset_y = 0;
-  paint_area_width    = gegl_buffer_get_width  (paint_buffer);
-  paint_area_height   = gegl_buffer_get_height (paint_buffer);
-
-  if (gimp_source_core_use_source (source_core, options))
+  n_strokes  = gimp_symmetry_get_size (sym);
+  for (i = 0; i < n_strokes; i++)
     {
-      src_buffer =
-        GIMP_SOURCE_CORE_GET_CLASS (source_core)->get_source (source_core,
-                                                              drawable,
-                                                              paint_options,
-                                                              src_pickable,
-                                                              src_offset_x,
-                                                              src_offset_y,
-                                                              paint_buffer,
-                                                              paint_buffer_x,
-                                                              paint_buffer_y,
-                                                              &paint_area_offset_x,
-                                                              &paint_area_offset_y,
-                                                              &paint_area_width,
-                                                              &paint_area_height,
-                                                              &src_rect);
-      if (! src_buffer)
-        return;
-    }
+      coords = gimp_symmetry_get_coords (sym, i);
+
+      paint_buffer = gimp_paint_core_get_paint_buffer (paint_core, drawable,
+                                                       paint_options, coords,
+                                                       &paint_buffer_x,
+                                                       &paint_buffer_y,
+                                                       NULL, NULL);
+      if (! paint_buffer)
+        continue;
+
+      paint_area_offset_x = 0;
+      paint_area_offset_y = 0;
+      paint_area_width    = gegl_buffer_get_width  (paint_buffer);
+      paint_area_height   = gegl_buffer_get_height (paint_buffer);
+
+      src_offset_x = base_src_offset_x;
+      src_offset_y = base_src_offset_y;
+      if (gimp_source_core_use_source (source_core, options))
+        {
+          /* When using a source, use the same for every stroke. */
+          src_offset_x = src_offset_x - coords->x + origin->x;
+          src_offset_y = src_offset_y - coords->y + origin->y;
+          src_buffer =
+            GIMP_SOURCE_CORE_GET_CLASS (source_core)->get_source (source_core,
+                                                                  drawable,
+                                                                  paint_options,
+                                                                  src_pickable,
+                                                                  src_offset_x,
+                                                                  src_offset_y,
+                                                                  paint_buffer,
+                                                                  paint_buffer_x,
+                                                                  paint_buffer_y,
+                                                                  &paint_area_offset_x,
+                                                                  &paint_area_offset_y,
+                                                                  &paint_area_width,
+                                                                  &paint_area_height,
+                                                                  &src_rect);
+
+          if (! src_buffer)
+            continue;
+        }
 
-  /*  Set the paint buffer to transparent  */
-  gegl_buffer_clear (paint_buffer, NULL);
-
-  GIMP_SOURCE_CORE_GET_CLASS (source_core)->motion (source_core,
-                                                    drawable,
-                                                    paint_options,
-                                                    coords,
-                                                    opacity,
-                                                    src_pickable,
-                                                    src_buffer,
-                                                    &src_rect,
-                                                    src_offset_x,
-                                                    src_offset_y,
-                                                    paint_buffer,
-                                                    paint_buffer_x,
-                                                    paint_buffer_y,
-                                                    paint_area_offset_x,
-                                                    paint_area_offset_y,
-                                                    paint_area_width,
-                                                    paint_area_height);
-
-  if (src_buffer)
-    g_object_unref (src_buffer);
+      /*  Set the paint buffer to transparent  */
+      gegl_buffer_clear (paint_buffer, NULL);
+
+      op = gimp_symmetry_get_operation (sym, i,
+                                        gegl_buffer_get_width (paint_buffer),
+                                        gegl_buffer_get_height (paint_buffer));
+      GIMP_SOURCE_CORE_GET_CLASS (source_core)->motion (source_core,
+                                                        drawable,
+                                                        paint_options,
+                                                        coords,
+                                                        op,
+                                                        opacity,
+                                                        src_pickable,
+                                                        src_buffer,
+                                                        &src_rect,
+                                                        src_offset_x,
+                                                        src_offset_y,
+                                                        paint_buffer,
+                                                        paint_buffer_x,
+                                                        paint_buffer_y,
+                                                        paint_area_offset_x,
+                                                        paint_area_offset_y,
+                                                        paint_area_width,
+                                                        paint_area_height);
+
+      if (src_buffer)
+        g_object_unref (src_buffer);
+    }
 }
 
 gboolean
diff --git a/app/paint/gimpsourcecore.h b/app/paint/gimpsourcecore.h
index 4f71bb0..583f3ab 100644
--- a/app/paint/gimpsourcecore.h
+++ b/app/paint/gimpsourcecore.h
@@ -77,6 +77,7 @@ struct _GimpSourceCoreClass
                                GimpDrawable      *drawable,
                                GimpPaintOptions  *paint_options,
                                const GimpCoords  *coords,
+                               GeglNode          *op,
                                gdouble            opacity,
                                GimpPickable      *src_pickable,
                                GeglBuffer        *src_buffer,
@@ -103,7 +104,7 @@ gboolean gimp_source_core_use_source (GimpSourceCore    *source_core,
 void     gimp_source_core_motion     (GimpSourceCore    *source_core,
                                       GimpDrawable      *drawable,
                                       GimpPaintOptions  *paint_options,
-                                      const GimpCoords  *coords);
+                                      GimpSymmetry      *sym);
 
 
 #endif  /*  __GIMP_SOURCE_CORE_H__  */
diff --git a/app/pdb/image-guides-cmds.c b/app/pdb/image-guides-cmds.c
index 10e78c9..e81335c 100644
--- a/app/pdb/image-guides-cmds.c
+++ b/app/pdb/image-guides-cmds.c
@@ -27,6 +27,7 @@
 
 #include "pdb-types.h"
 
+#include "cairo.h"
 #include "core/gimpguide.h"
 #include "core/gimpimage-guides.h"
 #include "core/gimpimage-undo-push.h"
diff --git a/app/widgets/Makefile.am b/app/widgets/Makefile.am
index 4fecc8c..021d143 100644
--- a/app/widgets/Makefile.am
+++ b/app/widgets/Makefile.am
@@ -337,6 +337,8 @@ libappwidgets_a_sources = \
        gimpstringaction.h              \
        gimpstrokeeditor.c              \
        gimpstrokeeditor.h              \
+       gimpsymmetryeditor.c            \
+       gimpsymmetryeditor.h            \
        gimptagentry.c                  \
        gimptagentry.h                  \
        gimptagpopup.c                  \
diff --git a/app/widgets/gimphelp-ids.h b/app/widgets/gimphelp-ids.h
index 796e70f..f087b77 100644
--- a/app/widgets/gimphelp-ids.h
+++ b/app/widgets/gimphelp-ids.h
@@ -557,6 +557,7 @@
 #define GIMP_HELP_INFO_DIALOG                     "gimp-info-dialog"
 #define GIMP_HELP_MODULE_DIALOG                   "gimp-module-dialog"
 #define GIMP_HELP_NAVIGATION_DIALOG               "gimp-navigation-dialog"
+#define GIMP_HELP_SYMMETRY_DIALOG                 "gimp-symmetry-dialog"
 #define GIMP_HELP_TEXT_EDITOR_DIALOG              "gimp-text-editor-dialog"
 #define GIMP_HELP_TIPS_DIALOG                     "gimp-tips-dialog"
 #define GIMP_HELP_UNDO_DIALOG                     "gimp-undo-dialog"
diff --git a/app/widgets/gimpsymmetryeditor.c b/app/widgets/gimpsymmetryeditor.c
new file mode 100644
index 0000000..eb5169d
--- /dev/null
+++ b/app/widgets/gimpsymmetryeditor.c
@@ -0,0 +1,506 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimpsymmetryeditor.c
+ * Copyright (C) 2015 Jehan <jehan girinstud io>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <gegl.h>
+#include <gtk/gtk.h>
+
+#include "libgimpwidgets/gimpwidgets.h"
+
+#include "widgets-types.h"
+
+#include "config/gimpguiconfig.h"
+
+#include "core/gimp.h"
+#include "core/gimpcontext.h"
+#include "core/gimpimage.h"
+#include "core/gimpimage-symmetry.h"
+#include "core/gimpsymmetry.h"
+
+#include "gimpdocked.h"
+#include "gimpmenufactory.h"
+#include "gimppropwidgets.h"
+#include "gimpspinscale.h"
+#include "gimpsymmetryeditor.h"
+
+#include "gimp-intl.h"
+
+enum
+{
+  PROP_0,
+  PROP_GIMP,
+};
+
+struct _GimpSymmetryEditorPrivate
+{
+  Gimp            *gimp;
+  GimpImage       *image;
+
+  GtkWidget       *menu;
+  GtkWidget       *options_frame;
+};
+
+static void        gimp_symmetry_editor_docked_iface_init (GimpDockedInterface   *iface);
+
+/* Signal handlers on the GObject. */
+static void        gimp_symmetry_editor_constructed       (GObject               *object);
+static void        gimp_symmetry_editor_dispose           (GObject               *object);
+static void        gimp_symmetry_editor_set_property      (GObject               *object,
+                                                           guint                  property_id,
+                                                           const GValue          *value,
+                                                           GParamSpec            *pspec);
+static void        gimp_symmetry_editor_get_property      (GObject               *object,
+                                                           guint                  property_id,
+                                                           GValue                *value,
+                                                           GParamSpec            *pspec);
+
+/* Signal handlers on the context. */
+static void        gimp_symmetry_editor_image_changed     (GimpContext           *context,
+                                                           GimpImage             *image,
+                                                           GimpSymmetryEditor    *editor);
+
+/* Signal handlers on the contextual image. */
+static void        gimp_symmetry_editor_symmetry_notify   (GimpImage             *image,
+                                                           GParamSpec            *pspec,
+                                                           GimpSymmetryEditor    *editor);
+
+/* Signal handlers on the symmetry. */
+static void        gimp_symmetry_editor_symmetry_updated  (GimpSymmetry          *symmetry,
+                                                           GimpImage             *image,
+                                                           GimpSymmetryEditor    *editor);
+
+/* Private functions. */
+static void        gimp_symmetry_editor_set_options       (GimpSymmetryEditor    *editor,
+                                                           GimpSymmetry          *symmetry);
+
+
+G_DEFINE_TYPE_WITH_CODE (GimpSymmetryEditor, gimp_symmetry_editor,
+                         GIMP_TYPE_EDITOR,
+                         G_IMPLEMENT_INTERFACE (GIMP_TYPE_DOCKED,
+                                                gimp_symmetry_editor_docked_iface_init))
+
+#define parent_class gimp_symmetry_editor_parent_class
+
+static void
+gimp_symmetry_editor_class_init (GimpSymmetryEditorClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed  = gimp_symmetry_editor_constructed;
+  object_class->dispose      = gimp_symmetry_editor_dispose;
+  object_class->set_property = gimp_symmetry_editor_set_property;
+  object_class->get_property = gimp_symmetry_editor_get_property;
+
+  g_object_class_install_property (object_class, PROP_GIMP,
+                                   g_param_spec_object ("gimp",
+                                                        NULL, NULL,
+                                                        GIMP_TYPE_GIMP,
+                                                        GIMP_PARAM_READWRITE |
+                                                        G_PARAM_CONSTRUCT_ONLY));
+
+  g_type_class_add_private (klass, sizeof (GimpSymmetryEditorPrivate));
+}
+
+static void
+gimp_symmetry_editor_docked_iface_init (GimpDockedInterface *docked_iface)
+{
+}
+
+static void
+gimp_symmetry_editor_init (GimpSymmetryEditor *editor)
+{
+  GtkScrolledWindow *scrolled_window;
+
+  editor->p = G_TYPE_INSTANCE_GET_PRIVATE (editor,
+                                           GIMP_TYPE_SYMMETRY_EDITOR,
+                                           GimpSymmetryEditorPrivate);
+
+  gtk_widget_set_size_request (GTK_WIDGET (editor), -1, 200);
+
+  /* Scrolled window to keep the dock size reasonable. */
+  scrolled_window = GTK_SCROLLED_WINDOW (gtk_scrolled_window_new (NULL, NULL));
+
+  gtk_scrolled_window_set_policy (scrolled_window,
+                                  GTK_POLICY_AUTOMATIC,
+                                  GTK_POLICY_AUTOMATIC);
+
+  gtk_box_pack_start (GTK_BOX (editor),
+                      GTK_WIDGET (scrolled_window),
+                      TRUE, TRUE, 0);
+  gtk_widget_show (GTK_WIDGET (scrolled_window));
+
+  /* A frame to hold the symmetry options. */
+  editor->p->options_frame = gimp_frame_new ("");
+  gtk_scrolled_window_add_with_viewport (scrolled_window,
+                                         editor->p->options_frame);
+}
+
+static void
+gimp_symmetry_editor_constructed (GObject *object)
+{
+  GimpSymmetryEditor *editor = GIMP_SYMMETRY_EDITOR (object);
+  GimpContext           *user_context;
+
+  G_OBJECT_CLASS (parent_class)->constructed (object);
+
+  user_context = gimp_get_user_context (editor->p->gimp);
+
+  g_signal_connect_object (user_context, "image-changed",
+                           G_CALLBACK (gimp_symmetry_editor_image_changed),
+                           editor,
+                           0);
+
+  gimp_symmetry_editor_image_changed (user_context,
+                                      gimp_context_get_image (user_context),
+                                      editor);
+}
+
+static void
+gimp_symmetry_editor_dispose (GObject *object)
+{
+  GimpSymmetryEditor *editor = GIMP_SYMMETRY_EDITOR (object);
+
+  g_clear_object (&editor->p->image);
+
+  G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+gimp_symmetry_editor_set_property (GObject      *object,
+                                   guint         property_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  GimpSymmetryEditor *editor = GIMP_SYMMETRY_EDITOR (object);
+
+  switch (property_id)
+    {
+    case PROP_GIMP:
+      editor->p->gimp = g_value_get_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+gimp_symmetry_editor_get_property (GObject    *object,
+                                   guint       property_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  GimpSymmetryEditor *editor = GIMP_SYMMETRY_EDITOR (object);
+
+  switch (property_id)
+    {
+    case PROP_GIMP:
+      g_value_set_object (value, editor->p->gimp);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+gimp_symmetry_editor_image_changed (GimpContext        *context,
+                                    GimpImage          *image,
+                                    GimpSymmetryEditor *editor)
+{
+  GimpGuiConfig *guiconfig;
+
+  if (image == editor->p->image)
+    return;
+
+  guiconfig = GIMP_GUI_CONFIG (editor->p->gimp->config);
+
+  /* Disconnect and unref the previous image. */
+  if (editor->p->image)
+    {
+      g_signal_handlers_disconnect_by_func (editor->p->image,
+                                            G_CALLBACK (gimp_symmetry_editor_symmetry_notify),
+                                            editor);
+      g_object_unref (editor->p->image);
+      editor->p->image = NULL;
+    }
+
+  /* Destroy the previous menu. */
+  if (editor->p->menu)
+    gtk_widget_destroy (editor->p->menu);
+  editor->p->menu = NULL;
+
+  if (image && guiconfig->playground_symmetry)
+    {
+      GtkListStore *store;
+      GtkTreeIter   iter;
+      GList        *syms;
+      GList        *sym_iter;
+      GimpSymmetry *symmetry;
+
+      store = gimp_int_store_new ();
+
+      /* The menu of available symmetries. */
+      syms = gimp_image_symmetry_list ();
+      for (sym_iter = syms; sym_iter; sym_iter = g_list_next (sym_iter))
+        {
+          GimpSymmetryClass *klass;
+          GType              type;
+
+          type = (GType) sym_iter->data;
+          klass = g_type_class_ref (type);
+
+          gtk_list_store_prepend (store, &iter);
+          gtk_list_store_set (store, &iter,
+                              GIMP_INT_STORE_LABEL,
+                              klass->label,
+                              GIMP_INT_STORE_VALUE,
+                              sym_iter->data,
+                              -1);
+          g_type_class_unref (klass);
+        }
+      g_list_free (syms);
+
+      gtk_list_store_prepend (store, &iter);
+      gtk_list_store_set (store, &iter,
+                          GIMP_INT_STORE_LABEL, _("None"),
+                          GIMP_INT_STORE_VALUE, G_TYPE_NONE,
+                          -1);
+      editor->p->menu = gimp_prop_int_combo_box_new (G_OBJECT (image),
+                                                     "symmetry",
+                                                     GIMP_INT_STORE (store));
+      g_object_unref (store);
+
+      gimp_int_combo_box_set_label (GIMP_INT_COMBO_BOX (editor->p->menu),
+                                    _("Symmetry"));
+      g_object_set (editor->p->menu, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
+
+      gtk_box_pack_start (GTK_BOX (editor), editor->p->menu,
+                          FALSE, FALSE, 0);
+      gtk_box_reorder_child (GTK_BOX (editor), editor->p->menu, 0);
+      gtk_widget_show (editor->p->menu);
+
+      /* Connect to symmetry change. */
+      g_signal_connect (image, "notify::symmetry",
+                        G_CALLBACK (gimp_symmetry_editor_symmetry_notify),
+                        editor);
+
+      /* Update the symmetry options. */
+      symmetry = gimp_image_symmetry_selected (image);
+      gimp_symmetry_editor_set_options (editor, symmetry);
+      editor->p->image = g_object_ref (image);
+    }
+  else if (! guiconfig->playground_symmetry)
+    {
+      GtkWidget *label;
+
+      /* Display a text when the feature is disabled. */
+      label = gtk_label_new (_("Symmetry Painting is disabled.\n"
+                               "You can enable the feature in the "
+                               "\"Experimental Playground\" section of \"Preferences\"."));
+      gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
+      gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.0);
+      gimp_label_set_attributes (GTK_LABEL (label),
+                                 PANGO_ATTR_WEIGHT, PANGO_WEIGHT_BOLD,
+                                 -1);
+      gtk_container_add (GTK_CONTAINER (editor->p->options_frame), label);
+      gtk_widget_show (label);
+      gtk_widget_show (editor->p->options_frame);
+    }
+}
+
+static void
+gimp_symmetry_editor_symmetry_notify (GimpImage           *image,
+                                      GParamSpec          *pspec,
+                                      GimpSymmetryEditor  *editor)
+{
+  GimpSymmetry *symmetry = NULL;
+
+  if (image &&
+      (symmetry = gimp_image_symmetry_selected (image)))
+    {
+      g_signal_connect (symmetry, "update-ui",
+                        G_CALLBACK (gimp_symmetry_editor_symmetry_updated),
+                        editor);
+    }
+
+  gimp_symmetry_editor_set_options (editor, symmetry);
+}
+
+static void
+gimp_symmetry_editor_symmetry_updated (GimpSymmetry       *symmetry,
+                                       GimpImage          *image,
+                                       GimpSymmetryEditor *editor)
+{
+  GimpContext *context;
+
+  g_return_if_fail (GIMP_IS_SYMMETRY (symmetry));
+
+  context = gimp_get_user_context (editor->p->gimp);
+  if (image != context->image ||
+      symmetry != gimp_image_symmetry_selected (image))
+    {
+      g_signal_handlers_disconnect_by_func (G_OBJECT (symmetry),
+                                            gimp_symmetry_editor_symmetry_updated,
+                                            editor);
+      return;
+    }
+
+  gimp_symmetry_editor_set_options (editor, symmetry);
+}
+
+/*  private functions  */
+
+static void
+gimp_symmetry_editor_set_options (GimpSymmetryEditor *editor,
+                                  GimpSymmetry       *symmetry)
+{
+  GimpSymmetryClass  *klass;
+  GtkWidget          *frame;
+  GtkWidget          *vbox;
+  GParamSpec        **specs;
+  gint                n_properties;
+  gint                i;
+
+  frame = editor->p->options_frame;
+
+  /* Clean the old frame */
+  gtk_widget_hide (frame);
+  gtk_container_foreach (GTK_CONTAINER (frame),
+                         (GtkCallback) gtk_widget_destroy, NULL);
+
+  if (! symmetry || symmetry->type == GIMP_TYPE_SYMMETRY)
+    return;
+
+  klass = g_type_class_ref (symmetry->type);
+  gtk_frame_set_label (GTK_FRAME (frame),
+                       klass->label);
+  g_type_class_unref (klass);
+
+  vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2);
+  gtk_container_add (GTK_CONTAINER (frame), vbox);
+  gtk_widget_show (vbox);
+
+  specs = gimp_symmetry_get_settings (symmetry, &n_properties);
+
+  for (i = 0; i < n_properties; i++)
+    {
+      GParamSpec  *spec;
+      const gchar *name;
+      const gchar *blurb;
+
+      if (specs[i] == NULL)
+        {
+          GtkWidget *separator;
+
+          separator = gtk_hseparator_new ();
+          gtk_box_pack_start (GTK_BOX (vbox), separator,
+                              FALSE, FALSE, 0);
+          gtk_widget_show (separator);
+          continue;
+        }
+      spec = G_PARAM_SPEC (specs[i]);
+
+      name = g_param_spec_get_name (spec);
+      blurb = g_param_spec_get_blurb (spec);
+
+      switch (spec->value_type)
+        {
+        case G_TYPE_BOOLEAN:
+            {
+              GtkWidget *checkbox;
+
+              checkbox = gimp_prop_check_button_new (G_OBJECT (symmetry),
+                                                     name,
+                                                     blurb);
+              gtk_box_pack_start (GTK_BOX (vbox), checkbox,
+                                  FALSE, FALSE, 0);
+              gtk_widget_show (checkbox);
+            }
+          break;
+        case G_TYPE_DOUBLE:
+        case G_TYPE_INT:
+        case G_TYPE_UINT:
+            {
+              GtkWidget *scale;
+              gdouble    minimum;
+              gdouble    maximum;
+
+              if (spec->value_type == G_TYPE_DOUBLE)
+                {
+                  minimum = G_PARAM_SPEC_DOUBLE (spec)->minimum;
+                  maximum = G_PARAM_SPEC_DOUBLE (spec)->maximum;
+                }
+              else if (spec->value_type == G_TYPE_INT)
+                {
+                  minimum = G_PARAM_SPEC_INT (spec)->minimum;
+                  maximum = G_PARAM_SPEC_INT (spec)->maximum;
+                }
+              else
+                {
+                  minimum = G_PARAM_SPEC_UINT (spec)->minimum;
+                  maximum = G_PARAM_SPEC_UINT (spec)->maximum;
+                }
+
+              scale = gimp_prop_spin_scale_new (G_OBJECT (symmetry),
+                                                name, blurb,
+                                                1.0, 10.0, 1);
+              gimp_spin_scale_set_scale_limits (GIMP_SPIN_SCALE (scale),
+                                                minimum,
+                                                maximum);
+              gtk_box_pack_start (GTK_BOX (vbox), scale, TRUE, TRUE, 0);
+              gtk_widget_show (scale);
+            }
+          break;
+        default:
+          /* Type of parameter we haven't handled yet. */
+          continue;
+        }
+    }
+
+  g_free (specs);
+
+  /* Finally show the frame. */
+  gtk_widget_show (frame);
+}
+
+/*  public functions  */
+
+GtkWidget *
+gimp_symmetry_editor_new (Gimp            *gimp,
+                          GimpImage       *image,
+                          GimpMenuFactory *menu_factory)
+{
+  GimpSymmetryEditor *editor;
+
+  g_return_val_if_fail (GIMP_IS_GIMP (gimp), NULL);
+  g_return_val_if_fail (GIMP_IS_MENU_FACTORY (menu_factory), NULL);
+  g_return_val_if_fail (image == NULL || GIMP_IS_IMAGE (image), NULL);
+
+  editor = g_object_new (GIMP_TYPE_SYMMETRY_EDITOR,
+                         "gimp",            gimp,
+                         "menu-factory",    menu_factory,
+                         NULL);
+
+  return GTK_WIDGET (editor);
+}
diff --git a/app/widgets/gimpsymmetryeditor.h b/app/widgets/gimpsymmetryeditor.h
new file mode 100644
index 0000000..fd4d1cc
--- /dev/null
+++ b/app/widgets/gimpsymmetryeditor.h
@@ -0,0 +1,58 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimpsymmetryeditor.h
+ * Copyright (C) 2015 Jehan <jehan girinstud io>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __GIMP_SYMMETRY_EDITOR_H__
+#define __GIMP_SYMMETRY_EDITOR_H__
+
+
+#include "gimpeditor.h"
+
+
+#define GIMP_TYPE_SYMMETRY_EDITOR            (gimp_symmetry_editor_get_type ())
+#define GIMP_SYMMETRY_EDITOR(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIMP_TYPE_SYMMETRY_EDITOR, 
GimpSymmetryEditor))
+#define GIMP_SYMMETRY_EDITOR_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), GIMP_TYPE_SYMMETRY_EDITOR, 
GimpSymmetryEditorClass))
+#define GIMP_IS_SYMMETRY_EDITOR(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIMP_TYPE_SYMMETRY_EDITOR))
+#define GIMP_IS_SYMMETRY_EDITOR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIMP_TYPE_SYMMETRY_EDITOR))
+#define GIMP_SYMMETRY_EDITOR_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), GIMP_TYPE_SYMMETRY_EDITOR, 
GimpSymmetryEditorClass))
+
+
+typedef struct _GimpSymmetryEditorPrivate  GimpSymmetryEditorPrivate;
+typedef struct _GimpSymmetryEditorClass    GimpSymmetryEditorClass;
+
+struct _GimpSymmetryEditor
+{
+  GimpEditor                 parent_instance;
+
+  GimpSymmetryEditorPrivate *p;
+};
+
+struct _GimpSymmetryEditorClass
+{
+  GimpEditorClass  parent_class;
+};
+
+
+GType             gimp_symmetry_editor_get_type         (void) G_GNUC_CONST;
+GtkWidget       * gimp_symmetry_editor_new              (Gimp               *gimp,
+                                                         GimpImage          *image,
+                                                         GimpMenuFactory    *menu_factory);
+
+
+#endif  /*  __GIMP_SYMMETRY_EDITOR_H__  */
diff --git a/app/widgets/widgets-types.h b/app/widgets/widgets-types.h
index 335d8de..d617088 100644
--- a/app/widgets/widgets-types.h
+++ b/app/widgets/widgets-types.h
@@ -76,6 +76,7 @@ typedef struct _GimpHistogramEditor          GimpHistogramEditor;
 typedef struct _GimpImageEditor              GimpImageEditor;
 typedef struct _GimpSamplePointEditor        GimpSamplePointEditor;
 typedef struct _GimpSelectionEditor          GimpSelectionEditor;
+typedef struct _GimpSymmetryEditor           GimpSymmetryEditor;
 typedef struct _GimpUndoEditor               GimpUndoEditor;
 
 
diff --git a/app/xcf/xcf-load.c b/app/xcf/xcf-load.c
index 72e45ed..e433edf 100644
--- a/app/xcf/xcf-load.c
+++ b/app/xcf/xcf-load.c
@@ -45,6 +45,7 @@
 #include "core/gimpimage-metadata.h"
 #include "core/gimpimage-private.h"
 #include "core/gimpimage-sample-points.h"
+#include "core/gimpimage-symmetry.h"
 #include "core/gimpimage-undo.h"
 #include "core/gimpitemstack.h"
 #include "core/gimplayer-floating-sel.h"
@@ -53,6 +54,7 @@
 #include "core/gimpparasitelist.h"
 #include "core/gimpprogress.h"
 #include "core/gimpselection.h"
+#include "core/gimpsymmetry.h"
 #include "core/gimptemplate.h"
 
 #include "text/gimptextlayer.h"
@@ -157,6 +159,8 @@ xcf_load_image (Gimp     *gimp,
   gint                image_type;
   GimpPrecision       precision = GIMP_PRECISION_U8_GAMMA;
   gint                num_successful_elements = 0;
+  GList              *syms;
+  GList              *iter;
 
   /* read in the image width, height and type */
   info->cp += xcf_read_int32 (info->input, (guint32 *) &width, 1);
@@ -269,6 +273,39 @@ xcf_load_image (Gimp     *gimp,
                                  gimp_parasite_name (parasite));
     }
 
+  /* check for symmetry parasites */
+  syms = gimp_image_symmetry_list ();
+  for (iter = syms; iter; iter = g_list_next (iter))
+    {
+      GType  type = (GType) iter->data;
+      gchar *parasite_name = gimp_symmetry_parasite_name (type);
+
+      parasite = gimp_image_parasite_find (image,
+                                           parasite_name);
+      g_free (parasite_name);
+      if (parasite)
+        {
+          GimpSymmetry *sym = gimp_symmetry_from_parasite (parasite,
+                                                           image,
+                                                           type);
+
+          if (sym)
+            {
+              GimpImagePrivate *private = GIMP_IMAGE_GET_PRIVATE (image);
+
+              gimp_parasite_list_remove (private->parasites,
+                                         gimp_parasite_name (parasite));
+
+              gimp_image_symmetry_add (image, sym);
+
+              g_signal_emit_by_name (sym, "active-changed", NULL);
+              if (sym->active)
+                gimp_image_symmetry_select (image, type);
+            }
+        }
+    }
+  g_list_free (syms);
+
   /* migrate the old "exif-data" parasite */
   parasite = gimp_image_parasite_find (GIMP_IMAGE (image),
                                        "exif-data");
diff --git a/app/xcf/xcf-save.c b/app/xcf/xcf-save.c
index d545775..0e8e0ae 100644
--- a/app/xcf/xcf-save.c
+++ b/app/xcf/xcf-save.c
@@ -45,11 +45,13 @@
 #include "core/gimpimage-metadata.h"
 #include "core/gimpimage-private.h"
 #include "core/gimpimage-sample-points.h"
+#include "core/gimpimage-symmetry.h"
 #include "core/gimplayer.h"
 #include "core/gimplayermask.h"
 #include "core/gimpparasitelist.h"
 #include "core/gimpprogress.h"
 #include "core/gimpsamplepoint.h"
+#include "core/gimpsymmetry.h"
 
 #include "text/gimptextlayer.h"
 #include "text/gimptextlayer-xcf.h"
@@ -337,11 +339,13 @@ xcf_save_image_props (XcfInfo    *info,
                       GimpImage  *image,
                       GError    **error)
 {
-  GimpImagePrivate *private         = GIMP_IMAGE_GET_PRIVATE (image);
-  GimpParasite     *grid_parasite   = NULL;
-  GimpParasite     *meta_parasite   = NULL;
-  GimpParasite     *compat_parasite = NULL;
-  GimpUnit          unit            = gimp_image_get_unit (image);
+  GimpImagePrivate *private            = GIMP_IMAGE_GET_PRIVATE (image);
+  GimpParasite     *grid_parasite      = NULL;
+  GimpParasite     *meta_parasite      = NULL;
+  GimpParasite     *compat_parasite    = NULL;
+  GList            *symmetry_parasites = NULL;
+  GList            *iter;
+  GimpUnit          unit               = gimp_image_get_unit (image);
   gdouble           xres;
   gdouble           yres;
 
@@ -428,6 +432,18 @@ xcf_save_image_props (XcfInfo    *info,
       gimp_parasite_list_add (private->parasites, compat_parasite);
     }
 
+  if (g_list_length (gimp_image_symmetry_get (image)))
+    {
+      GimpParasite *parasite  = NULL;
+
+      for (iter = gimp_image_symmetry_get (image); iter; iter = g_list_next (iter))
+        {
+          parasite = gimp_symmetry_to_parasite (GIMP_SYMMETRY (iter->data));
+          gimp_parasite_list_add (private->parasites, parasite);
+          symmetry_parasites = g_list_prepend (symmetry_parasites, parasite);
+        }
+    }
+
   if (gimp_parasite_list_length (private->parasites) > 0)
     {
       xcf_check_error (xcf_save_prop (info, image, PROP_PARASITES, error,
@@ -454,6 +470,17 @@ xcf_save_image_props (XcfInfo    *info,
                                  gimp_parasite_name (compat_parasite));
       gimp_parasite_free (compat_parasite);
     }
+
+  for (iter = symmetry_parasites; iter; iter = g_list_next (iter))
+    {
+      GimpParasite *parasite = iter->data;
+
+      gimp_parasite_list_remove (private->parasites,
+                                 gimp_parasite_name (parasite));
+    }
+  g_list_free_full (symmetry_parasites,
+                    (GDestroyNotify) gimp_parasite_free);
+
   xcf_check_error (xcf_save_prop (info, image, PROP_END, error));
 
   return TRUE;
diff --git a/icons/Color/16/gimp-symmetry.png b/icons/Color/16/gimp-symmetry.png
new file mode 100644
index 0000000..32fa5a4
Binary files /dev/null and b/icons/Color/16/gimp-symmetry.png differ
diff --git a/icons/Color/24/gimp-symmetry.png b/icons/Color/24/gimp-symmetry.png
new file mode 100644
index 0000000..d7f4b2f
Binary files /dev/null and b/icons/Color/24/gimp-symmetry.png differ
diff --git a/icons/Color/24/gimp-symmetry.xcf b/icons/Color/24/gimp-symmetry.xcf
new file mode 100644
index 0000000..fb593e9
Binary files /dev/null and b/icons/Color/24/gimp-symmetry.xcf differ
diff --git a/icons/Color/Makefile.am b/icons/Color/Makefile.am
index 328b0bf..5f6d517 100644
--- a/icons/Color/Makefile.am
+++ b/icons/Color/Makefile.am
@@ -161,6 +161,7 @@ icons16_DATA = \
        16/gimp-shape-circle.png                        \
        16/gimp-shape-diamond.png                       \
        16/gimp-shape-square.png                        \
+       16/gimp-symmetry.png                    \
        16/gimp-template.png                            \
        16/gimp-text-layer.png                          \
        16/gimp-toilet-paper.png                        \
@@ -376,6 +377,7 @@ icons24_DATA = \
        24/gimp-move-to-screen.png              \
        24/gimp-print-resolution.png            \
        24/gimp-sample-point.png                \
+       24/gimp-symmetry.png                    \
        24/gimp-template.png                    \
        24/gimp-text-dir-ltr.png                \
        24/gimp-text-dir-rtl.png                \
diff --git a/libgimpwidgets/gimpicons.h b/libgimpwidgets/gimpicons.h
index ec7ee80..09c7efc 100644
--- a/libgimpwidgets/gimpicons.h
+++ b/libgimpwidgets/gimpicons.h
@@ -232,6 +232,7 @@ G_BEGIN_DECLS
 #define GIMP_STOCK_INPUT_DEVICE             "gimp-input-device"
 #define GIMP_STOCK_CURSOR                   "gimp-cursor"
 #define GIMP_STOCK_SAMPLE_POINT             "gimp-sample-point"
+#define GIMP_STOCK_SYMMETRY                 "gimp-symmetry"
 #define GIMP_STOCK_DYNAMICS                 "gimp-dynamics"
 #define GIMP_STOCK_TOOL_PRESET              "gimp-tool-preset"
 
diff --git a/menus/dialogs-menuitems.xml b/menus/dialogs-menuitems.xml
index 373f498..f7608bf 100644
--- a/menus/dialogs-menuitems.xml
+++ b/menus/dialogs-menuitems.xml
@@ -14,6 +14,7 @@
   <menuitem action="dialogs-undo-history" />
   <menuitem action="dialogs-cursor" />
   <menuitem action="dialogs-sample-points" />
+  <menuitem action="dialogs-symmetry"/>
   <separator />
   <menuitem action="dialogs-colors" />
   <menuitem action="dialogs-brushes" />


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