1# -*- encoding: utf-8 -*-
2#
3#
4# Copyright (C) 2015 André Wobst <wobsta@pyx-project.org>
5#
6# This file is part of PyX (https://pyx-project.org/).
7#
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.
12#
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.
17#
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
21
22import io, copy, time, xml.sax.saxutils
23from . import bbox, config, style, version, unit, trafo
24
25svg_uri = "http://www.w3.org/2000/svg"
26xlink_uri = "http://www.w3.org/1999/xlink"
27
28class SVGregistry:
29
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 = []
37
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)
45
46    def mergeregistry(self, registry):
47        for resource in registry.resources:
48            self.add(resource)
49
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")
56
57#
58# Abstract base class
59#
60
61class SVGresource:
62
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
69
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
74
75    def output(self, xml, writer, registry):
76        raise NotImplementedError("output not implemented for %s" % repr(self))
77
78
79#
80# XML generator with shortcut namespace support
81#
82
83class SVGGenerator(xml.sax.saxutils.XMLGenerator):
84
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
90
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
104
105    def convertAttrs(self, attrs):
106        return {self.convertName(name): value for name, value in attrs.items()}
107
108    def startDocument(self, *args, **kwargs):
109        if not self.passthrough:
110            raise NotImplemented("use startSVGDocument")
111
112    def endDocument(self, *args, **kwargs):
113        if not self.passthrough:
114            raise NotImplemented("use endSVGDocument")
115
116    def startElementNS(self, *args, **kwargs):
117        if not self.passthrough:
118            raise NotImplemented("use startSVGElement")
119        super().startElementNS(*args, **kwargs)
120
121    def endElementNS(self, *args, **kwargs):
122        if not self.passthrough:
123            raise NotImplemented("use endSVGElement")
124        super().endElementNS(*args, **kwargs)
125
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
134
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
145
146    def newline_and_tell(self):
147        self.characters("\n")
148        self.newline = True
149        return self.svg.tell()
150
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
162
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()
170
171
172#
173# Writer
174#
175
176class SVGwriter:
177
178    def __init__(self, document, file, textaspath=True, meshasbitmapresolution=300, text_as_path=None, mesh_as_bitmap_resolution=None):
179        self._fontmap = None
180        if text_as_path is not None:
181            logger.warning("SVGwriter: text_as_path deprecated, use textaspath instead")
182            textaspath = text_as_path
183        self.textaspath = textaspath
184        if mesh_as_bitmap_resolution is not None:
185            logger.warning("SVGwriter: mesh_as_bitmap_resolution deprecated, use meshasbitmapresolution instead")
186            meshasbitmapresolution = mash_as_bitmap_resolution
187        self.meshasbitmapresolution = meshasbitmapresolution
188
189        # dictionary mapping font names to dictionaries mapping encoding names to encodings
190        # encodings themselves are mappings from glyphnames to codepoints
191        self.encodings = {}
192
193        if len(document.pages) != 1:
194            raise ValueError("SVG file can be constructed out of a single page document only")
195        page = document.pages[0]
196
197        pagefile = io.BytesIO()
198        pagesvg = SVGGenerator(pagefile)
199        registry = SVGregistry()
200        acontext = context()
201        pagebbox = bbox.empty()
202
203        pagesvg.startSVGDocument()
204        pagesvg.startSVGElement("svg", {})
205        pagexml_start = pagesvg.newline_and_tell()
206        page.processSVG(pagesvg, self, acontext, registry, pagebbox)
207        pagexml_end = pagesvg.newline_and_tell()
208        pagesvg.endSVGElement("svg")
209        pagesvg.endSVGDocument()
210
211        x = SVGGenerator(file, xlink=pagesvg.xlink_used)
212        x.startSVGDocument()
213        attrs = {"fill": "none", "version": "1.1"}
214        if pagebbox:
215            # note that svg uses an inverse y coordinate; to compansate this
216            # PyX writes negative y coordinates and the viewbox needs to be
217            # adjusted accordingly (by that instead of a transforamtion
218            # a text remains upright).
219            llx, lly, urx, ury = pagebbox.highrestuple_pt()
220            attrs["viewBox"] = "%g %g %g %g" % (llx, -ury, urx-llx, ury-lly)
221            attrs["x"] = "%gpt" % llx
222            attrs["y"] = "%gpt" % -ury
223            attrs["width"] = "%gpt" % (urx-llx)
224            attrs["height"] = "%gpt" % (ury-lly)
225        style.linewidth.normal.processSVGattrs(attrs, self, acontext, registry)
226        style.miterlimit.lessthan11deg.processSVGattrs(attrs, self, acontext, registry)
227        x.startSVGElement("svg", attrs)
228        registry.output(x, self)
229        pagedata = pagefile.getvalue()
230        x.newline_and_tell()
231        file.write(pagedata[pagexml_start:pagexml_end])
232        x.endSVGElement("svg")
233        x.endSVGDocument()
234
235    def getfontmap(self):
236        if self._fontmap is None:
237            # late import due to cyclic dependency
238            from pyx.dvi import mapfile
239            fontmapfiles = config.getlist("text", "psfontmaps", ["psfonts.map"])
240            self._fontmap = mapfile.readfontmap(fontmapfiles)
241        return self._fontmap
242
243
244
245class context:
246
247    def __init__(self):
248        self.linewidth_pt = unit.topt(style.linewidth.normal.width)
249        self.strokeattr = True
250        self.fillattr = True
251        self.fillcolor = "black"
252        self.strokecolor = "black"
253        self.fillopacity = 1
254        self.strokeopacity = 1
255        self.indent = 1
256
257    def __call__(self, **kwargs):
258        newcontext = copy.copy(self)
259        newcontext.indent += 1
260        for key, value in list(kwargs.items()):
261            setattr(newcontext, key, value)
262        return newcontext
263