Files

141 lines
4.8 KiB
Python

"""Materialize the supported inline SVG geometry-property subset.
SVG 2 lets selected geometry values participate in CSS. PPT Master does not
run a CSS engine, but it can safely compile literal per-element ``style``
declarations into the equivalent XML geometry attributes before any existing
SVG post-processing or DrawingML conversion runs.
"""
from __future__ import annotations
import copy
import math
import re
from pathlib import Path
from xml.etree import ElementTree as ET
SVG_NS = 'http://www.w3.org/2000/svg'
XLINK_NS = 'http://www.w3.org/1999/xlink'
INLINE_GEOMETRY_PROPERTIES = {
'rect': frozenset({'x', 'y', 'width', 'height', 'rx', 'ry'}),
'circle': frozenset({'cx', 'cy', 'r'}),
'ellipse': frozenset({'cx', 'cy', 'rx', 'ry'}),
'image': frozenset({'x', 'y', 'width', 'height'}),
'svg': frozenset({'x', 'y', 'width', 'height'}),
'use': frozenset({'x', 'y', 'width', 'height'}),
}
_GEOMETRY_LIKE_PROPERTIES = frozenset({
'x', 'y', 'width', 'height', 'rx', 'ry', 'cx', 'cy', 'r',
'x1', 'y1', 'x2', 'y2', 'dx', 'dy', 'points', 'd',
})
_NON_NEGATIVE_PROPERTIES = frozenset({'width', 'height', 'rx', 'ry', 'r'})
_PX_LENGTH_RE = re.compile(
r'^\s*([-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?)\s*(px)?\s*$',
re.IGNORECASE,
)
class GeometryStyleError(ValueError):
"""Reject inline geometry that cannot be compiled deterministically."""
def _local_tag(elem: ET.Element) -> str:
tag = str(elem.tag)
return tag.rsplit('}', 1)[-1] if '}' in tag else tag
def _format_number(value: float) -> str:
if abs(value) < 1e-12:
value = 0.0
return f'{value:.12g}'
def _normalize_px_length(raw: str, tag: str, prop: str) -> str:
"""Return one finite CSS px literal as a unitless SVG user value."""
match = _PX_LENGTH_RE.fullmatch(raw)
if match is None:
raise GeometryStyleError(
f'<{tag}> inline geometry {prop} requires a finite px literal; '
f'got {raw!r}'
)
value = float(match.group(1))
if not math.isfinite(value):
raise GeometryStyleError(
f'<{tag}> inline geometry {prop} must be finite; got {raw!r}'
)
if match.group(2) is None and value != 0:
raise GeometryStyleError(
f'<{tag}> inline geometry {prop} requires px for non-zero values; '
f'got {raw!r}'
)
if prop in _NON_NEGATIVE_PROPERTIES and value < 0:
raise GeometryStyleError(
f'<{tag}> inline geometry {prop} cannot be negative; got {raw!r}'
)
return _format_number(value)
def materialize_inline_geometry_properties(root: ET.Element) -> int:
"""Compile supported inline geometry declarations into XML attributes."""
materialized = 0
for elem in root.iter():
style = elem.get('style')
if not style:
continue
tag = _local_tag(elem)
supported = INLINE_GEOMETRY_PROPERTIES.get(tag, frozenset())
retained: list[str] = []
element_materialized = 0
for raw_declaration in style.split(';'):
declaration = raw_declaration.strip()
if not declaration:
continue
if ':' not in declaration:
retained.append(declaration)
continue
raw_name, raw_value = declaration.split(':', 1)
name = raw_name.strip().lower()
value = raw_value.strip()
if name not in _GEOMETRY_LIKE_PROPERTIES:
retained.append(declaration)
continue
if name not in supported:
raise GeometryStyleError(
f'<{tag}> does not support inline geometry property {name!r}; '
'use the element\'s XML geometry attribute instead'
)
elem.set(name, _normalize_px_length(value, tag, name))
materialized += 1
element_materialized += 1
if element_materialized == 0:
continue
if retained:
elem.set('style', '; '.join(retained))
else:
elem.attrib.pop('style', None)
return materialized
def validate_inline_geometry_properties(root: ET.Element) -> list[str]:
"""Return inline geometry errors without mutating the caller's SVG tree."""
try:
materialize_inline_geometry_properties(copy.deepcopy(root))
except GeometryStyleError as exc:
return [str(exc)]
return []
def materialize_inline_geometry_in_file(svg_path: Path) -> int:
"""Materialize inline geometry in one SVG file in place."""
tree = ET.parse(str(svg_path))
count = materialize_inline_geometry_properties(tree.getroot())
if count:
ET.register_namespace('', SVG_NS)
ET.register_namespace('xlink', XLINK_NS)
tree.write(str(svg_path), encoding='unicode', xml_declaration=False)
return count