# layout - the cut, and where every value came from Source: https://truecopy.dev/docs/layout/ Module: truecopy/layout It is pure geometry over plain data. It offers three ways to find columns, and the coordinates that let a person point at a value instead of hunting for it. No engine, no bytes, no clock. Every function here takes what it is given and returns something new, which is why the whole reading can be exercised without a PDF and without a browser. ## A page as a table of cells ```ts import { cellsOf } from 'truecopy/layout'; const rows = cellsOf(page); // string[][] const rows = cellsOf(page, myBoundaries); // cut against your own lines ``` `profileColumns`, `findRowAnomalies` and `explainRows` all want exactly this shape. ## Three ways to find the columns, and when each is right ### `boundariesFromRecurrence(rows, minimumSupport?, gap?)` **The one to reach for.** Keep only the x that come back row after row. The spread of x over a whole page counts the letterhead, the address block, the footer and every word inside a long description. Measured on a real statement: **twelve columns where the table has five**, and a cut that wrong takes every judgement downstream with it. What separates a column from furniture needs no knowledge of the document at all: a real column's left edge **recurs**; a word in the middle of a description does not. A weak band at either **edge** is kept: a column used twice in forty is a lone credit and is real, while a lone x in the middle is a label that drifted. `gap` is how far apart two x have to be to open two columns, **in the unit they were measured in**; see the rulers below. Left out, the PDF value is used. ### `boundariesFromAnchors(anchors)` When the page gives you something you trust: the x of a header label, a rule drawn on the page, a cut a person confirmed. Each boundary falls halfway between two anchors, in the same shape `columnAt` and `rowToCells` already read, so a reader swaps one for the other and changes nothing else. ### `columnBoundaries(items, gap?)` The raw spread of x over everything given. It knows nothing and works on any page, which is its strength and its limit. `pageFrom` uses it, so `page.columnBoundaries` is always this one. > Page-wide, never row-wide. Deciding "column or not" on the gap between two neighbouring items glues a cell to the next as soon as they happen to touch on that row, and that is how a whole career disappeared into `20130 trim.` ## One engine, three rulers Nothing above needs an x to be a point. The cut asks _which left edges come back row after row_, and that question is the same whichever ruler measured them, which is how the same code reads a page, a paste and a CSV. `TextPage.unit` names the ruler, and `gapFor(unit)` turns it into the one number the cut needs. | `CoordinateUnit` | x is | `gapFor` | | ---------------- | ------------------------------------------------ | -------- | | `points` | a PDF's own unit, origin bottom-left _(default)_ | 18 | | `characters` | the character a field starts at | 2 | | `index` | the field's position in the line | 0 | `characters` is two and not one because a column of amounts is usually flush right: its values start one or two characters apart and are still one column. `index` is zero because x **is** the column there: any two different indices are two columns, and nothing lies between them. A page with no `unit` came from a PDF, which is the only ruler this library had before text got one. ## Where every value came from ```ts import { cellsOf, placesOf } from 'truecopy/layout'; const cells = cellsOf(page); // cells[3][1] is what the fourth row holds const places = placesOf(page); // places[3][1] is where to find it // { page: 1, x: 430, y: 700, width: 30 } ``` The two are laid out **identically**, so they are read together. A cell nothing fell into has no place: `null`, not a rectangle of nothing. This is what makes a correction screen usable. Handed a list of figures, a person hunts through the document again; handed a place, they jump to it. `ReviewableRow.where` carries the same thing for the rows a person is asked to confirm. **No height**, because none is known: a text item carries no height here, and inventing a line spacing would draw a rectangle the document never had. Your renderer knows its own line box better than this library does. `placeOf(items, page)` is the same question asked of any group of items: a row is all of them, a cell is the ones that fell into one column. ## Rows `rowsFrom(items, boundaries)` groups items by **proximity of baseline**, never by rounding. Rounding cuts space into fixed buckets, and two items a tenth apart land in different buckets whenever they straddle a bucket edge, so a row of the table splits in two, at the mercy of where it happened to sit on the page. Joining a row puts **one tab between columns and nothing inside one**. The "nothing" matters as much as the tab: the two halves of an amount are two items of the same cell, and separating them turns one number into two. ## Assembling a document - `pageFrom(number, width, height, items)`: items into a page, cut and rowed. - `documentFrom(pages, origin, name)`: pages into a document. - `documentFromText(text, name)`: a paste, a CSV, an export. **One page**, because a paste was never paginated, and one row per line with blank lines dropped. It picks its own ruler: the field's index when a delimiter recurs line after line, the character otherwise. Prose gives one field per line, and the row comes back whole. Map of this site for a model: https://truecopy.dev/llms.txt Every page in one file: https://truecopy.dev/llms-full.txt