Release standalone PPT template workflow 0.5.0
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
#!/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", []),
|
||||
"shapeImageAssets": master.get("shapeImageAssets", []),
|
||||
"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")),
|
||||
"showMasterShapes": layout.get("showMasterShapes", True),
|
||||
"backgroundAsset": layout.get("backgroundAsset"),
|
||||
"imageAssets": layout.get("imageAssets", []),
|
||||
"shapeImageAssets": layout.get("shapeImageAssets", []),
|
||||
"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")),
|
||||
"showInheritedShapes": slide.get("showInheritedShapes", True),
|
||||
"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
|
||||
Reference in New Issue
Block a user