readTable - the two-line path
truecopy/table
Rows and cells out of a file with no configuration at all, plus the field that says what the reading could not vouch for.
import { readTable } from 'truecopy/table';
const { rows, warnings, boundaries, document } = await readTable(file, options?);
| Field | |
|---|---|
rows |
string[][] — every row of every page, cut into cells |
warnings |
string[] — what could not be vouched for |
boundaries |
number[][] — the cut used, one list per page |
document |
the Document, for a reader that needs more than the cells |
options is OpenOptions — caps, deadline, workerSrc, pdfjs.
Whatever was dropped, the same two lines
file is a PDF, a table pasted out of one, a CSV, a TSV, an OCR page. One engine reads all of them, because the cut votes on which left edges come back row after row — and that question does not care what measured them. A document only has to say where its fields start.
| What was dropped | Where a column starts | What the reading prints |
|---|---|---|
| a PDF | the item’s x, in points | cut at 100, 290 |
| a table pasted with spaces | the character the field starts at | cut at characters 7, 22 |
| a file written with a delimiter | the field’s index — a CSV lines nothing up | cut on the delimiter |
| prose, or a file that quotes its fields | nowhere | every row came back whole |
Tab, semicolon, pipe and comma, in that order — and only when the count recurs line after line. What makes a delimiter real is what makes a column real.
Two refusals come with that, and both are the point rather than a gap:
- A file that quotes its fields is not split. A quoted field may hold the delimiter itself, and splitting anyway shifts every column after it. Half-parsing a CSV is exactly the plausible-but-wrong reading this library argues against, so the rows come back whole with a warning.
- An aligned paste is not read as a CSV because its amounts hold commas. Every French amount does, so counting commas finds exactly one on every line of a pasted statement — and cutting on it would turn
12,40into two columns. Alignment wins; only a tab overrides it, a tab never being punctuation.
warnings is the whole point
Whoever types “extract tables from pdf” wants rows, and making them write a kindOf and choose thresholds before the first result is twenty minutes most people do not spend. So this is two lines.
What it does not do is return a bare array. A plausible-looking table out of a document that was misread is worse than nothing, because nobody checks a table that looks right — hand one over with no signal and this library becomes the thing it was written to prevent, and a worse one than the extractors that have spent years tuning their heuristics.
An empty
warningsis not a promise. It says nothing looked wrong from the shape of the page. That is a much smaller claim than “this reading is right”, and the distance between the two is what the rest of the library is for.
Every warning is computed without knowing anything about your document:
page N carries no text at all— a blank page, a scan, an image;page N shows no column at all— the page is prose, or the cut failed and every row came back whole;column C of page N is filled on only X%— the cut may have invented that column out of a letterhead;the pages disagree on how many columns there are— usually a different table, and joining them makes a third that is neither.
boundaries, and why they are not the page’s own
readTable does not cut on page.columnBoundaries. That one is the spread of x over everything printed — letterhead, address block, footer — and on a real page it proposes columns the table never had: measured, twelve where the table has five.
It uses boundariesFromRecurrence instead: keep only the x that come back row after row, because a real column’s left edge recurs and a word in the middle of a description does not.
boundaries is handed back so you can explain or re-cut the same page against the same lines:
import { explainDocument } from 'truecopy/explain';
const { document, boundaries } = await readTable(file);
console.log(
explainDocument(document, {
boundariesOf: (page) => boundaries[page.pageNumber - 1]
})
);
Without that, the heading and the cells would tell two different stories.
When two lines are not enough
The moment something downstream acts on the rows — a budget, a report, a decision — you want a reading that checks itself rather than one that merely looks fine:
findRowAnomaliesdrops the rows that break the table’s own shape;validatesays whether enough well-formed records came back;readDocumentrefuses to return a reading that contradicts its document.