fix cross-device link error
[PyX.git] / pyx / svgwriter.py
blob45862672bc0e35774c8529d5bd26dca549aa8b9d
1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2015 André Wobst <wobsta@users.sourceforge.net>
6 # This file is part of PyX (http://pyx.sourceforge.net/).
8 # PyX is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
13 # PyX is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with PyX; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 import io, copy, time, xml.sax.saxutils
23 from . import bbox, config, style, version, unit, trafo
25 svg_uri = "http://www.w3.org/2000/svg"
26 xlink_uri = "http://www.w3.org/1999/xlink"
28 class SVGregistry:
30 def __init__(self):
31 # in order to keep a consistent order of the registered resources we
32 # not only store them in a hash but also keep an ordered list (up to a
33 # possible merging of resources, in which case the first instance is
34 # kept)
35 self.resourceshash = {}
36 self.resourceslist = []
38 def add(self, resource):
39 rkey = (resource.type, resource.id)
40 if rkey in self.resourceshash:
41 self.resourceshash[rkey].merge(resource)
42 else:
43 self.resourceshash[rkey] = resource
44 self.resourceslist.append(resource)
46 def mergeregistry(self, registry):
47 for resource in registry.resources:
48 self.add(resource)
50 def output(self, xml, writer):
51 if self.resourceslist:
52 xml.startSVGElement("defs", {})
53 for resource in self.resourceslist:
54 resource.output(xml, writer, self)
55 xml.endSVGElement("defs")
58 # Abstract base class
61 class SVGresource:
63 def __init__(self, type, id):
64 # Every SVGresource has to have a type and a unique id.
65 # Resources with the same type and id will be merged
66 # when they are registered in the SVGregistry
67 self.type = type
68 self.id = id
70 def merge(self, other):
71 """ merge self with other, which has to be a resource of the same type and with
72 the same id"""
73 pass
75 def output(self, xml, writer, registry):
76 raise NotImplementedError("output not implemented for %s" % repr(self))
80 # XML generator with shortcut namespace support
83 class SVGGenerator(xml.sax.saxutils.XMLGenerator):
85 def __init__(self, svg, xlink=True):
86 super().__init__(svg, "utf-8", short_empty_elements=True)
87 self.svg = svg
88 self.xlink_enabled = xlink
89 self.passthrough = False
91 def convertName(self, name):
92 split = name.split(":")
93 if len(split) == 1:
94 uri = svg_uri
95 name = split[0]
96 else:
97 short_uri, name = split
98 assert short_uri == "xlink"
99 if not self.xlink_enabled:
100 raise ValueError("xlink namespace found but not enabled")
101 self.xlink_used = True
102 uri = xlink_uri
103 return uri, name
105 def convertAttrs(self, attrs):
106 return {self.convertName(name): value for name, value in attrs.items()}
108 def startDocument(self, *args, **kwargs):
109 if not self.passthrough:
110 raise NotImplemented("use startSVGDocument")
112 def endDocument(self, *args, **kwargs):
113 if not self.passthrough:
114 raise NotImplemented("use endSVGDocument")
116 def startElementNS(self, *args, **kwargs):
117 if not self.passthrough:
118 raise NotImplemented("use startSVGElement")
119 super().startElementNS(*args, **kwargs)
121 def endElementNS(self, *args, **kwargs):
122 if not self.passthrough:
123 raise NotImplemented("use endSVGElement")
124 super().endElementNS(*args, **kwargs)
126 def startSVGDocument(self):
127 super().startDocument()
128 super().startPrefixMapping(None, svg_uri)
129 if self.xlink_enabled:
130 super().startPrefixMapping("xlink", xlink_uri)
131 self.indent = 0
132 self.newline = True
133 self.xlink_used = False
135 def startSVGElement(self, name, attrs):
136 if name != "tspan":
137 if not self.newline:
138 self.characters("\n")
139 self.characters(" "*self.indent)
140 super().startElementNS(self.convertName(name), None, self.convertAttrs(attrs))
141 if name != "tspan":
142 self.indent += 1
143 self.last_was_end = False
144 self.newline = False
146 def newline_and_tell(self):
147 self.characters("\n")
148 self.newline = True
149 return self.svg.tell()
151 def endSVGElement(self, name):
152 if name != "tspan":
153 self.indent -= 1
154 if self.last_was_end:
155 if not self.newline:
156 self.characters("\n")
157 self.characters(" "*self.indent)
158 super().endElementNS(self.convertName(name), None)
159 if name != "tspan":
160 self.last_was_end = True
161 self.newline = False
163 def endSVGDocument(self):
164 assert not self.indent
165 self.characters("\n")
166 super().endPrefixMapping(None)
167 if self.xlink_enabled:
168 super().endPrefixMapping("xlink")
169 super().endDocument()
173 # Writer
176 class SVGwriter:
178 def __init__(self, document, file, text_as_path=True, mesh_as_bitmap_resolution=300):
179 self._fontmap = None
180 self.text_as_path = text_as_path
181 self.mesh_as_bitmap_resolution = mesh_as_bitmap_resolution
183 # dictionary mapping font names to dictionaries mapping encoding names to encodings
184 # encodings themselves are mappings from glyphnames to codepoints
185 self.encodings = {}
187 if len(document.pages) != 1:
188 raise ValueError("SVG file can be constructed out of a single page document only")
189 page = document.pages[0]
191 pagefile = io.BytesIO()
192 pagesvg = SVGGenerator(pagefile)
193 registry = SVGregistry()
194 acontext = context()
195 pagebbox = bbox.empty()
197 pagesvg.startSVGDocument()
198 pagesvg.startSVGElement("svg", {})
199 pagexml_start = pagesvg.newline_and_tell()
200 page.processSVG(pagesvg, self, acontext, registry, pagebbox)
201 pagexml_end = pagesvg.newline_and_tell()
202 pagesvg.endSVGElement("svg")
203 pagesvg.endSVGDocument()
205 x = SVGGenerator(file, xlink=pagesvg.xlink_used)
206 x.startSVGDocument()
207 attrs = {"fill": "none", "version": "1.1"}
208 if pagebbox:
209 # note that svg uses an inverse y coordinate; to compansate this
210 # PyX writes negative y coordinates and the viewbox needs to be
211 # adjusted accordingly (by that instead of a transforamtion
212 # a text remains upright).
213 llx, lly, urx, ury = pagebbox.highrestuple_pt()
214 attrs["viewBox"] = "%g %g %g %g" % (llx, -ury, urx-llx, ury-lly)
215 attrs["x"] = "%gpt" % llx
216 attrs["y"] = "%gpt" % -ury
217 attrs["width"] = "%gpt" % (urx-llx)
218 attrs["height"] = "%gpt" % (ury-lly)
219 style.linewidth.normal.processSVGattrs(attrs, self, acontext, registry)
220 style.miterlimit.lessthan11deg.processSVGattrs(attrs, self, acontext, registry)
221 x.startSVGElement("svg", attrs)
222 registry.output(x, self)
223 pagedata = pagefile.getvalue()
224 x.newline_and_tell()
225 file.write(pagedata[pagexml_start:pagexml_end])
226 x.endSVGElement("svg")
227 x.endSVGDocument()
229 def getfontmap(self):
230 if self._fontmap is None:
231 # late import due to cyclic dependency
232 from pyx.dvi import mapfile
233 fontmapfiles = config.getlist("text", "psfontmaps", ["psfonts.map"])
234 self._fontmap = mapfile.readfontmap(fontmapfiles)
235 return self._fontmap
239 class context:
241 def __init__(self):
242 self.linewidth_pt = unit.topt(style.linewidth.normal.width)
243 self.strokeattr = True
244 self.fillattr = True
245 self.fillcolor = "black"
246 self.strokecolor = "black"
247 self.fillopacity = 1
248 self.strokeopacity = 1
249 self.indent = 1
251 def __call__(self, **kwargs):
252 newcontext = copy.copy(self)
253 newcontext.indent += 1
254 for key, value in list(kwargs.items()):
255 setattr(newcontext, key, value)
256 return newcontext