65 خطوط
4.1 KiB
Python
65 خطوط
4.1 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
|
|
pcb = doc.document_type == 'pcb'
|
|
layer_visibility = {layer.id: layer.visible for layer in doc.layers}
|
|
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': 'PCB' if pcb else '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 pcb:
|
|
span = getattr(p, "layer_ids", [])
|
|
visible = layer_visibility.get(p.layer_id, True) and (p.role != "drill" or not span or any(layer_visibility.get(lid, False) for lid in span))
|
|
target = node(target, 'g', data_layer_id=p.layer_id or '',
|
|
data_layer_span=' '.join(getattr(p, 'layer_ids', [])), data_role=p.role,
|
|
display='inline' if visible else 'none')
|
|
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)
|