pdftext: extract PDF text in a re-exec'd child (sift pdftext) with a deadline
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>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user