From 1ae60dba63b851aee547d371f7638a808691e977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Thu, 19 Oct 2017 23:50:21 +0200 Subject: [PATCH] render XML directly as string for better performance and more cleanup --- src/c3nav/mapdata/models/level.py | 2 +- src/c3nav/mapdata/utils/svg.py | 136 ++++++++++++------------------ 2 files changed, 57 insertions(+), 81 deletions(-) diff --git a/src/c3nav/mapdata/models/level.py b/src/c3nav/mapdata/models/level.py index d18797c0..dc9d2065 100644 --- a/src/c3nav/mapdata/models/level.py +++ b/src/c3nav/mapdata/models/level.py @@ -141,7 +141,7 @@ class Level(SpecificLocation, models.Model): door_geometries = cascaded_union(tuple(d.geometry for d in doors)) level_geometry = cascaded_union((space_geometries, building_geometries, door_geometries)) level_geometry = level_geometry.difference(hole_geometries) - level_clip = svg.register_geometry(level_geometry, defid='level', as_clip_path=True) + level_clip = svg.register_clip_path(level_geometry, defid='level', as_clip_path=True) svg.add_geometry(fill_color='#ececec', clip_path=level_clip) # color in spaces diff --git a/src/c3nav/mapdata/utils/svg.py b/src/c3nav/mapdata/utils/svg.py index 8f7ca574..20379dfb 100644 --- a/src/c3nav/mapdata/utils/svg.py +++ b/src/c3nav/mapdata/utils/svg.py @@ -2,7 +2,6 @@ import io import math import re import subprocess -import xml.etree.ElementTree as ET from django.conf import settings from django.core.checks import Error, register @@ -45,10 +44,10 @@ class SVGImage: # how many pixels around the image should be added and later cropped (otherwise rsvg does not blur correctly) self.buffer_px = int(math.ceil(buffer*self.scale)) - # create base elements and counter for dynamic definition ids - self.g = ET.Element('g', {}) - self.defs = ET.Element('defs') - self.def_i = 0 + # create base elements and counter for clip path ids + self.g = '' + self.defs = '' + self.clip_path_i = 0 # keep track which area of the image has which altitude currently self.altitudes = {} @@ -63,28 +62,22 @@ class SVGImage: height_px = self.height * self.scale + (self.buffer_px * 2 if buffer else 0) return height_px, width_px - def get_element(self, buffer=False): + def get_xml(self, buffer=False): # get the root element as an ElementTree element, with or without buffer height_px, width_px = (self._trim_decimals(str(i)) for i in self.get_dimensions_px(buffer)) offset_px = self._trim_decimals(str(-self.buffer_px)) if buffer else '0' - root = ET.Element('svg', { - 'width': width_px, - 'height': height_px, - 'xmlns:svg': 'http://www.w3.org/2000/svg', - 'xmlns': 'http://www.w3.org/2000/svg', - 'xmlns:xlink': 'http://www.w3.org/1999/xlink', - }) - if buffer: - root.attrib['viewBox'] = ' '.join((offset_px, offset_px, width_px, height_px)) - if len(self.defs): - root.append(self.defs) - if len(self.g): - root.append(self.g) - return root - def get_xml(self, buffer=False): - # get xml of the svg as a string - return ET.tostring(self.get_element(buffer=buffer)).decode() + attribs = ' viewBox="'+' '.join((offset_px, offset_px, width_px, height_px))+'"' if buffer else '' + + result = ('') + if self.defs: + result += ''+self.defs+'' + if self.g: + result += ''+self.g+'' + result += '' + return result def get_png(self, f=None): # render the image to png. returns bytes if f is None, otherwise it calls f.write() @@ -131,16 +124,11 @@ class SVGImage: return png f.write(png) - def new_defid(self): - defid = 's'+str(self.def_i) - self.def_i += 1 - return defid - def _trim_decimals(self, data): # remove trailing zeros from a decimal return re.sub(r'([0-9]+)((\.[1-9])[0-9]+|\.[0-9]+)?', r'\1\3', data) - def _create_geometry(self, geometry): + def _create_geometry(self, geometry, attribs=''): # convert a shapely geometry into an svg xml element # scale and move the object into position, this is equivalent to: @@ -152,21 +140,16 @@ class SVGImage: -(self.left)*self.scale, (self.top)*self.scale)) element = self._trim_decimals(re.sub(r' (opacity|fill|fill-rule|stroke|stroke-width)="[^"]*"', '', geometry.svg(0, '#FFFFFF'))) - if not element.startswith('' - element = ET.fromstring(element) + if not element.startswith(''+element+'' + elif attribs: + element = element[:2]+attribs+element[2:] return element - def register_geometry(self, geometry, defid=None, as_clip_path=False, comment=None): - if defid is None: - defid = self.new_defid() - - element = self._create_geometry(geometry) - - if as_clip_path: - element.tag = 'clipPath' - element.set('id', defid) - self.defs.append(element) + def register_clip_path(self, geometry): + defid = str(self.clip_path_i) + self.defs += '' + self.clip_path_i += 1 return defid def add_shadow(self, geometry, elevation, clip_path=None): @@ -183,25 +166,16 @@ class SVGImage: blur_id = 'blur'+str(int(elevation*100)) if elevation not in self.blurs: - blur_filter = ET.Element('filter', {'id': blur_id, - 'width': '200%', - 'height': '200%', - 'x': '-50%', - 'y': '-50%'}) - blur_filter.append(ET.Element('feGaussianBlur', - {'stdDeviation': str(blur_radius * self.scale)})) - - self.defs.append(blur_filter) + self.defs += ('' + '' + '') self.blurs.add(elevation) - shadow = self._create_geometry(shadow_geom) - shadow.set('filter', 'url(#'+blur_id+')') - shadow.set('fill', '#000') - shadow.set('fill-opacity', '0.2') + attribs = ' filter="url(#'+blur_id+')" fill="#000" fill-opacity="0.2"' if clip_path: - shadow_clip = self.register_geometry(clip_path, as_clip_path=True) - shadow.set('clip-path', 'url(#'+shadow_clip+')') - self.g.append(shadow) + attribs += ' clip-path="url(#'+self.register_clip_path(clip_path)+'"' + shadow = self._create_geometry(shadow_geom, attribs) + self.g += shadow def clip_altitudes(self, new_geometry, new_altitude=None): # registrer new geometry with specific (or no) altitude @@ -226,6 +200,27 @@ class SVGImage: # draw a shapely geometry with a given style # if altitude is set, the geometry will get a calculated shadow relative to the other geometries # if elevation is set, the geometry will get a shadow with exactly this elevation + + attribs = ' fill="'+(fill_color or 'none')+'"' + if fill_opacity: + attribs += ' fill-opacity="'+str(fill_opacity)[:4]+'"' + if stroke_px: + attribs += ' stroke-width="'+self._trim_decimals(str(stroke_px))+'"' + elif stroke_width: + attribs += ' stroke-width="'+self._trim_decimals(str(stroke_width * self.scale))+'"' + if stroke_color: + attribs += ' stroke="'+stroke_color+'"' + if stroke_opacity: + attribs += ' stroke-opacity="'+str(stroke_opacity)[:4]+'"' + if stroke_linejoin: + attribs += ' stroke-linejoin="'+stroke_linejoin+'"' + if opacity: + attribs += ' opacity="'+str(opacity)[:4]+'"' + if filter: + attribs += ' filter="url(#'+filter+')"' + if clip_path: + attribs += ' clip-path="url(#'+clip_path+')"' + if geometry is not None: if not geometry: return @@ -241,29 +236,10 @@ class SVGImage: self.clip_altitudes(geometry, altitude) - element = self._create_geometry(geometry) + element = self._create_geometry(geometry, attribs) else: - element = ET.Element('rect', {'width': '100%', 'height': '100%'}) - element.set('fill', fill_color or 'none') - if fill_opacity: - element.set('fill-opacity', str(fill_opacity)[:4]) - if stroke_px: - element.set('stroke-width', self._trim_decimals(str(stroke_px))) - elif stroke_width: - element.set('stroke-width', self._trim_decimals(str(stroke_width * self.scale))) - if stroke_color: - element.set('stroke', stroke_color) - if stroke_opacity: - element.set('stroke-opacity', str(stroke_opacity)[:4]) - if stroke_linejoin: - element.set('stroke-linejoin', stroke_linejoin) - if opacity: - element.set('opacity', str(opacity)[:4]) - if filter: - element.set('filter', 'url(#'+filter+')') - if clip_path: - element.set('clip-path', 'url(#'+clip_path+')') + element = '' - self.g.append(element) + self.g += element return element