| 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 |