Developer Tools

Converting PDFs to Markdown Locally for LLM Context and RAG Pipelines

20 min read
Local PDF conversion for retrieval pipelines

Some documents never get to be documents. A signed contract has to become context for a model, a scanned invoice has to become searchable, and an internal report has to reach an assistant that nobody is allowed to paste it into. In each case the next step is a converter, and in each case most converters run on someone else’s server, so the file crosses the wire before it becomes text.

Converting a PDF to Markdown in your own browser is a solved problem for some pages and an unsolved one for others, and that difference matters more than the tool you pick. After reading this, you will know which pages of a PDF convert cleanly, which ones deserve a second look, and which ones belong in a Python pipeline instead.

Why Document Conversion Breaks Before Your Model Ever Sees It

Retrieval quality is decided at ingestion, not at query time. A chunker splits your document on headings, and it splits it well only if the headings survived conversion; when a table arrives as a stream of cell values, no chunking strategy can rescue it, because the retriever will return that stream every time somebody asks about the row it came from. Every downstream complaint about a bad answer traces back to this stage, which is why the boring part of PDF conversion deserves more attention than the model you plan to put on top of it.

A PDF page is one of two things. A born-digital page carries a text layer: every word sits in the file with a font, a size, and a position, which is why you can select it, copy it, and search it.1 A scanned page carries no such layer; it is a picture of words, and copying from it returns nothing at all, because there is no text behind the pixels to copy.2

Uploading a file to a hosted converter adds a cost you cannot see from the outside. Contracts, medical letters, internal reports and private wiki notes are exactly the documents people should not paste into a web form, and the upload is invisible: the page loads, the file goes up, the text comes back. The tool on this page keeps the file in the tab and tells you how it read each page, using a simple threshold. A page needs at least twenty characters of real text to count as a text page. A fourteen-page contract with twelve text pages reports exactly that split.

Scanned pages do not arrive as PDFs. They arrive as paper, and somebody has to put a scanner between the two, which is why the feeder size of a scanner decides how quickly you can build a corpus. The seven machines below sit in the same price band and differ mostly in how much paper they swallow before you touch them again.

Born-Digital Pages: Rebuilding Structure From the Text Layer

The text layer stores words and coordinates, not structure. It never says which line is a heading, so the converter rebuilds that structure from geometry, reading text items through PDF.js, the same JavaScript engine Firefox uses to display PDFs.3 Words that share a baseline become one line, and CapyToolkit’s PDF to Markdown converter that reads the text layer inside your browser measures the font size and position of every item it pulls out, because those two numbers carry the only structural hints the file offers.

Size does most of the talking. The most common font size on a page is treated as body text, and lines that are clearly larger than that become headings, mapped to #, ##, and ### in order of size. Paragraph breaks come from spacing rather than from any marker in the file: a gap well above the page’s typical line gap ends a paragraph and starts the next one, and a word broken with a hyphen at the end of a line is rejoined with the rest of the word on the following line.

Lists and code blocks fall out of the same evidence. A line that starts with a bullet glyph, or with a number followed by a period, becomes a list item, and indentation sets the nesting depth. A run of text in a monospace font becomes a fenced code block, with the relative indentation of the lines preserved inside the fence. All of this runs in milliseconds per page. Every character comes out of the file exactly as the file stored it, with no model and no inference anywhere in the path.

When the words form a grid

Tables are the part people care about most and the part a text layer stores worst. There is no table object in the PDF; there are only words at positions.4 A wide horizontal gap between two words on the same line reads as a cell boundary, and when at least three rows break at the same three or more positions, those positions become the column boundaries of a table.

The first of those rows becomes the header row of a Markdown pipe table, so a price list or a timetable keeps its columns instead of collapsing into a single long sentence that no retriever can split. The cost of this inference is that the page also earns a Table detected flag, and the honest move is to compare that table against the source before you let it into an index. Positions tell you where the cells were, not what they mean.

Headings, lists, and code blocks from spacing alone

Take a page with a heading at the top and a small table under it. The heading reads as a heading rather than as body text, because its font size sits above the page’s most common size, and it becomes # Quarterly figures by region. The table below keeps its Region, Revenue and Staff columns, and the rows under it become Markdown rows rather than paragraphs, which means a chunker that splits on table boundaries still gets clean rows to work with.

That is why a text-layer conversion is the highest-fidelity path you can run in a browser. No OCR involved, no model guessing, no download. The failure mode is structural rather than textual: the words are right and the punctuation is right, but a heading can be misread as body text, or a two-column page can be read in the wrong order. Character-level errors, the ones that ruin a search, are not the failure mode here.

Diagram of how PDF text layer geometry becomes Markdown headings, lists, code blocks and pipe tables
A text layer stores words with a size and a position, and the converter rebuilds the whole document structure from those two numbers alone.

Scanned Pages: The Local Vision Model and Its Download

A scanned page needs a reader. The tool runs granite-docling-258M, a 258-million-parameter vision-language model from IBM Research released under the Apache 2.0 license, and its model card on Hugging Face describes the design in detail.5 The model looks at the page image and writes DocTags, a markup format that labels each element on the page, including title, paragraph, list, code block, formula, and table, together with where that element sits.6

DocTags then become Markdown.7 Tables turn into pipe tables, merged cells stay as HTML so their structure survives, and the running headers and footers that repeat on every page of a report drop out instead of polluting every chunk. Because the model sees the layout rather than a list of words, it can tell a heading from a caption and a table from a paragraph of numbers, which is the part a plain OCR engine cannot do.

Small print survives because of how the page is prepared. The page is rendered with its longest side at 2048 pixels, and the model’s processor cuts that image into 512-pixel tiles plus one reduced overview of the whole page.8 That tiling matches the vision encoder behind the model, a patch-16 architecture built for 512-pixel inputs,9 so a dense paragraph of ten-point text is still several tiles of legible pixels rather than one stretched thumbnail.

Everything runs through Transformers.js on the GPU via WebGPU,10 and the tool loads the model’s full 32-bit build, about 1.27 GB.11 Smaller compressed builds were tried and rejected: the 16-bit files produced nothing but exclamation marks, and the 4-bit files dropped an entire table column while the rest of the page still looked plausible, which is the worst kind of failure because it survives a glance. Before you commit to that download, verifying that your browser exposes WebGPU the way a local model run does takes a minute and saves you the wait. Speed depends on the GPU and on how much text a page carries. The Converting row shows an estimate once the first scanned page finishes, and the browser caches the model files, so later visits start in seconds.

What the OCR fallback gives up

Three situations send a scanned page down a different path: no WebGPU in the browser, a model download that failed, and model output that comes back unreadable. In each case the tool falls back to Tesseract OCR, which reads the page as lines of characters and keeps Tesseract’s own paragraph structure, joins wrapped lines back into sentences, and separates paragraphs with a blank line. What it cannot do is rebuild a table or recognize a heading, because Tesseract sees ink rather than layout: the upstream project documents that it has a known problem recognizing text and data from tables without custom segmentation and layout analysis.12 The fallback leans on the browser build of Tesseract that runs without a server, so the page is still read on your own machine.

That is a real loss for retrieval. A Tesseract page gives you readable prose, which is enough for a keyword search and useless for a question whose answer lives in a table row. The info line above the Convert to Markdown button states which path your scanned pages will take before you start, so the choice is visible rather than implicit, and you can decide to run a proper pipeline on the pages that matter.

Reading the Flags: Which Pages Deserve a Second Look

Every converter guesses somewhere, and this one admits it. Each page can carry a flag naming the concrete reason for the doubt, and the flag sits next to the source page rather than in a separate report you have to cross-reference. Nothing gets marked as uncertain without the reason being written out where you can read it before you trust the output.

The three flag families cover the three ways a page goes wrong, and each one names a specific doubt rather than a general unease. A flag on a text page means the geometry looked unusual, a flag on a model page means the output showed a known failure pattern, and a flag on an OCR page means the confidence score came back low. Read the reason before you trust the page.

  • Text pages: grid words suggesting a table, two-column layouts, and a large share of unreadable characters from a broken font mapping.
  • Model pages: the output hit the length limit, or the text started repeating itself.
  • OCR pages: Tesseract’s average confidence fell below 70 percent, using the mean text confidence and per-word confidence values the Tesseract API exposes.13

Two-column pages are still read column by column, because silently reordering a page is worse than leaving it readable. The flag asks you to confirm the order rather than trusting it. Use the Flagged filter to step through only those pages, and on a flagged text page, with WebGPU available, press Re-run with AI model to replace that one page with the model’s reading.

Converted Markdown is not always something you want to read on a monitor. A device with an e-ink screen handles a long document the way paper does, and both of these take a converted file without asking you to install anything.

Shaping the Output File: Tables, Figures, Page Markers, Blank Pages

Merged cells need a different output format. A Markdown pipe table cannot express a cell that spans two rows, so when the converter finds one it keeps the table as HTML inside the Markdown file, which the CommonMark specification allows as a raw HTML block.14 The result renders as a real table in most Markdown viewers, and it keeps the values under the columns they belong to instead of shifting them sideways.

Figures are not exported as images. On a model page, a photo or chart becomes a hidden HTML comment containing the word image followed by its caption as plain text, so you can see that something was there and what it was called without carrying the pixels around. On a text page, pictures are simply skipped, because the text layer never held them.

Page markers and blank pages are the two switches that decide how the file maps back to the original. Include page markers starts each page with an HTML comment naming the page number, invisible in a rendered document and searchable when you need to check a passage against the PDF. Blank pages are labeled and left out by default, which suits the separator sheets and blank backs a duplex scanner picks up, and Include blank pages keeps the marker so the numbering still lines up with the file. Both switches update the result instantly, with no reconversion. Copy puts the whole document on the clipboard. Download .md saves a file whose name starts with capytoolkit-, ready for Obsidian, a documentation repository, or a hand-assembled prompt.

Decision map of what the exported Markdown file contains: pipe tables, raw HTML tables for merged cells, HTML comments for figures, page number comments and blank page markers
What lands in the exported file depends on the page, and every choice the converter makes is written into the output as visible Markdown or as an HTML comment.

Where a Browser Converter Sits Next to a Python Pipeline

None of this makes the browser the right place for every document, and pretending otherwise wastes an afternoon. A single file that nobody is allowed to upload is a browser job, while a corpus that needs to be re-processed every month is a pipeline job. The honest comparison looks like this:

ConcernBrowser converterLocal Python stack (Marker, MinerU, Docling)Cloud parsing API
InstallNothing to installPython environment, model weightsCredentials and account
HardwareA WebGPU-capable browser for scanned pagesUsually a discrete GPUA rented server
PrivacyFile never leaves the tabFile stays on your machineFile crosses the wire
Layout fidelityGood on text layers, strong on tables with the local modelHighest, with per-document models and rulesVaries by vendor
ThroughputOne document at a timeBatch jobs over a corpusBatch jobs, fastest to start

The browser wins on setup and on documents you cannot send anywhere. There is no environment to build, no model to host, no retention policy to read, and a contract stays in the tab it was opened in. The Python stack wins on throughput and on the hard cases: a corpus of ten thousand documents, a layout that defeats positional heuristics, or a pipeline that has to re-run whenever the model changes.15

The browser path also works on one document at a time, on one machine, which is a real limit. A large ingestion job belongs in a pipeline, while a one-off contract or a wiki note belongs in the tab. When you have the extracted numbers in hand and want to query them, the SQL workbench that runs DuckDB against local CSV, JSON and Parquet files in the browser keeps that step local too, and how DuckDB compiled to WebAssembly runs the query engine in the same tab covers the part that would otherwise need a server. A wider set of browser-based utilities that run entirely on your own device covers the rest of a document job without an install. Which converter you pick matters less than whether the table survived, and that is what the flags exist to protect.

Keeping a Confidential Corpus Off the Wire

Here is what the tab actually does with your file. The PDF is opened from memory and never sent anywhere; the page images, the extracted text and the finished Markdown all stay in the browser. The only network traffic is the one-time model download, which comes from Hugging Face or from CapyToolkit’s own CDN as a fallback, and that request carries no part of your document. A hosted converter has the opposite exposure model, where the file crosses the wire and lands in someone else’s retention policy, whether or not you read that policy.

The neighboring tools keep the same promise for the other half of a document job. When a contract arrives as twelve scanned pages and needs deskewing first, the phone scan deskew tool that turns a photo into a clean PDF produces a file that converts properly instead of a skewed one that OCR misreads. The local PDF toolkit for merging, splitting and reordering pages handles the rearrangement that usually happens right before conversion. When the document has to be prepared for sharing rather than for indexing, the offline OCR and redaction tool that removes names, IDs and account numbers from a PDF does that work locally too. The prompt token counter that sizes the finished text before you paste it anywhere then tells you what the converted document will cost you in context, and how each model family counts tokens differently explains why the same text prices out differently across providers.

A local pipeline eventually runs out of room on the drive it started on. Ten thousand scanned pages, the model weights and the embeddings all end up on one device, so the storage is worth choosing deliberately.

Checking the Markdown Against the Source Before You Index It

Before the file goes into a retriever, work through this list. Each step takes a minute on a single document and saves hours of guessing later, because every silent conversion failure shows up as a retrieval problem long after the source file is gone and nobody remembers which conversion produced the index:

  1. Read every flagged page against its source page.
  2. Confirm that detected tables have the columns you expect.
  3. Check that page markers line up with the pages you indexed.
  4. Verify the heading hierarchy survived intact.
  5. Search the Markdown for the terms your retriever depends on.

A conversion that looks plausible on the first screen is not evidence of anything. The failures that matter are silent: a table that lost a column, a two-column page read in the wrong order, a heading demoted to body text so the chunker never splits where you expected. You will not see them in the output. You will see them in the retrieval results days later, when somebody asks a question whose answer was sitting in a table the whole time.

That is the argument for treating the flags as a worklist rather than a warning. The conversion step decides what your model can ever retrieve, so the twenty minutes spent comparing flagged pages against their sources cost less than the week spent wondering why the index will not answer questions the document clearly answers.

Sources
  1. 1.

    “Portable Document Format,” Wikipedia, en.wikipedia.org, accessed October 2026. https://en.wikipedia.org/wiki/PDF

  2. 2.

    “Optical character recognition,” Wikipedia, en.wikipedia.org, accessed October 2026. https://en.wikipedia.org/wiki/Optical_character_recognition

  3. 3.

    Mozilla, “add built-in PDF support to Firefox with PDF.js,” Bug 714712, bugzilla.mozilla.org, 2012. https://bugzilla.mozilla.org/show_bug.cgi?id=714712

  4. 4.

    “Overview of Text Extraction from PDFs,” Informer, BCS, archive-irsg.bcs.org, 2016. https://archive-irsg.bcs.org/informer/wp-content/uploads/OverviewOfTextExtractionFromPDFs.pdf

  5. 5.

    “Granite Docling | IBM Granite,” IBM, ibm.com, accessed October 2026. https://www.ibm.com/granite/docs/models/docling

  6. 6.

    Ahmed Nassar et al., “SmolDocling: An ultra-compact vision-language model for end-to-end multi-modal document conversion,” arXiv, arxiv.org, March 2025. https://arxiv.org/abs/2503.11576

  7. 7.

    Dave Bergmann, “IBM Granite-Docling: End-to-end document understanding with one tiny model,” IBM, ibm.com, September 2025. https://www.ibm.com/new/announcements/granite-docling-end-to-end-document-conversion

  8. 8.

    “granite-docling-258M preprocessor_config.json,” IBM Granite, huggingface.co, accessed October 2026. https://huggingface.co/ibm-granite/granite-docling-258M/raw/main/preprocessor_config.json

  9. 9.

    Michael Tschannen et al., “SigLIP 2: Multilingual Vision-Language Encoders with Improved Semantic Understanding, Localization, and Dense Features,” arXiv, arxiv.org, February 2025. https://arxiv.org/abs/2502.14786

  10. 10.

    “Transformers.js: State-of-the-Art Machine Learning for the Web,” Hugging Face, github.com, accessed October 2026. https://github.com/huggingface/transformers.js

  11. 11.

    “granite-docling-258M-ONNX (ONNX weights),” IBM Granite, huggingface.co, accessed October 2026. https://huggingface.co/onnx-community/granite-docling-258M-ONNX/tree/main/onnx

  12. 12.

    “Improving the quality of the output,” Tesseract documentation, tesseract-ocr.github.io, accessed October 2026. https://tesseract-ocr.github.io/tessdoc/ImproveQuality.html

  13. 13.

    “tesseract.js type definitions (index.d.ts),” naptha, raw.githubusercontent.com, accessed October 2026. https://raw.githubusercontent.com/naptha/tesseract.js/master/src/index.d.ts

  14. 14.

    “CommonMark Spec,” John MacFarlane, github.com, accessed October 2026. https://github.com/commonmark/commonmark-spec/blob/master/spec.txt

  15. 15.

    “docling · PyPI,” PyPI, pypi.org, accessed October 2026. https://pypi.org/project/docling/

More in Developer Tools