返回 ppt-master
pptx_to_svg.py
根目录 / skills / ppt-master / scripts / pptx_to_svg.py
1 #!/usr/bin/env python3
2 """CLI entry: convert a .pptx file to one SVG per slide.
3
4 Usage:
5 python3 pptx_to_svg.py <pptx_file> [-o <output_dir>] [--embed-images]
6 [--media-subdir <name>] [--keep-hidden]
7 [--inheritance-mode {both,layered,flat}]
8 [--strict]
9
10 Output structure (default --inheritance-mode both):
11 <output_dir>/
12 svg/ layered machine input: masters/layouts/slides
13 svg-flat/ self-contained visual preview slides
14 animations.json normalized transition/object-motion sidecar
15 <media_subdir>/ (default: assets/)
16 image1.png
17 image2.png
18 ...
19
20 If -o is omitted, writes alongside the source file as <pptx_stem>_pptx_to_svg/.
21
22 This is the semantic import counterpart to svg_to_pptx.py: it reads OOXML
23 directly and emits declared SVG/native-marker subsets without claiming an
24 arbitrary lossless PPTX round trip.
25 """
26
27 from __future__ import annotations
28
29 import argparse
30 import sys
31 from pathlib import Path
32 from xml.etree import ElementTree as ET
33 from zipfile import BadZipFile
34
35 # Allow running this script from anywhere
36 sys.path.insert(0, str(Path(__file__).resolve().parent))
37
38 from console_encoding import configure_utf8_stdio
39 from pptx_to_svg import convert_pptx_to_svg
40 from pptx_to_svg.converter import ConvertOptions
41
42 configure_utf8_stdio()
43
44
45 def _diagnostic_preview(message: str, limit: int = 240) -> str:
46 """Return one compact CLI preview while the report retains full detail."""
47 compact = " ".join(message.split())
48 if len(compact) <= limit:
49 return compact
50 return compact[: limit - 3].rstrip() + "..."
51
52
53 def _reconstruction_only_graphics(result: object) -> list[tuple[int, str]]:
54 """Return slide/object labels for generated placeholders."""
55 artifacts = getattr(result, "flat_slides", None) or getattr(result, "slides", [])
56 diagnostics: list[tuple[int, str]] = []
57 for artifact in artifacts:
58 try:
59 root = ET.fromstring(artifact.svg)
60 except ET.ParseError:
61 continue
62 for elem in root.iter():
63 fallback_kind = (
64 elem.get("data-pptx-fallback-kind")
65 or elem.get("data-pptx-visual-status")
66 )
67 if fallback_kind != "placeholder":
68 continue
69 marker_id = elem.get("id") or elem.get("data-name") or "<unnamed>"
70 diagnostics.append((artifact.index, marker_id))
71 return diagnostics
72
73
74 def parse_args() -> argparse.Namespace:
75 parser = argparse.ArgumentParser(
76 description="Convert .pptx to per-slide SVG by reading OOXML directly.",
77 )
78 parser.add_argument("pptx_file", help="Path to the source .pptx file")
79 parser.add_argument(
80 "-o",
81 "--output",
82 help="Output directory (default: <pptx_stem>_pptx_to_svg beside source)",
83 )
84 parser.add_argument(
85 "--media-subdir",
86 default="assets",
87 help="Subdirectory for extracted media (default: assets)",
88 )
89 parser.add_argument(
90 "--embed-images",
91 action="store_true",
92 help="Base64-embed images inline instead of writing files",
93 )
94 parser.add_argument(
95 "--keep-hidden",
96 action="store_true",
97 help='Include shapes marked hidden="1"',
98 )
99 parser.add_argument(
100 "--inheritance-mode",
101 choices=("both", "layered", "flat"),
102 default="both",
103 help=(
104 "How to render inheritance. 'both' (default) writes layered SVGs "
105 "under svg/ and complete preview slides under svg-flat/. "
106 "'layered' writes only svg/ plus inheritance.json. 'flat' writes "
107 "self-contained slides under svg/ for backward compatibility."
108 ),
109 )
110 parser.add_argument(
111 "--strict",
112 action="store_true",
113 help=(
114 "Stop on the first unsupported/malformed source construct instead "
115 "of the default tolerant conversion with diagnostics"
116 ),
117 )
118 return parser.parse_args()
119
120
121 def main() -> int:
122 args = parse_args()
123 pptx_path = Path(args.pptx_file).expanduser().resolve()
124 if not pptx_path.exists():
125 print(f"Error: file does not exist: {pptx_path}", file=sys.stderr)
126 return 1
127 if pptx_path.suffix.lower() != ".pptx":
128 print(f"Error: expected a .pptx file, got: {pptx_path.name}", file=sys.stderr)
129 return 1
130
131 output_dir = (
132 Path(args.output).expanduser().resolve()
133 if args.output
134 else pptx_path.with_name(f"{pptx_path.stem}_pptx_to_svg")
135 )
136
137 options = ConvertOptions(
138 media_subdir=args.media_subdir,
139 embed_images=args.embed_images,
140 keep_hidden=args.keep_hidden,
141 inheritance_mode=args.inheritance_mode,
142 strict=args.strict,
143 )
144
145 try:
146 result = convert_pptx_to_svg(pptx_path, output_dir, options)
147 except (BadZipFile, ET.ParseError, OSError, RuntimeError, ValueError) as exc:
148 print(f"Error: PPTX-to-SVG conversion failed: {exc}", file=sys.stderr)
149 return 1
150
151 print(f"Source: {pptx_path.name}")
152 print(f"Canvas: {result.canvas_px[0]:.0f} x {result.canvas_px[1]:.0f} px")
153 if result.theme_colors:
154 scheme = ", ".join(f"{k}={v}" for k, v in sorted(result.theme_colors.items()))
155 print(f"Theme colors: {scheme}")
156 if result.theme_fonts:
157 fonts = ", ".join(f"{k}={v}" for k, v in result.theme_fonts.items())
158 print(f"Theme fonts: {fonts}")
159 print(f"Slides converted: {len(result.slides)}")
160 if result.diagnostics:
161 print(
162 f"Warning: {len(result.diagnostics)} source construct(s) were "
163 "normalized, omitted, or replaced; see conversion-report.json.",
164 file=sys.stderr,
165 )
166 for item in result.diagnostics[:20]:
167 location = (
168 f"slide {item.slide_index}"
169 if item.slide_index
170 else item.part_path
171 )
172 shape = item.shape_name or item.shape_id
173 if shape:
174 location = f"{location}, {shape}" if location else shape
175 print(
176 f" {location or 'package'}: {item.code}: "
177 f"{_diagnostic_preview(item.message)}",
178 file=sys.stderr,
179 )
180 if len(result.diagnostics) > 20:
181 print(
182 f" ... and {len(result.diagnostics) - 20} more",
183 file=sys.stderr,
184 )
185 reconstruction_only = _reconstruction_only_graphics(result)
186 if reconstruction_only:
187 print(
188 "Warning: chart placeholder(s) without a baked preview are "
189 "reconstruction-only. Default export keeps the placeholder; "
190 "--native-charts-and-tables may reconstruct entries with a valid "
191 "replacement marker:",
192 file=sys.stderr,
193 )
194 for slide_index, marker_id in reconstruction_only[:20]:
195 print(f" slide {slide_index}: {marker_id}", file=sys.stderr)
196 if len(reconstruction_only) > 20:
197 print(
198 f" ... and {len(reconstruction_only) - 20} more",
199 file=sys.stderr,
200 )
201 print(f"Output: {output_dir}")
202 print(f"Animation config: {output_dir / 'animations.json'}")
203 print(f"Conversion report: {output_dir / 'conversion-report.json'}")
204 return 0
205
206
207 if __name__ == "__main__":
208 raise SystemExit(main())
209
209 lines PYTHON