P5a review: outlines from named destinations; pool race; fast over-limit; revalidation under the limiter; Accept pdf
- pdftext/outline: resolve named destinations through the catalog /Dests and the /Names /Dests tree, resolve indirect /Dest, /A and /Title, and treat reader.Null (what Dict.Get returns for a missing key) as absent. Before this every LaTeX and Acrobat PDF had an empty outline. pdftest gains NamedDests; TestOutlineNamedDestinations. - api: pool() always goes through its Once (data race on concurrent first use); TestDefaultPoolConcurrentFirstUse. Revalidation of a stale cache entry counts against the caller/host limit. - pdftext/runner: output over MaxText kills the child at once instead of waiting for the deadline; TestRunnerTooMuchTextFailsFast. - fetch: Accept includes application/pdf;q=0.8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+131
-56
@@ -24,27 +24,33 @@ func walkOutline(doc *reader.Document) []OutlineItem {
|
||||
if !ok {
|
||||
return out
|
||||
}
|
||||
outlines, err := doc.Resolve(catDict.Get("Outlines"))
|
||||
if err != nil || outlines == nil {
|
||||
return out
|
||||
}
|
||||
outlinesDict, ok := reader.ToDict(outlines)
|
||||
outlines, ok := reader.ToDict(resolve(doc, catDict.Get("Outlines")))
|
||||
if !ok {
|
||||
return out
|
||||
}
|
||||
first, ok := toRef(outlinesDict.Get("First"))
|
||||
first, ok := toRef(outlines.Get("First"))
|
||||
if !ok {
|
||||
return out
|
||||
}
|
||||
o := &outliner{doc: doc, cat: catDict}
|
||||
seen := map[reader.Ref]bool{}
|
||||
walkChildren(doc, first, 1, seen, &out)
|
||||
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 walkChildren(doc *reader.Document, ref reader.Ref, level int, seen map[reader.Ref]bool, out *[]OutlineItem) {
|
||||
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] {
|
||||
@@ -52,24 +58,20 @@ func walkChildren(doc *reader.Document, ref reader.Ref, level int, seen map[read
|
||||
}
|
||||
seen[cur] = true
|
||||
|
||||
obj, err := doc.Resolve(cur)
|
||||
if err != nil || obj == nil {
|
||||
break
|
||||
}
|
||||
d, ok := reader.ToDict(obj)
|
||||
d, ok := reader.ToDict(resolve(o.doc, cur))
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
if page, ok := destPage(doc, d); ok {
|
||||
*out = append(*out, OutlineItem{Title: textString(d.Get("Title")), Page: page, Level: level})
|
||||
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 {
|
||||
walkChildren(doc, child, level+1, seen, out)
|
||||
o.walkChildren(child, level+1, seen, out)
|
||||
if len(*out) >= maxOutlineItems {
|
||||
return
|
||||
}
|
||||
@@ -83,57 +85,130 @@ func walkChildren(doc *reader.Document, ref reader.Ref, level int, seen map[read
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// 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" {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
dest = resolve(o.doc, a.Get("D"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return reader.Ref{}, false
|
||||
return o.pageOfDest(dest, 0)
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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) {
|
||||
|
||||
Reference in New Issue
Block a user