Release standalone PPT template workflow 0.5.0
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PPT Master - Shape Boolean SVG Fragment Tool
|
||||
|
||||
Combine closed SVG shapes or resolvable text outlines and print the resulting
|
||||
canonical SVG path fragment to stdout. Result geometry is in SVG-root coordinate
|
||||
space: replace the operands at their original z-order under the final semantic
|
||||
or structured parent, never under the old transformed ancestor. The source SVG
|
||||
is read-only; this tool never rewrites the page.
|
||||
|
||||
Usage:
|
||||
python3 scripts/shape_boolean_svg.py render SVG_FILE \
|
||||
--operation OPERATION --source ID --source ID --id OUTPUT_ID
|
||||
|
||||
Examples:
|
||||
python3 scripts/shape_boolean_svg.py render slide.svg \
|
||||
--operation intersect --source circle --source card --id overlap
|
||||
python3 scripts/shape_boolean_svg.py render slide.svg \
|
||||
--operation subtract --source body --source cutout --id result \
|
||||
--fill "#2563EB" --stroke none
|
||||
python3 scripts/shape_boolean_svg.py render cover.svg \
|
||||
--operation subtract --source scrim --source chapter-number --id reveal
|
||||
|
||||
Dependencies:
|
||||
skia-pathops, local PPT Master modules, and uharfbuzz for text operands
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Sequence
|
||||
|
||||
from console_encoding import configure_utf8_stdio
|
||||
|
||||
|
||||
configure_utf8_stdio()
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Print the Boolean result of closed SVG shapes or resolvable text "
|
||||
"outlines as canonical SVG path fragments in SVG-root coordinate "
|
||||
"space. Insert the result at the original z-order under the final "
|
||||
"semantic or structured parent, never under the old transformed "
|
||||
"ancestor. The source file is never modified."
|
||||
),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
render_parser = subparsers.add_parser(
|
||||
"render",
|
||||
help="Print SVG-root-coordinate Boolean-result paths to stdout.",
|
||||
)
|
||||
render_parser.add_argument(
|
||||
"svg_file",
|
||||
type=Path,
|
||||
help="Source SVG containing the operand elements.",
|
||||
)
|
||||
render_parser.add_argument(
|
||||
"--operation",
|
||||
required=True,
|
||||
choices=("union", "combine", "fragment", "intersect", "subtract"),
|
||||
help="PowerPoint-compatible merge-shapes operation.",
|
||||
)
|
||||
render_parser.add_argument(
|
||||
"--source",
|
||||
action="append",
|
||||
required=True,
|
||||
dest="source_ids",
|
||||
metavar="ID",
|
||||
help=(
|
||||
"Operand element id in merge order; repeat at least twice. "
|
||||
"The first operand is primary for subtract and style inheritance."
|
||||
),
|
||||
)
|
||||
render_parser.add_argument(
|
||||
"--id",
|
||||
required=True,
|
||||
dest="output_id",
|
||||
help="Stable id for the result, or the base id for fragment results.",
|
||||
)
|
||||
render_parser.add_argument(
|
||||
"--font-dir",
|
||||
action="append",
|
||||
default=[],
|
||||
dest="font_dirs",
|
||||
type=Path,
|
||||
metavar="PATH",
|
||||
help=(
|
||||
"Additional font directory for text operands; repeat as needed. "
|
||||
"Explicit directories are searched before system font directories."
|
||||
),
|
||||
)
|
||||
render_parser.add_argument(
|
||||
"--fill",
|
||||
help="Override the primary shape's solid SVG fill, or use none.",
|
||||
)
|
||||
render_parser.add_argument(
|
||||
"--fill-opacity",
|
||||
type=float,
|
||||
help="Override fill opacity from 0 to 1.",
|
||||
)
|
||||
render_parser.add_argument(
|
||||
"--stroke",
|
||||
help="Override the primary shape's solid SVG stroke, or use none.",
|
||||
)
|
||||
render_parser.add_argument(
|
||||
"--stroke-width",
|
||||
type=float,
|
||||
help="Override stroke width in SVG page units.",
|
||||
)
|
||||
render_parser.add_argument(
|
||||
"--stroke-opacity",
|
||||
type=float,
|
||||
help="Override stroke opacity from 0 to 1.",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
try:
|
||||
style = _style_from_args(args)
|
||||
_validate_render_args(args)
|
||||
from svg_to_pptx.shape_boolean import (
|
||||
render_boolean_svg_fragments,
|
||||
)
|
||||
|
||||
fragment = render_boolean_svg_fragments(
|
||||
args.svg_file,
|
||||
operation=args.operation,
|
||||
source_ids=args.source_ids,
|
||||
output_id=args.output_id,
|
||||
style=style or None,
|
||||
font_dirs=args.font_dirs,
|
||||
)
|
||||
except (OSError, RuntimeError, ValueError) as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(fragment)
|
||||
return 0
|
||||
|
||||
|
||||
def _validate_render_args(args: argparse.Namespace) -> None:
|
||||
if len(args.source_ids) < 2:
|
||||
raise ValueError("--source must be repeated at least twice")
|
||||
if len(set(args.source_ids)) != len(args.source_ids):
|
||||
raise ValueError("--source ids must be unique")
|
||||
if not args.output_id.strip():
|
||||
raise ValueError("--id must not be empty")
|
||||
for font_dir in args.font_dirs:
|
||||
if not font_dir.is_dir():
|
||||
raise ValueError(f"--font-dir is not a directory: {font_dir}")
|
||||
|
||||
|
||||
def _style_from_args(args: argparse.Namespace) -> dict[str, str]:
|
||||
style: dict[str, str] = {}
|
||||
if args.fill is not None:
|
||||
style["fill"] = args.fill
|
||||
if args.fill_opacity is not None:
|
||||
_validate_opacity("--fill-opacity", args.fill_opacity)
|
||||
style["fill-opacity"] = str(args.fill_opacity)
|
||||
if args.stroke is not None:
|
||||
style["stroke"] = args.stroke
|
||||
if args.stroke_width is not None:
|
||||
if not math.isfinite(args.stroke_width) or args.stroke_width < 0:
|
||||
raise ValueError("--stroke-width must be greater than or equal to 0")
|
||||
style["stroke-width"] = str(args.stroke_width)
|
||||
if args.stroke_opacity is not None:
|
||||
_validate_opacity("--stroke-opacity", args.stroke_opacity)
|
||||
style["stroke-opacity"] = str(args.stroke_opacity)
|
||||
return style
|
||||
|
||||
|
||||
def _validate_opacity(option: str, value: float) -> None:
|
||||
if not math.isfinite(value) or not 0 <= value <= 1:
|
||||
raise ValueError(f"{option} must be between 0 and 1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user