Write in Markdown. Track in Git. Ship as PDF.
md2pdf is a CLI for turning the technical Markdown you already write — design docs, runbooks, security reports — into clean, deliverable PDFs (or Word/DOCX). Mermaid diagrams render as inline SVG. Japanese (and other CJK) text renders without font breakage. Single Go binary, drop-in for CI.
Output of examples/06-japanese-document: Japanese text and a colored Mermaid flowchart, both rendered cleanly.
- 📐 Mermaid diagrams as inline SVG — vector-clean, no rasterization
- 🇯🇵 Japanese / CJK text out of the box — Noto Sans CJK JP preconfigured
- 📝 GitHub-flavored Markdown — tables, fenced code blocks, strikethrough
- 📃 PDF or DOCX output —
-format docxexports editable Word documents (via pandoc) - 🎞️ Slides and PowerPoint —
-slides(or Marp'smarp: true) prints one page per slide, and-format pptxwrites a PowerPoint deck with presenter notes - 👀 Read it in the terminal —
-format console, or themdviewname the same binary also answers to, renders the document as styled, wrapped ANSI text and pages it throughless(no conversion tools needed), drawing Mermaid flowcharts and sequence diagrams as inline images on kitty, iTerm2 and Sixel terminals and as box-drawing text art everywhere else - 🤖 CI-friendly single binary —
go installand you're done - 📄 Configurable — page size, margins, fonts
Homebrew (macOS / Linux) — recommended
brew install 135yshr/tap/md2pdfThis installs two commands: md2pdf, and mdview for reading documents in
the terminal (see Reading in the terminal with
mdview — it is the same binary under a
second name). It also installs
mermaid-cli,
so diagrams work out of the box. Two things it cannot install, because a
Homebrew formula is not allowed to depend on a cask:
brew install --cask chromium # needed for PDF and for Mermaid
brew install --cask font-noto-sans-cjk # needed for Japanese text in PDFGoogle Chrome counts as the browser; point md2pdf at any location with
CHROME_PATH. -format console needs neither.
pandoc is declared optional and is only needed for -format docx — either
brew install pandoc, or brew install --with-pandoc 135yshr/tap/md2pdf.
Go install
go install github.com/135yshr/md2pdf/cmd/md2pdf@latest
# Optional: the mdview name, which reads documents in the terminal by default.
# go install can only produce one name, so link the second one yourself.
ln -s "$(go env GOPATH)/bin/md2pdf" "$(go env GOPATH)/bin/mdview"Build from source
git clone https://github.com/135yshr/md2pdf.git
cd md2pdf
go build -o md2pdf ./cmd/md2pdf
ln -s md2pdf mdview # optional, see "Reading in the terminal with mdview"md2pdf uses external tools for diagram rendering and PDF generation. Install them after installing md2pdf:
# Mermaid CLI (diagram rendering)
npm install -g @mermaid-js/mermaid-cli
# A Chromium (PDF generation)
brew install --cask chromium # macOS; Google Chrome also works
sudo apt install -y chromium # Debian/Ubuntu
# pandoc — only needed for DOCX output (-format docx)
brew install pandoc # macOS / Linux (Homebrew)
# sudo apt install pandoc # Ubuntu / DebianFor Japanese text support, install the Noto Sans CJK JP font:
macOS
brew install font-noto-sans-cjkUbuntu / Debian
sudo apt install fonts-noto-cjkmd2pdf document.mdA document.pdf file will be generated in the same directory. To export Word
instead, run md2pdf -format docx document.md.
Installed for you by brew install 135yshr/tap/md2pdf:
| Dependency | Purpose | Install |
|---|---|---|
| mmdc | Mermaid → SVG/PNG | brew install mermaid-cli, or npm install -g @mermaid-js/mermaid-cli |
| A Chromium browser | HTML → PDF, and slide images for PPTX | brew install --cask chromium / apt install chromium (Google Chrome also works; override with CHROME_PATH) |
| pandoc | Markdown → DOCX (only for -format docx) |
brew install pandoc / apt install pandoc |
| Noto Sans CJK | Japanese font (optional) | brew install --cask font-noto-sans-cjk / apt install fonts-noto-cjk |
| Go 1.26+ | Build from source only | https://go.dev |
-format console needs none of these — it renders in-process and only uses a pager if one is installed.
Check your machine with -doctor, which reports what is present and which
formats can run, without converting anything:
$ md2pdf -doctor
Chromium ok /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
printing PDFs, capturing PPTX slides, and rendering Mermaid diagrams via mmdc
mmdc missing brew install mermaid-cli, or npm install -g @mermaid-js/mermaid-cli
Mermaid diagrams in pdf, html and docx output — only needed for documents containing Mermaid diagrams
pandoc missing brew install pandoc, or apt install pandoc
-format docx
Noto CJK font missing brew install --cask font-noto-sans-cjk, or apt install fonts-noto-cjk
Japanese text in pdf and html output — output still works without it
pdf ready
html ready
docx not ready (needs pandoc)
pptx ready
console ready
It exits non-zero when a format is blocked, so it works as a check in a setup
script. Note what it does not claim: a missing mmdc leaves pdf ready,
because a document with no Mermaid blocks converts without it.
Why the browser and the font are not installed by Homebrew. Both are casks,
and a formula cannot depend on a cask: Homebrew's formula DSL has no cask:
dependency at all, while the cask DSL accepts both formula: and cask:. So
brew install 135yshr/tap/md2pdf brings mermaid-cli (and pandoc if you ask
for it) and stops there.
Note also that brew install mermaid-cli does not bring a browser of its
own — Homebrew's own formula test asserts that mmdc fails with
Could not find chrome-headless-shell. md2pdf works because it hands mmdc the
browser it resolved itself, which is the same one it uses to print the PDF.
md2pdf [options] <input.md>
mdview [options] <input.md>... # the same binary, rendering to the terminal| Flag | Default | Description |
|---|---|---|
-o <path> |
<input>.pdf |
Output path (.docx extension implies -format docx; not allowed with -format console; required when reading from stdin for pdf/docx) |
-format <fmt> |
pdf (console when invoked as mdview) |
Output format: pdf, html, docx, or console (aliases term, terminal; inferred from -o extension when omitted, including .html/.htm) |
-doctor |
— | Report which runtime dependencies are present and which formats can run, then exit |
-css <path> |
— | Custom CSS applied after the built-in stylesheet, so its rules win. Repeatable; later files override earlier ones. Used by pdf and html |
-font <path> |
auto-detected | Noto Sans CJK JP Regular font |
-font-bold <path> |
auto-detected | Noto Sans CJK JP Bold font |
-font-medium <path> |
auto-detected | Noto Sans CJK JP Medium font |
-mmdc <path> |
auto-detected | Path to mmdc binary |
-pandoc <path> |
auto-detected | Path to pandoc binary (used for -format docx) |
-docx-font <family> |
Yu Gothic |
Font family for DOCX output (Latin + East Asian) |
-puppeteer-config <f> |
auto-generated | Puppeteer JSON config for mmdc |
-page-size <size> |
A4 |
A4, Letter, or A3 |
-margin-top <m> |
18mm |
Top margin |
-margin-bottom <m> |
18mm |
Bottom margin |
-margin-left <m> |
14mm |
Left margin |
-margin-right <m> |
14mm |
Right margin |
-width <cols> |
terminal width | Console word-wrap width, capped at 120 columns (-format console) |
-style <name|path> |
auto |
Console theme: auto, dark, light, notty, ascii, dracula, pink, tokyo-night, or a JSON stylesheet path (env: GLAMOUR_STYLE) |
-pager |
true | Page console output through $PAGER (default less -R -F); -pager=false writes straight to stdout |
-mermaid-render <mode> |
auto |
How Mermaid blocks are drawn in console output: auto, image, ascii or source (-format console) |
-v |
false | Verbose output (progress logs go to stderr) |
-version |
— | Print version and exit |
# Basic conversion
md2pdf document.md
# Custom output path
md2pdf -o report.pdf document.md
# Export to Word (DOCX)
md2pdf -format docx document.md
md2pdf -o report.docx document.md # format inferred from extension
# Read the document in the terminal
mdview document.md # shorthand for -format console
mdview docs/*.md # several documents, in order
md2pdf -format html document.md # stop at HTML, no Chromium
md2pdf -format html -css brand.css document.md # iterate on CSS in a browser
md2pdf -css brand.css -css client.css -o report.pdf document.md
md2pdf -format console document.md
md2pdf -format console -style dark -width 100 document.md
md2pdf -format console -pager=false document.md | cat # plain text, no color
md2pdf -format console -mermaid-render image document.md # require inline diagrams
md2pdf -format console -mermaid-render ascii document.md # always draw text art
md2pdf -format console -mermaid-render source document.md # always show Mermaid source
cat doc.md | md2pdf -format console - # read from stdin
gh pr view 41 --json body -q .body | md2pdf -o pr.pdf - # pipe into a PDF
md2pdf -format console docs/*.md # several files at once
# Explicit font path
md2pdf -font /usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc document.md
# Letter size with wider margins
md2pdf -page-size Letter -margin-left 20mm -margin-right 20mm document.md
# Verbose output
md2pdf -v document.md-slides, or marp: true in the front-matter, turns pdf and html output into
a deck: the document is split at every top-level ---, *** or ___, and each
slide starts a new page.
---
marp: true
---
# Title slide
---
## Second slide-
Only separators at the top level split. A
---inside a code block, a list or a blockquote stays where it is, and a setext underline (Titleover---) is still a heading. -
Two separators in a row, or one at either end, make an empty slide, as in Marp.
-
Each page is exactly one slide with no margin: 16:9 (1280×720 px) by default, or 4:3 (960×720 px) with
size: 4:3in the front-matter.-page-sizeand-margin-*are for documents, and passing them for a deck is an error rather than silently ignored. Content that does not fit a slide is cut off at its edge instead of spilling onto another page — and md2pdf says so, on stderr and without needing-v:warning: deck.md: slide 2 overflows the slide (height); the rest is cut off -
Decks get their own stylesheet (30px text, slide-sized headings, padded slides);
-cssfiles still apply after it. -
The Marp
classdirective adds classes to a slide.leadis built in and centers a title slide:<!-- _class: lead --> # Title
_classapplies to its own slide,<!-- class: x -->to its slide and every one after it, andclass:in the front-matter to every slide. Other Marp directives (paginate,header,theme, ...) are recognized and ignored for now, rather than showing up as text. -
A Mermaid diagram is scaled down to the space its slide has left for it, keeping its aspect ratio, so a large flowchart fits under its heading instead of being cut off. A small diagram keeps its natural size. A diagram inside a list or blockquote is held to the slide's height rather than fitted exactly, so it may still need a slide of its own.
-
Any other HTML comment on its own line is a presenter note for its slide, as in Marp. It is left off the slide; PPTX output puts it on the slide's notes page.
-
-slidesis rejected withdocxandconsole, which have no pages. Amarp: truedocument converted to those formats renders as a document.
-format pptx, or an -o path ending in .pptx, writes a PowerPoint deck. The
document is always treated as slides — marp: true is not needed — so everything
under Slides applies.
md2pdf -format pptx deck.md # writes deck.pptx
md2pdf deck.md -o talk.pptx- Each slide is rendered by the same Chromium that prints the PDF and placed on
its own PowerPoint slide as a picture at twice its size (2560×1440 for 16:9),
so the deck looks exactly like the PDF — fonts,
-css, Mermaid diagrams and all. The trade-off is that slide contents are not editable in PowerPoint. - Presenter notes (HTML comments) go to each slide's notes page, and the
front-matter
titlebecomes the presentation's title. - It opens in PowerPoint, Keynote, Google Slides and LibreOffice Impress. It needs a Chromium, like PDF output, and no other tool.
- Engineers writing design docs, runbooks, or technical specs in Markdown
- Teams using Mermaid diagrams (flowcharts, sequence diagrams) in their documents
- Consultants and security teams delivering reports as PDFs to clients
- Japanese-speaking developers tired of fighting font breakage in Markdown-to-PDF tools
- Anyone who manages docs in Git and needs to ship them as PDF artifacts
flowchart TD
A["Markdown (.md)"] --> E{format?}
E -->|pdf| B[goldmark parser]
B --> C[HTML builder]
B -->|extracts Mermaid blocks| D[mmdc CLI]
D -->|inline SVG| C
C --> F["Chromium (DevTools Protocol)"]
F --> G[PDF output]
E -->|docx| J[Mermaid → PNG]
J --> H["pandoc (gfm reader)"]
H --> I[DOCX output]
E -->|console| K["glamour (ANSI renderer)"]
K --> L["Terminal / $PAGER"]
Each format reads from the source that renders best for it.
- PDF (default) — goldmark converts Markdown to HTML (GFM tables, fenced code blocks), Mermaid blocks are rendered to inline SVG via
mmdc, a self-contained HTML file is assembled with GitHub-flavored CSS and@font-facedeclarations for Noto Sans CJK JP, and a headless Chromium browser, driven directly over the DevTools Protocol, prints it to PDF. No Python or Playwright is involved — the same browser also servesmmdc, so one Chromium covers both stages. - Console — the Markdown is rendered to styled ANSI text by glamour (the library behind glow), wrapped to the terminal width and paged through
$PAGER. The theme follows the terminal background unless-stylesays otherwise; color is dropped entirely whenNO_COLORis set or the output is piped. Mermaid blocks are drawn as inline images when the terminal supports one of the image protocols below, as box-drawing text art when it does not, and otherwise stay visible as their source — so console output still needs no conversion tools of its own. - DOCX — the Markdown is sent directly to
pandoc(itsgfmreader, no HTML in between), so pandoc produces clean, Word-native paragraph and list styles. Mermaid blocks are rasterized to PNG and spliced back in as image references (Word cannot reliably display pandoc-embedded SVG). A generated reference document gives the output a readable, Japanese-friendly look: a 10.5pt body, compact blue headings, bordered GFM tables, and theYu Gothicfont (override with-docx-font).
Custom CSS. -css injects a stylesheet after the built-in
GitHub-flavored CSS, in the same inline <style> block, so your rules win the
cascade:
md2pdf -css brand.css document.mdIt is repeatable, and later files override earlier ones — useful for a shared house style plus a per-client override:
md2pdf -css house.css -css client-acme.css -o acme.pdf document.mdPrecedence is: built-in stylesheet → first -css → … → last -css.
A stylesheet containing </style is rejected, since it would close the inline
block the CSS is embedded in. A missing -css file fails immediately, naming it.
-css applies to pdf and html. DOCX styling goes through pandoc's reference
document instead (see -docx-font), and console output is styled by -style.
HTML output. -format html stops the pipeline after the HTML is assembled
and writes that file, so Chromium is never started:
md2pdf -format html document.md # -> document.html
md2pdf -o page.html document.md # .html/.htm also infers the formatThis is the fast way to iterate on custom CSS — reload in a browser instead of re-rendering a PDF each time — and it is useful on its own for publishing to a static site.
The HTML is self-contained in the same sense the PDF pipeline needs: the
stylesheet is inlined and Mermaid diagrams are embedded as inline SVG.
Image paths are left exactly as written in the Markdown, so they resolve
relative to the HTML file. That works for the default output location beside the
input; if you send the HTML elsewhere with -o, copy the images along with it.
A single .md path works for every format. Two extras relax that:
Standard input. - reads the document from standard input instead of a file:
cat doc.md | md2pdf -format console -
gh issue view 30 --json body -q .body | md2pdf -o issue.pdf -- Relative paths inside the document (images, resources) resolve against the current directory, since a piped document has no location of its own.
-ois required forpdfanddocx, because there is no input filename to derive the output name from.- Empty standard input is an error, not an empty document: an empty pipe almost always means the command upstream produced nothing, and failing loudly makes the pipeline fail too. Whitespace-only counts as empty. An empty file is unaffected and still renders an empty document, as it always has.
- Passing
-when standard input is a terminal fails immediately rather than waiting silently for something to be typed.
Several files, console only. Multiple paths render in order, separated by a rule:
md2pdf -format console docs/*.md
md2pdf -format console intro.md guide.md appendix.md- Only
-format consoleaccepts more than one path. Merging documents into one PDF or DOCX would need decisions about heading level shifts, page breaks and per-file image bases, sopdfanddocxkeep the single-document contract and say so explicitly. - Every path is checked before anything is rendered, so a typo in the third of three files fails immediately and names that file.
- Repeats are kept: the same file passed twice renders twice.
-cannot be combined with file paths.
The separator is glamour's own horizontal rule, so it follows the theme — dimmed
under a dark style, plain ASCII under -style ascii, colorless under
NO_COLOR — and looks like a --- written in the document itself.
Front-matter. A YAML block at the very top of a document is read as metadata rather than rendered, in every format:
---
title: Quarterly report
---
# Q3titlebecomes the HTML/PDF<title>; without it the first heading is used, as before. Other keys are ignored for now.- The block must start on the first line with
---and close with---or.... A---anywhere else is an ordinary thematic break, and an unclosed block or one holding plain text rather thankey: valuepairs renders as Markdown, exactly as it did before front-matter was recognized. - Invalid YAML is an error naming the file, since the block was clearly meant as metadata.
md2pdf -format console document.md is a lot to type for something you do all
day, so the binary also answers to the name mdview. Invoked under that
name it renders to the terminal by default:
mdview document.md
mdview docs/*.md
git show HEAD:README.md | mdview -It is the same binary — nothing extra is installed and nothing is duplicated.
brew install 135yshr/tap/md2pdf creates the name for you. Elsewhere, make it
yourself, with either a symlink or a copy:
ln -s "$(command -v md2pdf)" ~/.local/bin/mdview # Unix
copy md2pdf.exe mdview.exe # Windows (the release zip
# already ships mdview.exe)The name only chooses the default; everything else still works, and
md2pdf itself is unchanged. In order of precedence:
| Invocation | Output |
|---|---|
md2pdf document.md |
PDF, as always |
mdview document.md |
the terminal |
mdview -format pdf document.md |
PDF — an explicit -format wins |
mdview -o report.pdf document.md |
PDF — the -o extension wins over the name |
mdview -o notes.txt document.md |
an error: that path names no format, and -o cannot be used with terminal output |
Because the choice comes from argv[0], a parent process can invoke the binary
under any name it likes. That is only ever a default: a script that cares which
format it gets should pass -format.
With -format console, Mermaid blocks are drawn as diagrams rather than printed
as source. md2pdf walks a fallback chain and -mermaid-render picks where to
start. Anything the chain cannot draw — a diagram type without a text-art
renderer, or a block that fails to parse — keeps its Mermaid source, so no
document ever loses content:
| Mode | Behavior |
|---|---|
auto (default) |
Inline image → text art → Mermaid source, taking the first that works |
image |
Require an inline image. A terminal that cannot show one, a missing mmdc, or a diagram that fails to rasterize are all errors rather than a quiet downgrade |
ascii |
Always draw box-drawing text art (source only for diagram types it cannot draw) |
source |
Always print the Mermaid source as a code block |
Step 1 — inline images. Used when the terminal supports an image protocol,
the output is an interactive terminal, NO_COLOR is unset, and mmdc is
installed. Protocols are detected from TERM and TERM_PROGRAM:
| Protocol | Terminals |
|---|---|
| kitty graphics | kitty, Ghostty (TERM=xterm-kitty, TERM=xterm-ghostty, KITTY_WINDOW_ID) |
| iTerm2 inline images | iTerm2, WezTerm (TERM_PROGRAM=iTerm.app, TERM_PROGRAM=WezTerm) |
| Sixel | foot, mlterm, yaft, and any TERM containing sixel |
Step 2 — box-drawing text art. Rendered in-process by mermaid-ascii, so it needs no external tool and works on any terminal, when piped, and under the pager:
┌─────────────┐ ┌─────────────┐
│ │ │ │
│ Client Apps ├─1─Login─►│ API Gateway │
│ │ │ │
└─────────────┘ └─────────────┘
Only flowchart / graph and sequenceDiagram are drawn as text art. Every
other diagram type — gantt, pie, erDiagram, stateDiagram, classDiagram
and the rest — keeps its Mermaid source. (erDiagram is excluded deliberately:
the library parses it but lays the entities out misaligned.)
Step 3 — Mermaid source, exactly as v0.7.0 printed it.
Notes:
-style ascii, orGLAMOUR_STYLE=ascii, draws the art with plain+,-and|instead of Unicode box-drawing characters.- Text art wider than the wrap width is clipped to it, keeping the diagram's shape rather than letting the terminal fold lines into fragments.
- Inline images bypass the pager: neither the kitty nor the iTerm2 sequences
survive a trip through
less. Text art pages normally, so-mermaid-render asciiis the way to page a long document and still see diagrams. - Images are never written when the output is piped or redirected, or when
NO_COLORis set; those cases fall through to text art, which is plain text. With-mermaid-render imagethey are an error instead. - A Mermaid block carrying a YAML frontmatter title block is handled normally; the title is printed above the diagram, as Mermaid does.
- Sixel detection is environment-based, because querying the terminal directly
would require putting it into raw mode and waiting for a reply. A Sixel
terminal that sets none of the markers above is therefore not detected and
gets text art instead;
-mermaid-render imagereports the missing capability rather than forcing a protocol.
Pandoc, md-to-pdf, and other Markdown-to-PDF tools are powerful and flexible. md2pdf focuses on a narrower use case: turning technical Markdown with Mermaid diagrams and Japanese (CJK) text into clean PDFs with minimal setup.
The screenshots below show what happens when you convert the same Japanese
PRD document (examples/06-japanese-document/input.md) with each tool out of
the box.
These are not bugs in Pandoc or md-to-pdf — both can produce great results once you configure CJK fonts and Mermaid plugins. md2pdf is just preconfigured for this exact combination, so it works without any extra setup.
md2pdf drives a headless Chromium to print the PDF, and mmdc needs one to
render diagrams. Both use the same browser, located in this order:
CHROME_PATH, if set- common Linux paths (
/usr/bin/chromium,/usr/bin/google-chrome, …) - a Playwright browser cache, if one happens to exist
/Applications/Google Chrome.appand/Applications/Chromium.appon macOS
If none is found, point md2pdf at the browser you have:
export CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"Console output needs no browser at all, so -format console keeps working.
# Every test. The integration test skips itself when its tools are absent.
go test ./...
# Require the integration toolchain (mmdc + a Chromium): a missing tool
# now fails instead of skipping.
MD2PDF_REQUIRE_INTEGRATION=1 go test ./...
# With verbose output
go test -v ./...Contributions are welcome! Please read CONTRIBUTING.md before opening a pull request.
MIT License — see LICENSE.