diff --git a/testsuite/gsk/meson.build b/testsuite/gsk/meson.build index 8c994e7c2a..c584382dc4 100644 --- a/testsuite/gsk/meson.build +++ b/testsuite/gsk/meson.build @@ -359,6 +359,8 @@ endforeach tests = [ ['transform'], ['shader'], + ['path'], + ['path-special-cases'], ] test_cargs = [] diff --git a/testsuite/gsk/path-special-cases.c b/testsuite/gsk/path-special-cases.c new file mode 100644 index 0000000000..300c05ee97 --- /dev/null +++ b/testsuite/gsk/path-special-cases.c @@ -0,0 +1,320 @@ +/* + * Copyright © 2020 Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: Benjamin Otte + */ + +#include + +/* testcases from path_parser.rs in librsvg */ +static void +test_rsvg_parse (void) +{ + struct { + const char *in; + const char *out; + } tests[] = { + { "", "" }, + // numbers + { "M 10 20", "M 10 20" }, + { "M -10 -20", "M -10 -20" }, + { "M .10 0.20", "M 0.1 0.2" }, + { "M -.10 -0.20", "M -0.1 -0.2" }, + { "M-.10-0.20", "M -0.1 -0.2" }, + { "M10.5.50", "M 10.5 0.5" }, + { "M.10.20", "M 0.1 0.2" }, + { "M .10E1 .20e-4", "M 1 2e-05" }, + { "M-.10E1-.20", "M -1 -0.2" }, + { "M10.10E2 -0.20e3", "M 1010 -200" }, + { "M-10.10E2-0.20e-3", "M -1010 -0.0002" }, + { "M1e2.5", "M 100 0.5" }, + { "M1e-2.5", "M 0.01 0.5" }, + { "M1e+2.5", "M 100 0.5" }, + // bogus numbers + { "M+", NULL }, + { "M-", NULL }, + { "M+x", NULL }, + { "M10e", NULL }, + { "M10ex", NULL }, + { "M10e-", NULL }, + { "M10e+x", NULL }, + // numbers with comma + { "M 10, 20", "M 10 20" }, + { "M -10,-20", "M -10 -20" }, + { "M.10 , 0.20", "M 0.1 0.2" }, + { "M -.10, -0.20 ", "M -0.1 -0.2" }, + { "M-.10-0.20", "M -0.1 -0.2" }, + { "M.10.20", "M 0.1 0.2" }, + { "M .10E1,.20e-4", "M 1 2e-05" }, + { "M-.10E-2,-.20", "M -0.001 -0.2" }, + { "M10.10E2,-0.20e3", "M 1010 -200" }, + { "M-10.10E2,-0.20e-3", "M -1010 -0.0002" }, + // single moveto + { "M 10 20 ", "M 10 20" }, + { "M10,20 ", "M 10 20" }, + { "M10 20 ", "M 10 20" }, + { " M10,20 ", "M 10 20" }, + // relative moveto + { "m10 20", "M 10 20" }, + // absolute moveto with implicit lineto + { "M10 20 30 40", "M 10 20 L 30 40" }, + { "M10,20,30,40", "M 10 20 L 30 40" }, + { "M.1-2,3E2-4", "M 0.1 -2 L 300 -4" }, + // relative moveto with implicit lineto + { "m10 20 30 40", "M 10 20 L 40 60" }, + // relative moveto with relative lineto sequence + { "m 46,447 l 0,0.5 -1,0 -1,0 0,1 0,12", + "M 46 447 L 46 447.5 L 45 447.5 L 44 447.5 L 44 448.5 L 44 460.5" }, + // absolute moveto with implicit linetos + { "M10,20 30,40,50 60", "M 10 20 L 30 40 L 50 60" }, + // relative moveto with implicit linetos + { "m10 20 30 40 50 60", "M 10 20 L 40 60 L 90 120" }, + // absolute moveto moveto + { "M10 20 M 30 40", "M 10 20 M 30 40" }, + // relative moveto moveto + { "m10 20 m 30 40", "M 10 20 M 40 60" }, + // relative moveto lineto moveto + { "m10 20 30 40 m 50 60", "M 10 20 L 40 60 M 90 120" }, + // absolute moveto lineto + { "M10 20 L30,40", "M 10 20 L 30 40" }, + // relative moveto lineto + { "m10 20 l30,40", "M 10 20 L 40 60" }, + // relative moveto lineto lineto abs lineto + { "m10 20 30 40l30,40,50 60L200,300", + "M 10 20 L 40 60 L 70 100 L 120 160 L 200 300" }, + // horizontal lineto + { "M10 20 H30", "M 10 20 L 30 20" }, + { "M 10 20 H 30 40", "M 10 20 L 30 20 L 40 20" }, + { "M10 20 H30,40-50", "M 10 20 L 30 20 L 40 20 L -50 20" }, + { "m10 20 h30,40-50", "M 10 20 L 40 20 L 80 20 L 30 20" }, + // vertical lineto + { "M10 20 V30", "M 10 20 L 10 30" }, + { "M10 20 V30 40", "M 10 20 L 10 30 L 10 40" }, + { "M10 20 V30,40-50", "M 10 20 L 10 30 L 10 40 L 10 -50" }, + { "m10 20 v30,40-50", "M 10 20 L 10 50 L 10 90 L 10 40" }, + // curveto + { "M10 20 C 30,40 50 60-70,80", "M 10 20 C 30 40, 50 60, -70 80" }, + { "M10 20 C 30,40 50 60-70,80,90 100,110 120,130,140", + "M 10 20 C 30 40, 50 60, -70 80 C 90 100, 110 120, 130 140" }, + { "m10 20 c 30,40 50 60-70,80,90 100,110 120,130,140", + "M 10 20 C 40 60, 60 80, -60 100 C 30 200, 50 220, 70 240" }, + { "m10 20 c 30,40 50 60-70,80 90 100,110 120,130,140", + "M 10 20 C 40 60, 60 80, -60 100 C 30 200, 50 220, 70 240" }, + // smooth curveto + { "M10 20 S 30,40-50,60", "M 10 20 C 10 20, 30 40, -50 60" }, + { "M10 20 S 30,40 50 60-70,80,90 100", + "M 10 20 C 10 20, 30 40, 50 60 C 70 80, -70 80, 90 100" }, + // quadratic curveto + { "M10 20 Q30 40 50 60", "M 10 20 Q 30 40, 50 60" }, + { "M10 20 Q30 40 50 60,70,80-90 100", + "M 10 20 Q 30 40, 50 60 Q 70 80, -90 100" }, + { "m10 20 q 30,40 50 60-70,80 90 100", + "M 10 20 Q 40 60, 60 80 Q -10 160, 150 180" }, + // smooth quadratic curveto + { "M10 20 T30 40", "M 10 20 Q 10 20, 30 40" }, + { "M10 20 Q30 40 50 60 T70 80", "M 10 20 Q 30 40, 50 60 Q 70 80, 70 80" }, + { "m10 20 q 30,40 50 60t-70,80", + "M 10 20 Q 40 60, 60 80 Q 80 100, -10 160" }, + // elliptical arc. Exact numbers depend on too much math, so just verify + // that these parse successfully + { "M 1 3 A 1 2 3 00 6 7", "path" }, + { "M 1 2 A 1 2 3 016 7", "path" }, + { "M 1 2 A 1 2 3 10,6 7", "path" }, + { "M 1 2 A 1 2 3 1,1 6 7", "path" }, + { "M 1 2 A 1 2 3 1 1 6 7", "path" }, + { "M 1 2 A 1 2 3 1 16 7", "path" }, + // close path + { "M10 20 Z", "M 10 20 Z" }, + { "m10 20 30 40 m 50 60 70 80 90 100z", "M 10 20 L 40 60 M 90 120 L 160 200 L 250 300 Z" }, + // must start with moveto + { " L10 20", NULL }, + // moveto args + { "M", NULL }, + { "M,", NULL }, + { "M10", NULL }, + { "M10,", NULL }, + { "M10x", NULL }, + { "M10,x", NULL }, + { "M10-20,", NULL }, + { "M10-20-30", NULL }, + { "M10-20-30 x", NULL }, + // closepath args + { "M10-20z10", NULL }, + { "M10-20z,", NULL }, + // lineto args + { "M10-20L10", NULL }, + { "M 10,10 L 20,20,30", NULL }, + { "M 10,10 L 20,20,", NULL }, + // horizontal lineto args + { "M10-20H", NULL }, + { "M10-20H,", NULL }, + { "M10-20H30,", NULL }, + // vertical lineto args + { "M10-20v", NULL }, + { "M10-20v,", NULL }, + { "M10-20v30,", NULL }, + // curveto args + { "M10-20C1", NULL }, + { "M10-20C1,", NULL }, + { "M10-20C1 2", NULL }, + { "M10-20C1,2,", NULL }, + { "M10-20C1 2 3", NULL }, + { "M10-20C1,2,3", NULL }, + { "M10-20C1,2,3,", NULL }, + { "M10-20C1 2 3 4", NULL }, + { "M10-20C1,2,3,4", NULL }, + { "M10-20C1,2,3,4,", NULL }, + { "M10-20C1 2 3 4 5", NULL }, + { "M10-20C1,2,3,4,5", NULL }, + { "M10-20C1,2,3,4,5,", NULL }, + { "M10-20C1,2,3,4,5,6,", NULL }, + // smooth curveto args + { "M10-20S1", NULL }, + { "M10-20S1,", NULL }, + { "M10-20S1 2", NULL }, + { "M10-20S1,2,", NULL }, + { "M10-20S1 2 3", NULL }, + { "M10-20S1,2,3,", NULL }, + { "M10-20S1,2,3,4,", NULL }, + // quadratic curveto args + { "M10-20Q1", NULL }, + { "M10-20Q1,", NULL }, + { "M10-20Q1 2", NULL }, + { "M10-20Q1,2,", NULL }, + { "M10-20Q1 2 3", NULL }, + { "M10-20Q1,2,3", NULL }, + { "M10-20Q1,2,3,", NULL }, + { "M10 20 Q30 40 50 60,", NULL }, + // smooth quadratic curveto args + { "M10-20T1", NULL }, + { "M10-20T1,", NULL }, + { "M10 20 T 30 40,", NULL }, + // elliptical arc args + { "M10-20A1", NULL }, + { "M10-20A1,", NULL }, + { "M10-20A1 2", NULL }, + { "M10-20A1 2,", NULL }, + { "M10-20A1 2 3", NULL }, + { "M10-20A1 2 3,", NULL }, + { "M10-20A1 2 3 4", NULL }, + { "M10-20A1 2 3 1", NULL }, + { "M10-20A1 2 3,1,", NULL }, + { "M10-20A1 2 3 1 5", NULL }, + { "M10-20A1 2 3 1 1", NULL }, + { "M10-20A1 2 3,1,1,", NULL }, + { "M10-20A1 2 3 1 1 6", NULL }, + { "M10-20A1 2 3,1,1,6,", NULL }, + { "M 1 2 A 1 2 3 1.0 0.0 6 7", NULL }, + { "M10-20A1 2 3,1,1,6,7,", NULL }, + // misc + { "M.. 1,0 0,100000", NULL }, + { "M 10 20,M 10 20", NULL }, + { "M 10 20, M 10 20", NULL }, + { "M 10 20, M 10 20", NULL }, + { "M 10 20, ", NULL }, + }; + int i; + + for (i = 0; i < G_N_ELEMENTS (tests); i++) + { + GskPath *path; + char *string; + char *string2; + + if (g_test_verbose ()) + g_print ("%d: %s\n", i, tests[i].in); + + path = gsk_path_parse (tests[i].in); + if (tests[i].out) + { + g_assert_nonnull (path); + string = gsk_path_to_string (path); + gsk_path_unref (path); + + if (strcmp (tests[i].out, "path") != 0) + { + /* Preferred, but doesn't work, because gsk_path_print() + * prints numbers with insane accuracy + */ + /* g_assert_cmpstr (tests[i].out, ==, string); */ + path = gsk_path_parse (tests[i].out); + g_assert_nonnull (path); + + string2 = gsk_path_to_string (path); + gsk_path_unref (path); + + g_assert_cmpstr (string, ==, string2); + + g_free (string2); + } + + path = gsk_path_parse (string); + g_assert_nonnull (path); + + string2 = gsk_path_to_string (path); + gsk_path_unref (path); + + g_assert_cmpstr (string, ==, string2); + + g_free (string); + g_free (string2); + } + else + g_assert_null (path); + } +} + +/* Test that circles and rectangles serialize as expected and can be + * round-tripped through strings. + */ +static void +test_serialize_custom_contours (void) +{ + GskPathBuilder *builder; + GskPath *path; + GskPath *path1; + char *string; + char *string1; + + 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 ("M 150 100 A 50 50 0 0 0 50 100 A 50 50 0 0 0 150 100 z M 111 222 h 333 v 444 h -333 z", ==, string); + + path1 = gsk_path_parse (string); + string1 = gsk_path_to_string (path1); + g_assert_cmpstr (string, ==, string1); + + g_free (string); + g_free (string1); + gsk_path_unref (path); + gsk_path_unref (path1); +} + +int +main (int argc, + char *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + + g_test_add_func ("/path/rsvg-parse", test_rsvg_parse); + g_test_add_func ("/path/serialize-custom-contours", test_serialize_custom_contours); + + return g_test_run (); +} diff --git a/testsuite/gsk/path.c b/testsuite/gsk/path.c new file mode 100644 index 0000000000..dd7da82a3d --- /dev/null +++ b/testsuite/gsk/path.c @@ -0,0 +1,654 @@ +/* + * Copyright © 2020 Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: Benjamin Otte + */ + +#include + +static float +random_weight (void) +{ + if (g_test_rand_bit ()) + return g_test_rand_double_range (1, 20); + else + return 1.0 / g_test_rand_double_range (1, 20); +} + +static GskPath * +create_random_degenerate_path (guint max_contours) +{ +#define N_DEGENERATE_PATHS 15 + GskPathBuilder *builder; + guint i; + + builder = gsk_path_builder_new (); + + switch (g_test_rand_int_range (0, N_DEGENERATE_PATHS)) + { + case 0: + /* empty path */ + break; + + case 1: + /* a single point */ + gsk_path_builder_move_to (builder, + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000)); + break; + + case 2: + /* N points */ + for (i = 0; i < MIN (10, max_contours); i++) + { + gsk_path_builder_move_to (builder, + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000)); + } + break; + + case 3: + /* 1 closed point */ + gsk_path_builder_move_to (builder, + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000)); + gsk_path_builder_close (builder); + break; + + case 4: + /* the same point closed N times */ + gsk_path_builder_move_to (builder, + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000)); + for (i = 0; i < MIN (10, max_contours); i++) + { + gsk_path_builder_close (builder); + } + break; + + case 5: + /* a zero-width and zero-height rect */ + gsk_path_builder_add_rect (builder, + &GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + 0, 0)); + break; + + case 6: + /* a zero-width rect */ + gsk_path_builder_add_rect (builder, + &GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + 0, + g_test_rand_double_range (-1000, 1000))); + break; + + case 7: + /* a zero-height rect */ + gsk_path_builder_add_rect (builder, + &GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + 0)); + break; + + case 8: + /* a negative-size rect */ + gsk_path_builder_add_rect (builder, + &GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 0), + g_test_rand_double_range (-1000, 0))); + break; + + case 9: + /* an absolutely random rect */ + gsk_path_builder_add_rect (builder, + &GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000))); + break; + + case 10: + /* an absolutely random rect */ + gsk_path_builder_add_rect (builder, + &GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000))); + break; + + case 11: + /* an absolutely random circle */ + gsk_path_builder_add_circle (builder, + &GRAPHENE_POINT_INIT (g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000)), + g_test_rand_double_range (1, 1000)); + break; + + case 12: + /* a zero-length line */ + { + graphene_point_t point = GRAPHENE_POINT_INIT (g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000)); + gsk_path_builder_move_to (builder, point.x, point.y); + gsk_path_builder_line_to (builder, point.x, point.y); + } + break; + + case 13: + /* a curve with start == end */ + { + graphene_point_t point = GRAPHENE_POINT_INIT (g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000)); + gsk_path_builder_move_to (builder, point.x, point.y); + gsk_path_builder_cubic_to (builder, + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + point.x, point.y); + } + break; + + case 14: + /* a conic with start == end */ + { + graphene_point_t point = GRAPHENE_POINT_INIT (g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000)); + gsk_path_builder_move_to (builder, point.x, point.y); + gsk_path_builder_conic_to (builder, + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + point.x, point.y, + random_weight ()); + } + break; + + case N_DEGENERATE_PATHS: + default: + g_assert_not_reached (); + } + + return gsk_path_builder_free_to_path (builder); +} + +static GskPath * +create_random_path (guint max_contours); + +static void +add_shape_contour (GskPathBuilder *builder) +{ +#define N_SHAPE_CONTOURS 3 + switch (g_test_rand_int_range (0, N_SHAPE_CONTOURS)) + { + case 0: + gsk_path_builder_add_rect (builder, + &GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (1, 1000), + g_test_rand_double_range (1, 1000))); + break; + + case 1: + gsk_path_builder_add_circle (builder, + &GRAPHENE_POINT_INIT (g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000)), + g_test_rand_double_range (1, 1000)); + break; + + case 2: + { + GskPath *path = create_random_path (1); + gsk_path_builder_add_path (builder, path); + gsk_path_unref (path); + } + break; + + case N_SHAPE_CONTOURS: + default: + g_assert_not_reached (); + break; + } +} + +static void +add_standard_contour (GskPathBuilder *builder) +{ + guint i, n; + + if (g_test_rand_bit ()) + { + if (g_test_rand_bit ()) + gsk_path_builder_move_to (builder, + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000)); + else + gsk_path_builder_rel_move_to (builder, + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000)); + } + + /* that 20 is random, but should be enough to get some + * crazy self-intersecting shapes */ + n = g_test_rand_int_range (1, 20); + for (i = 0; i < n; i++) + { + switch (g_test_rand_int_range (0, 8)) + { + case 0: + gsk_path_builder_line_to (builder, + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000)); + break; + + case 1: + gsk_path_builder_rel_line_to (builder, + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000)); + break; + + case 2: + gsk_path_builder_quad_to (builder, + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000)); + break; + + case 3: + gsk_path_builder_rel_quad_to (builder, + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000)); + break; + + case 4: + gsk_path_builder_cubic_to (builder, + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000)); + break; + + case 5: + gsk_path_builder_rel_cubic_to (builder, + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000)); + break; + + case 6: + gsk_path_builder_conic_to (builder, + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + random_weight ()); + break; + + case 7: + gsk_path_builder_rel_conic_to (builder, + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + g_test_rand_double_range (-1000, 1000), + random_weight ()); + break; + + default: + g_assert_not_reached(); + break; + } + } + + if (g_test_rand_bit ()) + gsk_path_builder_close (builder); +} + +static GskPath * +create_random_path (guint max_contours) +{ + GskPathBuilder *builder; + guint i, n; + + /* 5% chance for a weird shape */ + if (!g_test_rand_int_range (0, 20)) + return create_random_degenerate_path (max_contours); + + builder = gsk_path_builder_new (); + n = g_test_rand_int_range (1, 10); + n = MIN (n, max_contours); + + for (i = 0; i < n; i++) + { + /* 2/3 of shapes are standard contours */ + if (g_test_rand_int_range (0, 3)) + add_standard_contour (builder); + else + add_shape_contour (builder); + } + + return gsk_path_builder_free_to_path (builder); +} + +typedef struct { + GskPathOperation op; + graphene_point_t pts[4]; + float weight; +} PathOperation; + +static void +_g_string_append_double (GString *string, + double d) +{ + char buf[G_ASCII_DTOSTR_BUF_SIZE]; + + g_ascii_dtostr (buf, G_ASCII_DTOSTR_BUF_SIZE, d); + g_string_append (string, buf); +} + +static void +_g_string_append_point (GString *string, + const graphene_point_t *pt) +{ + _g_string_append_double (string, pt->x); + g_string_append_c (string, ' '); + _g_string_append_double (string, pt->y); +} + +static void +path_operation_print (const PathOperation *p, + GString *string) +{ + switch (p->op) + { + case GSK_PATH_MOVE: + g_string_append (string, "M "); + _g_string_append_point (string, &p->pts[0]); + break; + + case GSK_PATH_CLOSE: + g_string_append (string, " Z"); + break; + + case GSK_PATH_LINE: + g_string_append (string, " L "); + _g_string_append_point (string, &p->pts[1]); + break; + + case GSK_PATH_QUAD: + g_string_append (string, " Q "); + _g_string_append_point (string, &p->pts[1]); + g_string_append (string, ", "); + _g_string_append_point (string, &p->pts[2]); + break; + + case GSK_PATH_CUBIC: + g_string_append (string, " C "); + _g_string_append_point (string, &p->pts[1]); + g_string_append (string, ", "); + _g_string_append_point (string, &p->pts[2]); + g_string_append (string, ", "); + _g_string_append_point (string, &p->pts[3]); + break; + + case GSK_PATH_CONIC: + /* This is not valid SVG */ + g_string_append (string, " O "); + _g_string_append_point (string, &p->pts[1]); + g_string_append (string, ", "); + _g_string_append_point (string, &p->pts[2]); + g_string_append (string, ", "); + _g_string_append_double (string, p->weight); + break; + + default: + g_assert_not_reached(); + return; + } +} + +static gboolean +path_operation_equal (const PathOperation *p1, + const PathOperation *p2, + float epsilon) +{ + if (p1->op != p2->op) + return FALSE; + + /* No need to compare pts[0] for most ops, that's just + * duplicate work. */ + switch (p1->op) + { + case GSK_PATH_MOVE: + return graphene_point_near (&p1->pts[0], &p2->pts[0], epsilon); + + case GSK_PATH_LINE: + case GSK_PATH_CLOSE: + return graphene_point_near (&p1->pts[1], &p2->pts[1], epsilon); + + case GSK_PATH_QUAD: + return graphene_point_near (&p1->pts[1], &p2->pts[1], epsilon) + && graphene_point_near (&p1->pts[2], &p2->pts[2], epsilon); + + case GSK_PATH_CUBIC: + return graphene_point_near (&p1->pts[1], &p2->pts[1], epsilon) + && graphene_point_near (&p1->pts[2], &p2->pts[2], epsilon) + && graphene_point_near (&p1->pts[3], &p2->pts[3], epsilon); + + case GSK_PATH_CONIC: + return graphene_point_near (&p1->pts[1], &p2->pts[1], epsilon) + && graphene_point_near (&p1->pts[2], &p2->pts[2], epsilon) + && G_APPROX_VALUE (p1->weight, p2->weight, epsilon); + + default: + g_return_val_if_reached (FALSE); + } +} + +static gboolean +collect_path_operation_cb (GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts, + float weight, + gpointer user_data) +{ + g_array_append_vals (user_data, + (PathOperation[1]) { { + op, + { + GRAPHENE_POINT_INIT(pts[0].x, pts[0].y), + GRAPHENE_POINT_INIT(n_pts > 1 ? pts[1].x : 0, + n_pts > 1 ? pts[1].y : 0), + GRAPHENE_POINT_INIT(n_pts > 2 ? pts[2].x : 0, + n_pts > 2 ? pts[2].y : 0), + GRAPHENE_POINT_INIT(n_pts > 3 ? pts[3].x : 0, + n_pts > 3 ? pts[3].y : 0) + }, + weight + } }, + 1); + return TRUE; +} + +static GArray * +collect_path (GskPath *path) +{ + GArray *array = g_array_new (FALSE, FALSE, sizeof (PathOperation)); + + /* Use -1 here because we want all the flags, even future additions */ + gsk_path_foreach (path, -1, collect_path_operation_cb, array); + + return array; +} + +static void +assert_path_equal_func (const char *domain, + const char *file, + int line, + const char *func, + GskPath *path1, + GskPath *path2, + float epsilon) +{ + GArray *ops1, *ops2; + guint i; + + ops1 = collect_path (path1); + ops2 = collect_path (path2); + + for (i = 0; i < MAX (ops1->len, ops2->len); i++) + { + PathOperation *op1 = i < ops1->len ? &g_array_index (ops1, PathOperation, i) : NULL; + PathOperation *op2 = i < ops2->len ? &g_array_index (ops2, PathOperation, i) : NULL; + + if (op1 == NULL || op2 == NULL || !path_operation_equal (op1, op2, epsilon)) + { + GString *string; + guint j; + + /* Find the operation we start to print */ + for (j = i; j-- > 0; ) + { + PathOperation *op = &g_array_index (ops1, PathOperation, j); + if (op->op == GSK_PATH_MOVE) + break; + if (j + 3 == i) + { + j = i - 1; + break; + } + } + + string = g_string_new (j == 0 ? "" : "... "); + for (; j < i; j++) + { + PathOperation *op = &g_array_index (ops1, PathOperation, j); + path_operation_print (op, string); + g_string_append_c (string, ' '); + } + + g_string_append (string, "\\\n "); + if (op1) + { + path_operation_print (op1, string); + if (ops1->len > i + 1) + g_string_append (string, " ..."); + } + g_string_append (string, "\n "); + if (op1) + { + path_operation_print (op2, string); + if (ops2->len > i + 1) + g_string_append (string, " ..."); + } + + g_assertion_message (domain, file, line, func, string->str); + + g_string_free (string, TRUE); + } + } + + g_array_free (ops1, TRUE); + g_array_free (ops2, TRUE); +} +#define assert_path_equal(p1,p2) assert_path_equal_func(G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, (p1),(p2), FLOAT_EPSILON) +#define assert_path_equal_with_epsilon(p1,p2, epsilon) \ + assert_path_equal_func(G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, (p1),(p2), (epsilon)) + +static void +test_create (void) +{ + GskPath *path1, *path2, *built; + GskPathBuilder *builder; + guint i; + char *s; + GString *str; + + for (i = 0; i < 1000; i++) + { + builder = gsk_path_builder_new (); + path1 = create_random_path (G_MAXUINT); + gsk_path_builder_add_path (builder, path1); + path2 = create_random_path (G_MAXUINT); + gsk_path_builder_add_path (builder, path2); + built = gsk_path_builder_free_to_path (builder); + + str = g_string_new (NULL); + gsk_path_print (path1, str); + if (!gsk_path_is_empty (path1) && !gsk_path_is_empty (path2)) + g_string_append_c (str, ' '); + gsk_path_print (path2, str); + + s = gsk_path_to_string (built); + + g_assert_cmpstr (s, ==, str->str); + g_string_free (str, TRUE); + g_free (s); + + gsk_path_unref (built); + gsk_path_unref (path2); + gsk_path_unref (path1); + } +} + +static void +test_parse (void) +{ + int i; + + for (i = 0; i < 1000; i++) + { + GskPath *path1, *path2; + char *string1, *string2; + + path1 = create_random_path (G_MAXUINT); + string1 = gsk_path_to_string (path1); + g_assert_nonnull (string1); + + path2 = gsk_path_parse (string1); + g_assert_nonnull (path2); + string2 = gsk_path_to_string (path2); + g_assert_nonnull (string2); + + assert_path_equal_with_epsilon (path1, path2, 1.f / 1024); + + gsk_path_unref (path2); + gsk_path_unref (path1); + g_free (string2); + g_free (string1); + } +} + +int +main (int argc, + char *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + + g_test_add_func ("/path/create", test_create); + g_test_add_func ("/path/parse", test_parse); + + return g_test_run (); +}