Markdown to PDF in a single Rust binary. No browser print step, no LaTeX, no Node runtime — a self-contained typesetter with real typography, font subsetting and syntax highlighting.
  • Rust 99.3%
  • Shell 0.7%
Find a file
2026-08-26 00:03:58 +03:00
.github/workflows Public portfolio release 2026-08-26 00:00:35 +03:00
assets Public portfolio release 2026-08-26 00:00:35 +03:00
data Public portfolio release 2026-08-26 00:00:35 +03:00
docs Public portfolio release 2026-08-26 00:00:35 +03:00
examples Public portfolio release 2026-08-26 00:00:35 +03:00
fmd-font Public portfolio release 2026-08-26 00:00:35 +03:00
scripts Preserve executable script modes 2026-08-26 00:03:58 +03:00
src Public portfolio release 2026-08-26 00:00:35 +03:00
tests Public portfolio release 2026-08-26 00:00:35 +03:00
.gitattributes Public portfolio release 2026-08-26 00:00:35 +03:00
.gitignore Public portfolio release 2026-08-26 00:00:35 +03:00
AGENTS.md Public portfolio release 2026-08-26 00:00:35 +03:00
Cargo.lock Public portfolio release 2026-08-26 00:00:35 +03:00
Cargo.toml Public portfolio release 2026-08-26 00:00:35 +03:00
CHANGELOG.md Public portfolio release 2026-08-26 00:00:35 +03:00
LICENSE Public portfolio release 2026-08-26 00:00:35 +03:00
README.md Public portfolio release 2026-08-26 00:00:35 +03:00
rust-toolchain.toml Public portfolio release 2026-08-26 00:00:35 +03:00

FrankenMarkdown — Markdown in, a typeset page out

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.
  • KnuthPlass 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:

![System map](images/system.svg)

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

Same idea, different job — one thing done properly, nothing in the middle, and a result you can check:

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.