Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Markdown to PDF

pdfboss composes CommonMark+GFM source into a themed, paginated PDF: the pdfboss create md CLI subcommand, pdfboss.md.to_pdf in Python, and pdfboss_markdown::to_pdf in Rust. This is the reverse of Markdown output, which extracts Markdown from a PDF; the composed document reads back through the whole toolkit like any other file.

What the composition covers: headings, paragraphs, bulleted and numbered lists (nested included), GFM tables with per-column alignment, fenced and indented code blocks, block quotes, thematic breaks, emphasis, strong, strikethrough and inline code runs, hyperlinks (emitted as real clickable /Link annotations), and images (PNG or JPEG, detected by content, with relative paths resolved against a base directory). Raw HTML fragments are skipped and reported rather than half-rendered.

Two properties worth designing around:

  • Deterministic. The same markdown, theme and options always produce byte-identical output: nothing reads a clock, randomness or the environment.
  • Replace-and-report. Text renders in the standard Helvetica, Times and Courier families, which encode as WinAnsi. A character outside that encoding is replaced with ? and tallied in a report naming each replaced character; a clean document reports nothing.

Themes

A theme is a small CSS subset: element-type selectors only, cascading over the built-in default theme, with inheritance from body down. The twenty selectable elements are body, h1h6, p, code, pre, blockquote, ul, ol, li, table, th, td, a, del and hr. Properties include font-family (helvetica, times, courier, or the sans-serif/ serif/monospace aliases), font-size (pt, px, em, mm, cm, in), font-style, font-weight, color and background-color (named, #hex or rgb()), text-align, text-decoration, line-height, and margin/padding with their per-side forms. Parse errors are strict and located: a typo fails with a line and column rather than being ignored.

body { font-family: times; font-size: 10.5pt; color: #222; }
h1   { font-family: helvetica; font-size: 2.2em; color: #a33; }
code { font-family: courier; background-color: #eee; }
pre  { background-color: #eee; padding: 8pt; }

CLI

pdfboss create md notes.md -o notes.pdf --theme theme.css --size letter

--theme takes a CSS file (omitted, the built-in default theme applies); --size is a3, a4 (default), a5, letter or legal, and --landscape swaps width and height. Relative image paths in the markdown resolve against the input file's directory. The result round-trips immediately:

pdfboss info themed.pdf     # 1 page, 612 x 792 pt
pdfboss text themed.pdf     # the composed text back out

Python

pdfboss.md.to_pdf returns the PDF as bytes. theme is CSS source text, not a path: read the file yourself when the theme lives in one. size names the page size case-insensitively, landscape swaps the dimensions, and base_dir anchors relative image paths (default: the current directory).

from pathlib import Path

import pdfboss

theme = Path("theme.css").read_text()
pdf = pdfboss.md.to_pdf(Path("notes.md").read_text(), theme=theme, size="letter")
Path("notes.pdf").write_bytes(pdf)

Replacements and skipped raw HTML surface as a single UserWarning naming what changed, so a clean run stays silent and a lossy one is visible without being fatal:

import warnings

import pdfboss

with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    pdf = pdfboss.md.to_pdf("Snowman ☃ here")
for warning in caught:
    print(warning.message)   # replaced 1 character unavailable in the standard fonts: '☃'×1

An unknown size or an invalid theme raises PdfError.

Rust

pdfboss_markdown::to_pdf(markdown, &options) returns the composed pdfboss_write::Pdf (still a value, not yet bytes) alongside the replace-and-report Report. Options carries the parsed Theme, the PageSize and the image base_dir; Theme::parse reads CSS source, Theme::default_theme() is the built-in look. Serialize the Pdf with any of the write paths:

use pdfboss_markdown::{to_pdf, Options, PageSize, Theme};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let markdown = std::fs::read_to_string("notes.md")?;
    let theme = Theme::parse("h1 { font-family: helvetica; color: #a33; }")?;
    let options = Options {
        theme,
        page_size: PageSize::Letter,
        ..Options::default()
    };
    let (pdf, report) = to_pdf(&markdown, &options)?;
    if !report.is_empty() {
        eprintln!("{}", report.summary());
    }
    pdf.save("notes.pdf")?;
    Ok(())
}

Because the result is a plain Pdf, everything from Creating PDFs applies afterwards: set metadata, attach an outline or embedded files, add page labels or viewer preferences, append pages of your own, or stream the bytes asynchronously.

Round trip

Composition and extraction are the two directions of one engine. A document composed from Markdown reads back with pdfboss text, renders with pdfboss render, and, closing the loop, pdfboss md re-infers headings, lists and tables from the composed layout. The composed text uses only standard-14 faces, so rendering paints it at the full font tier.