#!/bin/bash # render-cards.sh — paper-cards HTML slides → PNG, one per file, with headless Chrome. # usage: render-cards.sh [WxH] (default 1080x1350; story 1080x1920) # Renders every /*.html to /.png in name order, verifies each PNG's # dimensions from its IHDR header, and prints one line per slide. Deterministic, no key, no network # beyond the Google Fonts import inside the slide (system fallback fonts if offline). # Exit 3 = no Chrome found: the drafter degrades to copy-only (caption + slide text on the issue). set -uo pipefail IN="${1:?usage: render-cards.sh [WxH]}" OUT="${2:?usage: render-cards.sh [WxH]}" SIZE="${3:-1080x1350}" W="${SIZE%x*}"; H="${SIZE#*x}" CHROME="${CHROME:-/Applications/Google Chrome.app/Contents/MacOS/Google Chrome}" if [ ! -x "$CHROME" ]; then CHROME="$(command -v google-chrome || command -v google-chrome-stable || command -v chromium || command -v chromium-browser || true)" fi if [ -z "$CHROME" ] || [ ! -x "$CHROME" ]; then echo "render-cards: no Chrome/Chromium found (set CHROME=) — degrade to copy-only" >&2 exit 3 fi shopt -s nullglob files=("$IN"/*.html) if [ ${#files[@]} -eq 0 ]; then echo "render-cards: no *.html in $IN" >&2; exit 2; fi if [ ${#files[@]} -gt 10 ]; then echo "render-cards: ${#files[@]} slides > 10 (Instagram carousel limit)" >&2; exit 2; fi mkdir -p "$OUT" # Own throwaway profile (never the user's): --no-sandbox is required with it on macOS, else headless hangs. PROFILE="$(mktemp -d "${TMPDIR:-/tmp}/render-cards.XXXXXX")" trap 'rm -rf "$PROFILE" 2>/dev/null || true' EXIT rc=0 for f in "${files[@]}"; do name="$(basename "$f" .html)" png="$OUT/$name.png" log="$PROFILE/$name.log"; : > "$log" # Background + poll: on macOS headless Chrome 15x sometimes never exits after writing the file # (and always hangs when its output goes to /dev/null), so wait for its own "bytes written" line. "$CHROME" --headless=new --disable-gpu --hide-scrollbars --force-device-scale-factor=1 --user-data-dir="$PROFILE" --no-first-run --no-sandbox \ --timeout=12000 --window-size="$W,$H" --screenshot="$png" "file://$(cd "$(dirname "$f")" && pwd)/$(basename "$f")" \ >"$log" 2>&1 & cpid=$! for _ in $(seq 1 120); do grep -q "bytes written to file" "$log" 2>/dev/null && break kill -0 "$cpid" 2>/dev/null || break sleep 0.25 done kill "$cpid" 2>/dev/null; sleep 1; kill -9 "$cpid" 2>/dev/null; wait "$cpid" 2>/dev/null if [ ! -s "$png" ]; then echo "FAIL $name: no PNG written ($(tail -n 1 "$log" 2>/dev/null))" >&2; rc=1; continue; fi # IHDR: bytes 16..23 = width, height (big-endian) dims="$(python3 -c 'import struct,sys; b=open(sys.argv[1],"rb").read(24); w,h=struct.unpack(">II", b[16:24]); print(f"{w}x{h}")' "$png")" if [ "$dims" != "$SIZE" ]; then echo "FAIL $name: $dims (want $SIZE)" >&2; rc=1; continue; fi echo "ok $name.png $dims $(wc -c < "$png" | tr -d ' ') bytes" done exit $rc