Merge pull request #1051 from anthrotype/svg-parse
add parser for SVG paths supporting the pen protocol
This commit is contained in:
commit
71e97b3e74
6
Lib/fontTools/svgLib/__init__.py
Normal file
6
Lib/fontTools/svgLib/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from __future__ import print_function, division, absolute_import
|
||||||
|
from fontTools.misc.py23 import *
|
||||||
|
|
||||||
|
from .path import SVGPath, parse_path
|
||||||
|
|
||||||
|
__all__ = ["SVGPath", "parse_path"]
|
58
Lib/fontTools/svgLib/path/__init__.py
Normal file
58
Lib/fontTools/svgLib/path/__init__.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
from __future__ import (
|
||||||
|
print_function, division, absolute_import, unicode_literals)
|
||||||
|
from fontTools.misc.py23 import *
|
||||||
|
|
||||||
|
from fontTools.pens.transformPen import TransformPen
|
||||||
|
from .parser import parse_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
from xml.etree import cElementTree as ElementTree # python 2
|
||||||
|
except ImportError: # pragma nocover
|
||||||
|
from xml.etree import ElementTree # python 3
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [tostr(s) for s in ("SVGPath", "parse_path")]
|
||||||
|
|
||||||
|
|
||||||
|
class SVGPath(object):
|
||||||
|
""" Parse SVG ``path`` elements from a file or string, and draw them
|
||||||
|
onto a glyph object that supports the FontTools Pen protocol.
|
||||||
|
|
||||||
|
For example, reading from an SVG file and drawing to a Defcon Glyph:
|
||||||
|
|
||||||
|
import defcon
|
||||||
|
glyph = defcon.Glyph()
|
||||||
|
pen = glyph.getPen()
|
||||||
|
svg = SVGPath("path/to/a.svg")
|
||||||
|
svg.draw(pen)
|
||||||
|
|
||||||
|
Or reading from a string containing SVG data, using the alternative
|
||||||
|
'fromstring' (a class method):
|
||||||
|
|
||||||
|
data = '<?xml version="1.0" ...'
|
||||||
|
svg = SVGPath.fromstring(data)
|
||||||
|
svg.draw(pen)
|
||||||
|
|
||||||
|
Both constructors can optionally take a 'transform' matrix (6-float
|
||||||
|
tuple, or a FontTools Transform object) to modify the draw output.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, filename=None, transform=None):
|
||||||
|
if filename is None:
|
||||||
|
self.root = ElementTree.ElementTree()
|
||||||
|
else:
|
||||||
|
tree = ElementTree.parse(filename)
|
||||||
|
self.root = tree.getroot()
|
||||||
|
self.transform = transform
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromstring(cls, data, transform=None):
|
||||||
|
self = cls(transform=transform)
|
||||||
|
self.root = ElementTree.fromstring(data)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def draw(self, pen):
|
||||||
|
if self.transform:
|
||||||
|
pen = TransformPen(pen, self.transform)
|
||||||
|
for el in self.root.findall(".//{http://www.w3.org/2000/svg}path[@d]"):
|
||||||
|
parse_path(el.get("d"), pen)
|
216
Lib/fontTools/svgLib/path/parser.py
Normal file
216
Lib/fontTools/svgLib/path/parser.py
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
# SVG Path specification parser.
|
||||||
|
# This is an adaptation from 'svg.path' by Lennart Regebro (@regebro),
|
||||||
|
# modified so that the parser takes a FontTools Pen object instead of
|
||||||
|
# returning a list of svg.path Path objects.
|
||||||
|
# The original code can be found at:
|
||||||
|
# https://github.com/regebro/svg.path/blob/4f9b6e3/src/svg/path/parser.py
|
||||||
|
# Copyright (c) 2013-2014 Lennart Regebro
|
||||||
|
# License: MIT
|
||||||
|
|
||||||
|
from __future__ import (
|
||||||
|
print_function, division, absolute_import, unicode_literals)
|
||||||
|
from fontTools.misc.py23 import *
|
||||||
|
import re
|
||||||
|
|
||||||
|
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
|
||||||
|
UPPERCASE = set('MZLHVCSQTA')
|
||||||
|
|
||||||
|
COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
|
||||||
|
FLOAT_RE = re.compile("[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
|
||||||
|
|
||||||
|
|
||||||
|
def _tokenize_path(pathdef):
|
||||||
|
for x in COMMAND_RE.split(pathdef):
|
||||||
|
if x in COMMANDS:
|
||||||
|
yield x
|
||||||
|
for token in FLOAT_RE.findall(x):
|
||||||
|
yield token
|
||||||
|
|
||||||
|
|
||||||
|
def parse_path(pathdef, pen, current_pos=(0, 0)):
|
||||||
|
""" Parse SVG path definition (i.e. "d" attribute of <path> elements)
|
||||||
|
and call a 'pen' object's moveTo, lineTo, curveTo, qCurveTo and closePath
|
||||||
|
methods.
|
||||||
|
|
||||||
|
If 'current_pos' (2-float tuple) is provided, the initial moveTo will
|
||||||
|
be relative to that instead being absolute.
|
||||||
|
|
||||||
|
Arc segments (commands "A" or "a") are not currently supported, and raise
|
||||||
|
NotImplementedError.
|
||||||
|
"""
|
||||||
|
# In the SVG specs, initial movetos are absolute, even if
|
||||||
|
# specified as 'm'. This is the default behavior here as well.
|
||||||
|
# But if you pass in a current_pos variable, the initial moveto
|
||||||
|
# will be relative to that current_pos. This is useful.
|
||||||
|
current_pos = complex(*current_pos)
|
||||||
|
|
||||||
|
elements = list(_tokenize_path(pathdef))
|
||||||
|
# Reverse for easy use of .pop()
|
||||||
|
elements.reverse()
|
||||||
|
|
||||||
|
start_pos = None
|
||||||
|
command = None
|
||||||
|
last_control = None
|
||||||
|
|
||||||
|
while elements:
|
||||||
|
|
||||||
|
if elements[-1] in COMMANDS:
|
||||||
|
# New command.
|
||||||
|
last_command = command # Used by S and T
|
||||||
|
command = elements.pop()
|
||||||
|
absolute = command in UPPERCASE
|
||||||
|
command = command.upper()
|
||||||
|
else:
|
||||||
|
# If this element starts with numbers, it is an implicit command
|
||||||
|
# and we don't change the command. Check that it's allowed:
|
||||||
|
if command is None:
|
||||||
|
raise ValueError("Unallowed implicit command in %s, position %s" % (
|
||||||
|
pathdef, len(pathdef.split()) - len(elements)))
|
||||||
|
last_command = command # Used by S and T
|
||||||
|
|
||||||
|
if command == 'M':
|
||||||
|
# Moveto command.
|
||||||
|
x = elements.pop()
|
||||||
|
y = elements.pop()
|
||||||
|
pos = float(x) + float(y) * 1j
|
||||||
|
if absolute:
|
||||||
|
current_pos = pos
|
||||||
|
else:
|
||||||
|
current_pos += pos
|
||||||
|
|
||||||
|
# M is not preceded by Z; it's an open subpath
|
||||||
|
if start_pos is not None:
|
||||||
|
pen.endPath()
|
||||||
|
|
||||||
|
pen.moveTo((current_pos.real, current_pos.imag))
|
||||||
|
|
||||||
|
# when M is called, reset start_pos
|
||||||
|
# This behavior of Z is defined in svg spec:
|
||||||
|
# http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
|
||||||
|
start_pos = current_pos
|
||||||
|
|
||||||
|
# Implicit moveto commands are treated as lineto commands.
|
||||||
|
# So we set command to lineto here, in case there are
|
||||||
|
# further implicit commands after this moveto.
|
||||||
|
command = 'L'
|
||||||
|
|
||||||
|
elif command == 'Z':
|
||||||
|
# Close path
|
||||||
|
if current_pos != start_pos:
|
||||||
|
pen.lineTo((start_pos.real, start_pos.imag))
|
||||||
|
pen.closePath()
|
||||||
|
current_pos = start_pos
|
||||||
|
start_pos = None
|
||||||
|
command = None # You can't have implicit commands after closing.
|
||||||
|
|
||||||
|
elif command == 'L':
|
||||||
|
x = elements.pop()
|
||||||
|
y = elements.pop()
|
||||||
|
pos = float(x) + float(y) * 1j
|
||||||
|
if not absolute:
|
||||||
|
pos += current_pos
|
||||||
|
pen.lineTo((pos.real, pos.imag))
|
||||||
|
current_pos = pos
|
||||||
|
|
||||||
|
elif command == 'H':
|
||||||
|
x = elements.pop()
|
||||||
|
pos = float(x) + current_pos.imag * 1j
|
||||||
|
if not absolute:
|
||||||
|
pos += current_pos.real
|
||||||
|
pen.lineTo((pos.real, pos.imag))
|
||||||
|
current_pos = pos
|
||||||
|
|
||||||
|
elif command == 'V':
|
||||||
|
y = elements.pop()
|
||||||
|
pos = current_pos.real + float(y) * 1j
|
||||||
|
if not absolute:
|
||||||
|
pos += current_pos.imag * 1j
|
||||||
|
pen.lineTo((pos.real, pos.imag))
|
||||||
|
current_pos = pos
|
||||||
|
|
||||||
|
elif command == 'C':
|
||||||
|
control1 = float(elements.pop()) + float(elements.pop()) * 1j
|
||||||
|
control2 = float(elements.pop()) + float(elements.pop()) * 1j
|
||||||
|
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||||
|
|
||||||
|
if not absolute:
|
||||||
|
control1 += current_pos
|
||||||
|
control2 += current_pos
|
||||||
|
end += current_pos
|
||||||
|
|
||||||
|
pen.curveTo((control1.real, control1.imag),
|
||||||
|
(control2.real, control2.imag),
|
||||||
|
(end.real, end.imag))
|
||||||
|
current_pos = end
|
||||||
|
last_control = control2
|
||||||
|
|
||||||
|
elif command == 'S':
|
||||||
|
# Smooth curve. First control point is the "reflection" of
|
||||||
|
# the second control point in the previous path.
|
||||||
|
|
||||||
|
if last_command not in 'CS':
|
||||||
|
# If there is no previous command or if the previous command
|
||||||
|
# was not an C, c, S or s, assume the first control point is
|
||||||
|
# coincident with the current point.
|
||||||
|
control1 = current_pos
|
||||||
|
else:
|
||||||
|
# The first control point is assumed to be the reflection of
|
||||||
|
# the second control point on the previous command relative
|
||||||
|
# to the current point.
|
||||||
|
control1 = current_pos + current_pos - last_control
|
||||||
|
|
||||||
|
control2 = float(elements.pop()) + float(elements.pop()) * 1j
|
||||||
|
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||||
|
|
||||||
|
if not absolute:
|
||||||
|
control2 += current_pos
|
||||||
|
end += current_pos
|
||||||
|
|
||||||
|
pen.curveTo((control1.real, control1.imag),
|
||||||
|
(control2.real, control2.imag),
|
||||||
|
(end.real, end.imag))
|
||||||
|
current_pos = end
|
||||||
|
last_control = control2
|
||||||
|
|
||||||
|
elif command == 'Q':
|
||||||
|
control = float(elements.pop()) + float(elements.pop()) * 1j
|
||||||
|
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||||
|
|
||||||
|
if not absolute:
|
||||||
|
control += current_pos
|
||||||
|
end += current_pos
|
||||||
|
|
||||||
|
pen.qCurveTo((control.real, control.imag), (end.real, end.imag))
|
||||||
|
current_pos = end
|
||||||
|
last_control = control
|
||||||
|
|
||||||
|
elif command == 'T':
|
||||||
|
# Smooth curve. Control point is the "reflection" of
|
||||||
|
# the second control point in the previous path.
|
||||||
|
|
||||||
|
if last_command not in 'QT':
|
||||||
|
# If there is no previous command or if the previous command
|
||||||
|
# was not an Q, q, T or t, assume the first control point is
|
||||||
|
# coincident with the current point.
|
||||||
|
control = current_pos
|
||||||
|
else:
|
||||||
|
# The control point is assumed to be the reflection of
|
||||||
|
# the control point on the previous command relative
|
||||||
|
# to the current point.
|
||||||
|
control = current_pos + current_pos - last_control
|
||||||
|
|
||||||
|
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||||
|
|
||||||
|
if not absolute:
|
||||||
|
end += current_pos
|
||||||
|
|
||||||
|
pen.qCurveTo((control.real, control.imag), (end.real, end.imag))
|
||||||
|
current_pos = end
|
||||||
|
last_control = control
|
||||||
|
|
||||||
|
elif command == 'A':
|
||||||
|
raise NotImplementedError('arcs are not supported')
|
||||||
|
|
||||||
|
# no final Z command, it's an open path
|
||||||
|
if start_pos is not None:
|
||||||
|
pen.endPath()
|
0
Tests/svgLib/path/__init__.py
Normal file
0
Tests/svgLib/path/__init__.py
Normal file
297
Tests/svgLib/path/parser_test.py
Normal file
297
Tests/svgLib/path/parser_test.py
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
from __future__ import print_function, absolute_import, division
|
||||||
|
|
||||||
|
from fontTools.misc.py23 import *
|
||||||
|
from fontTools.pens.recordingPen import RecordingPen
|
||||||
|
from fontTools.svgLib import parse_path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pathdef, expected",
|
||||||
|
[
|
||||||
|
|
||||||
|
# Examples from the SVG spec
|
||||||
|
|
||||||
|
(
|
||||||
|
"M 100 100 L 300 100 L 200 300 z",
|
||||||
|
[
|
||||||
|
("moveTo", ((100.0, 100.0),)),
|
||||||
|
("lineTo", ((300.0, 100.0),)),
|
||||||
|
("lineTo", ((200.0, 300.0),)),
|
||||||
|
("lineTo", ((100.0, 100.0),)),
|
||||||
|
("closePath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
# for Z command behavior when there is multiple subpaths
|
||||||
|
(
|
||||||
|
"M 0 0 L 50 20 M 100 100 L 300 100 L 200 300 z",
|
||||||
|
[
|
||||||
|
("moveTo", ((0.0, 0.0),)),
|
||||||
|
("lineTo", ((50.0, 20.0),)),
|
||||||
|
("endPath", ()),
|
||||||
|
("moveTo", ((100.0, 100.0),)),
|
||||||
|
("lineTo", ((300.0, 100.0),)),
|
||||||
|
("lineTo", ((200.0, 300.0),)),
|
||||||
|
("lineTo", ((100.0, 100.0),)),
|
||||||
|
("closePath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"M100,200 C100,100 250,100 250,200 S400,300 400,200",
|
||||||
|
[
|
||||||
|
("moveTo", ((100.0, 200.0),)),
|
||||||
|
("curveTo", ((100.0, 100.0),
|
||||||
|
(250.0, 100.0),
|
||||||
|
(250.0, 200.0))),
|
||||||
|
("curveTo", ((250.0, 300.0),
|
||||||
|
(400.0, 300.0),
|
||||||
|
(400.0, 200.0))),
|
||||||
|
("endPath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"M100,200 C100,100 400,100 400,200",
|
||||||
|
[
|
||||||
|
("moveTo", ((100.0, 200.0),)),
|
||||||
|
("curveTo", ((100.0, 100.0),
|
||||||
|
(400.0, 100.0),
|
||||||
|
(400.0, 200.0))),
|
||||||
|
("endPath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"M100,500 C25,400 475,400 400,500",
|
||||||
|
[
|
||||||
|
("moveTo", ((100.0, 500.0),)),
|
||||||
|
("curveTo", ((25.0, 400.0),
|
||||||
|
(475.0, 400.0),
|
||||||
|
(400.0, 500.0))),
|
||||||
|
("endPath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"M100,800 C175,700 325,700 400,800",
|
||||||
|
[
|
||||||
|
("moveTo", ((100.0, 800.0),)),
|
||||||
|
("curveTo", ((175.0, 700.0),
|
||||||
|
(325.0, 700.0),
|
||||||
|
(400.0, 800.0))),
|
||||||
|
("endPath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"M600,200 C675,100 975,100 900,200",
|
||||||
|
[
|
||||||
|
("moveTo", ((600.0, 200.0),)),
|
||||||
|
("curveTo", ((675.0, 100.0),
|
||||||
|
(975.0, 100.0),
|
||||||
|
(900.0, 200.0))),
|
||||||
|
("endPath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"M600,500 C600,350 900,650 900,500",
|
||||||
|
[
|
||||||
|
("moveTo", ((600.0, 500.0),)),
|
||||||
|
("curveTo", ((600.0, 350.0),
|
||||||
|
(900.0, 650.0),
|
||||||
|
(900.0, 500.0))),
|
||||||
|
("endPath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"M600,800 C625,700 725,700 750,800 S875,900 900,800",
|
||||||
|
[
|
||||||
|
("moveTo", ((600.0, 800.0),)),
|
||||||
|
("curveTo", ((625.0, 700.0),
|
||||||
|
(725.0, 700.0),
|
||||||
|
(750.0, 800.0))),
|
||||||
|
("curveTo", ((775.0, 900.0),
|
||||||
|
(875.0, 900.0),
|
||||||
|
(900.0, 800.0))),
|
||||||
|
("endPath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"M200,300 Q400,50 600,300 T1000,300",
|
||||||
|
[
|
||||||
|
("moveTo", ((200.0, 300.0),)),
|
||||||
|
("qCurveTo", ((400.0, 50.0),
|
||||||
|
(600.0, 300.0))),
|
||||||
|
("qCurveTo", ((800.0, 550.0),
|
||||||
|
(1000.0, 300.0))),
|
||||||
|
("endPath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
# End examples from SVG spec
|
||||||
|
|
||||||
|
# Relative moveto
|
||||||
|
(
|
||||||
|
"M 0 0 L 50 20 m 50 80 L 300 100 L 200 300 z",
|
||||||
|
[
|
||||||
|
("moveTo", ((0.0, 0.0),)),
|
||||||
|
("lineTo", ((50.0, 20.0),)),
|
||||||
|
("endPath", ()),
|
||||||
|
("moveTo", ((100.0, 100.0),)),
|
||||||
|
("lineTo", ((300.0, 100.0),)),
|
||||||
|
("lineTo", ((200.0, 300.0),)),
|
||||||
|
("lineTo", ((100.0, 100.0),)),
|
||||||
|
("closePath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
# Initial smooth and relative curveTo
|
||||||
|
(
|
||||||
|
"M100,200 s 150,-100 150,0",
|
||||||
|
[
|
||||||
|
("moveTo", ((100.0, 200.0),)),
|
||||||
|
("curveTo", ((100.0, 200.0),
|
||||||
|
(250.0, 100.0),
|
||||||
|
(250.0, 200.0))),
|
||||||
|
("endPath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
# Initial smooth and relative qCurveTo
|
||||||
|
(
|
||||||
|
"M100,200 t 150,0",
|
||||||
|
[
|
||||||
|
("moveTo", ((100.0, 200.0),)),
|
||||||
|
("qCurveTo", ((100.0, 200.0),
|
||||||
|
(250.0, 200.0))),
|
||||||
|
("endPath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
# relative l command
|
||||||
|
(
|
||||||
|
"M 100 100 L 300 100 l -100 200 z",
|
||||||
|
[
|
||||||
|
("moveTo", ((100.0, 100.0),)),
|
||||||
|
("lineTo", ((300.0, 100.0),)),
|
||||||
|
("lineTo", ((200.0, 300.0),)),
|
||||||
|
("lineTo", ((100.0, 100.0),)),
|
||||||
|
("closePath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
# relative q command
|
||||||
|
(
|
||||||
|
"M200,300 q200,-250 400,0",
|
||||||
|
[
|
||||||
|
("moveTo", ((200.0, 300.0),)),
|
||||||
|
("qCurveTo", ((400.0, 50.0),
|
||||||
|
(600.0, 300.0))),
|
||||||
|
("endPath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
# absolute H command
|
||||||
|
(
|
||||||
|
"M 100 100 H 300 L 200 300 z",
|
||||||
|
[
|
||||||
|
("moveTo", ((100.0, 100.0),)),
|
||||||
|
("lineTo", ((300.0, 100.0),)),
|
||||||
|
("lineTo", ((200.0, 300.0),)),
|
||||||
|
("lineTo", ((100.0, 100.0),)),
|
||||||
|
("closePath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
# relative h command
|
||||||
|
(
|
||||||
|
"M 100 100 h 200 L 200 300 z",
|
||||||
|
[
|
||||||
|
("moveTo", ((100.0, 100.0),)),
|
||||||
|
("lineTo", ((300.0, 100.0),)),
|
||||||
|
("lineTo", ((200.0, 300.0),)),
|
||||||
|
("lineTo", ((100.0, 100.0),)),
|
||||||
|
("closePath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
# absolute V command
|
||||||
|
(
|
||||||
|
"M 100 100 V 300 L 200 300 z",
|
||||||
|
[
|
||||||
|
("moveTo", ((100.0, 100.0),)),
|
||||||
|
("lineTo", ((100.0, 300.0),)),
|
||||||
|
("lineTo", ((200.0, 300.0),)),
|
||||||
|
("lineTo", ((100.0, 100.0),)),
|
||||||
|
("closePath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
# relative v command
|
||||||
|
(
|
||||||
|
"M 100 100 v 200 L 200 300 z",
|
||||||
|
[
|
||||||
|
("moveTo", ((100.0, 100.0),)),
|
||||||
|
("lineTo", ((100.0, 300.0),)),
|
||||||
|
("lineTo", ((200.0, 300.0),)),
|
||||||
|
("lineTo", ((100.0, 100.0),)),
|
||||||
|
("closePath", ()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_parse_path(pathdef, expected):
|
||||||
|
pen = RecordingPen()
|
||||||
|
parse_path(pathdef, pen)
|
||||||
|
|
||||||
|
assert pen.value == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"pathdef1, pathdef2",
|
||||||
|
[
|
||||||
|
# don't need spaces between numbers and commands
|
||||||
|
(
|
||||||
|
"M 100 100 L 200 200",
|
||||||
|
"M100 100L200 200",
|
||||||
|
),
|
||||||
|
# repeated implicit command
|
||||||
|
(
|
||||||
|
"M 100 200 L 200 100 L -100 -200",
|
||||||
|
"M 100 200 L 200 100 -100 -200"
|
||||||
|
),
|
||||||
|
# don't need spaces before a minus-sign
|
||||||
|
(
|
||||||
|
"M100,200c10-5,20-10,30-20",
|
||||||
|
"M 100 200 c 10 -5 20 -10 30 -20"
|
||||||
|
),
|
||||||
|
# closed paths have an implicit lineTo if they don't
|
||||||
|
# end on the same point as the initial moveTo
|
||||||
|
(
|
||||||
|
"M 100 100 L 300 100 L 200 300 z",
|
||||||
|
"M 100 100 L 300 100 L 200 300 L 100 100 z"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_equivalent_paths(pathdef1, pathdef2):
|
||||||
|
pen1 = RecordingPen()
|
||||||
|
parse_path(pathdef1, pen1)
|
||||||
|
|
||||||
|
pen2 = RecordingPen()
|
||||||
|
parse_path(pathdef2, pen2)
|
||||||
|
|
||||||
|
assert pen1.value == pen2.value
|
||||||
|
|
||||||
|
|
||||||
|
def test_exponents():
|
||||||
|
# It can be e or E, the plus is optional, and a minimum of +/-3.4e38 must be supported.
|
||||||
|
pen = RecordingPen()
|
||||||
|
parse_path("M-3.4e38 3.4E+38L-3.4E-38,3.4e-38", pen)
|
||||||
|
expected = [
|
||||||
|
("moveTo", ((-3.4e+38, 3.4e+38),)),
|
||||||
|
("lineTo", ((-3.4e-38, 3.4e-38),)),
|
||||||
|
("endPath", ()),
|
||||||
|
]
|
||||||
|
|
||||||
|
assert pen.value == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_implicit_command():
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
parse_path("M 100 100 L 200 200 Z 100 200", RecordingPen())
|
||||||
|
assert exc_info.match("Unallowed implicit command")
|
||||||
|
|
||||||
|
|
||||||
|
def test_arc_not_implemented():
|
||||||
|
pathdef = "M300,200 h-150 a150,150 0 1,0 150,-150 z"
|
||||||
|
with pytest.raises(NotImplementedError) as exc_info:
|
||||||
|
parse_path(pathdef, RecordingPen())
|
||||||
|
assert exc_info.match("arcs are not supported")
|
80
Tests/svgLib/path/path_test.py
Normal file
80
Tests/svgLib/path/path_test.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
from __future__ import print_function, absolute_import, division
|
||||||
|
|
||||||
|
from fontTools.misc.py23 import *
|
||||||
|
from fontTools.pens.recordingPen import RecordingPen
|
||||||
|
from fontTools.svgLib import SVGPath
|
||||||
|
|
||||||
|
import os
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
|
|
||||||
|
SVG_DATA = """\
|
||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="1000.0" height="1000.0">
|
||||||
|
<path d="M 100 100 L 300 100 L 200 300 z"/>
|
||||||
|
<path d="M100,200 C100,100 250,100 250,200 S400,300 400,200"/>
|
||||||
|
</svg>
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXPECTED_PEN_COMMANDS = [
|
||||||
|
("moveTo", ((100.0, 100.0),)),
|
||||||
|
("lineTo", ((300.0, 100.0),)),
|
||||||
|
("lineTo", ((200.0, 300.0),)),
|
||||||
|
("lineTo", ((100.0, 100.0),)),
|
||||||
|
("closePath", ()),
|
||||||
|
("moveTo", ((100.0, 200.0),)),
|
||||||
|
("curveTo", ((100.0, 100.0),
|
||||||
|
(250.0, 100.0),
|
||||||
|
(250.0, 200.0))),
|
||||||
|
("curveTo", ((250.0, 300.0),
|
||||||
|
(400.0, 300.0),
|
||||||
|
(400.0, 200.0))),
|
||||||
|
("endPath", ())
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SVGPathTest(object):
|
||||||
|
|
||||||
|
def test_from_svg_file(self):
|
||||||
|
pen = RecordingPen()
|
||||||
|
with NamedTemporaryFile(delete=False) as tmp:
|
||||||
|
tmp.write(tobytes(SVG_DATA))
|
||||||
|
try:
|
||||||
|
svg = SVGPath(tmp.name)
|
||||||
|
svg.draw(pen)
|
||||||
|
finally:
|
||||||
|
os.remove(tmp.name)
|
||||||
|
|
||||||
|
assert pen.value == EXPECTED_PEN_COMMANDS
|
||||||
|
|
||||||
|
def test_fromstring(self):
|
||||||
|
pen = RecordingPen()
|
||||||
|
svg = SVGPath.fromstring(SVG_DATA)
|
||||||
|
svg.draw(pen)
|
||||||
|
|
||||||
|
assert pen.value == EXPECTED_PEN_COMMANDS
|
||||||
|
|
||||||
|
def test_transform(self):
|
||||||
|
pen = RecordingPen()
|
||||||
|
svg = SVGPath.fromstring(SVG_DATA,
|
||||||
|
transform=(1.0, 0, 0, -1.0, 0, 1000))
|
||||||
|
svg.draw(pen)
|
||||||
|
|
||||||
|
assert pen.value == [
|
||||||
|
("moveTo", ((100.0, 900.0),)),
|
||||||
|
("lineTo", ((300.0, 900.0),)),
|
||||||
|
("lineTo", ((200.0, 700.0),)),
|
||||||
|
("lineTo", ((100.0, 900.0),)),
|
||||||
|
("closePath", ()),
|
||||||
|
("moveTo", ((100.0, 800.0),)),
|
||||||
|
("curveTo", ((100.0, 900.0),
|
||||||
|
(250.0, 900.0),
|
||||||
|
(250.0, 800.0))),
|
||||||
|
("curveTo", ((250.0, 700.0),
|
||||||
|
(400.0, 700.0),
|
||||||
|
(400.0, 800.0))),
|
||||||
|
("endPath", ())
|
||||||
|
]
|
Loading…
x
Reference in New Issue
Block a user