返回 ppt-master
preset_shape_svg.py
根目录 / skills / ppt-master / scripts / preset_shape_svg.py
1 #!/usr/bin/env python3
2 """
3 PPT Master - Preset Shape SVG Fragment Tool
4
5 List DrawingML presets or print one compact canonical native-preset SVG group to
6 stdout for manual insertion into a hand-authored page or template.
7
8 Usage:
9 python3 scripts/preset_shape_svg.py list [--search QUERY]
10 python3 scripts/preset_shape_svg.py describe PRESET
11 python3 scripts/preset_shape_svg.py render PRESET --id ID --frame X Y W H
12
13 Examples:
14 python3 scripts/preset_shape_svg.py list --search arrow
15 python3 scripts/preset_shape_svg.py describe rightArrow
16 python3 scripts/preset_shape_svg.py render rightArrow --id next-step \
17 --frame 160 210 320 112 --fill "#2563EB" --stroke none
18
19 Dependencies:
20 None (only uses standard library and local PPT Master modules)
21 """
22
23 from __future__ import annotations
24
25 import argparse
26 import json
27 import sys
28 from typing import Sequence
29
30 from console_encoding import configure_utf8_stdio
31 from pptx_shapes import CONNECTOR_PRESET_TYPES, get_preset_registry
32 from pptx_to_svg.preset_authoring import render_preset_shape_fragment
33
34
35 configure_utf8_stdio()
36
37
38 def build_parser() -> argparse.ArgumentParser:
39 parser = argparse.ArgumentParser(
40 description=(
41 "Print one compact canonical DrawingML preset SVG group. "
42 "This tool never writes SVG files or page layouts."
43 ),
44 formatter_class=argparse.RawDescriptionHelpFormatter,
45 )
46 subparsers = parser.add_subparsers(dest="command", required=True)
47
48 list_parser = subparsers.add_parser(
49 "list",
50 help="List preset names, optionally filtered by substring.",
51 )
52 list_parser.add_argument(
53 "--search",
54 default="",
55 help="Case-insensitive preset-name substring.",
56 )
57
58 describe_parser = subparsers.add_parser(
59 "describe",
60 help="Print preset adjustment and path metadata as JSON.",
61 )
62 describe_parser.add_argument("preset", help="DrawingML preset name.")
63
64 render_parser = subparsers.add_parser(
65 "render",
66 help="Print one canonical authored-preset <g> fragment to stdout.",
67 )
68 render_parser.add_argument("preset", help="DrawingML preset name.")
69 render_parser.add_argument(
70 "--id",
71 required=True,
72 dest="element_id",
73 help="Stable unique SVG group id.",
74 )
75 render_parser.add_argument(
76 "--frame",
77 required=True,
78 nargs=4,
79 type=float,
80 metavar=("X", "Y", "WIDTH", "HEIGHT"),
81 help="SVG frame in the fragment's insertion coordinate space.",
82 )
83 render_parser.add_argument(
84 "--object-kind",
85 choices=("shape", "connector"),
86 default="shape",
87 help="Emit a normal shape or a native PowerPoint connector.",
88 )
89 render_parser.add_argument(
90 "--name",
91 help="Optional PowerPoint object name.",
92 )
93 render_parser.add_argument(
94 "--fill",
95 default="none",
96 help="Solid SVG fill, or none.",
97 )
98 render_parser.add_argument(
99 "--fill-opacity",
100 type=float,
101 help="Fill opacity from 0 to 1.",
102 )
103 render_parser.add_argument(
104 "--stroke",
105 default="none",
106 help="Solid SVG stroke, or none.",
107 )
108 render_parser.add_argument(
109 "--stroke-width",
110 type=float,
111 help="Stroke width in SVG page units; defaults to 1 when stroked.",
112 )
113 render_parser.add_argument(
114 "--stroke-opacity",
115 type=float,
116 help="Stroke opacity from 0 to 1.",
117 )
118 render_parser.add_argument(
119 "--stroke-linecap",
120 choices=("butt", "round", "square"),
121 )
122 render_parser.add_argument(
123 "--stroke-linejoin",
124 choices=("miter", "round", "bevel"),
125 )
126 render_parser.add_argument(
127 "--adjust",
128 action="append",
129 default=[],
130 metavar="NAME=FORMULA",
131 help=(
132 "DrawingML adjustment formula; repeat for multiple guides, "
133 "for example --adjust 'adj1=val 50000'."
134 ),
135 )
136 return parser
137
138
139 def main(argv: Sequence[str] | None = None) -> int:
140 parser = build_parser()
141 args = parser.parse_args(argv)
142 registry = get_preset_registry()
143
144 if args.command == "list":
145 query = args.search.casefold().strip()
146 names = [
147 name for name in registry.names
148 if not query or query in name.casefold()
149 ]
150 if not names:
151 print(f"No preset names match {args.search!r}", file=sys.stderr)
152 return 1
153 print("\n".join(names))
154 return 0
155
156 if args.command == "describe":
157 if args.preset not in registry:
158 print(f"Unknown DrawingML preset: {args.preset!r}", file=sys.stderr)
159 return 1
160 definition = registry.get(args.preset)
161 payload = {
162 "preset": definition.name,
163 "connector_preset": definition.name in CONNECTOR_PRESET_TYPES,
164 "adjustments": [
165 {"name": guide.name, "formula": guide.formula}
166 for guide in definition.adjustments
167 ],
168 "path_count": len(definition.paths),
169 "connection_site_count": len(definition.connections),
170 "has_text_rectangle": definition.text_rectangle is not None,
171 }
172 print(json.dumps(payload, ensure_ascii=False, indent=2))
173 return 0
174
175 try:
176 adjustments = _parse_adjustments(args.adjust)
177 style = _style_from_args(args)
178 fragment = render_preset_shape_fragment(
179 args.preset,
180 tuple(args.frame),
181 adjustments=adjustments,
182 object_kind=args.object_kind,
183 element_id=args.element_id,
184 name=args.name,
185 style=style,
186 )
187 except ValueError as exc:
188 print(f"Error: {exc}", file=sys.stderr)
189 return 1
190 print(fragment)
191 return 0
192
193
194 def _parse_adjustments(values: Sequence[str]) -> dict[str, str]:
195 adjustments: dict[str, str] = {}
196 for value in values:
197 name, separator, formula = value.partition("=")
198 name = name.strip()
199 formula = formula.strip()
200 if not separator or not name or not formula:
201 raise ValueError(
202 f"Invalid adjustment {value!r}; expected NAME=FORMULA"
203 )
204 if name in adjustments:
205 raise ValueError(f"Duplicate adjustment guide: {name!r}")
206 adjustments[name] = formula
207 return adjustments
208
209
210 def _style_from_args(args: argparse.Namespace) -> dict[str, str]:
211 style = {
212 "fill": args.fill,
213 "stroke": args.stroke,
214 }
215 if args.fill_opacity is not None:
216 style["fill-opacity"] = str(args.fill_opacity)
217 if args.stroke != "none":
218 style["stroke-width"] = str(
219 1.0 if args.stroke_width is None else args.stroke_width
220 )
221 elif args.stroke_width is not None:
222 raise ValueError("--stroke-width requires a non-none --stroke")
223 if args.stroke_opacity is not None:
224 style["stroke-opacity"] = str(args.stroke_opacity)
225 if args.stroke_linecap is not None:
226 style["stroke-linecap"] = args.stroke_linecap
227 if args.stroke_linejoin is not None:
228 style["stroke-linejoin"] = args.stroke_linejoin
229 return style
230
231
232 if __name__ == "__main__":
233 raise SystemExit(main())
234
234 lines PYTHON