~ubuntu-branches/debian/sid/pyx3/sid

« back to all changes in this revision

Viewing changes to pyx/svgwriter.py

  • Committer: Package Import Robot
  • Author(s): Stuart Prescott
  • Date: 2015-05-08 00:00:33 UTC
  • mfrom: (1.1.1)
  • Revision ID: package-import@ubuntu.com-20150508000033-fp4theggupemhx24
Tags: 0.14-1
* New upstream release.
  - revert upstream change to sphinx conf.py to permit building with older
    python3-sphinx.
* Bump standards version to 3.9.6 (no changes required).
* Add SVG and PDF to package descriptions.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- encoding: utf-8 -*-
 
2
#
 
3
#
 
4
# Copyright (C) 2015 André Wobst <wobsta@users.sourceforge.net>
 
5
#
 
6
# This file is part of PyX (http://pyx.sourceforge.net/).
 
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
 
 
22
import io, copy, time, xml.sax.saxutils
 
23
from . import bbox, config, style, version, unit, trafo
 
24
 
 
25
svg_uri = "http://www.w3.org/2000/svg"
 
26
xlink_uri = "http://www.w3.org/1999/xlink"
 
27
 
 
28
class 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
 
 
61
class 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
 
 
83
class 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
 
 
176
class SVGwriter:
 
177
 
 
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
 
182
 
 
183
        # dictionary mapping font names to dictionaries mapping encoding names to encodings
 
184
        # encodings themselves are mappings from glyphnames to codepoints
 
185
        self.encodings = {}
 
186
 
 
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]
 
190
 
 
191
        pagefile = io.BytesIO()
 
192
        pagesvg = SVGGenerator(pagefile)
 
193
        registry = SVGregistry()
 
194
        acontext = context()
 
195
        pagebbox = bbox.empty()
 
196
 
 
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()
 
204
 
 
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()
 
228
 
 
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
 
236
 
 
237
 
 
238
 
 
239
class context:
 
240
 
 
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
 
250
 
 
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