Implement the arc to curve points calculation using the original svg.path library and use the curve points for the pen. The Arc class and methods to parameterize and get curve points are copied. This makes the svg to glif conversion possible for any SVGs.
336 lines
12 KiB
Python
336 lines
12 KiB
Python
# 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 *
|
|
from math import sqrt, cos, sin, acos, degrees, radians
|
|
import re
|
|
|
|
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
|
|
UPPERCASE = set('MZLHVCSQTA')
|
|
|
|
COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
|
|
FLOAT_RE = re.compile("[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
|
|
|
|
class Arc(object):
|
|
|
|
def __init__(self, start, radius, rotation, arc, sweep, end):
|
|
"""radius is complex, rotation is in degrees,
|
|
large and sweep are 1 or 0 (True/False also work)"""
|
|
|
|
self.start = start
|
|
self.radius = radius
|
|
self.rotation = rotation
|
|
self.arc = bool(arc)
|
|
self.sweep = bool(sweep)
|
|
self.end = end
|
|
|
|
self._parameterize()
|
|
|
|
def _parameterize(self):
|
|
# Conversion from endpoint to center parameterization
|
|
# http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
|
|
|
cosr = cos(radians(self.rotation))
|
|
sinr = sin(radians(self.rotation))
|
|
dx = (self.start.real - self.end.real) / 2
|
|
dy = (self.start.imag - self.end.imag) / 2
|
|
x1prim = cosr * dx + sinr * dy
|
|
x1prim_sq = x1prim * x1prim
|
|
y1prim = -sinr * dx + cosr * dy
|
|
y1prim_sq = y1prim * y1prim
|
|
|
|
rx = self.radius.real
|
|
rx_sq = rx * rx
|
|
ry = self.radius.imag
|
|
ry_sq = ry * ry
|
|
|
|
# Correct out of range radii
|
|
radius_check = (x1prim_sq / rx_sq) + (y1prim_sq / ry_sq)
|
|
if radius_check > 1:
|
|
rx *= sqrt(radius_check)
|
|
ry *= sqrt(radius_check)
|
|
rx_sq = rx * rx
|
|
ry_sq = ry * ry
|
|
|
|
t1 = rx_sq * y1prim_sq
|
|
t2 = ry_sq * x1prim_sq
|
|
c = sqrt(abs((rx_sq * ry_sq - t1 - t2) / (t1 + t2)))
|
|
|
|
if self.arc == self.sweep:
|
|
c = -c
|
|
cxprim = c * rx * y1prim / ry
|
|
cyprim = -c * ry * x1prim / rx
|
|
|
|
self.center = complex((cosr * cxprim - sinr * cyprim) +
|
|
((self.start.real + self.end.real) / 2),
|
|
(sinr * cxprim + cosr * cyprim) +
|
|
((self.start.imag + self.end.imag) / 2))
|
|
|
|
ux = (x1prim - cxprim) / rx
|
|
uy = (y1prim - cyprim) / ry
|
|
vx = (-x1prim - cxprim) / rx
|
|
vy = (-y1prim - cyprim) / ry
|
|
n = sqrt(ux * ux + uy * uy)
|
|
p = ux
|
|
theta = degrees(acos(p / n))
|
|
if uy < 0:
|
|
theta = -theta
|
|
self.theta = theta % 360
|
|
|
|
n = sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy))
|
|
p = ux * vx + uy * vy
|
|
d = p/n
|
|
# In certain cases the above calculation can through inaccuracies
|
|
# become just slightly out of range, f ex -1.0000000000000002.
|
|
if d > 1.0:
|
|
d = 1.0
|
|
elif d < -1.0:
|
|
d = -1.0
|
|
delta = degrees(acos(d))
|
|
if (ux * vy - uy * vx) < 0:
|
|
delta = -delta
|
|
self.delta = delta % 360
|
|
if not self.sweep:
|
|
self.delta -= 360
|
|
|
|
def point(self, pos):
|
|
angle = radians(self.theta + (self.delta * pos))
|
|
cosr = cos(radians(self.rotation))
|
|
sinr = sin(radians(self.rotation))
|
|
|
|
x = (cosr * cos(angle) * self.radius.real - sinr * sin(angle) *
|
|
self.radius.imag + self.center.real)
|
|
y = (sinr * cos(angle) * self.radius.real + cosr * sin(angle) *
|
|
self.radius.imag + self.center.imag)
|
|
return complex(x, y)
|
|
|
|
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':
|
|
# Arc
|
|
radius = float(elements.pop()) + float(elements.pop()) * 1j
|
|
rotation = float(elements.pop())
|
|
arc = float(elements.pop())
|
|
sweep = float(elements.pop())
|
|
end = float(elements.pop()) + float(elements.pop()) * 1j
|
|
|
|
if not absolute:
|
|
if end == 0:
|
|
# Guard against a situation where arc start and end being same.
|
|
# That results division by zero issues in Arc parameterization.
|
|
end = 0.00009
|
|
end += current_pos
|
|
|
|
svg_arc = Arc(current_pos, radius, rotation, arc, sweep, end)
|
|
arc_points = []
|
|
for x in range(1, 5):
|
|
# There are infinite points in an arc, but for our context,
|
|
# define the arc using 5 points(0.2, 0.4, 0.6...)
|
|
arc_point = svg_arc.point(x*0.2)
|
|
arc_points.append((arc_point.real, arc_point.imag))
|
|
pen.qCurveTo(
|
|
(current_pos.real, current_pos.imag),
|
|
*arc_points,
|
|
(end.real, end.imag)
|
|
)
|
|
current_pos = end
|
|
|
|
# no final Z command, it's an open path
|
|
if start_pos is not None:
|
|
pen.endPath()
|