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