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

plan_export.py

210 lines · skills/pptx-posters/scripts/plan_export.py at 5f2bc03

1#!/usr/bin/env python3
2"""Create a requirement-bound PowerPoint export and print plan."""
3
4from __future__ import annotations
5
6import argparse
7import sys
8from typing import Any
9
10from _common import CliError, emit_json
11from _manifest import load_and_validate_manifest
14def build_export_plan(
15 document: dict[str, Any],
16 validation: dict[str, Any],
17) -> dict[str, Any]:
18 canvas = validation["canvas"]
19 physical = validation["physical_output"]
20 conference = document["requirements"]["conference"]
21 printer = document["requirements"]["printer"]
22 delivery = conference["required_delivery_format"]
23 color_mode = printer["accepted_color_mode"]
24 print_scale = float(physical["print_scale"])
25 font_faces = sorted(
26 {
27 element["font_face"]
28 for element in document["elements"]
29 if element["type"] == "text"
30 }
31 )
33 blockers: list[dict[str, str]] = []
34 manual_actions: list[str] = [
35 "Open only the generated, technically inspected .pptx in a fully patched PowerPoint.",
36 "Run Review > Check Accessibility, inspect the Reading Order pane, and test keyboard/screen-reader navigation.",
37 "Inspect every edge, text box, figure, equation, glyph, and QR fallback at 100% and at final-output scale.",
38 "Confirm every declared font is installed and licensed on the review/export workstation; inspect substitution in PowerPoint and the exported PDF.",
39 ]
40 if color_mode == "CMYK":
41 blockers.append(
42 {
43 "code": "CMYK_CONVERSION_REQUIRED",
44 "message": (
45 "PowerPoint is an RGB authoring workflow. The confirmed printer "
46 "requires CMYK, so obtain a printer-approved conversion/profile "
47 "and proof; this tool cannot claim the PPTX or native PDF is "
48 "CMYK-compliant."
49 ),
50 }
51 )
52 elif color_mode == "PRINTER_MANAGED":
53 manual_actions.append(
54 "Ask the printer to document its RGB-to-output conversion/profile and approve a color proof."
55 )
56 else:
57 manual_actions.append(
58 "Confirm the printer accepts RGB and approve a physical or contract color proof."
59 )
61 if delivery in {"PDF", "PDF_AND_PPTX"}:
62 manual_actions.extend(
63 [
64 "In PowerPoint, export a PDF using Standard/high print quality rather than Minimum size.",
65 "Record the exporter and settings. On macOS, 'Best for electronic distribution and accessibility' uses Microsoft's online conversion service; use a local route when document transmission is not authorized, then inspect tag/link limitations.",
66 "Measure the exported PDF page size against the canvas; the physical artboard is a separate output target. Verify font rendering, tags, links, and image quality independently.",
67 ]
68 )
69 elif delivery == "OTHER":
70 blockers.append(
71 {
72 "code": "DELIVERY_FORMAT_REVIEW",
73 "message": (
74 "The conference requires an OTHER format; obtain and record the "
75 "exact organizer workflow before delivery."
76 ),
77 }
78 )
80 if float(physical["bleed_in"]) > 0:
81 manual_actions.append(
82 "The slide maps to the artboard including bleed. Confirm trim/crop handling with the printer; PowerPoint does not make this a press-ready preflight."
83 )
84 if abs(print_scale - 1.0) > 1e-6:
85 manual_actions.append(
86 f"Scale uniformly to {print_scale * 100.0:.4f}% at output; do not use nonuniform fit-to-page scaling. If delivery requires a full-size PDF, arrange an approved PDF scaling step and recheck page size, tags, fonts, and effective DPI. This plan does not resize or export a PDF."
87 )
88 else:
89 manual_actions.append("Output at 100% with no fit-to-page rescaling.")
91 manual_actions.extend(
92 [
93 "Run inspect_pptx.py, check_layout.py, inventory_images.py, and check_palette.py on the final source package.",
94 "Test-print a reduced-scale proof and obtain author sign-off on scientific content, citations, dimensions, color, and accessibility.",
95 "Retain the exact visible URL/text next to each QR code and test the final printed QR code with multiple devices.",
96 ]
97 )
98 return {
99 "schema_version": "1.0",
100 "manifest": {
101 "path": validation["manifest_path"],
102 "content_sha256": validation["content_sha256"],
103 "approval_status": validation["approval_status"],
104 },
105 "ready_for_manual_export": not blockers,
106 "dimensions": {
107 "powerpoint_canvas_width_in": float(canvas["width_in"]),
108 "powerpoint_canvas_height_in": float(canvas["height_in"]),
109 "final_trim_width_in": float(physical["trim_width_in"]),
110 "final_trim_height_in": float(physical["trim_height_in"]),
111 "bleed_in_each_edge": float(physical["bleed_in"]),
112 "final_artboard_width_in": float(physical["artboard_width_in"]),
113 "final_artboard_height_in": float(physical["artboard_height_in"]),
114 "safe_margin_in_inside_trim": float(physical["safe_margin_in"]),
115 "uniform_print_scale": print_scale,
116 "uniform_print_scale_percent": print_scale * 100.0,
117 },
118 "requirements": {
119 "conference_source_id": conference["source_id"],
120 "conference_max_width_in": conference["max_width_in"],
121 "conference_max_height_in": conference["max_height_in"],
122 "conference_orientation": conference["orientation"],
123 "conference_delivery_format": delivery,
124 "printer_source_id": printer["source_id"],
125 "printer_accepted_color_mode": color_mode,
126 "printer_scaling_allowed": printer["scaling_allowed"],
127 },
128 "powerpoint_constraints": {
129 "custom_dimension_range_in": [1.0, 56.0],
130 "all_slides_same_size": True,
131 "color_authoring_mode": "RGB",
132 "notice": (
133 "Physical trim size, PowerPoint canvas size, and output artboard "
134 "size are separate. Conference and printer records in this manifest, "
135 "not a generic poster preset, control delivery."
136 ),
137 },
138 "quality_thresholds": {
139 "minimum_font_pt_final": document["quality"][
140 "minimum_font_pt_final"
141 ],
142 "font_guidance_basis": document["quality"]["font_guidance_basis"],
143 "font_guidance_source_id": document["quality"][
144 "font_guidance_source_id"
145 ],
146 "minimum_raster_dpi_final": document["quality"][
147 "minimum_raster_dpi_final"
148 ],
149 "raster_dpi_basis": document["quality"]["raster_dpi_basis"],
150 "raster_dpi_source_id": document["quality"][
151 "raster_dpi_source_id"
152 ],
153 },
154 "font_preflight": {
155 "declared_font_faces": font_faces,
156 "fonts_embedded_by_generator": False,
157 "notice": (
158 "A font name in PresentationML does not prove availability, "
159 "embeddability, or correct rendering. PowerPoint and PDF review "
160 "remain required."
161 ),
162 },
163 "pdf_preflight": {
164 "exported_by_tool": False,
165 "canvas_width_pt": float(canvas["width_in"]) * 72.0,
166 "canvas_height_pt": float(canvas["height_in"]) * 72.0,
167 "final_artboard_width_pt": float(physical["artboard_width_in"]) * 72.0,
168 "final_artboard_height_pt": float(physical["artboard_height_in"]) * 72.0,
169 "full_size_pdf_requires_scaling": abs(print_scale - 1.0) > 1e-6,
170 },
171 "media_profile": {
172 "accepted_manifest_assets": ["PNG", "JPEG"],
173 "audio_video_or_linked_media_allowed": False,
174 },
175 "blockers": blockers,
176 "manual_actions": manual_actions,
177 }
180def build_parser() -> argparse.ArgumentParser:
181 parser = argparse.ArgumentParser(
182 description=(
183 "Create a no-network export/print plan from approved conference and "
184 "printer requirements, explicitly separating canvas, trim, bleed, "
185 "scaling, RGB/CMYK handling, and final-output quality."
186 )
187 )
188 parser.add_argument("manifest", help="approved local poster manifest JSON")
189 parser.add_argument("--output", help="optional new JSON plan path")
190 return parser
193def main(argv: list[str] | None = None) -> int:
194 parser = build_parser()
195 try:
196 args = parser.parse_args(argv)
197 _, document, validation = load_and_validate_manifest(
198 args.manifest,
199 verify_assets=False,
200 require_approval=True,
201 )
202 report = build_export_plan(document, validation)
203 emit_json(report, output=args.output)
204 return 0 if report["ready_for_manual_export"] else 1
205 except CliError as exc:
206 parser.exit(2, f"error: {exc}\n")
209if __name__ == "__main__":
210 sys.exit(main())