279 lines
10 KiB
Python
279 lines
10 KiB
Python
#!/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: <pptx_stem>_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())
|