Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
16 changes: 16 additions & 0 deletions extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
33 changes: 33 additions & 0 deletions tables_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]))
}
}
Loading