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, ok := reader.ToDict(resolve(doc, catDict.Get("Outlines"))) if !ok { return out } first, ok := toRef(outlines.Get("First")) if !ok { return out } o := &outliner{doc: doc, cat: catDict} seen := map[reader.Ref]bool{} o.walkChildren(first, 1, seen, &out) return out } // outliner resolves destinations for one document. The page index and the named // destinations are built once, on first use. type outliner struct { doc *reader.Document cat reader.Dict pages map[reader.Ref]int names map[string]reader.Object // named destinations from /Dests and the /Names /Dests tree } // 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 (o *outliner) walkChildren(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 d, ok := reader.ToDict(resolve(o.doc, cur)) if !ok { break } if page, ok := o.destPage(d); ok { *out = append(*out, OutlineItem{Title: textString(resolve(o.doc, d.Get("Title"))), Page: page, Level: level}) if len(*out) >= maxOutlineItems { return } } if child, ok := toRef(d.Get("First")); ok { o.walkChildren(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: /Dest, or a // /GoTo action's /D; either may be an explicit array, a dictionary holding /D, // or a name looked up in the catalog's named destinations (LaTeX's hyperref and // Acrobat write names, so without them most papers have no outline). func (o *outliner) destPage(d reader.Dict) (int, bool) { dest := resolve(o.doc, d.Get("Dest")) if dest == nil { if a, ok := reader.ToDict(resolve(o.doc, d.Get("A"))); ok { if s, ok := reader.ToName(a.Get("S")); ok && s == "GoTo" { dest = resolve(o.doc, a.Get("D")) } } } return o.pageOfDest(dest, 0) } // pageOfDest maps a destination value to its page. depth bounds the chain // name → dictionary → array, which a malformed file could make circular. func (o *outliner) pageOfDest(dest reader.Object, depth int) (int, bool) { if dest == nil || depth > 4 { return 0, false } switch v := dest.(type) { case reader.Array: if len(v) == 0 { return 0, false } ref, ok := v[0].(reader.Ref) if !ok { return 0, false } return o.pageOf(ref) case reader.Dict: return o.pageOfDest(resolve(o.doc, v.Get("D")), depth+1) case reader.Name: return o.pageOfDest(o.named(string(v)), depth+1) case reader.String: return o.pageOfDest(o.named(string(v)), depth+1) } return 0, false } // pageOf maps a page reference to its 1-based page number. func (o *outliner) pageOf(ref reader.Ref) (int, bool) { if o.pages == nil { o.pages = make(map[reader.Ref]int, o.doc.PageCount()) for i := 1; i <= o.doc.PageCount(); i++ { if pr, ok := o.doc.PageRef(i); ok { o.pages[pr] = i } } } p, ok := o.pages[ref] return p, ok } // named returns the destination a name points to, from the PDF 1.1 /Dests // dictionary or the /Names /Dests name tree. func (o *outliner) named(name string) reader.Object { if o.names == nil { o.names = map[string]reader.Object{} if dests, ok := reader.ToDict(resolve(o.doc, o.cat.Get("Dests"))); ok { for k, v := range dests { o.names[string(k)] = v } } if names, ok := reader.ToDict(resolve(o.doc, o.cat.Get("Names"))); ok { o.nameTree(resolve(o.doc, names.Get("Dests")), map[reader.Ref]bool{}, 0) } } return resolve(o.doc, o.names[name]) } // maxNames bounds a name tree read; maxTreeDepth bounds its nesting. const ( maxNames = 200000 maxTreeDepth = 32 ) // nameTree reads a name-tree node: leaf /Names pairs [key value …] and /Kids. func (o *outliner) nameTree(node reader.Object, seen map[reader.Ref]bool, depth int) { d, ok := reader.ToDict(node) if !ok || depth > maxTreeDepth || len(o.names) >= maxNames { return } if pairs, ok := reader.ToArray(resolve(o.doc, d.Get("Names"))); ok { for i := 0; i+1 < len(pairs) && len(o.names) < maxNames; i += 2 { if k, ok := reader.ToString(resolve(o.doc, pairs[i])); ok { o.names[string(k)] = pairs[i+1] } } } if kids, ok := reader.ToArray(resolve(o.doc, d.Get("Kids"))); ok { for _, k := range kids { if ref, ok := k.(reader.Ref); ok { if seen[ref] { continue } seen[ref] = true } o.nameTree(resolve(o.doc, k), seen, depth+1) } } } // resolve follows a reference. A missing, null or unresolvable object is nil: // reader's Dict.Get answers reader.Null{} for an absent key, not nil. func resolve(doc *reader.Document, obj reader.Object) reader.Object { if obj == nil { return nil } if _, isNull := obj.(reader.Null); isNull { return nil } v, err := doc.Resolve(obj) if err != nil { return nil } if _, isNull := v.(reader.Null); isNull { return nil } return v } // 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 }