The parser is pure Go but a goroutine cannot be killed, so extraction runs in a child process: the binary re-executed as `sift pdftext`, reading the PDF on stdin and writing JSON Lines on stdout, killed at a deadline by the parent. pdftext.go holds the types, the five errors, Extract (reader.Open with ErrWrongPassword -> ErrEncrypted, per-page recover, trailer /Info /Title, and rule-3 UTF-16BE/Latin-1 text decode) and ChildMain (soft 768 MiB memory limit, -no env read, no network, exit codes 0/3/4/1). outline.go walks /Outlines -> /First -> /Next with /First children at level+1, resolving /Dest and /GoTo /A dests to 1-based pages, bounded at 10,000 items and never visiting an object twice. runner.go is the Runner: env-isolated child (nil env is an empty environment), 60 s deadline, 64 MiB output cap, 200 MiB stdin cap, stderr capped at 4 KiB, exit-code to error mapping, and Doc parsing with ErrNoText. cmd/sift/main.go adds the unlisted `pdftext` case. tools.go is removed; the go-pdfkit modules stay vendored. Co-Authored-By: ornith-1.5-35b-a3b <ornith-1.5-35b-a3b@llama.cpp.invalid>
143 lines
3.6 KiB
Go
143 lines
3.6 KiB
Go
package pdftext
|
|
|
|
import (
|
|
"github.com/go-pdfkit/reader"
|
|
)
|
|
|
|
// maxOutlineItems bounds a walk over a malformed outline that claims more
|
|
// entries than the file could hold.
|
|
const maxOutlineItems = 10000
|
|
|
|
// walkOutline returns the document's outline as a depth-first, pre-order list:
|
|
// each item before its own children. The result is never nil.
|
|
func walkOutline(doc *reader.Document) []OutlineItem {
|
|
out := []OutlineItem{}
|
|
trailer := doc.Trailer()
|
|
if trailer == nil {
|
|
return out
|
|
}
|
|
cat, err := doc.Resolve(trailer.Get("Root"))
|
|
if err != nil || cat == nil {
|
|
return out
|
|
}
|
|
catDict, ok := reader.ToDict(cat)
|
|
if !ok {
|
|
return out
|
|
}
|
|
outlines, err := doc.Resolve(catDict.Get("Outlines"))
|
|
if err != nil || outlines == nil {
|
|
return out
|
|
}
|
|
outlinesDict, ok := reader.ToDict(outlines)
|
|
if !ok {
|
|
return out
|
|
}
|
|
first, ok := toRef(outlinesDict.Get("First"))
|
|
if !ok {
|
|
return out
|
|
}
|
|
seen := map[reader.Ref]bool{}
|
|
walkChildren(doc, first, 1, seen, &out)
|
|
return out
|
|
}
|
|
|
|
// walkChildren walks the /Next chain from ref at the given level, emitting each
|
|
// resolvable item and recursing into its /First children (level + 1). It stops
|
|
// after maxOutlineItems items and never visits an object twice.
|
|
func walkChildren(doc *reader.Document, ref reader.Ref, level int, seen map[reader.Ref]bool, out *[]OutlineItem) {
|
|
cur := ref
|
|
for cur != (reader.Ref{}) {
|
|
if seen[cur] {
|
|
break
|
|
}
|
|
seen[cur] = true
|
|
|
|
obj, err := doc.Resolve(cur)
|
|
if err != nil || obj == nil {
|
|
break
|
|
}
|
|
d, ok := reader.ToDict(obj)
|
|
if !ok {
|
|
break
|
|
}
|
|
|
|
if page, ok := destPage(doc, d); ok {
|
|
*out = append(*out, OutlineItem{Title: textString(d.Get("Title")), Page: page, Level: level})
|
|
if len(*out) >= maxOutlineItems {
|
|
return
|
|
}
|
|
}
|
|
|
|
if child, ok := toRef(d.Get("First")); ok {
|
|
walkChildren(doc, child, level+1, seen, out)
|
|
if len(*out) >= maxOutlineItems {
|
|
return
|
|
}
|
|
}
|
|
|
|
next, ok := toRef(d.Get("Next"))
|
|
if !ok {
|
|
break
|
|
}
|
|
cur = next
|
|
}
|
|
}
|
|
|
|
// destPage resolves an item's destination to a 1-based page number. It reports
|
|
// ok only when the destination is an array whose first element is a page
|
|
// reference that maps to a real page; a named or otherwise unresolvable
|
|
// destination is not ok, so the caller skips the item.
|
|
func destPage(doc *reader.Document, d reader.Dict) (int, bool) {
|
|
if ref, ok := destRef(doc, d); ok {
|
|
if page, ok := pageOf(doc, ref); ok {
|
|
return page, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// destRef finds the destination page reference, from /Dest or from a /GoTo
|
|
// action's /D.
|
|
func destRef(doc *reader.Document, d reader.Dict) (reader.Ref, bool) {
|
|
if o := d.Get("Dest"); o != nil {
|
|
if arr, ok := reader.ToArray(o); ok && len(arr) > 0 {
|
|
if ref, ok := arr[0].(reader.Ref); ok {
|
|
return ref, true
|
|
}
|
|
}
|
|
}
|
|
if o := d.Get("A"); o != nil {
|
|
if a, ok := reader.ToDict(o); ok {
|
|
if s, ok := reader.ToName(a.Get("S")); ok && s == "GoTo" {
|
|
if d2 := a.Get("D"); d2 != nil {
|
|
if arr, ok := reader.ToArray(d2); ok && len(arr) > 0 {
|
|
if ref, ok := arr[0].(reader.Ref); ok {
|
|
return ref, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return reader.Ref{}, false
|
|
}
|
|
|
|
// pageOf maps a reference to its 1-based page by comparing it with each page's
|
|
// reference.
|
|
func pageOf(doc *reader.Document, ref reader.Ref) (int, bool) {
|
|
for i := 1; i <= doc.PageCount(); i++ {
|
|
pr, ok := doc.PageRef(i)
|
|
if ok && pr == ref {
|
|
return i, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// toRef reports the reference a dictionary entry names, when it is a direct
|
|
// reference rather than an already-resolved object.
|
|
func toRef(o reader.Object) (reader.Ref, bool) {
|
|
ref, ok := o.(reader.Ref)
|
|
return ref, ok
|
|
}
|