render XML directly as string for better performance and more cleanup

This commit is contained in:
Laura Klünder 2017-10-19 23:50:21 +02:00
parent 88f7f232b7
commit 1ae60dba63
2 changed files with 57 additions and 81 deletions

View file

@ -141,7 +141,7 @@ class Level(SpecificLocation, models.Model):
door_geometries = cascaded_union(tuple(d.geometry for d in doors)) door_geometries = cascaded_union(tuple(d.geometry for d in doors))
level_geometry = cascaded_union((space_geometries, building_geometries, door_geometries)) level_geometry = cascaded_union((space_geometries, building_geometries, door_geometries))
level_geometry = level_geometry.difference(hole_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) svg.add_geometry(fill_color='#ececec', clip_path=level_clip)
# color in spaces # color in spaces

View file

@ -2,7 +2,6 @@ import io
import math import math
import re import re
import subprocess import subprocess
import xml.etree.ElementTree as ET
from django.conf import settings from django.conf import settings
from django.core.checks import Error, register 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) # 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)) self.buffer_px = int(math.ceil(buffer*self.scale))
# create base elements and counter for dynamic definition ids # create base elements and counter for clip path ids
self.g = ET.Element('g', {}) self.g = ''
self.defs = ET.Element('defs') self.defs = ''
self.def_i = 0 self.clip_path_i = 0
# keep track which area of the image has which altitude currently # keep track which area of the image has which altitude currently
self.altitudes = {} self.altitudes = {}
@ -63,28 +62,22 @@ class SVGImage:
height_px = self.height * self.scale + (self.buffer_px * 2 if buffer else 0) height_px = self.height * self.scale + (self.buffer_px * 2 if buffer else 0)
return height_px, width_px return height_px, width_px
def get_element(self, buffer=False): def get_xml(self, buffer=False):
# get the root <svg> element as an ElementTree element, with or without buffer # get the root <svg> 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)) 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' 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): attribs = ' viewBox="'+' '.join((offset_px, offset_px, width_px, height_px))+'"' if buffer else ''
# get xml of the svg as a string
return ET.tostring(self.get_element(buffer=buffer)).decode() result = ('<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"'
' xmlns:xlink="http://www.w3.org/1999/xlink" width="'+width_px+'" height="'+height_px+'"' +
attribs+'>')
if self.defs:
result += '<defs>'+self.defs+'</defs>'
if self.g:
result += '<g>'+self.g+'</g>'
result += '</svg>'
return result
def get_png(self, f=None): def get_png(self, f=None):
# render the image to png. returns bytes if f is None, otherwise it calls f.write() # render the image to png. returns bytes if f is None, otherwise it calls f.write()
@ -131,16 +124,11 @@ class SVGImage:
return png return png
f.write(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): def _trim_decimals(self, data):
# remove trailing zeros from a decimal # remove trailing zeros from a decimal
return re.sub(r'([0-9]+)((\.[1-9])[0-9]+|\.[0-9]+)?', r'\1\3', data) 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 # convert a shapely geometry into an svg xml element
# scale and move the object into position, this is equivalent to: # 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)) -(self.left)*self.scale, (self.top)*self.scale))
element = self._trim_decimals(re.sub(r' (opacity|fill|fill-rule|stroke|stroke-width)="[^"]*"', '', element = self._trim_decimals(re.sub(r' (opacity|fill|fill-rule|stroke|stroke-width)="[^"]*"', '',
geometry.svg(0, '#FFFFFF'))) geometry.svg(0, '#FFFFFF')))
if not element.startswith('<g '): if not element.startswith('<g'):
element = '<g>'+element+'</g>' element = '<g'+attribs+'>'+element+'</g>'
element = ET.fromstring(element) elif attribs:
element = element[:2]+attribs+element[2:]
return element return element
def register_geometry(self, geometry, defid=None, as_clip_path=False, comment=None): def register_clip_path(self, geometry):
if defid is None: defid = str(self.clip_path_i)
defid = self.new_defid() self.defs += '<clipPath'+self._create_geometry(geometry, ' id="clip'+defid+'"')[2:-2]+'clipPath>'
self.clip_path_i += 1
element = self._create_geometry(geometry)
if as_clip_path:
element.tag = 'clipPath'
element.set('id', defid)
self.defs.append(element)
return defid return defid
def add_shadow(self, geometry, elevation, clip_path=None): def add_shadow(self, geometry, elevation, clip_path=None):
@ -183,25 +166,16 @@ class SVGImage:
blur_id = 'blur'+str(int(elevation*100)) blur_id = 'blur'+str(int(elevation*100))
if elevation not in self.blurs: if elevation not in self.blurs:
blur_filter = ET.Element('filter', {'id': blur_id, self.defs += ('<filter id="'+blur_id+'" width="200%" height="200%" x="-50%" y="-50%">'
'width': '200%', '<feGaussianBlur stdDeviation="'+str(blur_radius * self.scale)+'"/>'
'height': '200%', '</filter>')
'x': '-50%',
'y': '-50%'})
blur_filter.append(ET.Element('feGaussianBlur',
{'stdDeviation': str(blur_radius * self.scale)}))
self.defs.append(blur_filter)
self.blurs.add(elevation) self.blurs.add(elevation)
shadow = self._create_geometry(shadow_geom) attribs = ' filter="url(#'+blur_id+')" fill="#000" fill-opacity="0.2"'
shadow.set('filter', 'url(#'+blur_id+')')
shadow.set('fill', '#000')
shadow.set('fill-opacity', '0.2')
if clip_path: if clip_path:
shadow_clip = self.register_geometry(clip_path, as_clip_path=True) attribs += ' clip-path="url(#'+self.register_clip_path(clip_path)+'"'
shadow.set('clip-path', 'url(#'+shadow_clip+')') shadow = self._create_geometry(shadow_geom, attribs)
self.g.append(shadow) self.g += shadow
def clip_altitudes(self, new_geometry, new_altitude=None): def clip_altitudes(self, new_geometry, new_altitude=None):
# registrer new geometry with specific (or no) altitude # registrer new geometry with specific (or no) altitude
@ -226,6 +200,27 @@ class SVGImage:
# draw a shapely geometry with a given style # 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 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 # 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 geometry is not None:
if not geometry: if not geometry:
return return
@ -241,29 +236,10 @@ class SVGImage:
self.clip_altitudes(geometry, altitude) self.clip_altitudes(geometry, altitude)
element = self._create_geometry(geometry) element = self._create_geometry(geometry, attribs)
else: else:
element = ET.Element('rect', {'width': '100%', 'height': '100%'}) element = '<rect width="100%" height="100%"'+attribs+'>'
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+')')
self.g.append(element) self.g += element
return element return element