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