QuincyLeo29 downloadsExport the active reading view's live rendered DOM to PDF with Chromium's native print engine.
English | 简体中文
Export the active Obsidian Reading view's live rendered DOM to PDF via Chromium's native print engine. Faithfully preserves the WYSIWYG layout, theme, and content produced by second-pass plugin rendering.
isDesktopOnly: true; depends on Electron's BrowserWindow.webContents.printToPDF)app:, file:, blob:, capacitor:, same-origin http) are converted to Data URLs; <canvas> elements are snapshotted as PNGs; open Shadow DOM is cloned along with everything elsestyleSheet + adoptedStyleSheets + CSS custom properties (CSS Variables), so theme/colors/fonts stay consistent~/ expansioncontextIsolation, disables nodeIntegration and DevTools; during snapshotting <script> tags, on* event attributes, and javascript: URLs are stripped| Item | Requirement |
|---|---|
| Obsidian | Desktop ≥ 1.4.0 (other versions untested) |
| OS | Windows / macOS / Linux (follows Obsidian's support range) |
| Mobile | ❌ Not supported (Electron dependency) |
| Note state | Must be in Reading view (preview mode) |
Manual install (only supported method for now)
<Your Vault>/.obsidian/plugins/export-readview-pdf/
├── main.js
└── manifest.json
.obsidian/plugins/export-readview-pdf/ inside your vaultnpm install and npm run build in the repository rootmain.js and manifest.json into the plugin directoryOpen a note, switch to Reading view, then choose one of:
book-open-text)Export Read-view PDF / 将当前阅读视图导出为PDFNotifications during export:
Rendering read-view PDF...
↓
Exported read-view PDF to /path/to/<NoteName>.readview.pdf.
If any local images could not be inlined, an extra message is appended: N local image(s) could not be embedded and were kept as links.
Pre-flight checks (export aborts with a notice if any fails):
this.exporting is false)MarkdownView in preview mode.markdown-preview-view container exists)Path: Settings → Community plugins → export-readview-pdf
| Value | Description |
|---|---|
zh-CN (default) |
Chinese UI, command name "将当前阅读视图导出为PDF" |
en |
English UI, command name "Export Read-view PDF" |
Switching updates the command palette entry and Ribbon tooltip live (internally re-registers via removeCommand + addCommand).
exports/pdf)/Users/you/Documents/PDF, D:\PDFs)~ / ~/ expansion to the home directoryPDF output directory does not exist: <path>| Option | Key | Default | Range |
|---|---|---|---|
| Landscape | landscape |
false |
true / false |
| Display headers and footers | displayHeaderFooter |
false |
true / false |
| Print background | printBackground |
true |
true / false |
| Scale | scale |
1.0 |
0.1 – 2.0, step 0.1 |
| Page size | pageSize |
A4 |
A3 / A4 / A5 / Legal / Letter / Tabloid |
| Top margin | margins.top |
0.4 in |
0.1 – 2.0 in |
| Bottom margin | margins.bottom |
0.4 in |
0.1 – 2.0 in |
| Left margin | margins.left |
0.4 in |
0.1 – 2.0 in |
| Right margin | margins.right |
0.4 in |
0.1 – 2.0 in |
| Prefer CSS page size | preferCSSPageSize |
false |
true / false |
Header/footer template (only when displayHeaderFooter is on):
┌────────────────────────────────────────────┐
│ 2026-07-17 Note Title│ ← Header: date on the left, title (ellipsis) on the right
├────────────────────────────────────────────┤
│ │
│ < Note content > │
│ │
├────────────────────────────────────────────┤
│ 3/12 │ ← Footer: current page / total pages
└────────────────────────────────────────────┘
When headers/footers are enabled, top/bottom margins are auto-bumped to at least 0.5 in so they don't overlap the body.
Effect of preferCSSPageSize:
false (default): clears Obsidian's "readable line width" cap (.is-readable-line-width / --file-line-width) so content fills the full paper widthtrue: uses the size defined in CSS @page; the "page size" dropdown is ignoredReset output path → clears back to vault rootReset PDF settings → all PDF options restored to the defaults in the table above<NoteBaseName>.readview.pdf ← first export
<NoteBaseName>.readview-1.pdf ← incremented on name collision
<NoteBaseName>.readview-2.pdf
...
<NoteBaseName>.readview-9999.pdf ← up to 9999; if still colliding, error out
Files are written with fs.open(path, "wx") (exclusive create), so concurrent exports never overwrite each other.
┌──────────────────────────────────────────────────────────────┐
│ User clicks Ribbon / triggers command │
└────────────────────────────┬─────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ main.js · ExportReadViewPdfPlugin.exportActiveReadingView() │
│ · Check: not concurrent / MarkdownView / preview / rendered │
└────────────────────────────┬─────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ src/export.js · ExportReadViewPdfJob.run() │
└────────────────────────────┬─────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
① Materialize view ② Build HTML snapshot ③ Print to PDF
ReadingViewMateri- HtmlSnapshotBuilder PdfPrinter
alizer.materialize() .build() .render()
│ │ │
▼ ▼ ▼
· Wait until every · Clone DOM + sync · Off-screen
renderer.section input/textarea/ BrowserWindow
is rendered=true details state · loadURL(blob:HTML)
· updateShownSections · Copy every · Wait for images
· Clone each section stylesheet + + fonts to settle
into an off-screen adoptedStyleSheets · webContents
host (-100000 px, · Inline CSS custom .printToPDF(opts)
pointer-events: properties (--*) · Verify %PDF- header
none, opacity:0) · Images → Data URL and return an
· MutationObserver <canvas> → <img> ArrayBuffer
until DOM is quiet Shadow DOM → <template>
for 800ms · Strip <script>/on*/
· Detect plugin javascript:
controller.isBusy() · Wrap in ancestor
(e.g. Interlinear) context
│
▼
┌──────────────────────────────────────────────────────────────┐
│ src/export.js · PdfOutputWriter.writePdf() │
│ · Resolve output dir (vault root / ~/ / absolute) │
│ · fs.open(..., "wx") exclusive write, increment on collision│
└────────────────────────────┬─────────────────────────────────┘
│
▼
✅ Notify user of output path
obsidian-export-readview-pdf/
├── manifest.json # Plugin metadata
├── main.js # esbuild output; the file loaded by Obsidian
├── package.json # Build and development scripts
│
├── src/
│ ├── main.js # Plugin entry, command/Ribbon registration
│ │ # · ExportReadViewPdfPlugin
│ │
│ ├── settings.js # Settings store + settings UI
│ # · LANGUAGE_OPTIONS (bilingual copy)
│ # · DEFAULT_PDF_OPTIONS
│ # · PdfOptions (normalize + validate)
│ # · ExportReadViewPdfSettings (load/save)
│ # · ExportReadViewPdfSettingTab (UI)
│
│ ├── export.js # Export orchestration + Electron print + file write
│ # · ExportReadViewPdfJob (chains the three phases)
│ # · PdfPrinter (BrowserWindow + printToPDF)
│ # · PdfOutputWriter (path resolution + exclusive write)
│
│ └── snapshot.js # DOM materialization + HTML snapshot + resource inlining
│ # · ReadingViewMaterializer (clones rendered sections)
│ # · HtmlSnapshotBuilder (assembles the full HTML)
│ # · DomSnapshotUtils (state sync / canvas / shadow)
│ # · StyleSnapshotUtils (stylesheet serialization)
│ # · ImageSnapshotUtils (image → Data URL)
│ # · HtmlSerializationUtils (escaping & attribute serialization)
│
├── LICENSE # MIT
└── README.md
BrowserWindow:sandbox: true, contextIsolation: true, nodeIntegration: falsedevTools: false, focusable: false, skipTaskbar: true, show: falseframe: false, never visible to the userDomSnapshotUtils.sanitizeSnapshot):<script> elementon* event attributes and nonce attributeshref/src values starting with javascript:contenteditable to falseURL.createObjectURL, then revokeObjectURL in the finally block%PDF-; otherwise the export errors outisDesktopOnly: true)view.previewMode.renderer.sections, sizerEl, previewEl, etc. If Obsidian changes these APIs, the plugin errors with This Obsidian version does not expose the complete Reading view rendererapp:, file:, blob:, capacitor: are inlined); cross-origin images keep their original URL and depend on the print window's network access@font-face pointing at remote URLs) are not inlined as binaries, but their URLs are carried through in the stylesheet text| Notice / Exception | Explanation / Handling |
|---|---|
A read-view export is already in progress. |
Another export is still running; wait for it to finish |
Open a note in Reading view before exporting. |
Current view is not a Markdown view, or not in preview mode |
The active Reading view has not finished rendering yet. |
Obsidian's internal renderer isn't ready; scroll a bit and retry |
Timed out while waiting for Obsidian to render every Reading view section |
Section rendering didn't complete within 20s; usually a huge note or a stuck background plugin |
Timed out while waiting for reading-view plugins to finish rendering |
The DOM kept mutating for 120s (e.g. long-running Interlinear or Dataview task) |
PDF output directory does not exist: <path> |
Output directory missing; create it first |
No permission to write PDF to: <path> |
No write permission (EACCES / EPERM) |
Electron BrowserWindow is unavailable; restart Obsidian and try again |
Electron remote module isn't loaded; restart Obsidian |
Electron returned invalid PDF data |
Print result isn't a PDF (print engine glitch); retry |
main.js and ignores other JavaScript files referenced by it. The source modules therefore live under src/ and use normal relative require calls during development, but npm run build must be run before distribution so esbuild bundles every module into the root main.js loaded by Obsidianfs / os / path) + Obsidian API + Electron APIexport-readview-pdf:; filter for it in Obsidian's DevTools (Cmd/Ctrl + Shift + I)| Constant | Value | Location |
|---|---|---|
RENDERER_READY_TIMEOUT_MS |
20000 | src/snapshot.js |
DOM_QUIET_MS |
800 | src/snapshot.js |
DOM_SETTLE_TIMEOUT_MS |
120000 | src/snapshot.js |
NOTICE_DURATION_MS |
4000 | src/main.js |
EXPORT_FILE_SUFFIX |
.readview.pdf |
src/export.js |
PDF_HEADER_FOOTER_MINIMUM_MARGIN_INCHES |
0.5 | src/export.js |
MIT © 2026 QuincyLeo (Quincy-Leo)
See LICENSE for details.