#!/usr/bin/env python3 """ PPT Master - Mirror Template Materializer Materialize a deterministic structured SVG template workspace from one Type A PPTX import workspace. The editable authoring IR is the only authoring input; lossless SVG files are consulted solely to restore unchanged supported source objects. Final templates and imported vectors contain no IR-only source refs. Usage: python3 scripts/mirror_template_materialize.py \ Dependencies: None (standard library and sibling PPT Master modules only). """ from __future__ import annotations import argparse import base64 import copy import hashlib import json import math import os import re import sys import tempfile from collections import Counter from dataclasses import dataclass from pathlib import Path from typing import Any, Iterable from urllib.parse import unquote, urlsplit, urlunsplit from xml.etree import ElementTree as ET from compact_svg_coordinates import compact_svg_tree, format_coordinate from console_encoding import configure_utf8_stdio from native_payloads import ( PAYLOAD_STORE_RELATIVE_PATH, NativePayloadError, NativePayloadStats, build_native_attribute_records, collect_native_attribute_record_keys, externalize_native_attribute_records, externalize_native_payloads, hydrate_native_payload_refs, serialize_native_payload_store, ) from pptx_shapes import svg_preset_preview_fingerprint from svg_authoring_view import ( AUTHORING_MANIFEST_NAME, AUTHORING_SCHEMA, SOURCE_REF_ATTRIBUTE, semantic_subtree_sha256, ) from svg_finalize.flatten_tspan import flatten_text_with_tspans from svg_to_pptx.pptx_package.template_structure import ( TemplateStructureError, parse_template_slides, ) from template_text_slots import ( analyze_template_text_slots, text_slot_integrity_sha256, ) configure_utf8_stdio() SVG_NS = "http://www.w3.org/2000/svg" XLINK_NS = "http://www.w3.org/1999/xlink" XML_NS = "http://www.w3.org/XML/1998/namespace" NATIVE_STRUCTURE_SCHEMA = "ppt-master.native-structure.v1" VECTOR_INVENTORY_SCHEMA = "vector_asset_inventory.v1" TEMPLATE_EXECUTION_MANIFEST_NAME = "template_execution_manifest.json" TEMPLATE_EXECUTION_MANIFEST_SCHEMA = "ppt-master.template-execution-manifest.v1" TEMPLATE_TEXT_SLOTS_DIR = "template_execution" TEMPLATE_TEXT_SLOTS_SCHEMA = "ppt-master.template-text-slots.v2-min" IMPORTED_ICON_NAMESPACE = "imported" TRANSPARENT_PIXEL_DATA_URI = ( "data:image/png;base64," "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUA" "AXpeqz8AAAAASUVORK5CYII=" ) ET.register_namespace("", SVG_NS) ET.register_namespace("xlink", XLINK_NS) _NON_VISUAL_TAGS = frozenset({"defs", "desc", "metadata", "style", "title"}) _BITMAP_EXTENSIONS = frozenset({ ".avif", ".bmp", ".gif", ".jpeg", ".jpg", ".png", ".tif", ".tiff", ".webp", }) _INHERITED_PRESENTATION_ATTRIBUTES = frozenset({ "color", "fill", "fill-opacity", "font-family", "font-size", "font-style", "font-weight", "letter-spacing", "paint-order", "shape-rendering", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "text-anchor", "text-decoration", "word-spacing", }) _AGGREGATE_GROUP_ATTRIBUTES = frozenset({ "clip-path", "filter", "mask", "mix-blend-mode", "opacity", }) _URL_REFERENCE_RE = re.compile(r"url\(\s*(['\"]?)#([^)'\"\s]+)\1\s*\)") _CSS_ID_RE = re.compile(r"#([A-Za-z_][A-Za-z0-9_.:-]*)") _SAFE_KEY_RE = re.compile(r"[^A-Za-z0-9_.-]+") _TRANSFORM_NUMBER = r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?" _AXIS_REFLECTION_RE = re.compile( rf"^\s*translate\(\s*({_TRANSFORM_NUMBER})[\s,]+" rf"({_TRANSFORM_NUMBER})\s*\)\s*" rf"scale\(\s*({_TRANSFORM_NUMBER})[\s,]+" rf"({_TRANSFORM_NUMBER})\s*\)\s*" rf"translate\(\s*({_TRANSFORM_NUMBER})[\s,]+" rf"({_TRANSFORM_NUMBER})\s*\)\s*$" ) class MirrorMaterializationError(RuntimeError): """Reject incomplete, ambiguous, or unsafe mirror materialization input.""" @dataclass(frozen=True) class SourceRefRecord: source_path: tuple[int, ...] initial_authoring_subtree_sha256: str @dataclass(frozen=True) class AuthoringDocument: name: str authoring_path: Path source_path: Path source_sha256: str source_refs: dict[str, SourceRefRecord] @dataclass(frozen=True) class VectorAssetRecord: icon: str asset_path: Path origin_document: str expected_sha256: str source_refs: tuple[str, ...] @dataclass(frozen=True) class SlotPlan: slot_id: str semantic_role: str placeholder_type: str | None idx: int | None shape_id: str bounds: tuple[float, float, float, float] @dataclass class RestorationStats: rehydrated_refs: int = 0 fallback_refs: int = 0 structural_refs: int = 0 detached_connector_endpoints: int = 0 upright_text_compensations: int = 0 def merge(self, other: "RestorationStats") -> None: self.rehydrated_refs += other.rehydrated_refs self.fallback_refs += other.fallback_refs self.structural_refs += other.structural_refs self.detached_connector_endpoints += other.detached_connector_endpoints self.upright_text_compensations += other.upright_text_compensations def as_dict(self) -> dict[str, int]: return { "rehydrated_refs": self.rehydrated_refs, "fallback_refs": self.fallback_refs, "structural_refs": self.structural_refs, "detached_connector_endpoints": self.detached_connector_endpoints, "upright_text_compensations": self.upright_text_compensations, } @dataclass class MaterializedFile: relative_path: Path payload: bytes def _json_bytes(payload: dict[str, object]) -> bytes: return ( json.dumps(payload, ensure_ascii=False, indent=2) + "\n" ).encode("utf-8") def _compact_json_bytes(payload: dict[str, object]) -> bytes: return ( json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n" ).encode("utf-8") def _template_execution_manifest_files( materialized_roots: list[tuple[Path, ET.Element]], source_import: dict[str, object] | None, ) -> list[MaterializedFile]: """Serialize one compact roster plus per-prototype text-slot sidecars.""" templates: list[dict[str, object]] = [] files: list[MaterializedFile] = [] for relative_path, root in sorted( materialized_roots, key=lambda item: item[0].as_posix(), ): prototype = relative_path.name try: analyzed_slots = analyze_template_text_slots(root) except ValueError as exc: raise MirrorMaterializationError( f"Cannot project text slots for {prototype}: {exc}" ) from exc text_slots = [slot.model_payload() for slot in analyzed_slots] editable_text_slot_count = sum(slot.editable for slot in analyzed_slots) text_slots_path = ( Path("templates") / TEMPLATE_TEXT_SLOTS_DIR / f"{relative_path.stem}.text-slots.json" ) files.append(MaterializedFile( text_slots_path, _compact_json_bytes({ "schema": TEMPLATE_TEXT_SLOTS_SCHEMA, "prototype": prototype, "text_slot_count": len(text_slots), "tool_integrity_sha256": text_slot_integrity_sha256(analyzed_slots), "text_slots": text_slots, }), )) templates.append({ "prototype": prototype, "page_type": relative_path.stem.split("_", 1)[-1], "viewBox": root.get("viewBox"), "master": root.get("data-pptx-master"), "layout": root.get("data-pptx-layout"), "layout_name": root.get("data-pptx-layout-name"), "text_slot_count": len(text_slots), "editable_text_slot_count": editable_text_slot_count, "text_slots_path": text_slots_path.relative_to( Path("templates") ).as_posix(), }) payload = { "schema": TEMPLATE_EXECUTION_MANIFEST_SCHEMA, "replication_mode": "mirror", "template_root": ".", "template_count": len(templates), "text_slots_schema": TEMPLATE_TEXT_SLOTS_SCHEMA, "execution_policy": ( "This manifest and its text_slots_path records are derived tool " "metadata, not page-authoring inputs. Retain the complete project " "Design Spec and lock once per valid uncompacted execution context; " "use page-context only for on-demand diagnostics or telemetry. " "Read the selected complete prototype once per valid context and " "again only after a known change or context invalidation. Choose " "semantic replacements and edit only existing " "visible text values; structured export validates text/tspan " "topology and attributes against the prototype." ), "source_import": source_import or { "warning_count": 0, "by_code": {}, }, "templates": templates, } files.append(MaterializedFile( Path("templates") / TEMPLATE_EXECUTION_MANIFEST_NAME, _json_bytes(payload), )) return files def _source_import_summary(import_workspace: Path) -> dict[str, object] | None: """Summarize source-owned tolerant-import diagnostics by stable code.""" report_path = import_workspace / "conversion-report.json" try: report = json.loads(report_path.read_text(encoding="utf-8")) except FileNotFoundError: return None except (OSError, json.JSONDecodeError) as exc: raise MirrorMaterializationError( f"Cannot read source conversion report {report_path}: {exc}" ) from exc diagnostics = report.get("diagnostics") if not isinstance(diagnostics, list): raise MirrorMaterializationError( f"Source conversion report has no diagnostics array: {report_path}" ) by_code: Counter[str] = Counter() samples: dict[str, str] = {} for item in diagnostics: if not isinstance(item, dict): continue severity = str(item.get("severity") or "warning").lower() if severity != "warning": continue code = str(item.get("code") or "unknown") by_code[code] += 1 message = item.get("message") if code not in samples and isinstance(message, str) and message: samples[code] = message return { "warning_count": sum(by_code.values()), "by_code": dict(sorted(by_code.items())), "samples": dict(sorted(samples.items())), } def _local_name(name: object) -> str: return name.rsplit("}", 1)[-1] if isinstance(name, str) else "" def _axis_reflection_transform(value: str | None) -> str | None: """Return one exact importer axis-reflection transform when valid.""" if not value: return None match = _AXIS_REFLECTION_RE.fullmatch(value) if match is None: return None cx, cy, scale_x, scale_y, offset_x, offset_y = ( float(token) for token in match.groups() ) if not ( math.isclose(abs(scale_x), 1.0, abs_tol=1e-9) and math.isclose(abs(scale_y), 1.0, abs_tol=1e-9) and (scale_x < 0 or scale_y < 0) and math.isclose(offset_x, -cx, abs_tol=1e-7) and math.isclose(offset_y, -cy, abs_tol=1e-7) ): return None return value.strip() def _compensate_reflected_group_text(root: ET.Element) -> int: """Keep browser-visible text upright inside imported flipped groups.""" parent_by_child = { child: parent for parent in root.iter() for child in parent } reflected_groups: list[tuple[int, ET.Element, str]] = [] for element in root.iter(): if _local_name(element.tag) != "g": continue transform = _axis_reflection_transform(element.get("transform")) if transform is None: continue depth = 0 current = element while current in parent_by_child: depth += 1 current = parent_by_child[current] reflected_groups.append((depth, element, transform)) wrapped = 0 for _depth, group, transform in sorted( reflected_groups, key=lambda item: item[0], reverse=True, ): text_elements = [ element for element in group.iter() if _local_name(element.tag) == "text" ] for text in text_elements: current_parents = { child: parent for parent in group.iter() for child in parent } parent = current_parents.get(text) if parent is None: continue position = list(parent).index(text) parent.remove(text) wrapper = ET.Element( f"{{{SVG_NS}}}g", {"transform": transform}, ) wrapper.append(text) parent.insert(position, wrapper) wrapped += 1 return wrapped def _sha256_bytes(payload: bytes) -> str: return hashlib.sha256(payload).hexdigest() def _sha256_file(path: Path) -> str: return _sha256_bytes(path.read_bytes()) def _load_json(path: Path, *, context: str) -> dict[str, Any]: try: payload = json.loads(path.read_text(encoding="utf-8")) except OSError as exc: raise MirrorMaterializationError(f"Cannot read {context}: {path}: {exc}") from exc except json.JSONDecodeError as exc: raise MirrorMaterializationError( f"Invalid JSON in {context}: {path}: {exc}" ) from exc if not isinstance(payload, dict): raise MirrorMaterializationError(f"{context} must be a JSON object: {path}") return payload def _require_list(value: object, *, context: str) -> list[Any]: if not isinstance(value, list): raise MirrorMaterializationError(f"{context} must be a list") return value def _require_string(value: object, *, context: str) -> str: if not isinstance(value, str) or not value.strip(): raise MirrorMaterializationError(f"{context} must be a non-empty string") return value.strip() def _require_boolean(value: object, *, context: str) -> bool: if not isinstance(value, bool): raise MirrorMaterializationError(f"{context} must be a boolean") return value def _resolve_inside(root: Path, relative: str, *, context: str) -> Path: parsed = urlsplit(relative) if parsed.scheme or parsed.netloc or Path(parsed.path).is_absolute(): raise MirrorMaterializationError(f"{context} must be a relative path: {relative}") resolved = (root / unquote(parsed.path)).resolve() try: resolved.relative_to(root.resolve()) except ValueError as exc: raise MirrorMaterializationError( f"{context} escapes its declared root: {relative}" ) from exc return resolved def _parse_svg(path: Path) -> ET.Element: parser = ET.XMLParser(target=ET.TreeBuilder(insert_comments=True, insert_pis=True)) try: root = ET.fromstring(path.read_bytes(), parser=parser) except (OSError, ET.ParseError) as exc: raise MirrorMaterializationError(f"Cannot parse SVG {path}: {exc}") from exc if _local_name(root.tag) != "svg": raise MirrorMaterializationError(f"SVG root is not : {path}") return root def _source_element(root: ET.Element, path: tuple[int, ...]) -> ET.Element: element = root try: for index in path: element = list(element)[index] except (IndexError, TypeError) as exc: raise MirrorMaterializationError( f"Source-ref path no longer resolves: {list(path)}" ) from exc return element def _source_identity(element: ET.Element) -> str | None: scope = element.get("data-pptx-shape-scope") shape_id = element.get("data-pptx-shape-id") if not scope or not shape_id: return None return f"{scope}:{shape_id}" def _load_authoring_documents( workspace: Path, ) -> tuple[Path, dict[str, AuthoringDocument]]: authoring_root = workspace / "authoring-svg" manifest_path = authoring_root / AUTHORING_MANIFEST_NAME manifest = _load_json(manifest_path, context="authoring manifest") if manifest.get("schema") != AUTHORING_SCHEMA: raise MirrorMaterializationError( f"Unsupported authoring manifest schema: {manifest.get('schema')!r}" ) if manifest.get("projection_kind") != "layered": raise MirrorMaterializationError( "Mirror materialization requires projection_kind='layered'" ) if manifest.get("authoring_root") != ".": raise MirrorMaterializationError("authoring_manifest.json authoring_root must be '.'") if manifest.get("source_ref_attribute") != SOURCE_REF_ATTRIBUTE: raise MirrorMaterializationError( "authoring_manifest.json uses an unsupported source-ref attribute" ) source_root_raw = _require_string( manifest.get("source_root"), context="authoring_manifest.json source_root", ) source_root = (authoring_root / source_root_raw).resolve() expected_source_root = (workspace / "svg").resolve() if source_root != expected_source_root: raise MirrorMaterializationError( "Type A mirror authoring manifest must resolve source_root to " f"{expected_source_root}, found {source_root}" ) documents_raw = _require_list( manifest.get("documents"), context="authoring_manifest.json documents", ) documents: dict[str, AuthoringDocument] = {} for index, raw in enumerate(documents_raw): if not isinstance(raw, dict): raise MirrorMaterializationError(f"documents[{index}] must be an object") authoring_name = _require_string( raw.get("authoring"), context=f"documents[{index}].authoring", ) source_name = _require_string( raw.get("source"), context=f"documents[{index}].source", ) if authoring_name in documents: raise MirrorMaterializationError( f"Duplicate authoring manifest document: {authoring_name}" ) authoring_path = _resolve_inside( authoring_root, authoring_name, context=f"documents[{index}].authoring", ) source_path = _resolve_inside( source_root, source_name, context=f"documents[{index}].source", ) if not authoring_path.is_file() or authoring_path.suffix.lower() != ".svg": raise MirrorMaterializationError( f"Authoring SVG is missing: {authoring_path}" ) if not source_path.is_file() or source_path.suffix.lower() != ".svg": raise MirrorMaterializationError(f"Lossless source SVG is missing: {source_path}") expected_source_sha = _require_string( raw.get("source_sha256"), context=f"documents[{index}].source_sha256", ) actual_source_sha = _sha256_file(source_path) if actual_source_sha != expected_source_sha: raise MirrorMaterializationError( f"Lossless source SVG changed: {source_path.name}; expected " f"{expected_source_sha}, found {actual_source_sha}" ) refs_raw = raw.get("source_refs") if not isinstance(refs_raw, dict): raise MirrorMaterializationError( f"documents[{index}].source_refs must be an object" ) refs: dict[str, SourceRefRecord] = {} source_root_element = _parse_svg(source_path) for source_ref, ref_raw in refs_raw.items(): if not isinstance(source_ref, str) or not isinstance(ref_raw, dict): raise MirrorMaterializationError( f"Invalid source-ref record in {authoring_name}" ) path_raw = ref_raw.get("source_path") if not ( isinstance(path_raw, list) and all(isinstance(item, int) and item >= 0 for item in path_raw) ): raise MirrorMaterializationError( f"{authoring_name} source ref {source_ref!r} has invalid source_path" ) initial_hash = _require_string( ref_raw.get("initial_authoring_subtree_sha256"), context=f"{authoring_name} source ref {source_ref!r} hash", ) source_path_tuple = tuple(path_raw) source_element = _source_element(source_root_element, source_path_tuple) if _source_identity(source_element) != source_ref: raise MirrorMaterializationError( f"{authoring_name} source ref {source_ref!r} resolves to " f"{_source_identity(source_element)!r}" ) refs[source_ref] = SourceRefRecord(source_path_tuple, initial_hash) documents[authoring_name] = AuthoringDocument( name=authoring_name, authoring_path=authoring_path, source_path=source_path, source_sha256=expected_source_sha, source_refs=refs, ) actual_authoring_files = { path.relative_to(authoring_root).as_posix() for path in authoring_root.rglob("*.svg") if path.is_file() } if actual_authoring_files != set(documents): raise MirrorMaterializationError( "Authoring manifest/file roster differs; missing=" f"{sorted(set(documents) - actual_authoring_files)}, extra=" f"{sorted(actual_authoring_files - set(documents))}" ) if manifest.get("file_count") != len(documents): raise MirrorMaterializationError( "authoring_manifest.json file_count does not match documents" ) expected_ref_count = sum(len(document.source_refs) for document in documents.values()) if manifest.get("source_ref_count") != expected_ref_count: raise MirrorMaterializationError( "authoring_manifest.json source_ref_count does not match documents" ) return authoring_root, documents def _load_vector_assets( workspace: Path, documents: dict[str, AuthoringDocument], ) -> dict[str, VectorAssetRecord]: inventory_path = workspace / "authoring-svg_vector_asset_inventory.json" if not inventory_path.exists(): return {} inventory = _load_json(inventory_path, context="vector asset inventory") if inventory.get("schema") != VECTOR_INVENTORY_SCHEMA: raise MirrorMaterializationError( f"Unsupported vector inventory schema: {inventory.get('schema')!r}" ) if inventory.get("icon_namespace") != IMPORTED_ICON_NAMESPACE: raise MirrorMaterializationError( "Mirror vector inventory must use icon_namespace='imported'" ) icons_root = workspace / "icons" records: dict[str, VectorAssetRecord] = {} for index, raw in enumerate( _require_list(inventory.get("assets"), context="vector inventory assets") ): if not isinstance(raw, dict): raise MirrorMaterializationError(f"vector assets[{index}] must be an object") icon = _require_string(raw.get("icon"), context=f"assets[{index}].icon") asset = _require_string(raw.get("asset"), context=f"assets[{index}].asset") origin = _require_string(raw.get("svg"), context=f"assets[{index}].svg") if not icon.startswith(f"{IMPORTED_ICON_NAMESPACE}/"): raise MirrorMaterializationError( f"Vector asset {icon!r} is outside imported/ namespace" ) if icon in records: raise MirrorMaterializationError(f"Duplicate vector asset id: {icon}") if origin not in documents: raise MirrorMaterializationError( f"Vector asset {icon!r} names unknown origin document {origin!r}" ) asset_path = _resolve_inside( icons_root, asset, context=f"vector asset {icon!r}", ) if not asset_path.is_file() or asset_path.suffix.lower() != ".svg": raise MirrorMaterializationError(f"Vector asset is missing: {asset_path}") expected_sha256 = _require_string( raw.get("asset_sha256"), context=f"vector asset {icon!r} asset_sha256", ) actual_sha256 = _sha256_file(asset_path) if actual_sha256 != expected_sha256: raise MirrorMaterializationError( f"Vector asset {icon!r} changed; expected {expected_sha256}, " f"found {actual_sha256}" ) refs_raw = _require_list( raw.get("source_refs", []), context=f"vector asset {icon!r} source_refs", ) if not all(isinstance(item, str) and item for item in refs_raw): raise MirrorMaterializationError( f"Vector asset {icon!r} source_refs must be strings" ) records[icon] = VectorAssetRecord( icon=icon, asset_path=asset_path, origin_document=origin, expected_sha256=expected_sha256, source_refs=tuple(refs_raw), ) if inventory.get("asset_count") != len(records): raise MirrorMaterializationError("Vector inventory asset_count is stale") return records def _source_ref_counts(root: ET.Element) -> dict[str, int]: counts: dict[str, int] = {} for element in root.iter(): source_ref = element.get(SOURCE_REF_ATTRIBUTE) if source_ref: counts[source_ref] = counts.get(source_ref, 0) + 1 return counts def _imported_icon_refs(root: ET.Element) -> set[str]: return { value for element in root.iter() if (value := (element.get("data-icon") or "").strip()) if value.startswith(f"{IMPORTED_ICON_NAMESPACE}/") } def _validate_source_ref_closure( documents: dict[str, AuthoringDocument], vector_assets: dict[str, VectorAssetRecord], ) -> set[str]: direct_counts: dict[str, dict[str, int]] = {} referenced_icons: set[str] = set() for name, document in documents.items(): root = _parse_svg(document.authoring_path) direct_counts[name] = _source_ref_counts(root) referenced_icons.update(_imported_icon_refs(root)) unknown_icons = sorted(referenced_icons - set(vector_assets)) if unknown_icons: raise MirrorMaterializationError( "Authoring SVG references missing imported vector asset(s): " + ", ".join(unknown_icons) ) asset_counts_by_document: dict[str, dict[str, int]] = { name: {} for name in documents } for icon in sorted(referenced_icons): record = vector_assets[icon] root = _parse_svg(record.asset_path) counts = _source_ref_counts(root) if set(counts) != set(record.source_refs): raise MirrorMaterializationError( f"Vector asset {icon!r} source-ref inventory is stale; expected " f"{sorted(record.source_refs)}, found {sorted(counts)}" ) duplicate_refs = sorted(ref for ref, count in counts.items() if count != 1) if duplicate_refs: raise MirrorMaterializationError( f"Vector asset {icon!r} contains duplicate source refs: " + ", ".join(duplicate_refs) ) origin_counts = asset_counts_by_document[record.origin_document] for source_ref, count in counts.items(): origin_counts[source_ref] = origin_counts.get(source_ref, 0) + count for name, document in documents.items(): combined = dict(direct_counts[name]) for source_ref, count in asset_counts_by_document[name].items(): combined[source_ref] = combined.get(source_ref, 0) + count expected = set(document.source_refs) actual = set(combined) duplicates = sorted(ref for ref, count in combined.items() if count != 1) if actual != expected or duplicates: raise MirrorMaterializationError( f"{name} source-ref closure differs from its manifest; missing=" f"{sorted(expected - actual)}, extra={sorted(actual - expected)}, " f"duplicates={duplicates}" ) return referenced_icons def _load_native_graph(workspace: Path) -> dict[str, Any]: native_path = workspace / "native_structure.json" native = _load_json(native_path, context="native structure") if native.get("schema") != NATIVE_STRUCTURE_SCHEMA: raise MirrorMaterializationError( f"Unsupported native structure schema: {native.get('schema')!r}" ) source = native.get("source") if not isinstance(source, dict): raise MirrorMaterializationError("native_structure.json source must be an object") template_name = _require_string( source.get("templateFile"), context="native_structure.json source.templateFile", ) template_path = _resolve_inside( workspace, template_name, context="native source template", ) if not template_path.is_file(): raise MirrorMaterializationError(f"Native source template is missing: {template_path}") expected_sha = _require_string( source.get("sha256"), context="native_structure.json source.sha256", ) actual_sha = _sha256_file(template_path) if actual_sha != expected_sha: raise MirrorMaterializationError( f"Native source template changed; expected {expected_sha}, found {actual_sha}" ) slide_size = native.get("slideSize") if not isinstance(slide_size, dict): raise MirrorMaterializationError("native_structure.json slideSize is missing") for field in ("width_px", "height_px"): value = slide_size.get(field) if not isinstance(value, (int, float)) or not math.isfinite(value) or value <= 0: raise MirrorMaterializationError(f"native slideSize.{field} must be positive") return native def _load_inheritance(workspace: Path) -> dict[str, Any]: return _load_json(workspace / "svg" / "inheritance.json", context="inheritance graph") def _validate_graph_roster( native: dict[str, Any], inheritance: dict[str, Any], documents: dict[str, AuthoringDocument], ) -> None: masters = _require_list(native.get("masters"), context="native masters") layouts = _require_list(native.get("layouts"), context="native layouts") slides = _require_list(native.get("slides"), context="native slides") inheritance_masters_raw = _require_list( inheritance.get("masters"), context="inheritance masters", ) master_file_by_part = { _require_string(item.get("partPath"), context="inheritance Master partPath"): _require_string(item.get("file"), context="inheritance Master file") for item in inheritance_masters_raw if isinstance(item, dict) } for index, master in enumerate(masters): if not isinstance(master, dict): raise MirrorMaterializationError(f"native masters[{index}] must be an object") package_part = _require_string( master.get("packagePart"), context=f"native masters[{index}].packagePart", ) svg_file = master_file_by_part.get(package_part) if svg_file is None: raise MirrorMaterializationError( f"Native Master part {package_part!r} has no inheritance SVG file" ) master["svgFile"] = svg_file expected_files: set[str] = set() for collection, field in ( (masters, "svgFile"), (layouts, "svgFile"), (slides, "layeredSvgFile"), ): for index, item in enumerate(collection): if not isinstance(item, dict): raise MirrorMaterializationError(f"native {field}[{index}] must be an object") expected_files.add( _require_string(item.get(field), context=f"native {field}[{index}]") ) if expected_files != set(documents): raise MirrorMaterializationError( "Native graph and authoring document roster differ; missing=" f"{sorted(expected_files - set(documents))}, extra=" f"{sorted(set(documents) - expected_files)}" ) inherited_masters = { _require_string(item.get("file"), context="inheritance master file") for item in inheritance_masters_raw if isinstance(item, dict) } inherited_layouts = { _require_string(item.get("file"), context="inheritance layout file"): item for item in _require_list(inheritance.get("layouts"), context="inheritance layouts") if isinstance(item, dict) } inherited_slides = { int(item.get("index")): item for item in _require_list(inheritance.get("slides"), context="inheritance slides") if isinstance(item, dict) and isinstance(item.get("index"), int) } if inherited_masters != {item["svgFile"] for item in masters}: raise MirrorMaterializationError("Inheritance Master roster differs from native graph") if set(inherited_layouts) != {item["svgFile"] for item in layouts}: raise MirrorMaterializationError("Inheritance Layout roster differs from native graph") if set(inherited_slides) != {int(item["index"]) for item in slides}: raise MirrorMaterializationError("Inheritance Slide roster differs from native graph") master_file_by_key = {item["key"]: item["svgFile"] for item in masters} layout_file_by_key = {item["key"]: item["svgFile"] for item in layouts} for layout in layouts: inherited = inherited_layouts[layout["svgFile"]] if inherited.get("master") != master_file_by_key.get(layout.get("masterKey")): raise MirrorMaterializationError( f"Layout {layout.get('key')!r} Master parent differs across native facts" ) inherited_visibility = _require_boolean( inherited.get("showMasterShapes"), context=f"inheritance Layout {layout.get('key')!r} showMasterShapes", ) native_visibility = _require_boolean( layout.get("showMasterShapes"), context=f"native Layout {layout.get('key')!r} showMasterShapes", ) if inherited_visibility != native_visibility: raise MirrorMaterializationError( f"Layout {layout.get('key')!r} showMasterShapes differs across facts" ) for slide in slides: inherited = inherited_slides[int(slide["index"])] if inherited.get("layout") != layout_file_by_key.get(slide.get("layoutKey")): raise MirrorMaterializationError( f"Slide {slide.get('index')} Layout parent differs across native facts" ) if inherited.get("master") != master_file_by_key.get(slide.get("masterKey")): raise MirrorMaterializationError( f"Slide {slide.get('index')} Master parent differs across native facts" ) inherited_visibility = _require_boolean( inherited.get("showInheritedShapes"), context=f"inheritance Slide {slide.get('index')} showInheritedShapes", ) native_visibility = _require_boolean( slide.get("showInheritedShapes"), context=f"native Slide {slide.get('index')} showInheritedShapes", ) if inherited_visibility != native_visibility: raise MirrorMaterializationError( f"Slide {slide.get('index')} showInheritedShapes differs across facts" ) def _absolutize_local_hrefs(root: ET.Element, base_dir: Path) -> None: for element in root.iter(): for attribute in ("href", f"{{{XLINK_NS}}}href"): value = element.get(attribute) if not value or value.startswith("#"): continue parsed = urlsplit(value) if parsed.scheme or parsed.netloc or not parsed.path: continue absolute = (base_dir / unquote(parsed.path)).resolve().as_posix() element.set( attribute, urlunsplit(("", "", absolute, parsed.query, parsed.fragment)), ) def _rehydrate_tree( root: ET.Element, document: AuthoringDocument, *, excluded_refs: set[str], ) -> RestorationStats: source_root = _parse_svg(document.source_path) stats = RestorationStats() def restore(element: ET.Element) -> ET.Element: source_ref = element.get(SOURCE_REF_ATTRIBUTE) record = document.source_refs.get(source_ref or "") if source_ref and record is None: raise MirrorMaterializationError( f"{document.name} contains unknown source ref {source_ref!r}" ) if source_ref and source_ref in excluded_refs: stats.structural_refs += 1 elif source_ref and record is not None: actual_hash = semantic_subtree_sha256( element, ignored_attributes=frozenset({SOURCE_REF_ATTRIBUTE}), ) if actual_hash == record.initial_authoring_subtree_sha256: restored = copy.deepcopy(_source_element(source_root, record.source_path)) _absolutize_local_hrefs(restored, document.source_path.parent) stats.rehydrated_refs += 1 return restored stats.fallback_refs += 1 children = list(element) for index, child in enumerate(children): replacement = restore(child) if replacement is child: continue replacement.tail = child.tail element.remove(child) element.insert(index, replacement) return element restore(root) return stats def _line_groups(text: ET.Element) -> list[list[ET.Element]]: """Return direct tspan runs grouped by their imported visual line.""" groups: list[list[ET.Element]] = [] for tspan in text: if _local_name(tspan.tag) != "tspan": return [] raw_dy = _optional_float(tspan.get("dy")) starts_line = ( not groups or tspan.get("x") is not None or tspan.get("y") is not None or (raw_dy is not None and abs(raw_dy) > 1e-9) ) if starts_line: groups.append([tspan]) else: groups[-1].append(tspan) return groups def _normalized_text(value: str) -> str: return " ".join(value.split()) def _text_body_segments(metadata: ET.Element) -> list[list[str]] | None: if metadata.get("data-pptx-encoding") != "base64": return None try: payload = base64.b64decode((metadata.text or "").strip(), validate=True) text_body = ET.fromstring(payload) except (ValueError, ET.ParseError): return None paragraphs: list[list[str]] = [] for paragraph in text_body: if _local_name(paragraph.tag) != "p": continue segments = [""] for child in paragraph: tag = _local_name(child.tag) if tag == "br": segments.append("") elif tag in {"r", "fld"}: segments[-1] += "".join( node.text or "" for node in child.iter() if _local_name(node.tag) == "t" ) paragraphs.append(segments) return paragraphs def _mark_explicit_breaks( authored_element: ET.Element, source_element: ET.Element, ) -> None: metadata = next( ( child for child in source_element if _local_name(child.tag) == "metadata" and child.get("data-pptx-part") == "txbody" ), None, ) if metadata is None: return paragraphs = _text_body_segments(metadata) if paragraphs is None: return authored_texts = [ child for child in authored_element if _local_name(child.tag) == "text" ] if len(authored_texts) != len(paragraphs): return pending: list[ET.Element] = [] for text, segments in zip(authored_texts, paragraphs): groups = _line_groups(text) if not groups: if len(segments) > 1: return continue line_texts = [ _normalized_text( "".join(text for node in group for text in node.itertext()) ) for group in groups ] cursor = 0 for segment_index, segment in enumerate(segments): target = _normalized_text(segment) combined = "" start = cursor while cursor < len(line_texts): combined = _normalized_text(" ".join((combined, line_texts[cursor]))) cursor += 1 if combined == target: break if target and not target.startswith(combined): return if combined != target: return if segment_index > 0 and start < len(groups): pending.append(groups[start][0]) if cursor != len(groups): return for tspan in pending: tspan.set("data-paragraph-soft-break", "0") def _annotate_unchanged_explicit_text_breaks( root: ET.Element, document: AuthoringDocument, excluded_refs: set[str], ) -> None: """Recover native a:br semantics without restoring a stale txBody.""" if not excluded_refs: return source_root = _parse_svg(document.source_path) for element in root.iter(): source_ref = element.get(SOURCE_REF_ATTRIBUTE) record = document.source_refs.get(source_ref or "") if source_ref not in excluded_refs or record is None: continue actual_hash = semantic_subtree_sha256( element, ignored_attributes=frozenset({SOURCE_REF_ATTRIBUTE}), ) if actual_hash != record.initial_authoring_subtree_sha256: continue _mark_explicit_breaks( element, _source_element(source_root, record.source_path), ) def _prepared_document( document: AuthoringDocument, *, excluded_refs: set[str], ) -> tuple[ET.Element, RestorationStats]: root = _parse_svg(document.authoring_path) _absolutize_local_hrefs(root, document.authoring_path.parent) stats = _rehydrate_tree(root, document, excluded_refs=excluded_refs) _annotate_unchanged_explicit_text_breaks(root, document, excluded_refs) return root, stats def _safe_prefix(value: str) -> str: normalized = _SAFE_KEY_RE.sub("-", value).strip("-.") return normalized or "node" def _namespace_ids(root: ET.Element, prefix: str) -> None: mapping: dict[str, str] = {} for element in root.iter(): element_id = element.get("id") if element_id: mapping[element_id] = f"{prefix}{_safe_prefix(element_id)}" for element in root.iter(): element_id = element.get("id") if element_id: element.set("id", mapping[element_id]) for name, value in list(element.attrib.items()): if name == "id": continue if value.startswith("#") and value[1:] in mapping: element.set(name, f"#{mapping[value[1:]]}") continue rewritten = _URL_REFERENCE_RE.sub( lambda match: f"url(#{mapping.get(match.group(2), match.group(2))})", value, ) if name.rsplit("}", 1)[-1] in {"aria-describedby", "aria-labelledby"}: rewritten = " ".join(mapping.get(token, token) for token in rewritten.split()) element.set(name, rewritten) if _local_name(element.tag) == "style" and element.text: element.text = _CSS_ID_RE.sub( lambda match: f"#{mapping.get(match.group(1), match.group(1))}", element.text, ) def _parse_style(value: str | None) -> dict[str, str]: declarations: dict[str, str] = {} for raw in (value or "").split(";"): if ":" not in raw: continue name, declaration = raw.split(":", 1) if name.strip() and declaration.strip(): declarations[name.strip().lower()] = declaration.strip() return declarations def _style_text(declarations: dict[str, str]) -> str: return ";".join(f"{name}:{value}" for name, value in declarations.items()) def _hidden(element: ET.Element) -> bool: style = _parse_style(element.get("style")) display = (element.get("display") or style.get("display") or "").strip().lower() visibility = ( element.get("visibility") or style.get("visibility") or "" ).strip().lower() opacity = (element.get("opacity") or style.get("opacity") or "").strip() if display == "none" or visibility in {"hidden", "collapse"}: return True try: return bool(opacity) and float(opacity) <= 0 except ValueError: return False def _paint_value(element: ET.Element, name: str) -> str | None: value = element.get(name) if value is not None: return value.strip().lower() return _parse_style(element.get("style")).get(name) def _visible_leaf(element: ET.Element) -> bool: if _hidden(element): return False tag = _local_name(element.tag) if tag in _NON_VISUAL_TAGS: return False if tag in {"g", "svg", "a"}: return any(_visible_leaf(child) for child in element) if tag in {"rect", "image", "foreignObject"}: for dimension in ("width", "height"): raw = element.get(dimension) if raw is not None: try: if float(raw) <= 0: return False except ValueError: pass return True if tag in {"line", "polyline"}: stroke = _paint_value(element, "stroke") return stroke not in {None, "none", "transparent"} if tag in {"path", "polygon", "circle", "ellipse"}: fill = _paint_value(element, "fill") stroke = _paint_value(element, "stroke") return not ( fill in {"none", "transparent"} and stroke in {None, "none", "transparent"} ) if tag == "text": return bool("".join(element.itertext()).strip()) return True def _merge_group_inheritance(parent: ET.Element, child: ET.Element) -> None: for name in _INHERITED_PRESENTATION_ATTRIBUTES: if parent.get(name) is not None and child.get(name) is None: child.set(name, parent.get(name, "")) parent_style = _parse_style(parent.get("style")) child_style = _parse_style(child.get("style")) if parent_style: merged = dict(parent_style) merged.update(child_style) child.set("style", _style_text(merged)) parent_transform = (parent.get("transform") or "").strip() child_transform = (child.get("transform") or "").strip() if parent_transform: child.set( "transform", " ".join(item for item in (parent_transform, child_transform) if item), ) def _flatten_fixed_group(group: ET.Element, *, context: str) -> list[ET.Element]: visible_children = [child for child in group if _visible_leaf(child)] group_style = _parse_style(group.get("style")) aggregate_attrs = { name for name in _AGGREGATE_GROUP_ATTRIBUTES if group.get(name) is not None } aggregate_attrs.update( name for name in _AGGREGATE_GROUP_ATTRIBUTES if name in group_style ) if len(visible_children) > 1 and aggregate_attrs: raise MirrorMaterializationError( f"{context} cannot expand a multi-child group with aggregate effect(s): " + ", ".join(sorted(aggregate_attrs)) ) atoms: list[ET.Element] = [] for child in visible_children: child_style = _parse_style(child.get("style")) conflicting_aggregate_attrs = { name for name in aggregate_attrs if child.get(name) is not None or name in child_style } if conflicting_aggregate_attrs: raise MirrorMaterializationError( f"{context} cannot collapse nested aggregate effect(s): " + ", ".join(sorted(conflicting_aggregate_attrs)) ) item = copy.deepcopy(child) _merge_group_inheritance(group, item) for name in aggregate_attrs: if group.get(name) is not None: item.set(name, group.get(name, "")) if _local_name(item.tag) == "g": atoms.extend(_flatten_fixed_group(item, context=context)) else: atoms.append(item) if len(atoms) == 1: atom = atoms[0] for name in ("data-pptx-frame", "data-pptx-object", "data-pptx-prst"): if group.get(name) is not None and atom.get(name) is None: atom.set(name, group.get(name, "")) return atoms def _flatten_fixed_text_atoms(atoms: Iterable[ET.Element]) -> list[ET.Element]: flattened: list[ET.Element] = [] for atom in atoms: if _local_name(atom.tag) != "text": flattened.append(atom) continue scratch = ET.Element(f"{{{SVG_NS}}}svg") scratch.append(atom) tree = ET.ElementTree(scratch) flatten_text_with_tspans(tree, merge_paragraphs=False) flattened.extend(list(scratch)) return flattened def _fixed_atoms( root: ET.Element, *, scope: str, key: str, placeholder_source_refs: set[str] | None = None, ) -> list[ET.Element]: atoms: list[ET.Element] = [] serial = 0 placeholder_source_refs = placeholder_source_refs or set() for child in root: tag = _local_name(child.tag) if ( tag in _NON_VISUAL_TAGS or child.get("data-ph-type") is not None or child.get(SOURCE_REF_ATTRIBUTE) in placeholder_source_refs ): continue if not _visible_leaf(child): continue if tag == "g": expanded = _flatten_fixed_group( child, context=f"{scope} {key} element {child.get('id') or ''}", ) else: expanded = [copy.deepcopy(child)] expanded = _flatten_fixed_text_atoms(expanded) source_ref = child.get(SOURCE_REF_ATTRIBUTE) source_token = source_ref.split(":", 1)[-1] if source_ref else str(serial + 1) for part, atom in enumerate(expanded, start=1): serial += 1 atom.set( "id", f"{scope}-{_safe_prefix(key)}-{_safe_prefix(source_token)}-{part}", ) atom.set("data-pptx-layer", scope) atom.set("data-pptx-editable", "false") atom.attrib.pop("data-ph-type", None) atoms.append(atom) return atoms def _frame(element: ET.Element | None) -> tuple[float, float, float, float] | None: if element is None: return None raw = (element.get("data-pptx-frame") or "").replace(",", " ").split() if len(raw) != 4: return None try: values = tuple(float(item) for item in raw) except ValueError: return None if not all(math.isfinite(item) for item in values) or values[2] <= 0 or values[3] <= 0: return None return values def _element_by_ref(root: ET.Element, source_ref: str) -> ET.Element | None: matches = [ element for element in root.iter() if element.get(SOURCE_REF_ATTRIBUTE) == source_ref ] if len(matches) > 1: raise MirrorMaterializationError(f"Duplicate source ref in document: {source_ref}") return matches[0] if matches else None def _placeholder_guide_by_semantic( root: ET.Element, semantic_role: str, ) -> ET.Element | None: matches = [] for element in root.iter(): placeholder_type = element.get("data-ph-type") if not placeholder_type: continue if _semantic_role({"type": placeholder_type}) == semantic_role: matches.append(element) if len(matches) == 1: return matches[0] if matches or semantic_role != "object": return None body_matches = [ element for element in root.iter() if element.get("data-ph-type") == "body" ] return body_matches[0] if len(body_matches) == 1 else None def _placeholder_idx(raw: object) -> int | None: if raw is None: return None try: value = int(str(raw)) except ValueError as exc: raise MirrorMaterializationError(f"Invalid placeholder idx: {raw!r}") from exc if value < 0: raise MirrorMaterializationError(f"Placeholder idx must be non-negative: {raw!r}") return value def _semantic_role(placeholder: dict[str, Any]) -> str: role = str(placeholder.get("semanticRole") or "").strip() placeholder_type = str(placeholder.get("type") or "").strip() mapping = { "ctrTitle": "title", "title": "title", "subTitle": "subtitle", "body": "body", "dt": "date", "ftr": "footer", "sldNum": "slide-number", "pic": "picture", "chart": "chart", "tbl": "table", "media": "media", "obj": "object", } normalized = role or mapping.get(placeholder_type, "object") if normalized not in { "title", "subtitle", "body", "picture", "chart", "table", "object", "media", "date", "footer", "slide-number", }: raise MirrorMaterializationError( f"Unsupported placeholder semantic role: {normalized!r}" ) return normalized def _placeholder_match( candidate: dict[str, Any], target: dict[str, Any], ) -> bool: candidate_idx = _placeholder_idx(candidate.get("idx")) target_idx = _placeholder_idx(target.get("idx")) if candidate_idx == target_idx and _semantic_role(candidate) == _semantic_role(target): return True return ( candidate_idx == target_idx and str(candidate.get("type") or "") == str(target.get("type") or "") ) def _placeholder_with_semantic_fallback( candidates: Iterable[dict[str, Any]], target: dict[str, Any], ) -> dict[str, Any] | None: candidate_list = list(candidates) exact = [item for item in candidate_list if _placeholder_match(item, target)] if len(exact) == 1: return exact[0] if len(exact) > 1: raise MirrorMaterializationError( f"Ambiguous placeholder identity for semantic role {_semantic_role(target)!r}" ) semantic = [ item for item in candidate_list if _semantic_role(item) == _semantic_role(target) ] return semantic[0] if len(semantic) == 1 else None def _native_geometry(placeholder: dict[str, Any]) -> tuple[float, float, float, float] | None: geometry = placeholder.get("geometry") if not isinstance(geometry, dict): return None try: values = tuple(float(geometry[name]) for name in ("x", "y", "width", "height")) except (KeyError, TypeError, ValueError): return None if not all(math.isfinite(item) for item in values) or values[2] <= 0 or values[3] <= 0: return None return values def _slot_plans( layout: dict[str, Any], layout_root: ET.Element, source_slides: list[dict[str, Any]], slide_roots: dict[int, ET.Element], master: dict[str, Any], master_root: ET.Element, ) -> list[SlotPlan]: plans: list[SlotPlan] = [] placeholders = _require_list( layout.get("placeholders", []), context=f"layout {layout.get('key')} placeholders", ) master_placeholders = _require_list( master.get("placeholders", []), context=f"master {master.get('key')} placeholders", ) for raw in placeholders: if not isinstance(raw, dict): raise MirrorMaterializationError( f"Layout {layout.get('key')} placeholder must be an object" ) shape_id = _require_string( raw.get("shapeId"), context=f"layout {layout.get('key')} placeholder shapeId", ) bounds = _frame(_element_by_ref(layout_root, f"layout:{shape_id}")) if bounds is None: for slide in source_slides: matching = next( ( item for item in slide.get("placeholders", []) if isinstance(item, dict) and _placeholder_match(item, raw) ), None, ) if matching is None: continue slide_element = _element_by_ref( slide_roots[int(slide["index"])], f"slide:{matching['shapeId']}", ) bounds = _frame(slide_element) or _native_geometry(matching) if bounds is not None: break if bounds is None: matching_master = _placeholder_with_semantic_fallback( (item for item in master_placeholders if isinstance(item, dict)), raw, ) if matching_master is not None: master_element = _element_by_ref( master_root, f"master:{matching_master['shapeId']}", ) bounds = _frame(master_element) or _native_geometry(matching_master) else: bounds = _frame( _placeholder_guide_by_semantic(master_root, _semantic_role(raw)) ) bounds = bounds or _native_geometry(raw) if bounds is None: raise MirrorMaterializationError( f"Layout {layout.get('key')!r} placeholder {shape_id!r} has no " "positive deterministic bounds in Layout, Slide, Master, or native facts" ) plans.append(SlotPlan( slot_id=f"slot-{_safe_prefix(str(layout['key']))}-{_safe_prefix(shape_id)}", semantic_role=_semantic_role(raw), placeholder_type=(str(raw.get("type")) if raw.get("type") is not None else None), idx=_placeholder_idx(raw.get("idx")), shape_id=shape_id, bounds=bounds, )) effective_indices = [plan.idx if plan.idx is not None else 0 for plan in plans] if len(effective_indices) != len(set(effective_indices)): raise MirrorMaterializationError( f"Layout {layout.get('key')!r} has duplicate effective placeholder idx values" ) return plans def _copy_text_carrier(source: ET.Element | None) -> ET.Element | None: if source is None: return None texts = [element for element in source.iter() if _local_name(element.tag) == "text"] if not texts: return None if len(texts) == 1: carrier = copy.deepcopy(texts[0]) else: carrier = ET.Element(f"{{{SVG_NS}}}text", dict(texts[0].attrib)) previous_y = _optional_float(texts[0].get("y")) first_output = True for text_index, text in enumerate(texts): text_y = _optional_float(text.get("y")) text_children = [ child for child in text if _local_name(child.tag) == "tspan" ] if text_children: for child_index, child in enumerate(text_children): tspan = copy.deepcopy(child) tspan.attrib.pop("y", None) if first_output: tspan.attrib.pop("x", None) elif child_index == 0 and text_y is not None and previous_y is not None: tspan.set("x", text.get("x", texts[0].get("x", "0"))) tspan.set("dy", format_coordinate(text_y - previous_y)) if text_index > 0 and child_index == 0: tspan.set("data-paragraph-soft-break", "0") carrier.append(tspan) first_output = False elif text.text: attrs: dict[str, str] = {} if not first_output and text_y is not None and previous_y is not None: attrs["x"] = text.get("x", texts[0].get("x", "0")) attrs["dy"] = format_coordinate(text_y - previous_y) if text_index > 0: attrs["data-paragraph-soft-break"] = "0" tspan = ET.SubElement(carrier, f"{{{SVG_NS}}}tspan", attrs) tspan.text = text.text first_output = False if text_y is not None: previous_y = text_y carrier.attrib.pop("id", None) _normalize_mergeable_tspans(carrier) source_frame = _frame(source) if source_frame is not None: carrier.set( "data-pptx-frame", " ".join(format_coordinate(value) for value in source_frame), ) carrier.set("data-pptx-carrier", "true") return carrier def _visible_placeholder_decoration(element: ET.Element) -> bool: """Return whether one non-text placeholder subtree paints any pixels.""" if _hidden(element): return False tag = _local_name(element.tag) if tag in _NON_VISUAL_TAGS or tag == "text": return False if tag in {"g", "svg", "a"}: return any(_visible_placeholder_decoration(child) for child in element) if tag in {"image", "foreignObject", "use"}: return _visible_leaf(element) if tag in {"line", "polyline"}: stroke = _paint_value(element, "stroke") return stroke not in {None, "none", "transparent"} if tag in {"rect", "path", "polygon", "circle", "ellipse"}: fill = _paint_value(element, "fill") stroke = _paint_value(element, "stroke") filter_value = _paint_value(element, "filter") return ( fill not in {"none", "transparent"} or stroke not in {None, "none", "transparent"} or filter_value not in {None, "none"} ) return False def _placeholder_decorations(source: ET.Element | None) -> list[ET.Element]: """Copy direct visual children that decorate one text placeholder.""" if source is None: return [] decorations = [ copy.deepcopy(child) for child in source if _visible_placeholder_decoration(child) ] for decoration in decorations: _merge_group_inheritance(source, decoration) _copy_placeholder_native_attributes(source, decorations) return decorations def _copy_placeholder_native_attributes( source: ET.Element, geometries: list[ET.Element], ) -> None: """Carry one logical placeholder geometry's native identity to its leaf.""" if len(geometries) == 1: decoration = geometries[0] native_attributes = { "data-pptx-frame", "data-pptx-geometry-kind", "data-pptx-geometry-reason", "data-pptx-geometry-sha256", "data-pptx-geometry-status", "data-pptx-object", "data-pptx-prst", } for name, value in source.attrib.items(): if name in native_attributes or name.startswith("data-pptx-av-"): if decoration.get(name) is None: decoration.set(name, value) def _placeholder_geometry(source: ET.Element | None) -> list[ET.Element]: """Copy direct source geometry even when it has no visible local paint.""" if source is None: return [] geometry_tags = { "circle", "ellipse", "line", "path", "polygon", "polyline", "rect", } geometries = [ copy.deepcopy(child) for child in source if _local_name(child.tag) in geometry_tags and not _hidden(child) ] for geometry in geometries: _merge_group_inheritance(source, geometry) _copy_placeholder_native_attributes(source, geometries) return geometries def _apply_placeholder_paint(source: ET.Element, target: ET.Element) -> None: """Apply inherited paint without replacing Slide-owned geometry.""" paint_attributes = { "color", "fill", "fill-opacity", "fill-rule", "filter", "mix-blend-mode", "opacity", "paint-order", "shape-rendering", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "vector-effect", } source_style = _parse_style(source.get("style")) target_style = _parse_style(target.get("style")) for name in paint_attributes: value = source.get(name) if value is None: value = source_style.get(name) if value is None: continue target.set(name, value) target_style.pop(name, None) if target_style: target.set("style", _style_text(target_style)) else: target.attrib.pop("style", None) def _remap_placeholder_decorations( decorations: list[ET.Element], from_frame: tuple[float, float, float, float] | None, to_frame: tuple[float, float, float, float] | None, ) -> None: """Map inherited decoration geometry onto the effective Slide frame.""" if from_frame is None or to_frame is None: return if all( math.isclose(source, target, rel_tol=1e-9, abs_tol=1e-9) for source, target in zip(from_frame, to_frame) ): return from_x, from_y, from_width, from_height = from_frame to_x, to_y, to_width, to_height = to_frame scale_x = to_width / from_width scale_y = to_height / from_height translate_x = to_x - from_x * scale_x translate_y = to_y - from_y * scale_y frame_transform = "matrix({})".format( " ".join(( _format_number(scale_x), "0", "0", _format_number(scale_y), format_coordinate(translate_x), format_coordinate(translate_y), )) ) for decoration in decorations: existing = (decoration.get("transform") or "").strip() decoration.set( "transform", " ".join(value for value in (frame_transform, existing) if value), ) def _resolved_placeholder_decorations( source: ET.Element | None, layout_guide: ET.Element | None, master_guide: ET.Element | None, ) -> list[ET.Element]: """Resolve text-placeholder decoration through Slide/Layout/Master.""" local = _placeholder_decorations(source) local_geometry = ( source is not None and source.get("data-pptx-placeholder-local-geometry") == "true" ) if local and local_geometry: return local inherited: list[ET.Element] = [] inherited_guide: ET.Element | None = None for candidate in (layout_guide, master_guide): inherited = _placeholder_decorations(candidate) if inherited: inherited_guide = candidate break if not inherited: return local geometry_tags = { "circle", "ellipse", "line", "path", "polygon", "polyline", "rect", } if local and not local_geometry: if ( len(local) != 1 or len(inherited) != 1 or _local_name(local[0].tag) not in geometry_tags or _local_name(inherited[0].tag) not in geometry_tags ): return local _apply_placeholder_paint(local[0], inherited[0]) if ( source is None or not local_geometry or len(inherited) != 1 ): _remap_placeholder_decorations( inherited, _frame(inherited_guide), _frame(source), ) return inherited source_geometry = _placeholder_geometry(source) inherited_tag = _local_name(inherited[0].tag) if not source_geometry or inherited_tag not in geometry_tags: return inherited for geometry in source_geometry: _apply_placeholder_paint(inherited[0], geometry) return source_geometry def _normalize_mergeable_tspans(text: ET.Element) -> None: """Mark the first line of a positional text block for paragraph merging.""" tspans = [child for child in text if _local_name(child.tag) == "tspan"] if len(tspans) < 2 or not any( any(tspan.get(name) is not None for name in ("x", "y", "dy")) for tspan in tspans ): return first = tspans[0] if first.get("x") is None and text.get("x") is not None: first.set("x", text.get("x", "")) if first.get("y") is None and first.get("dy") is None: first.set("dy", "0") def _blank_text_carrier( plan: SlotPlan, layout_guide: ET.Element | None, master_guide: ET.Element | None, ) -> ET.Element: carrier = _copy_text_carrier(layout_guide) if carrier is None: carrier = _copy_text_carrier(master_guide) if carrier is None: x, y, _width, height = plan.bounds carrier = ET.Element( f"{{{SVG_NS}}}text", { "x": format_coordinate(x), "y": format_coordinate(y + min(height, 24)), "font-size": "18", "fill": "#000000", "data-pptx-carrier": "true", }, ) carrier.text = None for child in list(carrier): carrier.remove(child) if carrier.get("data-pptx-frame") is None: carrier.set( "data-pptx-frame", " ".join(format_coordinate(value) for value in plan.bounds), ) carrier.set("data-pptx-carrier", "true") return carrier def _blank_image_carrier(plan: SlotPlan) -> ET.Element: x, y, width, height = plan.bounds return ET.Element( f"{{{SVG_NS}}}image", { "x": format_coordinate(x), "y": format_coordinate(y), "width": format_coordinate(width), "height": format_coordinate(height), "href": TRANSPARENT_PIXEL_DATA_URI, "preserveAspectRatio": "none", "data-pptx-carrier": "true", }, ) def _format_number(value: float) -> str: return f"{value:.8f}".rstrip("0").rstrip(".") or "0" def _optional_float(value: str | None) -> float | None: if value is None: return None try: parsed = float(value) except ValueError: return None return parsed if math.isfinite(parsed) else None def _slot_wrapper( plan: SlotPlan, source: ET.Element | None, *, layout_guide: ET.Element | None, master_guide: ET.Element | None, ) -> tuple[ET.Element, list[ET.Element]]: wrapper = ET.Element( f"{{{SVG_NS}}}g", { "id": plan.slot_id, "data-pptx-placeholder": plan.semantic_role, "data-pptx-bounds": " ".join( format_coordinate(item) for item in plan.bounds ), }, ) if plan.idx is not None: wrapper.set("data-pptx-idx", str(plan.idx)) extras: list[ET.Element] = [] if plan.semantic_role in { "title", "subtitle", "body", "date", "footer", "slide-number", }: carrier = _copy_text_carrier(source) if carrier is None: carrier = _blank_text_carrier(plan, layout_guide, master_guide) extras = _resolved_placeholder_decorations( source, layout_guide, master_guide, ) for extra in extras: extra.attrib.pop("data-pptx-carrier", None) wrapper.append(carrier) return wrapper, extras if plan.semantic_role == "object": proxy_source = source if proxy_source is None: proxy_source = layout_guide if proxy_source is None: proxy_source = master_guide if proxy_source is None: raise MirrorMaterializationError( f"Object slot {plan.slot_id!r} has no visible proxy source" ) visible = [copy.deepcopy(child) for child in proxy_source if _visible_leaf(child)] if not visible: raise MirrorMaterializationError( f"Object slot {plan.slot_id!r} has no visible proxy content" ) wrapper.set("data-pptx-binding", "proxy") for child in visible: wrapper.append(child) return wrapper, extras expected_tags = { "picture": {"image", "svg"}, "media": {"image", "svg"}, "chart": {"g"}, "table": {"g"}, }[plan.semantic_role] carrier_source = source or layout_guide candidates = [ element for element in (carrier_source.iter() if carrier_source is not None else []) if _local_name(element.tag) in expected_tags and _visible_leaf(element) ] if ( not candidates and source is None and layout_guide is not None and plan.semantic_role in {"picture", "media"} ): wrapper.append(_blank_image_carrier(plan)) return wrapper, extras if len(candidates) != 1: raise MirrorMaterializationError( f"Slot {plan.slot_id!r} requires exactly one visible " f"{plan.semantic_role} carrier, found {len(candidates)}" ) carrier = copy.deepcopy(candidates[0]) if plan.semantic_role in {"chart", "table"}: marker = carrier.get("data-pptx-replace-with") if marker != plan.semantic_role: raise MirrorMaterializationError( f"Slot {plan.slot_id!r} {plan.semantic_role} carrier lacks native marker" ) carrier.set("data-pptx-carrier", "true") wrapper.append(carrier) return wrapper, extras def _strip_source_refs(root: ET.Element) -> None: for element in root.iter(): element.attrib.pop(SOURCE_REF_ATTRIBUTE, None) element.attrib.pop("data-ph-type", None) def _is_full_canvas_rect( element: ET.Element, width: float, height: float, ) -> bool: if _local_name(element.tag) != "rect": return False try: values = tuple( float(element.get(name, default)) for name, default in ( ("x", "0"), ("y", "0"), ("width", "0"), ("height", "0"), ) ) except ValueError: return False fill = _paint_value(element, "fill") return ( math.isclose(values[0], 0, abs_tol=0.01) and math.isclose(values[1], 0, abs_tol=0.01) and math.isclose(values[2], width, abs_tol=0.01) and math.isclose(values[3], height, abs_tol=0.01) and fill not in {None, "none", "transparent"} ) def _non_visual_nodes(roots: Iterable[ET.Element]) -> tuple[ET.Element | None, list[ET.Element]]: definitions: list[ET.Element] = [] styles: list[ET.Element] = [] for root in roots: for child in root: tag = _local_name(child.tag) if tag == "defs": definitions.extend(copy.deepcopy(list(child))) elif tag == "style": styles.append(copy.deepcopy(child)) defs_node = ET.Element(f"{{{SVG_NS}}}defs") if definitions else None if defs_node is not None: for definition in definitions: defs_node.append(definition) return defs_node, styles def _slide_placeholder_map(slide: dict[str, Any]) -> dict[str, dict[str, Any]]: return { f"slide:{item['shapeId']}": item for item in slide.get("placeholders", []) if isinstance(item, dict) and item.get("shapeId") is not None } def _matching_slide_placeholder( slide: dict[str, Any], layout_placeholder: dict[str, Any], ) -> dict[str, Any] | None: matches = [ item for item in slide.get("placeholders", []) if isinstance(item, dict) and _placeholder_match(item, layout_placeholder) ] if len(matches) > 1: raise MirrorMaterializationError( f"Slide {slide.get('index')} ambiguously matches Layout placeholder " f"{layout_placeholder.get('shapeId')}" ) return matches[0] if matches else None def _compose_template( *, native: dict[str, Any], master: dict[str, Any], layout: dict[str, Any], master_root: ET.Element, layout_root: ET.Element, slide: dict[str, Any] | None, slide_root: ET.Element | None, slot_plans: list[SlotPlan], ) -> ET.Element: width = float(native["slideSize"]["width_px"]) height = float(native["slideSize"]["height_px"]) root = ET.Element( f"{{{SVG_NS}}}svg", { "version": "1.1", "width": format_coordinate(width), "height": format_coordinate(height), "viewBox": ( f"0 0 {format_coordinate(width)} {format_coordinate(height)}" ), "data-pptx-master": str(master["key"]), "data-pptx-master-name": str(master["name"]), "data-pptx-layout": str(layout["key"]), "data-pptx-layout-name": str(layout["name"]), "data-pptx-show-master-shapes": str( _require_boolean( layout.get("showMasterShapes"), context=f"Layout {layout.get('key')!r} showMasterShapes", ) ).lower(), "data-pptx-show-inherited-shapes": str( _require_boolean( slide.get("showInheritedShapes"), context=f"Slide {slide.get('index')} showInheritedShapes", ) if slide else True ).lower(), }, ) roots = [master_root, layout_root] if slide_root is not None: roots.append(slide_root) defs, styles = _non_visual_nodes(roots) if defs is not None: root.append(defs) for style in styles: root.append(style) master_atoms = _fixed_atoms(master_root, scope="master", key=str(master["key"])) layout_placeholder_refs = { f"layout:{item['shapeId']}" for item in layout.get("placeholders", []) if isinstance(item, dict) and item.get("shapeId") is not None } layout_atoms = _fixed_atoms( layout_root, scope="layout", key=str(layout["key"]), placeholder_source_refs=layout_placeholder_refs, ) master_backgrounds = [ atom for atom in master_atoms if _is_full_canvas_rect(atom, width, height) ] layout_backgrounds = [ atom for atom in layout_atoms if _is_full_canvas_rect(atom, width, height) ] master_shapes = [atom for atom in master_atoms if atom not in master_backgrounds] layout_shapes = [atom for atom in layout_atoms if atom not in layout_backgrounds] slide_backgrounds: list[ET.Element] = [] slide_content: list[ET.Element] = [] source_placeholder_elements: dict[str, ET.Element] = {} if slide_root is not None and slide is not None: placeholder_refs = _slide_placeholder_map(slide) for index, child in enumerate(slide_root): if _local_name(child.tag) in _NON_VISUAL_TAGS: continue source_ref = child.get(SOURCE_REF_ATTRIBUTE) if source_ref in placeholder_refs: source_placeholder_elements[source_ref] = child continue item = copy.deepcopy(child) if _is_full_canvas_rect(item, width, height): item.set("id", item.get("id") or f"slide-{slide['index']}-background") item.set("data-pptx-layer", "slide") item.set("data-pptx-editable", "false") slide_backgrounds.append(item) else: item.set("id", item.get("id") or f"slide-{slide['index']}-node-{index + 1}") slide_content.append(item) if len(slide_backgrounds) > 1: raise MirrorMaterializationError( f"Slide {slide['index']} has more than one full-canvas solid background" ) layout_placeholders = { str(item["shapeId"]): item for item in layout.get("placeholders", []) if isinstance(item, dict) and item.get("shapeId") is not None } master_placeholders = [ item for item in master.get("placeholders", []) if isinstance(item, dict) ] slots: list[ET.Element] = [] slot_extras: list[ET.Element] = [] for plan in slot_plans: layout_placeholder = layout_placeholders[plan.shape_id] layout_guide = _element_by_ref(layout_root, f"layout:{plan.shape_id}") master_placeholder = _placeholder_with_semantic_fallback( master_placeholders, layout_placeholder, ) master_guide = ( _element_by_ref(master_root, f"master:{master_placeholder['shapeId']}") if master_placeholder is not None else _placeholder_guide_by_semantic( master_root, plan.semantic_role, ) ) source_element = None if slide is not None: slide_placeholder = _matching_slide_placeholder(slide, layout_placeholder) if slide_placeholder is not None: source_element = source_placeholder_elements.get( f"slide:{slide_placeholder['shapeId']}" ) if source_element is None: raise MirrorMaterializationError( f"Slide {slide['index']} placeholder {slide_placeholder['shapeId']} " "is missing from its authoring SVG" ) wrapper, extras = _slot_wrapper( plan, source_element, layout_guide=layout_guide, master_guide=master_guide, ) slots.append(wrapper) slot_extras.extend(extras) for element in ( *master_backgrounds, *layout_backgrounds, *slide_backgrounds, *master_shapes, *layout_shapes, *slide_content, *slot_extras, *slots, ): _strip_source_refs(element) root.append(element) _strip_source_refs(root) return root def _page_type(slide: dict[str, Any]) -> str: raw = str(slide.get("pageType") or "").strip().lower() mapping = { "cover_candidate": "cover", "toc_candidate": "toc", "chapter_candidate": "chapter", "content_candidate": "content", "ending_candidate": "ending", } return mapping.get(raw, "content") def _serialize_svg(root: ET.Element) -> bytes: payload = ET.tostring(root, encoding="utf-8", xml_declaration=False) return payload if payload.endswith(b"\n") else payload + b"\n" def _refresh_preset_preview_hashes(root: ET.Element) -> None: """Rebind imported preset guards after deterministic SVG ID namespacing.""" for element in root.iter(): if ( _local_name(element.tag) == "g" and element.get("data-pptx-object") in {"shape", "connector"} and element.get("data-pptx-prst") is not None and element.get("data-pptx-preview-sha256") is not None ): fingerprint = svg_preset_preview_fingerprint(element) element.set("data-pptx-preview-sha256", fingerprint) for descendant in element.iter(): if descendant.get("data-pptx-preview-sha256") is not None: descendant.set("data-pptx-preview-sha256", fingerprint) def _sanitize_connector_references(root: ET.Element) -> int: """Drop endpoint bindings whose native target is absent from this SVG tree.""" identities = { ( element.get("data-pptx-shape-scope"), element.get("data-pptx-shape-id"), ) for element in root.iter() if element.get("data-pptx-shape-scope") and element.get("data-pptx-shape-id") } detached: set[tuple[str, str, str]] = set() for element in root.iter(): connector_scope = element.get("data-pptx-shape-scope") or "unknown" connector_id = element.get("data-pptx-shape-id") or element.get("id") or "unknown" for endpoint in ("start", "end"): target_id_attr = f"data-pptx-{endpoint}-shape-id" target_id = element.get(target_id_attr) if target_id is None: continue target_scope = element.get( f"data-pptx-{endpoint}-shape-scope", connector_scope, ) if (target_scope, target_id) in identities: continue detached.add((connector_scope, connector_id, endpoint)) for suffix in ("shape-id", "shape-scope", "site"): element.attrib.pop(f"data-pptx-{endpoint}-{suffix}", None) return len(detached) def _local_asset_path(value: str, base_dir: Path) -> Path | None: if not value or value.startswith("#"): return None parsed = urlsplit(value) if parsed.scheme == "file": return Path(unquote(parsed.path)).resolve() if parsed.scheme or parsed.netloc or not parsed.path: return None path = Path(unquote(parsed.path)) return path.resolve() if path.is_absolute() else (base_dir / path).resolve() def _detected_bitmap_extension(path: Path) -> str | None: header = path.read_bytes()[:32] if header.startswith(b"\x89PNG\r\n\x1a\n"): return ".png" if header.startswith(b"\xff\xd8\xff"): return ".jpg" if header.startswith((b"GIF87a", b"GIF89a")): return ".gif" if header.startswith(b"BM"): return ".bmp" if header.startswith((b"II*\x00", b"MM\x00*")): return ".tiff" if len(header) >= 12 and header[:4] == b"RIFF" and header[8:12] == b"WEBP": return ".webp" return None def _rewrite_packaged_assets( root: ET.Element, *, source_base: Path, final_svg_path: Path, template_workspace: Path, asset_sources: dict[Path, Path], ) -> None: for element in root.iter(): for attribute in ("href", f"{{{XLINK_NS}}}href"): value = element.get(attribute) if value is None: continue source = _local_asset_path(value, source_base) if source is None: continue if not source.is_file(): raise MirrorMaterializationError( f"Referenced local asset is missing: {source}" ) detected_extension = _detected_bitmap_extension(source) if detected_extension is not None: source_suffix = source.suffix.lower() equivalent_suffixes = ( {".jpg", ".jpeg"} if detected_extension == ".jpg" else {detected_extension} ) packaged_name = ( source.name if source_suffix in equivalent_suffixes else f"{source.stem}{detected_extension}" ) relative_target = Path("images") / packaged_name elif source.suffix.lower() in _BITMAP_EXTENSIONS: relative_target = Path("images") / source.name else: relative_target = Path("templates") / "assets" / source.name previous = asset_sources.get(relative_target) if previous is not None and _sha256_file(previous) != _sha256_file(source): raise MirrorMaterializationError( f"Asset basename collision with different content: {source.name}" ) asset_sources[relative_target] = source target = template_workspace / relative_target relative_href = os.path.relpath(target, final_svg_path.parent).replace(os.sep, "/") parsed = urlsplit(value) element.set( attribute, urlunsplit(("", "", relative_href, parsed.query, parsed.fragment)), ) def _materialize_icon( record: VectorAssetRecord, document: AuthoringDocument, ) -> tuple[ET.Element, RestorationStats]: actual_sha256 = _sha256_file(record.asset_path) if actual_sha256 != record.expected_sha256: raise MirrorMaterializationError( f"Vector asset {record.icon!r} changed during materialization; " f"expected {record.expected_sha256}, found {actual_sha256}" ) root = _parse_svg(record.asset_path) _absolutize_local_hrefs(root, record.asset_path.parent) stats = _rehydrate_tree(root, document, excluded_refs=set()) _strip_source_refs(root) return root, stats def _ensure_no_source_refs(path: Path) -> None: root = _parse_svg(path) refs = sorted(_source_ref_counts(root)) if refs: raise MirrorMaterializationError( f"Materialized file still contains source refs: {path}: {refs[:5]}" ) def _preflight_output( import_workspace: Path, template_workspace: Path, relative_files: Iterable[Path], ) -> None: import_resolved = import_workspace.resolve() output_resolved = template_workspace.resolve() try: output_resolved.relative_to(import_resolved) except ValueError: pass else: raise MirrorMaterializationError( "Template workspace must not be inside the import workspace" ) try: import_resolved.relative_to(output_resolved) except ValueError: pass else: raise MirrorMaterializationError( "Import workspace must not be inside the template workspace" ) templates_root = template_workspace / "templates" if templates_root.exists(): if not templates_root.is_dir(): raise MirrorMaterializationError( f"Template output is not a directory: {templates_root}" ) existing = sorted(path for path in templates_root.iterdir()) if existing: raise MirrorMaterializationError( f"Template output must be empty before mirror materialization: " f"{templates_root}; first entry: {existing[0].name}" ) collisions = [ template_workspace / relative for relative in relative_files if os.path.lexists(template_workspace / relative) ] if collisions: raise MirrorMaterializationError( f"Output file already exists: {collisions[0]}" ) def _nearest_existing_directory(path: Path) -> Path: candidate = path while not candidate.exists(): if candidate.parent == candidate: break candidate = candidate.parent if not candidate.is_dir(): raise MirrorMaterializationError(f"Output parent is not a directory: {candidate}") return candidate def _ensure_directory(path: Path, created: list[Path]) -> None: missing: list[Path] = [] candidate = path while not candidate.exists(): if os.path.lexists(candidate): raise MirrorMaterializationError( f"Output parent is not a directory: {candidate}" ) missing.append(candidate) candidate = candidate.parent if not candidate.is_dir(): raise MirrorMaterializationError(f"Output parent is not a directory: {candidate}") for directory in reversed(missing): directory.mkdir() created.append(directory) def _publish_files( template_workspace: Path, staged_root: Path, relative_files: list[Path], ) -> None: created_dirs: list[Path] = [] published: list[Path] = [] try: for relative in relative_files: target = template_workspace / relative _ensure_directory(target.parent, created_dirs) staging_device = staged_root.stat().st_dev for relative in relative_files: target = template_workspace / relative if target.parent.stat().st_dev != staging_device: raise MirrorMaterializationError( f"Cannot atomically publish across filesystems: {target}" ) if os.path.lexists(target): raise MirrorMaterializationError( f"Output appeared while mirror files were staged: {target}" ) for relative in relative_files: target = template_workspace / relative (staged_root / relative).replace(target) published.append(target) except (OSError, MirrorMaterializationError) as exc: rollback_errors: list[str] = [] for path in reversed(published): try: path.unlink(missing_ok=True) except OSError as rollback_exc: rollback_errors.append(f"could not remove {path}: {rollback_exc}") for directory in reversed(created_dirs): try: directory.rmdir() except OSError: pass if rollback_errors: raise MirrorMaterializationError( f"Mirror publish failed ({exc}); rollback was incomplete: " + "; ".join(rollback_errors) ) from exc raise MirrorMaterializationError(f"Mirror publish failed: {exc}") from exc def materialize_mirror_template( import_workspace: Path, template_workspace: Path, ) -> dict[str, Any]: """Validate one Type A import graph and publish its mirror SVG contract.""" authoring_root, documents = _load_authoring_documents(import_workspace) vector_assets = _load_vector_assets(import_workspace, documents) referenced_icons = _validate_source_ref_closure(documents, vector_assets) native = _load_native_graph(import_workspace) inheritance = _load_inheritance(import_workspace) _validate_graph_roster(native, inheritance, documents) masters = {item["key"]: item for item in native["masters"]} layouts = {item["key"]: item for item in native["layouts"]} slides = sorted(native["slides"], key=lambda item: int(item["index"])) if [int(item["index"]) for item in slides] != list(range(1, len(slides) + 1)): raise MirrorMaterializationError( "Mirror source slide indexes must be contiguous and start at 1" ) prepared_masters: dict[str, ET.Element] = {} prepared_layouts: dict[str, ET.Element] = {} prepared_slides: dict[int, ET.Element] = {} total_stats = RestorationStats() for key, master in masters.items(): document = documents[master["svgFile"]] root, stats = _prepared_document( document, excluded_refs=set(document.source_refs), ) _namespace_ids(root, f"m-{_safe_prefix(str(key))}-") prepared_masters[key] = root total_stats.merge(stats) for key, layout in layouts.items(): document = documents[layout["svgFile"]] root, stats = _prepared_document( document, excluded_refs=set(document.source_refs), ) _namespace_ids(root, f"l-{_safe_prefix(str(key))}-") prepared_layouts[key] = root total_stats.merge(stats) for slide in slides: index = int(slide["index"]) document = documents[slide["layeredSvgFile"]] placeholder_refs = { f"slide:{item['shapeId']}" for item in slide.get("placeholders", []) if isinstance(item, dict) and item.get("shapeId") is not None } root, stats = _prepared_document(document, excluded_refs=placeholder_refs) _namespace_ids(root, f"s-{index:03d}-") prepared_slides[index] = root total_stats.merge(stats) slides_by_layout: dict[str, list[dict[str, Any]]] = {key: [] for key in layouts} for slide in slides: slides_by_layout[str(slide["layoutKey"])].append(slide) plans_by_layout: dict[str, list[SlotPlan]] = {} for key, layout in layouts.items(): master_key = str(layout["masterKey"]) plans_by_layout[key] = _slot_plans( layout, prepared_layouts[key], slides_by_layout[key], prepared_slides, masters[master_key], prepared_masters[master_key], ) materialized_roots: list[tuple[Path, ET.Element]] = [] for slide in slides: index = int(slide["index"]) layout = layouts[str(slide["layoutKey"])] master = masters[str(slide["masterKey"])] filename = Path("templates") / f"{index:03d}_{_page_type(slide)}.svg" root = _compose_template( native=native, master=master, layout=layout, master_root=prepared_masters[str(master["key"])], layout_root=prepared_layouts[str(layout["key"])], slide=slide, slide_root=prepared_slides[index], slot_plans=plans_by_layout[str(layout["key"])], ) materialized_roots.append((filename, root)) unused_layouts = [ layout for key, layout in layouts.items() if not slides_by_layout[key] ] for layout in sorted(unused_layouts, key=lambda item: str(item["key"])): master = masters[str(layout["masterKey"])] filename = Path("templates") / f"layout_{layout['key']}.svg" root = _compose_template( native=native, master=master, layout=layout, master_root=prepared_masters[str(master["key"])], layout_root=prepared_layouts[str(layout["key"])], slide=None, slide_root=None, slot_plans=plans_by_layout[str(layout["key"])], ) materialized_roots.append((filename, root)) asset_sources: dict[Path, Path] = {} files: list[MaterializedFile] = [] output_roots: list[tuple[Path, ET.Element]] = [] native_payloads: dict[str, bytes] = {} native_payload_stats = NativePayloadStats() for relative_path, root in materialized_roots: final_path = template_workspace / relative_path _rewrite_packaged_assets( root, source_base=authoring_root, final_svg_path=final_path, template_workspace=template_workspace, asset_sources=asset_sources, ) total_stats.detached_connector_endpoints += _sanitize_connector_references(root) total_stats.upright_text_compensations += ( _compensate_reflected_group_text(root) ) compact_svg_tree(root, compact_native_frames=False) _refresh_preset_preview_hashes(root) try: native_payload_stats.merge( externalize_native_payloads(root, native_payloads) ) except NativePayloadError as exc: raise MirrorMaterializationError( f"Cannot externalize native payloads in {relative_path}: {exc}" ) from exc output_roots.append((relative_path, root)) for icon in sorted(referenced_icons): record = vector_assets[icon] icon_root, stats = _materialize_icon( record, documents[record.origin_document], ) total_stats.merge(stats) relative_path = Path("icons") / f"{icon}.svg" final_path = template_workspace / relative_path _rewrite_packaged_assets( icon_root, source_base=record.asset_path.parent, final_svg_path=final_path, template_workspace=template_workspace, asset_sources=asset_sources, ) total_stats.detached_connector_endpoints += _sanitize_connector_references( icon_root ) compact_svg_tree(icon_root, compact_native_frames=False) _refresh_preset_preview_hashes(icon_root) try: native_payload_stats.merge( externalize_native_payloads(icon_root, native_payloads) ) except NativePayloadError as exc: raise MirrorMaterializationError( f"Cannot externalize native payloads in {relative_path}: {exc}" ) from exc output_roots.append((relative_path, icon_root)) native_record_keys: set[str] = set() try: for _relative_path, root in output_roots: native_record_keys.update( collect_native_attribute_record_keys(root) ) native_record_ids, native_records = build_native_attribute_records( native_record_keys ) for _relative_path, root in output_roots: native_payload_stats.merge( externalize_native_attribute_records(root, native_record_ids) ) except NativePayloadError as exc: raise MirrorMaterializationError( f"Cannot externalize native attribute records: {exc}" ) from exc for relative_path, root in output_roots: files.append(MaterializedFile(relative_path, _serialize_svg(root))) execution_manifest_path = Path("templates") / TEMPLATE_EXECUTION_MANIFEST_NAME files.extend( _template_execution_manifest_files( materialized_roots, _source_import_summary(import_workspace), ) ) for relative_target, source in sorted(asset_sources.items()): files.append(MaterializedFile(relative_target, source.read_bytes())) payload_store: bytes | None = None if native_payloads or native_records: try: payload_store = serialize_native_payload_store( native_payloads, native_records, ) except NativePayloadError as exc: raise MirrorMaterializationError( f"Cannot serialize native payload store: {exc}" ) from exc files.append( MaterializedFile(PAYLOAD_STORE_RELATIVE_PATH, payload_store) ) relative_files = [item.relative_path for item in files] if len(relative_files) != len(set(relative_files)): raise MirrorMaterializationError("Materializer produced duplicate output paths") _preflight_output(import_workspace, template_workspace, relative_files) staging_parent = _nearest_existing_directory(template_workspace.parent) with tempfile.TemporaryDirectory( prefix=".mirror-template-materialize-", dir=staging_parent, ) as temporary: staged_root = Path(temporary) / "staged" for item in files: target = staged_root / item.relative_path target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(item.payload) template_paths = sorted( staged_root / relative for relative, _root in materialized_roots ) try: parse_template_slides(template_paths) except TemplateStructureError as exc: raise MirrorMaterializationError( f"Materialized structured SVG contract is invalid: {exc}" ) from exc for relative in relative_files: if relative.suffix.lower() == ".svg": staged_svg = staged_root / relative try: hydrate_native_payload_refs(_parse_svg(staged_svg), staged_svg) except NativePayloadError as exc: raise MirrorMaterializationError( f"Materialized native payload reference is invalid: " f"{relative}: {exc}" ) from exc _ensure_no_source_refs(staged_svg) _publish_files( template_workspace, staged_root, sorted(relative_files, key=lambda item: item.as_posix()), ) return { "schema": "ppt-master.mirror-materialization-report.v1", "import_workspace": str(import_workspace), "template_workspace": str(template_workspace), "source_slide_indexes": [int(item["index"]) for item in slides], "source_slide_count": len(slides), "master_count": len(masters), "layout_count": len(layouts), "unused_layout_count": len(unused_layouts), "template_svg_count": len(materialized_roots), "template_execution_manifest": execution_manifest_path.as_posix(), "template_text_slot_manifest_count": len(materialized_roots), "imported_vector_count": len(referenced_icons), "packaged_asset_count": len(asset_sources), "restoration": total_stats.as_dict(), "native_payloads": { **native_payload_stats.as_dict(), "unique_count": len(native_payloads), "unique_record_count": len(native_records), "unique_raw_bytes": sum(len(payload) for payload in native_payloads.values()), "store_bytes": len(payload_store or b""), "store_path": ( PAYLOAD_STORE_RELATIVE_PATH.as_posix() if payload_store is not None else None ), }, "files": [ item.relative_path.as_posix() for item in sorted(files, key=lambda item: item.relative_path.as_posix()) ], } def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description=( "Materialize a deterministic mirror template from one PPTX import " "workspace's layered authoring IR." ) ) parser.add_argument( "import_workspace", type=Path, help="Type A PPTX import workspace containing authoring-svg/ and native facts", ) parser.add_argument( "template_workspace", type=Path, help="Empty template workspace destination (templates/ must be absent or empty)", ) return parser def main(argv: list[str] | None = None) -> int: args = build_parser().parse_args(argv) import_workspace = args.import_workspace.resolve() template_workspace = args.template_workspace.resolve() if not import_workspace.is_dir(): print(f"Error: import workspace does not exist: {import_workspace}", file=sys.stderr) return 1 try: report = materialize_mirror_template(import_workspace, template_workspace) except (MirrorMaterializationError, OSError) as exc: print(f"Error: {exc}", file=sys.stderr) return 1 print(json.dumps(report, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())