make-pdf

Original🇺🇸 English
Translated
3 scripts

Turn any Markdown file into a publication-quality, print-ready PDF with proper margins, page numbers, cover pages, running headers, and a clickable table of contents. Mermaid and Excalidraw fences render as vector diagrams fully offline, and --to html or --to docx emit a self-contained web page or Word document. Use when asked to "make a PDF", "export to PDF", "turn this markdown into a document", or "generate a document".

3installs
Added on

NPX Install

npx skill4agent add time-attack/gstack make-pdf

Tags

Translated version includes tags in frontmatter

Host-neutral runtime bindings

These assignments select stable paths only; they do not install anything or grant consent:
bash
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
GSTACK_ROOT="$GSTACK_HOME"
GSTACK_STATE_ROOT="$GSTACK_HOME"
GSTACK_BIN="$GSTACK_HOME/bin"
BUN_CMD="$GSTACK_BIN/bun"
B="$GSTACK_BIN/browse"
P="$GSTACK_BIN/make-pdf"

Optional runtime binding

This workflow requires the
pdf
capability. Read
references/RUNTIME.md
before attempting installation. Pure planning or document review can continue without it. If unavailable, follow the consent-first
pdf
capability handoff in
references/RUNTIME.md
. It expands to
pdf
,
diagram
, and
browser
; do not guess a checkout-relative setup command.

make-pdf: publication-quality PDFs from markdown

Turn
.md
files into PDFs that look like Faber & Faber essays: 1in margins, left-aligned body, Helvetica throughout, curly quotes and em dashes, optional cover page and clickable TOC, diagonal DRAFT watermark when you need it. Copy-paste from the PDF produces clean words, never "S a i l i n g".
On Linux, install
fonts-liberation
for correct rendering — Helvetica and Arial aren't present by default, and Liberation Sans is the standard metric-compatible fallback. CI and Docker builds install it automatically via Dockerfile.ci.
Emoji need a color-emoji font. macOS (Apple Color Emoji) and Windows (Segoe UI Emoji) ship one; most Linux distros and containers ship none, so emoji render as empty boxes (▯). the explicitly approved
pdf
runtime capability attempts to install
fonts-noto-color-emoji
on Linux (apt/dnf/pacman/apk, best-effort) and the print CSS falls back through Apple / Segoe / Noto emoji families. Set
GSTACK_SKIP_FONTS=1
to skip the install (CI without sudo, managed or offline machines).

Core patterns

80% case — memo/letter

One command, no flags. Gets a clean PDF with running header + page numbers
  • CONFIDENTIAL footer by default.
bash
$P generate letter.md                 # writes /tmp/letter.pdf
$P generate letter.md letter.pdf      # explicit output path

Publication mode — cover + TOC + chapter breaks

bash
$P generate --cover --toc --author "Garry Tan" --title "On Horizons" \
  essay.md essay.pdf
Each top-level H1 in the markdown starts a new page. Disable with
--no-chapter-breaks
for memos that happen to have multiple H1s.

Draft-stage watermark

bash
$P generate --watermark DRAFT memo.md draft.pdf
Diagonal 10% opacity DRAFT across every page. When the draft is final, drop the flag and regenerate.

Fast iteration via preview

bash
$P preview essay.md
Renders HTML with the same print CSS and opens it in your browser. Refresh as you edit the markdown. Skip the PDF round trip until you're ready.

Brand-free (no CONFIDENTIAL footer)

bash
$P generate --no-confidential memo.md memo.pdf

Diagrams — mermaid and excalidraw fences render as pictures

A column-0
```mermaid
or
```excalidraw
fence in the markdown renders as a crisp vector diagram, fully offline (vendored bundle, no CDN). Indented fences (inside lists) stay plain code blocks by design. A broken fence produces a visible red diagnostic block with the parse error — never silent raw code.
Fence info-string options:
```mermaid title="Auth flow"        ← caption + aria-label
```mermaid render=false             ← keep it as a code block (today's behavior)
```mermaid page=landscape           ← force this diagram onto a landscape page
```mermaid page=portrait            ← veto auto-landscape for this diagram
A
```excalidraw
fence contains a full .excalidraw scene file (what excalidraw.com saves). Authoring NEW diagrams from English is
/diagram
's job — it emits an editable triplet (source, .excalidraw, SVG/PNG) and pairs with this skill: embed the
.mmd
source in your markdown, not the PNG.

Images — scaled right, never truncated

Local images inline automatically (relative paths resolve against the markdown file). Every image caps at the content box — zero truncation, ever. Oversized photos downscale to print resolution (300dpi) so payloads stay small with no visible quality loss.
Remote (http/https) images are blocked with a visible placeholder by default — offline posture; pass
--allow-network
to fetch them. An image that resolves outside the markdown's directory (even via symlink) still inlines, but warns loudly;
--strict
makes it fatal. Files over 64MB or non-regular files (fifos, devices) degrade to a placeholder instead of hanging the run.
Per-image directives, written immediately after the image:
![chart](data.png){width=full}      ← stretch to content-box width
![chart](data.png){width=50%}       ← percentage or 3in/8cm/200px
![wide](arch.png){page=landscape}   ← give it its own landscape page
![wide](shot.png){page=portrait}    ← veto auto-landscape
Wide, small-text diagram images auto-promote to their own landscape page (conservative: aspect ≥ 1.8, width over ~2.5x the content box, AND a diagram-ish alt word — diagram/architecture/flowchart/chart/graph). The promoted page is vertically centered. When the heuristic guesses wrong,
{page=portrait}
vetoes it; false negatives just need
{page=landscape}
.

Other formats — single-file HTML and Word

bash
$P generate readme.md out.html --to html    # ONE self-contained file: inline
                                            # SVG diagrams, data-URI images,
                                            # zero network refs, screen-readable
$P generate readme.md out.docx --to docx    # Word: content fidelity (headings,
                                            # tables, code, diagrams as PNG) —
                                            # layout is Word's, not ours
--to
is the output format.
--format
is something else entirely (a
--page-size
alias) — don't confuse them.

CI mode — fail loud on missing assets

bash
$P generate docs.md --strict     # missing, remote, out-of-tree, oversized,
                                 # and non-regular-file images exit non-zero
                                 # instead of warn + placeholder

Common flags

Page layout:
  --margins <dim>            1in (default) | 72pt | 2.54cm | 25mm
  --page-size letter|a4|legal

Structure:
  --cover                    Cover page (title, author, date, hairline rule)
  --toc                      Clickable TOC with page numbers
  --no-chapter-breaks        Don't start a new page at every H1

Branding:
  --watermark <text>         Diagonal watermark ("DRAFT", "CONFIDENTIAL")
  --header-template <html>   Custom running header
  --footer-template <html>   Custom footer (mutex with --page-numbers)
  --no-confidential          Suppress the CONFIDENTIAL right-footer

Output:
  --to pdf|html|docx         Output format (default: pdf). html = single
                             self-contained file; docx = content fidelity.
  --strict                   Missing, remote, out-of-tree, oversized, or
                             non-regular-file images fail the run (CI mode).
  --page-numbers             "N of M" footer (default on)
  --tagged                   Accessible PDF (default on)
  --outline                  PDF bookmarks from headings (default on)
  --quiet                    Suppress progress on stderr
  --verbose                  Per-stage timings

Network:
  --allow-network            Fetch external images. Off by default: remote
                             images render as a visible blocked placeholder
                             (no tracking pixels fetch at print time).

Metadata:
  --title "..."              Document title (defaults to first H1)
  --author "..."             Author for cover + PDF metadata
  --date "..."               Date for cover (defaults to today)

When Claude should run it

Watch for markdown-to-PDF intent. Any of these patterns → run
$P generate
:
  • "Can you make this markdown a PDF"
  • "Export it as a PDF"
  • "Turn this letter into a PDF"
  • "I need a PDF of the essay"
  • "Print this as a PDF for me"
If the user has a
.md
file open and says "make it look nice", propose
$P generate --cover --toc
and ask before running.

Debugging

  • Output looks empty / blank → check browse daemon is running:
    $B status
    .
  • Fragmented text on copy-paste → highlight.js output (Phase 4). Retry with
    --no-syntax
    once that flag exists. For now, remove fenced code blocks and regenerate.
  • Paged.js timeout → probably no headings in the markdown. Drop
    --toc
    .
  • "[remote image blocked]" placeholder in the output → add
    --allow-network
    (understand you're giving the markdown file permission to fetch from its image URLs).
  • Generated PDF too tall/wide →
    --page-size a4
    or
    --margins 0.75in
    .

Output contract

stdout: /tmp/letter.pdf          ← just the path, one line
stderr: Rendering HTML...        ← progress spinner (unless --quiet)
        Generating PDF...
        Done in 1.5s. 43 words · 22KB · /tmp/letter.pdf

exit code: 0 success / 1 bad args / 2 render error / 3 Paged.js timeout
           / 4 browse unavailable
Capture the path:
PDF=$($P generate letter.md)
— then use
$PDF
.