Files
Templated-PPT-Master/runtime/ppt-master/scripts/mirror_template_materialize.py
T

2936 lines
105 KiB
Python

#!/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 \
<import_workspace> <template_workspace>
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 <svg>: {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 '<g>'}",
)
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())