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>
340 lines
9.4 KiB
Go
340 lines
9.4 KiB
Go
package reader
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
)
|
|
|
|
// An Operation is one step of a content stream: an operator and the operands
|
|
// that precede it. An inline image is reported as the operator "BI" with its
|
|
// dictionary and data attached, since the three keywords it spans are one
|
|
// thing.
|
|
type Operation struct {
|
|
Operator string
|
|
Operands []Object
|
|
Image *InlineImage
|
|
}
|
|
|
|
// An InlineImage is the dictionary and the still-encoded bytes of a BI … ID …
|
|
// EI sequence. Its dictionary uses the abbreviated keys inline images are
|
|
// written with; [InlineImage.Expanded] gives the ordinary spelling.
|
|
type InlineImage struct {
|
|
Dict Dict
|
|
Raw []byte
|
|
}
|
|
|
|
// inlineKeys maps the abbreviations an inline image dictionary uses to the
|
|
// names their long form would have.
|
|
var inlineKeys = map[Name]Name{
|
|
"BPC": "BitsPerComponent",
|
|
"CS": "ColorSpace",
|
|
"D": "Decode",
|
|
"DP": "DecodeParms",
|
|
"F": "Filter",
|
|
"H": "Height",
|
|
"IM": "ImageMask",
|
|
"I": "Interpolate",
|
|
"W": "Width",
|
|
"L": "Length",
|
|
}
|
|
|
|
// inlineColourSpaces maps the abbreviated colour space names.
|
|
var inlineColourSpaces = map[Name]Name{
|
|
"G": "DeviceGray",
|
|
"RGB": "DeviceRGB",
|
|
"CMYK": "DeviceCMYK",
|
|
"I": "Indexed",
|
|
}
|
|
|
|
// Expanded returns the image's dictionary with the abbreviated keys and colour
|
|
// space names written out, so it can be read like any image XObject.
|
|
//
|
|
// A dictionary may say the same thing twice and disagree with itself: /W 20
|
|
// beside /Width 10, /CS /RGB beside /ColorSpace /3chanRGB. The specification
|
|
// permits both spellings in an inline image and says nothing about which wins,
|
|
// and the public implementations disagree — pdf.js asks for the abbreviation
|
|
// and falls back to the written-out name, MuPDF asks the other way round.
|
|
//
|
|
// The abbreviation wins here. Two reasons: it is the spelling Table 93 gives
|
|
// for an inline image, the written-out name being the tolerated alias; and
|
|
// safedocs' own fixture for this case marks the abbreviation as the line to
|
|
// remove "to see the effect", which is a statement about what the reference
|
|
// viewer does.
|
|
//
|
|
// What is not defensible is the answer changing between runs. Expanding in one
|
|
// pass over the map made the winner depend on Go's map iteration order, which
|
|
// is deliberately random: the same file drew differently each time it was
|
|
// opened, with no way for a caller to notice. Two passes settle it — the
|
|
// written-out names first, then the abbreviations over them — and no order
|
|
// within either pass can matter, because the names in each are distinct.
|
|
func (im *InlineImage) Expanded() Dict {
|
|
out := Dict{}
|
|
for k, v := range im.Dict {
|
|
if _, abbreviated := inlineKeys[k]; abbreviated {
|
|
continue
|
|
}
|
|
out[k] = expandColourSpace(k, v)
|
|
}
|
|
for k, v := range im.Dict {
|
|
long, abbreviated := inlineKeys[k]
|
|
if !abbreviated {
|
|
continue
|
|
}
|
|
out[long] = expandColourSpace(long, v)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// expandColourSpace writes out an abbreviated colour space name, which is the
|
|
// one value that is abbreviated as well as its key.
|
|
func expandColourSpace(k Name, v Object) Object {
|
|
if k != "ColorSpace" {
|
|
return v
|
|
}
|
|
n, ok := ToName(v)
|
|
if !ok {
|
|
return v
|
|
}
|
|
if long, ok := inlineColourSpaces[n]; ok {
|
|
return long
|
|
}
|
|
return v
|
|
}
|
|
|
|
// A ContentScanner walks a content stream one operation at a time. A stream
|
|
// with rubbish in it yields the operations around the rubbish rather than
|
|
// nothing at all, which is what a renderer needs; [ContentScanner.Err] reports
|
|
// the first thing that did not parse.
|
|
type ContentScanner struct {
|
|
lex lexer
|
|
pending []Object
|
|
err error
|
|
}
|
|
|
|
// NewContentScanner reads operations from a decoded content stream.
|
|
func NewContentScanner(data []byte) *ContentScanner {
|
|
return &ContentScanner{lex: lexer{buf: data}}
|
|
}
|
|
|
|
// Err reports the first malformed token the scan stepped over, if any.
|
|
func (s *ContentScanner) Err() error { return s.err }
|
|
|
|
// Next returns the next operation, and false once the stream is exhausted.
|
|
func (s *ContentScanner) Next() (Operation, bool) {
|
|
for {
|
|
t, err := s.lex.next()
|
|
if err != nil {
|
|
s.note(err)
|
|
// Step over the byte that would not parse and carry on.
|
|
if s.lex.pos < len(s.lex.buf) {
|
|
s.lex.pos++
|
|
continue
|
|
}
|
|
return Operation{}, false
|
|
}
|
|
if t.kind == tokEOF {
|
|
return Operation{}, false
|
|
}
|
|
if t.kind != tokKeyword {
|
|
s.operand(t)
|
|
continue
|
|
}
|
|
switch string(t.text) {
|
|
case "true":
|
|
s.pending = append(s.pending, Bool(true))
|
|
continue
|
|
case "false":
|
|
s.pending = append(s.pending, Bool(false))
|
|
continue
|
|
case "null":
|
|
s.pending = append(s.pending, Null{})
|
|
continue
|
|
case "BI":
|
|
img, err := s.inlineImage()
|
|
if err != nil {
|
|
s.note(err)
|
|
return Operation{}, false
|
|
}
|
|
return s.emit("BI", img), true
|
|
}
|
|
return s.emit(string(t.text), nil), true
|
|
}
|
|
}
|
|
|
|
// operand parses one operand, an already-read token starting it.
|
|
func (s *ContentScanner) operand(t token) {
|
|
p := &parser{lex: s.lex}
|
|
o, err := p.parseFrom(t)
|
|
s.lex = p.lex
|
|
if err != nil {
|
|
s.note(err)
|
|
if s.lex.pos < len(s.lex.buf) {
|
|
s.lex.pos++
|
|
}
|
|
return
|
|
}
|
|
s.pending = append(s.pending, o)
|
|
}
|
|
|
|
// emit finishes an operation and clears the operands waiting for it.
|
|
func (s *ContentScanner) emit(op string, img *InlineImage) Operation {
|
|
out := Operation{Operator: op, Operands: s.pending, Image: img}
|
|
s.pending = nil
|
|
return out
|
|
}
|
|
|
|
// note keeps the first error the scan met.
|
|
func (s *ContentScanner) note(err error) {
|
|
if s.err == nil {
|
|
s.err = err
|
|
}
|
|
}
|
|
|
|
// inlineImage reads the dictionary after BI and the data after ID.
|
|
func (s *ContentScanner) inlineImage() (*InlineImage, error) {
|
|
d := Dict{}
|
|
for {
|
|
t, err := s.lex.next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if t.kind == tokEOF {
|
|
return nil, &SyntaxError{t.pos, "an inline image has no ID"}
|
|
}
|
|
if t.kind == tokKeyword && string(t.text) == "ID" {
|
|
break
|
|
}
|
|
if t.kind != tokName {
|
|
return nil, &SyntaxError{t.pos, "an inline image key is not a name"}
|
|
}
|
|
p := &parser{lex: s.lex}
|
|
v, err := p.parseObject()
|
|
s.lex = p.lex
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
d[Name(t.text)] = v
|
|
}
|
|
// Exactly one white-space byte separates ID from the data.
|
|
if s.lex.pos < len(s.lex.buf) && isSpace(s.lex.buf[s.lex.pos]) {
|
|
s.lex.pos++
|
|
}
|
|
start := s.lex.pos
|
|
end, err := inlineImageEnd(s.lex.buf, start, d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
raw := s.lex.buf[start:end]
|
|
s.lex.pos = end
|
|
// Step over the EI that follows.
|
|
for s.lex.pos < len(s.lex.buf) && isSpace(s.lex.buf[s.lex.pos]) {
|
|
s.lex.pos++
|
|
}
|
|
if bytes.HasPrefix(s.lex.buf[s.lex.pos:], []byte("EI")) {
|
|
s.lex.pos += 2
|
|
}
|
|
return &InlineImage{Dict: d, Raw: raw}, nil
|
|
}
|
|
|
|
// Operations tokenises a whole content stream. The error it returns describes
|
|
// what did not parse; the operations it returns are still worth having.
|
|
func Operations(data []byte) ([]Operation, error) {
|
|
s := NewContentScanner(data)
|
|
var out []Operation
|
|
for {
|
|
op, ok := s.Next()
|
|
if !ok {
|
|
return out, s.Err()
|
|
}
|
|
out = append(out, op)
|
|
}
|
|
}
|
|
|
|
// PageContent returns the decoded content stream of the i'th page, counting
|
|
// from one. A page whose /Contents is an array has its streams joined, as the
|
|
// specification requires, with a newline between them.
|
|
//
|
|
// A content stream whose filters cannot be applied contributes what could be
|
|
// salvaged from it rather than failing the page, which is what a renderer
|
|
// needs; the error is for a page that is not there at all.
|
|
// [Document.PageContentDecoded] says whether any salvaging happened.
|
|
func (d *Document) PageContent(i int) ([]byte, error) {
|
|
dec, err := d.PageContentDecoded(i)
|
|
return dec.Data, err
|
|
}
|
|
|
|
// PageContentDecoded is [Document.PageContent] with the outcome of the decode
|
|
// attached: [Decoded.Recovered] reports that at least one of the page's content
|
|
// streams could not be decoded cleanly, and [Decoded.Cause] says why.
|
|
func (d *Document) PageContentDecoded(i int) (Decoded, error) {
|
|
page, err := d.Page(i)
|
|
if err != nil {
|
|
return Decoded{}, err
|
|
}
|
|
return d.contentOf(page)
|
|
}
|
|
|
|
// contentOf decodes a page's /Contents, whether one stream or several.
|
|
func (d *Document) contentOf(page Dict) (Decoded, error) {
|
|
o, err := d.Resolve(page.Get("Contents"))
|
|
if err != nil {
|
|
return Decoded{}, err
|
|
}
|
|
switch v := o.(type) {
|
|
case *Stream:
|
|
return d.decodedContent(v), nil
|
|
case Array:
|
|
var out Decoded
|
|
for _, e := range v {
|
|
eo, err := d.Resolve(e)
|
|
if err != nil {
|
|
return Decoded{}, err
|
|
}
|
|
s, ok := ToStream(eo)
|
|
if !ok {
|
|
continue
|
|
}
|
|
part := d.decodedContent(s)
|
|
if part.Recovered && !out.Recovered {
|
|
out.Recovered, out.Cause, out.Filter = true, part.Cause, part.Filter
|
|
out.Undecoded = part.Undecoded
|
|
}
|
|
if len(part.Data) == 0 {
|
|
continue
|
|
}
|
|
if len(out.Data) > 0 {
|
|
out.Data = append(out.Data, '\n')
|
|
}
|
|
out.Data = append(out.Data, part.Data...)
|
|
}
|
|
return out, nil
|
|
}
|
|
return Decoded{}, nil
|
|
}
|
|
|
|
// decodedContent decodes one content stream. An image filter has no business
|
|
// being there, so the bytes it holds are not content: they are reported as
|
|
// salvage, not handed over as if they could be tokenised.
|
|
func (d *Document) decodedContent(s *Stream) Decoded {
|
|
dec := d.DecodeStreamRecovering(s)
|
|
if dec.Image != "" {
|
|
return Decoded{
|
|
Undecoded: dec.Data,
|
|
Recovered: true,
|
|
Filter: dec.Image,
|
|
Cause: fmt.Errorf("reader: a content stream is filtered as an image (/%s)", dec.Image),
|
|
}
|
|
}
|
|
return dec
|
|
}
|
|
|
|
// PageOperations tokenises the i'th page's content stream, counting from one.
|
|
func (d *Document) PageOperations(i int) ([]Operation, error) {
|
|
data, err := d.PageContent(i)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ops, _ := Operations(data)
|
|
return ops, nil
|
|
}
|