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
17 changes: 17 additions & 0 deletions images.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ type Image struct {
// JPXDecode, JBIG2Decode, CCITTFaxDecode — and is empty when the bytes
// were samples that no image codec had to read.
Filter string
// Decoded says the dictionary carried a /Decode array, which maps the
// stored samples onto the range the colour space wants and is applied
// here. It is worth knowing about because it is the one thing that makes
// this picture differ from the same picture as another tool EXTRACTS it:
// pdfimages writes the samples as stored, so a /Decode of [1 0] on a
// one-bit mask makes the two exact complements of each other, and every
// pixel differs for a reason that is not a disagreement.
Decoded bool
// Stencil says the picture is a one-bit mask, which carries no colours of
// its own: it paints whatever colour is in force through its own shape.
// Its Pic has that shape in the alpha channel and black everywhere else,
Expand Down Expand Up @@ -114,6 +122,7 @@ func (r *renderer) decoded(name string, st *reader.Stream, res reader.Dict) []Im
out = append(out, Image{
Name: name,
Filter: imageFilterOf(r.doc, st),
Decoded: r.hasDecodeArray(st.Dict),
Stencil: bool(stencil),
Pic: &raster.Image{W: s.w, H: s.h, Pix: s.pix},
})
Expand All @@ -133,13 +142,21 @@ func (r *renderer) decoded(name string, st *reader.Stream, res reader.Dict) []Im
out = append(out, Image{
Name: name + "/" + string(key),
Filter: imageFilterOf(r.doc, ms),
Decoded: r.hasDecodeArray(ms.Dict),
Stencil: true,
Pic: &raster.Image{W: s.w, H: s.h, Pix: s.pix},
})
}
return out
}

// hasDecodeArray says whether a picture's samples were mapped through a
// /Decode array on the way out.
func (r *renderer) hasDecodeArray(dict reader.Dict) bool {
_, ok := reader.ToArray(resolve(r.doc, dict.Get("Decode")))
return ok
}

// imageFilterOf names the image format a stream's filter chain stopped at, and
// is empty when the chain ran to samples.
func imageFilterOf(d *reader.Document, st *reader.Stream) string {
Expand Down
61 changes: 61 additions & 0 deletions images_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,3 +331,64 @@ func TestAMaskNothingCanDecodeIsLeftOut(t *testing.T) {
t.Errorf("a mask of no size came back as a picture: %+v", got)
}
}

func TestAPictureSaysWhetherADecodeArrayShapedIt(t *testing.T) {
// A /Decode of [1 0] on a one-bit mask makes this picture and the same
// picture as pdfimages EXTRACTS it exact complements of each other: every
// pixel differs, and none of it is a disagreement. Whoever compares the
// two has to be able to tell that case apart from a real one.
for _, tc := range []struct {
name string
extra reader.Dict
want bool
}{
{"no array at all", nil, false},
{"an array", reader.Dict{"Decode": reader.Array{reader.Integer(1), reader.Integer(0)}}, true},
{"something that is not an array", reader.Dict{"Decode": reader.Integer(1)}, false},
} {
t.Run(tc.name, func(t *testing.T) {
dict := reader.Dict{
"Width": reader.Integer(2), "Height": reader.Integer(1),
"ColorSpace": reader.Name("DeviceGray"), "BitsPerComponent": reader.Integer(8),
}
for k, v := range tc.extra {
dict[k] = v
}
d := pageWithImage(t, dict, []byte{0x00, 0xff}, "")
got, err := Images(d, 1)
if err != nil {
t.Fatal(err)
}
if len(got) != 1 || got[0].Decoded != tc.want {
t.Errorf("got %+v, want Decoded=%v", got, tc.want)
}
})
}
}

func TestAMaskSaysSoToo(t *testing.T) {
// The mask is where this actually happens: 22 of the 54 JBIG2 soft masks
// in a corpus of scanned medical documents carry /Decode [1 0], and not
// one of the 194 stencils does.
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),
"Decode": reader.Array{reader.Integer(1), reader.Integer(0)},
}, Raw: []byte{0x00, 0xff}})
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 || got[0].Decoded || !got[1].Decoded {
t.Errorf("got %+v", got)
}
}
Loading