Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
395 lines (323 sloc) 13.8 KB
import xml.etree.ElementTree as ET
import re
# paths are explained here:
# https://www.w3schools.com/graphics/svg_path.asp
# sample (the letter a):
# m is move to
# q is a quadatric bezier curve
# z is close path
# d = m 161.0332,166.95004
# q -4.35547,0 -6.03515,0.9961 -1.67969,0.99609 -1.67969,3.39844 0,1.91406
# 1.25,3.04687 1.26953,1.11328 3.4375,1.11328 2.98828,0 4.78516,-2.10937
# 1.8164,-2.12891 1.8164,-5.64453 l 0,-0.80079 -3.57422,0 z m
# 7.16797,-1.48437 0,12.48047 -3.59375,0 0,-3.32031 q -1.23047,1.99218
# -3.0664,2.94921 -1.83594,0.9375 -4.49219,0.9375 -3.35938,0
# -5.35156,-1.875 -1.97266,-1.89453 -1.97266,-5.05859 0,-3.69141
# 2.46094,-5.56641 2.48047,-1.875 7.38281,-1.875 l 5.03906,0
# 0,-0.35156 q 0,-2.48047 -1.64062,-3.82812 -1.6211,-1.36719
# -4.57032,-1.36719 -1.875,0 -3.65234,0.44922 -1.77734,0.44922
# -3.41797,1.34765 l 0,-3.32031 q 1.97266,-0.76172 3.82813,-1.13281
# 1.85547,-0.39063 3.61328,-0.39063 4.74609,0 7.08984,2.46094
# 2.34375,2.46094 2.34375,7.46094 z
# but that has bezier curvers which kicad doesn't support
# do as described here to get segments
# http://www.inkscapeforum.com/viewtopic.php?t=4308
# the same a (though choppy) looks like this:
# <path
# d="m 161.0332,166.95004 -2.84283,0.1445 -3.19232,0.8516
# -0.41993,0.84961 -0.41992,0.84961 -0.41992,0.84961 -0.41992,0.84961
# 0.43815,1.90148 0.81185,1.14539 1.71875,0.55664 1.71875,0.55664
# 2.39258,-1.05468 2.39258,-1.05469 0.9082,-2.82226 0.9082,-2.82227
# 0,-0.4004 0,-0.40039 -1.78711,0 z
# m 7.16797,-1.48437 0,6.24023
# 0,6.24024 -1.79688,0 -1.79687,0 0,-1.66016 0,-1.66015
# -1.5332,1.4746 -1.5332,1.47461 -2.2461,0.46875 -2.24609,0.46875
# -2.67578,-0.9375 -2.67578,-0.9375 -1.2647,-1.89966
# -0.70796,-3.15893 0.61523,-1.39161 0.61524,-1.3916 0.61523,-1.3916
# 0.61524,-1.3916 3.89847,-1.40586 3.48434,-0.46914 2.51953,0
# 2.51953,0 0,-0.17578 0,-0.17578 0.0804,-1.64462 -1.72101,-2.1835
# -2.28516,-0.6836 -2.28516,-0.68359 -1.82617,0.22461
# -1.82617,0.22461 -1.70899,0.67382 -1.70898,0.67383 0,-1.66016
# 0,-1.66015 1.91406,-0.56641 1.91407,-0.5664 1.80664,-0.19532
# 1.80664,-0.19531 1.88598,0.30761 1.88599,0.30762 1.65893,0.92285
# 1.65894,0.92286 2.48728,4.46222 z"
class SVGShape:
def __init__(self, bound, holes):
self.bound = bound
self.holes = holes
# in this class, I have a bunch of places where a helper method modifies the
# class. I don't really like the way I did that. It's not good coding.
class SVGPath:
def get_float(self):
m = re.match("(-?[0-9]+(\.[0-9]*)?([eE]\-?[0-9]+)?)", self.d)
if (not m):
raise ValueError("expecting number, got {}".format(self.d))
self.d = self.d[len(m.group(1)):].lstrip(" \t\n,")
return float(m.group(1))
def get_cmd(self, default):
if (re.match("[lLmMzZ]", self.d[0])):
retval = self.d[0]
self.d = self.d[1:].lstrip(" \t\n,")
return retval
return default
def get_pt(self):
return (self.get_float(), self.get_float())
def pt_add(self, a, b):
return (a[0]+b[0], a[1]+b[1])
def transform_pt(self, pt):
if not self.trans:
return pt
# svg matrix transforms are described here:
# quoting from here:
# https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
# matrix(<a> <b> <c> <d> <e> <f>)
# This transform definition 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 c e
# b d f
# 0 0 1 )
# which maps coordinates from a previous coordinate system into a
# new coordinate system by the following matrix equalities:
# (xnewCoordSys ) = ( a c e) ( xprevCoordSys ) = ( axprevCoordSys+cyprevCoordSys+e )
# ynewCoordSys b d f yprevCoordSys bxprevCoordSys+dyprevCoordSys+f
# 1 0 0 1 1 ) 1
newx = self.trans[0] * pt[0] + self.trans[2] * pt[1] + self.trans[4]
newy = self.trans[1] * pt[0] + self.trans[3] * pt[1] + self.trans[5]
return (newx, newy)
def append_pt(self, pt):
self.curpoly.append(self.transform_pt(pt))
def parse_path_string(self, d, trans):
self.trans = trans
self.polys = []
self.curpoly = None
firstloc = None
curloc = None
curcmd = None
self.d = d.lstrip(" \t\n,")
while self.d:
# get_cmd returns passed argument as fallback
cmd = self.get_cmd(curcmd)
if (cmd == 'M'):
curloc = self.get_pt()
if (not firstloc):
firstloc = curloc
if (not self.curpoly):
self.curpoly = []
self.polys.append(self.curpoly)
self.append_pt(curloc)
curcmd = 'L'
elif (cmd == 'm'):
pt = self.get_pt()
if (curloc):
curloc = self.pt_add(curloc, pt)
else:
# the first m of a path is treated as absolute
curloc = pt
if (not firstloc):
firstloc = curloc
if (not self.curpoly):
self.curpoly = []
self.polys.append(self.curpoly)
self.append_pt(curloc)
curcmd = 'l'
elif (cmd == 'l'):
pt = self.get_pt()
curloc = self.pt_add(curloc, pt)
self.append_pt(curloc)
curcmd = 'l'
elif (cmd == 'L'):
curloc = self.get_pt()
self.append_pt(curloc)
curcmd = 'L'
elif (cmd == 'z') or (cmd == 'Z'):
self.append_pt(firstloc)
self.curpoly = None
curloc = firstloc
firstloc = None
# http://www.ariel.com.au/a/python-point-int-poly.html
# determine if a point is inside a given polygon or not
# Polygon is a list of (x,y) pairs.
@staticmethod
def point_inside_polygon(x,y,poly):
n = len(poly)
inside =False
p1x,p1y = poly[0]
for i in range(n+1):
p2x,p2y = poly[i % n]
if y > min(p1y,p2y):
if y <= max(p1y,p2y):
if x <= max(p1x,p2x):
if p1y != p2y:
xinters = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x
if p1x == p2x or x <= xinters:
inside = not inside
p1x,p1y = p2x,p2y
return inside
def group_by_bound_and_holes(self):
# inkscape gives me a path by first listing the boundaries and then the holes.
# I want to know a boundary and its holes, then another boundary,...
# this function will reorder the polys that way.
bounds = []
holes = []
for poly in self.polys:
if not poly_is_hole(poly):
bounds.append(poly)
else:
holes.append(poly)
# a lookup table keyed off the boundary index.
# the values are lists of holes
bound_holes = {}
for bi, bound in enumerate(bounds):
for hi, hole in enumerate(holes):
# pass x and y of the first point of hole.
if SVGPath.point_inside_polygon(hole[0][0], hole[0][1], bound):
if bi not in bound_holes:
bound_holes[bi] = []
bound_holes[bi].append(hole)
retshapes = []
for bi, bound in enumerate(bounds):
if (bi in bound_holes):
retshapes.append(SVGShape(bound, bound_holes[bi]))
else:
retshapes.append(SVGShape(bound, []))
return retshapes
def __init__(self, d, trans):
self.origd = d
self.parse_path_string(d,trans)
# this site was helpful in debugging
# http://www.bluebit.gr/matrix-calculator/multiply.aspx
def multiply_transforms(a, b):
# this assumes a and b are represented with these indexes
# 0 2 4 <- these are indexes of a and b
# 1 3 5
# "0" "0" "1" <- these always have the value 0 0 1
retval = [
a[0]*b[0]+a[2]*b[1], # 0
a[1]*b[0]+a[3]*b[1], # 1
a[0]*b[2]+a[2]*b[3], # 2
a[1]*b[2]+a[3]*b[3], # 3
a[0]*b[4]+a[2]*b[5]+a[4], # 4
a[1]*b[4]+a[3]*b[5]+a[5] # 5
]
return retval
# svg files can have multiple levels of transformation.
# find and combine them.
def combine_path_transforms(curtrans, curnode, parent_map):
if ('transform' in curnode.attrib):
trans = None
mo = re.search("matrix\((.*)\)", curnode.attrib['transform'])
if (mo):
trans = [float(x) for x in mo.group(1).split(',')]
mo = re.search("translate\((.*)\)", curnode.attrib['transform'])
# quoting again from https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
# translate(<x> [<y>])
# This transform definition specifies a translation by x and y.
# This is equivalent to matrix(1 0 0 1 x y).
# If y is not provided, it is assumed to be zero.
if (mo):
trans = (1, 0, 0, 1) + tuple([float(x) for x in mo.group(1).split(',')])
if len(trans) == 5:
trans = trans + (0)
if not trans:
raise ValueError('wasnt able to match transform')
if (curtrans):
# need to multiply trans
print("need to multiple {} and {}".format(trans, curtrans))
curtrans = multiply_transforms(trans, curtrans)
else:
print("have trans {}".format(trans))
curtrans = trans
if (curnode not in parent_map):
return curtrans
return combine_path_transforms(curtrans, parent_map[curnode], parent_map)
def get_mm_from_dimension(val):
mo = re.match("([0-9\.\-]+)(mm|cm|m|in|ft)", val)
if (not mo):
raise ValueError("expecting number with mm,cm,m,in, or ft dimension, got '{}'".format(val))
num = float(mo.group(1))
dim = mo.group(2)
# number to multiply by to get mm
multiplier = {
"mm": 1.0,
"cm": 10.0,
"m": 1000.0,
"in": 25.4,
"ft": 25.4*12
}
if (dim not in multiplier):
raise ValueError("this shouldn't happen. expecing a dimension of mm,cm,m,in or ft. Got {}".format(dim))
return num*multiplier[dim]
def parse_svg_path(filepath):
tree = ET.parse(filepath)
root = tree.getroot()
# width="100mm"
# height="100mm"
# viewBox="0 0 354.33071 354.33071"
width = get_mm_from_dimension(root.attrib['width'])
height = get_mm_from_dimension(root.attrib['height'])
box = [float(x) for x in root.attrib['viewBox'].split(" ")]
# throughout path parsing, various transformations need to be applied
# these are embedded in the path as well as parent groups.
# the last transformation is into the target coordinate system,
# based a projection of viewbox into 0->width and 0->height
# xfinal = (x-xl_viewbox)/(xh_viewbox-xl_viewbox)*width
# so we have a stretch factor of 1/(xh-xl)*width and an offset of xl*width
# as with the rest of the transformations, I reference here:
# https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
# this page is also helpful:
# https://en.wikipedia.org/wiki/Transformation_matrix
(xl, yl, xh, yh) = tuple(box)
# a c e 1.0/(xh-xl)*width 0 xl*width
# b d f 0 1.0/(yh-yl)*height yl*height
# 0 0 1
coordtrans = (1.0/(xh-xl)*width, 0,
0, 1.0/(yh-yl)*height,
xl*width, yl*height)
print("coortrans is {}".format(coordtrans))
# from here: https://stackoverflow.com/a/20132342/23630
parent_map = {c:p for p in tree.iter() for c in p}
retval = []
for path in root.iter('{http://www.w3.org/2000/svg}path'):
points = path.attrib['d']
# here I pass in None as the initial transform.
# this is because combine, combines on the way up.
# coordtrans is the top most transform.
# I could also have combine do the matrix stuff on the way
# down, but it was already written when I realized I needed
# coordtrans
curtrans = combine_path_transforms(None, path, parent_map)
if (curtrans):
curtrans = multiply_transforms(coordtrans, curtrans)
else:
curtrans = coordtrans
print("using transform {}".format(curtrans))
p = SVGPath(points, curtrans)
retval.append(p)
return retval
def path_bbox(path):
xl = min([min([pt[0] for pt in poly]) for poly in path.polys])
xh = max([max([pt[0] for pt in poly]) for poly in path.polys])
yl = min([min([pt[1] for pt in poly]) for poly in path.polys])
yh = max([max([pt[1] for pt in poly]) for poly in path.polys])
return (xl,yl,xh,yh)
def poly_is_hole(poly):
# to determine if a poly is a hole or outer boundary i check for
# clockwise or counter-clockwise.
# As suggested here:
# https://stackoverflow.com/a/1165943/23630
# I take the area under the curve, and if it's positive or negative
# I'll know bounds or hole.
lastpt = poly[-1]
area = 0.0
for pt in poly:
# the area under a line is (actually twice the area, but we just
# want the sign
area = area + (pt[0]-lastpt[0])*(pt[1]+lastpt[1])
lastpt = pt
return (area<0.0)
# paths= parse_svg_path('/home/mmccoo/kicad/kicad_mmccoo/svg2border/drawing.svg')
# for path in paths:
# print("path")
# for poly in path.polys:
# print(" points {}".format(poly))