- Rust 99.3%
- Shell 0.7%
| .github/workflows | ||
| assets | ||
| data | ||
| docs | ||
| examples | ||
| fmd-font | ||
| scripts | ||
| src | ||
| tests | ||
| .gitattributes | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| LICENSE | ||
| README.md | ||
| rust-toolchain.toml | ||
FrankenMarkdown turns a Markdown file into a finished PDF. One binary, one command, one output. There is no browser print step, no LaTeX install, no Node runtime, and nothing loaded from the host's font directory.
Quick start · Options · Images · Library · Design · Changelog
The whole path is in this repository: the parser, the line breaker, the syntax
highlighter, the font subsetter, the SVG and image handling, and the PDF writer.
The renderer itself pulls in no third-party crates — the CLI feature adds clap
for argument parsing and nothing else — and unsafe is forbidden.
The default page design is Marginalia — warm black type in one readable column, a single accent color spent on navigation rather than decoration, measured tables, and quiet code. The point is a page that reads as composed rather than exported.
Quick start
Everything here builds on stable Rust; Cargo.toml declares 1.85 as the
floor. The checkout pins a nightly toolchain in rust-toolchain.toml
because the dev checks use it, so pass +stable if you would rather not.
From a checkout:
cargo build --release
Render a file:
target/release/fmd notes.md
That writes notes.pdf beside the source. It will not overwrite the Markdown
input, and -o puts the PDF anywhere you like:
target/release/fmd notes.md -o build/notes.pdf
Markdown can also arrive on stdin or as a string, in which case -o is
required:
fmd - -o notes.pdf < notes.md
fmd --text '# A small document' -o small.pdf
Compared with
FrankenMarkdown does one job: turn a Markdown file into a PDF you would be happy to hand to someone. The narrow scope is what lets it ship as a single binary with no runtime to install.
| Compared with | Why use FrankenMarkdown | Use the other tool when |
|---|---|---|
| Pandoc | Its default PDF route expects a LaTeX installation; this is one binary with nothing to set up around it. | You need Pandoc's format matrix, or you already have a LaTeX template you like. |
| md-to-pdf and other headless-Chrome converters | No browser to install, patch, or pin, and the output does not shift underneath you when Chrome updates. | You want to style the page with CSS you already know. |
| WeasyPrint or wkhtmltopdf | Markdown goes straight to PDF, with no HTML-and-CSS stage in the middle to debug. | You are rendering HTML rather than Markdown. |
| Typst or LaTeX | The source stays ordinary Markdown that still reads fine on GitHub. | You want a real typesetting language, macros, or mathematical layout. |
Options
fmd handbook.md --toc
fmd report.md --accent '#1F3A5F' --title 'Quarterly report' --author 'A. Writer'
fmd essay.md --font serif
fmd reference.md --line-numbers
| Flag | What it does |
|---|---|
--toc |
A compact clickable contents section after the opening H1 |
--accent '#RRGGBB' |
Replaces the one semantic accent, everywhere it is used |
--font sans|serif |
IBM Plex Sans (default) or Computer Modern |
--line-numbers |
Muted line numbers in fenced code blocks |
--title, --author |
PDF metadata; the first H1 is still the visible title |
--image DEST=PATH |
Bind a Markdown image destination to a local file |
--no-remote-images |
Never fetch anything over the network |
--margin-top … --margin-left |
Override the page frame, in points |
--json |
Report the result as one line of JSON |
--accent takes a single six-digit RGB color and derives the rest: links,
contents leaders and page numbers, the H1 rule, folios, blockquote bars, code
details, task checks, and the H2/H3 ink mixtures. Body text stays neutral.
fmd guide.md --toc --accent '#B88919'
fmd guide.md --toc --accent '#2D6248'
Messages, warnings, and the JSON line all go to stderr. Exit codes are stable:
64 usage, 66 unreadable input, 70 render failure, 73 write failure. Set
SOURCE_DATE_EPOCH and the same input produces the same bytes. fmd --help
lists the rest, including the input and image size limits.
What it renders
- Headings (ATX and setext), paragraphs, emphasis, strikethrough, inline code, thematic breaks, and hard breaks.
- Inline links, reference links, and bare autolinks, written into the PDF as real link annotations.
- Ordered, unordered, nested, and task lists, with markers in the margin and a shared content edge.
- Tables with measured columns and per-cell alignment, rather than equal-width squeezing.
- Fenced code with clean-room highlighting for Rust, Python, Go, C/C++, SQL, JSON, TOML, YAML, INI, shell, PowerShell, HTML/XML/SVG, CSS, Markdown, and Mermaid; long tokens wrap instead of overflowing.
- Blockquotes, nested as deeply as the source goes.
- PNG and JPEG as image objects; supported SVG stays vector.
- Knuth–Plass paragraph breaking, real kerning and ligatures, Liang hyphenation, justification, and CJK line breaking.
- Embedded font subsets, selectable text, Unicode maps, document outlines, metadata, and hierarchical tagged structure with page furniture marked as artifacts.
Raw HTML is not generally interpreted. Media-only <img> fragments (including
the usual centered README wrapper) and direct <svg>…</svg> render as images;
other HTML is set as plain text instead of being silently dropped.
Images
Relative images resolve from the Markdown file's own directory:

Paths must stay inside that directory — no traversal, no absolute paths, no symlinks pointing elsewhere. For stdin, generated Markdown, or an asset that lives somewhere else, bind the destination explicitly:
fmd generated.md --image 'images/system.svg=./assets/current-system.svg'
HTTP(S) destinations are fetched with curl, capped at 32 MiB with a 20
second timeout each. If a fetch fails the document still renders, uses the alt
text, and warns. --no-remote-images keeps the run entirely offline.
Library
The library does no filesystem or network I/O of its own. Callers pass in Markdown, fonts, and image bytes.
use franken_markdown::{PdfOptions, render_pdf};
let options = PdfOptions {
table_of_contents: true,
..PdfOptions::default()
};
let pdf = render_pdf("# Field notes\n\nA quiet page.", &options)?;
std::fs::write("field-notes.pdf", pdf)?;
# Ok::<(), Box<dyn std::error::Error>>(())
Use parse_markdown with render_pdf_document when one parsed document is
rendered more than once. PdfOptions also carries the theme, page frame,
metadata, accent, supplied fonts, supplied images, and the code line-number
setting.
Design
The renderer owns the page directly instead of styling an intermediate HTML document, which is what makes the rest possible: line breaking and column allocation use the real embedded metrics; headings, lists, tables, code, and page furniture share one vertical rhythm; links, tags, outlines, and reading order are written into the PDF itself; and the output does not shift because a machine has different fonts or a newer browser.
docs/DESIGN.md covers the page, the type registers, and what the accent actually touches.
Development
cargo fmt --check
cargo check --all-targets
cargo clippy --all-targets -- -D warnings
cargo test
scripts/ holds the determinism check, the pagination proof, a PDF/UA
spot-check that skips unless veraPDF is installed, and pdf-look.sh, which
renders a fixture in both font modes and builds contact sheets — PDF design
changes are reviewed as pages, not as diffs.
Render the document that exercises most of the engine:
cargo run --release --bin fmd -- examples/showcase.md --toc -o showcase.pdf
Related
Same idea, different job — one thing done properly, nothing in the middle, and a result you can check:
- Agentic Word Documents — Word documents built from Markdown and Excel, with a verified PDF
- FrankenFile — self-hosted file drop; six characters, links expire
The rest are listed on my profile.
Provenance and license
This is a PDF-only fork of
franken_markdown 0.3.5
by Jeffrey Emanuel. The parser, font engine, line-breaking work, tagged-PDF
foundation, image and SVG support, and deterministic writer are the upstream
base. The fork drops the HTML renderer, the WASM package, the batch runtime,
and the math workspace, replaces the multi-mode CLI with fmd input.md, and
rebuilds the page design around the single remaining output.
The project code is available under the plain MIT License.
Bundled fonts keep their own licenses — IBM Plex Sans, Computer Modern Unicode,
Source Sans 3, Source Code Pro, and a Noto Sans Math subset, all SIL OFL 1.1,
with the license texts stored beside them in
fmd-font/fonts. The bundled US English hyphenation patterns
carry their own notice in data.