Merge pull request #1564 from fonttools/svg-shapes

[svgLib] Support line, polyline and matrix transforms
This commit is contained in:
Miguel Sousa 2019-04-03 12:31:35 -07:00 committed by GitHub
commit 5e627c5228
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 121 additions and 17 deletions

View File

@ -55,6 +55,10 @@ class SVGPath(object):
# xpath | doesn't seem to reliable work so just walk it # xpath | doesn't seem to reliable work so just walk it
for el in self.root.iter(): for el in self.root.iter():
pb.add_path_from_element(el) 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) parse_path(path, pen)

View File

@ -1,3 +1,6 @@
import re
def _prefer_non_zero(*args): def _prefer_non_zero(*args):
for arg in args: for arg in args:
if arg != 0: if arg != 0:
@ -16,12 +19,28 @@ def _strip_xml_ns(tag):
return tag.split('}', 1)[1] if '}' in tag else 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): class PathBuilder(object):
def __init__(self): def __init__(self):
self.paths = [] self.paths = []
self.transforms = []
def _start_path(self, initial_path=''): def _start_path(self, initial_path=''):
self.paths.append(initial_path) self.paths.append(initial_path)
self.transforms.append(None)
def _end_path(self): def _end_path(self):
self._add('z') self._add('z')
@ -68,6 +87,25 @@ class PathBuilder(object):
def v(self, y): def v(self, y):
self._vhline('v', 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): def _parse_rect(self, rect):
x = float(rect.attrib.get('x', 0)) x = float(rect.attrib.get('x', 0))
y = float(rect.attrib.get('y', 0)) y = float(rect.attrib.get('y', 0))
@ -105,6 +143,10 @@ class PathBuilder(object):
self._start_path('M' + poly.attrib['points']) self._start_path('M' + poly.attrib['points'])
self._end_path() 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): def _parse_circle(self, circle):
cx = float(circle.attrib.get('cx', 0)) cx = float(circle.attrib.get('cx', 0))
cy = float(circle.attrib.get('cy', 0)) cy = float(circle.attrib.get('cy', 0))
@ -134,4 +176,6 @@ class PathBuilder(object):
if not callable(parse_fn): if not callable(parse_fn):
return False return False
parse_fn(el) parse_fn(el)
if 'transform' in el.attrib:
self.transforms[-1] = _transform(el.attrib['transform'])
return True return True

View File

@ -7,71 +7,127 @@ import pytest
@pytest.mark.parametrize( @pytest.mark.parametrize(
"svg_xml, expected_path", "svg_xml, expected_path, expected_transform",
[ [
# path: direct passthrough # path: direct passthrough
( (
"<path d='I love kittens'/>", "<path d='I love kittens'/>",
"I love kittens" "I love kittens",
None
), ),
# path no @d # path no @d
( (
"<path duck='Mallard'/>", "<path duck='Mallard'/>",
None None,
None
),
# line
(
'<line x1="10" x2="50" y1="110" y2="150"/>',
'M10,110 L50,150',
None
),
# line, decimal positioning
(
'<line x1="10.0" x2="50.5" y1="110.2" y2="150.7"/>',
'M10,110.2 L50.5,150.7',
None
), ),
# rect: minimal valid example # rect: minimal valid example
( (
"<rect width='1' height='1'/>", "<rect width='1' height='1'/>",
"M0,0 H1 V1 H0 V0 z", "M0,0 H1 V1 H0 V0 z",
None
), ),
# rect: sharp corners # rect: sharp corners
( (
"<rect x='10' y='11' width='17' height='11'/>", "<rect x='10' y='11' width='17' height='11'/>",
"M10,11 H27 V22 H10 V11 z", "M10,11 H27 V22 H10 V11 z",
None
), ),
# rect: round corners # rect: round corners
( (
"<rect x='9' y='9' width='11' height='7' rx='2'/>", "<rect x='9' y='9' width='11' height='7' rx='2'/>",
"M11,9 H18 A2,2 0 0 1 20,11 V14 A2,2 0 0 1 18,16 H11" "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", " A2,2 0 0 1 9,14 V11 A2,2 0 0 1 11,9 z",
None
),
# rect: simple
(
"<rect x='11.5' y='16' width='11' height='2'/>",
"M11.5,16 H22.5 V18 H11.5 V16 z",
None
),
# rect: the one above plus a rotation
(
"<rect x='11.5' y='16' transform='matrix(0.7071 -0.7071 0.7071 0.7071 -7.0416 16.9999)' width='11' height='2'/>",
"M11.5,16 H22.5 V18 H11.5 V16 z",
(0.7071, -0.7071, 0.7071, 0.7071, -7.0416, 16.9999)
), ),
# polygon # polygon
( (
"<polygon points='30,10 50,30 10,30'/>", "<polygon points='30,10 50,30 10,30'/>",
"M30,10 50,30 10,30 z" "M30,10 50,30 10,30 z",
None
),
# polyline
(
"<polyline points='30,10 50,30 10,30'/>",
"M30,10 50,30 10,30",
None
), ),
# circle, minimal valid example # circle, minimal valid example
( (
"<circle r='1'/>", "<circle r='1'/>",
"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 # circle
( (
"<circle cx='600' cy='200' r='100'/>", "<circle cx='600' cy='200' r='100'/>",
"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 # circle, decimal positioning
( (
"<circle cx='12' cy='6.5' r='1.5'></circle>", "<circle cx='12' cy='6.5' r='1.5'></circle>",
"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
(
'<circle transform="matrix(0.9871 -0.1602 0.1602 0.9871 -7.525 8.6516)" cx="49.9" cy="51" r="14.3"/>',
'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 # ellipse
( (
'<ellipse cx="100" cy="50" rx="100" ry="50"/>', '<ellipse cx="100" cy="50" rx="100" ry="50"/>',
'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 # ellipse, decimal positioning
( (
'<ellipse cx="100.5" cy="50" rx="10" ry="50.5"/>', '<ellipse cx="100.5" cy="50" rx="10" ry="50.5"/>',
'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
(
'<ellipse transform="matrix(0.9557 -0.2945 0.2945 0.9557 -14.7694 20.1454)" cx="59.5" cy="59.1" rx="30.9" ry="11.9"/>',
'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 = shapes.PathBuilder()
pb.add_path_from_element(etree.fromstring(svg_xml)) pb.add_path_from_element(etree.fromstring(svg_xml))
if expected_path: if expected_path:
expected = [expected_path] expected_paths = [expected_path]
expected_transforms = [expected_transform]
else: else:
expected = [] expected_paths = []
assert pb.paths == expected expected_transforms = []
assert pb.paths == expected_paths
assert pb.transforms == expected_transforms