返回 ppt-master
native_structure.py
根目录 / skills / ppt-master / scripts / template_import / native_structure.py
1 #!/usr/bin/env python3
2 """
3 PPT Master - Native Template Structure Contract
4
5 Build a portable master/layout contract from the PPTX template import manifest.
6
7 Usage:
8 Imported by pptx_template_import.py.
9
10 Examples:
11 write_native_structure_bundle(source_pptx, output_dir, manifest)
12
13 Dependencies:
14 None (only uses standard library)
15 """
16
17 from __future__ import annotations
18
19 import hashlib
20 import json
21 import shutil
22 from pathlib import Path
23 from typing import Any
24
25
26 SCHEMA = "ppt-master.native-structure.v1"
27 SOURCE_TEMPLATE_NAME = "source_template.pptx"
28 CONTRACT_NAME = "native_structure.json"
29
30
31 def _sha256(path: Path) -> str:
32 digest = hashlib.sha256()
33 with path.open("rb") as handle:
34 for chunk in iter(lambda: handle.read(1024 * 1024), b""):
35 digest.update(chunk)
36 return digest.hexdigest()
37
38
39 def _copy_source_template(source: Path, destination: Path) -> None:
40 if source.resolve() == destination.resolve():
41 return
42 shutil.copy2(source, destination)
43
44
45 def _has_reusable_structure(manifest: dict[str, Any]) -> bool:
46 layouts = manifest.get("layouts", [])
47 masters = manifest.get("masters", [])
48 slides = manifest.get("slides", [])
49 used_layouts = [layout for layout in layouts if layout.get("usedBySlides")]
50 candidate_layouts = used_layouts or layouts
51 if any(
52 layout.get("backgroundAsset")
53 or int(layout.get("drawableShapeCount") or 0) > 0
54 or any(
55 placeholder.get("semanticRole")
56 not in {"date", "footer", "slide-number", "other"}
57 for placeholder in layout.get("placeholders", [])
58 )
59 for layout in candidate_layouts
60 ):
61 return True
62 if any(
63 master.get("backgroundAsset")
64 or int(master.get("drawableShapeCount") or 0) > 0
65 or master.get("imageAssets")
66 for master in masters
67 ):
68 return True
69 return any(slide.get("placeholders") for slide in slides)
70
71
72 def build_native_structure(
73 source_pptx: Path,
74 manifest: dict[str, Any],
75 ) -> dict[str, Any]:
76 """Build the portable source-package structure contract."""
77 masters = manifest.get("masters", [])
78 layouts = manifest.get("layouts", [])
79 slides = manifest.get("slides", [])
80 master_key_by_path = {
81 master["path"]: f"master_{index:02d}"
82 for index, master in enumerate(masters, start=1)
83 }
84 layout_key_by_path = {
85 layout["path"]: f"layout_{index:02d}"
86 for index, layout in enumerate(layouts, start=1)
87 }
88 complete_graph = (
89 bool(masters and layouts)
90 and all(
91 layout.get("parentPath") in master_key_by_path
92 for layout in layouts
93 )
94 and all(
95 slide.get("layoutPath") in layout_key_by_path
96 and slide.get("masterPath") in master_key_by_path
97 for slide in slides
98 )
99 )
100 reusable_structure = _has_reusable_structure(manifest)
101 recommended_mode = "preserve" if complete_graph and reusable_structure else "template"
102 reasons: list[str] = []
103 if not complete_graph:
104 reasons.append("incomplete-master-layout-graph")
105 if not reusable_structure:
106 reasons.append("source-structure-is-minimal")
107 if complete_graph and reusable_structure:
108 reasons.append("source-master-layout-contract-is-reusable")
109 if len(masters) > 1:
110 reasons.append("source-uses-multiple-masters")
111
112 master_contracts = []
113 for master in masters:
114 key = master_key_by_path[master["path"]]
115 master_contracts.append({
116 "key": key,
117 "name": master.get("displayName") or master.get("name") or key,
118 "packagePart": master["path"],
119 "themePart": master.get("themePath"),
120 "theme": master.get("theme") or {"colors": {}, "fonts": {}},
121 "backgroundAsset": master.get("backgroundAsset"),
122 "imageAssets": master.get("imageAssets", []),
123 "shapeImageAssets": master.get("shapeImageAssets", []),
124 "drawableShapeCount": master.get("drawableShapeCount", 0),
125 "layoutKeys": [
126 layout_key_by_path[layout["path"]]
127 for layout in layouts
128 if layout.get("parentPath") == master["path"]
129 ],
130 })
131
132 layout_contracts = []
133 for layout in layouts:
134 key = layout_key_by_path[layout["path"]]
135 layout_contracts.append({
136 "key": key,
137 "name": layout.get("displayName") or layout.get("name") or key,
138 "type": layout.get("layoutType"),
139 "packagePart": layout["path"],
140 "masterKey": master_key_by_path.get(layout.get("parentPath")),
141 "showMasterShapes": layout.get("showMasterShapes", True),
142 "backgroundAsset": layout.get("backgroundAsset"),
143 "imageAssets": layout.get("imageAssets", []),
144 "shapeImageAssets": layout.get("shapeImageAssets", []),
145 "drawableShapeCount": layout.get("drawableShapeCount", 0),
146 "placeholders": layout.get("placeholders", []),
147 "usedBySlides": layout.get("usedBySlides", []),
148 "svgFile": layout.get("svgFile"),
149 })
150
151 slide_contracts = []
152 for slide in slides:
153 slide_contracts.append({
154 "index": slide["index"],
155 "pageType": slide.get("pageType"),
156 "layoutKey": layout_key_by_path.get(slide.get("layoutPath")),
157 "masterKey": master_key_by_path.get(slide.get("masterPath")),
158 "showInheritedShapes": slide.get("showInheritedShapes", True),
159 "placeholders": slide.get("placeholders", []),
160 "layeredSvgFile": slide.get("svgFile"),
161 "flatSvgFile": slide.get("flatSvgFile"),
162 })
163
164 return {
165 "schema": SCHEMA,
166 "source": {
167 "name": source_pptx.name,
168 "templateFile": SOURCE_TEMPLATE_NAME,
169 "sha256": _sha256(source_pptx),
170 },
171 "slideSize": manifest.get("slideSize", {}),
172 "strategy": {
173 "preservationEligible": complete_graph,
174 "reusableStructureDetected": reusable_structure,
175 "recommendedMode": recommended_mode,
176 "recommendationScope": "source-structure-assessment",
177 "templateOutputMode": "template",
178 "downstreamTemplateAdherence": "strategist-confirmed-explicit-structure",
179 "hasMultipleMasters": len(masters) > 1,
180 "reasonCodes": reasons,
181 },
182 "masters": master_contracts,
183 "layouts": layout_contracts,
184 "slides": slide_contracts,
185 }
186
187
188 def write_native_structure_bundle(
189 source_pptx: Path,
190 output_dir: Path,
191 manifest: dict[str, Any],
192 ) -> dict[str, Any]:
193 """Copy the source template and write its portable structure contract."""
194 source_copy = output_dir / SOURCE_TEMPLATE_NAME
195 _copy_source_template(source_pptx, source_copy)
196 contract = build_native_structure(source_pptx, manifest)
197 (output_dir / CONTRACT_NAME).write_text(
198 json.dumps(contract, ensure_ascii=False, indent=2) + "\n",
199 encoding="utf-8",
200 )
201 return contract
202
202 lines PYTHON