Files
Templated-PPT-Master/runtime/ppt-master/scripts/pptx_template_import.py
T

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())