#!/usr/bin/env python3 """ PPT Master - SVG Coordinate Compactor Compact model-facing page-space SVG coordinates to at most two decimal places without rounding normalized crop ratios or transform linear coefficients. Usage: python3 scripts/compact_svg_coordinates.py [--inplace] Examples: python3 scripts/compact_svg_coordinates.py projects/example/templates --inplace python3 scripts/compact_svg_coordinates.py imported/authoring-svg Dependencies: None (standard library only). """ from __future__ import annotations import argparse import json import math import re import stat import sys import tempfile from dataclasses import dataclass from pathlib import Path from typing import Optional from xml.etree import ElementTree as ET from console_encoding import configure_utf8_stdio configure_utf8_stdio() COORDINATE_DECIMAL_PLACES = 2 _NUMBER_TOKEN = r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?" _NUMBER_RE = re.compile(rf"^{_NUMBER_TOKEN}$") _TRANSFORM_FUNCTION_RE = re.compile(r"([A-Za-z]+)\s*\(([^)]*)\)") _COMPACTABLE_ATTRIBUTE_RE = re.compile( r"(?data-pptx-frame|data-pptx-bounds|transform)" r"(?P\s*=\s*)" r"(?P[\"'])" r"(?P.*?)" r"(?P=quote)", re.DOTALL, ) @dataclass class CoordinateCompactionStats: """Count safely compacted coordinate-bearing SVG attributes.""" native_frames: int = 0 bounds: int = 0 transforms: int = 0 @property def changed_attributes(self) -> int: return ( self.native_frames + self.bounds + self.transforms ) def merge(self, other: "CoordinateCompactionStats") -> None: self.native_frames += other.native_frames self.bounds += other.bounds self.transforms += other.transforms def as_dict(self) -> dict[str, int]: return { "native_frames": self.native_frames, "bounds": self.bounds, "transforms": self.transforms, "changed_attributes": self.changed_attributes, } def _number_tokens(value: str) -> list[str] | None: stripped = value.strip() if not stripped: return None tokens = re.split(r"[\s,]+", stripped) if not tokens or any(_NUMBER_RE.fullmatch(token) is None for token in tokens): return None return tokens def format_coordinate(value: str | float) -> str: """Format one finite page-space coordinate with at most two decimals.""" numeric = float(value) if not math.isfinite(numeric): raise ValueError(f"Coordinate is not finite: {value!r}") compact = ( f"{numeric:.{COORDINATE_DECIMAL_PLACES}f}".rstrip("0").rstrip(".") ) return "0" if compact in {"", "-0"} else compact def _compact_coordinate_quad(value: str) -> str: tokens = _number_tokens(value) if tokens is None or len(tokens) != 4: return value return " ".join(format_coordinate(token) for token in tokens) def _compact_transform(value: str) -> str: def replace(match: re.Match[str]) -> str: name, arguments = match.groups() tokens = _number_tokens(arguments) if tokens is None: return match.group(0) lowered = name.lower() compacted: list[str] if lowered == "translate" and len(tokens) in {1, 2}: compacted = [format_coordinate(token) for token in tokens] elif lowered == "rotate" and len(tokens) == 3: compacted = [ tokens[0], format_coordinate(tokens[1]), format_coordinate(tokens[2]), ] elif lowered == "matrix" and len(tokens) == 6: compacted = [ *tokens[:4], format_coordinate(tokens[4]), format_coordinate(tokens[5]), ] else: return match.group(0) return f"{name}({' '.join(compacted)})" return _TRANSFORM_FUNCTION_RE.sub(replace, value) def _compact_attribute_value( name: str, value: str, *, compact_native_frames: bool, ) -> str: if name == "data-pptx-frame": return _compact_coordinate_quad(value) if compact_native_frames else value if name == "data-pptx-bounds": return _compact_coordinate_quad(value) if name == "transform": return _compact_transform(value) return value def _record_change(stats: CoordinateCompactionStats, name: str) -> None: if name == "data-pptx-frame": stats.native_frames += 1 elif name == "data-pptx-bounds": stats.bounds += 1 elif name == "transform": stats.transforms += 1 def compact_svg_tree( root: ET.Element, *, compact_native_frames: bool = True, ) -> CoordinateCompactionStats: """Compact safe coordinate metadata in one parsed SVG tree.""" stats = CoordinateCompactionStats() for element in root.iter(): for name in ( "data-pptx-frame", "data-pptx-bounds", "transform", ): current = element.get(name) if current is None: continue compacted = _compact_attribute_value( name, current, compact_native_frames=compact_native_frames, ) if compacted == current: continue element.set(name, compacted) _record_change(stats, name) return stats def compact_svg_text( text: str, *, compact_native_frames: bool = True, ) -> tuple[str, CoordinateCompactionStats]: """Compact safe coordinates while preserving unrelated SVG formatting.""" stats = CoordinateCompactionStats() def replace(match: re.Match[str]) -> str: name = match.group("name") current = match.group("value") compacted = _compact_attribute_value( name, current, compact_native_frames=compact_native_frames, ) if compacted == current: return match.group(0) _record_change(stats, name) return ( f"{name}{match.group('spacing')}{match.group('quote')}" f"{compacted}{match.group('quote')}" ) return _COMPACTABLE_ATTRIBUTE_RE.sub(replace, text), stats def _svg_files(input_path: Path) -> list[Path]: if input_path.is_file(): return [input_path] if input_path.suffix.lower() == ".svg" else [] return sorted(path for path in input_path.rglob("*.svg") if path.is_file()) def _write_atomic(path: Path, payload: str) -> None: mode = stat.S_IMODE(path.stat().st_mode) with tempfile.NamedTemporaryFile( mode="w", encoding="utf-8", newline="\n", prefix=f".{path.name}.", suffix=".tmp", dir=path.parent, delete=False, ) as handle: temporary_path = Path(handle.name) handle.write(payload) try: temporary_path.chmod(mode) temporary_path.replace(path) except OSError: temporary_path.unlink(missing_ok=True) raise def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description=( "Compact safe page-space SVG coordinates to at most two decimal " "places. Runs as a dry-run unless --inplace is supplied." ), ) parser.add_argument("input", type=Path, help="SVG file or directory") parser.add_argument( "--inplace", action="store_true", help="Atomically replace changed SVG files", ) parser.add_argument( "--keep-native-frames", action="store_true", help=( "Leave data-pptx-frame unchanged while compacting " "data-pptx-bounds and transform translations" ), ) return parser def main(argv: Optional[list[str]] = None) -> int: args = build_parser().parse_args(argv) input_path = args.input.resolve() if not input_path.exists(): print( json.dumps( {"error": f"Input does not exist: {input_path}"}, ensure_ascii=False, ), file=sys.stderr, ) return 1 paths = _svg_files(input_path) if not paths: print( json.dumps( {"error": f"No SVG files found under {input_path}"}, ensure_ascii=False, ), file=sys.stderr, ) return 1 staged: list[tuple[Path, str]] = [] totals = CoordinateCompactionStats() bytes_before = 0 bytes_after = 0 try: for path in paths: original = path.read_text(encoding="utf-8") ET.fromstring(original) compacted, stats = compact_svg_text( original, compact_native_frames=not args.keep_native_frames, ) totals.merge(stats) before = len(original.encode("utf-8")) after = len(compacted.encode("utf-8")) bytes_before += before bytes_after += after if compacted != original: staged.append((path, compacted)) except (OSError, UnicodeDecodeError, ET.ParseError, ValueError) as exc: print( json.dumps({"error": str(exc)}, ensure_ascii=False), file=sys.stderr, ) return 1 if args.inplace: try: for path, payload in staged: _write_atomic(path, payload) except OSError as exc: print( json.dumps({"error": str(exc)}, ensure_ascii=False), file=sys.stderr, ) return 1 print(json.dumps({ "input": str(input_path), "inplace": args.inplace, "files_scanned": len(paths), "files_changed": len(staged), "bytes_before": bytes_before, "bytes_after": bytes_after, "bytes_saved": bytes_before - bytes_after, "coordinates": totals.as_dict(), }, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())