Files
sift/vendor/github.com/go-pdfkit/extract/image.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

189 lines
5.5 KiB
Go

package extract
import (
"github.com/go-pdfkit/reader"
)
// An Image is a picture the page places, and where it puts it.
//
// What comes back is the picture as the file holds it — the bytes and the
// dictionary describing them — rather than pixels. A JPEG comes out as a JPEG,
// which is what anyone pulling pictures out of a document wants: re-encoding
// it would lose something for nothing. Turning the other kinds into pixels
// means reading their colour space, which is the renderer's work rather than
// this package's.
type Image struct {
// Name is what the page's resources call it, or empty for one written
// into the content stream.
Name string
// Width and Height are the picture's own, in samples.
Width, Height int
// X and Y are where its bottom left corner lands on the page, and
// DrawnWidth and DrawnHeight how large it is drawn, in points. A picture
// turned or skewed by the page still reports the box it covers.
X, Y float64
DrawnWidth, DrawnHeight float64
// Filter is the last filter still on the data: DCTDecode for a JPEG that
// comes out as one, JPXDecode for a JPEG 2000, and empty for data this
// has unfiltered into plain samples.
Filter reader.Name
// Data is the picture's bytes, with every filter this understands undone.
Data []byte
// Dict is the image's own dictionary, which says how to read the data:
// its colour space, how many bits a sample takes, and whether it is a
// mask.
Dict reader.Dict
// Inline reports a picture written into the content stream rather than
// held as a resource of its own.
Inline bool
}
// Images is every picture the page places, in the order it places them. The
// same picture placed twice comes back twice, since where it lands is part of
// what is being asked for.
func Images(d *reader.Document, page int) ([]Image, error) {
e, err := walk(d, page, true)
if err != nil {
return nil, err
}
return e.images, nil
}
// doXObject handles the Do operator: a form is walked into, and a picture is
// noted where it lands.
func (e *extractor) doXObject(g *state, operands []reader.Object, resources reader.Dict, depth int) {
if len(operands) == 0 {
return
}
name, ok := reader.ToName(operands[0])
if !ok {
return
}
xobjects, ok := e.doc.GetDict(resources, "XObject")
if !ok {
return
}
stream, ok := reader.ToStream(resolve(e.doc, xobjects.Get(name)))
if !ok {
return
}
switch sub, _ := reader.ToName(resolve(e.doc, stream.Dict.Get("Subtype"))); sub {
case "Form":
e.doForm(g, stream, resources, depth)
case "Image":
e.noteImage(g, string(name), stream.Dict, stream.Raw, false)
}
}
// doForm walks into a form, which is a piece of a page held apart and drawn
// where the page says.
func (e *extractor) doForm(g *state, stream *reader.Stream, resources reader.Dict, depth int) {
data, img, err := e.doc.DecodeStream(stream)
if err != nil || img != "" {
return
}
inner := *g
if m := floatArray(e.doc, stream.Dict.Get("Matrix")); len(m) >= 6 {
inner.ctm = matrix{m[0], m[1], m[2], m[3], m[4], m[5]}.mul(g.ctm)
}
own, ok := e.doc.GetDict(stream.Dict, "Resources")
if !ok {
own = resources
}
e.run(data, own, inner, depth+1)
}
// inlineImage notes a picture written into the content stream.
func (e *extractor) inlineImage(g *state, im *reader.InlineImage) {
if im == nil {
return
}
e.noteImage(g, "", im.Expanded(), im.Raw, true)
}
// noteImage works out where a picture lands and records it. A picture occupies
// the unit square of the transform in force, so its corners are where that
// square's are.
func (e *extractor) noteImage(g *state, name string, dict reader.Dict, raw []byte, inline bool) {
minX, minY, maxX, maxY := unitSquareBox(g.ctm)
img := Image{
Name: name,
Width: int(intOf(resolve(e.doc, dict.Get("Width")), 0)),
Height: int(intOf(resolve(e.doc, dict.Get("Height")), 0)),
X: minX,
Y: minY,
DrawnWidth: maxX - minX,
DrawnHeight: maxY - minY,
Dict: dict,
Inline: inline,
}
// A caller after the text is not after the pixels, and undoing a
// picture's filters to hand it nothing is the most expensive thing a page
// can be made to do.
if e.wantPictures {
data, filter, err := reader.Decode(dict, raw, e.doc.Resolver())
if err == nil {
img.Data, img.Filter = data, filter
} else {
img.Data = raw
}
}
e.images = append(e.images, img)
}
// unitSquareBox is the box the unit square covers once the transform has had
// its way with it.
func unitSquareBox(m matrix) (minX, minY, maxX, maxY float64) {
first := true
for _, c := range [][2]float64{{0, 0}, {1, 0}, {1, 1}, {0, 1}} {
x, y := m.apply(c[0], c[1])
if first {
minX, minY, maxX, maxY = x, y, x, y
first = false
continue
}
minX, maxX = smaller(minX, x), larger(maxX, x)
minY, maxY = smaller(minY, y), larger(maxY, y)
}
return minX, minY, maxX, maxY
}
func smaller(a, b float64) float64 {
if a < b {
return a
}
return b
}
func larger(a, b float64) float64 {
if a > b {
return a
}
return b
}
// intOf reads a whole number, or gives a default.
func intOf(o reader.Object, def int64) int64 {
if v, ok := reader.ToInt(o); ok {
return v
}
return def
}
// floatArray reads an array of numbers, or nothing.
func floatArray(d *reader.Document, o reader.Object) []float64 {
arr, ok := reader.ToArray(resolve(d, o))
if !ok {
return nil
}
out := make([]float64, 0, len(arr))
for _, e := range arr {
v, ok := reader.ToFloat(resolve(d, e))
if !ok {
return nil
}
out = append(out, v)
}
return out
}