返回 ppt-master
__init__.py
1 """Native PowerPoint object converters for explicit SVG metadata markers."""
2
3 from __future__ import annotations
4
5 import sys
6 from typing import Any
7 from xml.etree import ElementTree as ET
8
9 from ..drawingml.context import ConvertContext, ShapeResult
10 from ..drawingml.utils import _xml_escape
11 from .chart_data import _chart_data, _chart_plot_area_layout
12 from .chart_style import (
13 _axis_titles,
14 _chart_companion_entries,
15 _chart_companion_text_xml,
16 _chart_text_sizes,
17 _chart_title_is_bounded,
18 _classic_chart_style,
19 _native_chart_chrome_errors,
20 _native_chart_chrome_warnings,
21 _native_chart_export_payload,
22 _validate_chart_companion_boxes,
23 )
24 from .chart_xml import _chart_rels_xml, _chart_xml
25 from .chartex import (
26 _chart_ex_colors_xml,
27 _chart_ex_rels_xml,
28 _chart_ex_style_xml,
29 _chart_ex_xml,
30 )
31 from .fallback_hash import (
32 native_fallback_contract_warnings,
33 require_fresh_native_fallback,
34 snapshot_native_fallback_freshness,
35 stamp_native_fallback_baseline,
36 )
37 from .formula import FormulaSpec, build_native_formula, validate_formula_payload
38 from .formula_compiler import estimate_inline_formula_vertical_extent
39 from .inline_formula import (
40 INLINE_FORMULA_ATTR,
41 inline_formula_marker_errors,
42 )
43 from .marker_common import (
44 CHART_CONTENT_TYPE,
45 CHARTEX_CONTENT_TYPE,
46 CHARTEX_REL_TYPE,
47 CHARTEX_URI,
48 CHART_COLOR_STYLE_CONTENT_TYPE,
49 CHART_REL_TYPE,
50 CHART_STYLE_CONTENT_TYPE,
51 CHART_URI,
52 _NATIVE_KINDS,
53 _bounds,
54 _load_payload,
55 _local_tag,
56 _native_marker_validation_context,
57 _validate_bounds_inputs,
58 native_marker_transform,
59 )
60 from .marker_attributes import (
61 NativeMarkerAttributeError,
62 native_fallback_kind,
63 native_import_source,
64 native_metadata_payload_matches,
65 native_marker_legacy_warnings,
66 native_replacement_kind,
67 native_replacement_status,
68 )
69 from .marker_status import native_marker_status_errors
70 from .table import (
71 _build_native_table,
72 _native_table_warnings,
73 _validate_table_payload,
74 )
75 from .workbook import (
76 _minimal_category_chart_workbook,
77 _minimal_chart_ex_workbook,
78 _minimal_xy_chart_workbook,
79 )
80
81 __all__ = [
82 "convert_native_object",
83 "estimate_inline_formula_vertical_extent",
84 "INLINE_FORMULA_ATTR",
85 "NativeMarkerAttributeError",
86 "native_fallback_kind",
87 "native_import_source",
88 "native_metadata_payload_matches",
89 "native_marker_legacy_warnings",
90 "native_object_marker_warnings",
91 "native_replacement_kind",
92 "native_replacement_status",
93 "native_marker_transform",
94 "inline_formula_marker_errors",
95 "snapshot_native_fallback_freshness",
96 "stamp_native_fallback_baseline",
97 "validate_native_object_marker",
98 "validate_native_object_marker_with_warnings",
99 ]
100
101
102 def _build_native_chart(elem: ET.Element, ctx: ConvertContext, payload: dict[str, Any]) -> ShapeResult:
103 chart_data = _chart_data(payload)
104 off_x, off_y, ext_cx, ext_cy = _bounds(elem, payload, ctx)
105
106 shape_id = ctx.next_id()
107 rel_id = ctx.next_rel_id()
108 local_index = 1 + sum(1 for part in ctx.package_files if part.startswith("ppt/charts/chart"))
109 part_index = ctx.slide_num * 100 + local_index
110 workbook_name = f"Microsoft_Excel_Sheet{part_index}.xlsx"
111 workbook_part = f"ppt/embeddings/{workbook_name}"
112
113 if chart_data["kind"] == "chartex":
114 chart_name = f"chartEx{part_index}.xml"
115 style_name = f"style{part_index}.xml"
116 colors_name = f"colors{part_index}.xml"
117 chart_part = f"ppt/charts/{chart_name}"
118 chart_rels_part = f"ppt/charts/_rels/{chart_name}.rels"
119 style_part = f"ppt/charts/{style_name}"
120 colors_part = f"ppt/charts/{colors_name}"
121 graphic_uri = CHARTEX_URI
122 chart_ref_xml = (
123 f'<cx:chart xmlns:cx="{CHARTEX_URI}" '
124 f'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" '
125 f'r:id="{rel_id}"/>'
126 )
127 ctx.rel_entries.append({
128 "id": rel_id,
129 "type": CHARTEX_REL_TYPE,
130 "target": f"../charts/{chart_name}",
131 })
132 ctx.package_files[chart_part] = _chart_ex_xml(payload, chart_data, chart_rels_id="rId1")
133 ctx.package_files[chart_rels_part] = _chart_ex_rels_xml(
134 f"../embeddings/{workbook_name}",
135 style_name,
136 colors_name,
137 )
138 ctx.package_files[style_part] = _chart_ex_style_xml()
139 ctx.package_files[colors_part] = _chart_ex_colors_xml(payload)
140 ctx.package_files[workbook_part] = _minimal_chart_ex_workbook(chart_data)
141 ctx.content_type_overrides[chart_part] = CHARTEX_CONTENT_TYPE
142 ctx.content_type_overrides[style_part] = CHART_STYLE_CONTENT_TYPE
143 ctx.content_type_overrides[colors_part] = CHART_COLOR_STYLE_CONTENT_TYPE
144 else:
145 chart_name = f"chart{part_index}.xml"
146 chart_part = f"ppt/charts/{chart_name}"
147 chart_rels_part = f"ppt/charts/_rels/{chart_name}.rels"
148 graphic_uri = CHART_URI
149 chart_ref_xml = (
150 '<c:chart xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" '
151 f'r:id="{rel_id}"/>'
152 )
153 ctx.rel_entries.append({
154 "id": rel_id,
155 "type": CHART_REL_TYPE,
156 "target": f"../charts/{chart_name}",
157 })
158 ctx.package_files[chart_part] = _chart_xml(
159 elem,
160 payload,
161 chart_rels_id="rId1",
162 chart_data=chart_data,
163 inherited_styles=ctx.inherited_styles,
164 primary_language=ctx.primary_language,
165 chart_bounds=(off_x, off_y, ext_cx, ext_cy),
166 )
167 ctx.package_files[chart_rels_part] = _chart_rels_xml(f"../embeddings/{workbook_name}")
168 if chart_data["kind"] == "xy":
169 ctx.package_files[workbook_part] = _minimal_xy_chart_workbook(chart_data)
170 else:
171 ctx.package_files[workbook_part] = _minimal_category_chart_workbook(chart_data)
172 ctx.content_type_overrides[chart_part] = CHART_CONTENT_TYPE
173
174 name = _xml_escape(str(payload.get("name") or elem.get("id") or f"Native Chart {shape_id}"))
175 chart_frame_xml = f'''<p:graphicFrame>
176 <p:nvGraphicFramePr>
177 <p:cNvPr id="{shape_id}" name="{name}"/>
178 <p:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></p:cNvGraphicFramePr>
179 <p:nvPr/>
180 </p:nvGraphicFramePr>
181 <p:xfrm><a:off x="{off_x}" y="{off_y}"/><a:ext cx="{ext_cx}" cy="{ext_cy}"/></p:xfrm>
182 <a:graphic>
183 <a:graphicData uri="{graphic_uri}">
184 {chart_ref_xml}
185 </a:graphicData>
186 </a:graphic>
187 </p:graphicFrame>'''
188 text_sizes = _chart_text_sizes(payload, elem, ctx.inherited_styles)
189 chart_style = _classic_chart_style(payload, elem, ctx.inherited_styles)
190 companion_xml = _chart_companion_text_xml(
191 ctx,
192 payload,
193 chart_bounds=(off_x, off_y, ext_cx, ext_cy),
194 chart_style=chart_style,
195 note_font_size=text_sizes["note"],
196 title_font_size=text_sizes["title"],
197 include_title=(
198 chart_data["kind"] == "chartex"
199 or _chart_title_is_bounded(payload)
200 ),
201 include_subtitle_as_caption=chart_data["kind"] == "chartex",
202 )
203 xml = chart_frame_xml + companion_xml
204 return ShapeResult(xml=xml, bounds_emu=(off_x, off_y, off_x + ext_cx, off_y + ext_cy))
205
206
207 def _validate_native_object_marker_payload(
208 elem: ET.Element,
209 *,
210 validate_chrome: bool = True,
211 ctx: ConvertContext | None = None,
212 ancestors: tuple[ET.Element, ...] = (),
213 require_fresh_fallback: bool = False,
214 ) -> tuple[str, dict[str, Any], list[list[Any]] | FormulaSpec | None]:
215 try:
216 kind = native_replacement_kind(elem)
217 except NativeMarkerAttributeError as exc:
218 raise RuntimeError(str(exc)) from exc
219 if not kind:
220 return "", {}, None
221 status_errors = native_marker_status_errors(elem)
222 if status_errors:
223 raise RuntimeError("; ".join(status_errors))
224 if kind not in _NATIVE_KINDS:
225 raise RuntimeError(f"Unsupported data-pptx-replace-with value: {kind}")
226 if _local_tag(elem) != "g":
227 raise RuntimeError("Native PPTX replacement markers must be <g> elements")
228 native_marker_transform(elem.get("transform"))
229 if require_fresh_fallback and kind in {"chart", "table"}:
230 require_fresh_native_fallback(elem, use_runtime_snapshot=True)
231
232 try:
233 payload = _load_payload(elem, kind)
234 except NativeMarkerAttributeError as exc:
235 raise RuntimeError(str(exc)) from exc
236 bounds_ctx = ctx or _native_marker_validation_context(elem, ancestors)
237 off_x, off_y, ext_cx, ext_cy, _ = _validate_bounds_inputs(elem, payload, bounds_ctx)
238 validated_data: list[list[Any]] | FormulaSpec | None = None
239 if kind == "table":
240 table_rows, col_count, _merge_layout = _validate_table_payload(payload)
241 validated_data = table_rows
242 if ext_cx < col_count or ext_cy < len(table_rows):
243 raise RuntimeError(
244 "Native PPTX table bounds must provide at least one EMU per row and column"
245 )
246 elif kind == "chart":
247 chart_data = _chart_data(payload)
248 _chart_plot_area_layout(
249 chart_data,
250 (off_x, off_y, ext_cx, ext_cy),
251 )
252 _validate_chart_companion_boxes(
253 payload,
254 chart_bounds=(off_x, off_y, ext_cx, ext_cy),
255 include_title=(
256 chart_data["kind"] == "chartex"
257 or _chart_title_is_bounded(payload)
258 ),
259 include_subtitle_as_caption=chart_data["kind"] == "chartex",
260 )
261 if validate_chrome and native_import_source(elem) != "pptx":
262 chrome_errors = _native_chart_chrome_errors(elem, payload)
263 if chrome_errors:
264 raise RuntimeError("; ".join(chrome_errors))
265 else:
266 validated_data = validate_formula_payload(payload, ctx=ctx)
267 return kind, payload, validated_data
268
269
270 def validate_native_object_marker(
271 elem: ET.Element,
272 *,
273 ancestors: tuple[ET.Element, ...] = (),
274 ) -> None:
275 """Validate a native replacement marker without mutating the package."""
276 _validate_native_object_marker_payload(elem, ancestors=ancestors)
277
278
279 def validate_native_object_marker_with_warnings(
280 elem: ET.Element,
281 *,
282 ancestors: tuple[ET.Element, ...] = (),
283 document_root: ET.Element | None = None,
284 ) -> list[str]:
285 """Validate a native replacement marker and return non-fatal warnings."""
286 kind, payload, validated_data = _validate_native_object_marker_payload(
287 elem,
288 ancestors=ancestors,
289 )
290 warnings = (
291 native_fallback_contract_warnings(
292 elem,
293 document_root=document_root,
294 )
295 if kind in {"chart", "table"} else []
296 )
297 if kind == "table" and isinstance(validated_data, list):
298 warnings.extend(_native_table_warnings(elem, validated_data))
299 elif kind == "chart":
300 warnings.extend(_native_chart_chrome_warnings(elem, payload))
301 return warnings
302
303
304 def native_object_marker_warnings(
305 elem: ET.Element,
306 *,
307 ancestors: tuple[ET.Element, ...] = (),
308 document_root: ET.Element | None = None,
309 ) -> list[str]:
310 """Return non-fatal warnings for a native replacement marker."""
311 return validate_native_object_marker_with_warnings(
312 elem,
313 ancestors=ancestors,
314 document_root=document_root,
315 )
316
317
318 def convert_native_object(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None:
319 """Convert a marked SVG group to its native PowerPoint object."""
320 try:
321 kind = native_replacement_kind(elem)
322 except NativeMarkerAttributeError as exc:
323 raise RuntimeError(str(exc)) from exc
324 if not kind:
325 return None
326
327 kind, payload, validated_data = _validate_native_object_marker_payload(
328 elem,
329 validate_chrome=False,
330 ctx=ctx,
331 require_fresh_fallback=True,
332 )
333 if kind == "formula":
334 formula_spec = (
335 validated_data if isinstance(validated_data, FormulaSpec) else None
336 )
337 return build_native_formula(elem, ctx, payload, formula_spec)
338
339 marker_id = elem.get("id") or "<unnamed>"
340 for warning in native_fallback_contract_warnings(
341 elem,
342 use_runtime_snapshot=True,
343 ):
344 print(
345 f" Warning: data-pptx-replace-with marker {marker_id}: {warning}",
346 file=sys.stderr,
347 )
348 if kind == "table":
349 return _build_native_table(elem, ctx, payload)
350 payload, warnings = _native_chart_export_payload(elem, payload)
351 for warning in warnings:
352 print(
353 f" Warning: data-pptx-replace-with marker {marker_id}: {warning}",
354 file=sys.stderr,
355 )
356 return _build_native_chart(elem, ctx, payload)
357
357 lines PYTHON