From 2eb8c843ef78623db695f032466aec0572154fb1 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Sat, 26 Sep 2026 04:48:13 -0700 Subject: [PATCH] p5a: pdftest gains NamedDests (owner review); review notes Co-Authored-By: Claude Opus 5.5 --- docs/plans/p5a/README.md | 10 +++++++++ .../p5a/_files/internal/pdftest/pdftest.go | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/docs/plans/p5a/README.md b/docs/plans/p5a/README.md index 6bc5d76..3fbcdb0 100644 --- a/docs/plans/p5a/README.md +++ b/docs/plans/p5a/README.md @@ -64,3 +64,13 @@ reaches orion until the owner's review, the merge, and straylight's pin bump. files (task 06 now allows exactly that field). - 2026-09-26, the given test's gate note: task 06 ends with `make gate` red on exactly the three task-07 tests (the pattern crossbar v2.2 used); the gate runs green after 07. +- 2026-09-26, tasks 06–08 after the split: 52, 19 and 14 min, each first-gate. `run-plan`: all + tasks done at 04:37. +- **Owner review** (details in `docs/implementer-log.md`): end-to-end on real PDFs (arXiv, NIST, + RP2040 datasheet, a PhD thesis) against dixie's 9B found one real defect the given tests could + not see — **no outline for any LaTeX or Acrobat PDF**: named destinations were skipped (my task + 02 said to skip them) and reader's `Dict.Get` answers `reader.Null{}`, not nil, for a missing + key, which hid the `/A` action. Fixed with a name-tree lookup; `internal/pdftest` gained + `NamedDests` (owner change to a given file; the copy here is the new reference). Also fixed: a + race on the default pool, revalidation outside the per-host limit (my spec), the runner waiting + for the deadline on over-limit output, `Accept` lacking `application/pdf` (my spec). diff --git a/docs/plans/p5a/_files/internal/pdftest/pdftest.go b/docs/plans/p5a/_files/internal/pdftest/pdftest.go index 62508de..20de7ff 100644 --- a/docs/plans/p5a/_files/internal/pdftest/pdftest.go +++ b/docs/plans/p5a/_files/internal/pdftest/pdftest.go @@ -25,6 +25,10 @@ type Doc struct { Pages []string Outline []Outline Scanned []int + // NamedDests writes the outline the way LaTeX's hyperref and Acrobat do: each item is a + // /GoTo action to a named destination (resolved through the catalog's /Names /Dests tree) + // and its /Title is an indirect object. Without it items carry a direct /Dest array. + NamedDests bool } // Build returns the PDF bytes for d. It panics on a Doc it cannot express (an outline entry @@ -51,9 +55,15 @@ func Build(d Doc) []byte { scanned[p] = true } + // With NamedDests the outline items are followed by one title object per item and then + // the name tree, so the numbers below stay fixed by position. + nameTree := outlineRoot + 1 + 2*len(d.Outline) catalog := "<< /Type /Catalog /Pages 2 0 R" if len(d.Outline) > 0 { catalog += fmt.Sprintf(" /Outlines %d 0 R /PageMode /UseOutlines", outlineRoot) + if d.NamedDests { + catalog += fmt.Sprintf(" /Names << /Dests %d 0 R >>", nameTree) + } } obj(catalog + " >>") var kids []string @@ -93,6 +103,9 @@ func Build(d Doc) []byte { } n := first + i item := fmt.Sprintf("<< /Title (%s) /Parent %d 0 R /Dest [%d 0 R /XYZ null null null]", esc(o.Title), outlineRoot, pageObj(o.Page-1)) + if d.NamedDests { + item = fmt.Sprintf("<< /Title %d 0 R /Parent %d 0 R /A << /S /GoTo /D (dest.%d) >>", last+1+i, outlineRoot, i) + } if i > 0 { item += fmt.Sprintf(" /Prev %d 0 R", n-1) } @@ -101,6 +114,14 @@ func Build(d Doc) []byte { } obj(item + " >>") } + if d.NamedDests { + var names []string + for i, o := range d.Outline { + obj(fmt.Sprintf("(%s)", esc(o.Title))) + names = append(names, fmt.Sprintf("(dest.%d) [%d 0 R /XYZ null null null]", i, pageObj(o.Page-1))) + } + obj(fmt.Sprintf("<< /Names [%s] >>", strings.Join(names, " "))) + } } xref := b.Len()