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