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
39 changes: 27 additions & 12 deletions image.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,34 @@ func unitSquareBounds(m geometry.Matrix, w, h int) image.Rectangle {
// name a grid it would take the whole machine to hold.
const maxImagePixels = 64 << 20

// decodeImage turns an image XObject into a grid of colours.
// decodeImage turns an image XObject into the grid of colours a page draws:
// the picture the codec read, with whatever mask it names applied to it.
func (r *renderer) decodeImage(dict reader.Dict, raw []byte, resources reader.Dict) *sampled {
out := r.decodeBase(dict, raw, resources)
if out == nil {
return nil
}
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
}

// decodeBase is the picture the codec read, with no mask applied.
//
// It is separate because the two questions are different. What a page draws
// is the composited picture; what a codec produced is this one, and comparing
// a codec against another implementation means comparing THIS, since the other
// implementation hands its masks back separately too. Measured the wrong way
// round, 21 of 22 JPEG 2000 pictures in a corpus of scanned pages looked
// wrong, and 11 of them differed only by an /SMask that had been applied to
// one side and not the other.
func (r *renderer) decodeBase(dict reader.Dict, raw []byte, resources reader.Dict) *sampled {
w := int(intOr(resolve(r.doc, dict.Get("Width")), 0))
h := int(intOr(resolve(r.doc, dict.Get("Height")), 0))
if w <= 0 || h <= 0 || w*h > maxImagePixels {
Expand Down Expand Up @@ -152,17 +178,6 @@ func (r *renderer) decodeImage(dict reader.Dict, raw []byte, resources reader.Di
// not drawn rather than drawn wrong. Every filter the reader hands back
// unread has an arm above, so the first of those is a case this cannot
// reach today and the check is here for the second.
if out == nil {
return nil
}
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
44 changes: 40 additions & 4 deletions images.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ type Image struct {
// mattered here — a release went out drawing scanned pages dark because "ink
// appeared" was measured instead of "the right ink".
//
// What comes back is what each CODEC produced, not what the page draws. A
// picture that names a mask is returned unmasked, and the mask is returned
// beside it as its own entry, named for the key that named it. That is what
// pdfimages does, and it is the only way the two can be compared: applying a
// mask to one side and not the other made 21 of 22 JPEG 2000 pictures in a
// corpus of scanned pages look wrong, when 11 of them differed by nothing but
// the /SMask.
//
// It also means a picture whose mask cannot be read still comes back. [Page]
// declines to draw that one, because how much of it shows is unknown — but the
// codec read it, and this is about the codec.
//
// A picture nothing here can decode is left out rather than returned empty,
// which is the same answer [Page] gives by not drawing it. Inline images —
// the ones written into the content stream — are not returned: they belong to
Expand Down Expand Up @@ -89,10 +101,15 @@ func (r *renderer) imagesIn(res reader.Dict, depth int) []Image {
if sub != "Image" {
continue
}
s := r.decodeImage(st.Dict, st.Raw, res)
if s == nil {
continue
}
out = append(out, r.decoded(name, st, res)...)
}
return out
}

// decoded reads one image XObject and the mask it names, if any.
func (r *renderer) decoded(name string, st *reader.Stream, res reader.Dict) []Image {
var out []Image
if s := r.decodeBase(st.Dict, st.Raw, res); s != nil {
stencil, _ := reader.ToBool(resolve(r.doc, st.Dict.Get("ImageMask")))
out = append(out, Image{
Name: name,
Expand All @@ -101,6 +118,25 @@ func (r *renderer) imagesIn(res reader.Dict, depth int) []Image {
Pic: &raster.Image{W: s.w, H: s.h, Pix: s.pix},
})
}
// A mask is a picture in its own right, stored in its own filter, and it
// is very often the one that is wrong: JBIG2 is almost never a page's
// content and almost always its mask.
for _, key := range []reader.Name{"SMask", "Mask"} {
ms, ok := reader.ToStream(resolve(r.doc, st.Dict.Get(key)))
if !ok {
continue
}
s := r.decodeBase(ms.Dict, ms.Raw, res)
if s == nil {
continue
}
out = append(out, Image{
Name: name + "/" + string(key),
Filter: imageFilterOf(r.doc, ms),
Stencil: true,
Pic: &raster.Image{W: s.w, H: s.h, Pix: s.pix},
})
}
return out
}

Expand Down
111 changes: 111 additions & 0 deletions images_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,114 @@ func TestAPageThatIsNotThereHasNoPictures(t *testing.T) {
t.Error("page nine of a one-page document came back without complaint")
}
}

func TestAMaskComesBackBesideThePictureItShapes(t *testing.T) {
// A picture that names a mask is returned unmasked, with the mask beside
// it. Applying it to one side and not the other made 21 of 22 JPEG 2000
// pictures in a corpus of scanned pages look wrong, when 11 of them
// differed by nothing but the /SMask.
d := pageWithResources(t, func(w *reader.Writer) reader.Dict {
mask := w.Add(&reader.Stream{Dict: reader.Dict{
"Type": reader.Name("XObject"), "Subtype": reader.Name("Image"),
"Width": reader.Integer(2), "Height": reader.Integer(1),
"ColorSpace": reader.Name("DeviceGray"), "BitsPerComponent": reader.Integer(8),
}, Raw: []byte{0x00, 0x00}})
return reader.Dict{"XObject": reader.Dict{"I": w.Add(&reader.Stream{Dict: reader.Dict{
"Type": reader.Name("XObject"), "Subtype": reader.Name("Image"),
"Width": reader.Integer(2), "Height": reader.Integer(1),
"ColorSpace": reader.Name("DeviceGray"), "BitsPerComponent": reader.Integer(8),
"SMask": mask,
}, Raw: []byte{0x00, 0xff}})}}
})
got, err := Images(d, 1)
if err != nil {
t.Fatal(err)
}
if len(got) != 2 {
t.Fatalf("%d pictures, want the picture and its mask", len(got))
}
// The mask says nothing shows. The picture must come back anyway, opaque:
// that is the codec's answer, and the codec is what this is about.
if got[0].Name != "I" || got[0].Pic.Pix[3] != 255 {
t.Errorf("the picture came back masked: %+v alpha %d", got[0], got[0].Pic.Pix[3])
}
if got[1].Name != "I/SMask" || !got[1].Stencil {
t.Errorf("the mask came back as %+v", got[1])
}
}

func TestAPictureWhoseMaskCannotBeReadStillComesBack(t *testing.T) {
// Page declines to draw this one, because how much of it shows is
// unknown. The codec read it, and this is about the codec.
d := pageWithResources(t, func(w *reader.Writer) reader.Dict {
bad := w.Add(&reader.Stream{Dict: reader.Dict{
"Type": reader.Name("XObject"), "Subtype": reader.Name("Image"),
"Width": reader.Integer(2), "Height": reader.Integer(1),
"Filter": reader.Name("JPXDecode"),
}, Raw: []byte{0xff, 0x4f, 0xff, 0x51, 0, 1}})
return reader.Dict{"XObject": reader.Dict{"I": w.Add(&reader.Stream{Dict: reader.Dict{
"Type": reader.Name("XObject"), "Subtype": reader.Name("Image"),
"Width": reader.Integer(2), "Height": reader.Integer(1),
"ColorSpace": reader.Name("DeviceGray"), "BitsPerComponent": reader.Integer(8),
"SMask": bad,
}, Raw: []byte{0x00, 0xff}})}}
})
got, err := Images(d, 1)
if err != nil {
t.Fatal(err)
}
if len(got) != 1 || got[0].Name != "I" {
t.Fatalf("got %+v", got)
}
// And the page still refuses to draw it, which is the other half of the
// rule and must not have moved.
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 an image whose mask cannot be read", ink)
}
}

func TestAMaskThatIsNotAPictureIsNotOne(t *testing.T) {
// /Mask may be an array of colour ranges rather than a stream, which is a
// different mechanism and not a picture to hand back.
d := pageWithResources(t, func(w *reader.Writer) reader.Dict {
return reader.Dict{"XObject": reader.Dict{"I": w.Add(&reader.Stream{Dict: reader.Dict{
"Type": reader.Name("XObject"), "Subtype": reader.Name("Image"),
"Width": reader.Integer(2), "Height": reader.Integer(1),
"ColorSpace": reader.Name("DeviceGray"), "BitsPerComponent": reader.Integer(8),
"Mask": reader.Array{reader.Integer(0), reader.Integer(0)},
}, Raw: []byte{0x00, 0xff}})}}
})
got, err := Images(d, 1)
if err != nil {
t.Fatal(err)
}
if len(got) != 1 {
t.Errorf("a colour-key mask came back as a picture: %+v", got)
}
}

func TestAMaskNothingCanDecodeIsLeftOut(t *testing.T) {
d := pageWithResources(t, func(w *reader.Writer) reader.Dict {
bad := w.Add(&reader.Stream{Dict: reader.Dict{
"Type": reader.Name("XObject"), "Subtype": reader.Name("Image"),
"Width": reader.Integer(0), "Height": reader.Integer(0),
}, Raw: []byte{}})
return reader.Dict{"XObject": reader.Dict{"I": w.Add(&reader.Stream{Dict: reader.Dict{
"Type": reader.Name("XObject"), "Subtype": reader.Name("Image"),
"Width": reader.Integer(2), "Height": reader.Integer(1),
"ColorSpace": reader.Name("DeviceGray"), "BitsPerComponent": reader.Integer(8),
"Mask": bad,
}, Raw: []byte{0x00, 0xff}})}}
})
got, err := Images(d, 1)
if err != nil {
t.Fatal(err)
}
if len(got) != 1 {
t.Errorf("a mask of no size came back as a picture: %+v", got)
}
}
Loading