Files
sift/vendor/github.com/go-pdfkit/reader/object.go
T
kyleandClaude Opus 5.5 cfde35397a 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>
2026-09-25 20:47:38 -07:00

245 lines
6.1 KiB
Go

// 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
}