From dedf14ac8ad30d88f6ee3266907f562aab09640b Mon Sep 17 00:00:00 2001 From: Rod Sheeter Date: Sun, 17 Feb 2019 21:23:11 -0800 Subject: [PATCH 1/4] Roughing in transform=matrix(...) --- Lib/fontTools/svgLib/path/__init__.py | 3 +- Lib/fontTools/svgLib/path/shapes.py | 17 ++++++++ Tests/svgLib/path/shapes_test.py | 56 ++++++++++++++++++++------- 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/Lib/fontTools/svgLib/path/__init__.py b/Lib/fontTools/svgLib/path/__init__.py index 017ff57e6..012438e11 100644 --- a/Lib/fontTools/svgLib/path/__init__.py +++ b/Lib/fontTools/svgLib/path/__init__.py @@ -55,6 +55,7 @@ class SVGPath(object): # xpath | doesn't seem to reliable work so just walk it for el in self.root.iter(): pb.add_path_from_element(el) - for path in pb.paths: + for path, transform in zip(pb.paths, pb.transforms): + # TODO use transform parse_path(path, pen) diff --git a/Lib/fontTools/svgLib/path/shapes.py b/Lib/fontTools/svgLib/path/shapes.py index efbfc91a6..ad50dbf31 100644 --- a/Lib/fontTools/svgLib/path/shapes.py +++ b/Lib/fontTools/svgLib/path/shapes.py @@ -1,3 +1,5 @@ +import re + def _prefer_non_zero(*args): for arg in args: if arg != 0: @@ -16,12 +18,25 @@ def _strip_xml_ns(tag): return tag.split('}', 1)[1] if '}' in tag else tag +def _transform(raw_value): + # start simple: if you aren't exactly matrix(...) then no love + match = re.match(r'matrix\((.*)\)', raw_value) + if not match: + raise NotImplementedError + matrix = tuple(float(p) for p in re.split(r'\s+|,', match.group(1))) + if len(matrix) != 6: + raise ValueError('wrong # of terms in %s' % raw_value) + return matrix + + class PathBuilder(object): def __init__(self): self.paths = [] + self.transforms = [] def _start_path(self, initial_path=''): self.paths.append(initial_path) + self.transforms.append(None) def _end_path(self): self._add('z') @@ -134,4 +149,6 @@ class PathBuilder(object): if not callable(parse_fn): return False parse_fn(el) + if 'transform' in el.attrib: + self.transforms[-1] = _transform(el.attrib['transform']) return True diff --git a/Tests/svgLib/path/shapes_test.py b/Tests/svgLib/path/shapes_test.py index 6202d60f6..c9eaac160 100644 --- a/Tests/svgLib/path/shapes_test.py +++ b/Tests/svgLib/path/shapes_test.py @@ -7,71 +7,97 @@ import pytest @pytest.mark.parametrize( - "svg_xml, expected_path", + "svg_xml, expected_path, expected_transform", [ # path: direct passthrough ( - "", - "I love kittens" + "", + "I love kittens", + None ), # path no @d ( - "", - None + "", + None, + None ), # rect: minimal valid example ( "", "M0,0 H1 V1 H0 V0 z", + None ), # rect: sharp corners ( "", "M10,11 H27 V22 H10 V11 z", + None ), # rect: round corners ( "", "M11,9 H18 A2,2 0 0 1 20,11 V14 A2,2 0 0 1 18,16 H11" " A2,2 0 0 1 9,14 V11 A2,2 0 0 1 11,9 z", + None + ), + # rect: simple + ( + "", + "M11.5,16 H22.5 V18 H11.5 V16 z", + None + ), + # rect: the one above plus a rotation + ( + "", + "M11.5,16 H22.5 V18 H11.5 V16 z", + (0.7071, -0.7071, 0.7071, 0.7071, -7.0416, 16.9999) ), # polygon ( "", - "M30,10 50,30 10,30 z" + "M30,10 50,30 10,30 z", + None ), # circle, minimal valid example ( "", - "M-1,0 A1,1 0 1 1 1,0 A1,1 0 1 1 -1,0" + "M-1,0 A1,1 0 1 1 1,0 A1,1 0 1 1 -1,0", + None ), # circle ( "", - "M500,200 A100,100 0 1 1 700,200 A100,100 0 1 1 500,200" + "M500,200 A100,100 0 1 1 700,200 A100,100 0 1 1 500,200", + None ), # circle, decimal positioning ( "", - "M10.5,6.5 A1.5,1.5 0 1 1 13.5,6.5 A1.5,1.5 0 1 1 10.5,6.5" + "M10.5,6.5 A1.5,1.5 0 1 1 13.5,6.5 A1.5,1.5 0 1 1 10.5,6.5", + None ), # ellipse ( '', - 'M0,50 A100,50 0 1 1 200,50 A100,50 0 1 1 0,50' + 'M0,50 A100,50 0 1 1 200,50 A100,50 0 1 1 0,50', + None ), # ellipse, decimal positioning ( '', - 'M90.5,50 A10,50.5 0 1 1 110.5,50 A10,50.5 0 1 1 90.5,50' + 'M90.5,50 A10,50.5 0 1 1 110.5,50 A10,50.5 0 1 1 90.5,50', + None ), ] ) -def test_el_to_path(svg_xml, expected_path): +def test_el_to_path(svg_xml, expected_path, expected_transform): pb = shapes.PathBuilder() pb.add_path_from_element(etree.fromstring(svg_xml)) if expected_path: - expected = [expected_path] + expected_paths = [expected_path] + expected_transforms = [expected_transform] else: - expected = [] - assert pb.paths == expected + expected_paths = [] + expected_transforms = [] + assert pb.paths == expected_paths + assert pb.transforms == expected_transforms From d5adee46d9abd94c7a190441d235f931d69bc7dc Mon Sep 17 00:00:00 2001 From: Miguel Sousa Date: Tue, 2 Apr 2019 00:30:09 -0700 Subject: [PATCH 2/4] [svgLib] Complete support for matrix transforms --- Lib/fontTools/svgLib/path/__init__.py | 7 +++++-- Lib/fontTools/svgLib/path/shapes.py | 3 +++ Tests/svgLib/path/shapes_test.py | 12 ++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/svgLib/path/__init__.py b/Lib/fontTools/svgLib/path/__init__.py index 012438e11..5dd3329cf 100644 --- a/Lib/fontTools/svgLib/path/__init__.py +++ b/Lib/fontTools/svgLib/path/__init__.py @@ -55,7 +55,10 @@ class SVGPath(object): # xpath | doesn't seem to reliable work so just walk it for el in self.root.iter(): pb.add_path_from_element(el) + original_pen = pen for path, transform in zip(pb.paths, pb.transforms): - # TODO use transform + if transform: + pen = TransformPen(original_pen, transform) + else: + pen = original_pen parse_path(path, pen) - diff --git a/Lib/fontTools/svgLib/path/shapes.py b/Lib/fontTools/svgLib/path/shapes.py index ad50dbf31..a0b529429 100644 --- a/Lib/fontTools/svgLib/path/shapes.py +++ b/Lib/fontTools/svgLib/path/shapes.py @@ -19,6 +19,9 @@ def _strip_xml_ns(tag): def _transform(raw_value): + # TODO assumes a 'matrix' transform. + # No other transform functions are supported at the moment. + # https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform # start simple: if you aren't exactly matrix(...) then no love match = re.match(r'matrix\((.*)\)', raw_value) if not match: diff --git a/Tests/svgLib/path/shapes_test.py b/Tests/svgLib/path/shapes_test.py index c9eaac160..550eef623 100644 --- a/Tests/svgLib/path/shapes_test.py +++ b/Tests/svgLib/path/shapes_test.py @@ -76,6 +76,12 @@ import pytest "M10.5,6.5 A1.5,1.5 0 1 1 13.5,6.5 A1.5,1.5 0 1 1 10.5,6.5", None ), + # circle, with transform + ( + '', + 'M35.6,51 A14.3,14.3 0 1 1 64.2,51 A14.3,14.3 0 1 1 35.6,51', + (0.9871, -0.1602, 0.1602, 0.9871, -7.525, 8.6516) + ), # ellipse ( '', @@ -88,6 +94,12 @@ import pytest 'M90.5,50 A10,50.5 0 1 1 110.5,50 A10,50.5 0 1 1 90.5,50', None ), + # ellipse, with transform + ( + '', + 'M28.6,59.1 A30.9,11.9 0 1 1 90.4,59.1 A30.9,11.9 0 1 1 28.6,59.1', + (0.9557, -0.2945, 0.2945, 0.9557, -14.7694, 20.1454) + ), ] ) def test_el_to_path(svg_xml, expected_path, expected_transform): From 8d9a935eaa13a28e482ed614da92cf034b1556a6 Mon Sep 17 00:00:00 2001 From: Miguel Sousa Date: Tue, 2 Apr 2019 12:46:36 -0700 Subject: [PATCH 3/4] [svgLib] Add support for polyline element --- Lib/fontTools/svgLib/path/shapes.py | 5 +++++ Tests/svgLib/path/shapes_test.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/Lib/fontTools/svgLib/path/shapes.py b/Lib/fontTools/svgLib/path/shapes.py index a0b529429..ac19a9908 100644 --- a/Lib/fontTools/svgLib/path/shapes.py +++ b/Lib/fontTools/svgLib/path/shapes.py @@ -1,5 +1,6 @@ import re + def _prefer_non_zero(*args): for arg in args: if arg != 0: @@ -123,6 +124,10 @@ class PathBuilder(object): self._start_path('M' + poly.attrib['points']) self._end_path() + def _parse_polyline(self, poly): + if 'points' in poly.attrib: + self._start_path('M' + poly.attrib['points']) + def _parse_circle(self, circle): cx = float(circle.attrib.get('cx', 0)) cy = float(circle.attrib.get('cy', 0)) diff --git a/Tests/svgLib/path/shapes_test.py b/Tests/svgLib/path/shapes_test.py index 550eef623..a7b82eabb 100644 --- a/Tests/svgLib/path/shapes_test.py +++ b/Tests/svgLib/path/shapes_test.py @@ -58,6 +58,12 @@ import pytest "M30,10 50,30 10,30 z", None ), + # polyline + ( + "", + "M30,10 50,30 10,30", + None + ), # circle, minimal valid example ( "", From 299b5bcf8548f2b40a573c41c9f4fc922a995bcd Mon Sep 17 00:00:00 2001 From: Miguel Sousa Date: Tue, 2 Apr 2019 14:21:18 -0700 Subject: [PATCH 4/4] [svgLib] Add support for line element --- Lib/fontTools/svgLib/path/shapes.py | 19 +++++++++++++++++++ Tests/svgLib/path/shapes_test.py | 12 ++++++++++++ 2 files changed, 31 insertions(+) diff --git a/Lib/fontTools/svgLib/path/shapes.py b/Lib/fontTools/svgLib/path/shapes.py index ac19a9908..4cc633ada 100644 --- a/Lib/fontTools/svgLib/path/shapes.py +++ b/Lib/fontTools/svgLib/path/shapes.py @@ -87,6 +87,25 @@ class PathBuilder(object): def v(self, y): self._vhline('v', y) + def _line(self, c, x, y): + self._add('%s%s,%s' % (c, _ntos(x), _ntos(y))) + + def L(self, x, y): + self._line('L', x, y) + + def l(self, x, y): + self._line('l', x, y) + + def _parse_line(self, line): + x1 = float(line.attrib.get('x1', 0)) + y1 = float(line.attrib.get('y1', 0)) + x2 = float(line.attrib.get('x2', 0)) + y2 = float(line.attrib.get('y2', 0)) + + self._start_path() + self.M(x1, y1) + self.L(x2, y2) + def _parse_rect(self, rect): x = float(rect.attrib.get('x', 0)) y = float(rect.attrib.get('y', 0)) diff --git a/Tests/svgLib/path/shapes_test.py b/Tests/svgLib/path/shapes_test.py index a7b82eabb..99c70fdbe 100644 --- a/Tests/svgLib/path/shapes_test.py +++ b/Tests/svgLib/path/shapes_test.py @@ -21,6 +21,18 @@ import pytest None, None ), + # line + ( + '', + 'M10,110 L50,150', + None + ), + # line, decimal positioning + ( + '', + 'M10,110.2 L50.5,150.7', + None + ), # rect: minimal valid example ( "",