2022-12-06 01:25:17 +01:00
|
|
|
# SVG Path specification parser
|
|
|
|
|
|
|
|
import re
|
|
|
|
from . import path
|
|
|
|
import xml.etree.ElementTree as ET
|
|
|
|
import re
|
|
|
|
import math
|
|
|
|
|
|
|
|
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
|
|
|
|
UPPERCASE = set('MZLHVCSQTA')
|
|
|
|
|
2024-04-02 01:42:23 +02:00
|
|
|
COMMAND_RE = re.compile(r"([MmZzLlHhVvCcSsQqTtAa])")
|
|
|
|
FLOAT_RE = re.compile(r"[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
|
2022-12-06 01:25:17 +01:00
|
|
|
|
|
|
|
SVG_COLORS = {
|
|
|
|
"aliceblue": (0.941176,0.972549,1),
|
|
|
|
"antiquewhite": (0.980392,0.921569,0.843137),
|
|
|
|
"aqua": (0,1,1),
|
|
|
|
"aquamarine": (0.498039,1,0.831373),
|
|
|
|
"azure": (0.941176,1,1),
|
|
|
|
"beige": (0.960784,0.960784,0.862745),
|
|
|
|
"bisque": (1,0.894118,0.768627),
|
|
|
|
"black": (0,0,0),
|
|
|
|
"blanchedalmond": (1,0.921569,0.803922),
|
|
|
|
"blue": (0,0,1),
|
|
|
|
"blueviolet": (0.541176,0.168627,0.886275),
|
|
|
|
"brown": (0.647059,0.164706,0.164706),
|
|
|
|
"burlywood": (0.870588,0.721569,0.529412),
|
|
|
|
"cadetblue": (0.372549,0.619608,0.627451),
|
|
|
|
"chartreuse": (0.498039,1,0),
|
|
|
|
"chocolate": (0.823529,0.411765,0.117647),
|
|
|
|
"coral": (1,0.498039,0.313725),
|
|
|
|
"cornflowerblue": (0.392157,0.584314,0.929412),
|
|
|
|
"cornsilk": (1,0.972549,0.862745),
|
|
|
|
"crimson": (0.862745,0.0784314,0.235294),
|
|
|
|
"cyan": (0,1,1),
|
|
|
|
"darkblue": (0,0,0.545098),
|
|
|
|
"darkcyan": (0,0.545098,0.545098),
|
|
|
|
"darkgoldenrod": (0.721569,0.52549,0.0431373),
|
|
|
|
"darkgray": (0.662745,0.662745,0.662745),
|
|
|
|
"darkgreen": (0,0.392157,0),
|
|
|
|
"darkgrey": (0.662745,0.662745,0.662745),
|
|
|
|
"darkkhaki": (0.741176,0.717647,0.419608),
|
|
|
|
"darkmagenta": (0.545098,0,0.545098),
|
|
|
|
"darkolivegreen": (0.333333,0.419608,0.184314),
|
|
|
|
"darkorange": (1,0.54902,0),
|
|
|
|
"darkorchid": (0.6,0.196078,0.8),
|
|
|
|
"darkred": (0.545098,0,0),
|
|
|
|
"darksalmon": (0.913725,0.588235,0.478431),
|
|
|
|
"darkseagreen": (0.560784,0.737255,0.560784),
|
|
|
|
"darkslateblue": (0.282353,0.239216,0.545098),
|
|
|
|
"darkslategray": (0.184314,0.309804,0.309804),
|
|
|
|
"darkslategrey": (0.184314,0.309804,0.309804),
|
|
|
|
"darkturquoise": (0,0.807843,0.819608),
|
|
|
|
"darkviolet": (0.580392,0,0.827451),
|
|
|
|
"deeppink": (1,0.0784314,0.576471),
|
|
|
|
"deepskyblue": (0,0.74902,1),
|
|
|
|
"dimgray": (0.411765,0.411765,0.411765),
|
|
|
|
"dimgrey": (0.411765,0.411765,0.411765),
|
|
|
|
"dodgerblue": (0.117647,0.564706,1),
|
|
|
|
"firebrick": (0.698039,0.133333,0.133333),
|
|
|
|
"floralwhite": (1,0.980392,0.941176),
|
|
|
|
"forestgreen": (0.133333,0.545098,0.133333),
|
|
|
|
"fuchsia": (1,0,1),
|
|
|
|
"gainsboro": (0.862745,0.862745,0.862745),
|
|
|
|
"ghostwhite": (0.972549,0.972549,1),
|
|
|
|
"gold": (1,0.843137,0),
|
|
|
|
"goldenrod": (0.854902,0.647059,0.12549),
|
|
|
|
"gray": (0.501961,0.501961,0.501961),
|
|
|
|
"grey": (0.501961,0.501961,0.501961),
|
|
|
|
"green": (0,0.501961,0),
|
|
|
|
"greenyellow": (0.678431,1,0.184314),
|
|
|
|
"honeydew": (0.941176,1,0.941176),
|
|
|
|
"hotpink": (1,0.411765,0.705882),
|
|
|
|
"indianred": (0.803922,0.360784,0.360784),
|
|
|
|
"indigo": (0.294118,0,0.509804),
|
|
|
|
"ivory": (1,1,0.941176),
|
|
|
|
"khaki": (0.941176,0.901961,0.54902),
|
|
|
|
"lavender": (0.901961,0.901961,0.980392),
|
|
|
|
"lavenderblush": (1,0.941176,0.960784),
|
|
|
|
"lawngreen": (0.486275,0.988235,0),
|
|
|
|
"lemonchiffon": (1,0.980392,0.803922),
|
|
|
|
"lightblue": (0.678431,0.847059,0.901961),
|
|
|
|
"lightcoral": (0.941176,0.501961,0.501961),
|
|
|
|
"lightcyan": (0.878431,1,1),
|
|
|
|
"lightgoldenrodyellow": (0.980392,0.980392,0.823529),
|
|
|
|
"lightgray": (0.827451,0.827451,0.827451),
|
|
|
|
"lightgreen": (0.564706,0.933333,0.564706),
|
|
|
|
"lightgrey": (0.827451,0.827451,0.827451),
|
|
|
|
"lightpink": (1,0.713725,0.756863),
|
|
|
|
"lightsalmon": (1,0.627451,0.478431),
|
|
|
|
"lightseagreen": (0.12549,0.698039,0.666667),
|
|
|
|
"lightskyblue": (0.529412,0.807843,0.980392),
|
|
|
|
"lightslategray": (0.466667,0.533333,0.6),
|
|
|
|
"lightslategrey": (0.466667,0.533333,0.6),
|
|
|
|
"lightsteelblue": (0.690196,0.768627,0.870588),
|
|
|
|
"lightyellow": (1,1,0.878431),
|
|
|
|
"lime": (0,1,0),
|
|
|
|
"limegreen": (0.196078,0.803922,0.196078),
|
|
|
|
"linen": (0.980392,0.941176,0.901961),
|
|
|
|
"magenta": (1,0,1),
|
|
|
|
"maroon": (0.501961,0,0),
|
|
|
|
"mediumaquamarine": (0.4,0.803922,0.666667),
|
|
|
|
"mediumblue": (0,0,0.803922),
|
|
|
|
"mediumorchid": (0.729412,0.333333,0.827451),
|
|
|
|
"mediumpurple": (0.576471,0.439216,0.858824),
|
|
|
|
"mediumseagreen": (0.235294,0.701961,0.443137),
|
|
|
|
"mediumslateblue": (0.482353,0.407843,0.933333),
|
|
|
|
"mediumspringgreen": (0,0.980392,0.603922),
|
|
|
|
"mediumturquoise": (0.282353,0.819608,0.8),
|
|
|
|
"mediumvioletred": (0.780392,0.0823529,0.521569),
|
|
|
|
"midnightblue": (0.0980392,0.0980392,0.439216),
|
|
|
|
"mintcream": (0.960784,1,0.980392),
|
|
|
|
"mistyrose": (1,0.894118,0.882353),
|
|
|
|
"moccasin": (1,0.894118,0.709804),
|
|
|
|
"navajowhite": (1,0.870588,0.678431),
|
|
|
|
"navy": (0,0,0.501961),
|
|
|
|
"oldlace": (0.992157,0.960784,0.901961),
|
|
|
|
"olive": (0.501961,0.501961,0),
|
|
|
|
"olivedrab": (0.419608,0.556863,0.137255),
|
|
|
|
"orange": (1,0.647059,0),
|
|
|
|
"orangered": (1,0.270588,0),
|
|
|
|
"orchid": (0.854902,0.439216,0.839216),
|
|
|
|
"palegoldenrod": (0.933333,0.909804,0.666667),
|
|
|
|
"palegreen": (0.596078,0.984314,0.596078),
|
|
|
|
"paleturquoise": (0.686275,0.933333,0.933333),
|
|
|
|
"palevioletred": (0.858824,0.439216,0.576471),
|
|
|
|
"papayawhip": (1,0.937255,0.835294),
|
|
|
|
"peachpuff": (1,0.854902,0.72549),
|
|
|
|
"peru": (0.803922,0.521569,0.247059),
|
|
|
|
"pink": (1,0.752941,0.796078),
|
|
|
|
"plum": (0.866667,0.627451,0.866667),
|
|
|
|
"powderblue": (0.690196,0.878431,0.901961),
|
|
|
|
"purple": (0.501961,0,0.501961),
|
|
|
|
"red": (1,0,0),
|
|
|
|
"rosybrown": (0.737255,0.560784,0.560784),
|
|
|
|
"royalblue": (0.254902,0.411765,0.882353),
|
|
|
|
"saddlebrown": (0.545098,0.270588,0.0745098),
|
|
|
|
"salmon": (0.980392,0.501961,0.447059),
|
|
|
|
"sandybrown": (0.956863,0.643137,0.376471),
|
|
|
|
"seagreen": (0.180392,0.545098,0.341176),
|
|
|
|
"seashell": (1,0.960784,0.933333),
|
|
|
|
"sienna": (0.627451,0.321569,0.176471),
|
|
|
|
"silver": (0.752941,0.752941,0.752941),
|
|
|
|
"skyblue": (0.529412,0.807843,0.921569),
|
|
|
|
"slateblue": (0.415686,0.352941,0.803922),
|
|
|
|
"slategray": (0.439216,0.501961,0.564706),
|
|
|
|
"slategrey": (0.439216,0.501961,0.564706),
|
|
|
|
"snow": (1,0.980392,0.980392),
|
|
|
|
"springgreen": (0,1,0.498039),
|
|
|
|
"steelblue": (0.27451,0.509804,0.705882),
|
|
|
|
"tan": (0.823529,0.705882,0.54902),
|
|
|
|
"teal": (0,0.501961,0.501961),
|
|
|
|
"thistle": (0.847059,0.74902,0.847059),
|
|
|
|
"tomato": (1,0.388235,0.278431),
|
|
|
|
"turquoise": (0.25098,0.878431,0.815686),
|
|
|
|
"violet": (0.933333,0.509804,0.933333),
|
|
|
|
"wheat": (0.960784,0.870588,0.701961),
|
|
|
|
"white": (1,1,1),
|
|
|
|
"whitesmoke": (0.960784,0.960784,0.960784),
|
|
|
|
"yellow": (1,1,0),
|
|
|
|
"yellowgreen": (0.603922,0.803922,0.196078),
|
|
|
|
}
|
|
|
|
|
|
|
|
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 applyMatrix(matrix, z):
|
|
|
|
return complex(z.real * matrix[0] + z.imag * matrix[1] + matrix[2],
|
|
|
|
z.real * matrix[3] + z.imag * matrix[4] + matrix[5] )
|
|
|
|
|
|
|
|
def matrixMultiply(matrix1, matrix2):
|
|
|
|
if matrix1 is None:
|
|
|
|
return matrix2
|
|
|
|
elif matrix2 is None:
|
|
|
|
return matrix1
|
|
|
|
|
|
|
|
m1 = [matrix1[0:3], matrix1[3:6] ] # don't need last row
|
|
|
|
m2 = [matrix2[0:3], matrix2[3:6], [0,0,1]]
|
|
|
|
|
|
|
|
out = []
|
|
|
|
|
|
|
|
for i in range(2):
|
|
|
|
for j in range(3):
|
|
|
|
out.append( sum(m1[i][k]*m2[k][j] for k in range(3)) )
|
|
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
def parse_path(pathdef, current_pos=0j, matrix = None, svgState=None):
|
|
|
|
if matrix is None:
|
|
|
|
scaler=lambda z : z
|
|
|
|
else:
|
|
|
|
scaler=lambda z : applyMatrix(matrix, z)
|
|
|
|
if svgState is None:
|
|
|
|
svgState = path.SVGState()
|
|
|
|
|
|
|
|
# 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.
|
|
|
|
elements = list(_tokenize_path(pathdef))
|
|
|
|
# Reverse for easy use of .pop()
|
|
|
|
elements.reverse()
|
|
|
|
|
|
|
|
segments = path.Path(svgState = svgState)
|
|
|
|
start_pos = None
|
|
|
|
command = 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
|
|
|
|
|
|
|
|
# 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:
|
|
|
|
segments.append(path.Line(scaler(current_pos), scaler(start_pos)))
|
|
|
|
if len(segments):
|
|
|
|
segments.closed = True
|
|
|
|
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
|
|
|
|
segments.append(path.Line(scaler(current_pos), scaler(pos)))
|
|
|
|
current_pos = pos
|
|
|
|
|
|
|
|
elif command == 'H':
|
|
|
|
x = elements.pop()
|
|
|
|
pos = float(x) + current_pos.imag * 1j
|
|
|
|
if not absolute:
|
|
|
|
pos += current_pos.real
|
|
|
|
segments.append(path.Line(scaler(current_pos), scaler(pos)))
|
|
|
|
current_pos = pos
|
|
|
|
|
|
|
|
elif command == 'V':
|
|
|
|
y = elements.pop()
|
|
|
|
pos = current_pos.real + float(y) * 1j
|
|
|
|
if not absolute:
|
|
|
|
pos += current_pos.imag * 1j
|
|
|
|
segments.append(path.Line(scaler(current_pos), scaler(pos)))
|
|
|
|
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
|
|
|
|
|
|
|
|
segments.append(path.CubicBezier(scaler(current_pos), scaler(control1), scaler(control2), scaler(end)))
|
|
|
|
current_pos = end
|
|
|
|
|
|
|
|
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 = scaler(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 = 2 * scaler(current_pos) - segments[-1].control2
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
segments.append(path.CubicBezier(scaler(current_pos), control1, scaler(control2), scaler(end)))
|
|
|
|
current_pos = end
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
segments.append(path.QuadraticBezier(scaler(current_pos), scaler(control), scaler(end)))
|
|
|
|
current_pos = end
|
|
|
|
|
|
|
|
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 = scaler(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 = 2 * scaler(current_pos) - segments[-1].control
|
|
|
|
|
|
|
|
end = float(elements.pop()) + float(elements.pop()) * 1j
|
|
|
|
|
|
|
|
if not absolute:
|
|
|
|
end += current_pos
|
|
|
|
|
|
|
|
segments.append(path.QuadraticBezier(scaler(current_pos), control, scaler(end)))
|
|
|
|
current_pos = end
|
|
|
|
|
|
|
|
elif command == 'A':
|
|
|
|
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:
|
|
|
|
end += current_pos
|
|
|
|
|
|
|
|
segments.append(path.Arc(current_pos, radius, rotation, arc, sweep, end, scaler))
|
|
|
|
current_pos = end
|
|
|
|
|
|
|
|
return segments
|
|
|
|
|
|
|
|
def path_from_ellipse(x, y, rx, ry, matrix, state):
|
|
|
|
arc = "M %.9f %.9f " % (x-rx,y)
|
|
|
|
arc += "A %.9f %.9f 0 0 1 %.9f %.9f " % (rx, ry, x+rx,y)
|
|
|
|
arc += "A %.9f %.9f 0 0 1 %.9f %.9f" % (rx, ry, x-rx,y)
|
|
|
|
return parse_path(arc, matrix=matrix, svgState=state)
|
|
|
|
|
|
|
|
def path_from_rect(x,y,w,h,rx,ry, matrix,state):
|
|
|
|
if not rx and not ry:
|
|
|
|
rect = "M %.9f %.9f h %.9f v %.9f h %.9f Z" % (x,y,w,h,-w)
|
|
|
|
else:
|
|
|
|
if rx is None:
|
|
|
|
rx = ry
|
|
|
|
elif ry is None:
|
|
|
|
ry = rx
|
|
|
|
rect = "M %.9f %.9f h %.9f " % (x+rx,y,w-2*rx)
|
|
|
|
rect += "a %.9f %.9f 0 0 1 %.9f %.9f " % (rx, ry, rx, ry)
|
|
|
|
rect += "v %.9f " % (h-2*ry)
|
|
|
|
rect += "a %.9f %.9f 0 0 1 %.9f %.9f " % (rx, ry, -rx, ry)
|
|
|
|
rect += "h %.9f " % -(w-2*rx)
|
|
|
|
rect += "a %.9f %.9f 0 0 1 %.9f %.9f " % (rx, ry, -rx, -ry)
|
|
|
|
rect += "v %.9f " % -(h-2*ry)
|
|
|
|
rect += "a %.9f %.9f 0 0 1 %.9f %.9f Z" % (rx, ry, rx, -ry)
|
|
|
|
return parse_path(rect, matrix=matrix, svgState=state)
|
|
|
|
|
|
|
|
def sizeFromString(text):
|
|
|
|
"""
|
|
|
|
Returns size in mm, if possible.
|
|
|
|
"""
|
|
|
|
text = re.sub(r'\s',r'', text)
|
|
|
|
try:
|
|
|
|
return float(text)*25.4/96 # px
|
|
|
|
except:
|
|
|
|
if text[-1] == '%':
|
|
|
|
return float(text[:-1]) # NOT mm
|
|
|
|
units = text[-2:].lower()
|
|
|
|
x = float(text[:-2])
|
|
|
|
convert = { 'mm':1, 'cm':10, 'in':25.4, 'px':25.4/96, 'pt':25.4/72, 'pc':12*25.4/72 }
|
|
|
|
try:
|
|
|
|
return x * convert[units]
|
|
|
|
except:
|
|
|
|
return x # NOT mm
|
|
|
|
|
|
|
|
def rgbFromColor(colorName):
|
|
|
|
colorName = colorName.strip().lower()
|
|
|
|
if colorName == 'none':
|
|
|
|
return None
|
|
|
|
cmd = re.split(r'[\s(),]+', colorName)
|
|
|
|
if cmd[0] == 'rgb':
|
|
|
|
colors = cmd[1:4]
|
|
|
|
outColor = []
|
|
|
|
for c in colors:
|
|
|
|
if c.endswith('%'):
|
|
|
|
outColor.append(float(c[:-1]) / 100.)
|
|
|
|
else:
|
|
|
|
outColor.append(float(c) / 255.)
|
|
|
|
return tuple(outColor)
|
|
|
|
elif colorName.startswith('#'):
|
|
|
|
if len(colorName) == 4:
|
|
|
|
return (int(colorName[1],16)/15., int(colorName[2],16)/15., int(colorName[3],16)/15.)
|
|
|
|
else:
|
|
|
|
return (int(colorName[1:3],16)/255., int(colorName[3:5],16)/255., int(colorName[5:7],16)/255.)
|
|
|
|
else:
|
|
|
|
return SVG_COLORS[colorName]
|
|
|
|
|
|
|
|
|
|
|
|
def getPathsFromSVG(svg):
|
|
|
|
def updateStateCommand(state,cmd,arg):
|
|
|
|
if cmd == 'fill':
|
|
|
|
state.fill = rgbFromColor(arg)
|
|
|
|
elif cmd == 'fill-opacity':
|
|
|
|
state.fillOpacity = float(arg)
|
|
|
|
elif cmd == 'fill-rule':
|
|
|
|
state.fillRule = arg
|
|
|
|
# if state.fill is None:
|
|
|
|
# state.fill = (0.,0.,0.)
|
|
|
|
elif cmd == 'stroke':
|
|
|
|
state.stroke = rgbFromColor(arg)
|
|
|
|
elif cmd == 'stroke-opacity':
|
|
|
|
state.strokeOpacity = rgbFromColor(arg)
|
|
|
|
elif cmd == 'stroke-width':
|
|
|
|
state.strokeWidth = float(arg)
|
|
|
|
elif cmd == 'vector-effect':
|
|
|
|
state.strokeWidthScaling = 'non-scaling-stroke' not in cmd
|
|
|
|
# todo better scaling for non-uniform cases?
|
|
|
|
|
|
|
|
def updateState(tree,state,matrix):
|
|
|
|
state = state.clone()
|
|
|
|
try:
|
|
|
|
style = re.sub(r'\s',r'', tree.attrib['style']).lower()
|
|
|
|
for item in style.split(';'):
|
|
|
|
cmd,arg = item.split(':')[:2]
|
|
|
|
updateStateCommand(state,cmd,arg)
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
|
|
|
for item in tree.attrib:
|
|
|
|
try:
|
|
|
|
updateStateCommand(state,item,tree.attrib[item])
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
|
|
|
if state.strokeWidth and state.strokeWidthScaling:
|
|
|
|
# this won't work great for non-uniform scaling
|
|
|
|
h = abs(applyMatrix(matrix, complex(0,state.strokeWidth)) - applyMatrix(matrix, 0j))
|
|
|
|
w = abs(applyMatrix(matrix, complex(state.strokeWidth,0)) - applyMatrix(matrix, 0j))
|
|
|
|
state.strokeWidth = (h+w)/2
|
|
|
|
return state
|
|
|
|
|
|
|
|
def reorder(a,b,c,d,e,f):
|
|
|
|
return [a,c,e, b,d,f]
|
|
|
|
|
|
|
|
def updateMatrix(tree, matrix):
|
|
|
|
try:
|
|
|
|
transformList = re.split(r'\)[\s,]+', tree.attrib['transform'].strip().lower())
|
|
|
|
except KeyError:
|
|
|
|
return matrix
|
|
|
|
|
|
|
|
for transform in transformList:
|
|
|
|
cmd = re.split(r'[,()\s]+', transform)
|
|
|
|
|
|
|
|
updateMatrix = None
|
|
|
|
|
|
|
|
if cmd[0] == 'matrix':
|
|
|
|
updateMatrix = reorder(*list(map(float, cmd[1:7])))
|
|
|
|
elif cmd[0] == 'translate':
|
|
|
|
x = float(cmd[1])
|
|
|
|
if len(cmd) >= 3 and cmd[2] != '':
|
|
|
|
y = float(cmd[2])
|
|
|
|
else:
|
|
|
|
y = 0
|
|
|
|
updateMatrix = reorder(1,0,0,1,x,y)
|
|
|
|
elif cmd[0] == 'scale':
|
|
|
|
x = float(cmd[1])
|
|
|
|
if len(cmd) >= 3 and cmd[2] != '':
|
|
|
|
y = float(cmd[2])
|
|
|
|
else:
|
|
|
|
y = x
|
|
|
|
updateMatrix = reorder(x,0,0, y,0,0)
|
|
|
|
elif cmd[0] == 'rotate':
|
|
|
|
theta = float(cmd[1]) * math.pi / 180.
|
|
|
|
c = math.cos(theta)
|
|
|
|
s = math.sin(theta)
|
|
|
|
updateMatrix = [c, -s, 0, s, c, 0]
|
|
|
|
if len(cmd) >= 4 and cmd[2] != '':
|
|
|
|
x = float(cmd[2])
|
|
|
|
y = float(cmd[3])
|
|
|
|
updateMatrix = matrixMultiply(updateMatrix, [1,0,-x, 0,1,-y])
|
|
|
|
updateMatrix = matrixMultiply([1,0,x, 0,1,y], updateMatrix)
|
|
|
|
elif cmd[0] == 'skewX':
|
|
|
|
theta = float(cmd[1]) * math.pi / 180.
|
|
|
|
updateMatrix = [1, math.tan(theta), 0, 0,1,0]
|
|
|
|
elif cmd[0] == 'skewY':
|
|
|
|
theta = float(cmd[1]) * math.pi / 180.
|
|
|
|
updateMatrix = [1,0,0, math.tan(theta),1,0]
|
|
|
|
|
|
|
|
matrix = matrixMultiply(matrix, updateMatrix)
|
|
|
|
|
|
|
|
return matrix
|
|
|
|
|
|
|
|
def updateStateAndMatrix(tree,state,matrix):
|
|
|
|
matrix = updateMatrix(tree,matrix)
|
|
|
|
return updateState(tree,state,matrix),matrix
|
|
|
|
|
|
|
|
def getPaths(paths, matrix, tree, state, savedElements):
|
|
|
|
def getFloat(attribute,default=0.):
|
|
|
|
try:
|
|
|
|
return float(tree.attrib[attribute].strip())
|
|
|
|
except KeyError:
|
|
|
|
return default
|
|
|
|
|
|
|
|
tag = re.sub(r'.*}', '', tree.tag).lower()
|
|
|
|
try:
|
|
|
|
savedElements[tree.attrib['id']] = tree
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
state, matrix = updateStateAndMatrix(tree, state, matrix)
|
|
|
|
if tag == 'path':
|
|
|
|
path = parse_path(tree.attrib['d'], matrix=matrix, svgState=state)
|
|
|
|
if len(path):
|
|
|
|
paths.append(path)
|
|
|
|
elif tag == 'circle':
|
|
|
|
path = path_from_ellipse(getFloat('cx'), getFloat('cy'), getFloat('r'), getFloat('r'), matrix, state)
|
|
|
|
paths.append(path)
|
|
|
|
elif tag == 'ellipse':
|
|
|
|
path = path_from_ellipse(getFloat('cx'), getFloat('cy'), getFloat('rx'), getFloat('ry'), matrix, state)
|
|
|
|
paths.append(path)
|
|
|
|
elif tag == 'line':
|
|
|
|
x1 = getFloat('x1')
|
|
|
|
y1 = getFloat('y1')
|
|
|
|
x2 = getFloat('x2')
|
|
|
|
y2 = getFloat('y2')
|
|
|
|
p = 'M %.9f %.9f L %.9f %.9f' % (x1,y1,x2,y2)
|
|
|
|
path = parse_path(p, matrix=matrix, svgState=state)
|
|
|
|
paths.append(path)
|
|
|
|
elif tag == 'polygon':
|
|
|
|
points = re.split(r'[\s,]+', tree.attrib['points'].strip())
|
|
|
|
p = ' '.join(['M', points[0], points[1], 'L'] + points[2:] + ['Z'])
|
|
|
|
path = parse_path(p, matrix=matrix, svgState=state)
|
|
|
|
paths.append(path)
|
|
|
|
elif tag == 'polyline':
|
|
|
|
points = re.split(r'[\s,]+', tree.attrib['points'].strip())
|
|
|
|
p = ' '.join(['M', points[0], points[1], 'L'] + points[2:])
|
|
|
|
path = parse_path(p, matrix=matrix, svgState=state)
|
|
|
|
paths.append(path)
|
|
|
|
elif tag == 'rect':
|
|
|
|
x = getFloat('x')
|
|
|
|
y = getFloat('y')
|
|
|
|
w = getFloat('width')
|
|
|
|
h = getFloat('height')
|
|
|
|
rx = getFloat('rx',default=None)
|
|
|
|
ry = getFloat('ry',default=None)
|
|
|
|
path = path_from_rect(x,y,w,h,rx,ry, matrix,state)
|
|
|
|
paths.append(path)
|
|
|
|
elif tag == 'g' or tag == 'svg':
|
|
|
|
for child in tree:
|
|
|
|
getPaths(paths, matrix, child, state, savedElements)
|
|
|
|
elif tag == 'use':
|
|
|
|
try:
|
|
|
|
link = None
|
|
|
|
for tag in tree.attrib:
|
|
|
|
if tag.strip().lower().endswith("}href"):
|
|
|
|
link = tree.attrib[tag]
|
|
|
|
break
|
|
|
|
if link is None or link[0] != '#':
|
|
|
|
raise KeyError
|
|
|
|
source = savedElements[link[1:]]
|
|
|
|
x = 0
|
|
|
|
y = 0
|
|
|
|
try:
|
|
|
|
x = float(tree.attrib['x'])
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
try:
|
|
|
|
y = float(tree.attrib['y'])
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
# TODO: handle width and height? (Inkscape does not)
|
|
|
|
matrix = matrixMultiply(matrix, reorder(1,0,0,1,x,y))
|
|
|
|
getPaths(paths, matrix, source, state, dict(savedElements))
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def scale(width, height, viewBox, z):
|
|
|
|
x = (z.real - viewBox[0]) / (viewBox[2] - viewBox[0]) * width
|
|
|
|
y = (viewBox[3]-z.imag) / (viewBox[3] - viewBox[1]) * height
|
|
|
|
return complex(x,y)
|
|
|
|
|
|
|
|
paths = []
|
|
|
|
|
|
|
|
try:
|
|
|
|
width = sizeFromString(svg.attrib['width'].strip())
|
|
|
|
except KeyError:
|
|
|
|
width = None
|
|
|
|
try:
|
|
|
|
height = sizeFromString(svg.attrib['height'].strip())
|
|
|
|
except KeyError:
|
|
|
|
height = None
|
|
|
|
|
|
|
|
try:
|
|
|
|
viewBox = list(map(float, re.split(r'[\s,]+', svg.attrib['viewBox'].strip())))
|
|
|
|
except KeyError:
|
|
|
|
if width is None or height is None:
|
|
|
|
raise KeyError
|
|
|
|
viewBox = [0, 0, width*96/25.4, height*96/25.4]
|
|
|
|
|
|
|
|
if width is None:
|
|
|
|
width = viewBox[2] * 25.4/96
|
|
|
|
|
|
|
|
if height is None:
|
|
|
|
height = viewBox[3] * 25.4/96
|
|
|
|
|
|
|
|
viewBoxWidth = viewBox[2]
|
|
|
|
viewBoxHeight = viewBox[3]
|
|
|
|
|
|
|
|
viewBox[2] += viewBox[0]
|
|
|
|
viewBox[3] += viewBox[1]
|
|
|
|
|
|
|
|
try:
|
|
|
|
preserve = svg.attrib['preserveAspectRatio'].strip().lower().split()
|
|
|
|
if len(preserve[0]) != 8:
|
|
|
|
raise KeyError
|
|
|
|
if len(preserve)>=2 and preserve[1] == 'slice':
|
|
|
|
if viewBoxWidth/viewBoxHeight > width/height:
|
|
|
|
# viewbox is wider than viewport, so scale by height to ensure
|
|
|
|
# viewbox covers the viewport
|
|
|
|
rescale = height / viewBoxHeight
|
|
|
|
else:
|
|
|
|
rescale = width / viewBoxWidth
|
|
|
|
else:
|
|
|
|
if viewBoxWidth/viewBoxHeight > width/height:
|
|
|
|
# viewbox is wider than viewport, so scale by width to ensure
|
|
|
|
# viewport covers the viewbox
|
|
|
|
rescale = width / viewBoxWidth
|
|
|
|
else:
|
|
|
|
rescale = height / viewBoxHeight
|
|
|
|
matrix = [rescale, 0, 0,
|
|
|
|
0, rescale, 0];
|
|
|
|
|
|
|
|
if preserve[0][0:4] == 'xmin':
|
|
|
|
# viewBox[0] to 0
|
|
|
|
matrix[2] = -viewBox[0] * rescale
|
|
|
|
elif preserve[0][0:4] == 'xmid':
|
|
|
|
# viewBox[0] to width/2
|
|
|
|
matrix[2] = -viewBox[0] * rescale + width/2
|
|
|
|
else: # preserve[0][0:4] == 'xmax':
|
|
|
|
# viewBox[0] to width
|
|
|
|
matrix[2] = -viewBox[0] * rescale + width
|
|
|
|
|
|
|
|
if preserve[0][4:8] == 'ymin':
|
|
|
|
# viewBox[1] to 0
|
|
|
|
matrix[5] = -viewBox[1] * rescale
|
|
|
|
elif preserve[0][4:8] == 'ymid':
|
|
|
|
# viewBox[0] to width/2
|
|
|
|
matrix[5] = -viewBox[1] * rescale + height/2
|
|
|
|
else: # preserve[0][4:8] == 'xmax':
|
|
|
|
# viewBox[0] to width
|
|
|
|
matrix[5] = -viewBox[1] * rescale + height
|
|
|
|
except:
|
|
|
|
matrix = [ width/viewBoxWidth, 0, -viewBox[0]* width/viewBoxWidth,
|
|
|
|
0, -height/viewBoxHeight, viewBox[3]*height/viewBoxHeight ]
|
|
|
|
|
|
|
|
getPaths(paths, matrix, svg, path.SVGState(), {})
|
|
|
|
|
|
|
|
return ( paths, applyMatrix(matrix, complex(viewBox[0], viewBox[1])),
|
|
|
|
applyMatrix(matrix, complex(viewBox[2], viewBox[3])) )
|
|
|
|
|
|
|
|
def getPathsFromSVGFile(filename):
|
|
|
|
return getPathsFromSVG(ET.parse(filename).getroot())
|
|
|
|
|