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 }