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>
129 lines
3.4 KiB
Go
129 lines
3.4 KiB
Go
package reader
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/zlib"
|
|
"fmt"
|
|
)
|
|
|
|
// objectsPerStream bounds how many objects go into one object stream. A
|
|
// stream has to be inflated whole to read any object in it, so packing
|
|
// everything into one would make opening a file read all of it.
|
|
const objectsPerStream = 200
|
|
|
|
// packedAt says which object stream an object was packed into, and where.
|
|
type packedAt struct {
|
|
stream int
|
|
index int
|
|
}
|
|
|
|
// pendingObject is an object waiting to be packed.
|
|
type pendingObject struct {
|
|
ref Ref
|
|
obj Object
|
|
}
|
|
|
|
// packObjects writes every waiting object into object streams. It is called
|
|
// once, from Finish.
|
|
func (w *Writer) packObjects() {
|
|
for start := 0; start < len(w.pending); start += objectsPerStream {
|
|
end := start + objectsPerStream
|
|
if end > len(w.pending) {
|
|
end = len(w.pending)
|
|
}
|
|
w.packGroup(w.pending[start:end])
|
|
}
|
|
w.pending = nil
|
|
}
|
|
|
|
// packGroup writes one object stream holding the given objects.
|
|
func (w *Writer) packGroup(group []pendingObject) {
|
|
var head, payload bytes.Buffer
|
|
for _, p := range group {
|
|
fmt.Fprintf(&head, "%d %d ", p.ref.Num, payload.Len())
|
|
payload.Write(AppendObject(nil, p.obj))
|
|
payload.WriteByte('\n')
|
|
}
|
|
body := append(head.Bytes(), payload.Bytes()...)
|
|
stream := w.Reserve()
|
|
for i, p := range group {
|
|
w.packed[p.ref.Num] = packedAt{stream: stream.Num, index: i}
|
|
}
|
|
w.writeInline(stream, &Stream{
|
|
Dict: Dict{
|
|
"Type": Name("ObjStm"),
|
|
"N": Integer(len(group)),
|
|
"First": Integer(head.Len()),
|
|
},
|
|
Raw: body,
|
|
})
|
|
}
|
|
|
|
// flateCompress compresses a stream's data, which is what makes packing worth doing.
|
|
func flateCompress(data []byte) []byte {
|
|
var buf bytes.Buffer
|
|
zw := zlib.NewWriter(&buf)
|
|
// A bytes.Buffer never refuses bytes, and Close only flushes.
|
|
zw.Write(data)
|
|
zw.Close()
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// finishWithXrefStream writes the cross-reference information as a stream
|
|
// rather than a table, which is the only form that can name an object inside
|
|
// an object stream.
|
|
func (w *Writer) finishWithXrefStream(trailer Dict) ([]byte, error) {
|
|
// Finish has already written the /Encrypt dictionary and put what a
|
|
// reader needs into the trailer.
|
|
w.packObjects()
|
|
xref := w.Reserve()
|
|
// The cross-reference stream is reserved after everything else — the
|
|
// object streams included — so it always carries the highest number.
|
|
high := xref.Num
|
|
start := w.buf.Len()
|
|
w.offsets[xref.Num] = start
|
|
|
|
rows := make([]byte, 0, (high+1)*7)
|
|
for num := 0; num <= high; num++ {
|
|
switch {
|
|
case num == 0:
|
|
rows = append(rows, 0, 0, 0, 0, 0, 0xFF, 0xFF)
|
|
case w.offsets[num] != 0 || num == xref.Num:
|
|
rows = append(rows, xrefRow(1, int64(w.offsets[num]), 0)...)
|
|
default:
|
|
if at, ok := w.packed[num]; ok {
|
|
rows = append(rows, xrefRow(2, int64(at.stream), int64(at.index))...)
|
|
continue
|
|
}
|
|
rows = append(rows, 0, 0, 0, 0, 0, 0xFF, 0xFF)
|
|
}
|
|
}
|
|
|
|
dict := Dict{
|
|
"Type": Name("XRef"),
|
|
"Size": Integer(high + 1),
|
|
"W": Array{Integer(1), Integer(4), Integer(2)},
|
|
}
|
|
for k, v := range trailer {
|
|
dict[k] = v
|
|
}
|
|
w.writeInlineRaw(xref, &Stream{Dict: dict, Raw: rows})
|
|
fmt.Fprintf(&w.buf, "startxref\n%d\n%%%%EOF\n", start)
|
|
if w.encrypt != nil && w.encrypt.err != nil {
|
|
w.note(w.encrypt.err)
|
|
}
|
|
if w.err != nil {
|
|
return nil, w.err
|
|
}
|
|
return w.buf.Bytes(), nil
|
|
}
|
|
|
|
// xrefRow renders one three-field row of a cross-reference stream.
|
|
func xrefRow(kind byte, f2, f3 int64) []byte {
|
|
return []byte{
|
|
kind,
|
|
byte(f2 >> 24), byte(f2 >> 16), byte(f2 >> 8), byte(f2),
|
|
byte(f3 >> 8), byte(f3),
|
|
}
|
|
}
|