StatementKit

Why PDF Tables Break When You Copy Them Into Excel

Faraz Ali Khan7 min read

Select a table in a PDF, copy it, and paste it into Excel, and you get one of three outcomes: everything crammed into a single column, a table with the wrong number of columns and text sitting in the wrong ones, or something that briefly looks right until you scroll down and find one row where a value has fused into its neighbor. This happens with well-made, professionally generated PDFs — bank statements included — often enough that it’s worth understanding the actual reason, because it isn’t a bug in your PDF reader or a quirk of Excel’s paste handling. It’s a structural fact about what a PDF is.

A PDF doesn’t contain a table. It contains instructions to draw text.

Open a PDF’s internal structure and you won’t find a <table> with <row> and <cell> elements, the way an HTML page or a Word document might represent one. What you find instead is closer to a set of drawing instructions: “place the character ‘D’ at position (72, 640),” “place the character ‘a’ at position (77, 640),” and so on, character by character (or in short runs of characters), each with its own x/y coordinate on the page. A PDF renderer — your PDF viewer, a browser, pdf.js — reads these instructions and draws each piece of text exactly where it’s told to, and the result looks like a neatly aligned table because whoever generated the PDF (usually a bank’s statement-rendering software) calculated coordinates that happen to line up visually.

Nothing in that file says “this text belongs to the Date column” or “this run of characters is a row.” The table you see is an emergent visual effect of careful positioning, not a structural feature you can query. That’s the entire problem, and everything downstream follows from it.

What “select and copy” actually does with that

When you select text in a PDF viewer and copy it, the viewer has to make a decision about what order to emit that text in, because the underlying instructions don’t carry row/column semantics — just positions. Most viewers do something reasonable: sort roughly top-to-bottom, then left-to-right within a rough row band. This works fine for a page of paragraphs. It works badly for a table, because a table’s whole point is that unrelated pieces of information (a date, a description, an amount) sit side by side on the same visual row, and “sort left to right within a row” is exactly the operation that smashes three column values into one run of text with no delimiter between them. Paste that into Excel and you get one column containing 03/25/2026 CHECK #1042 -450.00 as a single unbroken string, because as far as the copy operation was concerned, that’s what was on that row, in that order, with nothing marking where one field ends and the next begins.

Why this is worse for bank statements specifically

Bank statements compound the problem in a few predictable ways:

Columns aren’t fixed-width, and positions shift between banks — and sometimes between pages of the same statement. A parser can’t assume “the Date column always starts at x=72” across statements, or even reliably across every page of one statement, because some banks reflow column positions slightly after a page break.

Multi-line descriptions. A merchant description that wraps onto a second line is, at the coordinate level, indistinguishable from a second row’s Date column being blank — both are just “no text at this x-position on this line.” Getting this right requires deciding, per statement, whether a line with no leading date is a continuation of the row above or a genuinely separate row, which a pure copy-paste has no mechanism to do at all.

Numbers formatted in ways that don’t round-trip as numbers. Parenthesized negatives like (45.00), trailing DR/CR suffixes, and thousands separators all paste into Excel as text, not numeric values — which is why a pasted “table” often has every amount left-aligned instead of right-aligned, and why a SUM formula silently returns 0 until you notice and manually reformat every cell.

What it actually takes to reconstruct the table correctly

Doing this properly means working with the same coordinate data the PDF viewer has, deliberately, instead of relying on a generic copy-paste’s left-to-right-top-to-bottom heuristic. Concretely, that means:

1. Extract every piece of text with its x/y position, using something like pdf.js — a real PDF-parsing library, not a copy-paste operation — which exposes exactly this position data for every text run on the page.

2. Group text into rows using a vertical tolerance, not an exact y-coordinate match. Text on the “same line” rarely shares an identical y-value down to the decimal, because of font metrics and baseline positioning, so rows are grouped by proximity within a small tolerance band rather than exact equality.

3. Find column boundaries by looking at whitespace across the whole page, not one row at a time. This is the actual key step: look down every row on the page and find the x-ranges where, consistently, no text ever appears — the gaps. Those consistent gaps are the column boundaries, because a well-formed table has whitespace running the full height of the table between columns, even though the exact column width can vary from statement to statement. This is a projection of horizontal gaps across many rows, not a rule about “the third field on each line,” which is what makes it work across bank statements it’s never specifically been taught to expect.

4. Do this per page, independently, because — as mentioned above — some statements shift column x-positions slightly after a page break, and a column-boundary calculation that’s locked in from page one can silently misalign on page four.

5. Turn resulting cell text into actual typed values — parsing (45.00) and 45.00 DR into the numeric value -45.00, and a date string into an actual date — rather than leaving everything as text that merely displays correctly.

None of this is exotic technology. pdf.js is open source and free, and the whitespace-projection approach to column detection is a well-understood technique in the document-parsing space. What it requires is actually doing the work per statement, rather than assuming one fixed template will hold across every bank, every account type, and every page of every statement — which is the corner most naive “copy from PDF” workflows cut, and the reason the result so often looks almost right and is subtly, expensively wrong.

StatementKit is built around exactly this approach — reading text positions rather than trusting a fixed layout — which is also why it works on statements from banks it’s never specifically been configured for.

Working with US Bank statements specifically? See converting a US Bank statement to Excel.