[gtk/matthiasc/lottie-serialization: 5/5] path: Special-case rects and circles




commit 1af40af3cb7172cd7c48d9f7a2f99afd96410c7a
Author: Matthias Clasen <mclasen redhat com>
Date:   Wed Nov 25 01:03:13 2020 -0500

    path: Special-case rects and circles
    
    Write out the commands for rects and circles in a special
    way (without whitespace), and add code in the parser to
    recognize this, so we can successfully round-trip these
    through the SVG path format.
    
    Tests included.

 gsk/gskpath.c        | 172 ++++++++++++++++++++++++++++++++++++++++++++-------
 testsuite/gsk/path.c | 153 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 304 insertions(+), 21 deletions(-)
---
diff --git a/gsk/gskpath.c b/gsk/gskpath.c
index 7da1ed1f3b..00c8aaf949 100644
--- a/gsk/gskpath.c
+++ b/gsk/gskpath.c
@@ -195,14 +195,11 @@ gsk_rect_contour_print (const GskContour *contour,
 {
   const GskRectContour *self = (const GskRectContour *) contour;
 
-  g_string_append (string, "M ");
-  _g_string_append_point (string, &GRAPHENE_POINT_INIT (self->x, self->y));
-  g_string_append (string, " h ");
-  _g_string_append_double (string, self->width);
-  g_string_append (string, " v ");
-  _g_string_append_double (string, self->height);
-  g_string_append (string, " h ");
-  _g_string_append_double (string, - self->width);
+  /* Write commands in a form that gsk_path_new_from_string() recognizes */
+  g_string_append_printf (string, "M%g,%gh%gv%gh%gz",
+                          self->x, self->y,
+                          self->width, self->height,
+                          -self->width);
 }
 
 static gboolean
@@ -554,18 +551,20 @@ gsk_circle_contour_print (const GskContour *contour,
   const GskCircleContour *self = (const GskCircleContour *) contour;
   float mid_angle = (self->end_angle - self->start_angle) / 2;
 
-  g_string_append (string, "M ");
+  g_string_append_printf (string, "M%g,%g",
+                          self->center.x + cos (DEG_TO_RAD (self->start_angle)) * self->radius,
+                          self->center.y + sin (DEG_TO_RAD (self->start_angle)) * self->radius);
   _g_string_append_point (string, &GSK_CIRCLE_POINT_INIT (self, self->start_angle));
-  g_string_append (string, " A ");
-  _g_string_append_point (string, &GRAPHENE_POINT_INIT (self->radius, self->radius));
-  g_string_append_printf (string, " 0 0 %u",
-                          self->start_angle < self->end_angle ? 0 : 1);
-  _g_string_append_point (string, &GSK_CIRCLE_POINT_INIT (self, mid_angle));
-  g_string_append (string, " A ");
-  _g_string_append_point (string, &GRAPHENE_POINT_INIT (self->radius, self->radius));
-  g_string_append_printf (string, " 0 0 %u",
-                          self->start_angle < self->end_angle ? 0 : 1);
-  _g_string_append_point (string, &GSK_CIRCLE_POINT_INIT (self, self->end_angle));
+  g_string_append_printf (string, "A%g,%g,0,0,%u,%g,%g",
+                          self->radius, self->radius,
+                          self->start_angle < self->end_angle ? 0 : 1,
+                          self->center.x + cos (DEG_TO_RAD (mid_angle)) * self->radius,
+                          self->center.y + sin (DEG_TO_RAD (mid_angle)) * self->radius);
+  g_string_append_printf (string, "A%g,%g,0,0,%u,%g,%g",
+                          self->radius, self->radius,
+                          self->start_angle < self->end_angle ? 0 : 1,
+                          self->center.x + cos (DEG_TO_RAD (self->end_angle)) * self->radius,
+                          self->center.y + sin (DEG_TO_RAD (self->end_angle)) * self->radius);
   if (fabs (self->start_angle - self->end_angle >= 360))
     g_string_append (string, "Z");
 }
@@ -2194,6 +2193,115 @@ parse_command (const char **p,
   return FALSE;
 }
 
+static gboolean
+parse_char (const char **p,
+            char         ch)
+{
+  if (**p != ch)
+    return FALSE;
+  (*p)++;
+  return TRUE;
+}
+
+static gboolean
+parse_string (const char **p,
+              const char  *s)
+{
+  int len = strlen (s);
+  if (strncmp (*p, s, len) != 0)
+    return FALSE;
+  (*p) += len;
+  return TRUE;
+}
+
+static gboolean
+parse_rectangle (const char **p,
+                 double      *x,
+                 double      *y,
+                 double      *w,
+                 double      *h)
+{
+  const char *o = *p;
+  double w2;
+
+  /* Check for M%g,%gh%gv%gh%gz without any intervening whitespace */
+  if (parse_coordinate_pair (p, x, y) &&
+      parse_char (p, 'h') &&
+      parse_coordinate (p, w) &&
+      parse_char (p, 'v') &&
+      parse_coordinate (p, h) &&
+      parse_char (p, 'h') &&
+      parse_coordinate (p, &w2) &&
+      parse_char (p, 'z') &&
+      w2 == - *w)
+    {
+      const char *s;
+
+      for (s = o; s != *p; s++)
+        {
+          if (g_ascii_isspace (*s))
+            {
+              *p = o;
+              return FALSE;
+            }
+        }
+
+      skip_whitespace (p);
+
+      return TRUE;
+    }
+
+  *p = o;
+  return FALSE;
+}
+
+static gboolean
+parse_circle (const char **p,
+              double      *sx,
+              double      *sy,
+              double      *r)
+{
+  const char *o = *p;
+  double r1, r2, r3, mx, my, ex, ey;
+
+  /* Check for M%g,%gA%g,%g,0,1,0,%g,%gA%g,%g,0,1,0,%g,%g
+   * without any intervening whitespace
+   */
+  if (parse_coordinate_pair (p, sx, sy) &&
+      parse_char (p, 'A') &&
+      parse_coordinate_pair (p, r, &r1) &&
+      parse_string (p, "0,1,0,") &&
+      parse_coordinate_pair (p, &mx, &my) &&
+      parse_char (p, 'A') &&
+      parse_coordinate_pair (p, &r2, &r3) &&
+      parse_string (p, "0,1,0,") &&
+      parse_coordinate_pair (p, &ex, &ey) &&
+      parse_char (p, 'Z') &&
+      *r == r1 && r1 == r2 && r2 == r3 &&
+      *sx == ex && *sy == ey)
+    {
+      const char *s;
+
+      for (s = o; s != *p; s++)
+        {
+          if (g_ascii_isspace (*s))
+            {
+              *p = o;
+              return FALSE;
+            }
+        }
+
+      *r = r1;
+
+      skip_whitespace (p);
+
+      return TRUE;
+    }
+
+  *p = o;
+  return FALSE;
+}
+
 /**
  * gsk_path_parse:
  * @string: a string
@@ -2255,9 +2363,31 @@ gsk_path_parse (const char *string)
         case 'M':
         case 'm':
           {
-            double x1, y1;
+            double x1, y1, w, h, r;
 
-            if (parse_coordinate_pair (&p, &x1, &y1))
+            if (parse_rectangle (&p, &x1, &y1, &w, &h))
+              {
+                gsk_path_builder_add_rect (builder, &GRAPHENE_RECT_INIT (x1, y1, w, h));
+                if (strchr ("zZX", prev_cmd))
+                  {
+                    path_x = x1;
+                    path_y = y1;
+                  }
+                x = x1;
+                y = y1;
+              }
+            else if (parse_circle (&p, &x1, &y1, &r))
+              {
+                gsk_path_builder_add_circle (builder, &GRAPHENE_POINT_INIT (x1 - r, y1), r);
+                if (strchr ("zZX", prev_cmd))
+                  {
+                    path_x = x1;
+                    path_y = y1;
+                  }
+                x = x1;
+                y = y1;
+              }
+            else if (parse_coordinate_pair (&p, &x1, &y1))
               {
                 if (cmd == 'm')
                   {
diff --git a/testsuite/gsk/path.c b/testsuite/gsk/path.c
index 7eca487f4a..119bae4433 100644
--- a/testsuite/gsk/path.c
+++ b/testsuite/gsk/path.c
@@ -859,6 +859,157 @@ test_from_string (void)
     }
 }
 
+/* test that the parser can handle random paths */
+static void
+test_from_random_string (void)
+{
+  int i;
+
+  for (i = 0; i < 1000; i++)
+    {
+      GskPath *path = create_random_path (G_MAXUINT);
+      char *string = gsk_path_to_string (path);
+      GskPath *path1;
+
+      g_assert_nonnull (string);
+
+      path1 = gsk_path_parse (string);
+      g_assert_nonnull (path1);
+
+      gsk_path_unref (path1);
+      g_free (string);
+      gsk_path_unref (path);
+    }
+}
+
+typedef struct
+{
+  GskPathOperation op;
+  graphene_point_t pts[4];
+  gsize n_pts;
+} Contour;
+
+static gboolean
+append_contour (GskPathOperation        op,
+                const graphene_point_t *pts,
+                gsize                   n_pts,
+                float                   weight,
+                gpointer                user_data)
+{
+  GArray *a = user_data;
+  Contour c;
+  int i;
+
+  g_assert (n_pts <= 4);
+  c.op = op;
+  c.n_pts = n_pts;
+  for (i = 0; i < n_pts; i++)
+    c.pts[i] = pts[i];
+
+  g_array_append_val (a, c);
+  return  TRUE;
+}
+
+static GArray *
+path_to_contours (GskPath *path)
+{
+  GArray *a;
+
+  a = g_array_new (FALSE, FALSE, sizeof (Contour));
+  gsk_path_foreach (path, append_contour, a);
+
+  return a;
+}
+
+static gboolean
+contour_equal (Contour *c1,
+               Contour *c2)
+{
+  int i;
+
+  if (c1->op != c2->op)
+    return FALSE;
+
+  if (c1->n_pts != c2->n_pts)
+    return FALSE;
+
+  for (i = 0; i < c1->n_pts; i++)
+    {
+      if (c1->pts[i].x != c2->pts[i].x ||
+          c1->pts[i].y != c2->pts[i].y)
+        return FALSE;
+    }
+
+  return TRUE;
+}
+
+static gboolean
+contours_equal (GArray *a1,
+                GArray *a2)
+{
+  int i;
+
+  if (a1->len != a2->len)
+    return FALSE;
+
+  for (i = 0; i < a1->len; i++)
+    {
+      Contour *c1 = &g_array_index (a1, Contour, i);
+      Contour *c2 = &g_array_index (a2, Contour, i);
+
+      if (!contour_equal (c1, c2))
+        return FALSE;
+    }
+
+  return TRUE;
+}
+
+static gboolean
+path_equal (GskPath *path1,
+            GskPath *path2)
+{
+  GArray *a1, *a2;
+  gboolean ret;
+
+  a1 = path_to_contours (path1);
+  a2 = path_to_contours (path2);
+
+  ret = contours_equal (a1, a2);
+
+  g_array_unref (a1);
+  g_array_unref (a2);
+
+  return ret;
+}
+
+/* Test that circles and rectangles serialize as expected and can be
+ * round-tripped through strings.
+ */
+static void
+test_serialize (void)
+{
+  GskPathBuilder *builder;
+  GskPath *path;
+  GskPath *path1;
+  char *string;
+
+  builder = gsk_path_builder_new ();
+  gsk_path_builder_add_circle (builder, &GRAPHENE_POINT_INIT (100, 100), 50);
+  gsk_path_builder_add_rect (builder, &GRAPHENE_RECT_INIT (111, 222, 333, 444));
+  path = gsk_path_builder_free_to_path (builder);
+
+  string = gsk_path_to_string (path);
+  g_assert_cmpstr ("M150,100A50,50,0,1,0,50,100A50,50,0,1,0,150,100Z M111,222h333v444h-333z", ==, string);
+
+  path1 = gsk_path_parse (string);
+
+  g_assert_true (path_equal (path, path1));
+
+  g_free (string);
+  gsk_path_unref (path);
+  gsk_path_unref (path1);
+}
+
 int
 main (int   argc,
       char *argv[])
@@ -874,6 +1025,8 @@ main (int   argc,
   g_test_add_func ("/path/closest_point", test_closest_point);
   g_test_add_func ("/path/closest_point_for_point", test_closest_point_for_point);
   g_test_add_func ("/path/from-string", test_from_string);
+  g_test_add_func ("/path/from-random-string", test_from_random_string);
+  g_test_add_func ("/path/serialize", test_serialize);
 
   return g_test_run ();
 }


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