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