Skill Tracer
Skills › Scientific Agent Skills › pptx-posters › check_layout.py

check_layout.py

179 lines · skills/pptx-posters/scripts/check_layout.py at 5f2bc03

1#!/usr/bin/env python3
2"""Check PPTX shape bounds, overlap, reading order, and final font size."""
3
4from __future__ import annotations
5
6import argparse
7import math
8import sys
9from typing import Any
11from _common import CliError, emit_json
12from _manifest import load_and_validate_manifest
13from _pptx import analyze_layout
16def _positive_float(value: str) -> float:
17 try:
18 number = float(value)
19 except ValueError as exc:
20 raise argparse.ArgumentTypeError("expected a number") from exc
21 if not math.isfinite(number) or number <= 0:
22 raise argparse.ArgumentTypeError("value must be finite and greater than zero")
23 return number
26def build_parser() -> argparse.ArgumentParser:
27 parser = argparse.ArgumentParser(
28 description=(
29 "Inspect direct PPTX shape bounding boxes for overlap and out-of-bounds "
30 "placement, list package reading order, and screen explicit font sizes "
31 "at final print scale. No rendering or network access occurs."
32 )
33 )
34 parser.add_argument("pptx", help="local macro-free .pptx file")
35 source = parser.add_mutually_exclusive_group()
36 source.add_argument(
37 "--manifest",
38 help="approved poster manifest supplying print scale and font requirement",
39 )
40 source.add_argument(
41 "--print-scale",
42 type=_positive_float,
43 default=None,
44 help="physical-artboard/canvas scale when no manifest is supplied",
45 )
46 parser.add_argument(
47 "--minimum-font-pt-final",
48 type=_positive_float,
49 help=(
50 "final-output font threshold; default is manifest value or an explicit "
51 "18 pt project heuristic"
52 ),
53 )
54 parser.add_argument("--output", help="optional new JSON report path")
55 return parser
58def _manifest_settings(path: str) -> tuple[float, float, dict[str, Any]]:
59 _, document, validation = load_and_validate_manifest(
60 path,
61 verify_assets=False,
62 require_approval=True,
63 )
64 scale = float(validation["physical_output"]["print_scale"])
65 minimum = float(document["quality"]["minimum_font_pt_final"])
66 basis = {
67 "kind": document["quality"]["font_guidance_basis"],
68 "source_id": document["quality"]["font_guidance_source_id"],
69 }
70 return scale, minimum, {
71 "document": document,
72 "validation": validation,
73 "font_basis": basis,
74 }
77def apply_manifest_checks(
78 report: dict[str, Any],
79 document: dict[str, Any],
80 validation: dict[str, Any],
81) -> None:
82 """Append approved-canvas and direct reading-order mismatches to a report."""
83 expected = validation["canvas"]
84 actual = report["slide_size"]
85 if (
86 abs(float(expected["width_in"]) - float(actual["width_in"])) > 0.001
87 or abs(float(expected["height_in"]) - float(actual["height_in"])) > 0.001
88 ):
89 report["issues"].append(
90 {
91 "code": "CANVAS_MISMATCH",
92 "message": "PPTX slide dimensions do not match the approved manifest",
93 "expected": expected,
94 "actual": actual,
95 }
96 )
97 expected_order = [
98 {
99 "order": int(element["reading_order"]),
100 "name": (
101 f"R{int(element['reading_order']):03d}_"
102 f"{'TEXT' if element['type'] == 'text' else 'IMAGE'}_"
103 f"{element['id']}"
104 ),
105 }
106 for element in document["elements"]
107 ]
108 actual_order = (
109 report["slides"][0]["reading_order"] if report["slides"] else []
110 )
111 if actual_order != expected_order:
112 report["issues"].append(
113 {
114 "code": "READING_ORDER_MISMATCH",
115 "message": (
116 "direct PPTX shape order/names do not match the approved manifest"
117 ),
118 "expected": expected_order,
119 "actual": actual_order,
120 }
121 )
124def main(argv: list[str] | None = None) -> int:
125 parser = build_parser()
126 try:
127 args = parser.parse_args(argv)
128 manifest_context: dict[str, Any] | None = None
129 if args.manifest:
130 print_scale, manifest_minimum, manifest_context = _manifest_settings(
131 args.manifest
132 )
133 minimum_font = (
134 args.minimum_font_pt_final
135 if args.minimum_font_pt_final is not None
136 else manifest_minimum
137 )
138 else:
139 print_scale = args.print_scale if args.print_scale is not None else 1.0
140 minimum_font = (
141 args.minimum_font_pt_final
142 if args.minimum_font_pt_final is not None
143 else 18.0
144 )
145 report = analyze_layout(
146 args.pptx,
147 print_scale=print_scale,
148 minimum_font_pt_final=minimum_font,
149 )
150 if manifest_context is not None:
151 validation = manifest_context["validation"]
152 apply_manifest_checks(
153 report,
154 manifest_context["document"],
155 validation,
156 )
157 report["manifest"] = {
158 "path": validation["manifest_path"],
159 "content_sha256": validation["content_sha256"],
160 }
161 report["minimum_font_basis"] = manifest_context["font_basis"]
162 else:
163 report["minimum_font_basis"] = {
164 "kind": (
165 "caller_supplied"
166 if args.minimum_font_pt_final is not None
167 else "project_heuristic"
168 ),
169 "source_id": None,
170 }
171 report["pass"] = not report["issues"]
172 emit_json(report, output=args.output)
173 return 0 if report["pass"] else 1
174 except CliError as exc:
175 parser.exit(2, f"error: {exc}\n")
178if __name__ == "__main__":
179 sys.exit(main())