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"
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
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)
43
self.resourceshash[rkey] = resource
44
self.resourceslist.append(resource)
46
def mergeregistry(self, registry):
47
for resource in registry.resources:
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")
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
70
def merge(self, other):
71
""" merge self with other, which has to be a resource of the same type and with
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)
88
self.xlink_enabled = xlink
89
self.passthrough = False
91
def convertName(self, name):
92
split = name.split(":")
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
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)
133
self.xlink_used = False
135
def startSVGElement(self, name, attrs):
138
self.characters("\n")
139
self.characters(" "*self.indent)
140
super().startElementNS(self.convertName(name), None, self.convertAttrs(attrs))
143
self.last_was_end = False
146
def newline_and_tell(self):
147
self.characters("\n")
149
return self.svg.tell()
151
def endSVGElement(self, name):
154
if self.last_was_end:
156
self.characters("\n")
157
self.characters(" "*self.indent)
158
super().endElementNS(self.convertName(name), None)
160
self.last_was_end = True
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()
178
def __init__(self, document, file, text_as_path=True, mesh_as_bitmap_resolution=300):
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
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()
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)
207
attrs = {"fill": "none", "version": "1.1"}
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()
225
file.write(pagedata[pagexml_start:pagexml_end])
226
x.endSVGElement("svg")
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)
242
self.linewidth_pt = unit.topt(style.linewidth.normal.width)
243
self.strokeattr = True
245
self.fillcolor = "black"
246
self.strokecolor = "black"
248
self.strokeopacity = 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)