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
)