render XML directly as string for better performance and more cleanup
This commit is contained in:
parent
88f7f232b7
commit
1ae60dba63
2 changed files with 57 additions and 81 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue