Firemedic15's picture
download
raw
22.3 kB
#!/usr/bin/env python3
"""poster_check — unified CLI for HTML academic posters.
Six subcommands:
measure Print-emulate HTML in headless Chromium, measure all
``[data-measure-role]`` elements, report column-bottom
spread and gap to the next horizontal strip. The HARD
alignment gate (spread < 5 px is non-negotiable).
Carries the loop circuit breaker: consecutive failed
measurements are counted on disk; at the cap it exits
3 instead of measuring again (``--measure-budget`` /
``--reset-budget``).
pack ADVISORY column-feasibility pre-check -- run ONCE
before entering the measure loop. Probes each card
figure at its Gate A width-band endpoints in the
browser and reports which columns cannot reach the
footer-gap window by figure resizing alone
(REPACK_RECOMMENDED / FIGURE_ONLY_UNDERFILL).
fit-logos ADVISORY logo-zone packer (read-only). Measures the
header's logo zone and prints the row arrangement
that maximises the one uniform mark height, plus a
paste-ready snippet. Never edits the file: the agent
judges the proposal (optical weight, Gate E) and
applies it by hand -- or not at all.
preflight Static HTML scan: LaTeX residue, raw '<' inside
``$…$`` / ``$$…$$`` / ``\\(…\\)`` / ``\\[…\\]``,
missing local images, missing data-measure-role.
polish Visual-polish warnings on figure sizing, broken
images, typography orphans, and space-between fill.
Soft gate; warns by default. ``--strict`` to fail.
Hard-fails if there's no measurement markup at all.
verify-final Run ``pdfinfo`` on a rendered PDF; check page count,
dimensions match the expected canvas (``--canvas`` or
``--from-html``), and file size under a limit.
All logic lives in the ``_posterly`` package next to this file. This
script is a thin argparse dispatcher.
"""
from __future__ import annotations
import argparse
import os
import sys
# Make `_posterly` importable when this file is run directly via
# `python tools/poster_check.py …`.
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
if _THIS_DIR not in sys.path:
sys.path.insert(0, _THIS_DIR)
from _posterly import budget as _budget # noqa: E402
from _posterly import canvas as _canvas # noqa: E402
from _posterly import fitlogos as _fitlogos # noqa: E402
from _posterly import measure as _measure # noqa: E402
from _posterly import pack as _pack # noqa: E402
from _posterly import polish as _polish # noqa: E402
from _posterly import preflight as _preflight # noqa: E402
from _posterly import verify_final as _verify_final # noqa: E402
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="poster_check",
description="Measure / pack / preflight / polish / verify a "
"poster HTML+PDF pair.",
)
sub = p.add_subparsers(dest="cmd", required=True)
# --- measure --------------------------------------------------------
pm = sub.add_parser(
"measure",
help="alignment + gap gate (print-emulated, HARD gate)",
)
pm.add_argument("html", help="path to poster.html")
pm.add_argument(
"--max-spread", type=float, default=5.0,
help="hard gate: max column-bottom spread in px "
"(default 5.0; aim < 3.0)",
)
pm.add_argument(
"--min-gap", type=float, default=30.0,
help="min gap to footer-strip/footer (default 30 px)",
)
pm.add_argument(
"--max-gap", type=float, default=50.0,
help="max gap to footer-strip/footer (default 50 px)",
)
pm.add_argument(
"--canvas", type=_canvas.parse_canvas_arg, default=None,
help="override canvas (e.g. '60x36in' or 'A0 portrait'); "
"by default we parse @page from the HTML",
)
pm.add_argument(
"--allow-empty-column", action="store_true",
help="don't fail when a column has no cards "
"(fallback to column.bottom; risky)",
)
pm.add_argument(
"--allow-no-footer-gap", action="store_true",
help="don't fail when neither footer-strip nor footer exists "
"below the content",
)
pm.add_argument(
"--settle-ms", type=int, default=500,
help="extra wait after MathJax + fonts.ready settle "
"(default 500)",
)
pm.add_argument(
"--mathjax-timeout-ms", type=int, default=15000,
help="hard timeout for MathJax typeset (default 15000); "
"exceeding it FAILS the gate, not warns",
)
pm.add_argument(
"--min-canvas-fill", type=float, default=0.95,
help="hard gate: [data-measure-role='poster'] must fill at "
"least this fraction of the print viewport in BOTH "
"dimensions (default 0.95). Catches the silent 'forgot "
"the print media-query unit override' bug where the "
"poster renders at screen scale into a much bigger "
"print page.",
)
pm.add_argument(
"--max-canvas-fill", type=float, default=1.01,
help="hard gate: poster must NOT exceed this fraction of the "
"print viewport (default 1.01; i.e. <=1%% overshoot is "
"tolerated for sub-pixel rounding). The symmetric of "
"--min-canvas-fill catches the case where a hardcoded "
"`width` in px exceeds `@page size`.",
)
pm.add_argument(
"--position-tol-px", type=float, default=2.0,
help="hard gate: poster's bbox edges must align with the print "
"viewport's origin within this many px (default 2.0). "
"Catches `transform: translate*`, mis-positioned absolute "
"layout, and stray body margin in print.",
)
pm.add_argument(
"--max-clip-px", type=float, default=2.0,
help="hard gate: a card/column/hero whose content is clipped by "
"overflow:hidden|clip|scroll|auto by MORE than this many px "
"(scrollHeight-clientHeight) FAILS -- clipped content is "
"silently lost in print while the box still looks aligned. "
"Also the tolerance for the poster-root canvas-overflow gate "
"(content spilling past the canvas), so raising it relaxes "
"both (default 2.0; sub-pixel rounding tolerated).",
)
pm.add_argument(
"--max-intercard-gap", type=float,
default=_measure.DEFAULT_MAX_INTERCARD_GAP,
help="hard gate: max whitespace between consecutive stacked "
"cards in a column (default 50 px, same ceiling as the "
"footer gap). Catches `justify-content: space-between` "
"faking bottom alignment on an under-filled column -- "
"spread reads ~0 while a void sits mid-column.",
)
pm.add_argument(
"--min-intercard-gap", type=float,
default=_measure.DEFAULT_MIN_INTERCARD_GAP,
help="hard gate: min whitespace between consecutive stacked "
"cards (default 12 px). Tighter gaps bury the card's drop "
"shadow (templates ship `0 2u 6u`) under the next card, "
"fusing the stack into one slab. Set 0 to disable for "
"shadowless themes.",
)
pm.add_argument(
"--with-polish", action="store_true",
help="also run the visual-polish pass (figure sizing, orphans, "
"space-between, header logos, ...) on the SAME rendered "
"page -- one browser launch per loop round instead of two. "
"ADVISORY: the polish report prints at default thresholds "
"and never changes measure's exit code; the final soft "
"gate remains a standalone `polish` run (--strict).",
)
pm.add_argument(
"--json-out", default=None,
help="dump raw measurement to JSON",
)
pm.add_argument(
"--measure-budget", type=int,
default=_budget.DEFAULT_MEASURE_BUDGET,
help="circuit breaker: exit 3 after this many CONSECUTIVE "
"failed measurements on this poster. The counter persists "
"on disk next to the HTML, resets on the first PASS or "
"after 12h idle. 0 disables. (default %(default)s)",
)
pm.add_argument(
"--reset-budget", action="store_true",
help="zero the on-disk failure counter before measuring (a "
"deliberate fresh start, e.g. after a big re-pack). "
"Honoured even with --measure-budget 0: the state file is "
"cleared regardless of whether the breaker is enabled.",
)
pm.set_defaults(func=_measure.cmd_measure)
# --- pack -------------------------------------------------------------
pk = sub.add_parser(
"pack",
help="ADVISORY column-feasibility pre-check (run once BEFORE "
"the measure loop)",
)
pk.add_argument("html", help="path to poster.html")
pk.add_argument(
"--canvas", type=_canvas.parse_canvas_arg, default=None,
help="override canvas (default: parse @page from HTML)",
)
pk.add_argument(
"--max-spread", type=float, default=5.0,
help="spread threshold used for the cross-column feasibility "
"bound (default 5.0; keep in sync with measure)",
)
pk.add_argument(
"--min-gap", type=float, default=30.0,
help="min gap to footer-strip/footer (default 30 px)",
)
pk.add_argument(
"--max-gap", type=float, default=50.0,
help="max gap to footer-strip/footer (default 50 px)",
)
pk.add_argument(
"--wide-min-ratio", type=float, default=_pack.AR_WIDE_MIN,
help="Gate A floor probed for wide figures (AR>1.3); keep in "
"sync with polish (default %(default)s)",
)
pk.add_argument(
"--square-min-ratio", type=float, default=_pack.AR_SQUARE_MIN,
help="Gate A floor probed for square figures (0.8<=AR<=1.3); "
"keep in sync with polish (default %(default)s)",
)
pk.add_argument(
"--tall-min-ratio", type=float, default=_pack.AR_TALL_MIN,
help="Gate A floor probed for tall figures (AR<0.8); keep in "
"sync with polish (default %(default)s)",
)
pk.add_argument(
"--settle-ms", type=int, default=500,
help="extra wait after layout settles (default 500)",
)
pk.add_argument(
"--mathjax-timeout-ms", type=int, default=15000,
help="hard timeout for MathJax typeset (default 15000)",
)
pk.add_argument(
"--strict", action="store_true",
help="exit 1 when any column is REPACK_RECOMMENDED / "
"FIGURE_ONLY_UNDERFILL or the cross-column bound trips "
"(default: always exit 0 -- advisory)",
)
pk.add_argument(
"--json-out", default=None,
help="write the pack report as JSON",
)
pk.set_defaults(func=_pack.cmd_pack)
# --- fit-logos ------------------------------------------------------
fl = sub.add_parser(
"fit-logos",
help="ADVISORY logo-zone packer: print the max-uniform-height "
"row arrangement for the header logos (read-only)",
)
fl.add_argument("html", help="path to poster.html")
fl.add_argument(
"--canvas", type=_canvas.parse_canvas_arg, default=None,
help="override canvas (default: parse @page from HTML)",
)
fl.add_argument(
"--zone", default=None,
help="CSS selector for the logo zone(s). When given, ONLY this "
"selector is used -- no automatic discovery, and zero "
"matches is reported, never silently replaced. Default "
"discovery: any [data-logo-zone]; else the UNION of "
"data-lf-h0-stamped zones, .header .logo-row, and "
"standalone .header .logo-slot (nested candidates resolve "
"stamp > row > slot, outer wins ties) -- rows/slots INSIDE "
"an applied logo-pack are never auto-discovered",
)
fl.add_argument(
"--max-rows", type=int, default=_fitlogos.DEFAULT_MAX_ROWS,
help="row-partition search cap (default %(default)s)",
)
fl.add_argument(
"--hgap", type=float, default=None,
help="horizontal gap between marks in a row, px at true canvas "
"scale (default: the zone's own computed flex gap, else "
f"{_fitlogos.DEFAULT_HGAP_PX:.0f})",
)
fl.add_argument(
"--settle-ms", type=int, default=500,
help="extra wait after layout settles (default 500)",
)
fl.add_argument(
"--mathjax-timeout-ms", type=int, default=15000,
help="hard timeout for MathJax typeset (default 15000)",
)
fl.set_defaults(func=_fitlogos.cmd_fit_logos)
# --- preflight ------------------------------------------------------
pp = sub.add_parser(
"preflight",
help="static HTML lint (LaTeX residue, math, images, roles)",
)
pp.add_argument("html", help="path to poster.html")
pp.set_defaults(func=_preflight.cmd_preflight)
# --- polish ---------------------------------------------------------
ppl = sub.add_parser(
"polish",
help="visual-polish warnings (figure size, orphans, "
"space-between, flex/<br>, header logos)",
)
ppl.add_argument("html", help="path to poster.html")
ppl.add_argument(
"--canvas", type=_canvas.parse_canvas_arg, default=None,
help="override canvas (default: parse @page from HTML)",
)
ppl.add_argument(
"--settle-ms", type=int, default=500,
help="extra wait after layout settles (default 500)",
)
ppl.add_argument(
"--mathjax-timeout-ms", type=int, default=15000,
help="hard timeout for MathJax typeset (default 15000)",
)
ppl.add_argument(
"--wide-min-ratio", type=float,
default=_polish.DEFAULT_WIDE_MIN_RATIO,
help="wide figures (AR>1.3) must occupy >= this fraction of "
"card width (default %(default)s -- the defect FLOOR; a "
"figure-dominant card reads best at 90-100%%)",
)
ppl.add_argument(
"--tall-max-ratio", type=float,
default=_polish.DEFAULT_TALL_MAX_RATIO,
help="tall figures (AR<0.8) above this fraction trigger a "
"text-right recommendation (default %(default)s)",
)
ppl.add_argument(
"--tall-min-ratio", type=float,
default=_polish.DEFAULT_TALL_MIN_RATIO,
help="tall figures (AR<0.8) BELOW this fraction of card width "
"trigger FIG/TALL-SMALL (small + wide side margins); enlarge "
"or wrap text. Hard floor, not the ideal (aim 45-60%%) "
"(default %(default)s)",
)
ppl.add_argument(
"--square-min-ratio", type=float,
default=_polish.DEFAULT_SQUARE_MIN_RATIO,
help="square figures (0.8<=AR<=1.3) must occupy >= this "
"fraction (default %(default)s)",
)
ppl.add_argument(
"--hero-letterbox-fill", type=float,
default=_polish.DEFAULT_HERO_LETTERBOX_FILL,
help="HERO/STAGE-LETTERBOX: a hero figure filling BELOW this "
"fraction of its hero-stage width (while the stage is much "
"wider than the image AR, see --hero-letterbox-ar-mult) leaves "
"big symmetric side voids (default %(default)s)",
)
ppl.add_argument(
"--hero-letterbox-ar-mult", type=float,
default=_polish.DEFAULT_HERO_LETTERBOX_AR_MULT,
help="HERO/STAGE-LETTERBOX trips only when stage_AR / image_AR "
"exceeds this -- i.e. the stage is this many times wider "
"(relative to the image) than the image needs (default "
"%(default)s); guards against flagging a genuine full-bleed "
"hero where image AR ~= stage AR",
)
ppl.add_argument(
"--beside-void-ratio", type=float,
default=_polish.DEFAULT_BESIDE_VOID_RATIO,
help="FIG/BESIDE-TEXT-VOID: a figure floated beside text whose "
"text stops more than this fraction of the figure height "
"short of its bottom leaves an L-shaped void (default "
"%(default)s)",
)
ppl.add_argument(
"--max-space-between-fill", type=float,
default=_polish.DEFAULT_MAX_SPACE_BETWEEN_FILL,
help="warn if a space-between column has an inter-card gap "
"exceeding this fraction of column height "
"(default %(default)s)",
)
ppl.add_argument(
"--max-card-trailing", type=float,
default=_polish.DEFAULT_MAX_CARD_TRAILING,
help="warn (CARD/TRAILING) if a card leaves more than this "
"fraction of its height blank below the last line "
"(default %(default)s); catches a flex-stretched card "
"padded with whitespace to fake a full page",
)
ppl.add_argument(
"--max-card-inner-void", type=float,
default=_polish.DEFAULT_CARD_INNER_VOID,
help="warn (CARD/INNER-VOID) if a card's largest gap between two "
"stacked children exceeds the stated row-gap by more than this "
"fraction of card height (default %(default)s); catches a "
"bottom-pinned tail (margin-top:auto / space-*) opening a void "
"in the middle of a stretched equal-height card",
)
ppl.add_argument(
"--min-card-inner-void-px", type=float,
default=_polish.DEFAULT_CARD_INNER_VOID_PX,
help="CARD/INNER-VOID absolute floor in px (default %(default)s); a "
"gap must exceed BOTH this and --max-card-inner-void to flag, "
"so a sub-line gap on a small card stays quiet. The same "
"ratio+floor also drive TRACK/INNER-VOID on header/footer "
"tracks (a vertical masthead spine or side rail)",
)
ppl.add_argument(
"--min-contrast", type=float,
default=_polish.DEFAULT_MIN_CONTRAST,
help="warn (CONTRAST) if a rendered text run's WCAG ratio against "
"the solid ground beneath it falls below this (default "
"%(default)s -- flags only unambiguous defects; deliberate "
"muted inks sit ~3.5+; values above 7.0 are not supported -- "
"the collector only ships sub-7 samples). Text over "
"images/gradients/translucency is skipped",
)
ppl.add_argument(
"--min-unprotected-wraps", type=int,
default=_polish.DEFAULT_MIN_UNPROTECTED_WRAPS,
help="aggregate TEXT-WRAP note fires when at least this many "
"wrapped text blocks carry neither text-wrap: pretty nor "
"balance (default %(default)s) -- the dropped-base-defenses "
"signature of a custom skeleton",
)
ppl.add_argument(
"--logo-max-width-ratio", type=float,
default=_polish.DEFAULT_LOGO_MAX_WIDTH_RATIO,
help="warn (LOGO/WIDE) if a header logo renders wider than this "
"fraction of the header width (default %(default)s)",
)
ppl.add_argument(
"--logo-qr-tol", type=float,
default=_polish.DEFAULT_LOGO_QR_TOL,
help="warn (LOGO/QR-MISMATCH) if a non-wide header logo's height "
"differs from the QR's by more than this fraction; logo-wide "
"slots use a band instead (default %(default)s)",
)
ppl.add_argument(
"--rightblock-max-ratio", type=float,
default=_polish.DEFAULT_RIGHTBLOCK_MAX_RATIO,
help="warn (HEADER/TITLE-SQUEEZED) if the header's right block "
"exceeds this fraction of header width (default %(default)s)",
)
ppl.add_argument(
"--title-min-ratio", type=float,
default=_polish.DEFAULT_TITLE_MIN_RATIO,
help="warn (HEADER/TITLE-SQUEEZED) if the title block falls below "
"this fraction of header width (default %(default)s)",
)
ppl.add_argument(
"--title-offset-max", type=float,
default=_polish.DEFAULT_TITLE_OFFSET_MAX,
help="warn (HEADER/TITLE-OFFCENTER) if the title block's centre is "
"off the header's centre line by more than this fraction of "
"header width -- a heavier side block (logo/venue/QR) is "
"pulling the centred title aside (default %(default)s)",
)
ppl.add_argument(
"--strict", action="store_true",
help="exit non-zero when any warning is emitted",
)
ppl.set_defaults(func=_polish.cmd_polish)
# --- verify-final ---------------------------------------------------
pv = sub.add_parser(
"verify-final",
help="run pdfinfo + size/dimension/page checks on PDF",
)
pv.add_argument("pdf", help="path to poster.pdf")
pv.add_argument(
"--canvas", type=_canvas.parse_canvas_arg, default=None,
help="expected canvas (e.g. '60x36in' or 'A0 portrait'); "
"either this or --from-html is required",
)
pv.add_argument(
"--from-html", default=None,
help="read expected canvas from `@page { size }` in this "
"HTML; mutually exclusive with --canvas",
)
pv.add_argument(
"--dim-tol-in", type=float, default=0.05,
help="dimension tolerance in inches (default 0.05)",
)
pv.add_argument(
"--max-size-mb", type=float, default=20.0,
help="max file size in MB (default 20)",
)
pv.add_argument(
"--allow-rotated", action="store_true",
help="accept swapped W/H even when PDF declares no page "
"rotation (most posters should NOT need this)",
)
pv.set_defaults(func=_verify_final.cmd_verify_final)
return p
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
return args.func(args)
if __name__ == "__main__":
sys.exit(main())

Xet Storage Details

Size:
22.3 kB
·
Xet hash:
48fc675c27b1266ce0e1a81330b4f46d69d4a852255c6a779e2a58e42d918708

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.