Export Obsidian vaults to PDF with plugin support. Runs locally or in CI.
Project description
Export Obsidian vaults to PDF with plugin support. Runs locally or in CI.
Features
- Folder-tree → single PDF, preserving Obsidian's folder-note convention.
- Wiki links (
[[Page]],[[Page#section|alias]]) resolved to in-document anchors. - Embedded images (
![[img.png]]) resolved by name against a one-pass vault index, picking the file closest to the referencing page. - Obsidian callouts (
> [!note],> [!warning], …) styled per type. - YAML frontmatter parsed and stripped before rendering.
- Redline PDFs: tracked-changes diff between two git commits.
- Pluggable PDF templates (CSS + running header/footer + paged-media
@pagerules). Two ship in the box:defaultandredline. See TEMPLATING.md for the full spec. - Wide tables auto-rotated to landscape pages.
Plugin support modules translate non-core Obsidian-plugin syntax (Folder Notes, Dataview, Meta Bind, Tasks, …) into plain markdown so pandoc can render it. Third-party plugin support is installable as a separate Python package — see Working with plugins for disabling, options and authoring. Built-in modules:
| Name | Priority | Obsidian plugin | What it does |
|---|---|---|---|
folder_notes |
5 | LostPaul/obsidian-folder-notes | Maps the Folder/Folder.md convention onto the folder during tree building, so a folder note becomes its section's body. |
dataview |
10 | blacksmithgu/obsidian-dataview | Renders DQL queries (```dataview blocks, =this.field inline) as static markdown tables/lists at export time. Skipped when not running inside a git repo (because it needs vault-wide indexing). |
meta_bind |
20 | mProjectsCode/obsidian-meta-bind-plugin | Resolves VIEW[…] widgets against the page's frontmatter ({key} substitution, + concatenation, date(YYYY-MM-DD) / datetime(…) formatting). Strips INPUT[…] and BUTTON[…]. |
Installation
1. Install the CLI
macOS / Linux
curl -LsSf uvx.sh/obsidian-pdf-exporter/install.sh | sh
Windows
powershell -ExecutionPolicy ByPass -c "irm https://uvx.sh/obsidian-pdf-exporter/install.ps1 | iex"
A specific version:
curl -LsSf uvx.sh/obsidian-pdf-exporter/0.1.0/install.sh | sh
2. System dependencies
PDF rendering uses WeasyPrint, which links against GTK/Pango/Cairo at runtime. Pandoc is downloaded automatically on first run.
Easiest path — let the CLI diagnose and install for you:
obsidian-pdf-exporter doctor # report what is missing
obsidian-pdf-exporter doctor --fix # run the install command (asks for confirmation)
obsidian-pdf-exporter doctor --fix -y # ditto, no prompt — for CI
doctor only ever runs your platform's own package manager; it never downloads or hosts binaries itself.
| OS | What --fix runs |
Notes |
|---|---|---|
| Debian / Ubuntu | sudo apt-get install -y libpango-1.0-0 libpangoft2-1.0-0 … |
Other detected managers: dnf, pacman, zypper, apk. |
| macOS | brew install pango |
Requires Homebrew. |
| Windows | winget install --id tschoonj.GTK3 --accept-source-agreements --accept-package-agreements |
Installs the upstream GTK3 runtime via winget. Or install Inkscape (bundles GTK). |
If your environment is unusual and WeasyPrint cannot find the libs, set OBSIDIAN_PDF_GTK_PATH to an os.pathsep-separated list of directories containing the GTK DLLs/dylibs.
Quick start
# A standard export of a vault folder
obsidian-pdf-exporter export ./MyVault/SpaceA
# Custom title, version, output path
obsidian-pdf-exporter export ./MyVault/SpaceA \
--title "Quality Manual" --version 3.1.0 \
--output ./out/quality-manual.pdf
# Redline between two commits (must run inside a git repo)
obsidian-pdf-exporter redline ./MyVault/SpaceA \
--from-commit v3.0.0 --to-commit HEAD
# Discovery
obsidian-pdf-exporter list-templates
obsidian-pdf-exporter list-plugins
obsidian-pdf-exporter --help
The shorter alias ope is identical to obsidian-pdf-exporter.
Vault layout
The exporter walks a space folder (the ROOT argument) and turns every subfolder into a section. A folder note named after its folder is the section's body:
SpaceA/
├── SpaceA.md ← root note (becomes the document's intro page)
├── images/ ← attachments folder name is irrelevant
│ └── logo.png
├── Chapter A/
│ ├── Chapter A.md ← folder note for "Chapter A"
│ ├── img/ ← scoped attachments — picked over top-level on ties
│ │ └── logo.png
│ └── Section 1/
│ └── Section 1.md
└── Chapter B/
└── Chapter B.md
Three roots are recognised:
Space/Space.mdexists → used as the root note.Space/Space/Space.md→ the inner folder is the root, its siblings become children.- Neither → an anonymous root, immediate child folders are sections.
The whole vault is indexed once at the start of an export. Folders that contain no markdown (typical attachment folders, regardless of name) are pruned from the page tree automatically. .git, .obsidian, .vscode and any dotfile folder are always skipped.
Commands
export ROOT
Build a single PDF from a vault folder.
| Flag | Description | Default |
|---|---|---|
ROOT (positional) |
Vault folder to export. | required |
-T, --template NAME|PATH |
Template name (see list-templates) or path to a custom CSS file / template directory (see Templates). |
default |
-o, --output PATH |
Output PDF path. | ./.export/<title>.pdf |
-t, --title TEXT |
Document title. Used in <h1> and the running header. |
folder name |
--subtitle TEXT |
Document subtitle. | "" |
-v, --version TEXT |
Version label (e.g. 3.1.0). Shown in metadata + footer. |
empty |
-d, --date YYYY-MM-DD |
Document date. | today |
--include NAME |
Only include these top-level subfolders. Repeatable. | all |
--exclude NAME |
Skip these top-level subfolders. Repeatable. | none |
-O, --option key=value |
Free-form option forwarded to plugins / template. Repeatable. | — |
--disable-plugin NAME |
Skip a plugin (see list-plugins). Repeatable. |
none |
--debug |
Keep the intermediate build directory next to the PDF. | off |
Example:
obsidian-pdf-exporter export ./MyVault/SpaceA \
-T default \
--title "Quality Manual" --subtitle "Process documentation" \
--version 3.1.0 --date 2026-05-07 \
--include "Chapter A" --include "Chapter B" \
--disable-plugin meta_bind \
-O brand=acme -O confidentiality=internal
redline ROOT
Generate a tracked-changes PDF between two git commits. Must run inside the git repo that contains ROOT.
| Flag | Description | Default |
|---|---|---|
ROOT (positional) |
Vault folder inside the repo. | required |
--from-commit REF |
Older commit-ish (baseline). | required |
--to-commit REF |
Newer commit-ish. | HEAD |
-T, --template NAME|PATH |
Template name or path (same as export). |
redline |
-o, --output PATH |
Output PDF path. | ./.export/<title>_redline.pdf |
-t, --title TEXT |
Document title. | folder name |
--subtitle TEXT |
Document subtitle. | computed from hashes |
-O, --option key=value |
Forwarded to plugins / template. Repeatable. | — |
--disable-plugin NAME |
Skip a plugin. Repeatable. | none |
--debug |
Keep intermediates. | off |
Markup conventions in the rendered PDF:
- Word-level changes inside a single block: inline
<ins>/<del>styling. - Whole-block insertions: green
::: {.redline-ins}block. - Whole-block deletions: red
::: {.redline-del}block (headings demoted to bold so deleted sections do not pollute the TOC).
list-templates
Print every known template (built-in, packaged entry-point, user-config directory, runtime-registered) with its source.
list-plugins
Print every installed Obsidian-plugin support module, sorted by priority.
doctor
Diagnose missing native dependencies (GTK/Pango/Cairo, pandoc) and optionally install them via your platform's package manager. See System dependencies for the per-OS commands.
| Flag | Description | Default |
|---|---|---|
--fix |
Run the recommended install command instead of just reporting it. | off |
-y, --yes |
Skip the confirmation prompt (intended for CI). | off |
Exit codes: 0 if everything is in order, 1 if something is missing (or the user declined the fix), 130 if the user aborted at the confirm prompt.
version
Print the installed version.
Markdown features the exporter understands
Anything pandoc's GFM dialect understands works as-is. Obsidian-specific features are handled too:
| Feature | What happens |
|---|---|
[[Page]], [[Page#section|alias]] |
Resolved to an in-document anchor if the page exists in the tree, otherwise rendered as italic text. |
![[image.png]], ![[image.png|alt text]] |
Resolved against the vault index by filename. When several files share a name, the one with the deepest common ancestor with the referencing page wins. SVGs are inlined. |
> [!note] Title … callouts |
Converted to fenced divs with a class per type (note, tip, warning, danger, info, important, example, abstract, todo, success/check/done, question/help, failure/bug, quote). |
Inline > [!note] text (single line) |
Converted to an inline <span class="callout …">. |
| YAML frontmatter | Parsed (used by Meta Bind) and stripped before rendering. |
HTML comments <!-- … --> |
Stripped. |
| Wide tables | Auto-detected; rendered on landscape pages. |
| Image followed by a single italic line | The italic line is promoted to the image's caption. |
Plugins
The built-in plugin support modules are listed in Features. Operational details below.
Disabling a plugin
obsidian-pdf-exporter export ./vault --disable-plugin dataview
--disable-plugin is repeatable.
Passing options to plugins
obsidian-pdf-exporter export ./vault -O dataview.limit=200 -O brand=acme
-O key=value flags reach every plugin's process_markdown via context.options, and the template via ExportContext.options. Naming convention for plugin-scoped options: <plugin>.<key>.
Installing third-party plugin support
Anyone can publish a Python package that registers an ObsidianPlugin subclass under the obsidian_pdf_exporter.obsidian_plugins entry-point group. After pip install <pkg>, the plugin is available without any change to this project.
To author one, see CONTRIBUTING.md.
Templates
A template decides how the PDF looks: CSS, running header/footer, page size, margins, paged-media @page rules. Templates are independent of the markdown pipeline.
Built-in templates
| Name | Purpose |
|---|---|
default |
Brand-neutral A4 portrait with title-block heading, running document title in the top-left, version + page counter in the bottom-right, date in the bottom-left. |
redline |
Same chrome as default, plus styled <ins> / <del> and .redline-ins / .redline-del block markup. Used implicitly by the redline command. |
Selecting a template
obsidian-pdf-exporter export ./vault --template default
obsidian-pdf-exporter list-templates # what's installed right now
The value of --template is resolved in this order:
- If it is an existing path on disk → loaded as a custom template.
- Otherwise → looked up by name in the registry (built-in → packaged entry-point → user-config directory → runtime).
Custom templates without writing Python
Most users only need to swap CSS or add a logo and footer. Three no-Python paths cover that:
A. Drop-in CSS file
obsidian-pdf-exporter export ./vault --template ./brand.css
The filename stem (brand) becomes the template name. CSS only — no
header/footer HTML, no extra assets.
B. Template directory with manifest
my-template/
├── template.yaml # optional; sensible defaults if absent
├── main.css
├── header.html # optional: appended after <body>
├── page.css # optional: @page rules + running() positions
└── logo.svg # optional: copied next to the CSS at render time
# template.yaml — every key is optional
name: legal-pack
description: Legal cover sheet style
css: main.css # default: template.css or sole *.css
assets: [logo.svg] # default: every non-css/yaml/html sibling
running_html: header.html # appended after <body>
page_css: page.css # @page rules etc.
obsidian-pdf-exporter export ./vault --template ./my-template/
C. Named user-config template
Drop the directory into your user-config folder so it appears in
list-templates and works by short name from any vault:
~/.config/obsidian-pdf-exporter/templates/
└── legal-pack/
├── template.yaml
└── main.css
obsidian-pdf-exporter export ./vault --template legal-pack
The location can be overridden with OBSIDIAN_PDF_EXPORTER_TEMPLATES_DIR,
or moved by setting XDG_CONFIG_HOME.
Custom templates that need Python (markdown / HTML hooks)
If your template must mutate the assembled markdown or pandoc HTML
(process_markdown, process_html), or compute decorations from the
export context, write a Template subclass and register it under the
obsidian_pdf_exporter.templates entry-point group:
# my_brand_pdf/template.py
from importlib import resources
from obsidian_pdf_exporter.templates import Template
class MyBrandTemplate(Template):
name = "mybrand"
description = "ACME corporate template"
def get_css(self) -> str:
return resources.files(__package__).joinpath("mybrand.css").read_text("utf-8")
# pyproject.toml of my-brand-pdf
[project.entry-points."obsidian_pdf_exporter.templates"]
mybrand = "my_brand_pdf.template:MyBrandTemplate"
Install it (pip install my-brand-pdf or uv pip install -e .) and it
appears in list-templates immediately.
The full spec — every hook, the ExportContext object, header/footer
injection, asset shipping, runtime registration — is in
TEMPLATING.md.
Environment variables
| Name | Purpose |
|---|---|
OBSIDIAN_PDF_GTK_PATH |
os.pathsep-separated extra directories searched for GTK/Pango/Cairo libs (Windows + macOS). |
OBSIDIAN_PDF_EXPORTER_TEMPLATES_DIR |
Directory scanned for named user templates (default: $XDG_CONFIG_HOME/obsidian-pdf-exporter/templates). |
Exit codes
| Code | Meaning |
|---|---|
0 |
Success. |
1 |
Runtime error during export (see traceback). |
2 |
Bad CLI arguments (e.g. unknown template). |
Contributing
See CONTRIBUTING.md for development setup, testing, and PR guidelines.
License
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file obsidian_pdf_exporter-0.0.1.tar.gz.
File metadata
- Download URL: obsidian_pdf_exporter-0.0.1.tar.gz
- Upload date:
- Size: 49.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.11 {"installer":{"name":"uv","version":"0.11.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fa989a0d013cf3379ebace26ada1ddeca4fccdbc138f3c66a1b9bc12ad925505
|
|
| MD5 |
823126b6ae9b6ef86b33c42c989c4db8
|
|
| BLAKE2b-256 |
42f0e3d4d768242422072124466377852fbd4415558a54a2d021e45f7a5dd22e
|
File details
Details for the file obsidian_pdf_exporter-0.0.1-py3-none-any.whl.
File metadata
- Download URL: obsidian_pdf_exporter-0.0.1-py3-none-any.whl
- Upload date:
- Size: 65.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.11 {"installer":{"name":"uv","version":"0.11.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
23b8f90c044bba09e6f3d2e95c9aad5cb1d167153c037b46289bdc9cf6f2b75d
|
|
| MD5 |
c38617c1e178284226665a9bd9e7e38d
|
|
| BLAKE2b-256 |
08d9482851766bbefbb96f5363ad90833914523d6839cde7b6a46941970f2303
|