Skip to main content

pdfmd

Test PyPI

One command from Markdown to a good-looking PDF. pdfmd wraps Pandoc and fills in everything you would otherwise have to remember: sensible fonts and margins, the right Markdown dialect, your project's metadata/preamble/filter files, and a fallback chain across every PDF engine you have installed.

$ pdfmd lecture
AUTO: READER TITLE MARGIN MONOFONT. Use --verbose to see in full
OK    lecture.md

lecture.md rendered to PDF

That PDF came from examples/lecture.md, a plain Markdown file with no front matter and no configuration. Plain pandoc lecture.md -o lecture.pdf doesn't even get that far: its default engine, pdflatex, stops at the Δ on line 5 with an error. With a Unicode engine it would build, but with wide default margins, and with the # Title line as an ordinary section heading instead of a title.

Why

Pandoc can do nearly anything, but its defaults assume you'll pass the right flags every time. In practice that means one of two things: a long command you copy from an old shell history, or a Makefile in every folder. pdfmd turns that knowledge into defaults:

  • It decides per document, not globally. A file with no YAML front matter is treated as ordinary GitHub-flavoured Markdown. A file that has front matter is assumed to be written for Pandoc and is left alone.
  • It finds your project files. A metadata.yaml, preamble.tex or <name>.lua beside the document (or in a metadata/ folder next to it) is picked up automatically, so every document in a folder shares one house style without any flags.
  • It doesn't give up on the first engine. If lualatex fails or isn't installed, it tries the next engine, then the next, and tells you why each one failed.
  • It shows what it did. Every automatic decision prints an AUTO line, -v shows the exact Pandoc command, and every default can be switched off.

Install

pipx install pdfmd-cli

That puts a pdfmd command on your PATH, with its Python dependencies, in its own isolated environment. uv tool install pdfmd-cli does the same. To update later, run pipx upgrade pdfmd-cli. (The package is named pdfmd-cli because pdfmd on PyPI is an unrelated PDF-to-Markdown tool. The command is still pdfmd. For the latest unreleased code, use pipx install git+https://github.com/aliperdehan/pdfmd.)

pdfmd drives programs that pip can't install, so you also need:

  • Pandoc (required)
  • at least one PDF engine. Typst is the quickest start; a TeX distribution (MacTeX, TeX Live) gives the best results and is what you need for LaTeX packages and math-heavy documents.
brew install pipx pandoc typst              # macOS, the quick start
brew install --cask mactex-no-gui           # optional: full LaTeX (large)
sudo apt install pipx pandoc texlive-xetex  # Debian/Ubuntu
py -m pip install --user pipx; py -m pipx ensurepath          # Windows
winget install --id JohnMacFarlane.Pandoc; winget install --id Typst.Typst

Every push is tested on Windows, macOS and Linux (Pandoc + Typst: single files, CSV tables, a book, HTML output); see the Test workflow. LaTeX engines aren't part of that automated test on Windows yet.

Optional extras: Quarto for .qmd files, pandoc-crossref for @fig:/@tbl: references, and LibreOffice for Office files.

Without pipx

pdfmd.py is a single file that needs Python 3.10+. It also runs directly, and pyyaml/pypdf are optional (features that need them are skipped with a warning):

git clone https://github.com/aliperdehan/pdfmd.git ~/pdfmd
echo 'alias pdfmd="python3 ~/pdfmd/pdfmd.py"' >> ~/.zshrc   # or ~/.bashrc

macOS's built-in /usr/bin/python3 is 3.9, which is too old; pdfmd says so and exits.

Check what pdfmd can find on your system:

$ pdfmd --check-dependencies
OK    pandoc  (/opt/homebrew/bin/pandoc)
OK    1. lualatex  (/Library/TeX/texbin/lualatex)
OK    2. xelatex  (/Library/TeX/texbin/xelatex)
OK    3. pdflatex  (/Library/TeX/texbin/pdflatex)
OK    4. latexmk  (/Library/TeX/texbin/latexmk)
OK    5. tectonic  (/opt/homebrew/bin/tectonic)
OK    6. typst  (/opt/homebrew/bin/typst)
OK    7. weasyprint  (/opt/homebrew/bin/weasyprint)
MISS  8. wkhtmltopdf
...
OK    14. soffice  (/Applications/LibreOffice.app/Contents/MacOS/soffice)
OK    quarto (only needed for .qmd files)  (/usr/local/bin/quarto)

The numbers are the fallback order, and also shortcuts: -e 6 means -e typst.

Usage

Every command below can be run from inside examples/.

One document

pdfmd lecture                 # finds lecture.md, writes lecture.pdf beside it
pdfmd lecture.md --open       # ...and opens it when done
pdfmd ~/notes/lecture.md -d   # write the PDF into the current directory instead
pdfmd lecture -w              # watch: rebuild on every save, until Ctrl+C

A bare name is looked up as <name>.md. The name can contain dots: pdfmd notes-v1.2 builds notes-v1.2.md.

Other output formats

The format is taken from -o's extension, or given explicitly with --to:

pdfmd lecture -o lecture.html
pdfmd lecture -o lecture.docx
pdfmd lecture --to typst -o lecture.typ
pdfmd lecture -o lecture.tex      # a complete, compilable .tex, not a fragment

Slides

pdfmd slides -p                   # Beamer slides; each heading starts a slide

A Beamer slide from examples/slides.md

A whole folder

$ pdfmd notes -b
AUTO: READER TITLE MARGIN MONOFONT. Use --verbose to see in full
AUTO: MARGIN. Use --verbose to see in full
AUTO: MARGIN. Use --verbose to see in full
OK    notes/lecture.md
OK    notes/week1.md
OK    notes/week2.md

-b converts every .md in the folder into its own PDF, in parallel (-j N sets the number of workers). Add --recursive to include subfolders, and -o DIR to collect the PDFs somewhere else.

A book or report from several files

$ pdfmd book -r -o book.pdf
OK    book/01-intro.md
OK    book/02-methods.md
AUTO: MARGIN. Use --verbose to see in full
OK    REPORT  book.pdf

-r (also spelled --report or --book) joins every .md in the folder into a single PDF, in the order of each file's chapter: front-matter field. See examples/book/. -i FILE leaves one file out, and --exclude-unnumbered skips files without a chapter:.

Tables straight from a CSV file

Measured values:

::: {.csv file="data.csv"}
:::

A CSV file rendered as a table

  • The delimiter is detected from the extension (.tsv means tab), or set with delimiter=";".
  • The first row is the header unless you add header="false".
  • Large files are capped at 10 rows × 7 columns, so a huge CSV can't silently fill 40 pages. rows=all or cols=20 raise the cap. When a table is cut, the PDF itself shows a note saying so.

This works for every output format.

Not just Markdown

pdfmd paper.tex          # compiled directly with a LaTeX engine: reruns until
                         # references settle, runs bibtex/biber, and leaves
                         # no .aux/.log clutter (--keep-aux keeps them)
pdfmd minutes.docx       # Word/PowerPoint/Excel/ODF: converted by LibreOffice
pdfmd analysis.qmd       # handed to Quarto, so code chunks actually run
pdfmd page.html          # anything else Pandoc can read (give the extension)

What it does automatically

Most of these print an AUTO line, and each can be switched off individually.

AUTO kind When What happens
READER no YAML front matter reads the file as GitHub-flavoured Markdown (content-sized table columns, relaxed blank-line rules)
TITLE no front matter, first line is # Title that heading becomes the document title, and the remaining headings move up one level
MARGIN no margin or geometry set anywhere 1-inch margins instead of LaTeX's wide defaults
MAINFONT no mainfont: and no -f STIX Two Text, retried with DejaVu Serif if any glyph is missing (Times New Roman if DejaVu isn't installed)
MONOFONT the document contains code JetBrains Mono for code (Menlo or another installed monospace font if it isn't installed)
tablewidth a wide pipe table balances column widths so the table fits the page, and leaves narrow tables at their natural width
YAML / TEX / LUA project files found attaches metadata.yaml, preamble.tex, <name>.lua (see below)
citeproc @key / [@key, p. 90] citations adds --citeproc, so citations and the reference list render from your bibliography: without any flag (--no-citeproc turns it off)
crossref @fig:/@tbl: references adds the pandoc-crossref filter, ahead of citeproc
papersize pagesize: a4 (a common typo) converts it to Pandoc's real papersize:

-v explains each decision and prints the exact command it runs:

$ pdfmd lecture -v
CMD  pandoc lecture.md -o lecture.pdf --pdf-engine=lualatex -f gfm -V 'mainfont=STIX Two Text' -V geometry:margin=1in -V 'monofont=JetBrains Mono' --shift-heading-level-by=-1 --lua-filter .../pdfmd-tablewidth.lua
CMD  pandoc lecture.md -o lecture.pdf --pdf-engine=lualatex -f gfm -V 'mainfont=DejaVu Serif' ...
AUTO READER  lecture.md: no YAML front matter; reading as gfm
AUTO TITLE  lecture.md: promoted leading '# ' heading to Pandoc title metadata
AUTO MARGIN  lecture.md: no geometry/margin set; using geometry:margin=1in on LaTeX-family engines
AUTO MONOFONT  lecture.md: has code but no monofont set; using JetBrains Mono on LaTeX-family engines
OK    lecture.md

The second CMD line is the font fallback at work: STIX Two Text was missing a glyph, so the document was rebuilt with DejaVu Serif. (Temporary file paths are shortened here.)

When a PDF doesn't look the way the Markdown suggests, run -v first. The cause is usually one of these automatic decisions, and -v names it.

Switching defaults off

pdfmd lecture --no-auto                  # everything off: close to plain pandoc
pdfmd lecture --no-auto margin mainfont  # only these

Passing options to Pandoc

Any option pdfmd doesn't recognise is passed straight to Pandoc:

pdfmd lecture --toc --number-sections
pdfmd lecture -V fontsize=12pt

Settings inside the document

Or put settings in the document itself, so nobody has to remember the flag:

---
title: Lab report
pdfmd-options:
  no-auto: [margin, monofont]
  pdf-engine: tex        # only TeX engines; never fall back to HTML ones
---

pdf-engine: takes an engine name (lualatex, typst, ...) or a family: tex, typst, html or office. A family limits the fallback chain to that kind of engine. For example, a document full of chemical structures can require TeX, and a plain memo can skip the slow LaTeX run. -e on the command line always wins, and a bare -e means "try every engine".

Project files

pdfmd looks next to the document, and in a metadata/ folder beside it, for:

File Used as
metadata.yaml shared Pandoc metadata (fonts, bibliography, CSL, ...)
<name>.yaml per-document metadata, layered on top of metadata.yaml
report.yaml / book.yaml metadata for -r builds
preamble.tex, latex-preamble.tex, <name>-preamble.tex, preamble-*.tex LaTeX added to the header, generic files first
<name>.lua a Pandoc Lua filter for that document

Only these names are picked up automatically. An unrelated .lua or .tex file lying in the folder never changes a render. When several YAML files could apply and none is clearly meant, pdfmd stops and asks you to choose with -y FILE, rather than guessing. -y alone turns discovery off.

With a metadata/ folder, a report's own folder can hold nothing but report.md and report.pdf. Relative paths inside the metadata (such as bibliography: refs.bib) resolve from metadata/, and image paths in the document still resolve from the document's own folder.

One shared metadata file can also be symlinked into many folders. A relative bibliography: refs.bib inside it then finds the refs.bib next to the file's real copy, so you never need an absolute path.

Build stamps and snapshots

Both are off by default.

  • --stamp keeps a BUILD NOTES HTML comment at the end of the .md up to date. It records when the document was compiled and with which pdfmd version, plus the versions of any LaTeX packages you name with --stamp-packages. The comment is invisible in the PDF. --stamp-mode history also keeps a list of past compiles.
  • --backup saves a timestamped copy of the source into backup/ after every successful build. A copy is skipped when nothing changed, and keep: 30 limits how many are kept.

Both can be turned on for a whole folder from metadata.yaml:

pdfmd-options:
  stamp: true
  backup: { dir: backup, keep: 30 }

Separately, when pypdf is installed, every PDF built by pdfmd gets two hidden metadata keys, PdfmdVersions and PdfmdBuildDate, which you can read with pdfinfo -meta. Turn this off with --no-stamp-pdf-metadata.

Reference

  • pdfmd --help lists every flag.
  • The docstring at the top of pdfmd.py is the full reference for each behaviour and its edge cases.
  • CHANGELOG.md records what changed in each version and why.

License

MIT

Metadata

Release files for pdfmd-cli 3.11.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pdfmd-cli 3.11.2
File Size Uploaded
pdfmd_cli-3.11.2.tar.gz 96.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pdfmd-cli 3.11.2
File Interpreter ABI Platform
pdfmd_cli-3.11.2-py3-none-any.whl Python 3 none any Details

Total release size: 188.2 kB

Release files / pdfmd_cli-3.11.2.tar.gz

Download URL pdfmd_cli-3.11.2.tar.gz
Size 96.6 kB
Tags Source
SHA-256 checksum
How to use checksums
d14eab88cf962481c6e09d6ed2ec2dfa87cecf94b285664fef07cec48a6d07a3
BLAKE2b-256 checksum
How to use checksums
a17f048566c2dfd9b6365754a0d1842b431a22fd446228fa0f16b8c02bd7573c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.

Transparency log

Release files / pdfmd_cli-3.11.2-py3-none-any.whl

Download URL pdfmd_cli-3.11.2-py3-none-any.whl
Size 91.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6e9914da865466eb27c33f8fefc9f00c7cd5cf7cbe86e4d2c67ba0340d24822c
BLAKE2b-256 checksum
How to use checksums
656dde304c3423b98951a4bd29033fe2d40e4ed0dfad41f881943af8199ff15a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.

Transparency log

Release history Release notifications | RSS feed

3.15.1

2 release files

3.15.0

2 release files

This release

3.11.2 This release

2 release files

3.11.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page