v0.3.0 Rust CLI & library MIT

PDF to Markdown in milliseconds.

pdfmd parses the object graph, inflates streams, decodes fonts and interprets content operators itself — then recovers columns, tables, headings and lists. [dependencies] is empty.

View source
input.pdf1.05 MB · 17 pages
4.4 ms
output.mdstdout
# Layout-Aware Extraction ## 1  Two columns, one pass Reading order comes from glyphpositions, not the order bytesappear in the content stream. | stage   | alloc | ms  || ------- | ----- | --- || inflate | 1     | 0.9 || glyphs  | 0     | 2.1 | ![](figs/fig-1.png)

Illustrative output. The timing is the measured mean below.

0runtime crates
4.4 msmean, 17-page paper
99.04%line coverage
~700 KBrelease binary
01

Install

One crate, no build scripts

install the CLI
cargo install --path .
or build in place
cargo build --release  # → ./target/release/pdfmd
02

Usage

Files, stdin, or a URL

shell
pdfmd input.pdf                     # markdown to stdout
pdfmd input.pdf -o output.md        # write to a file
pdfmd input.pdf --page-breaks       # insert --- between pages
pdfmd input.pdf --extract-images figs -o out.md
cat input.pdf | pdfmd -             # read from stdin
pdfmd https://example.com/x.pdf     # fetched via curl on PATH

Image extraction passes through JPEG and JPEG 2000 streams, and converts decoded 8-bit DeviceGray / DeviceRGB / DeviceCMYK rasters to PNG.

03

Library

Same converter, one error type

main.rs
use pdfmd::{convert_pdf_to_markdown, ConvertOptions};

let result = convert_pdf_to_markdown(&pdf_bytes, &ConvertOptions::default())?;
print!("{}", result.markdown);
04

Markdown

Layout-aware, still best-effort

  • Multi-column pages read left-to-right, then top-to-bottom in each column.
  • GFM tables from ruled path grids and from aligned borderless columns.
  • Headings from tagged-PDF roles, font size, bold, numbered sections, and names such as Abstract.
  • Lists, bold/italic from the font name, monospace as fenced code, running headers stripped.
  • The first paragraph is promoted to # when it is a single-line title.
05

Speed

~3,900 pages/sec

End-to-end CLI on a 1.05 MB, 17-page arXiv paper (Apple Silicon, release, hyperfine --warmup 5 --runs 20 -N): min 3.8 ms, mean 4.4 ms ± 0.3 ms.

Pages extract in parallel via std::thread::scope, fonts are parsed once and cached document-wide, and the content-stream tokenizer and DEFLATE decoder borrow operands straight out of the source bytes — no per-operator or per-Huffman allocation.

document other published pdfmd speedup
1 page2.0 ms0.012 ms 167×
24 pages41.0 ms0.118 ms 347×
60 pages123.0 ms0.201 ms 612×
457 pages777.0 ms0.933 ms 833×

Those rows are throughput targets against other published library numbers, not a claim against those exact corpora or hardware.

06

Internals

Every layer lives in the crate

07

Limitations

What it does not try to do

  • Tables and columns are recovered when the layout is clear; spanned cells, nested tables and complex math stay best-effort reflow.
  • Fonts without a ToUnicode CMap, a standard encoding, or /Differences drop glyphs.
  • Heuristics target academic and prose documents, not forms or invoices.
  • Encrypted PDFs and LZWDecode streams are unsupported by design.