| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491 |
- #!/usr/bin/env bash
- # make.sh — minimax-pdf unified CLI
- # Usage: bash make.sh <command> [options]
- #
- # Commands:
- # check Verify all dependencies
- # fix Auto-install missing dependencies
- # run --title T --type TYPE Full pipeline → output.pdf
- # --out FILE Output path (default: output.pdf)
- # --author A --date D
- # --subtitle S
- # --abstract A Optional abstract text for cover
- # --cover-image URL Optional cover image URL/path
- # --content FILE Path to content.json (optional)
- # demo Build a full-featured demo to demo.pdf
- #
- # Document types:
- # report proposal resume portfolio academic general
- # minimal stripe diagonal frame editorial
- # magazine darkroom terminal poster
- #
- # Content block types:
- # h1 h2 h3 body bullet numbered callout table
- # image figure code math chart flowchart bibliography
- # divider caption pagebreak spacer
- #
- # Exit codes: 0 success, 1 usage error, 2 dep missing, 3 runtime error
- set -euo pipefail
- SCRIPTS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
- PY="python3"
- NODE="node"
- # ── Colour helpers ─────────────────────────────────────────────────────────────
- red() { printf '\033[0;31m%s\033[0m\n' "$*"; }
- green() { printf '\033[0;32m%s\033[0m\n' "$*"; }
- yellow() { printf '\033[0;33m%s\033[0m\n' "$*"; }
- bold() { printf '\033[1m%s\033[0m\n' "$*"; }
- # ── check ──────────────────────────────────────────────────────────────────────
- cmd_check() {
- local ok=true
- bold "Checking dependencies..."
- # Python
- if command -v python3 &>/dev/null; then
- green " ✓ python3 $(python3 --version 2>&1 | awk '{print $2}')"
- else
- red " ✗ python3 not found"
- ok=false
- fi
- # reportlab
- if python3 -c "import reportlab" 2>/dev/null; then
- green " ✓ reportlab"
- else
- yellow " ⚠ reportlab not installed (run: make.sh fix)"
- ok=false
- fi
- # pypdf
- if python3 -c "import pypdf" 2>/dev/null; then
- green " ✓ pypdf"
- else
- yellow " ⚠ pypdf not installed (run: make.sh fix)"
- ok=false
- fi
- # Node.js
- if command -v node &>/dev/null; then
- green " ✓ node $(node --version)"
- else
- red " ✗ node not found — cover rendering unavailable"
- ok=false
- fi
- # Playwright
- if node -e "require('playwright')" 2>/dev/null || \
- node -e "require(require('child_process').execSync('npm root -g').toString().trim()+'/playwright')" 2>/dev/null; then
- green " ✓ playwright"
- else
- yellow " ⚠ playwright not found (run: make.sh fix)"
- ok=false
- fi
- # matplotlib (optional — required for math/chart/flowchart; degrades gracefully)
- if python3 -c "import matplotlib" 2>/dev/null; then
- green " ✓ matplotlib (math, chart, flowchart blocks enabled)"
- else
- yellow " ⚠ matplotlib not installed — math/chart/flowchart blocks degrade to text (run: make.sh fix)"
- fi
- if $ok; then
- green "\nAll dependencies satisfied."
- exit 0
- else
- yellow "\nSome dependencies missing. Run: bash make.sh fix"
- exit 2
- fi
- }
- # ── fix ────────────────────────────────────────────────────────────────────────
- cmd_fix() {
- bold "Installing missing dependencies..."
- local rc=0
- # Python packages
- if command -v python3 &>/dev/null; then
- python3 -m pip install --break-system-packages -q reportlab pypdf matplotlib 2>/dev/null \
- || python3 -m pip install -q reportlab pypdf matplotlib 2>/dev/null \
- || { yellow " pip install failed — try: pip install reportlab pypdf matplotlib"; rc=3; }
- green " ✓ Python packages installed (reportlab, pypdf, matplotlib)"
- fi
- # Playwright
- if command -v npm &>/dev/null; then
- npm install -g playwright --silent 2>/dev/null && \
- npx playwright install chromium --silent 2>/dev/null && \
- green " ✓ Playwright + Chromium installed" || \
- { yellow " playwright install failed — try manually"; rc=3; }
- else
- yellow " npm not found — cannot install Playwright automatically"
- rc=2
- fi
- if [[ $rc -eq 0 ]]; then
- green "\nAll dependencies installed. Run: bash make.sh check"
- fi
- exit $rc
- }
- # ── run ────────────────────────────────────────────────────────────────────────
- cmd_run() {
- local title="Untitled Document"
- local type="general"
- local author=""
- local date=""
- local subtitle=""
- local abstract=""
- local cover_image=""
- local accent=""
- local cover_bg=""
- local content_file=""
- local out="output.pdf"
- local workdir
- workdir="$(mktemp -d)"
- # Parse options
- while [[ $# -gt 0 ]]; do
- case "$1" in
- --title) title="$2"; shift 2 ;;
- --type) type="$2"; shift 2 ;;
- --author) author="$2"; shift 2 ;;
- --date) date="$2"; shift 2 ;;
- --subtitle) subtitle="$2"; shift 2 ;;
- --abstract) abstract="$2"; shift 2 ;;
- --cover-image) cover_image="$2"; shift 2 ;;
- --accent) accent="$2"; shift 2 ;;
- --cover-bg) cover_bg="$2"; shift 2 ;;
- --content) content_file="$2"; shift 2 ;;
- --out) out="$2"; shift 2 ;;
- *) echo "Unknown option: $1"; exit 1 ;;
- esac
- done
- bold "Building: $title"
- echo " Type : $type"
- echo " Output : $out"
- # Step 1: tokens
- echo ""
- bold "Step 1/4 Generating design tokens..."
- local accent_args=()
- [[ -n "$accent" ]] && accent_args+=(--accent "$accent")
- [[ -n "$cover_bg" ]] && accent_args+=(--cover-bg "$cover_bg")
- $PY "$SCRIPTS/palette.py" \
- --title "$title" --type "$type" \
- --author "$author" --date "$date" \
- --out "$workdir/tokens.json" \
- "${accent_args[@]+"${accent_args[@]}"}"
- # Inject optional cover fields into tokens.json
- if [[ -n "$abstract" || -n "$cover_image" ]]; then
- PDF_ABSTRACT="$abstract" PDF_COVER_IMAGE="$cover_image" PDF_TOKENS="$workdir/tokens.json" \
- $PY - <<'PYEOF'
- import json, os
- with open(os.environ["PDF_TOKENS"]) as f:
- t = json.load(f)
- abstract = os.environ.get("PDF_ABSTRACT", "")
- cover_image = os.environ.get("PDF_COVER_IMAGE", "")
- if abstract:
- t["abstract"] = abstract
- if cover_image:
- t["cover_image"] = cover_image
- with open(os.environ["PDF_TOKENS"], "w") as f:
- json.dump(t, f, indent=2)
- PYEOF
- fi
- cat "$workdir/tokens.json" | $PY -c "
- import json,sys
- t=json.load(sys.stdin)
- print(f' Mood : {t[\"mood\"]}')
- print(f' Pattern : {t[\"cover_pattern\"]}')
- print(f' Fonts : {t[\"font_display\"]} / {t[\"font_body\"]}')"
- # Step 2: cover HTML + render
- echo ""
- bold "Step 2/4 Rendering cover..."
- local subtitle_args=()
- [[ -n "$subtitle" ]] && subtitle_args=(--subtitle "$subtitle")
- $PY "$SCRIPTS/cover.py" \
- --tokens "$workdir/tokens.json" \
- --out "$workdir/cover.html" \
- "${subtitle_args[@]+"${subtitle_args[@]}"}"
- $NODE "$SCRIPTS/render_cover.js" \
- --input "$workdir/cover.html" \
- --out "$workdir/cover.pdf"
- green " ✓ Cover rendered"
- # Step 3: body
- echo ""
- bold "Step 3/4 Rendering body pages..."
- if [[ -z "$content_file" ]]; then
- # Generate a minimal placeholder body
- cat > "$workdir/content.json" <<'JSON'
- [
- {"type":"h1", "text":"Document Body"},
- {"type":"body", "text":"Replace this with your content.json file using --content path/to/content.json"},
- {"type":"body", "text":"See the content.json schema in the skill README for the full list of supported block types: h1, h2, h3, body, bullet, callout, table, pagebreak, spacer."}
- ]
- JSON
- content_file="$workdir/content.json"
- yellow " No content file provided — using placeholder body."
- fi
- $PY "$SCRIPTS/render_body.py" \
- --tokens "$workdir/tokens.json" \
- --content "$content_file" \
- --out "$workdir/body.pdf"
- green " ✓ Body rendered"
- # Step 4: merge
- echo ""
- bold "Step 4/4 Merging and QA..."
- $PY "$SCRIPTS/merge.py" \
- --cover "$workdir/cover.pdf" \
- --body "$workdir/body.pdf" \
- --out "$out" \
- --title "$title"
- # Cleanup
- rm -rf "$workdir"
- }
- # ── fill ──────────────────────────────────────────────────────────────────────
- cmd_fill() {
- local input="" out="" values="" data_file="" inspect_only=false
- while [[ $# -gt 0 ]]; do
- case "$1" in
- --input) input="$2"; shift 2 ;;
- --out) out="$2"; shift 2 ;;
- --values) values="$2"; shift 2 ;;
- --data) data_file="$2"; shift 2 ;;
- --inspect) inspect_only=true; shift ;;
- *) echo "Unknown option: $1"; exit 1 ;;
- esac
- done
- if [[ -z "$input" ]]; then
- echo "Usage: make.sh fill --input form.pdf [--out filled.pdf] [--values '{...}'] [--data values.json] [--inspect]"
- exit 1
- fi
- if $inspect_only || [[ -z "$out" && -z "$values" && -z "$data_file" ]]; then
- bold "Inspecting form fields in: $input"
- $PY "$SCRIPTS/fill_inspect.py" --input "$input"
- return
- fi
- bold "Filling form: $input → $out"
- local val_args=""
- if [[ -n "$values" ]]; then val_args="--values $values"; fi
- if [[ -n "$data_file" ]]; then val_args="--data $data_file"; fi
- $PY "$SCRIPTS/fill_write.py" --input "$input" --out "$out" $val_args
- }
- # ── reformat ───────────────────────────────────────────────────────────────────
- cmd_reformat() {
- local input="" title="Reformatted Document" type="general"
- local author="" date="" out="output.pdf" subtitle=""
- local tmpdir
- tmpdir="$(mktemp -d)"
- while [[ $# -gt 0 ]]; do
- case "$1" in
- --input) input="$2"; shift 2 ;;
- --title) title="$2"; shift 2 ;;
- --type) type="$2"; shift 2 ;;
- --author) author="$2"; shift 2 ;;
- --date) date="$2"; shift 2 ;;
- --subtitle) subtitle="$2"; shift 2 ;;
- --out) out="$2"; shift 2 ;;
- *) echo "Unknown option: $1"; exit 1 ;;
- esac
- done
- if [[ -z "$input" ]]; then
- echo "Usage: make.sh reformat --input source.md --title T --type TYPE --out output.pdf"
- exit 1
- fi
- bold "Parsing: $input"
- $PY "$SCRIPTS/reformat_parse.py" --input "$input" --out "$tmpdir/content.json"
- green " ✓ Parsed to content.json"
- bold "Applying design and building PDF..."
- local sub_args=()
- [[ -n "$subtitle" ]] && sub_args=(--subtitle "$subtitle")
- cmd_run \
- --title "$title" --type "$type" \
- --author "$author" --date "$date" \
- --content "$tmpdir/content.json" \
- --out "$out" \
- "${sub_args[@]+"${sub_args[@]}"}"
- rm -rf "$tmpdir"
- }
- # ── demo ──────────────────────────────────────────────────────────────────────
- cmd_demo() {
- local tmpdir
- tmpdir="$(mktemp -d)"
- cat > "$tmpdir/content.json" <<'JSON'
- [
- {"type":"h1", "text":"Executive Summary"},
- {"type":"body", "text":"This document was generated by minimax-pdf — a skill for creating visually polished PDFs. Every design decision is rooted in the document type and content, not a generic template."},
- {"type":"callout", "text":"Key insight: design tokens flow from palette.py through every renderer, keeping cover and body visually consistent."},
- {"type":"h1", "text":"How It Works"},
- {"type":"h2", "text":"The Token Pipeline"},
- {"type":"body", "text":"The palette.py script infers a color palette and typography pair from the document type. These tokens are written to tokens.json and consumed by every downstream script."},
- {"type":"numbered","text":"palette.py generates color tokens, font selection, and the cover pattern"},
- {"type":"numbered","text":"cover.py renders the cover HTML using the selected pattern"},
- {"type":"numbered","text":"render_cover.js uses Playwright to convert the HTML cover to PDF"},
- {"type":"numbered","text":"render_body.py builds inner pages from content.json using ReportLab"},
- {"type":"numbered","text":"merge.py combines cover + body and runs final QA checks"},
- {"type":"h2", "text":"Cover Patterns"},
- {"type":"table",
- "headers": ["Pattern", "Document type", "Visual character"],
- "rows": [
- ["fullbleed", "report, general", "Deep background · dot-grid texture"],
- ["split", "proposal", "Left dark panel · right dot-grid"],
- ["typographic", "resume, academic", "Oversized display type · first-word accent"],
- ["atmospheric", "portfolio", "Dark bg · radial glow · dot-grid"],
- ["magazine", "magazine", "Cream bg · centered · hero image"],
- ["darkroom", "darkroom", "Navy bg · centered · grayscale image"],
- ["terminal", "terminal", "Near-black · grid lines · monospace"],
- ["poster", "poster", "White · thick sidebar · oversized title"]
- ]
- },
- {"type":"h1", "text":"Data Visualisation"},
- {"type":"h2", "text":"Performance Metrics (Chart)"},
- {"type":"body", "text":"Charts are rendered natively using matplotlib with a color palette derived from the document accent. No external chart services or image files required."},
- {"type":"chart",
- "chart_type": "bar",
- "title": "Quarterly Performance",
- "labels": ["Q1", "Q2", "Q3", "Q4"],
- "datasets": [
- {"label": "Revenue", "values": [120, 145, 132, 178]},
- {"label": "Expenses", "values": [95, 108, 99, 122]}
- ],
- "y_label": "USD (thousands)",
- "caption": "Quarterly revenue vs. expenses"
- },
- {"type":"h2", "text":"Market Share (Pie Chart)"},
- {"type":"chart",
- "chart_type": "pie",
- "labels": ["Product A", "Product B", "Product C", "Other"],
- "datasets": [{"values": [42, 28, 18, 12]}],
- "caption": "Annual market share by product line"
- },
- {"type":"pagebreak"},
- {"type":"h1", "text":"Mathematics"},
- {"type":"body", "text":"Display math is rendered via matplotlib mathtext — no LaTeX binary installation required. Inline references use standard [N] notation in body text."},
- {"type":"math", "text":"E = mc^2", "label":"(1)"},
- {"type":"math", "text":"\\int_0^\\infty e^{-x^2}\\,dx = \\frac{\\sqrt{\\pi}}{2}", "label":"(2)"},
- {"type":"math", "text":"\\sum_{n=1}^{\\infty} \\frac{1}{n^2} = \\frac{\\pi^2}{6}", "caption":"Basel problem (Euler, 1734)"},
- {"type":"h1", "text":"Process Flow"},
- {"type":"body", "text":"Flowcharts are drawn directly using matplotlib patches — no Graphviz or external tools needed. Supported node shapes: rect, diamond, oval, parallelogram."},
- {"type":"flowchart",
- "nodes": [
- {"id":"start", "label":"Start", "shape":"oval"},
- {"id":"input", "label":"Receive Input", "shape":"parallelogram"},
- {"id":"valid", "label":"Valid?", "shape":"diamond"},
- {"id":"proc", "label":"Process Data", "shape":"rect"},
- {"id":"err", "label":"Return Error", "shape":"rect"},
- {"id":"out", "label":"Return Result", "shape":"parallelogram"},
- {"id":"end", "label":"End", "shape":"oval"}
- ],
- "edges": [
- {"from":"start", "to":"input"},
- {"from":"input", "to":"valid"},
- {"from":"valid", "to":"proc", "label":"Yes"},
- {"from":"valid", "to":"err", "label":"No"},
- {"from":"proc", "to":"out"},
- {"from":"err", "to":"end"},
- {"from":"out", "to":"end"}
- ],
- "caption": "Data validation and processing flow"
- },
- {"type":"h1", "text":"Code Example"},
- {"type":"code", "language":"python",
- "text":"# Design token pipeline\ntokens = palette.build_tokens(\n title=\"Annual Report\",\n doc_type=\"report\",\n author=\"J. Smith\",\n date=\"March 2026\",\n)\nhtml = cover.render(tokens)\npdf = render_cover(html)"},
- {"type":"h1", "text":"Design Principles"},
- {"type":"body", "text":"The aesthetic system is documented in design/design.md. The core rule: every design decision must be rooted in the document content and purpose. A color chosen because it fits the content will always outperform a color chosen because it seems safe."},
- {"type":"h2", "text":"Restraint over decoration"},
- {"type":"body", "text":"The page is done when there is nothing left to remove. Accent color appears on section rules only — not on headings, not on bullets. No card components, no drop shadows."},
- {"type":"callout", "text":"A PDF passes the quality bar when a designer would not be embarrassed to hand it to a client."},
- {"type":"pagebreak"},
- {"type":"bibliography",
- "title": "References",
- "items": [
- {"id":"1","text":"Bringhurst, R. (2004). The Elements of Typographic Style (3rd ed.). Hartley & Marks."},
- {"id":"2","text":"Cairo, A. (2016). The Truthful Art: Data, Charts, and Maps for Communication. New Riders."},
- {"id":"3","text":"Hochuli, J. & Kinross, R. (1996). Designing Books: Practice and Theory. Hyphen Press."}
- ]
- }
- ]
- JSON
- cmd_run \
- --title "minimax-pdf demo" \
- --type "report" \
- --author "minimax-pdf skill" \
- --date "$(date '+%B %Y')" \
- --subtitle "A demonstration of the token-based design pipeline" \
- --content "$tmpdir/content.json" \
- --out "demo.pdf"
- rm -rf "$tmpdir"
- }
- # ── dispatch ───────────────────────────────────────────────────────────────────
- main() {
- if [[ $# -lt 1 ]]; then
- bold "minimax-pdf — make.sh"
- echo ""
- echo "Usage: bash make.sh <command> [options]"
- echo ""
- echo "Commands:"
- echo " check Verify all dependencies"
- echo " fix Auto-install missing deps"
- echo " run --title T --type TYPE CREATE: full pipeline → PDF"
- echo " [--author A] [--date D] [--subtitle S]"
- echo " [--abstract A] [--cover-image URL]"
- echo " [--accent #HEX] [--cover-bg #HEX]"
- echo " [--content content.json] [--out output.pdf]"
- echo " fill --input f.pdf FILL: inspect or fill form fields"
- echo " reformat --input doc.md REFORMAT: parse doc → apply design → PDF"
- echo " demo Build a full-featured demo PDF"
- exit 0
- fi
- case "$1" in
- check) cmd_check ;;
- fix) cmd_fix ;;
- run) shift; cmd_run "$@" ;;
- fill) shift; cmd_fill "$@" ;;
- reformat) shift; cmd_reformat "$@" ;;
- demo) cmd_demo ;;
- *) echo "Unknown command: $1"; exit 1 ;;
- esac
- }
- main "$@"
|