"""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) -> 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 p.clip: clip = node(defs, 'clipPath', id=f'clip-{i}', clipPathUnits='userSpaceOnUse') c = p.clip node(clip, 'rect', x=c.x, y=c.y, width=c.width, height=c.height) target = node(root, 'g', clip_path=f'url(#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) return tostring(root, encoding='utf-8', xml_declaration=True)