Files
altium-render-engine/backend/app/render.py
T

57 خطوط
3.6 KiB
Python

"""Safe SVG serializer: only application-owned SVG tags and attributes are emitted."""
import re
from xml.etree.ElementTree import Element, SubElement, tostring
from .models.schematic import Document
# XML 1.0 cannot represent these controls. They never become markup or CSS.
CONTROL = re.compile('[\x00-\x08\x0b\x0c\x0e-\x1f\ud800-\udfff]')
def clean(value): return CONTROL.sub('', str(value))
def node(parent, tag, **attrs):
return SubElement(parent, tag, {k.replace('_', '-'): clean(v) for k, v in attrs.items()})
def svg_document(doc: Document, annotations=None, id_prefix="") -> bytes:
b = doc.sheet.bounds
root = Element('svg', {'xmlns': 'http://www.w3.org/2000/svg', 'viewBox': f'{b.x} {b.y} {b.width} {b.height}',
'preserveAspectRatio': 'xMidYMid meet', 'role': 'img', 'aria-label': 'Schematic'})
node(root, 'rect', x=b.x, y=b.y, width=b.width, height=b.height, fill=doc.sheet.background)
defs = node(root, 'defs')
for i, p in enumerate(doc.primitives):
target = root
if annotations is not None:
annotation = annotations.get(p.component_id or p.object_id)
target = node(root, 'g', data_object_id=p.component_id or p.object_id, data_change_id=annotation['id'] if annotation else '')
if p.clip:
clip = node(defs, 'clipPath', id=f'{id_prefix}clip-{i}', clipPathUnits='userSpaceOnUse')
c = p.clip
node(clip, 'rect', x=c.x, y=c.y, width=c.width, height=c.height)
target = node(target, 'g', clip_path=f'url(#{id_prefix}clip-{i})')
if p.kind == 'path':
n = node(target, 'path', d=p.path, stroke=p.stroke, fill=p.fill,
stroke_width=p.width, stroke_linecap='round', stroke_linejoin='round', fill_rule=p.fill_rule)
if p.dash: n.set('stroke-dasharray', ' '.join(map(str, p.dash)))
elif p.kind == 'image':
# Populated only by the trusted bounded raster decoder; never a source URL.
if p.image_data.startswith('data:image/png;base64,'):
node(target, 'image', x=p.bounds.x, y=p.bounds.y, width=p.bounds.width, height=p.bounds.height,
href=p.image_data, preserveAspectRatio='xMidYMid meet' if p.keep_aspect else 'none')
else:
transform = f'translate({p.position.x} {p.position.y}) rotate({p.rotation})'
if p.mirrored: transform += ' scale(-1 1)'
n = node(target, 'text', transform=transform, fill=p.stroke, font_family=p.font_family,
font_size=p.font_size, font_weight='bold' if p.bold else 'normal',
font_style='italic' if p.italic else 'normal', text_anchor=p.anchor,
dominant_baseline=p.baseline, text_decoration='underline' if p.underline else 'none')
n.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve')
n.text = clean(p.text)
if annotations is not None:
colors = {'added':'#16854b','removed':'#cf3545','modified':'#b87b09','moved':'#2676c5'}
overlay = node(root, 'g', pointer_events='none')
for annotation in annotations.values():
b = annotation.get('bounds')
if not b: continue
color = colors[annotation['status']]
node(overlay, 'rect', x=b['x']-2, y=b['y']-2, width=max(4,b['width']+4), height=max(4,b['height']+4),
fill=color, fill_opacity='.09', stroke=color, stroke_width='1.5', vector_effect='non-scaling-stroke',
data_change_id=annotation['id'], data_diff_box='true')
return tostring(root, encoding='utf-8', xml_declaration=True)