234 lines
7.2 KiB
Python
234 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
PPT Master - Preset Shape SVG Fragment Tool
|
|
|
|
List DrawingML presets or print one compact canonical native-preset SVG group to
|
|
stdout for manual insertion into a hand-authored page or template.
|
|
|
|
Usage:
|
|
python3 scripts/preset_shape_svg.py list [--search QUERY]
|
|
python3 scripts/preset_shape_svg.py describe PRESET
|
|
python3 scripts/preset_shape_svg.py render PRESET --id ID --frame X Y W H
|
|
|
|
Examples:
|
|
python3 scripts/preset_shape_svg.py list --search arrow
|
|
python3 scripts/preset_shape_svg.py describe rightArrow
|
|
python3 scripts/preset_shape_svg.py render rightArrow --id next-step \
|
|
--frame 160 210 320 112 --fill "#2563EB" --stroke none
|
|
|
|
Dependencies:
|
|
None (only uses standard library and local PPT Master modules)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from typing import Sequence
|
|
|
|
from console_encoding import configure_utf8_stdio
|
|
from pptx_shapes import CONNECTOR_PRESET_TYPES, get_preset_registry
|
|
from pptx_to_svg.preset_authoring import render_preset_shape_fragment
|
|
|
|
|
|
configure_utf8_stdio()
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
description=(
|
|
"Print one compact canonical DrawingML preset SVG group. "
|
|
"This tool never writes SVG files or page layouts."
|
|
),
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
)
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
|
|
list_parser = subparsers.add_parser(
|
|
"list",
|
|
help="List preset names, optionally filtered by substring.",
|
|
)
|
|
list_parser.add_argument(
|
|
"--search",
|
|
default="",
|
|
help="Case-insensitive preset-name substring.",
|
|
)
|
|
|
|
describe_parser = subparsers.add_parser(
|
|
"describe",
|
|
help="Print preset adjustment and path metadata as JSON.",
|
|
)
|
|
describe_parser.add_argument("preset", help="DrawingML preset name.")
|
|
|
|
render_parser = subparsers.add_parser(
|
|
"render",
|
|
help="Print one canonical authored-preset <g> fragment to stdout.",
|
|
)
|
|
render_parser.add_argument("preset", help="DrawingML preset name.")
|
|
render_parser.add_argument(
|
|
"--id",
|
|
required=True,
|
|
dest="element_id",
|
|
help="Stable unique SVG group id.",
|
|
)
|
|
render_parser.add_argument(
|
|
"--frame",
|
|
required=True,
|
|
nargs=4,
|
|
type=float,
|
|
metavar=("X", "Y", "WIDTH", "HEIGHT"),
|
|
help="SVG frame in the fragment's insertion coordinate space.",
|
|
)
|
|
render_parser.add_argument(
|
|
"--object-kind",
|
|
choices=("shape", "connector"),
|
|
default="shape",
|
|
help="Emit a normal shape or a native PowerPoint connector.",
|
|
)
|
|
render_parser.add_argument(
|
|
"--name",
|
|
help="Optional PowerPoint object name.",
|
|
)
|
|
render_parser.add_argument(
|
|
"--fill",
|
|
default="none",
|
|
help="Solid SVG fill, or none.",
|
|
)
|
|
render_parser.add_argument(
|
|
"--fill-opacity",
|
|
type=float,
|
|
help="Fill opacity from 0 to 1.",
|
|
)
|
|
render_parser.add_argument(
|
|
"--stroke",
|
|
default="none",
|
|
help="Solid SVG stroke, or none.",
|
|
)
|
|
render_parser.add_argument(
|
|
"--stroke-width",
|
|
type=float,
|
|
help="Stroke width in SVG page units; defaults to 1 when stroked.",
|
|
)
|
|
render_parser.add_argument(
|
|
"--stroke-opacity",
|
|
type=float,
|
|
help="Stroke opacity from 0 to 1.",
|
|
)
|
|
render_parser.add_argument(
|
|
"--stroke-linecap",
|
|
choices=("butt", "round", "square"),
|
|
)
|
|
render_parser.add_argument(
|
|
"--stroke-linejoin",
|
|
choices=("miter", "round", "bevel"),
|
|
)
|
|
render_parser.add_argument(
|
|
"--adjust",
|
|
action="append",
|
|
default=[],
|
|
metavar="NAME=FORMULA",
|
|
help=(
|
|
"DrawingML adjustment formula; repeat for multiple guides, "
|
|
"for example --adjust 'adj1=val 50000'."
|
|
),
|
|
)
|
|
return parser
|
|
|
|
|
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
parser = build_parser()
|
|
args = parser.parse_args(argv)
|
|
registry = get_preset_registry()
|
|
|
|
if args.command == "list":
|
|
query = args.search.casefold().strip()
|
|
names = [
|
|
name for name in registry.names
|
|
if not query or query in name.casefold()
|
|
]
|
|
if not names:
|
|
print(f"No preset names match {args.search!r}", file=sys.stderr)
|
|
return 1
|
|
print("\n".join(names))
|
|
return 0
|
|
|
|
if args.command == "describe":
|
|
if args.preset not in registry:
|
|
print(f"Unknown DrawingML preset: {args.preset!r}", file=sys.stderr)
|
|
return 1
|
|
definition = registry.get(args.preset)
|
|
payload = {
|
|
"preset": definition.name,
|
|
"connector_preset": definition.name in CONNECTOR_PRESET_TYPES,
|
|
"adjustments": [
|
|
{"name": guide.name, "formula": guide.formula}
|
|
for guide in definition.adjustments
|
|
],
|
|
"path_count": len(definition.paths),
|
|
"connection_site_count": len(definition.connections),
|
|
"has_text_rectangle": definition.text_rectangle is not None,
|
|
}
|
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
return 0
|
|
|
|
try:
|
|
adjustments = _parse_adjustments(args.adjust)
|
|
style = _style_from_args(args)
|
|
fragment = render_preset_shape_fragment(
|
|
args.preset,
|
|
tuple(args.frame),
|
|
adjustments=adjustments,
|
|
object_kind=args.object_kind,
|
|
element_id=args.element_id,
|
|
name=args.name,
|
|
style=style,
|
|
)
|
|
except ValueError as exc:
|
|
print(f"Error: {exc}", file=sys.stderr)
|
|
return 1
|
|
print(fragment)
|
|
return 0
|
|
|
|
|
|
def _parse_adjustments(values: Sequence[str]) -> dict[str, str]:
|
|
adjustments: dict[str, str] = {}
|
|
for value in values:
|
|
name, separator, formula = value.partition("=")
|
|
name = name.strip()
|
|
formula = formula.strip()
|
|
if not separator or not name or not formula:
|
|
raise ValueError(
|
|
f"Invalid adjustment {value!r}; expected NAME=FORMULA"
|
|
)
|
|
if name in adjustments:
|
|
raise ValueError(f"Duplicate adjustment guide: {name!r}")
|
|
adjustments[name] = formula
|
|
return adjustments
|
|
|
|
|
|
def _style_from_args(args: argparse.Namespace) -> dict[str, str]:
|
|
style = {
|
|
"fill": args.fill,
|
|
"stroke": args.stroke,
|
|
}
|
|
if args.fill_opacity is not None:
|
|
style["fill-opacity"] = str(args.fill_opacity)
|
|
if args.stroke != "none":
|
|
style["stroke-width"] = str(
|
|
1.0 if args.stroke_width is None else args.stroke_width
|
|
)
|
|
elif args.stroke_width is not None:
|
|
raise ValueError("--stroke-width requires a non-none --stroke")
|
|
if args.stroke_opacity is not None:
|
|
style["stroke-opacity"] = str(args.stroke_opacity)
|
|
if args.stroke_linecap is not None:
|
|
style["stroke-linecap"] = args.stroke_linecap
|
|
if args.stroke_linejoin is not None:
|
|
style["stroke-linejoin"] = args.stroke_linejoin
|
|
return style
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|