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
34 changes: 25 additions & 9 deletions image.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,14 @@ func (r *renderer) decodeImage(dict reader.Dict, raw []byte, resources reader.Di
if out == nil {
return nil
}
r.applyTransparency(out, dict, resources)
if !r.applyTransparency(out, dict, resources) {
// A mask was named and could not be read, so how much of this image
// shows is unknown. Drawing it whole is the worst of the three
// answers: it is how a scanned page's high-resolution ink layer, which
// is meant to show through a stencil, ends up painted over the page as
// a solid dark rectangle.
return nil
}
return out
}

Expand Down Expand Up @@ -374,38 +381,46 @@ func (r *renderer) decodeInverts(dict reader.Dict) bool {
// applyTransparency reads whichever of the two ways a PDF says which parts of
// an image are see-through: a soft mask of its own, or a range of colours to
// treat as absent.
func (r *renderer) applyTransparency(s *sampled, dict reader.Dict, resources reader.Dict) {
// It reports whether the image may be drawn. A mask that is NAMED and cannot be
// READ means how much of the image shows is unknown, and an image drawn whole
// when most of it was meant to be invisible is worse than one not drawn: that
// is exactly how a scanned page goes wrong. Such a page is a low-resolution
// colour background with a high-resolution bitonal ink layer over it, and the
// ink layer is a dark rectangle masked by a JBIG2 stencil. Without the stencil
// it is a dark rectangle over the whole page.
func (r *renderer) applyTransparency(s *sampled, dict reader.Dict, resources reader.Dict) bool {
if stream, ok := reader.ToStream(resolve(r.doc, dict.Get("SMask"))); ok {
r.applySoftMask(s, stream, resources)
return
return r.applySoftMask(s, stream, resources)
}
maskEntry := resolve(r.doc, dict.Get("Mask"))
if stream, ok := reader.ToStream(maskEntry); ok {
r.applyStencilMask(s, stream, resources)
return r.applyStencilMask(s, stream, resources)
}
return true
}

// applySoftMask reads a grey image whose levels say how much of each pixel
// shows.
func (r *renderer) applySoftMask(s *sampled, stream *reader.Stream, resources reader.Dict) {
func (r *renderer) applySoftMask(s *sampled, stream *reader.Stream, resources reader.Dict) bool {
mask := r.decodeImage(stream.Dict, stream.Raw, resources)
if mask == nil {
return
return false
}
for y := 0; y < s.h; y++ {
for x := 0; x < s.w; x++ {
m := mask.at(x*mask.w/s.w, y*mask.h/s.h)
s.pix[(y*s.w+x)*4+3] = m.R
}
}
return true
}

// applyStencilMask reads a one-bit image whose set pixels are the ones to
// leave out.
func (r *renderer) applyStencilMask(s *sampled, stream *reader.Stream, resources reader.Dict) {
func (r *renderer) applyStencilMask(s *sampled, stream *reader.Stream, resources reader.Dict) bool {
mask := r.decodeImage(stream.Dict, stream.Raw, resources)
if mask == nil {
return
return false
}
for y := 0; y < s.h; y++ {
for x := 0; x < s.w; x++ {
Expand All @@ -417,6 +432,7 @@ func (r *renderer) applyStencilMask(s *sampled, stream *reader.Stream, resources
}
}
}
return true
}

// intOr reads an integer, or gives a default.
Expand Down
23 changes: 19 additions & 4 deletions image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,13 +419,28 @@ func imageWithMask(t *testing.T, entry reader.Name, maskDict reader.Dict, maskDa
return d
}

func TestAMaskThatCannotBeRead(t *testing.T) {
// Neither kind of mask, when it cannot be read, may take the image with
// it: the image is drawn as it stands.
func TestAMaskThatCannotBeReadTakesTheImageWithIt(t *testing.T) {
// This reverses what this package used to do, and the corpus is why.
//
// It used to draw the image as it stood, on the reasoning that an
// unreadable mask should not cost you the picture. That is right for a
// photograph with a soft edge and catastrophic for a scanned page, which
// is a low-resolution colour background with a HIGH-RESOLUTION BITONAL INK
// LAYER over it — and that ink layer is a dark rectangle whose shape comes
// entirely from a JBIG2 stencil. Drawn without the stencil it is a dark
// rectangle over the whole page.
//
// Measured against poppler over 250 scanned medical documents, the median
// page had 97% of its pixels wrong that way. Not drawing it leaves the
// background showing, which is what the page mostly is.
for _, entry := range []reader.Name{"SMask", "Mask"} {
d := imageWithMask(t, entry, reader.Dict{
"Width": reader.Integer(0), "Height": reader.Integer(0)}, nil)
wantBlack(t, draw(t, d, Options{}), 10, 10)
img := draw(t, d, Options{})
if !isWhite(img, 10, 10) {
t.Errorf("/%s: the image was drawn although how much of it shows "+
"is unknown: %s", entry, pixel(img, 10, 10))
}
}
}

Expand Down
Loading