返回 ppt-master
pptx_template_import.py
根目录 / skills / ppt-master / scripts / pptx_template_import.py
1 #!/usr/bin/env python3
2 """Unified PPTX preparation entry point for the /create-template workflow.
3
4 Reads OOXML directly via `pptx_to_svg` and writes a reusable reference workspace:
5
6 - `manifest.json` — single source of truth for slide size, theme colors, fonts,
7 asset inventory, and per-slide / per-layout / per-master metadata
8 - `native_structure.json` + `source_template.pptx` — source-structure facts and
9 a byte-identical analysis copy used to rebuild explicit SVG structure
10 - `assets/` — extracted reusable image assets
11 - `conversion-report.json` — source-recovery and fidelity diagnostics emitted
12 with SVG conversion
13 - `svg/` — canonical layered template view (every master
14 and layout in the deck rendered once each as `master_*.svg` /
15 `layout_*.svg`, slides contain only their own shapes, and an
16 `inheritance.json` describes the reuse graph)
17 - `svg-flat/` — optional verification view (`--inheritance-mode both`): each
18 `slide_NN.svg` is self-contained, so opening one slide shows the full page
19 like PowerPoint would
20 """
21
22 from __future__ import annotations
23
24 import argparse
25 import json
26 import shutil
27 import tempfile
28 from pathlib import Path
29 from xml.etree import ElementTree as ET
30 from zipfile import BadZipFile
31
32 from console_encoding import configure_utf8_stdio
33 from template_import.manifest import build_manifest
34 from template_import.native_structure import (
35 CONTRACT_NAME,
36 SOURCE_TEMPLATE_NAME,
37 write_native_structure_bundle,
38 )
39
40 configure_utf8_stdio()
41
42 _MANIFEST_NAME = "manifest.json"
43 _CONVERSION_REPORT_NAME = "conversion-report.json"
44
45
46 def parse_args() -> argparse.Namespace:
47 """Build the CLI argument parser for the import entry point."""
48 parser = argparse.ArgumentParser(
49 description="Prepare a PPTX reference workspace for /create-template."
50 )
51 parser.add_argument("pptx_file", help="Path to the source .pptx file")
52 parser.add_argument(
53 "-o",
54 "--output",
55 help="Output directory (default: <pptx_stem>_template_import beside the source file)",
56 )
57 parser.add_argument(
58 "--skip-manifest",
59 action="store_true",
60 help=(
61 "Skip metadata, asset inventory, native structure contract, and "
62 "preserved source-package generation"
63 ),
64 )
65 parser.add_argument(
66 "--manifest-only",
67 action="store_true",
68 help=(
69 "Only extract manifest.json + reusable assets + the native "
70 "structure/source pair, without exporting slides to SVG"
71 ),
72 )
73 parser.add_argument(
74 "--embed-images",
75 action="store_true",
76 help="Inline images as data: URIs instead of writing files to assets/",
77 )
78 parser.add_argument(
79 "--inheritance-mode",
80 choices=("both", "layered", "flat"),
81 default="layered",
82 help=(
83 "How to render master/layout shapes for slide SVGs. "
84 "'layered' (default): emit the canonical svg/ tree with master, "
85 "layout, and slide-local files plus svg/inheritance.json. "
86 "'both': also emit svg-flat/ with self-contained per-slide "
87 "verification files. In this mode svg/ still holds the layered "
88 "renderings (template designers see master/layout/slide as "
89 "separate files). 'flat': emit only self-contained slide SVGs "
90 "in svg/, the round-trip view used by svg_to_pptx."
91 ),
92 )
93 return parser.parse_args()
94
95
96 def _managed_asset_paths(output_dir: Path) -> set[Path]:
97 """Read the previous manifest's exact exported-asset roster."""
98 manifest_path = output_dir / _MANIFEST_NAME
99 try:
100 manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
101 except (OSError, UnicodeError, json.JSONDecodeError):
102 return set()
103 if not isinstance(manifest, dict):
104 return set()
105 assets = manifest.get("assets")
106 if not isinstance(assets, dict):
107 return set()
108 export_dir = assets.get("exportDir")
109 asset_names = assets.get("allAssets")
110 if export_dir != "assets" or not isinstance(asset_names, list):
111 return set()
112 if any(
113 not isinstance(name, str)
114 or not name
115 or name in {".", ".."}
116 or "/" in name
117 or "\\" in name
118 for name in asset_names
119 ):
120 return set()
121 return {
122 Path("assets") / name
123 for name in asset_names
124 }
125
126
127 def main() -> int:
128 """CLI entry point: write the PPTX reference workspace to disk."""
129 args = parse_args()
130 pptx_path = Path(args.pptx_file).expanduser().resolve()
131 if not pptx_path.exists():
132 print(f"Error: file does not exist: {pptx_path}")
133 return 1
134 if pptx_path.suffix.lower() != ".pptx":
135 print(f"Error: expected a .pptx file, got: {pptx_path.name}")
136 return 1
137
138 output_dir = (
139 Path(args.output).expanduser().resolve()
140 if args.output
141 else pptx_path.with_name(f"{pptx_path.stem}_template_import")
142 )
143
144 if args.skip_manifest and args.manifest_only:
145 print("Error: --skip-manifest and --manifest-only cannot be used together")
146 return 1
147
148 previous_assets = _managed_asset_paths(output_dir)
149 output_dir.parent.mkdir(parents=True, exist_ok=True)
150 staging_root = Path(tempfile.mkdtemp(
151 prefix=f".{output_dir.name}.import-",
152 dir=output_dir.parent,
153 ))
154 staged_dir = staging_root / "generated"
155 staged_dir.mkdir()
156
157 try:
158 manifest = None
159 native_structure = None
160 manifest_path = staged_dir / _MANIFEST_NAME
161 if not args.skip_manifest:
162 try:
163 manifest = build_manifest(
164 pptx_path,
165 staged_dir,
166 include_flat_svg=(
167 not args.manifest_only and args.inheritance_mode == "both"
168 ),
169 )
170 except (RuntimeError, OSError, ValueError) as exc:
171 print(f"Error: failed to extract PPTX metadata: {exc}")
172 return 1
173
174 manifest_path.write_text(
175 json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
176 encoding="utf-8",
177 )
178 try:
179 native_structure = write_native_structure_bundle(
180 pptx_path,
181 staged_dir,
182 manifest,
183 )
184 except (OSError, ValueError) as exc:
185 print(f"Error: failed to write native structure bundle: {exc}")
186 return 1
187
188 result = None
189 total_bytes = 0
190 if not args.manifest_only:
191 from pptx_to_svg import convert_pptx_to_svg
192 from pptx_to_svg.converter import ConvertOptions
193
194 options = ConvertOptions(
195 media_subdir="assets",
196 embed_images=args.embed_images,
197 keep_hidden=False,
198 inheritance_mode=args.inheritance_mode,
199 asset_name_map=(
200 manifest.get("assets", {}).get("assetMap", {})
201 if manifest else {}
202 ),
203 )
204 try:
205 result = convert_pptx_to_svg(pptx_path, staged_dir, options)
206 except (BadZipFile, ET.ParseError, OSError, RuntimeError, ValueError) as exc:
207 print(f"Error: failed to convert PPTX template source: {exc}")
208 return 1
209 total_bytes = sum(
210 len(art.svg.encode("utf-8"))
211 for art in result.slides
212 )
213
214 from pptx_to_svg.converter import publish_staged_workspace
215
216 try:
217 publish_staged_workspace(
218 output_dir,
219 staged_dir,
220 managed_root_files={
221 _MANIFEST_NAME,
222 CONTRACT_NAME,
223 SOURCE_TEMPLATE_NAME,
224 _CONVERSION_REPORT_NAME,
225 },
226 managed_relative_paths=previous_assets,
227 )
228 except (OSError, RuntimeError, ValueError) as exc:
229 print(f"Error: failed to publish PPTX template workspace: {exc}")
230 return 1
231
232 if args.manifest_only:
233 print(f"Imported PPTX template source: {pptx_path.name}")
234 print(f"Output directory: {output_dir}")
235 if manifest is not None:
236 print(f"Manifest: {manifest_path.name}")
237 print(f"Native structure: {CONTRACT_NAME}")
238 print(f"Source package analysis copy: {SOURCE_TEMPLATE_NAME}")
239 print(
240 "Source structure assessment: "
241 f"{native_structure['strategy']['recommendedMode']}"
242 )
243 print("Template output mode: explicit SVG structure")
244 print(f"Assets exported: {len(manifest['assets']['allAssets'])}")
245 print(f"Common assets: {len(manifest['assets']['commonAssets'])}")
246 print(f"Slides analyzed: {len(manifest['slides'])}")
247 print(f"Layouts (unique): {len(manifest.get('layouts', []))}")
248 print(f"Masters (unique): {len(manifest.get('masters', []))}")
249 return 0
250
251 print(f"Inheritance mode: {args.inheritance_mode}")
252 print(f"Exported SVG slides: {len(result.slides)}")
253 if args.inheritance_mode in {"layered", "both"}:
254 print(f"Exported masters: {len(result.masters)}")
255 print(f"Exported layouts: {len(result.layouts)}")
256 print("Inheritance graph: svg/inheritance.json")
257 if result.flat_slides:
258 print(f"Flat companion slides: {len(result.flat_slides)} (svg-flat/)")
259 if result.diagnostics:
260 print(
261 f"Source recovery warnings: {len(result.diagnostics)} "
262 f"({_CONVERSION_REPORT_NAME})"
263 )
264 print(f"SVG bytes (primary): {total_bytes}")
265 print(f"Output directory: {output_dir}")
266 if native_structure is not None:
267 print(
268 "Source structure assessment: "
269 f"{native_structure['strategy']['recommendedMode']}; "
270 "create-template rebuilds explicit SVG structure"
271 )
272 return 0
273 finally:
274 shutil.rmtree(staging_root, ignore_errors=True)
275
276
277 if __name__ == "__main__":
278 raise SystemExit(main())
279
279 lines PYTHON