How PDF2Sheet Extracts Tables — And Why Nothing Is Uploaded
No black box. This page explains exactly what the converter does with your file, what it can and cannot recover, and how to verify for yourself that no data leaves your browser.
Step 1 — read the text, and its position
The first thing that happens is that PDF.js, the open-source PDF engine behind
Firefox's built-in viewer, parses the file. Importing it from this site's own /vendor
folder is the only third-party code that runs.
PDF.js returns a list of text runs. Each run carries a string and a transform matrix — in practice, an x and y coordinate and a font size. That is the raw material:
str: "POS PURCHASE - WHOLE FOODS MKT" x: 130.0 y: 214.5 width: 132.4 height: 8.0
Note what is not in that list: rows, columns, borders, or any idea that these runs belong together. A PDF is a page description, not a data format. Everything after this point is reconstruction.
Step 2 — group runs into rows
Runs whose baselines sit within about a third of the font size of each other are treated as one line. The tolerance scales with the text: a 20 pt headline and an 8 pt caption still count as the same line if their baselines are two points apart, but two lines of 8 pt body text twelve points apart do not.
Inside a line, runs separated by less than roughly four-tenths of an em are joined — that is just a space in the same cell. Wider gaps stay as separate cells.
Step 3 — find the columns
This is where most converters go wrong, so it is worth being specific about what this one does. It clusters the left edge of every cell into column anchors, and assigns each cell to its nearest anchor.
Three consequences follow, and they are the reason this approach was chosen:
- A wide cell — "Closing balance", a long merchant description — starts in its column even though its middle extends past the next one. Centre-based assignment gets this wrong; left edges do not.
- A header printed on the same baseline as body text (a letterhead that says ACME BANK Statement period) cannot pollute the result, because each row votes for anchors independently.
- A sparse column survives. If only three rows have a value in the credit column, three cells starting at the same x are still strong evidence of a column.
Anchors need a minimum amount of support to exist, so a single stray label floating in whitespace does not create a column of its own.
Step 4 — decide what is actually the table
A page has furniture: bank name, address block, page numbers, legal text. The converter keeps the densest run of numeric rows — real table bodies contain amounts and quantities, letterheads do not — and pulls in the header line directly above it. Everything else is dropped when Only export the table is on.
Step 5 — clean up the values
Finally each cell is examined. Amounts become numbers: brackets mean negative, a trailing minus
means negative, both 1,234.56 and 1.234,56 are understood, and ISO
currency codes in front of a value are stripped. Identifiers that merely contain digits — account
numbers, invoice numbers, ZIP codes with a leading zero — are deliberately kept as text so their
leading zeros survive.
Why not OCR? Optical character recognition guesses characters from pixels. On a crisp digital PDF it is strictly worse than reading the text that is already there: slower, less accurate, and it needs a rendering engine. On a genuine scan it is the only option — but the results need a human to check every line, so this tool does not pretend to offer it. When it detects a scan, it says so.
How to verify that nothing is uploaded
You do not need to trust this page. Two minutes with your browser's developer tools settles it.
-
Open the Network panel
Press F12 (or Cmd+Opt+I on a Mac), choose the Network tab, then reload the page so you start from a clean list.
-
Clear the list, then convert a file
Click the clear button in the panel, then drop a PDF into the converter above. Watch the list as it runs.
-
Read what appears
You will see this site's own files: the HTML, the stylesheet,
app.js,engine.js, the PDF.js worker, the spreadsheet writer. No request carries your document. There is noPOST, nomultipart/form-data, no WebSocket.
The same check works offline: load the page, then disable your network adapter, and convert a file. It still works, because the conversion never needed a network in the first place.
Known limits, stated plainly
- Scans and photos. No text layer, no output. Detected and reported rather than silently returning an empty sheet.
- Columns with no whitespace between them. If two fields touch, there is no geometric signal to split on. The More columns setting can help when the gap is small but non-zero.
- Tables drawn as images inside a text PDF. A chart or a screenshot of a table is a picture, not text.
- Multi-line cells. A wrapped description is merged back into its row when the continuation has no date of its own. If the bank repeats the date on every line, the merge cannot fire.
- Dates are kept as printed. A statement that says
02 Sepwithout a year stays text; inventing a year would be worse than leaving it.
What the code is
The extraction engine is a single dependency-free JavaScript file of roughly 600 lines: row clustering, column anchoring, table detection, number parsing. PDF.js and SheetJS are used unmodified. There is no analytics script on tool pages, no tracking pixel, no cookie set by the converter.
FAQ
Does the PDF.js worker make a network request?
Once, to load the worker script from this site — the same kind of request as loading the page's CSS. It carries no document data; it is the code that does the parsing. After that the file is processed entirely in memory.
Could a future version start uploading files?
It could, which is exactly why the verification steps above are on this page. Any change that phoned home would be visible in your Network panel immediately. That is a stronger guarantee than a privacy policy sentence.
Why does a large file take a few seconds?
Parsing is CPU work on your machine rather than a server's, so a 200-page statement takes a moment. Nothing is being uploaded, which is why the progress bar moves at the speed of your processor rather than your connection.
Is the code open source?
The engine is readable in your browser's Sources panel — it is served unminified at /assets/js/engine.js. Read it, or run it yourself in Node; it is a UMD module and works outside the browser.