#!/usr/bin/env python3 """Unified PPTX preparation entry point for the /create-template workflow. Reads OOXML directly via `pptx_to_svg` and writes a reusable reference workspace: - `manifest.json` — single source of truth for slide size, theme colors, fonts, asset inventory, and per-slide / per-layout / per-master metadata - `native_structure.json` + `source_template.pptx` — source-structure facts and a byte-identical analysis copy used to rebuild explicit SVG structure - `assets/` — extracted reusable image assets - `conversion-report.json` — source-recovery and fidelity diagnostics emitted with SVG conversion - `svg/` — canonical layered template view (every master and layout in the deck rendered once each as `master_*.svg` / `layout_*.svg`, slides contain only their own shapes, and an `inheritance.json` describes the reuse graph) - `svg-flat/` — optional verification view (`--inheritance-mode both`): each `slide_NN.svg` is self-contained, so opening one slide shows the full page like PowerPoint would """ from __future__ import annotations import argparse import json import shutil import tempfile from pathlib import Path from xml.etree import ElementTree as ET from zipfile import BadZipFile from console_encoding import configure_utf8_stdio from template_import.manifest import build_manifest from template_import.native_structure import ( CONTRACT_NAME, SOURCE_TEMPLATE_NAME, write_native_structure_bundle, ) configure_utf8_stdio() _MANIFEST_NAME = "manifest.json" _CONVERSION_REPORT_NAME = "conversion-report.json" def parse_args() -> argparse.Namespace: """Build the CLI argument parser for the import entry point.""" parser = argparse.ArgumentParser( description="Prepare a PPTX reference workspace for /create-template." ) parser.add_argument("pptx_file", help="Path to the source .pptx file") parser.add_argument( "-o", "--output", help="Output directory (default: _template_import beside the source file)", ) parser.add_argument( "--skip-manifest", action="store_true", help=( "Skip metadata, asset inventory, native structure contract, and " "preserved source-package generation" ), ) parser.add_argument( "--manifest-only", action="store_true", help=( "Only extract manifest.json + reusable assets + the native " "structure/source pair, without exporting slides to SVG" ), ) parser.add_argument( "--embed-images", action="store_true", help="Inline images as data: URIs instead of writing files to assets/", ) parser.add_argument( "--inheritance-mode", choices=("both", "layered", "flat"), default="layered", help=( "How to render master/layout shapes for slide SVGs. " "'layered' (default): emit the canonical svg/ tree with master, " "layout, and slide-local files plus svg/inheritance.json. " "'both': also emit svg-flat/ with self-contained per-slide " "verification files. In this mode svg/ still holds the layered " "renderings (template designers see master/layout/slide as " "separate files). 'flat': emit only self-contained slide SVGs " "in svg/, the round-trip view used by svg_to_pptx." ), ) return parser.parse_args() def _managed_asset_paths(output_dir: Path) -> set[Path]: """Read the previous manifest's exact exported-asset roster.""" manifest_path = output_dir / _MANIFEST_NAME try: manifest = json.loads(manifest_path.read_text(encoding="utf-8")) except (OSError, UnicodeError, json.JSONDecodeError): return set() if not isinstance(manifest, dict): return set() assets = manifest.get("assets") if not isinstance(assets, dict): return set() export_dir = assets.get("exportDir") asset_names = assets.get("allAssets") if export_dir != "assets" or not isinstance(asset_names, list): return set() if any( not isinstance(name, str) or not name or name in {".", ".."} or "/" in name or "\\" in name for name in asset_names ): return set() return { Path("assets") / name for name in asset_names } def main() -> int: """CLI entry point: write the PPTX reference workspace to disk.""" args = parse_args() pptx_path = Path(args.pptx_file).expanduser().resolve() if not pptx_path.exists(): print(f"Error: file does not exist: {pptx_path}") return 1 if pptx_path.suffix.lower() != ".pptx": print(f"Error: expected a .pptx file, got: {pptx_path.name}") return 1 output_dir = ( Path(args.output).expanduser().resolve() if args.output else pptx_path.with_name(f"{pptx_path.stem}_template_import") ) if args.skip_manifest and args.manifest_only: print("Error: --skip-manifest and --manifest-only cannot be used together") return 1 previous_assets = _managed_asset_paths(output_dir) output_dir.parent.mkdir(parents=True, exist_ok=True) staging_root = Path(tempfile.mkdtemp( prefix=f".{output_dir.name}.import-", dir=output_dir.parent, )) staged_dir = staging_root / "generated" staged_dir.mkdir() try: manifest = None native_structure = None manifest_path = staged_dir / _MANIFEST_NAME if not args.skip_manifest: try: manifest = build_manifest( pptx_path, staged_dir, include_flat_svg=( not args.manifest_only and args.inheritance_mode == "both" ), ) except (RuntimeError, OSError, ValueError) as exc: print(f"Error: failed to extract PPTX metadata: {exc}") return 1 manifest_path.write_text( json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) try: native_structure = write_native_structure_bundle( pptx_path, staged_dir, manifest, ) except (OSError, ValueError) as exc: print(f"Error: failed to write native structure bundle: {exc}") return 1 result = None total_bytes = 0 if not args.manifest_only: from pptx_to_svg import convert_pptx_to_svg from pptx_to_svg.converter import ConvertOptions options = ConvertOptions( media_subdir="assets", embed_images=args.embed_images, keep_hidden=False, inheritance_mode=args.inheritance_mode, asset_name_map=( manifest.get("assets", {}).get("assetMap", {}) if manifest else {} ), ) try: result = convert_pptx_to_svg(pptx_path, staged_dir, options) except (BadZipFile, ET.ParseError, OSError, RuntimeError, ValueError) as exc: print(f"Error: failed to convert PPTX template source: {exc}") return 1 total_bytes = sum( len(art.svg.encode("utf-8")) for art in result.slides ) from pptx_to_svg.converter import publish_staged_workspace try: publish_staged_workspace( output_dir, staged_dir, managed_root_files={ _MANIFEST_NAME, CONTRACT_NAME, SOURCE_TEMPLATE_NAME, _CONVERSION_REPORT_NAME, }, managed_relative_paths=previous_assets, ) except (OSError, RuntimeError, ValueError) as exc: print(f"Error: failed to publish PPTX template workspace: {exc}") return 1 if args.manifest_only: print(f"Imported PPTX template source: {pptx_path.name}") print(f"Output directory: {output_dir}") if manifest is not None: print(f"Manifest: {manifest_path.name}") print(f"Native structure: {CONTRACT_NAME}") print(f"Source package analysis copy: {SOURCE_TEMPLATE_NAME}") print( "Source structure assessment: " f"{native_structure['strategy']['recommendedMode']}" ) print("Template output mode: explicit SVG structure") print(f"Assets exported: {len(manifest['assets']['allAssets'])}") print(f"Common assets: {len(manifest['assets']['commonAssets'])}") print(f"Slides analyzed: {len(manifest['slides'])}") print(f"Layouts (unique): {len(manifest.get('layouts', []))}") print(f"Masters (unique): {len(manifest.get('masters', []))}") return 0 print(f"Inheritance mode: {args.inheritance_mode}") print(f"Exported SVG slides: {len(result.slides)}") if args.inheritance_mode in {"layered", "both"}: print(f"Exported masters: {len(result.masters)}") print(f"Exported layouts: {len(result.layouts)}") print("Inheritance graph: svg/inheritance.json") if result.flat_slides: print(f"Flat companion slides: {len(result.flat_slides)} (svg-flat/)") if result.diagnostics: print( f"Source recovery warnings: {len(result.diagnostics)} " f"({_CONVERSION_REPORT_NAME})" ) print(f"SVG bytes (primary): {total_bytes}") print(f"Output directory: {output_dir}") if native_structure is not None: print( "Source structure assessment: " f"{native_structure['strategy']['recommendedMode']}; " "create-template rebuilds explicit SVG structure" ) return 0 finally: shutil.rmtree(staging_root, ignore_errors=True) if __name__ == "__main__": raise SystemExit(main())