diff --git a/README.md b/README.md index 21c14b1..3b0fb2e 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,18 @@ text, err := extract.Text(doc, 1) // the page as words, lines put back toget runs, err := extract.Runs(doc, 1) // every piece, with its place and its size imgs, err := extract.Images(doc, 1) // every picture, and where it lands tabs, err := extract.Tables(doc, 1) // the grids its text lines up into + +lines := extract.Lines(runs) // the runs gathered into their lines +s := extract.Join(lines[0]) // one line's pieces, spaced as words ``` +`Lines` and `Join` are exported because the rule has to be the **same** rule: +`Text` uses them to put a page's text back together, and anything reading +structure out of a page — where a paragraph ends, which line is a heading, as +[`document`](https://github.com/go-pdfkit/document) does — has to agree with it +line for line, or the two disagree about what the page says. A test asserts +that agreement rather than trusting it. + Pure Go, no C. It builds for `GOOS=js/wasm` and every architecture the fleet targets. diff --git a/extract.go b/extract.go index 64951f1..6103f66 100644 --- a/extract.go +++ b/extract.go @@ -174,3 +174,19 @@ func wantsSpace(prev, next Run, gap float64) bool { } return gap > space*0.3 } + +// Lines gathers runs into the lines they were drawn on, down the page and then +// left to right within each line. A line is allowed to wobble by a fraction of +// its own height, which is what a superscript does. +// +// It is exported because the rule has to be the SAME rule: [Assemble] uses it +// to put a page's text back together, and anything that reads structure out of +// a page — where a paragraph ends, which line is a heading — has to agree with +// it line for line or the two disagree about what the page says. +func Lines(runs []Run) [][]Run { return groupIntoLines(runs) } + +// Join puts one line's pieces back together, with a space wherever the gap +// between two of them is wide enough to be one and nothing where it is only +// kerning. It is [Assemble] for a single line, and exported for the same +// reason [Lines] is. +func Join(line []Run) string { return joinLine(line) } diff --git a/tables_test.go b/tables_test.go index 5e40813..2cc7369 100644 --- a/tables_test.go +++ b/tables_test.go @@ -563,3 +563,36 @@ func type3Page(t *testing.T, size int, content string) *reader.Document { }) }) } + +// ⛔ Lines and Join are exported so that the rule is the SAME rule. The +// control is that they agree with Assemble, which is what the rest of the +// fleet already reads a page with: if the two could disagree, a caller reading +// structure out of a page would be reading a different page from the one +// Text reports. +func TestLinesAndJoinAgreeWithAssemble(t *testing.T) { + d := pageWith(t, strings.Join([]string{ + "BT /F1 10 Tf 20 150 Td (Name) Tj 40 0 Td (Age) Tj ET", + "BT /F1 10 Tf 20 130 Td (Ada) Tj 40 0 Td (36) Tj ET", + "BT /F1 10 Tf 20 110 Td (one two) Tj ET", + }, "\n"), nil) + runs, err := Runs(d, 1) + if err != nil { + t.Fatal(err) + } + lines := Lines(runs) + if len(lines) != 3 { + t.Fatalf("%d lines, want 3", len(lines)) + } + var joined []string + for _, line := range lines { + joined = append(joined, Join(line)) + } + if got, want := strings.Join(joined, "\n"), Assemble(runs); got != want { + t.Errorf("Lines+Join read\n%q\nAssemble read\n%q", got, want) + } + // And the pieces are in reading order, which is what a caller walking + // them depends on. + if Join(lines[0]) != "Name Age" { + t.Errorf("the first line reads %q", Join(lines[0])) + } +}