clone

a browser-first diagram editor with live svg previews, local drafts and encrypted, accountless sharing

main
diagramzip / renderers / python / blockdiag-family / src / blockdiag / imagedraw / svg.py
svg.py 10.1K
1# -*- coding: utf-8 -*-2# Copyright 2011 Takeshi KOMIYA3#4# Licensed under the Apache License, Version 2.0 (the "License");5# you may not use this file except in compliance with the License.6# You may obtain a copy of the License at7#8# http://www.apache.org/licenses/LICENSE-2.09#10# Unless required by applicable law or agreed to in writing, software11# distributed under the License is distributed on an "AS IS" BASIS,12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13# See the License for the specific language governing permissions and14# limitations under the License.1516import os17import re18from base64 import b64encode1920from PIL.Image import Image2122from blockdiag.imagedraw import base as _base23from blockdiag.imagedraw.simplesvg import (a, defs, desc, ellipse, filter, g,24 image, path, pathdata, polygon,25 rect, svg, svgclass, text, title)26from blockdiag.imagedraw.utils import memoize27from blockdiag.imagedraw.utils.ellipse import endpoints as ellipse_endpoints28from blockdiag.utils import XY, Box, images, is_Pillow_available2930feGaussianBlur = svgclass('feGaussianBlur')313233def rgb(color):34 if isinstance(color, tuple):35 color = 'rgb(%d,%d,%d)' % color3637 return color383940def style(name):41 if name == 'blur':42 value = "filter:url(#filter_blur)"43 elif name == 'transp-blur':44 value = "filter:url(#filter_blur);opacity:0.7;fill-opacity:1"45 else:46 value = None4748 return value495051def dasharray(pattern, thick):52 if thick is None:53 thick = 15455 if pattern == 'dotted':56 value = 2 * thick57 elif pattern == 'dashed':58 value = 4 * thick59 elif pattern == 'none':60 value = "%d %d" % (0, 65535 * thick)61 elif re.search(r'^\d+(,\d+)*$', pattern or ""):62 lengths = [int(n) * thick for n in pattern.split(",")]63 value = " ".join(str(n) for n in lengths)64 else:65 value = None6667 return value686970def drawing_params(kwargs):71 params = {}7273 if 'style' in kwargs:74 params['stroke_dasharray'] = dasharray(kwargs.get('style'),75 kwargs.get('thick'))7677 if 'filter' in kwargs:78 params['style'] = style(kwargs.get('filter'))7980 return params818283class SVGImageDrawElement(_base.ImageDraw):84 self_generative_methods = ['group', 'anchor']85 supported_path = True86 baseline_text_rendering = True8788 def __init__(self, svg, parent=None):89 self.svg = svg9091 def path(self, pd, **kwargs):92 fill = kwargs.get('fill')93 outline = kwargs.get('outline')9495 p = path(pd, fill=rgb(fill), stroke=rgb(outline),96 **drawing_params(kwargs))97 self.svg.addElement(p)9899 def rectangle(self, box, **kwargs):100 thick = kwargs.get('thick')101 fill = kwargs.get('fill', 'none')102 outline = kwargs.get('outline')103104 r = rect(box.x, box.y, box.width, box.height,105 fill=rgb(fill), stroke=rgb(outline),106 stroke_width=thick, **drawing_params(kwargs))107 self.svg.addElement(r)108109 @memoize110 def textlinesize(self, string, font, **kwargs):111 if is_Pillow_available():112 if not hasattr(self, '_pil_drawer'):113 from blockdiag.imagedraw import png114 self._pil_drawer = png.ImageDrawEx(None)115116 return self._pil_drawer.textlinesize(string, font)117 else:118 from blockdiag.imagedraw.utils import textsize119 return textsize(string, font)120121 def text(self, point, string, font, **kwargs):122 fill = kwargs.get('fill')123124 size = self.textlinesize(string, font)125 point = point.shift(size.width / 2)126 t = text(point.x, point.y, string, fill=rgb(fill),127 font_family=font.generic_family, font_size=font.size,128 font_weight=font.weight, font_style=font.style,129 text_anchor='middle', textLength=size.width)130 self.svg.addElement(t)131132 def textarea(self, box, string, font, **kwargs):133 if 'rotate' in kwargs and kwargs['rotate'] != 0:134 self.rotated_textarea(box, string, font, **kwargs)135 else:136 lines = self.textfolder(box, string, font, **kwargs)137138 if kwargs.get('outline'):139 outline = kwargs.get('outline')140 self.rectangle(lines.outlinebox, fill='white', outline=outline)141142 rendered = False143 for string, point in lines.lines:144 self.text(point, string, font, **kwargs)145 rendered = True146147 if not rendered and font.size > 0:148 _font = font.duplicate()149 _font.size = int(font.size * 0.8)150 self.textarea(box, string, _font, **kwargs)151152 def rotated_textarea(self, box, string, font, **kwargs):153 angle = int(kwargs['rotate']) % 360154 del kwargs['rotate']155156 if angle in (90, 270):157 _box = Box(box[0], box[1],158 box[0] + box.height, box[1] + box.width)159 if angle == 90:160 _box = _box.shift(x=box.width)161 elif angle == 270:162 _box = _box.shift(y=box.height)163 elif angle == 180:164 _box = Box(box[2], box[3],165 box[2] + box.width, box[3] + box.height)166 else:167 _box = Box(box[0], box[1],168 box[0] + box.width, box[1] + box.height)169170 rotate = "rotate(%d,%d,%d)" % (angle, _box[0], _box[1])171 group = g(transform="%s" % rotate)172 self.svg.addElement(group)173174 elem = SVGImageDrawElement(group, self)175 elem.textarea(_box, string, font, **kwargs)176177 def line(self, points, **kwargs):178 fill = kwargs.get('fill')179 thick = kwargs.get('thick')180181 pd = pathdata(points[0].x, points[0].y)182 for pt in points[1:]:183 pd.line(pt.x, pt.y)184185 p = path(pd, fill="none", stroke=rgb(fill),186 stroke_width=thick, **drawing_params(kwargs))187 self.svg.addElement(p)188189 def arc(self, box, start, end, **kwargs):190 fill = kwargs.get('fill')191192 w = box.width / 2193 h = box.height / 2194195 if start > end:196 end += 360197198 endpoints = ellipse_endpoints(1, w, h, start, end)199 pt1 = XY(box.x + w + round(endpoints[0].x, 0),200 box.y + h + round(endpoints[0].y, 0))201 pt2 = XY(box.x + w + round(endpoints[1].x, 0),202 box.y + h + round(endpoints[1].y, 0))203204 if end - start > 180:205 largearc = 1206 else:207 largearc = 0208209 pd = pathdata(pt1[0], pt1[1])210 pd.ellarc(w, h, 0, largearc, 1, pt2[0], pt2[1])211 p = path(pd, fill="none", stroke=rgb(fill),212 **drawing_params(kwargs))213 self.svg.addElement(p)214215 def ellipse(self, box, **kwargs):216 fill = kwargs.get('fill')217 outline = kwargs.get('outline')218219 w = box.width / 2220 h = box.height / 2221 pt = box.center222223 e = ellipse(pt.x, pt.y, w, h, fill=rgb(fill),224 stroke=rgb(outline), **drawing_params(kwargs))225 self.svg.addElement(e)226227 def polygon(self, points, **kwargs):228 fill = kwargs.get('fill')229 outline = kwargs.get('outline')230231 pg = polygon(points, fill=rgb(fill), stroke=rgb(outline),232 **drawing_params(kwargs))233 self.svg.addElement(pg)234235 def image(self, box, url):236 if hasattr(url, 'read'):237 url = "data:;base64," + b64encode(url.read()).decode('utf-8')238 else:239 if isinstance(url, Image):240 ext = None241 else:242 ext = os.path.splitext(url)[1].lower()243244 if ext not in ('.jpg', '.png', '.gif'):245 stream = None246 try:247 stream = images.open(url, mode='png')248 url = "data:;base64," + b64encode(stream.read()).decode('utf-8')249 except IOError:250 url = None251 finally:252 if stream:253 stream.close()254255 im = image(url, box.x1, box.y1, box.width, box.height)256 self.svg.addElement(im)257258 def anchor(self, url):259 a_node = a(url)260 a_node.add_attribute('xlink:href', url)261 self.svg.addElement(a_node)262263 return SVGImageDrawElement(a_node, self)264265 def group(self):266 group = g()267 self.svg.addElement(group)268269 return SVGImageDrawElement(group, self)270271272class SVGImageDraw(SVGImageDrawElement):273 def __init__(self, filename, **kwargs):274 super(SVGImageDraw, self).__init__(None)275276 self.filename = filename277 self.options = kwargs278 self.set_canvas_size((0, 0))279280 def set_canvas_size(self, size):281 self.svg = svg(0, 0, size[0], size[1], **self.options)282 uri = 'http://www.inkscape.org/namespaces/inkscape'283 self.svg.add_attribute('xmlns:inkspace', uri)284 uri = 'http://www.w3.org/1999/xlink'285 self.svg.add_attribute('xmlns:xlink', uri)286287 # inkspace's Gaussian filter288 if self.options.get('style') != 'blur':289 fgb = feGaussianBlur(id='feGaussianBlur3780', stdDeviation=4.2)290 fgb.add_attribute('inkspace:collect', 'always')291 f = filter(-0.07875, -0.252, 1.1575, 1.504, id='filter_blur')292 f.add_attribute('inkspace:collect', 'always')293 f.addElement(fgb)294 d = defs(id='defs_block')295 d.addElement(f)296 self.svg.addElement(d)297298 self.svg.addElement(title('blockdiag'))299 self.svg.addElement(desc(self.options.get('code')))300301 def save(self, filename, size, _format):302 # Ignore format parameter; compatibility for ImageDrawEx.303304 if filename:305 self.filename = filename306307 if size:308 self.svg.attributes['width'] = size[0]309 self.svg.attributes['height'] = size[1]310311 image = self.svg.to_xml()312313 if self.filename:314 open(self.filename, 'wb').write(image.encode('utf-8'))315316 return image317318319def setup(self):320 from blockdiag.imagedraw import install_imagedrawer321 install_imagedrawer('svg', SVGImageDraw)