Blog

PDF to Markdown for RAG: what a good conversion keeps

A PDF stores none of the structure a model needs. Here are the five things a good conversion has to rebuild, and the ones that still go wrong.

A good PDF conversion keeps five things, and a PDF file stores none of them. Reading order, heading hierarchy, paragraph boundaries, the absence of page furniture, and a record of which page each piece came from. All five have to be rebuilt by inference, and the difference between a conversion that rebuilds them and one that does not is the difference between a retrieval system that works and one that quietly returns the wrong paragraph.

A PDF does not contain paragraphs

This is the fact everything else follows from, and it surprises people who have never had to read one with code.

A PDF is a set of drawing instructions. It says: put this glyph at this coordinate, in this font, at this size. Then the next one. There is no paragraph, no heading, no column, no list. Those are things your eye infers from size, weight and position on the page. The file itself does not know they exist.

So when a tool copies text out of a PDF, there is nothing structural to copy. The tool has to sort the glyphs into some order and hand them over as a wall of text. Everything a reader could see is thrown away in that step, and it is thrown away silently, which is why the problem is so easy to miss. The output looks like text. It just is not the text that was on the page.

Rebuilding the structure means measuring the geometry and making judgement calls about what it means. Here are the five calls that matter.

1. Reading order

Sort the text on a two column page by vertical position and you get the first line of the left column, then the first line of the right column, then the second line of the left column, and so on. The columns interleave. The result is grammatically shaped nonsense: sentences that were never written, assembled from two unrelated paragraphs.

It is worse than losing the text, because losing text is obvious and this is not. The output reads as fluent English at a glance. Fed to a model, it produces confident answers about claims nobody made.

The fix is to find the columns before reading anything. Our converter looks for a vertical gap that runs the height of a region, splits there, and reads each side to its end. That kind of split is easy to get wrong in the other direction, so it carries three conditions: the gap has to be genuinely wide, both sides have to hold a real share of the text, and both sides have to be at least two lines tall. Without those, a heading sitting beside a page number looks exactly like a two column layout.

Full width elements are handled first. A title spanning both columns is separated off by a horizontal cut before the vertical one runs, because otherwise the title’s own width hides the gutter underneath it.

2. Heading hierarchy

Headings are the most valuable thing in the document and the most commonly discarded, because in the file they are just text that happens to be bigger.

The usable signal is relative, not absolute. There is no size that means “heading”. So the body text size has to be measured across the whole document first, weighted by how many characters are set at each size, and anything consistently larger than that is a candidate. Then the sizes that appear get ranked, so the largest becomes an h1 and the next becomes an h2, which is how a document with a title and three section levels comes out with the levels in the right order.

Size alone misses a common case: documents that mark their sections with weight rather than size. Bold at body size is still a heading to a reader. The obvious rule would be “if the line is bold, it is a heading”, but that information is often not on offer. A text extraction pass does not necessarily report font weight at all, and the font family string can be as unhelpful as the word “serif”.

What is available is font identity: a renderer-assigned id that says two runs are set in different fonts, without saying how. That turns out to be the better question anyway. The rule becomes “a single line, at body size, set in a font that is not the body font”, which catches headings in bold, italic and small caps alike, where a bold flag would only have caught the first.

3. Paragraph boundaries

A PDF has a line break at the end of every visual line. Almost none of them are real. Join them all and paragraphs merge into each other; keep them all and a paragraph arrives as twenty fragments, none of which is a sentence.

The common rule is that a short last line ends a paragraph. It fails on ragged right text, where every line is short by some amount and the rule fires more or less at random.

The rule that holds is to ask whether the next word would have fitted. Measure the first word of the following line against the space left at the end of the previous one. If there was room and the author broke the line anyway, the paragraph ended. If there was not, it is a wrap and the lines belong together. The width estimate can be crude, roughly half the point size per character, because the question is not “how wide is this word” but “was there obviously room”, and that tolerates a lot of error.

Wrapped lines then get joined back, and words the typesetter split across a line break get closed up so that “under-” and “stand” become one word again rather than two fragments that embed as nothing.

That repair has a limit worth knowing about. A hyphen between two lowercase fragments is usually a typesetter’s break, but not always: “well-known”, broken after the hyphen, looks identical to “under-stand”, and closing it up gives “wellknown”. Telling the two apart needs a dictionary, which is a megabyte we are not going to ship into a browser for the case. Our converter lets you turn the repair off instead, which is the honest option.

4. The absence of page furniture

Every page of a report carries a running header, a footer, and a page number. To a reader these are invisible scaffolding. In extracted text they land in the middle of sentences, because they sit between the last line of one page and the first line of the next.

The consequence for retrieval is specific. If you split that text into chunks, some chunk will contain “…the committee resolved that Annual Report 2025 14 the funding be withdrawn…”. That chunk gets embedded with the noise inside it, and it is now slightly wrong in a way nothing downstream will ever flag.

Furniture is found by repetition rather than by position: text that appears in the same zone at the top or bottom of many pages, with the numbers normalised so that “page 3” and “page 4” count as the same thing. One page cannot tell you what its own furniture is. Twenty pages can.

5. Provenance, which nobody lists

Here is the one that gets left out of every list of what a conversion should preserve, and the one that decides whether the output is usable in production.

An answer built from a retrieved chunk is only worth anything if someone can check it. Check it against what? The chunk came from somewhere. If the conversion did not record where, the answer cannot cite a page, and an answer that cannot be checked is an answer nobody is allowed to act on. In a legal, medical or financial setting that is the whole game.

Page boundaries are in the PDF and they are free to record. The only question is how to carry them through a Markdown file without putting visible clutter in the text.

Our converter writes them as HTML comments: <!-- page 12 -->. A Markdown renderer shows nothing, a human reading the file sees a small marker, and a chunking script can read them and stamp each chunk with the page range it spans. It is not clever. It is just the option that survives the round trip, and the reason to prefer it over a horizontal rule or a heading is that neither of those is invisible when rendered.

There is a related point about the source. Whatever you convert, the original file still carries its own hidden record of who made it, which is a separate problem worth understanding before you put a batch of client documents through any pipeline.

Why Markdown, and not plain text or JSON

Three formats are plausible. The argument for Markdown is short.

Plain text loses the headings, which means it loses the only chunk boundary that carries meaning. Split plain text and you are splitting on character counts, because there is nothing else to split on.

JSON keeps everything, including structure a model does not need, and pays for it in tokens. Every brace, quote and key name is billed. It is the right choice when another program is the consumer. It is the wrong choice when a language model is.

Markdown keeps the structure that matters in the smallest number of characters that can express it. A heading is a hash and a space. It also has a property the other two do not: models have seen enormous quantities of it, so the conventions are already familiar rather than something to be inferred from your particular schema.

The practical version of the argument is that a heading in Markdown is simultaneously a piece of text the model reads, a boundary a splitter can cut on, and a label you can attach to the chunk. One character doing three jobs is hard to beat.

What still goes wrong

Four things, stated plainly, because a page that lists only what works is a product pitch.

Tables. A table is a two dimensional structure and text is one dimensional. Ours currently comes out as plain lines, which means a row loses its association with its headers, and a cell reading “30 days” stops meaning anything at all. Clustering cells into columns across rows is a genuinely separate problem and it is the next tool we are building. Any converter that claims tables are solved is worth testing before you believe it.

Scanned pages. If the page is a photograph of paper, there is no text to read and no amount of layout inference invents any. This needs optical character recognition first. Our converter detects the case and tells you, rather than handing back an empty file and letting you find out later, but it does not fix it.

Equations. Mathematical notation is laid out as individually positioned glyphs, and recovering the expression from those positions is its own research problem. Expect inline maths to come out scrambled.

Floating figures in multi column layouts. A figure that spans one and a half columns, or a caption that sits between columns, defeats a clean vertical split. The text is recovered but the order around the figure may not be.

A checklist for judging any converter

Run these against whatever you are considering, including ours. They take a few minutes and they separate the tools quickly.

  1. A two column page with a full width title. Does the title stay above both columns, and do the columns read to their ends rather than interleaving?
  2. A word hyphenated across a line break. Is it one word in the output?
  3. A hyphenated word broken across a page break. Harder, and more tools fail it.
  4. A numbered list that starts at seven. Does it still start at seven, or has it been silently renumbered to one? A document that refers to “clause 9” needs clause 9 to still be there.
  5. A running header that changes on odd and even pages. Is it removed, or does it appear mid-paragraph?
  6. A page that is an image. Does the tool tell you, or hand back nothing?
  7. A table with a merged cell. Almost everything fails this. Find out how it fails.
  8. Ligatures. Does “fi” survive as two characters or arrive as one unusual one?

Whatever you use, the test that matters most is the first one, because it is the failure that produces plausible output rather than obviously broken output.

If you want to try these against ours, the PDF to Markdown converter runs entirely in your browser, so you can put a document you are not free to upload through it and see what comes back.

The short version

  • A PDF stores glyphs at coordinates. Paragraphs, headings, columns and lists are inferred by the reader, and a conversion has to infer them again.
  • Reading order is the failure that matters most, because interleaved columns produce fluent nonsense rather than obvious breakage.
  • Headings are worth more than they look, because they are the only chunk boundary in the document that means anything.
  • Record which page each piece came from. An answer that cannot cite a page cannot be checked, and an answer that cannot be checked cannot be used.
  • Tables, scans and equations are still hard. Test them yourself rather than trusting anyone’s claim, ours included.

Further reading

  • ISO 32000-1, the PDF specification. Clause 9 covers text and fonts, and is the primary source for why none of the structure you can see is actually stored. Long, and worth having open.
  • pdf.js, Mozilla’s PDF renderer. Its text extraction pass is what most browser-based tools are built on, including ours, and reading what it does and does not report is the fastest way to understand the constraints.
  • Docling, a document conversion toolkit now hosted by the LF AI and Data Foundation. Considerably heavier than a browser tool, much stronger on tables, and worth using if you are converting thousands of files rather than one.
  • The CommonMark specification. Worth a skim if you are generating Markdown, because the rules about what does and does not need escaping are where most generated Markdown quietly breaks.