Formats August 2026 17 min read

Why PDFs have no tables

Open a bank statement and you see a table. The file does not contain one. It does not contain a row, a column, a cell, a line, or — reliably — even a word. It contains instructions to draw shapes at coordinates. Every structure you perceive is reconstructed, by your eye and then again by any software that reads it, and that reconstruction is where all the difficulty lives.

FlowParse
flowparse.io

The most convincing illusion in document processing

People are frequently surprised that extracting a table from a PDF is difficult. The document is right there, the table is obvious, and the numbers are already digital — no handwriting, no smudges, nothing to interpret. Surely it is a matter of reading it out.

The surprise comes from a reasonable but wrong assumption: that a PDF is a document in the sense a spreadsheet is a document. It is not. It is much closer to a vector drawing, and the fact that the drawing depicts text arranged in columns is a property of the picture rather than of the file's structure.

This is worth understanding for entirely practical reasons. It explains why two tools produce different results from the same file, why a converter that worked for years suddenly fails after a bank redesign, why some PDFs copy as gibberish, and why any serious extraction has to be verified rather than trusted.

What a PDF actually is

PDF descends from a page description language — a way of telling a printer exactly what marks to put where. Its central concern is that a page looks identical everywhere, which it achieves brilliantly, and which it achieves by specifying appearance rather than meaning.

A page in a PDF is a content stream: a sequence of drawing operators. Set a colour. Draw a rectangle. Choose a font. Move the text cursor to a coordinate. Show these glyphs. Move again. Show more. That is essentially the whole model, and it is the same model whether the page depicts a photograph, a poem or a bank statement.

Nothing in that model has a concept of a table. There is no operator that says "begin table", no notion of a cell, and no relationship recorded between a value and the heading above it. The heading is above it because both were drawn at coordinates that put one above the other.

The mental model that helps

Think of a PDF as a page of individually placed magnetic letters rather than as a document. Everything about where they sit is exact; nothing about what they belong to is recorded.
FlowParse
flowparse.io

Draw these glyphs, here

Text in a content stream lives inside text blocks. Within one, operators select a font and size, position the cursor, and show strings of glyph codes. Positioning can be adjusted between individual glyphs, which is how typesetting achieves kerning — the subtle spacing adjustments between letter pairs that make text look right.

That last capability is the root of a great deal of trouble. Because a producer can move the cursor between any two glyphs, there is no guarantee that a visually continuous word was emitted as one string. It can be emitted glyph by glyph, with a positioning instruction between each.

Which means that even "this is one word" is a conclusion rather than a fact. The file records marks and positions; every grouping above that — letters into words, words into fields, fields into rows — is inferred by something downstream.

No line, no word, no cell

It is worth being explicit about what is genuinely absent, because each absence corresponds to a class of extraction failure.

There is no line of text. Text sharing a vertical position looks like a line. The file does not say so, and two pieces of text on the same visual line may have been drawn at completely different points in the stream.

There is no word. Words are runs of glyphs separated by gaps that are large enough to read as spaces. How large is large enough depends on font, size and typesetting.

There is no cell. A value sits under a heading because their coordinates put it there. Nothing associates them, and if a value is nudged slightly the association changes with nothing recorded as having changed.

There is no row. Which is the one that matters most for statements, because a row is precisely the unit you need: one transaction with its date, description, amount and balance.

Spaces that are not there

A space character often does not exist in the file. Rather than drawing a blank glyph, the producer simply moves the cursor further along before drawing the next one. Visually identical; structurally quite different.

So extraction has to decide, for every gap, whether it represents a word boundary or is merely kerning. That decision is a threshold on distance relative to font size, and no single threshold is correct for every document — a tight condensed font and a loosely tracked one disagree about what a space looks like.

Set the threshold too low and words fragment into pieces. Set it too high and adjacent fields merge — which, in a statement, is how a description and an amount end up glued into one string, or how two amounts in adjacent columns become a single meaningless number.

That last failure is worth flagging, because it is not obviously wrong on inspection. A glued amount is still a number, still in the amount column, and still plausible. Only the arithmetic gives it away.

FlowParse
flowparse.io

Glyphs are not characters

The codes in a content stream index into a font, and they are not required to be Unicode. When a document embeds a subsetted font — carrying only the characters actually used, which keeps files small — the codes can be assigned arbitrarily.

To recover text, a reader needs a mapping from those codes back to characters. Producers are supposed to embed one. Not all of them do, and not all of them do it correctly, which is why some PDFs copy as complete nonsense while displaying perfectly. The picture was never in question; the text behind it was never properly declared.

Related smaller traps sit alongside. Ligatures may be a single glyph representing several characters. Some documents draw digits from a font where the code has no sensible character mapping at all. Numbers in a financial document are exactly where you least want this to happen.

When the mapping is missing entirely, treating the page as an image and recognising the characters visually can be the more reliable route — an unintuitive situation where a scan-style approach beats reading the file's own text.

Lines, or no lines, and neither settles it

Some statements draw ruled lines between rows and columns, and those lines are genuinely useful evidence about intended structure. They are drawing operators like any other — strokes and rectangles — but their positions correlate with the layout.

Plenty of statements draw none at all. Modern designs frequently rely on whitespace and alignment alone, sometimes with alternating background shading that is drawn as filled rectangles rather than as anything structural. A reader has no difficulty; a line-detection approach has nothing to find.

And lines can mislead. Decorative rules, boxes around summary panels, borders on a logo, an underline in a heading — all are the same kind of drawing operation as a table rule. An approach that trusts lines will confidently find a table in a page furniture element.

So lines are a hint to be weighed rather than a structure to be read. That is the recurring theme: every signal available is probabilistic, and robustness comes from combining several rather than trusting one.

Drawing order is not reading order

The content stream has a sequence, and it is tempting to treat it as reading order. Usually it roughly is, because most producers emit content in a natural order.

Nothing requires it. A generator may draw all of one column, then the next. It may paint a footer before the body, or place a watermark between rows, or emit a correction over the top of earlier content. The visual result is defined by coordinates and painting order, not by sequence in the stream.

Which means sequence is another hint rather than a fact. Extraction that relies on stream order will work on most documents and will produce scrambled output on the ones that do not follow the convention — and it will do so without any error, because nothing went wrong from the file's point of view.

FlowParse
flowparse.io

What extraction actually does

Given all that, the standard approach is clustering. Collect every glyph with its coordinates, then group items whose vertical positions fall within a tolerance into candidate rows, and items whose horizontal positions align across many rows into candidate columns.

It works remarkably well, because documents are typeset by machines that are consistent. If a producer places the date column at the same horizontal position on every row, the alignment is genuinely there to be found — the structure is recoverable precisely because the drawing was systematic.

And it degrades exactly where the document is inconsistent. A row whose amount is bold and therefore slightly taller. A line where a longer description pushes something onto a second visual line. A summary row typeset differently from transactions. Every one of those is a place where the assumption of consistency weakens.

Notice what clustering cannot do: it has no idea what the values mean. It groups by geometry. Whether the number it grouped into the amount column is an amount, a reference number or a page total is not a question geometry can answer.

FlowParse
flowparse.io

The wrapped description

The most common structural ambiguity in a bank statement, and a good illustration of why geometry alone is not enough.

A long payee name wraps onto a second visual line. Geometrically, there are now two rows. Logically, there is one transaction. Deciding which is right cannot be settled by position — the second line looks exactly like a row that happens to have no date and no amount.

And it is genuinely ambiguous, because some statements do have rows with no date, where a transaction shares the date of the one above it. So the same geometric pattern means "continuation" in one document and "new transaction" in another, and the difference is a matter of what the document means rather than how it looks.

FlowParse
flowparse.io

This is where reasoning about content earns its place: a line carrying no date and no amount is unlikely to be a transaction, and a line whose text reads as a continuation of the one above almost certainly is. That is a judgement, and it can be wrong — which is the argument for checking the result rather than trusting the judgement.

Page boundaries, where rows go to die

If you were designing an adversarial test for extraction, you would invent the page break. Everything difficult happens within a couple of centimetres of it.

Headers repeat, so column captions appear again mid-document and look like data if they are not recognised as repeated furniture. Page numbers and footers sit close to real rows. Many statements print a carried-forward subtotal at the bottom of a page and a brought-forward figure at the top of the next — numbers that look exactly like transactions and are not.

Include those and you have invented transactions that inflate the totals. Exclude too eagerly and you drop the real first or last row of a page. Both errors are silent, and both cluster in the one region of the document nobody scrolls carefully through.

This is the failure that produces a difference equal to the sum of a run of consecutive rows — one of the arithmetic fingerprints described in why reconciliations fail. We wrote about hunting it in our own extraction in eliminating row loss.

FlowParse
flowparse.io

Columns, and more than one account

Two layout patterns cause disproportionate trouble.

Separate debit and credit columns. Instead of one signed amount, there are two columns and the direction is implied by which one is populated. If clustering merges the two — easily done when they sit close together and most cells are empty — direction is lost entirely, and every payment reads as a receipt.

Several accounts in one document. A statement covering a current account, a savings account and a card looks like one long table with occasional headings. Treating it as one sequence produces a transaction list spanning multiple balance chains, and any completeness check against the header balances then compares things that do not belong together.

The second case is the reason our own totals check reports that it could not be evaluated when a document carries multiple accounts, rather than showing a result. One opening and closing pair cannot span several chains, and a tick that cannot be justified is worse than an honest inability to answer.

Layout featureWhy it is hardWhat goes wrong
Separate debit/credit columnsMostly empty, close togetherColumns merge; direction is lost
Wrapped descriptionsTwo visual rows, one transactionRows split, or continuations dropped
Repeated page headersLook like dataCaptions imported as transactions
Carried-forward subtotalsLook like amountsTotals inflated by invented rows
Several accounts per documentOne header, several chainsCompleteness check becomes meaningless
No ruled linesWhitespace onlyNothing for line detection to find
Landscape, many columnsNarrow columns close togetherAdjacent columns merge

The structure that could exist

In fairness to the format: PDF does have a mechanism for real structure. A tagged PDF carries a logical structure tree that marks content as headings, paragraphs, lists and — relevantly — tables, rows and cells. It exists largely for accessibility, so that assistive technology can present a document meaningfully.

Where it is present it removes almost all of the guesswork, because the producer has recorded what it meant rather than only what it drew. It is the difference between reading a structure and reconstructing one.

It is rare in bank statements. Most are generated by reporting engines that compose a visual page from a data source and emit appearance only — which is a slightly painful irony, since the structure existed in the bank's systems immediately before the document was produced and was discarded in producing it.

That irony is the actual argument for structured statement formats. The bank has the data; the PDF is a rendering of it. Getting CSV, an OFX-family file or a CAMT.053 is not asking for anything the bank does not already possess — it is asking to skip the step where the structure is thrown away.

When there is no text at all

Everything so far assumed the file contains text. A scanned or photographed statement contains an image, and nothing else. There are no glyph codes, no coordinates, no fonts — just pixels.

Character recognition has to infer the characters first, and only then can any of the layout reasoning begin. That stacks a second inference on top of the first, and the first one has its own characteristic errors: digits that resemble each other, decimal points lost to noise, a faint minus sign that disappears, columns skewed by a crooked scan.

Those errors are quiet in the worst way. A misrecognised digit produces a number that is entirely plausible — nothing about it looks wrong, because it is a perfectly ordinary amount. This is exactly the situation where checking against the document's own arithmetic stops being good practice and becomes the only real defence.

FlowParse
flowparse.io

Why two tools disagree about the same file

This is the observation that usually makes the whole picture click. Run one PDF through two extraction tools and you often get two different results — different row counts, different handling of a wrapped line, different treatment of a subtotal.

Neither is malfunctioning. Both are making the same series of judgement calls with different thresholds: what gap is a space, what vertical tolerance groups a row, whether ruled lines are trusted, whether a line without a date continues the previous one.

Judgement callTool A mightTool B might
Gap that counts as a spaceSplit a descriptionGlue two amounts together
Vertical tolerance for a rowSplit a tall row in twoMerge two tight rows
Trust ruled lines?Find a table in a summary boxFind no table at all
Line with no dateTreat as a continuationTreat as a new transaction
Repeated page headerImport it as dataDrop the real first row with it

If PDFs contained tables, this could not happen. Two tools reading a stored structure would agree, the way two spreadsheet applications agree about the contents of a cell. The disagreement is direct evidence that both are reconstructing.

It also explains the sudden failure after a redesign. A converter tuned to one layout encodes assumptions about that layout, and a bank changing its statement template invalidates them — usually without any error, because the tool still finds text where it looks and simply finds the wrong text.

The failure modes, and what each looks like

Every one of these produces output that looks reasonable. That is the common thread, and the reason a visual check is not enough.

FailureRoot causeHow it looks in the output
Glued amountsSpace threshold too wideOne plausible but wrong number
Split wordsSpace threshold too narrowFragmented descriptions
Lost minus signSign drawn separately or as a columnPayments appear as receipts
Dropped rows at page breaksFurniture and data confusedA shorter statement that still balances internally
Invented rowsSubtotals treated as transactionsInflated totals, plausible lines
Gibberish textMissing character mappingPerfect display, unreadable extraction
Scrambled orderStream order assumed to be reading orderCorrect values, wrong sequence
Merged columnsNarrow adjacent columnsReference numbers appearing as amounts

What follows from all this

The practical conclusion is not that PDF extraction is hopeless — it works very well most of the time. It is that the output is a reconstruction, and a reconstruction needs verification rather than assurance.

Which is why an accuracy percentage is such a poor thing to choose a tool by. A percentage is measured on the fields a tool read. It has nothing to say about rows it never saw, and a tool can read everything it finds perfectly while missing one line in ten. The number will look excellent.

Completeness is the property you actually want, and unlike accuracy it can be proved. A bank statement carries its own proof: opening balance plus every transaction equals the printed closing balance. If that holds to the cent, no row was dropped and no amount was misread in a way that moves the total — regardless of how the extraction was done.

That is why we run it on every statement, report the gap and the implicated rows when it fails, and say the check could not be evaluated rather than showing a tick when a document makes it impossible. Given everything above, a tool that cannot tell you whether its own reconstruction was complete is asking for a great deal of faith.

FlowParse
flowparse.io
Accuracy, honestly

We will not promise you 99 % — we will show you which rows to check

Every converter in this market advertises a number: 99 %, 99.5 %, 99.8 %. None of them publishes how it was measured or on which documents — and none of them can tell you which rows fall in the remainder. That is the part you find out later, when two amounts have been glued into one, a column has slid one place to the left, and a reconciliation will not close.

Our extraction is strong. It is also not magic — and neither is anyone else’s.

Reading a PDF is not a solved problem. A layout nobody has seen before, a faded thermal receipt, a bank that marks credits in its own way — each of those can produce a row that looks perfectly ordinary and is wrong. We build hard against that, and we still refuse to sell you a number, because the number is not the thing that protects you. Knowing exactly where to look is.

What actually goes wrong when a PDF is read

These are not hypotheticals. Every one of them is a defect we have found in real documents, reproduced, and built a check for — which is precisely why we can now point at them instead of averaging them into a percentage.

The defectWhat you see in the exportWhat it costs
Two amounts glued into oneOne plausible-looking figure instead of two rowsA total that is short by a whole transaction
A column slides one placeDates in the description, amounts in the balanceEvery row after it is wrong, and none looks wrong
A reference number read as the amount“Payment 910015” booked as 831.00A five-figure hole on a long statement
A credit sign droppedAn expense recorded as incomeThe error is twice the amount, in the wrong direction
A summary box counted as bookings“Previous balance / New balance” added as rowsTotals inflated by exactly the closing balance, twice
A page silently skippedA month that is simply shorterNothing to see — that is what makes it the worst one

So we built the layer that catches them

A second engine, deterministic — ours, and it runs on every document

After the AI reads the document, a separate layer re-does the document’s own arithmetic. Opening balance plus every transaction must equal the closing balance the bank printed. Each row’s running balance must follow from the one above it. Line items must sum to the invoice total. No AI, no confidence score, no guessing — these are proofs, and a document that fails one is provably misread.

It names the rows, not a percentage

When a check fails you do not get a lower score. You get row 48, row 133, row 1 902 — highlighted in place, red where a check proved the reading wrong and amber where it could not confirm it, with the column the check named brighter still. Open the Rows or JSON view, fix those, export. That is the whole loop, and it usually takes under a minute.

The document is the judge, not us

A bank statement carries its own proof: it can catch an error with no human, no reference data and no opinion from us. That is why we lead with it. Where a document genuinely cannot check itself — no running balance, no printed total — we say so, plainly, instead of letting silence imply that everything is fine.

Why the percentage is the wrong number to buy on

Statement sizeWhat “99 % accurate” quietly allowsWhat that means for you
300 rows3 wrong rowsAn afternoon — if you find them
1 200 rows12 wrong rowsA reconciliation that will not close
3 400 rows34 wrong rowsA six-figure error, in cases we have seen

Money work rewards being picky

Please read the flagged rows before you export. That is not a disclaimer — it is the one step that turns a good extraction into a correct one, and we have spent our engineering effort on making it short and precisely targeted rather than on rounding a number up. Anyone can print 99 %. Telling you exactly where the other 1 % is, is the harder promise, and it is the one we are willing to make.

Key takeaways

A PDF stores instructions to draw glyphs at coordinates. Rows, columns, cells, words and even spaces are reconstructions performed by whatever reads it, using clustering on geometry plus whatever hints — ruled lines, stream order, content — are available.

Every hint is probabilistic, which is why two tools disagree about the same file, why converters break silently after a redesign, why some PDFs copy as gibberish, and why page boundaries lose rows. None of these are bugs in a narrow sense; they are the consequences of inferring structure that was never recorded.

So prefer a structured format where one is available, and where it is not, insist on verification. The arithmetic a statement carries is the only thing in the whole chain that can contradict a confident reconstruction — which makes it the most valuable thing in it.

Frequently asked questions

Reconstruction is unavoidable. Guessing is not.

Convert a statement and see the completeness result: opening plus every transaction against the printed closing balance. When it holds you know nothing was dropped. When it does not, you get the gap and the rows worth examining.

FlowParse
flowparse.io

Related reading