Convert to Markdown

by notenerd
5
4
3
2
1
Score: 50/100

Description

Category: File Management

The Convert to Markdown plugin converts a wide range of files in your vault into Markdown notes while preserving the source material's structure, tables, images and ordering. It handles PDFs, Office and OpenDocument files, ebooks, saved web pages, email, notebooks, subtitle tracks, spreadsheets and common image formats. Conversion stays local and deterministic, with no cloud service or API key involved. For scanned PDFs and image files it uses local OCR, reports confidence and notes which pages needed recognition. The plugin also resolves multi column PDFs, rebuilds paragraphs, extracts images beside the note and records skipped items instead of hiding them. Notes are written next to the original file and existing notes are left untouched.

Reviews

No reviews yet.

Stats

1
stars
710
downloads
0
forks
68
days
67
days
67
days
0
total PRs
0
open PRs
0
closed PRs
0
merged PRs
0
total issues
0
open issues
0
closed issues
29
commits

Latest Version

2 months ago

Changelog

Webex's cue number and speaker name can share one line — 2 "Name" (id) — rather than sitting on two separate lines. Only the two-line form was recognized, so files using the one-line form got no speaker names and no correct paragraph grouping: with every cue reading as an anonymous speaker, consecutive cues merged purely by gap timing regardless of who was actually talking.

**00:00 Alex Kim (They/Them):** Good morning, thanks everyone for joining
today's sync.

**00:04 Priya Rao:** Happy to be here. Should we start with the roadmap
review? I can walk through where each workstream stands right now.

The cue number is now read as an optional prefix on the identifier line either way, and a speaker's own parentheses (Webex appends a participant's pronouns to their display name) no longer get misread as the closing parenthesis around the participant id.

Installing

Copy main.js and manifest.json into <vault>/.obsidian/plugins/convert-to-markdown/, then enable Convert to Markdown under Community plugins. Existing installs update from within Obsidian.

Desktop only.

README file from

Github

Convert to Markdown

Converts PDFs, Office and OpenDocument files, ebooks, saved web pages, email, notebooks, subtitle tracks, spreadsheets, plain text and images in your vault into Markdown notes, images and all.

No LLM, no API key, no cloud service, no external binary. Nothing you convert is uploaded anywhere: every byte is processed on your machine.

Documents are parsed directly from their own structure, which makes them fully deterministic and fully offline: the same file always produces byte-identical Markdown.

Tables come out as tables, PDFs and scanned pages included. And an invoice, a receipt or a bank statement can be converted as one: its vendor, dates, amounts or balances become note properties you can sort and sum in Bases or Dataview, the figures are checked against each other, and anything it couldn't find or had to guess is said in the note. See Invoices and receipts and Bank and card statements.

Pixels are the one exception. An image file has no structure to read, and neither does a page of a scanned PDF, so reading either means OCR. That's statistical rather than structural, and occasionally wrong, which is why it reports its confidence and says which pages it was used on. Tesseract is a local neural recogniser, not a language model and not a service. It also downloads its engine the first time you use it. Details under OCR.

A PowerPoint being converted to Markdown in Obsidian: the deck is picked from a
list, and the resulting note has the slide titles as headings, the bullets as
lists, the slide table as a Markdown table, and the deck's images embedded
alongside them.

Installing

Settings → Community plugins → Browse → search Convert to Markdown → Install, then Enable.

Nothing else. No runtime to install, no binary to put on PATH, no account. Desktop only, because it uses Node's zlib, which Obsidian mobile doesn't have.

The first time you convert an image or a scanned PDF, there's a one-off engine download (see OCR); the notice shows its progress. Everything else works immediately and offline.

Supported: .pdf · .docx .pptx .xlsx (and .docm .pptm .xlsm) · .odt .ods .odp · .epub · .html .htm .mhtml .mht · .eml · .ipynb · .vtt .srt · .csv .tsv · .txt · .png .jpg .jpeg .webp .gif .bmp .tif .tiff

Pairs well with Doc Preview, also by this author: it renders .pptx/.docx/.xlsx files in an Obsidian tab exactly as formatted, via a local LibreOffice install, so you can open the original beside the converted note, which helps with checking a conversion, or with deciding whether one's even worth doing.

Usage

  • Right-click a supported file in the file explorer → Convert to Markdown
  • Or run Convert to Markdown: Convert a file from the command palette

For a PDF or an image of an invoice, receipt or statement, the same menu has Convert to Markdown as invoice / receipt and Convert to Markdown as statement, and the command palette has Convert a file as … for each. The note then leads with that document's facts as properties; see Invoices and receipts.

The note is written next to the original (configurable), never overwriting an existing note. Anything the converter dropped (images it couldn't render, sheets you asked it to leave out, a page's navigation, an email attachment) is named in a collapsed callout at the end. Not counted: named, because "3 items skipped" tells you nothing you can act on.

The frontmatter's source field links back to the original as a bare-filename wikilink ([[report.docx]]) rather than its full path, for the same reason the images are: Obsidian resolves it by searching the vault each time, so the source file can be moved anywhere afterward without the link breaking. That only holds while the filename is unique. If another file elsewhere in the vault happens to share it, the full path is used instead, since a bare link would otherwise point at whichever one Obsidian happened to pick.

Coverage goes in the frontmatter, where it's read before the content rather than after two thousand lines of it:

sheets_converted: 23/23     # .xlsx, .ods
pages_converted: 40/40      # .pdf
form_fields_filled: 12/15   # .pdf, when it's a fillable form
chapters_converted: 31/31   # .epub
cues_converted: 812/812     # .vtt, .srt
cells_converted: 41/41      # .ipynb
lines_converted: 120/120    # .txt

For a PDF whose layout matters as much as its words, such as an invoice, a form or a statement, Embed the original PDF puts the PDF itself in the note, above or below the converted text, where Obsidian shows it as a scrollable viewer. The note embeds the PDF where it already is rather than copying it, by the same moveable bare-filename link as source. It's off by default, and PDF-only: Obsidian can't display Office or e-book files inline, so for those the source link is all an embed would give you anyway.

Settings

Setting What it does
Save converted notes / Output folder Next to the original (default), or in one folder
Extract images / Save images / Image folder Whether images are written out and embedded, and where. See Images
Embed the original PDF Show the PDF itself above or below the converted text
Date order How all-number dates like 06/10/2026 are read when converting as an invoice or statement
Convert hidden sheets Whether hidden sheets are converted. See Spreadsheets
OCR engine folder Supply the OCR engine from the vault instead of downloading it. See OCR
Add frontmatter / Add conversion notes The properties at the top, and the callout of what was dropped at the end
Open after converting Open the new note
Invoice / receipt and Statement → Template note, Extra labels Your own layout for that type's notes, and your own labels for its fields. See Your own template

Tables

Tables are rebuilt as Markdown tables wherever they turn up: Word, PowerPoint, spreadsheets and web pages have them as structure, and PDFs, scanned pages and photos have them as text that lines up, which is how they are found there:

| Date | Description | Withdrawals | Deposits | Balance |
| --- | --- | ---: | ---: | ---: |
| Sep 2 | Payroll deposit ACME CORP |  | 2,450.00 | 4,590.18 |
| Sep 3 | Rent e-transfer | 1,800.00 |  | 2,790.18 |

Empty cells stay empty in the right column, number columns are right-aligned, a description that wraps stays in its cell, and a header printed over several lines becomes one header. Text that only looks tabular (two columns of prose, monospaced text, a label beside its value) is left as text. A scan or photo that's a little crooked is straightened first. How it works, and where it stops, is under OCR and Known limits.

Invoices and receipts

Right-click a PDF or an image and choose Convert to Markdown as invoice / receipt (or run Convert a file as invoice / receipt) and the note's properties carry what you'd file it by:

type: invoice
vendor: Northwind Studio
invoice_number: NW-2026-0042
invoice_date: 2026-10-06
due_date: 2026-11-05
subtotal: 1310
tax: 170.3
total: 1469
currency: CAD
tags: [finance, invoices]

Amounts are plain numbers and dates are ISO dates, so Bases and Dataview can sort, filter and sum them. Below the properties come the original, embedded, and the full conversion, tables included. For example, with Dataview:

```dataview
TABLE vendor, total, due_date
FROM #invoices
WHERE due_date >= date(today)
SORT due_date
```

If your invoices use day-first dates, as most of the world outside the US does, check Date order in settings. Left on From your system's language, it follows the language Obsidian runs in, and a Mac set to US English reads 06/10/2026 as June 10. Either way, a date that could be read both ways is listed in ambiguous_fields.

Fields are found by their labels and where the value sits: Invoice #: 1042, Amount due with the figure tabbed out beside it, an address under Bill to. Labels printed in two languages (Invoice date / Date de facturation) match on either, and a more specific label wins over a vaguer one, so Total payable beats a table's bare Total. It works the same on scans and photos, read by OCR. It isn't AI and doesn't guess at meaning, so what it couldn't do is said in the note rather than papered over:

  • missing_fields names what an invoice should have and this one didn't show, such as a receipt with no invoice number, an invoice with no due date.
  • guessed_fields names what came from a fallback rather than a label: the vendor taken as the most prominent line at the top, a receipt's date taken as the first date printed.
  • ocr_fields names what was read by OCR, and so can be misread.
  • ambiguous_fields names all-number dates that read either way round (06/10/2026); Date order in settings decides which.

The figures are also checked against each other: the line items should add up to the subtotal, and the subtotal plus tax and shipping, less any discount, to the total. When they don't, because of a misread digit or an invoice that's simply wrong, the conversion notes say so with both numbers.

Your own template

Under Invoice / receipt (or Statement) in settings, Template note picks a note to lay that type's notes out by, and Create from built-in writes the built-in template into your vault as Invoice template.md or Statement template.md and opens it to start from. A template is an ordinary note with placeholders:

---
from: {{vendor}}
amount: {{total}}
due: {{due_date:DD MMM YYYY}}
status: unpaid
---
# {{vendor}} invoice {{invoice_number}}

{{original}}

{{content}}

Every field is a placeholder:

  • Invoice / receipt: {{vendor}}, {{invoice_number}}, {{invoice_date}}, {{due_date}}, {{po_number}}, {{bill_to}}, {{subtotal}}, {{tax}}, {{shipping}}, {{discount}}, {{total}}, {{currency}}, and {{line_items}} for the items table alone
  • Statement: {{institution}}, {{account_last4}}, {{period_start}}, {{period_end}}, {{opening_balance}}, {{closing_balance}}, {{currency}}, and {{transactions}} for the transactions table alone
  • Both: {{original}} for the embedded file, {{content}} for the whole conversion, and {{title}}, {{date}} and {{time}} as in Obsidian's own templates Values are written into the properties safely, whatever's in them: an invoice number like 0042 stays text, and a vendor with a colon in its name doesn't break the YAML. A field that wasn't found leaves its property empty rather than missing, so Bases still sees it.

The source and coverage properties (source, converted, missing_fields and the rest) are always added, in the template's place for them if it has one. Templater code is left untouched for you to run afterwards. A placeholder with a typo stays as typed and is named in the conversion notes, and a template note that's been deleted, or whose properties block never closes, gives way to the built-in template, with a note saying so.

Extra labels to read fields by, below the template, takes your own labels per field. If your landlord's invoices say Total to remit, add it to Total; if your bank calls the closing balance Balance this statement, add that to Closing balance.

Bank and card statements

Convert to Markdown as statement does the same for a bank or credit card statement:

type: statement
institution: Lakeshore Credit Union
account_last4: "8812"
period_start: 2026-10-01
period_end: 2026-10-31
opening_balance: 5000
closing_balance: 1683.43
currency: "$"
tags: [finance, statements]

Only the account number's last four digits become a property. Properties are what gets synced, searched and listed across a vault, and a full account number has no business there; the converted text below them is the statement as printed, so the full number is still in the note itself.

A statement that runs over several pages repeats its table header on each, and those pages' transactions are read as one table. Every row is checked against the running balance, and the opening balance plus the transactions against the closing balance. Which way round an amount counts differs (a credit is money in on a bank account and a payment on a credit card), so both readings are tried and the statement's own balances decide. A row whose balance doesn't follow is named by its date and description, and one misprinted balance is reported once, not as every row after it.

Statements take a template note and extra labels in settings, the same way invoices do. See Your own template.

Compared to markitdown

The Obsidian plugins in this space wrap Microsoft's markitdown, which is one abstraction over every format: it makes one-size-fits-all decisions about what structure means, and those decisions are where the mess comes from.

Nothing to install. A markitdown-based plugin needs Python on the machine and pip install markitdown, and fails with "Python is not installed or not found at the configured path" until it has both. This one is plain JavaScript inside Obsidian, which also means it works on a locked-down machine where installing Python is the step that's blocked.

Structure survives. Headings keep their levels, lists stay lists, tables stay tables, and images are written into the vault and embedded at the point they appeared. Running the same PDF through both, markitdown returns the text hard-wrapped exactly where the PDF's column broke, hyphens included (medica- / tions, occlu- / sion), because a PDF line break is a typesetting artifact it preserves rather than undoes. This one rejoins the paragraph and repairs the hyphenation.

Multi-column PDFs read in order. Columns are detected per page and each is read top to bottom before the next, so a two-column paper doesn't come out interleaved. Full-width titles and headings are recognised as spanning the columns and stay where they belong, and running headers are stripped before the columns are worked out. Otherwise a footer spread along the page bottom lands in the middle of the text.

Per format, what's read and what it buys:

Format What it reads What that buys
.docx word/document.xml paragraphs and numbering.xml Real heading levels from Word's own styles; ordered vs. unordered lists; tables; hyperlinks; bold/italic
.pptx Slide parts in p:sldIdLst order One section per slide in presentation order, title placeholders as headings, speaker notes, slide tables
.xlsx Every worksheet, sharedStrings.xml, styles.xml, pivot caches Dates instead of serial numbers, 27.38 instead of 27.383982300884924, hidden sheets converted rather than dropped, coverage in the frontmatter
.pdf The text layer, via pdf.js; OCR for pages that have none; a fillable form's fields Columns read in order, tables rebuilt from how their cells line up, headings from font size, paragraphs rejoined across line breaks, de-hyphenation, running headers/footers dropped, scanned pages read rather than refused. A filled-in form's entries as a table (they aren't in the text layer at all), with the fields left empty named
.odt .ods .odp content.xml, styles.xml, meta.xml Heading levels stated outright rather than inferred; the displayed value of a spreadsheet cell; footnotes collected at the end; speaker notes
.epub container.xml → the OPF spine and the contents document Chapters in reading order rather than alphabetical, chapter titles from the contents page, cover art, book metadata in the frontmatter
.html .htm The page's own structural markers The article rather than the whole page: navigation, cookie banners and site-map footers named and left out
.mhtml .mht The MIME archive Chrome and Word save pages as The page and its images, since a single-file save has them both
.eml Headers, the multipart body, transport encodings Correspondents and subject in the frontmatter, quoting preserved, inline images placed, attachments named
.ipynb Cells, and the richest usable form of each output Prose as prose, code fenced with its language, plots embedded, DataFrames as real tables rather than padded digits
.vtt .srt Cue timings, <v> voices, >> and dialogue dashes A readable transcript instead of 800 numbered stanzas. See Transcripts
.csv .tsv RFC 4180, with the delimiter worked out from the file Quoted fields containing commas and line breaks survive; a semicolon-separated European export isn't read as one column
.txt Lines, and the encoding: from a byte-order mark, else UTF-8, else windows-1252 The note reads exactly as the file did: a stray # or a row of === doesn't become a heading, indentation doesn't become a code block, typed - lists and > quotes stay lists and quotes. An assumed encoding is named in the frontmatter
.png .jpg .webp .gif .bmp .tiff Local OCR (Tesseract) Text off a screenshot or photo, laid out as paragraphs rather than one line per pixel row

Macro-enabled variants (.docm, .pptm, .xlsm) are the same parts plus a VBA blob, and convert identically.

Three of these share one reader, and share it because they are the same format in different envelopes rather than merely similar: an epub chapter, a saved web page and an email's HTML body are all HTML. .mhtml and .eml likewise share a MIME parser: a saved web page in MHTML is an email whose attachments happen to be the page's images.

Transcripts

A caption file's structure is timing, not prose. It is cut into two-second cues sized for the bottom of a screen, and every cue is hard-wrapped to the display width. Written out cue by cue you get hundreds of numbered stanzas nobody can read, where the timings, the least interesting thing in the file, are the only structure the layout expresses.

So the cues are put back together: lines rejoined, consecutive cues from the same speaker merged into paragraphs, and a timestamp kept at the head of each paragraph as an anchor back into the recording.

**00:01 Roger Bingham:** We are in New York City. We're actually at the
Lucern Hotel, just down the street from the American Museum of Natural History.

**00:15 Neil deGrasse Tyson:** Didn't we talk about enough in that conversation?

Speakers are read from every convention a real transcript turns up with: WebVTT's <v Name>, broadcast captioning's >> and >> NAME:, the subtitling convention of a dash at the start of each speaker's line, Webex's own export, which names the speaker on the cue's own identifier line (2 "Alex Kim (They/Them)" (100000001)): quoted name and participant id, ahead of the timing line rather than inside the cue text, and the bare Name: text several other transcription tools write straight into the cue itself. Webex's identifier line is read whether or not the cue number sits in front of the quoted name on that same line (real exports do both), and the name itself may hold its own parentheses (Webex appends a participant's pronouns to their display name), so the trailing (id) is matched at the end of the line rather than as the first parenthesised group found. The dash and the chevrons have to be understood anyway: > and - at the start of a line are a blockquote and a list item in Markdown, so leaving them in would corrupt the note even if you didn't want the speaker names.

The bare form is the one case with no markup at all to go on. John Doe: thanks for joining and Note: this call is recorded are typed identically, and only one of them is a speaker. So it's read conservatively: the whole file is scanned first for the shape, and a name is only trusted once it's shown up more than once. A transcript's actual speakers keep coming back; a stray aside essentially never repeats itself word for word.

Auto-generated captions get one extra repair. YouTube's scroll rather than cut: each cue restates the previous cue's last line and adds one new one, so the same words arrive two or three times and naive concatenation triples the transcript. Repeated text is matched at word boundaries and written once.

Cue count and duration go in the frontmatter (cues_converted: 812/812), and a Kind: chapters track becomes headings rather than paragraphs.

Images

Images are pulled out of the source file, written to <note name> attachments/ beside the note, and embedded where they actually sat: a figure inside a Word table cell comes out inside that table cell. Identical images are written once no matter how many times they're used, so a logo on forty slides is one file.

The folder is a setting. Image folder is a path relative to the converted note, where {{note}} stands for the note's name: XAttachment is a folder beside the note, ../assets is one level up, and a leading slash (/Assets/{{note}}) starts from the vault root. A path that would leave the vault stops the conversion with an error rather than writing somewhere else. Or set Save images to follow Obsidian's own Default location for new attachments, and converted images land wherever pasted ones do. Several notes can share one folder: an image that's already there under the same hash-stamped name is the same image, so it's linked rather than written again.

Converting an image file (a PNG screenshot, a scanned JPEG) moves that file into the image folder rather than copying it there, and the note embeds it: one file, where the setting says images go. The move goes through Obsidian, so anything already linking to the image follows it. If a different file already has that name in the folder, the image stays put and the conversion notes say so. A conversion that fails partway takes back everything it wrote: the empty note, any images, any folders it created, and the move.

Each one is named with a short hash of its own bytes (image-1-a3f9c2b7.png), and embedded by that bare filename rather than its full path. That's what lets the attachments folder be moved anywhere else in the vault, by Obsidian's file explorer, a sync tool, or dragged around in Finder, without breaking the embed: Obsidian finds a bare-filename embed by searching the vault each time it renders the note, rather than trusting a path stored at conversion time. The hash is what makes that safe to do at all: a plain image-1.png would collide the moment two different notes had a first image, and Obsidian would have no way to know which one a bare link meant.

Word, PowerPoint and Excel store images in their original encoding, so those come out untouched (.jpeg stays .jpeg). A PDF doesn't store files at all; it stores decoded pixels, so images from a PDF are re-encoded as PNG. Images under 64px on a side are treated as rules, bullets and icons and skipped.

Turn the whole thing off with Extract images in settings for text-only notes.

Spreadsheets

Every sheet is converted, hidden ones included. Hiding a sheet is a presentation choice, not a statement about the data. The hidden sheets in a workbook are as often the raw table a visible pivot summarises as they are scratch space, so leaving them out silently guts the note. The frontmatter carries the count (sheets_converted: 23/23), so how much of the workbook made it across is the first thing you see rather than a footnote after two thousand lines.

.ods behaves identically, and gets there with much less work: LibreOffice stores each cell's displayed text next to its raw value, so the number-format machinery .xlsx needs (parsing format codes to recover 27.38 from 27.383982300884924, scaling percentages) is simply unnecessary. The answer is already written down.

Convert hidden sheets in settings turns that off. When it's off, the skipped sheets are listed by name in the conversion notes, as in Skipped (hidden): raw data (monthly), Region, Agebracket, because a count alone tells you nothing you can act on. A hidden sheet that a visible sheet's formulas, a pivot cache or a chart series reads from is converted regardless of the setting: that dependency is a far stronger signal than the hidden flag, and it's reported too.

OCR

An image file has no structure to read, so converting one runs Tesseract, a classical OCR engine (line segmentation plus an LSTM character recogniser) compiled to WASM. It is not an LLM, takes no API key, and the image never leaves your machine.

Scanned PDFs go the same way, page by page and only where they have to. A page with a text layer is parsed exactly, as always; a page that is nothing but an image is handed to the recogniser instead. A document can contain both, and the note says which pages were which:

pages_converted: 40/40
pages_read_by_ocr: 12

[!info]- Conversion notes

  • 12 pages had no text layer and were read by OCR instead (pages 3, 4, 5, …). That part of the note is a recognition rather than an extraction and can be wrong. Lowest confidence was 71%.

The page image itself isn't also embedded. For a 200-page scan that would be 200 full-page PNGs burying the text they duplicate. Expect roughly a second or two per scanned page; the notice counts them off as it goes.

The engine and its English model are not shipped in the plugin. An Obsidian release can only contain main.js, manifest.json and styles.css, so they download from jsdelivr the first time you convert an image:

What Size Cached in
tesseract-core-simd-lstm.wasm.js, the recogniser ~3.7 MB Electron's HTTP cache
eng.traineddata, the English model 5.0 MB IndexedDB, by tesseract.js

That traffic is one-way: it fetches the engine, it never sends the image. After the first run OCR works with the network off, and nothing else in the plugin touches the network at all.

The download happens inside the first conversion, so that one takes noticeably longer than the rest. The notice reports what it's doing while it waits (loading language traineddata (61%)) rather than sitting on "Converting…".

Regions Tesseract is unsure of (lettering picked out of a photograph, logo marks, JPEG artefacts) are dropped rather than written into the note as gibberish, and counted in the conversion notes. Large display type reversed out of a coloured background is the common thing OCR misses; check the note against the image when the confidence warning appears.

Tables in a scan or a photo come out as tables, found the same way as in a PDF: from where the words sit. Tesseract's own layout analysis reads a table a column at a time (every date, then every description, then every amount) and can cut a narrow column of figures off into a block of its own and misread it, so a page that looks like it holds a table is read a second time as one block of lines, which keeps each row whole. A page tilted on the scanner or in a photo is straightened first, so rows don't drift into each other. Because a misread digit is a wrong amount rather than a typo, a table read with less than 80% confidence is flagged, and anything OCR read in a table's area that didn't make it into the table is named.

Two systematic artefacts are repaired afterwards, both of them the same kind of thing as the PDF extractor's de-hyphenation: undoing a known, predictable distortion rather than guessing at content.

Paragraphs are rebuilt from where the lines sit, not from Tesseract's own paragraph division. That division assumes scanned prose, and a screenshot isn't scanned prose: on a three-line paragraph of a web page it put the last line in a separate block, and on a chat transcript it split one message across two while merging a timestamp and an unrelated name into one. The geometry settles it: a line whose following word wouldn't have fitted was wrapped, so what comes next at the same left margin continues it; a line that stopped with room to spare ended its paragraph. Lines further apart than about one line's leading are never joined, which is what keeps separate chat bubbles separate.

A lone | is read back as I. In a sans-serif face a capital I has no serifs and no crossbar. It is a plain vertical stroke, pixel-identical to a pipe, so "I put together our plans" comes back as "| put together our plans". The repair is narrow on purpose: only a bar standing alone as a word with a lowercase word after it, which is the shape of the English pronoun, and never on a line holding two or more free-standing bars, where they are far more likely to be a table rule.

What isn't repaired is an image recognised as a letter. An avatar or an icon next to a line of text can come through as a stray Q or O, and there is no way to tell that from a real character without understanding the picture.

Locked-down machines

If that CDN is blocked, as is common on corporate networks, put the two files in a vault folder and name it in OCR engine folder in settings. OCR then reads them straight from disk and makes no network request at all, on the first run or any other.

Download them once somewhere with access:

  • https://cdn.jsdelivr.net/npm/tesseract.js-core@7/tesseract-core-simd-lstm.wasm.js
  • https://cdn.jsdelivr.net/npm/@tesseract.js-data/eng/4.0.0_best_int/eng.traineddata.gz

Both go in the same folder, keeping their filenames (the .gz may be unzipped, either works). If the folder is set but a file is missing, the conversion says which one.

Known limits

  • Column detection is geometric, not semantic. It reads the horizontal distribution of ink per page, so a layout no gutter runs through (text wrapped around a figure, a magazine-style collage) can still come out in the wrong order.
  • PDF tables are found by alignment, and only when it's convincing. A PDF has no tables, only text that lines up, so a table is recognised by strips of the page that none of its rows crosses. Prose never has those, but text set in narrow columns, or in a monospaced font where word gaps line up by chance, can. So a block whose columns read as running text is left as text. Two columns become a table only when the right one is numbers (a receipt, an invoice's totals); two columns of words are as often labels beside values. A missed table reads as before, a run of words; cells merged across columns aren't handled yet.
  • On a scan or photo, columns closer than a stretched word space read as one. OCR reports words rather than runs of text, so words are joined back into runs before tables are looked for, and two amount columns set tight together end up in one cell. A photo taken at an angle or of paper that isn't flat has no straight rows to find; tables there aren't looked for, and the note says so. A page that looks like it holds a table takes a second OCR pass, roughly doubling its time.
  • A scanned page is read through its page image, which is what a scan actually is. A page with no text layer whose lettering is drawn as vector outlines instead has no image to hand the recogniser, and is reported as producing nothing.
  • OCR is English only. The one language model that gets downloaded is eng; a scan in another language will come out as approximately-English nonsense, and the confidence warning is the signal.
  • Windows metafiles (EMF/WMF) can't be embedded because nothing in Obsidian renders them. They're skipped and counted.
  • Excel formulas export as their last-calculated value: the value Excel itself stored in the file.
  • Charts and embedded objects in decks are skipped and counted; a chart is a data structure, not a picture, so there's nothing to embed.
  • Letter-spaced PDF headings can pick up stray spaces (THI S BOOK); the gap between tracked glyphs is genuinely wider than an unspaced word break.
  • A plain .html file's images stay where they are. They sit beside the file on disk rather than inside it, and the converter only ever sees the file you picked. Remote images are kept as links; a single-file .mhtml save has the bytes and embeds them.
  • Main-content detection on a web page is a heuristic. It uses the page's own markers (main, article, ARIA roles, the usual container ids), and what it set aside is named in the conversion notes, so a page that hides its article in an unlabelled div is a visible omission rather than a mysteriously short note.
  • HTML tables can't express merged cells. A colspan or rowspan is laid out on Markdown's fixed grid with the covered cells left empty, which keeps every other column in the right place.
  • Legacy binary Office files (.doc, .xls, .ppt) and iWork files (.pages, .key, .numbers) aren't supported. Both are undocumented binary container formats rather than zipped XML, and getting them subtly wrong is easier than getting them right. Re-save as the modern equivalent.

Development

npm install
npm run dev     # watch build into main.js
npm run build   # type-check + production bundle

tools/convert.mjs runs the extractors outside Obsidian, so output can be checked against real files without reloading a vault:

OUT_DIR=/tmp/out node tools/convert.mjs ~/Downloads/deck.pptx ~/Downloads/report.pdf

OUT_DIR must already exist. The harness shims the DOM globals Obsidian gets from Electron and otherwise runs the real extractor code. OCR_PROGRESS=1 prints the OCR stage reporting that the plugin puts in its notice; SKIP_HIDDEN_SHEETS=1 is the harness's stand-in for turning Convert hidden sheets off.

To convert as a document type, set TYPE to its id (invoice, statement). TEMPLATE points at a template note to use, LABELS takes extra labels as JSON by field key, and DATE_ORDER is dmy (the default) or mdy:

TYPE=invoice LABELS='{"total":["total to remit"]}' OUT_DIR=/tmp/out node tools/convert.mjs bill.pdf

The source properties are fixed (converted: 2026-01-01 00:00) so runs can be compared byte for byte. How document types work, and what's still open, is in docs/plans/types-and-templates.md.

Obsidian's DOMParser handles XML and HTML; Node has neither, and no one package does both well. The harness uses @xmldom/xmldom for OOXML, OpenDocument and XHTML parts, and linkedom for text/html, which is the only one that parses tag soup (unclosed <p>, bare <img>) the way a browser does. Both are dev dependencies and neither is bundled.

Note on dependencies

The zip reader is hand-rolled (src/zip.ts) rather than JSZip. JSZip's transitive dependencies create <script> elements as a legacy task-scheduling trick, which gets plugins flagged by Obsidian's community-plugin review as "code obfuscation", a false positive that is simpler to avoid than to argue. PNG encoding for PDF images is hand-rolled too (src/png.ts), on top of the zlib the zip reader already uses, so it works identically in Obsidian and in the Node harness.

The runtime dependencies are pdfjs-dist and tesseract.js. Both normally load their worker script from a separate file or a CDN at runtime; a plugin release can only ship main.js, manifest.json and styles.css, so both workers are inlined at build time and handed to the library as a Blob URL. Tesseract's WASM engine and language data still come from a CDN on first use; they're too large to inline, and the language data has to be fetched somehow.

License

MIT

Third-party licenses

The released main.js is a bundle, so it carries these with it:

Bundled License
pdfjs-dist: PDF text and image extraction Apache-2.0
tesseract.js: OCR API and worker Apache-2.0
idb-keyval: via tesseract.js, caches the OCR model Apache-2.0
wasm-feature-detect: via tesseract.js, picks the SIMD build Apache-2.0

Fetched on the first image conversion rather than redistributed here:

Downloaded at runtime License
tesseract.js-core: the WASM recogniser Apache-2.0
tessdata eng.traineddata: the English model Apache-2.0

Apache-2.0 is compatible with this plugin's MIT license, and none of these ship a NOTICE file that would require further attribution. Nothing else is bundled: the zip reader and the PNG encoder are hand-rolled (src/zip.ts, src/png.ts) precisely so there is nothing more to carry.

Similar Plugins

info
• Similar plugins are suggested based on the common tags between the plugins.
Extract PDF Annotations
5 years ago by Franz Achermann
This is a plugin for https://obsidian.md. It extracts Annotations from PDF Files.
Obsidian Enhancing Export
4 years ago by YISH
This is an enhancing export plugin base on Pandoc for Obsidian (https://obsidian.md/ ). It's allow you to export to formats like Markdown、Markdown (Hugo https://gohugo.io/ )、Html、docx、Latex etc.
Text Extractor
4 years ago by Simon Cambier
A (companion) plugin to facilitate the extraction of text from images (OCR) and PDFs.
Webpage HTML Export
4 years ago by Nathan George
Export html from single files, canvas pages, or whole vaults. Direct access to the exported HTML files allows you to publish your digital garden anywhere. Focuses on flexibility, features, and style parity.
Awesome Reader
4 years ago by AwesomeDog
Make Obsidian a proper Reader.
Image OCR
3 years ago by kaffarell
Runs ocr on pasted images and posts result in details box. This allows to search in images.
Slide Note
3 years ago by Jinyan Xu
MathLive
3 years ago by Dan Zilberman
The must-have plugin for math in Obsidian
Image2LaTEX
3 years ago by Hugo Persson
This is a plugin for obsidian that will read your latest copied image from clipboard and generate math latex from it
Image to text OCR
3 years ago by Dario Baumberger
Convert a image in your note to text.
PDF break page
3 years ago by CG
Plugin for obsidian that adding shortcuts to create breakpages for pdf exports.
SwiftLaTeX Render
3 years ago by gboyd068
Marker PDF to MD
2 years ago by L3N0X
Make use of different AI models to convert your pdfs into markdown with perfect ocr, latex formulas, tables, images and more! Supports Mistral AI OCR (free) and self hosted variants!
PDF Highlights
6 years ago by Alexis Rondeau
Extract highlights, underlines and annotations from your PDFs into Obsidian
Better PDF
6 years ago by MSzturc
Goal of this Plugin in to implement a native PDF handling workflow in Obsidian
Taskbone
5 years ago by Dominik Schlund
Obsidian OCR plugin - extract text from images
Pandoc
5 years ago by Oliver Balfour
Pandoc document export plugin for Obsidian (https://obsidian.md)
Annotator
5 years ago by Elias Sundqvist
A plugin for reading and annotating PDFs and EPUBs in obsidian.
Markmind
5 years ago by Mark
A mind map, outline for obsidian,It support mobile and desktop
LLM Summary
2 years ago by QSun
wip
downloadPDF
2 years ago by Frieda
PDF2Image
2 years ago by RasmusAChr
kkh
2 years ago by 奈幾乃(uakms)
kkh の Obsidian 用プラグイン
Paperless
2 years ago by Talal Abou Haiba
BibDesk Integration
2 years ago by Andrea Alberti
Integration of Obsidian with bibtex files
PDF Paste
2 years ago by Cormac
Vision Recall
2 years ago by Travis Van Nimwegen
Transform screenshots into searchable Obsidian notes using AI vision and text analysis
Student Repo
2 years ago by Feirong.zfr
学生知识库助手(Student Repository Helper)是一个面向学生或学生家长的Obsidian 插件,这款插件旨在解决学生在学习阶段面临的资料管理难题,将学习过程中产生的各类重要资料,如试卷、笔记、关键文档、绘画手工作品等,进行系统性的数字化整合与管理,并利用 AI 助手定期进行学习分析总结。随着时间的推移,它将助力你逐步搭建起一座专属你自己的知识宝库,这座宝库将伴随你一生,成为你知识成长与积累的见证。
PDF Folder to Markdowns
2 years ago by CrisHood
Convert a folder of PDFs into a folder of Markdown files with embedded PDFs. This plugin is useful for users who want to migrate their PDF notes from different apps (e.g., Boox) or organize their reference materials inside Obsidian.
Images to Notes
a year ago by Rodolfo Terriquez
Turn photos of your handwritten notes into markdown
Handwriting OCR
a year ago by ikmolbo
Transform handwritten documents and scanned images into editable text with Handwriting OCR's AI-powered handwriting to text conversion.
AI Image OCR
a year ago by Rootiest
Obsidian plugin for AI-powered text extraction from images
OCR Extractor
10 months ago by Johnathan Ritzi
Obsidian plugin to extract text from PDFs, documents, images, etc. and store it as Markdown in notes
PDF++
5 months ago by ryotaushio
PDF++: the most Obsidian-native PDF annotation & viewing tool ever. Comes with optional Vim keybindings.
PDF++ Exporter
3 months ago by jldiaz