198 lines
6.7 KiB
Python
198 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
PPT Master - Native Template Structure Contract
|
|
|
|
Build a portable master/layout contract from the PPTX template import manifest.
|
|
|
|
Usage:
|
|
Imported by pptx_template_import.py.
|
|
|
|
Examples:
|
|
write_native_structure_bundle(source_pptx, output_dir, manifest)
|
|
|
|
Dependencies:
|
|
None (only uses standard library)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import shutil
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
SCHEMA = "ppt-master.native-structure.v1"
|
|
SOURCE_TEMPLATE_NAME = "source_template.pptx"
|
|
CONTRACT_NAME = "native_structure.json"
|
|
|
|
|
|
def _sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as handle:
|
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def _copy_source_template(source: Path, destination: Path) -> None:
|
|
if source.resolve() == destination.resolve():
|
|
return
|
|
shutil.copy2(source, destination)
|
|
|
|
|
|
def _has_reusable_structure(manifest: dict[str, Any]) -> bool:
|
|
layouts = manifest.get("layouts", [])
|
|
masters = manifest.get("masters", [])
|
|
slides = manifest.get("slides", [])
|
|
used_layouts = [layout for layout in layouts if layout.get("usedBySlides")]
|
|
candidate_layouts = used_layouts or layouts
|
|
if any(
|
|
layout.get("backgroundAsset")
|
|
or int(layout.get("drawableShapeCount") or 0) > 0
|
|
or any(
|
|
placeholder.get("semanticRole")
|
|
not in {"date", "footer", "slide-number", "other"}
|
|
for placeholder in layout.get("placeholders", [])
|
|
)
|
|
for layout in candidate_layouts
|
|
):
|
|
return True
|
|
if any(
|
|
master.get("backgroundAsset")
|
|
or int(master.get("drawableShapeCount") or 0) > 0
|
|
or master.get("imageAssets")
|
|
for master in masters
|
|
):
|
|
return True
|
|
return any(slide.get("placeholders") for slide in slides)
|
|
|
|
|
|
def build_native_structure(
|
|
source_pptx: Path,
|
|
manifest: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
"""Build the portable source-package structure contract."""
|
|
masters = manifest.get("masters", [])
|
|
layouts = manifest.get("layouts", [])
|
|
slides = manifest.get("slides", [])
|
|
master_key_by_path = {
|
|
master["path"]: f"master_{index:02d}"
|
|
for index, master in enumerate(masters, start=1)
|
|
}
|
|
layout_key_by_path = {
|
|
layout["path"]: f"layout_{index:02d}"
|
|
for index, layout in enumerate(layouts, start=1)
|
|
}
|
|
complete_graph = (
|
|
bool(masters and layouts)
|
|
and all(
|
|
layout.get("parentPath") in master_key_by_path
|
|
for layout in layouts
|
|
)
|
|
and all(
|
|
slide.get("layoutPath") in layout_key_by_path
|
|
and slide.get("masterPath") in master_key_by_path
|
|
for slide in slides
|
|
)
|
|
)
|
|
reusable_structure = _has_reusable_structure(manifest)
|
|
recommended_mode = "preserve" if complete_graph and reusable_structure else "template"
|
|
reasons: list[str] = []
|
|
if not complete_graph:
|
|
reasons.append("incomplete-master-layout-graph")
|
|
if not reusable_structure:
|
|
reasons.append("source-structure-is-minimal")
|
|
if complete_graph and reusable_structure:
|
|
reasons.append("source-master-layout-contract-is-reusable")
|
|
if len(masters) > 1:
|
|
reasons.append("source-uses-multiple-masters")
|
|
|
|
master_contracts = []
|
|
for master in masters:
|
|
key = master_key_by_path[master["path"]]
|
|
master_contracts.append({
|
|
"key": key,
|
|
"name": master.get("displayName") or master.get("name") or key,
|
|
"packagePart": master["path"],
|
|
"themePart": master.get("themePath"),
|
|
"theme": master.get("theme") or {"colors": {}, "fonts": {}},
|
|
"backgroundAsset": master.get("backgroundAsset"),
|
|
"imageAssets": master.get("imageAssets", []),
|
|
"drawableShapeCount": master.get("drawableShapeCount", 0),
|
|
"layoutKeys": [
|
|
layout_key_by_path[layout["path"]]
|
|
for layout in layouts
|
|
if layout.get("parentPath") == master["path"]
|
|
],
|
|
})
|
|
|
|
layout_contracts = []
|
|
for layout in layouts:
|
|
key = layout_key_by_path[layout["path"]]
|
|
layout_contracts.append({
|
|
"key": key,
|
|
"name": layout.get("displayName") or layout.get("name") or key,
|
|
"type": layout.get("layoutType"),
|
|
"packagePart": layout["path"],
|
|
"masterKey": master_key_by_path.get(layout.get("parentPath")),
|
|
"backgroundAsset": layout.get("backgroundAsset"),
|
|
"imageAssets": layout.get("imageAssets", []),
|
|
"drawableShapeCount": layout.get("drawableShapeCount", 0),
|
|
"placeholders": layout.get("placeholders", []),
|
|
"usedBySlides": layout.get("usedBySlides", []),
|
|
"svgFile": layout.get("svgFile"),
|
|
})
|
|
|
|
slide_contracts = []
|
|
for slide in slides:
|
|
slide_contracts.append({
|
|
"index": slide["index"],
|
|
"pageType": slide.get("pageType"),
|
|
"layoutKey": layout_key_by_path.get(slide.get("layoutPath")),
|
|
"masterKey": master_key_by_path.get(slide.get("masterPath")),
|
|
"placeholders": slide.get("placeholders", []),
|
|
"layeredSvgFile": slide.get("svgFile"),
|
|
"flatSvgFile": slide.get("flatSvgFile"),
|
|
})
|
|
|
|
return {
|
|
"schema": SCHEMA,
|
|
"source": {
|
|
"name": source_pptx.name,
|
|
"templateFile": SOURCE_TEMPLATE_NAME,
|
|
"sha256": _sha256(source_pptx),
|
|
},
|
|
"slideSize": manifest.get("slideSize", {}),
|
|
"strategy": {
|
|
"preservationEligible": complete_graph,
|
|
"reusableStructureDetected": reusable_structure,
|
|
"recommendedMode": recommended_mode,
|
|
"recommendationScope": "source-structure-assessment",
|
|
"templateOutputMode": "template",
|
|
"downstreamTemplateAdherence": "strategist-confirmed-explicit-structure",
|
|
"hasMultipleMasters": len(masters) > 1,
|
|
"reasonCodes": reasons,
|
|
},
|
|
"masters": master_contracts,
|
|
"layouts": layout_contracts,
|
|
"slides": slide_contracts,
|
|
}
|
|
|
|
|
|
def write_native_structure_bundle(
|
|
source_pptx: Path,
|
|
output_dir: Path,
|
|
manifest: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
"""Copy the source template and write its portable structure contract."""
|
|
source_copy = output_dir / SOURCE_TEMPLATE_NAME
|
|
_copy_source_template(source_pptx, source_copy)
|
|
contract = build_native_structure(source_pptx, manifest)
|
|
(output_dir / CONTRACT_NAME).write_text(
|
|
json.dumps(contract, ensure_ascii=False, indent=2) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
return contract
|