swh:1:snp:658089edcf6aef5f1ce59e311db02dd935c5e291
Tip revision: 77145fd0cac968cdaaf855ccc5108503f9ecd1f1 authored by Steven van den Broek on 19 July 2024, 15:08:08 UTC
Add link to Zenodo and add subtitle
Add link to Zenodo and add subtitle
Tip revision: 77145fd
svgtoipe.py
#!/usr/bin/env python3
# --------------------------------------------------------------------
# convert SVG to Ipe format
# --------------------------------------------------------------------
#
# Copyright (C) 2009-2019 Otfried Cheong
#
# svgtoipe is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# svgtoipe is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with svgtoipe; if not, you can find it at
# "http://www.gnu.org/copyleft/gpl.html", or write to the Free
# Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
# SVG Specification: https://www.w3.org/TR/SVG11
#
# TODO:
# - coordinates used in gradients (e.g. intertwingly.svg)
# - sanitize paths (Ipe refuses to load illegal paths, but they are common in SVG)
# - handle <use> elements
# - store gradients in the dictionary in a more useful format
# - parse title and desc elements and store in Ipe header
# - go through SVG test suite (see test_svg.py)
#
# --------------------------------------------------------------------
svgtoipe_version = "20191210"
import sys
import argparse
import xml.dom.minidom as xml
from xml.dom.minidom import Node
from xml.parsers.expat import ExpatError
import re
import math
import base64
import io
try:
from PIL import Image
have_pil = True
except:
have_pil = False
assert sys.hexversion >= 0x3000000, "Please run svgtoipe using Python 3"
ignored_nodes = set(["defs", "audio", "video", "metadata", "sodipodi:namedview", "style", "script", "SVGTestCase",
"animate", "animateTransform", "set" ])
# --------------------------------------------------------------------
# should extend this to cover list in https://www.w3.org/TR/SVG11/types.html#ColorKeywords
color_keywords = {
"aqua" :"rgb(0, 255, 255)",
"black" : "rgb(0, 0, 0)",
"blue" :"rgb(0, 0, 255)",
"firebrick" : "rgb(178, 34, 34)",
"fuchsia" :"rgb(255, 0, 255)",
"gray" :"rgb(128, 128, 128)",
"green" :"rgb(0, 128, 0)",
"grey" :"rgb(128, 128, 128)",
"lime" :"rgb(0, 255, 0)",
"maroon" :"rgb(128, 0, 0)",
"navy" :"rgb(0, 0, 128)",
"olive" :"rgb(128, 128, 0)",
"purple" :"rgb(128, 0, 128)",
"red" :"rgb(255, 0, 0)",
"silver" :"rgb(192, 192, 192)",
"teal" :"rgb(0, 128, 128)",
"white" :"rgb(255, 255, 255)",
"yellow" :"rgb(255, 255, 0)",
}
attribute_names = [ "stroke",
"fill",
"stroke-opacity",
"fill-opacity",
"stroke-width",
"fill-rule",
"stroke-linecap",
"stroke-linejoin",
"stroke-dasharray",
"stroke-dashoffset",
"stroke-miterlimit",
"opacity",
"font-size" ]
def printAttributes(n):
a = n.attributes
for i in range(a.length):
name = a.item(i).name
if name[:9] != "sodipodi:" and name[:9] != "inkscape:":
sys.stderr.write(" %s %s\n" % (name, n.getAttribute(name)))
def parse_float(txt):
if not txt:
return None
if txt.endswith('px') or txt.endswith('pt'):
return float(txt[:-2])
elif txt.endswith('pc'):
return 12 * float(txt[:-2])
elif txt.endswith('mm'):
return 72.0 * float(txt[:-2]) / 25.4
elif txt.endswith('cm'):
return 72.0 * float(txt[:-2]) / 2.54
elif txt.endswith('in'):
return 72.0 * float(txt[:-2])
else:
return float(txt)
def parse_opacity(txt):
if not txt:
return None
m = int(10 * (float(txt) + 0.05))
if m == 0: m = 1
return 10 * m
def parse_list(string):
return re.findall("([A-Za-z]|-?[0-9]+\.?[0-9]*(?:e-?[0-9]*)?)", string)
def parse_style(string):
sdict = {}
for item in string.split(';'):
if ':' in item:
key, value = item.split(':')
sdict[key.strip()] = value.strip()
return sdict
def parse_color_component(txt):
if txt.endswith("%"):
return float(txt[:-1]) / 100.0
else:
return int(txt) / 255.0
def parse_color(c):
if not c or c == 'none':
return None
if c in color_keywords:
c = color_keywords[c]
m = re.match(r"rgb\(([0-9\.]+%?),\s*([0-9\.]+%?),\s*([0-9\.]+%?)\s*\)", c)
if m:
r = parse_color_component(m.group(1))
g = parse_color_component(m.group(2))
b = parse_color_component(m.group(3))
return (r, g, b)
m = re.match(r"#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$", c)
if m:
r = int(m.group(1), 16) / 15.0
g = int(m.group(2), 16) / 15.0
b = int(m.group(3), 16) / 15.0
return (r, g, b)
m = re.match(r"#([0-9a-fA-F][0-9a-fA-F])([0-9a-fA-F][0-9a-fA-F])"
+ r"([0-9a-fA-F][0-9a-fA-F])$", c)
if m:
r = int(m.group(1), 16) / 255.0
g = int(m.group(2), 16) / 255.0
b = int(m.group(3), 16) / 255.0
return (r, g, b)
sys.stderr.write("Unknown color: %s\n" % c)
return None
def pnext(d, n):
l = []
while n > 0:
if (len(d) > 1 and d[1] == 'E'):
v = d.pop(0)
e = d.pop(0)
t = d.pop(0)
l.append(float(v) * 10**float(t))
else:
l.append(float(d.pop(0)))
n -= 1
return tuple(l)
def parse_path(out, d):
d = re.findall("([A-Za-z]|-?(?:\.[0-9]+|[0-9]+\.?[0-9]*)(?:e-?[0-9]+)?)", d)
x, y = 0.0, 0.0
xs, ys = 0.0, 0.0
x0, y0 = 0.0, 0.0
while d:
if not d[0][0] in "01234567890.-":
opcode = d.pop(0)
if opcode == 'M':
x, y = pnext(d, 2)
x0, y0 = x, y
# If this moveto command is the last command, it starts a nonsensical empty
# new subpath, with crashes ipe. Thus, we skip this command in this case.
# See also https://github.com/otfried/ipe-issues/issues/274
if d:
out.write("%g %g m\n" % (x, y))
opcode = 'L'
elif opcode == 'm':
x1, y1 = pnext(d, 2)
x += x1
y += y1
x0, y0 = x, y
# If this moveto command is the last command, it starts a nonsensical empty
# new subpath, with crashes ipe. Thus, we skip this command in this case.
# See also https://github.com/otfried/ipe-issues/issues/274
if d:
out.write("%g %g m\n" % (x, y))
opcode = 'l'
elif opcode == 'L':
x, y = pnext(d, 2)
out.write("%g %g l\n" % (x, y))
elif opcode == 'l':
x1, y1 = pnext(d, 2)
x += x1
y += y1
out.write("%g %g l\n" % (x, y))
elif opcode == 'H':
x = pnext(d, 1)[0]
out.write("%g %g l\n" % (x, y))
elif opcode == 'h':
x += pnext(d, 1)[0]
out.write("%g %g l\n" % (x, y))
elif opcode == 'V':
y = pnext(d, 1)[0]
out.write("%g %g l\n" % (x, y))
elif opcode == 'v':
y += pnext(d, 1)[0]
out.write("%g %g l\n" % (x, y))
elif opcode == 'C':
x1, y1, xs, ys, x, y = pnext(d, 6)
out.write("%g %g %g %g %g %g c\n" % (x1, y1, xs, ys, x, y))
elif opcode == 'c':
x1, y1, xs, ys, xf, yf = pnext(d, 6)
x1 += x; y1 += y
xs += x; ys += y
x += xf; y += yf
out.write("%g %g %g %g %g %g c\n" % (x1, y1, xs, ys, x, y))
elif opcode == 'S' or opcode == 's':
x2, y2, xf, yf = pnext(d, 4)
if opcode == 's':
x2 += x; y2 += y
xf += x; yf += y
x1 = x + (x - xs); y1 = y + (y - ys)
out.write("%g %g %g %g %g %g c\n" % (x1, y1, x2, y2, xf, yf))
xs, ys = x2, y2
x, y = xf, yf
elif opcode == 'Q':
xs, ys, x, y = pnext(d, 4)
out.write("%g %g %g %g q\n" % (xs, ys, x, y))
elif opcode == 'q':
xs, ys, xf, yf = pnext(d, 4)
xs += x; ys += y
x += xf; y += yf
out.write("%g %g %g %g q\n" % (xs, ys, x, y))
elif opcode == 'T' or opcode == 't':
xf, yf = pnext(d, 2)
if opcode == 't':
xf += x; yf += y
x1 = x + (x - xs); y1 = y + (y - ys)
out.write("%g %g %g %g q\n" % (x1, y1, xf, yf))
xs, ys = x1, y1
x, y = xf, yf
elif opcode == 'A' or opcode == 'a':
rx, ry, phi, large_arc, sweep, x2, y2 = pnext(d, 7)
if opcode == 'a':
x2 += x; y2 += y
draw_arc(out, x, y, rx, ry, phi, large_arc, sweep, x2, y2)
x, y = x2, y2
elif opcode in 'zZ':
x, y = x0, y0
out.write("h\n")
else:
sys.stderr.write("Unrecognised opcode: %s\n" % opcode)
def parse_transformation(txt):
d = re.findall("[a-zA-Z]+\([^)]*\)", txt)
m = Matrix()
while d:
m1 = Matrix(d.pop(0))
m = m * m1
return m
def get_gradientTransform(n):
if n.hasAttribute("gradientTransform"):
return parse_transformation(n.getAttribute("gradientTransform"))
return Matrix()
def parse_transform(n):
if n.hasAttribute("transform"):
return parse_transformation(n.getAttribute("transform"))
return None
# Convert from endpoint to center parameterization
# www.w3.org/TR/2003/REC-SVG11-20030114/implnote.html#ArcImplementationNotes
def draw_arc(out, x1, y1, rx, ry, phi, large_arc, sweep, x2, y2):
phi = math.pi * phi / 180.0
cp = math.cos(phi); sp = math.sin(phi)
dx = .5 * (x1 - x2); dy = .5 * (y1 - y2)
x1p = cp * dx + sp * dy; y1p = -sp * dx + cp * dy
r2 = (((rx * ry)**2 - (rx * y1p)**2 - (ry * x1p)**2)/
((rx * y1p)**2 + (ry * x1p)**2))
if r2 < 0: r2 = 0
r = math.sqrt(r2)
if large_arc == sweep:
r = -r
cxp = r * rx * y1p / ry; cyp = -r * ry * x1p / rx
cx = cp * cxp - sp * cyp + .5 * (x1 + x2)
cy = sp * cxp + cp * cyp + .5 * (y1 + y2)
m = Matrix([rx, 0, 0, ry, 0, 0])
m = Matrix([cp, sp, -sp, cp, cx, cy]) * m
if sweep == 0:
m = m * Matrix([1, 0, 0, -1, 0, 0])
out.write("%s %g %g a\n" % (str(m), x2, y2))
# --------------------------------------------------------------------
class Matrix():
# Default is identity matrix
def __init__(self, string = None):
self.values = [1, 0, 0, 1, 0, 0]
if not string or string == "":
return
if isinstance(string, list):
self.values = string
return
mat = re.match(r"([a-zA-Z]+)\(([^)]*)\)$", string)
if not mat:
sys.stderr.write("Unknown transform: %s\n" % string)
op = mat.group(1)
d = [float(x) for x in parse_list(mat.group(2))]
if op == "matrix":
self.values = d
elif op == "translate":
if len(d) == 1: d.append(0.0)
self.values = [1, 0, 0, 1, d[0], d[1]]
elif op == "scale":
if len(d) == 1: d.append(d[0])
sx, sy = d
self.values = [sx, 0, 0, sy, 0, 0]
elif op == "rotate":
phi = math.pi * d[0] / 180.0
self.values = [math.cos(phi), math.sin(phi),
-math.sin(phi), math.cos(phi), 0, 0]
elif op == "skewX":
tphi = math.tan(math.pi * d[0] / 180.0)
self.values = [1, 0, tphi, 1, 0, 0]
elif op == "skewY":
tphi = math.tan(math.pi * d[0] / 180.0)
self.values = [1, tphi, 0, 1, 0, 0]
else:
sys.stderr.write("Unknown transform: %s\n" % string)
def __call__(self, other):
return (self.values[0]*other[0] + self.values[2]*other[1] + self.values[4],
self.values[1]*other[0] + self.values[3]*other[1] + self.values[5])
def inverse(self):
d = float(self.values[0]*self.values[3] - self.values[1]*self.values[2])
return Matrix([self.values[3]/d, -self.values[1]/d,
-self.values[2]/d, self.values[0]/d,
(self.values[2]*self.values[5] -
self.values[3]*self.values[4])/d,
(self.values[1]*self.values[4] -
self.values[0]*self.values[5])/d])
def __mul__(self, other):
a, b, c, d, e, f = self.values
u, v, w, x, y, z = other.values
return Matrix([a*u + c*v, b*u + d*v, a*w + c*x,
b*w + d*x, a*y + c*z + e, b*y + d*z + f])
def __str__(self):
a, b, c, d, e, f = self.values
return "%g %g %g %g %g %g" % (a, b, c, d, e, f)
# --------------------------------------------------------------------
class Svg():
def __init__(self, fname):
try:
if fname == "--":
self.dom = xml.parseString(sys.stdin.read())
else:
self.dom = xml.parse(fname)
except ExpatError as exc:
sys.stderr.write('ERROR: Input is not valid xml data:\n')
sys.stderr.write(exc.message)
return
attr = { }
for a in attribute_names:
attr[a] = None
self.attributes = [ attr ]
self.defs = { }
self.symbols = { }
for n in self.dom.childNodes:
if n.nodeType == Node.ELEMENT_NODE and n.tagName == "svg":
if n.hasAttribute("viewBox"):
x, y, w, h = [float(x) for x in parse_list(n.getAttribute("viewBox"))]
self.width = w
self.height = h
self.origin = (x, y)
else:
self.width = parse_float(n.getAttribute("width")) or 500
self.height = parse_float(n.getAttribute("height")) or 500
self.origin = (0, 0)
self.root = n
return
# --------------------------------------------------------------------
def write_ipe_header(self):
self.out.write('<?xml version="1.0"?>\n')
self.out.write('<!DOCTYPE ipe SYSTEM "ipe.dtd">\n')
self.out.write('<ipe version="70212" creator="svgtoipe %s">\n' %
svgtoipe_version)
self.out.write('<ipestyle>\n')
self.out.write(('<layout paper="%d %d" frame="%d %d" ' +
'origin="0 0"/>\n') %
(self.width, self.height, self.width, self.height))
for t in range(10, 100, 10):
self.out.write('<opacity name="%d%%" value="0.%d"/>\n' % (t, t))
# set SVG defaults
self.out.write('<pathstyle cap="0" join="0" fillrule="wind"/>\n')
self.out.write('</ipestyle>\n')
# collect definitions
self.collect_definitions(self.root)
# write definitions into stylesheet
if len(self.defs) > 0:
self.out.write('<ipestyle>\n')
for k in self.defs:
if self.defs[k][0] == "linearGradient":
self.write_linear_gradient(k)
elif self.defs[k][0] == "radialGradient":
self.write_radial_gradient(k)
self.out.write('</ipestyle>\n')
def collect_definitions(self, node):
# Collect Everything inside a <defs> tag for which we have a defs_<tagname>() method
defs_children = node.getElementsByTagName('defs')
for c in defs_children:
for my_attr in dir(self):
if my_attr[:4] == 'def_':
tag_name = my_attr[4:]
# There is a parsing method for tags named <tag_name>
parseable_tags = node.getElementsByTagName(tag_name)
for tag in parseable_tags:
getattr(self, my_attr)(tag)
def parse_svg(self, outname, **kwargs):
""" parses the svg, and writes it to file.
outname, str, output filename. if '--' then parse_svg writes to stdout.
Keywords:
outmode, str, 'file' write in ipe file format.
'clipboard', write as ipe clipboard selection
"""
outmode = kwargs.get('outmode', 'file')
if outname == '--':
self.out = sys.stdout
else:
self.out = open(outname, "w")
# write header
if outmode == 'file':
self.write_ipe_header()
self.out.write('<page>\n')
elif outmode == "clipboard":
self.out.write('<ipeselection pos="0 0">\n')
else:
sys.stderr.write("bad output mode '{}'".format(outmode))
return
self.write_data()
if outmode == 'file':
self.out.write('</page>\n')
self.out.write('</ipe>\n')
elif outmode == 'clipboard':
self.out.write('</ipeselection>\n')
self.out.close()
def write_data(self):
# start real data
m = Matrix([1, 0, 0, 1, 0, self.height / 2.0])
m = m * Matrix([1, 0, 0, -1, 0, 0])
m = m * Matrix([1, 0, 0, 1,
-self.origin[0], -(self.origin[1] + self.height / 2.0)])
self.out.write('<group matrix="%s">\n' % str(m))
self.parse_nodes(self.root)
self.out.write('</group>\n')
def parse_nodes(self, root):
""" parses recognized svg elements """
for n in root.childNodes:
if n.nodeType != Node.ELEMENT_NODE:
continue
if n.tagName in ignored_nodes:
continue
nodeName = "node_" + n.tagName.replace(":","_")
if hasattr(self, nodeName):
getattr(self, nodeName)(n)
else:
sys.stderr.write("Unhandled node: %s\n" % n.tagName)
# --------------------------------------------------------------------
def write_linear_gradient(self, k):
typ, x1, x2, y1, y2, stops, matrix = self.defs[k]
self.out.write('<gradient name="g%s" type="axial" extend="yes"\n' % k)
self.out.write(' matrix="%s"' % str(matrix))
self.out.write(' coords="%g %g %g %g">\n' % (x1, y1, x2, y2))
for s in stops:
offset, color = s
self.out.write(' <stop offset="%g" color="%g %g %g"/>\n' %
(offset, color[0], color[1], color[2]))
self.out.write('</gradient>\n')
def write_radial_gradient(self, k):
typ, cx, cy, r, fx, fy, stops, matrix = self.defs[k]
self.out.write('<gradient name="g%s" type="radial" extend="yes"\n' % k)
self.out.write(' matrix="%s"' % str(matrix))
self.out.write(' coords="%g %g %g %g %g %g">\n' % (fx, fy, 0, cx, cy, r))
for s in stops:
offset, color = s
self.out.write(' <stop offset="%g" color="%g %g %g"/>\n' %
(offset, color[0], color[1], color[2]))
self.out.write('</gradient>\n')
def get_stops(self, n):
stops = []
for m in n.childNodes:
if m.nodeType != Node.ELEMENT_NODE:
continue
if m.tagName != "stop":
continue # should not happen
offs = m.getAttribute("offset") or "0"
if offs.endswith("%"):
offs = float(offs[:-1]) / 100.0
else:
offs = float(offs)
if m.hasAttribute("stop-color"):
color = parse_color(m.getAttribute("stop-color"))
else:
color = [ 0, 0, 0 ]
if m.hasAttribute("style"):
sdict = parse_style(m.getAttribute("style"))
if "stop-color" in sdict:
color = parse_color(sdict["stop-color"])
stops.append((offs, color))
if len(stops) == 0:
if n.hasAttribute("xlink:href"):
ref = n.getAttribute("xlink:href")
if ref.startswith("#") and ref[1:] in self.defs:
gradient = self.defs[ref[1:]]
# TODO: use a class instead of a tuple!
if gradient[0] == "linearGradient":
stops = gradient[5]
else:
stops = gradient[6]
return stops
def node_inkscape_clipboard(self, node):
self.parse_nodes(node)
def def_symbol(self, node):
# Symbols are parsed by pretending the symbol node is a group node and storing it in
# self.symbols. Later, whenever a <use ...> tag is encountered,
# we call node_g() on the fake group node.
self.symbols[node.getAttribute('id')] = node
def def_linearGradient(self, n):
#printAttributes(n)
kid = n.getAttribute("id")
x1 = 0; y1 = 0
x2 = self.width; y2 = self.height
if n.hasAttribute("x1"):
s = n.getAttribute("x1")
if s.endswith("%"):
x1 = self.width * float(s[:-1]) / 100.0
else:
x1 = parse_float(s)
if n.hasAttribute("x2"):
s = n.getAttribute("x2")
if s.endswith("%"):
x2 = self.width * float(s[:-1]) / 100.0
else:
x2 = parse_float(s)
if n.hasAttribute("y1"):
s = n.getAttribute("y1")
if s.endswith("%"):
y1 = self.width * float(s[:-1]) / 100.0
else:
y1 = parse_float(s)
if n.hasAttribute("y2"):
s = n.getAttribute("y2")
if s.endswith("%"):
y2 = self.width * float(s[:-1]) / 100.0
else:
y2 = parse_float(s)
matrix = get_gradientTransform(n)
stops = self.get_stops(n)
self.defs[kid] = ("linearGradient", x1, x2, y1, y2, stops, matrix)
def def_radialGradient(self, n):
#printAttributes(n)
kid = n.getAttribute("id")
cx = "50%"; cy = "50%"; r = "50%"
if n.hasAttribute("cx"):
cx = n.getAttribute("cx")
if cx.endswith("%"):
cx = self.width * float(cx[:-1]) / 100.0
else:
cx = parse_float(cx)
if n.hasAttribute("cy"):
cy = n.getAttribute("cy")
if cy.endswith("%"):
cy = self.width * float(cy[:-1]) / 100.0
else:
cy = parse_float(cy)
if n.hasAttribute("r"):
r = n.getAttribute("r")
if r.endswith("%"):
r = self.width * float(r[:-1]) / 100.0
else:
r = parse_float(r)
if n.hasAttribute("fx"):
s = n.getAttribute("fx")
if s.endswith("%"):
fx = self.width * float(s[:-1]) / 100.0
else:
fx = parse_float(s)
else:
fx = cx
if n.hasAttribute("fy"):
s = n.getAttribute("fy")
if s.endswith("%"):
fy = self.width * float(s[:-1]) / 100.0
else:
fy = parse_float(s)
else:
fy = cy
matrix = get_gradientTransform(n)
stops = self.get_stops(n)
self.defs[kid] = ("radialGradient", cx, cy, r, fx, fy, stops, matrix)
def def_clipPath(self, node):
kid = node.getAttribute("id")
# only a single path is implemented
for n in node.childNodes:
if n.nodeType != Node.ELEMENT_NODE or n.tagName != "path":
continue
m = parse_transform(n)
d = n.getAttribute("d")
output = io.StringIO()
parse_path(output, d)
path = output.getvalue()
output.close()
self.defs[kid] = ("clipPath", m, path)
return
def def_g(self, group):
self.collect_definitions(group)
def def_defs(self, node):
self.collect_definitions(node)
# --------------------------------------------------------------------
def parse_attributes(self, n):
pattr = self.attributes[-1]
attr = { }
for a in attribute_names:
if n.hasAttribute(a):
attr[a] = n.getAttribute(a)
else:
attr[a] = pattr[a]
if n.hasAttribute("style"):
sdict = parse_style(n.getAttribute("style"))
for a in attribute_names:
if a in sdict:
attr[a] = sdict[a]
return attr
def write_pathattributes(self, a):
stroke = parse_color(a["stroke"])
if stroke:
self.out.write(' stroke="%g %g %g"' % stroke)
fill = a["fill"]
if fill and fill.startswith("url("):
mat = re.match("url\(#([^)]+)\).*", fill)
if mat:
grad = mat.group(1)
if grad in self.defs and (self.defs[grad][0] == "linearGradient" or
self.defs[grad][0] == "radialGradient"):
self.out.write(' fill="1" gradient="g%s"' % grad)
elif fill is None:
self.out.write(' fill="0"')
else:
fill = parse_color(fill)
if fill:
self.out.write(' fill="%g %g %g"' % fill)
opacity = parse_opacity(a["opacity"])
fill_opacity = parse_opacity(a["fill-opacity"])
stroke_opacity = parse_opacity(a["stroke-opacity"])
if fill and fill_opacity:
opacity = fill_opacity
# if not fill and stroke and stroke_opacity:
# opacity = stroke_opacity
# if opacity and opacity != 100:
if fill:
if opacity and opacity != 100:
self.out.write(' opacity="%d%%"' % opacity)
if stroke and (not stroke_opacity or stroke_opacity == 100):
self.out.write(' stroke-opacity="opaque"')
if not fill and stroke:
if stroke_opacity:
opacity = stroke_opacity
if opacity:
self.out.write(' opacity="%d%%"' % opacity)
stroke_width = parse_float(a["stroke-width"])
if a["stroke-width"]:
self.out.write(' pen="%g"' % stroke_width)
if a["fill-rule"] == "nonzero":
self.out.write(' fillrule="wind"')
k = {"butt" : 0, "round" : 1, "square" : 2 }
if a["stroke-linecap"] in k:
self.out.write(' cap="%d"' % k[a["stroke-linecap"]])
k = {"miter" : 0, "round" : 1, "bevel" : 2 }
if a["stroke-linejoin"] in k:
self.out.write(' join="%d"' % k[a["stroke-linejoin"]])
dasharray = a["stroke-dasharray"]
dashoffset = a["stroke-dashoffset"]
if dasharray and dashoffset and dasharray != "none":
d = parse_list(dasharray)
off = parse_float(dashoffset)
self.out.write(' dash="[%s] %g"' % (" ".join(d), off))
# --------------------------------------------------------------------
def node_g(self, group):
# printAttributes(group)
attr = self.parse_attributes(group)
self.attributes.append(attr)
self.out.write('<group')
m = parse_transform(group)
if m:
self.out.write(' matrix="%s"' % m)
self.out.write('>\n')
self.parse_nodes(group)
self.out.write('</group>\n')
self.attributes.pop()
def node_use(self, n):
if not n.hasAttribute('xlink:href'):
print("Ignoring use node without xlink:href")
return
symbol_name = n.getAttribute('xlink:href')
if symbol_name[0] != '#':
print("Ignoring use node not referencing symbol ID")
return
symbol_name = symbol_name[1:]
if symbol_name not in self.symbols:
print("Ignoring use node for unknown symbol '{}'".format(symbol_name))
return
translate_x = '0'
if n.hasAttribute('x'):
translate_x = n.getAttribute('x')
translate_y = '0'
if n.hasAttribute('y'):
translate_y = n.getAttribute('y')
scale_x = 1.0
if n.hasAttribute('width'):
if n.getAttribute('width')[-1] != '%':
print("Ignoring use with non-relative width scale.")
return
scale_x = float(n.getAttribute('width')[:-1]) / 100.0
scale_y = 1.0
if n.hasAttribute('height'):
if n.getAttribute('height')[-1] != '%':
print("Ignoring use with non-relative height scale.")
return
scale_y = float(n.getAttribute('height')[:-1]) / 100.0
scale_m = Matrix("scale({} {})".format(scale_x, scale_y))
translate_m = Matrix(
"translate({} {})".format(translate_x, translate_y))
m = scale_m * translate_m
# Use a group for the possible transform matrix from this
# use node
self.out.write('<group matrix="{}">\n'.format(m))
self.node_g(self.symbols[symbol_name])
self.out.write('</group>\n')
def node_a(self, n):
url = n.getAttribute("xlink:href")
if url is not None:
self.out.write('<group url="%s">' % url)
self.parse_nodes(n)
if url is not None:
self.out.write('</group>\n')
def collect_text(self, root):
for n in root.childNodes:
if n.nodeType == Node.TEXT_NODE:
self.text += n.data
if n.nodeType != Node.ELEMENT_NODE:
continue
if n.tagName == "tspan": # recurse
self.collect_text(n)
def parse_text_style(self, t):
attrs = {}
if not t.hasAttribute("style"):
return attrs
tokens = t.getAttribute("style").split(";")
for token in tokens:
if (len(token.split(":")) != 2):
# Strange token
sys.stderr.write("Ignored style token: %s\n" % token)
continue
key, value = token.split(":")
value = value.strip().lower()
key = key.strip().lower()
if key == "font-weight":
if value in ("bold", "bolder"):
attrs["weight"] = "bold"
elif key == "font-size":
attrs["size"] = parse_float(value)
elif key == "font-family":
attrs["svg-families"] = [v.strip() for v in (value.split(","))]
elif key == "text-anchor":
attrs["anchor"] = value
return attrs
def parse_text_attrs(self, t):
raw_attrs = self.parse_attributes(t)
attrs = {}
if raw_attrs["font-size"]:
attrs["size"] = parse_float(raw_attrs["font-size"])
if raw_attrs["fill"]:
attrs["color"] = parse_color(raw_attrs["fill"])
return attrs
def map_svg_font_families(self, families):
# return the first family for which we find a mapping
for family in families:
if family in ("helvetica", "sans-serif"):
return "phv"
elif family in ("times new roman", "times", "serif"):
return "ptm"
return None
def node_text(self, t):
if not t.hasAttribute("x"):
x = 0.0
else:
x = float(t.getAttribute("x"))
if not t.hasAttribute("y"):
y = 0.0
else:
y = float(t.getAttribute("y"))
attributes = self.parse_text_style(t)
attributes.update(self.parse_text_attrs(t))
self.out.write('<text pos="%g %g"' % (x,y))
self.out.write(' transformations="affine" valign="baseline"')
m = parse_transform(t)
if not m: m = Matrix()
m = m * Matrix([1, 0, 0, -1, x, y]) * Matrix([1, 0, 0, 1, -x, -y])
self.out.write(' matrix="%s"' % m)
if "size" in attributes:
self.out.write(' size="%g"' % attributes["size"])
if "color" in attributes and attributes["color"] is not None:
self.out.write(' stroke="%g %g %g"' % attributes["color"])
halign = "left"
valign = "bottom"
if "anchor" in attributes:
if attributes["anchor"] == "middle":
halign = "center"
valign = "center"
elif attributes["anchor"] == "end":
halign = "right"
self.out.write(' valign="%s"' % valign)
self.out.write(' halign="%s"' % halign)
self.text = ""
self.collect_text(t)
if "svg-families" in attributes:
mapped_family = self.map_svg_font_families(attributes["svg-families"])
if mapped_family is not None:
self.text = "{\\fontfamily{" + mapped_family + "}\\selectfont{}" + self.text + "}"
if "weight" in attributes:
if attributes["weight"] == "bold":
self.text = "\\bf{" + self.text + "}"
self.out.write('>%s</text>\n' % self.text)
def node_image(self, node):
if not have_pil:
sys.stderr.write("No Python image library, <image> ignored\n")
return
href = node.getAttribute("xlink:href")
if not href.startswith("data:image/png;base64,"):
sys.stderr.write("Image ignored, href = %s...\n" % href[:40])
return
x = float(node.getAttribute("x"))
y = float(node.getAttribute("y"))
w = float(node.getAttribute("width"))
h = float(node.getAttribute("height"))
clipped = False
if node.hasAttribute("clip-path"):
mat = re.match("url\(#([^)]+)\).*", node.getAttribute("clip-path"))
if mat:
cp = mat.group(1)
if cp in self.defs and self.defs[cp][0] == "clipPath":
cp, m, path = self.defs[cp]
clipped = True
self.out.write('<group matrix="%s" clip="%s">\n' % (str(m), path))
self.out.write('<group matrix="%s">\n' % str(m.inverse()))
self.out.write('<image rect="%g %g %g %g"' % (x, y, x + w, y + h))
data = base64.b64decode(href[22:])
fin = io.StringIO(data)
image = Image.open(fin)
m = parse_transform(node)
if not m:
m = Matrix()
m = m * Matrix([1, 0, 0, -1, x, y+h]) * Matrix([1, 0, 0, 1, -x, -y])
self.out.write(' matrix="%s"' % m)
self.out.write(' width="%d" height="%d" ColorSpace="DeviceRGB"' %
image.size)
self.out.write(' BitsPerComponent="8" encoding="base64"> \n')
if True:
data = io.StringIO()
for pixel in image.getdata():
data.write("%c%c%c" % pixel[:3])
self.out.write(base64.b64encode(data.getvalue()))
data.close()
else:
count = 0
for pixel in image.getdata():
self.out.write("%02x%02x%02x" % pixel[:3])
count += 1
if count == 10:
self.out.write("\n")
count = 0
fin.close()
self.out.write('</image>\n')
if clipped:
self.out.write('</group>\n</group>\n')
# handled in def pass
def node_linearGradient(self, n):
pass
def node_radialGradient(self, n):
pass
def node_rect(self, n):
attr = self.parse_attributes(n)
self.out.write('<path')
m = parse_transform(n)
if m:
self.out.write(' matrix="%s"' % m)
self.write_pathattributes(attr)
self.out.write('>\n')
if not n.hasAttribute("x"):
x = 0.0
else:
x = parse_float(n.getAttribute("x"))
if not n.hasAttribute("y"):
y = 0.0
else:
y = parse_float(n.getAttribute("y"))
w = parse_float(n.getAttribute("width"))
h = parse_float(n.getAttribute("height"))
self.out.write("%g %g m %g %g l %g %g l %g %g l h\n" %
(x, y, x + w, y, x + w, y + h, x, y + h))
self.out.write('</path>\n')
def node_circle(self, n):
self.out.write('<path')
m = parse_transform(n)
if m:
self.out.write(' matrix="%s"' % m)
attr = self.parse_attributes(n)
self.write_pathattributes(attr)
self.out.write('>\n')
cx = 0
cy = 0
if n.hasAttribute("cx"):
cx = parse_float(n.getAttribute("cx"))
if n.hasAttribute("cy"):
cy = parse_float(n.getAttribute("cy"))
r = parse_float(n.getAttribute("r"))
self.out.write("%g 0 0 %g %g %g e\n" % (r, r, cx, cy))
self.out.write('</path>\n')
def node_ellipse(self, n):
self.out.write('<path')
m = parse_transform(n)
if m:
self.out.write(' matrix="%s"' % m)
attr = self.parse_attributes(n)
self.write_pathattributes(attr)
self.out.write('>\n')
cx = 0
cy = 0
if n.hasAttribute("cx"):
cx = float(n.getAttribute("cx"))
if n.hasAttribute("cy"):
cy = float(n.getAttribute("cy"))
rx = float(n.getAttribute("rx"))
ry = float(n.getAttribute("ry"))
self.out.write("%g 0 0 %g %g %g e\n" % (rx, ry, cx, cy))
self.out.write('</path>\n')
def node_line(self, n):
self.out.write('<path')
m = parse_transform(n)
if m:
self.out.write(' matrix="%s"' % m)
attr = self.parse_attributes(n)
self.write_pathattributes(attr)
self.out.write('>\n')
x1 = 0; y1 = 0; x2 = 0; y2 = 0
if n.hasAttribute("x1"):
x1 = float(n.getAttribute("x1"))
if n.hasAttribute("y1"):
y1 = float(n.getAttribute("y1"))
if n.hasAttribute("x2"):
x2 = float(n.getAttribute("x2"))
if n.hasAttribute("y2"):
y2 = float(n.getAttribute("y2"))
self.out.write("%g %g m %g %g l\n" % (x1, y1, x2, y2))
self.out.write('</path>\n')
def node_polyline(self, n):
self.polygon(n, closed=False)
def node_polygon(self, n):
self.polygon(n, closed=True)
def polygon(self, n, closed):
self.out.write('<path')
m = parse_transform(n)
if m:
self.out.write(' matrix="%s"' % m)
attr = self.parse_attributes(n)
self.write_pathattributes(attr)
self.out.write('>\n')
d = parse_list(n.getAttribute("points"))
op = "m"
while d:
x = float(d.pop(0))
y = float(d.pop(0))
self.out.write("%g %g %s\n" % (x, y, op))
op = "l"
if closed:
self.out.write("h\n")
self.out.write('</path>\n')
def node_path(self, n):
d = n.getAttribute("d")
if len(d) == 0:
# Empty paths crash ipe, filter them out
return
self.out.write('<path')
m = parse_transform(n)
if m:
self.out.write(' matrix="%s"' % m)
attr = self.parse_attributes(n)
self.write_pathattributes(attr)
self.out.write('>\n')
parse_path(self.out, d)
self.out.write('</path>\n')
# --------------------------------------------------------------------
def parse_arguments():
""" parses command line arguments"""
parser = argparse.ArgumentParser(
description="convert SVG into Ipe files",
epilog="""
Supported SVG elements:
path,image,rect,circle,ellipse,line,polygon,polyline
Supported SVG attributes:
group,clipPath,linearGradient,radialGradient
""")
parser.add_argument('-c', '--clipboard', dest='clipboard',
action='store_true',
help="""
if -c is present, svg data is written as clipboard content.
This allows pasting svg data from inkscape to ipe:
(1) Copy Inkscape elements to clipboard,
(2) run: 'xsel | ./svgtoipe.py -c -- | xsel -i',
(3) paste clipboard content into Ipe.
""")
parser.add_argument('infile', metavar='svg-file.svg',
help="input file in svg format. If infile = '--' svg data is read from stdin.")
parser.add_argument('outfile', metavar='ipe-file.ipe', nargs='?',
help="""
filename to write output. If no filename is given, the input filename
together with '.ipe' extension is used. If outfile is '--', then data
is written to stdout.""")
parser.set_defaults(
infile="--",
verbosity=0,
clipboard=False,
)
#try:
args = parser.parse_args()
if args.outfile is None:
if args.infile != "--":
args.outfile = args.infile[:-4] + ".ipe"
return args
#except Exception as exc:
#sys.stderr.write("parsing error:\n")
#sys.exit(1)
def main():
args = parse_arguments()
svg = Svg(args.infile)
if args.clipboard:
svg.parse_svg(args.outfile, outmode="clipboard")
else:
svg.parse_svg(args.outfile)
sys.exit(0)
if __name__ == '__main__':
main()
# --------------------------------------------------------------------