diff --git a/Lib/fontTools/svgLib/path/__init__.py b/Lib/fontTools/svgLib/path/__init__.py
index 017ff57e6..5dd3329cf 100644
--- a/Lib/fontTools/svgLib/path/__init__.py
+++ b/Lib/fontTools/svgLib/path/__init__.py
@@ -55,6 +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)
- for path in pb.paths:
+ original_pen = pen
+ for path, transform in zip(pb.paths, pb.transforms):
+ 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 efbfc91a6..4cc633ada 100644
--- a/Lib/fontTools/svgLib/path/shapes.py
+++ b/Lib/fontTools/svgLib/path/shapes.py
@@ -1,3 +1,6 @@
+import re
+
+
def _prefer_non_zero(*args):
for arg in args:
if arg != 0:
@@ -16,12 +19,28 @@ def _strip_xml_ns(tag):
return tag.split('}', 1)[1] if '}' in tag else 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:
+ 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')
@@ -68,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))
@@ -105,6 +143,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))
@@ -134,4 +176,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..99c70fdbe 100644
--- a/Tests/svgLib/path/shapes_test.py
+++ b/Tests/svgLib/path/shapes_test.py
@@ -7,71 +7,127 @@ 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
+ ),
+ # line
+ (
+ '',
+ 'M10,110 L50,150',
+ None
+ ),
+ # line, decimal positioning
+ (
+ '',
+ 'M10,110.2 L50.5,150.7',
+ 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
+ ),
+ # polyline
+ (
+ "",
+ "M30,10 50,30 10,30",
+ 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
+ ),
+ # 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
(
'',
- '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
+ ),
+ # 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):
+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