Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions ResearchStudio-Reel/skills/paper2poster/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: paper2poster
description: Render a pre-extracted paper's structured 9-section spec (`paper_spec.md`) into a single-page HTML academic poster, fit the layout to the page via an iterative measured-fill loop, and export it to print-ready PDF + PNG thumbnail. Requires the upstream `paper2assets` skill to have produced the input `<outdir>/` package (`manifest.json` at the root + an `assets/` folder holding `meta/paper_spec.md`, `meta/text.txt`, `meta/figures.json`, `meta/metadata.json`, zero or more `figures/*.png`, `logos/`, `qr/`) first. Use when the user wants an HTML poster, PDF/PNG export, or PPTX from a paper they already have extracted assets for, for example "render the poster", "make the poster from this spec", "export poster to PDF", or "paper2poster". The three skills paper2assets → paper2poster → html2pptx run in sequence, each invokable on its own.
description: Render a pre-extracted paper's structured 9-section spec (`paper_spec.md`) into a single-page HTML academic poster, fit the layout to the page via an iterative measured-fill loop, and export it to print-ready PDF + PNG thumbnail. Requires the upstream `paper2assets` skill to have produced an output bundle with `manifest.json` at the root and an `assets/` folder holding the paper metadata and media. Use when the user wants an HTML poster, PDF/PNG export, or PPTX from extracted paper assets, for example "render the poster", "make the poster from this spec", "export poster to PDF", or "paper2poster". The three skills paper2assets → paper2poster → html2pptx run in sequence, each invokable on its own.
allowed-tools: Bash(*), Read, Write, Edit, Grep, Glob, AskUserQuestion, WebFetch, WebSearch
---

Expand Down Expand Up @@ -322,7 +322,7 @@ The first three are **full themes** (they retheme the header + page background);

v1–v4 each render the conference **logo** when `assets/logos/_venue.png` exists (Step 6 `fetch_conf_logo.py`), else a text venue/year fallback in the same chip; v5 uses a text venue badge by design. All work for **2–6 institutions** (empty `LOGO_n` slots auto-hide). Logos are sized to **fill** their zone (single venue logo + a 2-row institution grid), and the logo chips **theme to the chosen style** via `--tb-chip-bg` / `--tb-chip-shadow`: solid → flat white chip (no shadow) on the accent band; framed → flat white chip on the white card; **simple → transparent chip (no frame), logos sit directly on the white header**. Default: pass **`--header random`** — `compose_poster.py` picks one DETERMINISTICALLY from the configured seed from **all five (v1–v5)**; v5 fills its own titlebar QR via `{{HDR_QR_*}}` (see the QR contract). Override via `POSTER_HEADER={v1|v2|v3|v4|v5}`.

**Default font policy:** the poster body font defaults to **Arial** — a cross-platform-safe family pre-installed on Mac + Windows PowerPoint, so the exported `.pptx` needs **no font embedding** and round-trips cleanly. To override, edit the chosen template's `--font-latin` CSS variable (in the `:root` block) to any of the 8 PPT-safe families: `Calibri | Aptos | Cambria | Arial | "Times New Roman" | Verdana | Georgia | "Trebuchet MS"`. The optional `POSTER_FONT` env var, when set, carries the same choice — but the default lives in the templates, not in any external script. To use **Inter** (the bundled webfont — more editorial, but not pre-installed), flip `--font-latin` back to `Inter, …` *and* run the html2pptx Inter embed step so the `.pptx` ships the font; the 4 Inter `@font-face` blocks stay defined (inert) in every template for exactly this one-line override.
**Default font policy:** the poster body defaults to **Arial**, and `--font-latin` may select any of the 8 Mac/Windows PowerPoint-safe family names: `Calibri | Aptos | Cambria | Arial | "Times New Roman" | Verdana | Georgia | "Trebuchet MS"`. These proprietary OS fonts are not reliably installed on the Linux renderer or every HTML client. Before browser geometry is measured, `check_poster.py` / `render_poster.py` therefore freeze their **browser** rendering to a self-contained, open-licensed DejaVu face of the same category (Sans or Serif), stored under `assets/fonts/` with its license but exposed under the requested CSS family name. This makes fill measurements, standalone HTML, PDF, and PNG deterministic while html2pptx continues emitting the requested native family name into `.pptx`; the deck can still have small metric differences because PowerPoint uses the real OS font. Literal Georgia/Arial/etc. in every artifact requires separately licensed webfont files. The optional `POSTER_FONT` env var, when set, carries the same choice — but the default lives in the templates. To use **Inter** (the bundled webfont — more editorial), flip `--font-latin` back to `Inter, …` *and* run the html2pptx Inter embed step so the `.pptx` ships the font; the 4 Inter `@font-face` blocks stay defined in every template for exactly this override.

**Composition catalog (landscape and portrait):**

Expand Down Expand Up @@ -461,9 +461,11 @@ python references/fit_logos.py --poster <outdir>/poster.html
python ~/.claude/skills/paper2poster/scripts/render_poster.py <outdir>/poster.html
```

Run this **before Step 7 (html2pptx)** so the expand is baked into `poster.html` *before* html2pptx reads it — the editable `poster.pptx` then matches the PDF/PNG instead of shipping the pre-expand layout. The script reads `@page { size: <W> <H> }` from the HTML, mirrors the bundled Inter webfonts into `<outdir>/assets/fonts/` (so the poster.html + its `assets/fonts/` stay self-contained for sharing across platforms), opens Chromium with print emulation, waits for MathJax to settle, applies the render-time expand, **bakes that expand back into `poster.html`**, then writes `<outdir>/poster.pdf` and `<outdir>/poster.png` (0.35× scale by default).
Run this **before Step 7 (html2pptx)** so the expand is baked into `poster.html` *before* html2pptx reads it — the editable `poster.pptx` then matches the PDF/PNG instead of shipping the pre-expand layout. The script reads `@page { size: <W> <H> }` from the HTML, mirrors the bundled Inter webfonts into `<outdir>/assets/fonts/`, freezes any selected OS-font stack to its licensed portable browser face, opens Chromium with print emulation, waits for MathJax to settle, applies the render-time expand, **bakes that expand back into `poster.html`**, then writes `<outdir>/poster.pdf` and `<outdir>/poster.png` (0.35× scale by default). The poster HTML and `assets/fonts/` therefore stay self-contained for sharing across platforms.

**Render-time "expand" (automatic, on by default).** Right before writing the PDF/PNG, `render_poster.py` runs one render-time fill pass: for every under-filled card it grows the row-gaps *between* the card's inner rows until the content reaches `POSTER_EXPAND_THRESHOLD` (default **0.98**). This makes a poster that converged at the 0.90 FULL gate read as visually full — no trailing whitespace — *without* re-grinding the fill loop to a tighter, ~2× slower gate. It is safe by construction, on two guardrails: (1) **figures are never resized** — they stay `flex:0 0 auto`, so a card's `<img>` keeps its exact pixel dimensions and aspect ratio even when the card it lives in is filled; (2) **a card is reverted if filling it would change its column/container height** (parent-height guard) — so a flex `.grow` card absorbs the fill *inside* its column (column bottom unchanged → fills the trailing column-bottom whitespace), while a grid/content card that would push the fixed-canvas layout taller is left alone. A card also stops at its **bottom-padding ceiling** (never eats padding → column bottoms stay aligned), so smaller cards finish a bit under 0.98 — that ceiling, `1 − padBot/cardHeight`, is their real "full". The expand result is then **persisted into `poster.html`** as a single `<style id="poster-expand-baked">` block (one `row-gap` rule per expanded section), so the editable HTML, its `D` debug overlay, the PDF/PNG, and the downstream html2pptx read all show the *same* expanded layout — not the pre-expand one. This is responsive-safe (the templates use a fixed internal layout scaled by an outer `transform: scale()`, so an inline px `row-gap` renders identically at any view size) and idempotent (a re-render replaces the block). It is written **only at this final render**, after Step 4's fill loop — so `check_poster.py slack/polish` during the loop still measure the natural top-aligned layout and the 0.90 FULL gate stays correct.
**Render-time "expand" (automatic, on by default).** Right before writing the PDF/PNG, `render_poster.py` takes one read-only snapshot of the natural poster, then provisionally grows the row-gaps *between* each under-filled card's inner rows toward `POSTER_EXPAND_THRESHOLD` (default **0.98**). This makes a poster that converged at the 0.90 FULL gate read as visually full — no trailing whitespace — *without* re-grinding the fill loop to a tighter, ~2× slower gate. Every proposal stops at its **bottom-padding ceiling** and is immediately reverted if it changes the card's column/container height. Figure/image cards may participate, but their rules are not trusted from the live DOM: the renderer bakes the provisional rules into `poster.html`, reloads the actual file, and retains a rule only when its cascade still wins, every original section/parent rectangle and visible media box stays fixed, media source/intrinsic/painted dimensions stay fixed, and every tracked figure still fills ≥90% of its section on at least one axis without overflow. A failure removes every provisional rule in that parent scope, rewrites the block, reloads, and repeats to a fixed point; settle/validation failures remove all optional rules. The final rule set must then survive one extra identical-file reload before capture. Only that validated durable page is used for the editable HTML, its `D` overlay, PDF/PNG, and downstream html2pptx, so all views share the same layout. The bake is one idempotent `<style id="poster-expand-baked">` block written only after Step 4, so the staged-fill loop still measures the natural top-aligned layout and its 0.90 FULL gate stays correct.

The surviving rules must also pass a **brand-new BrowserContext** open, not only warm reloads. Legacy recurring geometry fitters are guarded in the delivered HTML so they measure the unscaled fixed canvas and restore the exact viewer transform; after the cold-open page settles, script execution is frozen before PDF/PNG capture so a late timer cannot create a live-only export.

**Two tuning knobs (env vars) — the only layout dials you normally touch:**

Expand Down
15 changes: 15 additions & 0 deletions ResearchStudio-Reel/skills/paper2poster/scripts/check_poster.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import argparse
import os
import sys
from pathlib import Path

# Make `utils` importable when this file is run directly via
# `python tools/check_poster.py …`.
Expand All @@ -46,6 +47,7 @@
from utils import preflight as _preflight # noqa: E402
from utils import slack as _slack # noqa: E402
from utils import verify_final as _verify_final # noqa: E402
from utils.font_fidelity import freeze_system_font_webfont # noqa: E402


def build_parser() -> argparse.ArgumentParser:
Expand Down Expand Up @@ -314,6 +316,19 @@ def build_parser() -> argparse.ArgumentParser:

def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
# Geometry/fill measurements must use the same deterministic browser face
# that final rendering will bake into the standalone poster bundle. In
# particular, Linux naturally resolves Arial/Times New Roman to Nimbus,
# while the portable final mapping uses DejaVu; injecting only at final
# render would change wrapping *after* the strict fill gate passed.
if args.cmd in {"pack", "slack", "polish", "autofit"}:
try:
freeze_system_font_webfont(Path(args.html).resolve())
except Exception as exc: # soft, matching render_poster's font path
print(
f"[poster_check] WARN: portable font preparation skipped: {exc}",
file=sys.stderr,
)
return args.func(args)


Expand Down
Loading