P5a plan: PDF documents — seven Ornith tasks with acceptance tests first
docs/plans/p5a: 01 kind+fetch limits, 02 pdftext child + runner, 03 dcache PDF tables, 04 pdfpool, 05 pdfdoc render (map/pages/grep), 06 api, 07 digest citations, metrics, CLI, skill, unit, README. Given tests compile against a panic-only skeleton and fail there for the intended reasons; PDFs for tests are built by internal/pdftest (given), plus two qpdf-encrypted fixtures. Owner changes for the run: vendor go-pdfkit/extract v0.3.0 + reader v0.6.0 (pinned via tools.go until task 02); AGENTS.md, tools/run-plan.sh (enforces a Co-Authored-By trailer for the model), docs/implementer-log.md, `make gate`; gofmt (whitespace) on clean.go and guard; TestUnsupportedType uses a zip now that PDFs are supported; requestDeadline 90→210 s and writeTimeout 120→240 s for the PDF path's worst case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+244
@@ -0,0 +1,244 @@
|
||||
// Package reader takes existing PDF bytes apart: the object syntax, the
|
||||
// cross-reference machinery, stream filters, and the document structure built
|
||||
// on top of them. It is the parsing counterpart of github.com/go-pdfkit/pdfkit,
|
||||
// which writes PDF.
|
||||
//
|
||||
// Nothing outside the Go standard library is used, so the package builds for
|
||||
// js/wasm and for every architecture the fleet targets.
|
||||
package reader
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Kind names the eight basic PDF object types plus the two the file format
|
||||
// adds on top of them: streams and indirect references.
|
||||
type Kind uint8
|
||||
|
||||
// The object kinds. [Object.Kind] reports one of these.
|
||||
const (
|
||||
KindNull Kind = iota
|
||||
KindBool
|
||||
KindInteger
|
||||
KindReal
|
||||
KindString
|
||||
KindName
|
||||
KindArray
|
||||
KindDict
|
||||
KindStream
|
||||
KindRef
|
||||
)
|
||||
|
||||
var kindNames = [...]string{
|
||||
KindNull: "null",
|
||||
KindBool: "boolean",
|
||||
KindInteger: "integer",
|
||||
KindReal: "real",
|
||||
KindString: "string",
|
||||
KindName: "name",
|
||||
KindArray: "array",
|
||||
KindDict: "dictionary",
|
||||
KindStream: "stream",
|
||||
KindRef: "reference",
|
||||
}
|
||||
|
||||
// String names the kind, for error messages.
|
||||
func (k Kind) String() string {
|
||||
if int(k) >= len(kindNames) {
|
||||
return fmt.Sprintf("Kind(%d)", uint8(k))
|
||||
}
|
||||
return kindNames[k]
|
||||
}
|
||||
|
||||
// An Object is any value the PDF object syntax can express. The concrete types
|
||||
// are [Null], [Bool], [Integer], [Real], [String], [Name], [Array], [Dict],
|
||||
// [Ref] and *[Stream]; Kind tells them apart without a type switch.
|
||||
type Object interface {
|
||||
Kind() Kind
|
||||
}
|
||||
|
||||
// Null is the PDF null object. A missing dictionary entry resolves to Null
|
||||
// rather than to a nil Object, so callers never have to nil-check.
|
||||
type Null struct{}
|
||||
|
||||
// Kind implements [Object].
|
||||
func (Null) Kind() Kind { return KindNull }
|
||||
|
||||
// Bool is a PDF boolean.
|
||||
type Bool bool
|
||||
|
||||
// Kind implements [Object].
|
||||
func (Bool) Kind() Kind { return KindBool }
|
||||
|
||||
// Integer is a PDF integer. Numbers too large for an int64 are parsed as
|
||||
// [Real], which is what every other reader does.
|
||||
type Integer int64
|
||||
|
||||
// Kind implements [Object].
|
||||
func (Integer) Kind() Kind { return KindInteger }
|
||||
|
||||
// Real is a PDF real number.
|
||||
type Real float64
|
||||
|
||||
// Kind implements [Object].
|
||||
func (Real) Kind() Kind { return KindReal }
|
||||
|
||||
// String is a PDF string, already unescaped: the bytes a literal or hex string
|
||||
// denotes, with no interpretation of their text encoding.
|
||||
type String []byte
|
||||
|
||||
// Kind implements [Object].
|
||||
func (String) Kind() Kind { return KindString }
|
||||
|
||||
// Name is a PDF name with its #xx escapes resolved and the leading slash
|
||||
// dropped, so /Type is Name("Type").
|
||||
type Name string
|
||||
|
||||
// Kind implements [Object].
|
||||
func (Name) Kind() Kind { return KindName }
|
||||
|
||||
// Array is a PDF array.
|
||||
type Array []Object
|
||||
|
||||
// Kind implements [Object].
|
||||
func (Array) Kind() Kind { return KindArray }
|
||||
|
||||
// Dict is a PDF dictionary.
|
||||
type Dict map[Name]Object
|
||||
|
||||
// Kind implements [Object].
|
||||
func (Dict) Kind() Kind { return KindDict }
|
||||
|
||||
// Get returns the entry, or [Null] when it is absent. The value may still be a
|
||||
// [Ref]; use [Resolve] to follow it.
|
||||
func (d Dict) Get(k Name) Object {
|
||||
if v, ok := d[k]; ok && v != nil {
|
||||
return v
|
||||
}
|
||||
return Null{}
|
||||
}
|
||||
|
||||
// Ref is an indirect reference: the "12 0 R" that stands in for an object
|
||||
// stored elsewhere in the file.
|
||||
type Ref struct {
|
||||
Num int
|
||||
Gen int
|
||||
}
|
||||
|
||||
// Kind implements [Object].
|
||||
func (Ref) Kind() Kind { return KindRef }
|
||||
|
||||
// String renders the reference the way it appears in a file.
|
||||
func (r Ref) String() string { return fmt.Sprintf("%d %d R", r.Num, r.Gen) }
|
||||
|
||||
// Stream is a PDF stream: a dictionary plus the raw, still-encoded bytes
|
||||
// between the stream and endstream keywords. [Decode] applies the filter chain.
|
||||
type Stream struct {
|
||||
Dict Dict
|
||||
Raw []byte
|
||||
}
|
||||
|
||||
// Kind implements [Object].
|
||||
func (*Stream) Kind() Kind { return KindStream }
|
||||
|
||||
// A Resolver fetches the object an indirect reference names. A document
|
||||
// supplies one; a nil Resolver makes every reference resolve to [Null], which
|
||||
// is what a caller parsing an isolated fragment wants.
|
||||
type Resolver func(Ref) (Object, error)
|
||||
|
||||
// maxRefDepth bounds a chain of references pointing at references. A file can
|
||||
// make that chain a cycle, and a cycle must not hang the reader.
|
||||
const maxRefDepth = 64
|
||||
|
||||
// Resolve follows indirect references until it reaches a direct object. A nil
|
||||
// object, an absent entry and an unresolvable reference all yield [Null].
|
||||
func Resolve(o Object, r Resolver) (Object, error) {
|
||||
if o == nil {
|
||||
return Null{}, nil
|
||||
}
|
||||
for depth := 0; ; depth++ {
|
||||
ref, ok := o.(Ref)
|
||||
if !ok {
|
||||
return o, nil
|
||||
}
|
||||
if r == nil {
|
||||
return Null{}, nil
|
||||
}
|
||||
if depth >= maxRefDepth {
|
||||
return nil, fmt.Errorf("reader: reference chain longer than %d at %s", maxRefDepth, ref)
|
||||
}
|
||||
v, err := r(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if v == nil {
|
||||
return Null{}, nil
|
||||
}
|
||||
o = v
|
||||
}
|
||||
}
|
||||
|
||||
// ToBool reports the value of a boolean object.
|
||||
func ToBool(o Object) (bool, bool) {
|
||||
v, ok := o.(Bool)
|
||||
return bool(v), ok
|
||||
}
|
||||
|
||||
// ToInt reports the value of an integer object, and of a real whose value is a
|
||||
// whole number — files do write "3.0" where an integer belongs.
|
||||
func ToInt(o Object) (int64, bool) {
|
||||
switch v := o.(type) {
|
||||
case Integer:
|
||||
return int64(v), true
|
||||
case Real:
|
||||
if float64(int64(v)) == float64(v) {
|
||||
return int64(v), true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// ToFloat reports the value of any numeric object.
|
||||
func ToFloat(o Object) (float64, bool) {
|
||||
switch v := o.(type) {
|
||||
case Integer:
|
||||
return float64(v), true
|
||||
case Real:
|
||||
return float64(v), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// ToString reports the bytes of a string object.
|
||||
func ToString(o Object) ([]byte, bool) {
|
||||
v, ok := o.(String)
|
||||
return []byte(v), ok
|
||||
}
|
||||
|
||||
// ToName reports the value of a name object.
|
||||
func ToName(o Object) (Name, bool) {
|
||||
v, ok := o.(Name)
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// ToArray reports the elements of an array object.
|
||||
func ToArray(o Object) (Array, bool) {
|
||||
v, ok := o.(Array)
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// ToDict reports the dictionary of a dictionary object, or a stream's own
|
||||
// dictionary — a stream is a dictionary everywhere a dictionary is expected.
|
||||
func ToDict(o Object) (Dict, bool) {
|
||||
switch v := o.(type) {
|
||||
case Dict:
|
||||
return v, true
|
||||
case *Stream:
|
||||
return v.Dict, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// ToStream reports the stream object.
|
||||
func ToStream(o Object) (*Stream, bool) {
|
||||
v, ok := o.(*Stream)
|
||||
return v, ok
|
||||
}
|
||||
Reference in New Issue
Block a user