Files
wehub-resource-sync 8ef9db2513
Deploy to GitHub Pages / deploy (push) Has been cancelled
Deploy to GitHub Pages / build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:56:49 +08:00

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