Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion annots.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func (r *renderer) appearanceOf(dict reader.Dict) (*reader.Stream, bool) {
// rectangle — which is what makes a stamp put down at an angle land inside the
// box that was drawn for it.
func (r *renderer) drawAppearance(base gstate, stream *reader.Stream, rect [4]float64) {
content, img, err := r.doc.DecodeStream(stream)
content, img, err := r.salvaged(stream)
if err != nil || img != "" {
return
}
Expand Down
2 changes: 1 addition & 1 deletion calculator.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func (r *renderer) readCalculator(base functionBase, dict reader.Dict, stream *r
if stream == nil || len(base.rng) < 2 {
return nil
}
data, img, err := r.doc.DecodeStream(stream)
data, img, err := r.salvaged(stream)
if err != nil || img != "" {
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion funckinds.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func (r *renderer) readSampled(base functionBase, dict reader.Dict, stream *read
if stream == nil || len(base.rng) < 2 {
return nil
}
data, img, err := r.doc.DecodeStream(stream)
data, img, err := r.salvaged(stream)
if err != nil || img != "" {
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
github.com/go-gfx/gfx v0.10.0
github.com/go-opentype/fonts v0.9.0
github.com/go-opentype/opentype v0.10.0
github.com/go-pdfkit/reader v0.5.0
github.com/go-pdfkit/reader v0.6.0
)

require github.com/go-pdfkit/pdffont v0.3.0
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ github.com/go-opentype/opentype v0.10.0 h1:cZVMZ3RVkcijXmxlmpqyVkjlNc33aSB2ugKVW
github.com/go-opentype/opentype v0.10.0/go.mod h1:AOixevJf7XQaH7+WG+OMIOZEbYPXfMqklVk26Y6YTUU=
github.com/go-pdfkit/pdffont v0.3.0 h1:G5DKcAmsZJ0e17QhSrcUaL7PKEXKjUx4P/iGMl7bAbI=
github.com/go-pdfkit/pdffont v0.3.0/go.mod h1:bfmNLna1l1CljNX/Utg55YzFylovfmI7sJnvgA3bzKI=
github.com/go-pdfkit/reader v0.5.0 h1:DaJ5C6eKXPRG9Cdu+olA/0KLmsQgyHEC1coteQQuBxU=
github.com/go-pdfkit/reader v0.5.0/go.mod h1:fQFOVfCMUui1AdvD4qhimdyvvNr9KvvJ1S7IuKZjyV8=
github.com/go-pdfkit/reader v0.6.0 h1:KAabNOYUcTlZlNBTbG9bEhWP1NiZhjuhzUdavdTdfes=
github.com/go-pdfkit/reader v0.6.0/go.mod h1:fQFOVfCMUui1AdvD4qhimdyvvNr9KvvJ1S7IuKZjyV8=
8 changes: 6 additions & 2 deletions image.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,12 @@ func (r *renderer) decodeImage(dict reader.Dict, raw []byte, resources reader.Di
if w <= 0 || h <= 0 || w*h > maxImagePixels {
return nil
}
data, imageFilter, err := reader.Decode(dict, raw, r.doc.Resolver())
if err != nil {
// An image whose filter chain broke part way gives the rows it managed,
// which is what a viewer shows for a truncated scan. Bytes no filter
// decoded are in a field this cannot reach.
dec := reader.DecodeRecovering(dict, raw, r.doc.Resolver())
data, imageFilter := dec.Data, dec.Image
if len(data) == 0 {
return nil
}
if mask, ok := reader.ToBool(resolve(r.doc, dict.Get("ImageMask"))); ok && mask {
Expand Down
2 changes: 1 addition & 1 deletion mesh.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const patchSteps = 16

// readMesh reads the vertices or patches a mesh shading's stream holds.
func (r *renderer) readMesh(sh *shading, stream *reader.Stream) *mesh {
data, img, err := r.doc.DecodeStream(stream)
data, img, err := r.salvaged(stream)
if err != nil || img != "" {
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion pattern.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (r *renderer) readPattern(name reader.Name, resources reader.Dict) *pattern
if !isStream {
return nil
}
data, img, err := r.doc.DecodeStream(stream)
data, img, err := r.salvaged(stream)
if err != nil || img != "" {
return nil
}
Expand Down
23 changes: 20 additions & 3 deletions render.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,27 @@ func Page(d *reader.Document, i int, opt Options) (*raster.Image, error) {
img := raster.New(w, h)
fill(img, opt.background())

content, err := d.PageContent(i)
if err != nil {
return nil, err
// A page's content is drawn as far as it decoded. 263 streams in 212 of
// the 1 633 real forms cannot be decoded cleanly, and a strict read hands
// back nothing for all of them; this package already draws a page as far as
// it got when its time runs out, and a damaged stream is the same
// situation arriving another way.
//
// Nothing that no filter decoded is ever run: the reader keeps those bytes
// in Undecoded, set instead of Data rather than beside it, so taking Data
// cannot reach them. A page whose /Contents is filtered as an image — a
// JPEG where operators should be — arrives here with Data empty and a
// cause, and is refused rather than run.
// The error is dropped: PageContentDecoded returns one only when the page
// itself cannot be read, and it was read a few lines above. A /Contents
// that points at an object the file does not hold resolves to nothing
// rather than to an error, and a page with no content is a blank page —
// which is what this drew before and what other readers draw.
dec, _ := d.PageContentDecoded(i)
if len(dec.Data) == 0 && dec.Cause != nil {
return nil, dec.Cause
}
content := dec.Data
resources, _ := d.GetDict(page, "Resources")
r := &renderer{doc: d, img: img, fonts: map[int]*pdfFont{}, softMasks: map[softMaskKey][]uint8{}}
if !opt.AllLayers {
Expand Down
85 changes: 85 additions & 0 deletions salvage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package render

import (
"bytes"
"compress/zlib"
"testing"

"github.com/go-pdfkit/reader"
)

// truncatedFlate compresses s and then cuts the result, so inflating it yields
// a prefix of s and then fails — which is what a damaged stream in a real file
// looks like.
func truncatedFlate(t *testing.T, s string, keep int) []byte {
t.Helper()
var buf bytes.Buffer
zw := zlib.NewWriter(&buf)
if _, err := zw.Write([]byte(s)); err != nil {
t.Fatal(err)
}
if err := zw.Close(); err != nil {
t.Fatal(err)
}
b := buf.Bytes()
if keep >= len(b) {
t.Fatalf("keeping %d of %d bytes cuts nothing", keep, len(b))
}
return b[:keep]
}

func TestAPageWhoseContentIsDamagedIsDrawnAsFarAsItDecoded(t *testing.T) {
// A page of many squares, compressed and then cut. The first squares are
// in the part that inflates; the rest are not. Drawing nothing at all
// would be the old behaviour and is the wrong one: 263 streams in 212 of
// the 1 633 real forms cannot be decoded cleanly.
content := "0 g"
for i := 0; i < 200; i++ {
content += " 0 0 10 10 re f 10 0 10 10 re f 20 0 10 10 re f"
}
raw := truncatedFlate(t, content, 40)

w := reader.NewWriter("1.7")
pagesRef := w.Reserve()
page := w.Add(reader.Dict{"Type": reader.Name("Page"), "Parent": pagesRef,
"MediaBox": nums(0, 0, 40, 20),
"Contents": w.Add(&reader.Stream{
Dict: reader.Dict{"Filter": reader.Name("FlateDecode")}, Raw: raw})})
w.Put(pagesRef, reader.Dict{"Type": reader.Name("Pages"),
"Kids": reader.Array{page}, "Count": reader.Integer(1)})
out, err := w.Finish(reader.Dict{"Root": w.Add(reader.Dict{
"Type": reader.Name("Catalog"), "Pages": pagesRef})})
if err != nil {
t.Fatal(err)
}
d, err := reader.Open(out)
if err != nil {
t.Fatal(err)
}
img, err := Page(d, 1, Options{Scale: 1})
if err != nil {
t.Fatalf("a damaged page came back as an error: %v", err)
}
if inked(img) == 0 {
t.Error("a damaged page drew nothing at all")
}
}

func TestAFormWhoseFilterIsUnknownIsNotDrawn(t *testing.T) {
// A filter nothing can apply leaves no bytes any filter decoded, so there
// is nothing to draw — and the encoded bytes must not be run as operators.
d := layered(t, nil, func(w *reader.Writer, g []reader.Object) (string, reader.Array, reader.Dict) {
form := w.Add(&reader.Stream{Dict: reader.Dict{
"Type": reader.Name("XObject"), "Subtype": reader.Name("Form"),
"BBox": nums(0, 0, 100, 100), "Filter": reader.Name("NoSuchDecode"),
}, Raw: []byte("0 g 0 0 100 100 re f")})
return "/F Do", nil, reader.Dict{"XObject": reader.Dict{"F": form}}
}, 0)
img, err := Page(d, 1, Options{Scale: 1})
if err != nil {
t.Fatal(err)
}
if ink := inked(img); ink != 0 {
t.Errorf("%d pixels drawn from a stream no filter could decode", ink)
}
}
2 changes: 1 addition & 1 deletion softmask.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func (r *renderer) readSoftMask(entry reader.Object, g *gstate, resources reader
// drawSoftMask draws the mask's form on paper of its own and reads the mask
// off what comes out.
func (r *renderer) drawSoftMask(dict reader.Dict, form *reader.Stream, kind reader.Name, g *gstate, resources reader.Dict) []uint8 {
content, img, err := r.doc.DecodeStream(form)
content, img, err := r.salvaged(form)
if err != nil || img != "" {
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion space.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ func (r *renderer) lookupTable(o reader.Object) []byte {
return s
}
if stream, ok := reader.ToStream(resolved); ok {
data, img, err := r.doc.DecodeStream(stream)
data, img, err := r.salvaged(stream)
if err == nil && img == "" {
return data
}
Expand Down
21 changes: 21 additions & 0 deletions state.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,24 @@ func (r *renderer) narrow(g *gstate, cov []float64, ox, oy, w, h int, ok bool) {
}
g.clip = next
}

// salvaged decodes a stream and keeps whatever the filter chain managed to
// produce, which is not the same as keeping whatever bytes are left.
//
// 263 streams in 212 of the 1 633 real forms — 13.0% of them — cannot be
// decoded cleanly, and a strict decode hands back nothing for all 263. This
// package already draws a page as far as it got when its time runs out, and a
// damaged stream is the same situation arriving a different way: half a figure
// is worth more to somebody reading a form than none of it.
//
// What must never happen is bytes no filter decoded being painted as though
// they were content. reader.Decoded keeps those in a separate field, set
// instead of Data rather than beside it, so this reads Data and cannot reach
// them. That is a type doing the work, not a flag anyone has to remember.
func (r *renderer) salvaged(stream *reader.Stream) ([]byte, reader.Name, error) {
dec := r.doc.DecodeStreamRecovering(stream)
if len(dec.Data) == 0 && dec.Cause != nil {
return nil, "", dec.Cause
}
return dec.Data, dec.Image, nil
}
2 changes: 1 addition & 1 deletion text.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ func (r *renderer) drawType3Glyph(g *gstate, f *pdfFont, code int, resources rea
if !ok {
return
}
content, img, err := r.doc.DecodeStream(stream)
content, img, err := r.salvaged(stream)
if err != nil || img != "" {
return
}
Expand Down
2 changes: 1 addition & 1 deletion xobject.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (r *renderer) drawXObject(g *gstate, operands []reader.Object, resources re
// drawForm runs a form's own content inside the state that drew it, under its
// own matrix and its own bounding box.
func (r *renderer) drawForm(g *gstate, stream *reader.Stream, parent reader.Dict) {
content, img, err := r.doc.DecodeStream(stream)
content, img, err := r.salvaged(stream)
if err != nil || img != "" {
return
}
Expand Down
Loading