- some more work in the Excellon Editor - Drill adding
- some fixes in the image loader when clicking the an image that is in the list of previous loaded files
- a small fin in the SVG parser
- added a new Tcl command that is returning the name of the active object ('get_active') to accompany the 'set_active' command
- a small fix for the 2d graphic mode by replacing the q5agg matplotlib backend with the qtagg backend which should work with the Qt6
799 lines
27 KiB
Python
799 lines
27 KiB
Python
# ##########################################################
|
|
# FlatCAM: 2D Post-processing for Manufacturing #
|
|
# http://flatcam.org #
|
|
# Author: Juan Pablo Caram (c) #
|
|
# Date: 12/18/2015 #
|
|
# MIT Licence #
|
|
# #
|
|
# SVG Features supported: #
|
|
# * Groups #
|
|
# * Rectangles (w/ rounded corners) #
|
|
# * Circles #
|
|
# * Ellipses #
|
|
# * Polygons #
|
|
# * Polylines #
|
|
# * Lines #
|
|
# * Paths #
|
|
# * All transformations #
|
|
# #
|
|
# Reference: www.w3.org/TR/SVG/Overview.html #
|
|
# ##########################################################
|
|
|
|
# import xml.etree.ElementTree as ET
|
|
from svg.path import Line, Arc, CubicBezier, QuadraticBezier, parse_path
|
|
# from svg.path.path import Move
|
|
# from svg.path.path import Close
|
|
import svg.path
|
|
from shapely.geometry import LineString, MultiLineString, Point
|
|
from shapely.affinity import skew, affine_transform, rotate
|
|
import numpy as np
|
|
|
|
from appParsers.ParseFont import *
|
|
|
|
log = logging.getLogger('base2')
|
|
|
|
|
|
def svgparselength(lengthstr):
|
|
"""
|
|
Parse an SVG length string into a float and a units
|
|
string, if any.
|
|
|
|
:param lengthstr: SVG length string.
|
|
:return: Number and units pair.
|
|
:rtype: tuple(float, str|None)
|
|
"""
|
|
|
|
integer_re_str = r'[+-]?[0-9]+'
|
|
number_re_str = r'(?:[+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?' + r')|' + \
|
|
r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?)'
|
|
length_re_str = r'(' + number_re_str + r')(em|ex|px|in|cm|mm|pt|pc|%)?'
|
|
|
|
if lengthstr:
|
|
match = re.search(length_re_str, lengthstr)
|
|
if match:
|
|
return float(match.group(1)), match.group(2)
|
|
else:
|
|
return 0, 0
|
|
|
|
return
|
|
|
|
|
|
def svgparse_viewbox(root):
|
|
val = root.get('viewBox')
|
|
if val is None:
|
|
return 1.0
|
|
|
|
res = [float(x) for x in val.split()] or [float(x) for x in val.split(',')]
|
|
w = svgparselength(root.get('width'))[0]
|
|
# h = svgparselength(root.get('height'))[0]
|
|
|
|
v_w = res[2]
|
|
# v_h = res[3]
|
|
|
|
return w / v_w
|
|
|
|
|
|
def path2shapely(path, object_type, res=1.0, units='MM', factor=1.0):
|
|
"""
|
|
Converts an svg.path.Path into a Shapely
|
|
Polygon or LinearString.
|
|
|
|
:param path: svg.path.Path instance
|
|
:param object_type:
|
|
:param res: Resolution (minimum step along path)
|
|
:param units: FlatCAM units
|
|
:type units: str
|
|
:param factor: correction factor due of virtual units
|
|
:type factor: float
|
|
:return: Shapely geometry object
|
|
:rtype : Polygon
|
|
:rtype : LineString
|
|
"""
|
|
|
|
points = []
|
|
geometry = []
|
|
|
|
rings = []
|
|
closed = False
|
|
|
|
for component in path:
|
|
# Line
|
|
if isinstance(component, Line):
|
|
start = component.start
|
|
x, y = start.real, start.imag
|
|
if len(points) == 0 or points[-1] != (x, y):
|
|
points.append((x, y))
|
|
end = component.end
|
|
points.append((factor * end.real, factor * end.imag))
|
|
continue
|
|
|
|
# Arc, CubicBezier or QuadraticBezier
|
|
if isinstance(component, Arc) or \
|
|
isinstance(component, CubicBezier) or \
|
|
isinstance(component, QuadraticBezier):
|
|
|
|
# How many points to use in the discrete representation.
|
|
length = component.length(res / 10.0)
|
|
# steps = int(length / res + 0.5)
|
|
steps = int(length) * 2
|
|
|
|
if units == 'IN':
|
|
steps *= 25
|
|
|
|
# solve error when step is below 1,
|
|
# it may cause other problems, but LineString needs at least two points
|
|
# later edit: made the minimum nr of steps to be 10; left it like that to see that steps can be 0
|
|
if steps == 0 or steps < 10:
|
|
steps = 10
|
|
|
|
frac = 1.0 / steps
|
|
|
|
# print length, steps, frac
|
|
for i in range(steps):
|
|
point = component.point(i * frac)
|
|
x, y = point.real, point.imag
|
|
if len(points) == 0 or points[-1] != (x, y):
|
|
points.append((factor * x, factor * y))
|
|
end = component.point(1.0)
|
|
points.append((factor * end.real, factor * end.imag))
|
|
continue
|
|
|
|
# Move
|
|
if isinstance(component, svg.path.Move):
|
|
if not points:
|
|
continue
|
|
else:
|
|
rings.append(points)
|
|
if closed is False:
|
|
points = []
|
|
else:
|
|
closed = False
|
|
start = component.start
|
|
x, y = start.real, start.imag
|
|
points = [(factor * x, factor * y)]
|
|
continue
|
|
|
|
closed = False
|
|
|
|
# Close
|
|
if isinstance(component, svg.path.Close):
|
|
if not points:
|
|
continue
|
|
else:
|
|
rings.append(points)
|
|
points = []
|
|
closed = True
|
|
continue
|
|
log.warning("I don't know what this is: %s" % str(component))
|
|
continue
|
|
|
|
# if there are still points in points then add them to the last ring
|
|
|
|
if points:
|
|
rings.append(points)
|
|
|
|
try:
|
|
rings = MultiLineString(rings)
|
|
except Exception as e:
|
|
log.error("ParseSVG.path2shapely() MString --> %s" % str(e))
|
|
return None
|
|
|
|
rings_len = len(rings.geoms)
|
|
if rings_len > 0:
|
|
if rings_len == 1 and not isinstance(rings, MultiLineString):
|
|
# Polygons are closed and require more than 2 points
|
|
if Point(rings.geoms[0][0]).almost_equals(Point(rings.geoms[0][-1])) and len(rings.geoms[0]) > 2:
|
|
geo_element = Polygon(rings.geoms[0])
|
|
else:
|
|
geo_element = LineString(rings.geoms[0])
|
|
else:
|
|
try:
|
|
geo_element = Polygon(rings.geoms[0], rings.geoms[1:])
|
|
except Exception:
|
|
coords = []
|
|
for line in rings.geoms:
|
|
coords.append(line.coords[0])
|
|
coords.append(line.coords[1])
|
|
try:
|
|
geo_element = Polygon(coords)
|
|
except Exception:
|
|
geo_element = LineString(coords)
|
|
geometry.append(geo_element)
|
|
return geometry
|
|
|
|
|
|
def svgrect2shapely(rect, n_points=32, factor=1.0):
|
|
"""
|
|
Converts an SVG rect into Shapely geometry.
|
|
|
|
:param rect: Rect Element
|
|
:type rect: xml.etree.ElementTree.Element
|
|
:param n_points: number of points to approximate rectangles corners when having rounded corners
|
|
:type n_points: int
|
|
:param factor: correction factor due of virtual units
|
|
:type factor: float
|
|
:return: shapely.geometry.polygon.LinearRing
|
|
"""
|
|
w = svgparselength(rect.get('width'))[0]
|
|
h = svgparselength(rect.get('height'))[0]
|
|
|
|
x_obj = rect.get('x')
|
|
if x_obj is not None:
|
|
x = svgparselength(x_obj)[0] * factor
|
|
else:
|
|
x = 0
|
|
|
|
y_obj = rect.get('y')
|
|
if y_obj is not None:
|
|
y = svgparselength(y_obj)[0] * factor
|
|
else:
|
|
y = 0
|
|
|
|
rxstr = rect.get('rx')
|
|
rxstr = rxstr * factor if rxstr else rxstr
|
|
rystr = rect.get('ry')
|
|
rystr = rystr * factor if rystr else rystr
|
|
|
|
if rxstr is None and rystr is None: # Sharp corners
|
|
pts = [
|
|
(x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)
|
|
]
|
|
|
|
else: # Rounded corners
|
|
rx = 0.0 if rxstr is None else svgparselength(rxstr)[0]
|
|
ry = 0.0 if rystr is None else svgparselength(rystr)[0]
|
|
|
|
n_points = int(n_points / 4 + 0.5)
|
|
t = np.arange(n_points, dtype=float) / n_points / 4
|
|
|
|
x_ = (x + w - rx) + rx * np.cos(2 * np.pi * (t + 0.75))
|
|
y_ = (y + ry) + ry * np.sin(2 * np.pi * (t + 0.75))
|
|
|
|
lower_right = [(x_[i], y_[i]) for i in range(n_points)]
|
|
|
|
x_ = (x + w - rx) + rx * np.cos(2 * np.pi * t)
|
|
y_ = (y + h - ry) + ry * np.sin(2 * np.pi * t)
|
|
|
|
upper_right = [(x_[i], y_[i]) for i in range(n_points)]
|
|
|
|
x_ = (x + rx) + rx * np.cos(2 * np.pi * (t + 0.25))
|
|
y_ = (y + h - ry) + ry * np.sin(2 * np.pi * (t + 0.25))
|
|
|
|
upper_left = [(x_[i], y_[i]) for i in range(n_points)]
|
|
|
|
x_ = (x + rx) + rx * np.cos(2 * np.pi * (t + 0.5))
|
|
y_ = (y + ry) + ry * np.sin(2 * np.pi * (t + 0.5))
|
|
|
|
lower_left = [(x_[i], y_[i]) for i in range(n_points)]
|
|
|
|
pts = [(x + rx, y), (x - rx + w, y)] + \
|
|
lower_right + \
|
|
[(x + w, y + ry), (x + w, y + h - ry)] + \
|
|
upper_right + \
|
|
[(x + w - rx, y + h), (x + rx, y + h)] + \
|
|
upper_left + \
|
|
[(x, y + h - ry), (x, y + ry)] + \
|
|
lower_left
|
|
|
|
return Polygon(pts).buffer(0)
|
|
# return LinearRing(pts)
|
|
|
|
|
|
def svgcircle2shapely(circle, n_points=64, factor=1.0):
|
|
"""
|
|
Converts an SVG circle into Shapely geometry.
|
|
|
|
:param circle: Circle Element
|
|
:type circle: xml.etree.ElementTree.Element
|
|
:param n_points: circle resolution; nr of points to b e used to approximate a circle
|
|
:type n_points: int
|
|
:param factor:
|
|
:type factor: float
|
|
:return: Shapely representation of the circle.
|
|
:rtype: shapely.geometry.polygon.LinearRing
|
|
"""
|
|
# cx = float(circle.get('cx'))
|
|
# cy = float(circle.get('cy'))
|
|
# r = float(circle.get('r'))
|
|
cx = svgparselength(circle.get('cx'))[0] # TODO: No units support yet
|
|
cx = cx * factor if cx else cx
|
|
cy = svgparselength(circle.get('cy'))[0] # TODO: No units support yet
|
|
cy = cy * factor if cy else cy
|
|
r = svgparselength(circle.get('r'))[0] # TODO: No units support yet
|
|
r = r * factor if r else r
|
|
|
|
return Point(cx, cy).buffer(r, resolution=n_points)
|
|
|
|
|
|
def svgellipse2shapely(ellipse, n_points=64, factor=1.0):
|
|
"""
|
|
Converts an SVG ellipse into Shapely geometry
|
|
|
|
:param ellipse: Ellipse Element
|
|
:type ellipse: xml.etree.ElementTree.Element
|
|
:param n_points: Number of discrete points in output.
|
|
:type n_points: int
|
|
:param factor:
|
|
:type factor: float
|
|
:return: Shapely representation of the ellipse.
|
|
:rtype: shapely.geometry.polygon.LinearRing
|
|
"""
|
|
|
|
cx = svgparselength(ellipse.get('cx'))[0] # TODO: No units support yet
|
|
cx = cx * factor if cx else cx
|
|
cy = svgparselength(ellipse.get('cy'))[0] # TODO: No units support yet
|
|
cy = cy * factor if cy else cy
|
|
|
|
rx = svgparselength(ellipse.get('rx'))[0] # TODO: No units support yet
|
|
rx = rx * factor if rx else rx
|
|
ry = svgparselength(ellipse.get('ry'))[0] # TODO: No units support yet
|
|
ry = ry * factor if ry else ry
|
|
|
|
t = np.arange(n_points, dtype=float) / n_points
|
|
x = cx + rx * np.cos(2 * np.pi * t)
|
|
y = cy + ry * np.sin(2 * np.pi * t)
|
|
pts = [(x[i], y[i]) for i in range(n_points)]
|
|
|
|
return Polygon(pts).buffer(0)
|
|
# return LinearRing(pts)
|
|
|
|
|
|
def svgline2shapely(line, factor=1.0):
|
|
"""
|
|
|
|
:param line: Line element
|
|
:type line: xml.etree.ElementTree.Element
|
|
:param factor: correction factor due of virtual units
|
|
:type factor: float
|
|
:return: Shapely representation on the line.
|
|
:rtype: shapely.geometry.polygon.LineString
|
|
"""
|
|
|
|
x1 = svgparselength(line.get('x1'))[0] * factor
|
|
y1 = svgparselength(line.get('y1'))[0] * factor
|
|
x2 = svgparselength(line.get('x2'))[0] * factor
|
|
y2 = svgparselength(line.get('y2'))[0] * factor
|
|
|
|
return LineString([(x1, y1), (x2, y2)])
|
|
|
|
|
|
def svgpolyline2shapely(polyline, factor=1.0):
|
|
"""
|
|
|
|
:param polyline: Polyline element
|
|
:type polyline: xml.etree.ElementTree.Element
|
|
:param factor: correction factor due of virtual units
|
|
:type factor: float
|
|
:return: Shapely representation of the PolyLine
|
|
:rtype: shapely.geometry.polygon.LineString
|
|
"""
|
|
|
|
ptliststr = polyline.get('points')
|
|
points = parse_svg_point_list(ptliststr, factor)
|
|
|
|
return LineString(points)
|
|
|
|
|
|
def svgpolygon2shapely(polygon, n_points=64, factor=1.0):
|
|
"""
|
|
Convert a SVG polygon to a Shapely Polygon.
|
|
|
|
:param polygon:
|
|
:type polygon:
|
|
:param n_points: circle resolution; nr of points to b e used to approximate a circle
|
|
:type n_points: int
|
|
:param factor: correction factor due of virtual units
|
|
:type factor: float
|
|
:return: Shapely Polygon
|
|
"""
|
|
|
|
ptliststr = polygon.get('points')
|
|
points = parse_svg_point_list(ptliststr, factor)
|
|
|
|
return Polygon(points).buffer(0, resolution=n_points)
|
|
# return LinearRing(points)
|
|
|
|
|
|
def getsvggeo(node, object_type, root=None, units='MM', res=64, factor=1.0, app=None):
|
|
"""
|
|
Extracts and flattens all geometry from an SVG node
|
|
into a list of Shapely geometry.
|
|
|
|
:param node: xml.etree.ElementTree.Element
|
|
:param object_type:
|
|
:param root:
|
|
:param units: FlatCAM units
|
|
:param res: resolution to be used for circles buffering
|
|
:param factor: correction factor due of virtual units
|
|
:type factor: float
|
|
:param app: Application reference
|
|
|
|
:return: List of Shapely geometry
|
|
:rtype: list
|
|
"""
|
|
if root is None:
|
|
root = node
|
|
|
|
if app is not None:
|
|
log = app.log
|
|
else:
|
|
log = logging.getLogger('base2')
|
|
|
|
kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
|
|
geo = []
|
|
|
|
# Recurse
|
|
if len(node) > 0:
|
|
for child in node:
|
|
subgeo = getsvggeo(child, object_type, root=root, units=units, res=res, factor=factor, app=app)
|
|
if subgeo is not None:
|
|
if subgeo == 'fail':
|
|
return
|
|
geo += subgeo
|
|
|
|
# Parse
|
|
elif kind == 'path':
|
|
# log.debug("***PATH***")
|
|
P = parse_path(node.get('d'))
|
|
P = path2shapely(P, object_type, units=units, factor=factor)
|
|
# for path, the resulting geometry is already a list so no need to create a new one
|
|
geo = P
|
|
|
|
elif kind == 'rect':
|
|
# log.debug("***RECT***")
|
|
R = svgrect2shapely(node, n_points=res, factor=factor)
|
|
geo = [R]
|
|
|
|
elif kind == 'circle':
|
|
# log.debug("***CIRCLE***")
|
|
C = svgcircle2shapely(node, n_points=res, factor=factor)
|
|
geo = [C]
|
|
|
|
elif kind == 'ellipse':
|
|
# log.debug("***ELLIPSE***")
|
|
E = svgellipse2shapely(node, n_points=res, factor=factor)
|
|
geo = [E]
|
|
|
|
elif kind == 'polygon':
|
|
# log.debug("***POLYGON***")
|
|
poly = svgpolygon2shapely(node, n_points=res, factor=factor)
|
|
geo = [poly]
|
|
|
|
elif kind == 'line':
|
|
# log.debug("***LINE***")
|
|
line = svgline2shapely(node, factor=factor)
|
|
geo = [line]
|
|
|
|
elif kind == 'polyline':
|
|
# log.debug("***POLYLINE***")
|
|
pline = svgpolyline2shapely(node, factor=factor)
|
|
geo = [pline]
|
|
|
|
elif kind == 'use':
|
|
# log.debug('***USE***')
|
|
# href= is the preferred name for this[1], but inkscape still generates xlink:href=.
|
|
# [1] https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use#Attributes
|
|
href = node.attrib['href'] if 'href' in node.attrib else node.attrib['{http://www.w3.org/1999/xlink}href']
|
|
ref = root.find(".//*[@id='%s']" % href.replace('#', ''))
|
|
if ref is not None:
|
|
geo = getsvggeo(ref, object_type, root=root, units=units, res=res, factor=factor, app=app)
|
|
|
|
elif kind in ['defs', 'namedview', 'format', 'type', 'title', 'desc', 'svg']:
|
|
log.warning('SVG Element not supported: %s. Skipping to next.' % kind)
|
|
|
|
elif kind in ['g']:
|
|
log.warning("SVG Element %s not supported and causing failure. Cancelling." % kind)
|
|
return "fail"
|
|
|
|
else:
|
|
log.warning("Unknown kind: " + kind)
|
|
geo = None
|
|
|
|
# ignore transformation for unknown kind
|
|
if geo is not None:
|
|
# Transformations
|
|
if 'transform' in node.attrib:
|
|
trstr = node.get('transform')
|
|
trlist = parse_svg_transform(trstr)
|
|
# log.debug(trlist)
|
|
|
|
# Transformations are applied in reverse order
|
|
for tr in trlist[::-1]:
|
|
if tr[0] == 'translate':
|
|
geo = [translate(geoi, tr[1], tr[2]) for geoi in geo]
|
|
elif tr[0] == 'scale':
|
|
geo = [scale(geoi, tr[1], tr[2], origin=(0, 0))
|
|
for geoi in geo]
|
|
elif tr[0] == 'rotate':
|
|
geo = [rotate(geoi, tr[1], origin=(tr[2], tr[3]))
|
|
for geoi in geo]
|
|
elif tr[0] == 'skew':
|
|
geo = [skew(geoi, tr[1], tr[2], origin=(0, 0))
|
|
for geoi in geo]
|
|
elif tr[0] == 'matrix':
|
|
geo = [affine_transform(geoi, tr[1:]) for geoi in geo]
|
|
else:
|
|
raise Exception('Unknown transformation: %s', tr)
|
|
|
|
return geo
|
|
|
|
|
|
def getsvgtext(node, object_type, app, units='MM'):
|
|
"""
|
|
Extracts and flattens all geometry from an SVG node
|
|
into a list of Shapely geometry.
|
|
|
|
:param app: Main App
|
|
:param node: xml.etree.ElementTree.Element
|
|
:param object_type:
|
|
:param units: FlatCAM units
|
|
:return: List of Shapely geometry
|
|
:rtype: list
|
|
"""
|
|
kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
|
|
geo = []
|
|
|
|
# Recurse
|
|
if len(node) > 0:
|
|
for child in node:
|
|
subgeo = getsvgtext(child, object_type, app=app, units=units)
|
|
if subgeo is not None:
|
|
geo += subgeo
|
|
|
|
# Parse
|
|
elif kind == 'tspan':
|
|
current_attrib = node.attrib
|
|
txt = node.text
|
|
style_dict = {}
|
|
parrent_attrib = node.getparent().attrib
|
|
style = parrent_attrib['style']
|
|
|
|
try:
|
|
style_list = style.split(';')
|
|
for css in style_list:
|
|
style_dict[css.rpartition(':')[0]] = css.rpartition(':')[-1]
|
|
|
|
pos_x = float(current_attrib['x'])
|
|
pos_y = float(current_attrib['y'])
|
|
|
|
# should have used the instance from FlatCAMApp.App but how? without reworking everything ...
|
|
pf = ParseFont(app=app)
|
|
pf.get_fonts_by_types()
|
|
font_name = style_dict['font-family'].replace("'", '')
|
|
|
|
if style_dict['font-style'] == 'italic' and style_dict['font-weight'] == 'bold':
|
|
font_type = 'bi'
|
|
elif style_dict['font-weight'] == 'bold':
|
|
font_type = 'bold'
|
|
elif style_dict['font-style'] == 'italic':
|
|
font_type = 'italic'
|
|
else:
|
|
font_type = 'regular'
|
|
|
|
# value of 2.2 should have been 2.83 (conversion value from pixels to points)
|
|
# but the dimensions from Inkscape did not corelate with the ones after importing in FlatCAM
|
|
# so I adjusted this
|
|
font_size = svgparselength(style_dict['font-size'])[0] * 2.2
|
|
geo = [pf.font_to_geometry(txt,
|
|
font_name=font_name,
|
|
font_size=font_size,
|
|
font_type=font_type,
|
|
units=units,
|
|
coordx=pos_x,
|
|
coordy=pos_y)
|
|
]
|
|
|
|
geo = [(scale(g, 1.0, -1.0)) for g in geo]
|
|
except Exception as e:
|
|
log.error(str(e))
|
|
geo = None
|
|
else:
|
|
geo = None
|
|
|
|
# ignore transformation for unknown kind
|
|
if geo and geo is not None:
|
|
# Transformations
|
|
if 'transform' in node.attrib:
|
|
trstr = node.get('transform')
|
|
trlist = parse_svg_transform(trstr)
|
|
# log.debug(trlist)
|
|
|
|
# Transformations are applied in reverse order
|
|
for tr in trlist[::-1]:
|
|
if tr[0] == 'translate':
|
|
geo = [translate(geoi, tr[1], tr[2]) for geoi in geo]
|
|
elif tr[0] == 'scale':
|
|
geo = [scale(geoi, tr[1], tr[2], origin=(0, 0))
|
|
for geoi in geo]
|
|
elif tr[0] == 'rotate':
|
|
geo = [rotate(geoi, tr[1], origin=(tr[2], tr[3]))
|
|
for geoi in geo]
|
|
elif tr[0] == 'skew':
|
|
geo = [skew(geoi, tr[1], tr[2], origin=(0, 0))
|
|
for geoi in geo]
|
|
elif tr[0] == 'matrix':
|
|
geo = [affine_transform(geoi, tr[1:]) for geoi in geo]
|
|
else:
|
|
raise Exception('Unknown transformation: %s', tr)
|
|
|
|
if not geo:
|
|
geo = None
|
|
|
|
return geo
|
|
|
|
|
|
def parse_svg_point_list(ptliststr, factor):
|
|
"""
|
|
Returns a list of coordinate pairs extracted from the "points"
|
|
attribute in SVG polygons and polyline's.
|
|
|
|
:param ptliststr: "points" attribute string in polygon or polyline.
|
|
:param factor: correction factor due of virtual units
|
|
:type factor: float
|
|
:return: List of tuples with coordinates.
|
|
"""
|
|
|
|
pairs = []
|
|
last = None
|
|
pos = 0
|
|
i = 0
|
|
|
|
for match in re.finditer(r'(\s*,\s*)|(\s+)', ptliststr.strip(' ')):
|
|
|
|
val = float(ptliststr[pos:match.start()])
|
|
|
|
if i % 2 == 1:
|
|
pairs.append((factor * last, factor * val))
|
|
else:
|
|
last = val * factor
|
|
|
|
pos = match.end()
|
|
i += 1
|
|
|
|
# Check for last element
|
|
val = float(ptliststr[pos:])
|
|
if i % 2 == 1:
|
|
pairs.append((factor * last, factor * val))
|
|
else:
|
|
log.warning("Incomplete coordinates.")
|
|
|
|
return pairs
|
|
|
|
|
|
def parse_svg_transform(trstr):
|
|
"""
|
|
Parses an SVG transform string into a list
|
|
of transform names and their parameters.
|
|
|
|
Possible transformations are:
|
|
|
|
* Translate: translate(<tx> [<ty>]), which specifies
|
|
a translation by tx and ty. If <ty> is not provided,
|
|
it is assumed to be zero. Result is
|
|
['translate', tx, ty]
|
|
|
|
* Scale: scale(<sx> [<sy>]), which specifies a scale operation
|
|
by sx and sy. If <sy> is not provided, it is assumed to be
|
|
equal to <sx>. Result is: ['scale', sx, sy]
|
|
|
|
* Rotate: rotate(<rotate-angle> [<cx> <cy>]), which specifies
|
|
a rotation by <rotate-angle> degrees about a given point.
|
|
If optional parameters <cx> and <cy> are not supplied,
|
|
the rotate is about the origin of the current user coordinate
|
|
system. Result is: ['rotate', rotate-angle, cx, cy]
|
|
|
|
* Skew: skewX(<skew-angle>), which specifies a skew
|
|
transformation along the x-axis. skewY(<skew-angle>), which
|
|
specifies a skew transformation along the y-axis.
|
|
Result is ['skew', angle-x, angle-y]
|
|
|
|
* Matrix: matrix(<a> <b> <c> <d> <e> <f>), which specifies a
|
|
transformation in the form of a transformation matrix of six
|
|
values. matrix(a,b,c,d,e,f) is equivalent to applying the
|
|
transformation matrix [a b c d e f]. Result is
|
|
['matrix', a, b, c, d, e, f]
|
|
|
|
Note: All parameters to the transformations are "numbers",
|
|
i.e. no units present.
|
|
|
|
:param trstr: SVG transform string.
|
|
:type trstr: str
|
|
:return: List of transforms.
|
|
:rtype: list
|
|
"""
|
|
trlist = []
|
|
|
|
assert isinstance(trstr, str)
|
|
trstr = trstr.strip(' ')
|
|
|
|
integer_re_str = r'[+-]?[0-9]+'
|
|
number_re_str = r'(?:[+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?' + r')|' + \
|
|
r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?)'
|
|
|
|
# num_re_str = r'[\+\-]?[0-9\.e]+' # TODO: Negative exponents missing
|
|
comma_or_space_re_str = r'(?:(?:\s+)|(?:\s*,\s*))'
|
|
translate_re_str = r'translate\s*\(\s*(' + \
|
|
number_re_str + r')(?:' + \
|
|
comma_or_space_re_str + \
|
|
r'(' + number_re_str + r'))?\s*\)'
|
|
scale_re_str = r'scale\s*\(\s*(' + \
|
|
number_re_str + r')' + \
|
|
r'(?:' + comma_or_space_re_str + \
|
|
r'(' + number_re_str + r'))?\s*\)'
|
|
skew_re_str = r'skew([XY])\s*\(\s*(' + \
|
|
number_re_str + r')\s*\)'
|
|
rotate_re_str = r'rotate\s*\(\s*(' + \
|
|
number_re_str + r')' + \
|
|
r'(?:' + comma_or_space_re_str + \
|
|
r'(' + number_re_str + r')' + \
|
|
comma_or_space_re_str + \
|
|
r'(' + number_re_str + r'))?\s*\)'
|
|
matrix_re_str = r'matrix\s*\(\s*' + \
|
|
r'(' + number_re_str + r')' + comma_or_space_re_str + \
|
|
r'(' + number_re_str + r')' + comma_or_space_re_str + \
|
|
r'(' + number_re_str + r')' + comma_or_space_re_str + \
|
|
r'(' + number_re_str + r')' + comma_or_space_re_str + \
|
|
r'(' + number_re_str + r')' + comma_or_space_re_str + \
|
|
r'(' + number_re_str + r')\s*\)'
|
|
|
|
while len(trstr) > 0:
|
|
match = re.search(r'^' + translate_re_str, trstr)
|
|
if match:
|
|
trlist.append([
|
|
'translate',
|
|
float(match.group(1)),
|
|
float(match.group(2)) if (match.group(2) is not None) else 0.0
|
|
])
|
|
trstr = trstr[len(match.group(0)):].strip(' ')
|
|
continue
|
|
|
|
match = re.search(r'^' + scale_re_str, trstr)
|
|
if match:
|
|
trlist.append([
|
|
'scale',
|
|
float(match.group(1)),
|
|
float(match.group(2)) if (match.group(2) is not None) else float(match.group(1))
|
|
])
|
|
trstr = trstr[len(match.group(0)):].strip(' ')
|
|
continue
|
|
|
|
match = re.search(r'^' + skew_re_str, trstr)
|
|
if match:
|
|
trlist.append([
|
|
'skew',
|
|
float(match.group(2)) if match.group(1) == 'X' else 0.0,
|
|
float(match.group(2)) if match.group(1) == 'Y' else 0.0
|
|
])
|
|
trstr = trstr[len(match.group(0)):].strip(' ')
|
|
continue
|
|
|
|
match = re.search(r'^' + rotate_re_str, trstr)
|
|
if match:
|
|
trlist.append([
|
|
'rotate',
|
|
float(match.group(1)),
|
|
float(match.group(2)) if match.group(2) else 0.0,
|
|
float(match.group(3)) if match.group(3) else 0.0
|
|
])
|
|
trstr = trstr[len(match.group(0)):].strip(' ')
|
|
continue
|
|
|
|
match = re.search(r'^' + matrix_re_str, trstr)
|
|
if match:
|
|
trlist.append(['matrix'] + [float(x) for x in match.groups()])
|
|
trstr = trstr[len(match.group(0)):].strip(' ')
|
|
continue
|
|
|
|
# raise Exception("Don't know how to parse: %s" % trstr)
|
|
log.error("[ERROR] Don't know how to parse: %s" % trstr)
|
|
|
|
return trlist
|
|
|
|
# if __name__ == "__main__":
|
|
# tree = ET.parse('tests/svg/drawing.svg')
|
|
# root = tree.getroot()
|
|
# ns = re.search(r'\{(.*)\}', root.tag).group(1)
|
|
# print(ns)
|
|
# for geo in getsvggeo(root):
|
|
# print(geo)
|