返回 ppt-master
geometry_properties.py
根目录 / skills / ppt-master / scripts / svg_to_pptx / geometry_properties.py
1 """Materialize the supported inline SVG geometry-property subset.
2
3 SVG 2 lets selected geometry values participate in CSS. PPT Master does not
4 run a CSS engine, but it can safely compile literal per-element ``style``
5 declarations into the equivalent XML geometry attributes before any existing
6 SVG post-processing or DrawingML conversion runs.
7 """
8
9 from __future__ import annotations
10
11 import copy
12 import math
13 import re
14 from pathlib import Path
15 from xml.etree import ElementTree as ET
16
17
18 SVG_NS = 'http://www.w3.org/2000/svg'
19 XLINK_NS = 'http://www.w3.org/1999/xlink'
20
21 INLINE_GEOMETRY_PROPERTIES = {
22 'rect': frozenset({'x', 'y', 'width', 'height', 'rx', 'ry'}),
23 'circle': frozenset({'cx', 'cy', 'r'}),
24 'ellipse': frozenset({'cx', 'cy', 'rx', 'ry'}),
25 'image': frozenset({'x', 'y', 'width', 'height'}),
26 'svg': frozenset({'x', 'y', 'width', 'height'}),
27 'use': frozenset({'x', 'y', 'width', 'height'}),
28 }
29
30 _GEOMETRY_LIKE_PROPERTIES = frozenset({
31 'x', 'y', 'width', 'height', 'rx', 'ry', 'cx', 'cy', 'r',
32 'x1', 'y1', 'x2', 'y2', 'dx', 'dy', 'points', 'd',
33 })
34 _NON_NEGATIVE_PROPERTIES = frozenset({'width', 'height', 'rx', 'ry', 'r'})
35 _PX_LENGTH_RE = re.compile(
36 r'^\s*([-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?)\s*(px)?\s*$',
37 re.IGNORECASE,
38 )
39
40
41 class GeometryStyleError(ValueError):
42 """Reject inline geometry that cannot be compiled deterministically."""
43
44
45 def _local_tag(elem: ET.Element) -> str:
46 tag = str(elem.tag)
47 return tag.rsplit('}', 1)[-1] if '}' in tag else tag
48
49
50 def _format_number(value: float) -> str:
51 if abs(value) < 1e-12:
52 value = 0.0
53 return f'{value:.12g}'
54
55
56 def _normalize_px_length(raw: str, tag: str, prop: str) -> str:
57 """Return one finite CSS px literal as a unitless SVG user value."""
58 match = _PX_LENGTH_RE.fullmatch(raw)
59 if match is None:
60 raise GeometryStyleError(
61 f'<{tag}> inline geometry {prop} requires a finite px literal; '
62 f'got {raw!r}'
63 )
64 value = float(match.group(1))
65 if not math.isfinite(value):
66 raise GeometryStyleError(
67 f'<{tag}> inline geometry {prop} must be finite; got {raw!r}'
68 )
69 if match.group(2) is None and value != 0:
70 raise GeometryStyleError(
71 f'<{tag}> inline geometry {prop} requires px for non-zero values; '
72 f'got {raw!r}'
73 )
74 if prop in _NON_NEGATIVE_PROPERTIES and value < 0:
75 raise GeometryStyleError(
76 f'<{tag}> inline geometry {prop} cannot be negative; got {raw!r}'
77 )
78 return _format_number(value)
79
80
81 def materialize_inline_geometry_properties(root: ET.Element) -> int:
82 """Compile supported inline geometry declarations into XML attributes."""
83 materialized = 0
84 for elem in root.iter():
85 style = elem.get('style')
86 if not style:
87 continue
88 tag = _local_tag(elem)
89 supported = INLINE_GEOMETRY_PROPERTIES.get(tag, frozenset())
90 retained: list[str] = []
91 element_materialized = 0
92 for raw_declaration in style.split(';'):
93 declaration = raw_declaration.strip()
94 if not declaration:
95 continue
96 if ':' not in declaration:
97 retained.append(declaration)
98 continue
99 raw_name, raw_value = declaration.split(':', 1)
100 name = raw_name.strip().lower()
101 value = raw_value.strip()
102 if name not in _GEOMETRY_LIKE_PROPERTIES:
103 retained.append(declaration)
104 continue
105 if name not in supported:
106 raise GeometryStyleError(
107 f'<{tag}> does not support inline geometry property {name!r}; '
108 'use the element\'s XML geometry attribute instead'
109 )
110 elem.set(name, _normalize_px_length(value, tag, name))
111 materialized += 1
112 element_materialized += 1
113
114 if element_materialized == 0:
115 continue
116 if retained:
117 elem.set('style', '; '.join(retained))
118 else:
119 elem.attrib.pop('style', None)
120 return materialized
121
122
123 def validate_inline_geometry_properties(root: ET.Element) -> list[str]:
124 """Return inline geometry errors without mutating the caller's SVG tree."""
125 try:
126 materialize_inline_geometry_properties(copy.deepcopy(root))
127 except GeometryStyleError as exc:
128 return [str(exc)]
129 return []
130
131
132 def materialize_inline_geometry_in_file(svg_path: Path) -> int:
133 """Materialize inline geometry in one SVG file in place."""
134 tree = ET.parse(str(svg_path))
135 count = materialize_inline_geometry_properties(tree.getroot())
136 if count:
137 ET.register_namespace('', SVG_NS)
138 ET.register_namespace('xlink', XLINK_NS)
139 tree.write(str(svg_path), encoding='unicode', xml_declaration=False)
140 return count
141
141 lines PYTHON