From bdba1096bf3a4d4567ed0995c961b1ff4fe70a4b Mon Sep 17 00:00:00 2001 From: tannevaled Date: Fri, 28 Aug 2026 20:29:09 +0200 Subject: [PATCH] Read the ink layer of a scanned page, and stop showing its negative MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things, because the first exposed the second and neither is right alone. JBIG2 is decoded. A modern scanned page is two images: a low-resolution colour background stored as JPEG 2000, and a high-resolution bitonal ink layer over it stored as JBIG2. We could read the background and not the ink. Counting filters by what they encode as page CONTENT put JBIG2 in 20 documents of 3 217, which is why it had been set aside; counting the images it SHAPES put it in 4 089, because it is almost never the content — it is the /Mask. A stencil mask paints where its sample is 0, and applyStencilMask was clearing the alpha where the stencil painted rather than where it did not. Every explicit mask in the corpus was therefore showing the exact complement of itself: on a scanned page, the ink is the only part that was hidden. The test that covered it asserted the same inversion, so the suite was green. Asked which half of a two-colour page a mask of eight 0 bits and eight 1 bits paints, poppler answers the 0 half; so does this now, for a raw mask and a JBIG2 one alike. The inversion had been invisible because the two halves cancelled: masks that could not be decoded took their image with them (#26), and the ones that could were drawn inside out. Decoding JBIG2 without fixing the mask made 5 of 25 medical scans worse and 2 of them catastrophic — 98% of pixels wrong on the worst — which is how the second defect was found. Measured against poppler, first page of 25 medical scans: improved 11 unchanged 14 worse 0 mean differing pixels 0.0083 -> 0.0004 worst page 0.1087 -> 0.0004 No decoder is written here. The reference is github.com/dkrisman/gobig2, chosen by decoding 403 JBIG2 masks taken from real scans and comparing every pixel against poppler's own decoder: it was exact on all 396 it accepted, where the candidate that decoded the most was exact on 9%. Its seven refusals are a resource budget, not a defect, and this takes it at its default: a stream it declines is a stream not drawn, which is the rule the rest of the file follows. gobig2's limits are process-global, so they are not raised from inside a library. 100% statement coverage, go vet and -race clean, nine cross-compile targets, CGO_ENABLED=0. Co-Authored-By: Claude Opus 5 --- go.mod | 1 + go.sum | 2 + image.go | 45 ++++++--- image_test.go | 22 +++-- jbig2.go | 103 ++++++++++++++++++++ jbig2_test.go | 260 ++++++++++++++++++++++++++++++++++++++++++++++++++ jpx_test.go | 37 ------- 7 files changed, 412 insertions(+), 58 deletions(-) create mode 100644 jbig2.go create mode 100644 jbig2_test.go diff --git a/go.mod b/go.mod index 9dc94b6..cc5c5de 100644 --- a/go.mod +++ b/go.mod @@ -16,5 +16,6 @@ require ( require ( github.com/ajroetker/go-highway v0.0.4 // indirect + github.com/dkrisman/gobig2 v0.0.0-20260513123937-51e39052fde6 golang.org/x/sys v0.47.0 // indirect ) diff --git a/go.sum b/go.sum index 6bf10ff..8a46a32 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/ajroetker/go-highway v0.0.4 h1:RDQo+9OhTXI6BFctLo+5gYpHNbb92VYJ0ObnR4 github.com/ajroetker/go-highway v0.0.4/go.mod h1:C/zYPNSSpOaraejY89FUTZTyQNEhi5+rEbU0LjlqJeU= github.com/ajroetker/go-jpeg2000 v0.0.2 h1:ni8brffZrci4Kacx3nM5d92ipmTDfak84KgHYi6IxFw= github.com/ajroetker/go-jpeg2000 v0.0.2/go.mod h1:7ld88W47lZy0x8gRQesRGAonDPOpr6ev8rckjCAfbzE= +github.com/dkrisman/gobig2 v0.0.0-20260513123937-51e39052fde6 h1:hHPgbpPdcaSXtpLb9NMEUJPxsZ0cxGtGHwmvKGI36NA= +github.com/dkrisman/gobig2 v0.0.0-20260513123937-51e39052fde6/go.mod h1:2Ij0rpAVy1tZ+PQ9FM1jCpnVo/OOCMCD6rLtSp4JkEI= github.com/go-gfx/gfx v0.12.0 h1:zBaYHahRwM6yYr86ifRelX132ZjhN5hZNr/uW9dpIzQ= github.com/go-gfx/gfx v0.12.0/go.mod h1:DpRUcQrlLZH02CSB23J0iEk1F7mQH8w9WMF60xQBxDU= github.com/go-opentype/fonts v0.9.0 h1:slB6OB3riLyUPrOxqXe0s6/AzdenF1TDvCN8N87hhQk= diff --git a/image.go b/image.go index e06d3d6..db6a870 100644 --- a/image.go +++ b/image.go @@ -113,6 +113,15 @@ func (r *renderer) decodeImage(dict reader.Dict, raw []byte, resources reader.Di if len(data) == 0 { return nil } + if imageFilter == "JBIG2Decode" { + // Decoded here rather than in the switch below because a JBIG2 stream + // is far more often a stencil than an image, and a stencil never + // reaches that switch. + if data = r.decodeJBIG2(dict, data, w, h); data == nil { + return nil + } + imageFilter = "" + } if mask, ok := reader.ToBool(resolve(r.doc, dict.Get("ImageMask"))); ok && mask { // A stencil is one bit a pixel, so the bytes have to be samples. When // the filter chain stopped at an image format nothing here decodes, @@ -120,11 +129,11 @@ func (r *renderer) decodeImage(dict reader.Dict, raw []byte, resources reader.Di // noise through the shape of nothing. // // 273 of the image masks in the 1 633 real forms carry an encoded - // filter. 236 of them were faxes, which the reader now decodes; the - // nine that remain are JBIG2, and until something decodes those the - // honest answer is not to draw them. That is the rule the rest of this - // function already follows: "the image is not drawn rather than drawn - // wrong". + // filter. 236 of them were faxes and nine were JBIG2, both of which + // are decoded before this point. What is left is a format nothing + // here reads, and the honest answer is not to draw it. That is the + // rule the rest of this function already follows: "the image is not + // drawn rather than drawn wrong". if imageFilter != "" { return nil } @@ -138,11 +147,11 @@ func (r *renderer) decodeImage(dict reader.Dict, raw []byte, resources reader.Di out = decodeJPEG(data, w, h, r.decodeInverts(dict)) case "JPXDecode": out = decodeJPX(data, w, h) - default: - // A format nothing here can decode: the image is not drawn rather than - // drawn wrong. - return nil } + // No arm ran, or the one that ran could not read its bytes: the image is + // 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 } @@ -415,8 +424,18 @@ func (r *renderer) applySoftMask(s *sampled, stream *reader.Stream, resources re return true } -// applyStencilMask reads a one-bit image whose set pixels are the ones to -// leave out. +// applyStencilMask reads a one-bit image that says which parts of this one are +// painted. +// +// A mask sample of 0 means PAINT. The bit and the coverage run opposite ways, +// which is the whole difficulty: decodeImage returns a stencil whose alpha is +// set where the sample is 0, so alpha here already means "painted" and what has +// to be cleared is everything else. Reading the alpha as though it were the +// sample shows the exact complement of the picture — a scanned page whose text +// is the only part hidden. +// +// Asked which half of a two-colour page a mask of eight 0 bits and eight 1 bits +// paints, poppler answers the 0 half. func (r *renderer) applyStencilMask(s *sampled, stream *reader.Stream, resources reader.Dict) bool { mask := r.decodeImage(stream.Dict, stream.Raw, resources) if mask == nil { @@ -425,9 +444,7 @@ func (r *renderer) applyStencilMask(s *sampled, stream *reader.Stream, resources 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) - // A stencil mask marks what is hidden, so where it paints, the - // image does not. - if m.A > 127 { + if m.A <= 127 { s.pix[(y*s.w+x)*4+3] = 0 } } diff --git a/image_test.go b/image_test.go index 5d103b6..196f90d 100644 --- a/image_test.go +++ b/image_test.go @@ -220,9 +220,15 @@ func TestASoftMaskMakesAnImageSeeThrough(t *testing.T) { } func TestAMaskHidesPartOfAnImage(t *testing.T) { + // A mask sample of 0 means PAINT, so the half whose bit is 0 is the half + // that shows. This test asserted the complement of that, and so did the + // code: every explicit mask in the corpus was showing exactly the parts it + // was meant to hide. Asked the same question — which half of a + // two-colour page a mask of one 0 bit and one 1 bit paints — poppler + // answers the 0 half. w := reader.NewWriter("1.7") pagesRef := w.Reserve() - // A stencil that covers the left half. + // A stencil whose left half is 0 and right half is 1. mask := w.Add(&reader.Stream{Dict: reader.Dict{ "Type": reader.Name("XObject"), "Subtype": reader.Name("Image"), "Width": reader.Integer(2), "Height": reader.Integer(1), @@ -253,8 +259,8 @@ func TestAMaskHidesPartOfAnImage(t *testing.T) { t.Fatal(err) } img := draw(t, d, Options{}) - wantWhite(t, img, 5, 10) // hidden by the mask - wantBlack(t, img, 15, 10) // shown + wantBlack(t, img, 5, 10) // sample 0: painted + wantWhite(t, img, 15, 10) // sample 1: masked out } func TestAJPEGImage(t *testing.T) { @@ -482,15 +488,17 @@ func TestAMaskWhoseBytesAreStillEncodedIsNotDrawn(t *testing.T) { // nothing at all. // // 273 of the image masks in the 1 633 real forms carry an encoded filter. - // 236 were faxes, which the reader decodes as of go-pdfkit/reader#15; the - // nine that remain are JBIG2. Fifty-one first pages of real forms were - // showing this noise before either change. + // 236 were faxes and nine were JBIG2, and both are decoded before this + // point now. What can still arrive encoded is a mask a file gave a + // photographic filter, which no amount of decoding turns into one bit a + // pixel. Fifty-one first pages of real forms were showing this noise + // before any of it was decoded. for _, tc := range []struct { name string filter reader.Name drawn bool }{ - {"a format nothing here decodes", "JBIG2Decode", false}, + {"a mask given a photographic filter", "DCTDecode", false}, {"no filter at all", "", true}, } { t.Run(tc.name, func(t *testing.T) { diff --git a/jbig2.go b/jbig2.go new file mode 100644 index 0000000..73feeaf --- /dev/null +++ b/jbig2.go @@ -0,0 +1,103 @@ +// Copyright (c) 2026, the go-pdfkit/render authors +// All rights reserved. +// +// SPDX-License-Identifier: BSD-3-Clause + +package render + +import ( + "bytes" + "image" + + "github.com/dkrisman/gobig2" + "github.com/go-pdfkit/reader" +) + +// decodeJBIG2 turns a JBIG2 stream into the packed one-bit rows the rest of +// this file already reads, so a JBIG2 image goes on to be drawn by the same +// code as any other one-bit image and a JBIG2 stencil by the same code as any +// other stencil. +// +// JBIG2 is what a scanned page's ink is stored in, and it is almost never the +// page's content: it is the /Mask that gives the high-resolution ink layer its +// shape. Counting filters by what they encode as content put it in twenty +// documents; counting the images it SHAPES put it in four thousand. +// +// The two conventions are opposite. JBIG2 sets a bit where there is ink; a +// one-bit DeviceGray sample of 0 is black, and an image mask paints where its +// sample is 0. One inversion here serves both. +func (r *renderer) decodeJBIG2(dict reader.Dict, data []byte, w, h int) []byte { + img, err := jbig2Decode(data, r.jbig2Globals(dict)) + if err != nil || img == nil { + return nil + } + b := img.Bounds() + if b.Dx() != w || b.Dy() != h { + // The dictionary's size is the one the page's geometry was computed + // from. A stream that decodes to another size is not this image. + return nil + } + rowBytes := (w + 7) / 8 + out := make([]byte, rowBytes*h) + for i := range out { + out[i] = 0xff + } + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + gr, gg, gb, _ := img.At(b.Min.X+x, b.Min.Y+y).RGBA() + if (gr*299+gg*587+gb*114)/1000 < 0x8000 { + out[y*rowBytes+x/8] &^= 0x80 >> uint(x%8) + } + } + } + return out +} + +// jbig2Globals returns the shared segments the stream's decode parameters name, +// which is where an encoder puts the symbol dictionary several pages draw from. +// /DecodeParms runs parallel to /Filter, so it may be one dictionary or an +// array with one entry per filter in the chain. +func (r *renderer) jbig2Globals(dict reader.Dict) []byte { + parms := resolve(r.doc, dict.Get("DecodeParms")) + if arr, ok := reader.ToArray(parms); ok { + for _, v := range arr { + if g := r.globalsFrom(resolve(r.doc, v)); g != nil { + return g + } + } + return nil + } + return r.globalsFrom(parms) +} + +// globalsFrom reads the globals stream one decode-parameters dictionary names. +func (r *renderer) globalsFrom(parms reader.Object) []byte { + d, ok := reader.ToDict(parms) + if !ok { + return nil + } + st, ok := reader.ToStream(resolve(r.doc, d.Get("JBIG2Globals"))) + if !ok { + return nil + } + data, _, err := reader.DecodeStream(st, r.doc.Get) + if err != nil { + return nil + } + return data +} + +// jbig2Decode is a variable so a test can watch what happens when a decoder +// refuses, which is the case that decides whether the image is drawn wrong or +// not drawn. +var jbig2Decode = func(data, globals []byte) (image.Image, error) { + d, err := gobig2.NewDecoderEmbedded(bytes.NewReader(data), globals) + if err != nil { + return nil, err + } + img, err := d.Decode() + if err != nil { + return nil, err + } + return img, nil +} diff --git a/jbig2_test.go b/jbig2_test.go new file mode 100644 index 0000000..d1a770c --- /dev/null +++ b/jbig2_test.go @@ -0,0 +1,260 @@ +package render + +import ( + "errors" + "image" + "testing" + + "github.com/go-pdfkit/reader" +) + +// jbig2Ink is a JBIG2 stream 16 by 8 whose right half is ink. It is written out +// here rather than committed as a file, and it is synthetic: no scan of anyone's +// document enters the repository. It was made by encoding a bitmap with an +// encoder that is not this decoder, and it decodes to the same picture under +// gobig2, under that encoder's own decoder, and under poppler. +var jbig2Ink = []byte{ + 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x01, 0x00, 0x00, 0x00, 0x13, 0x00, + 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x27, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x03, + 0xff, 0xfd, 0xff, 0x02, 0xfe, 0xfe, 0xfe, 0x8f, 0x66, 0xff, 0xac, +} + +// indirect writes any stream inside an object as its own numbered object and +// puts a reference in its place, because that is the only form a real file +// uses: a globals stream is always referred to, never written where it is +// named. +func indirect(wr *reader.Writer, v reader.Object) reader.Object { + switch o := v.(type) { + case *reader.Stream: + return wr.Add(o) + case reader.Dict: + d := reader.Dict{} + for k, e := range o { + d[k] = indirect(wr, e) + } + return d + case reader.Array: + a := make(reader.Array, len(o)) + for i, e := range o { + a[i] = indirect(wr, e) + } + return a + } + return v +} + +// jbig2Page builds a page whose only content is one JBIG2 image filling it. +// extra is merged into the image dictionary, which is how a test says "and this +// one is a stencil" or "and its globals are over there". +func jbig2Page(t *testing.T, data []byte, w, h int, extra reader.Dict) *reader.Document { + t.Helper() + wr := reader.NewWriter("1.7") + pagesRef := wr.Reserve() + dict := reader.Dict{ + "Type": reader.Name("XObject"), "Subtype": reader.Name("Image"), + "Width": reader.Integer(w), "Height": reader.Integer(h), + "ColorSpace": reader.Name("DeviceGray"), "BitsPerComponent": reader.Integer(1), + "Filter": reader.Name("JBIG2Decode"), + } + for k, v := range extra { + dict[k] = indirect(wr, v) + } + img := wr.Add(&reader.Stream{Dict: dict, Raw: data}) + pageRef := wr.Add(reader.Dict{"Type": reader.Name("Page"), "Parent": pagesRef, + "MediaBox": nums(0, 0, 16, 8), + "Resources": reader.Dict{"XObject": reader.Dict{"S": img}}, + "Contents": wr.Add(&reader.Stream{Dict: reader.Dict{}, + Raw: []byte("q 16 0 0 8 0 0 cm /S Do Q")})}) + wr.Put(pagesRef, reader.Dict{"Type": reader.Name("Pages"), + "Kids": reader.Array{pageRef}, "Count": reader.Integer(1)}) + out, err := wr.Finish(reader.Dict{"Root": wr.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) + } + return d +} + +func TestAJBIG2ImageIsDrawn(t *testing.T) { + d := jbig2Page(t, jbig2Ink, 16, 8, nil) + img, err := Page(d, 1, Options{Scale: 1}) + if err != nil { + t.Fatal(err) + } + // Ink on the right, paper on the left — so a decoder handing back a flat + // or a mirrored picture is caught, not merely one handing back nothing. + if !isBlack(img, 12, 4) { + t.Errorf("the inked half is not inked: %s", pixel(img, 12, 4)) + } + if !isWhite(img, 3, 4) { + t.Errorf("the blank half is not blank: %s", pixel(img, 3, 4)) + } +} + +func TestAJBIG2StencilPaintsThroughItsShape(t *testing.T) { + // This is what JBIG2 nearly always IS. A scanned page's ink layer is a + // dark rectangle whose shape comes entirely from a JBIG2 stencil; the + // filter is in twenty documents as content and shapes four thousand + // images as a mask. + d := jbig2Page(t, jbig2Ink, 16, 8, reader.Dict{"ImageMask": reader.Bool(true)}) + img, err := Page(d, 1, Options{Scale: 1}) + if err != nil { + t.Fatal(err) + } + if !isBlack(img, 12, 4) { + t.Errorf("the stencil did not paint where it has ink: %s", pixel(img, 12, 4)) + } + if !isWhite(img, 3, 4) { + t.Errorf("the stencil painted where it has none: %s", pixel(img, 3, 4)) + } +} + +func TestAJBIG2StreamThatIsNotOneIsNotDrawn(t *testing.T) { + // The rule the rest of this file follows: not drawn rather than drawn + // wrong. Bytes that are not a JBIG2 stream must not become a rectangle. + d := jbig2Page(t, []byte{0, 1, 2, 3, 4, 5, 6, 7}, 16, 8, nil) + 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 bytes that are not a JBIG2 stream", ink) + } +} + +func TestAJBIG2StencilThatWillNotDecodeIsNotDrawn(t *testing.T) { + d := jbig2Page(t, []byte{0, 1, 2, 3}, 16, 8, reader.Dict{"ImageMask": reader.Bool(true)}) + img, err := Page(d, 1, Options{Scale: 1}) + if err != nil { + t.Fatal(err) + } + if ink := inked(img); ink != 0 { + t.Errorf("%d pixels painted through a stencil that could not be read", ink) + } +} + +func TestAJBIG2StreamThatIsNotTheSizeTheDictionarySaysIsNotDrawn(t *testing.T) { + // Unlike a JPEG 2000 codestream, a JBIG2 mask has no size of its own that + // the page can be laid out from: the geometry was computed from the + // dictionary. A stream that decodes to another size is not this image. + d := jbig2Page(t, jbig2Ink, 32, 32, nil) + 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 of the wrong size", ink) + } +} + +func TestAJBIG2DecoderThatRefusesTakesTheImageWithIt(t *testing.T) { + // The decoder refuses a stream whose symbols exceed its resource budget, + // which seven of four hundred real scanned masks do. Refusing is the right + // answer; drawing the rectangle anyway is not. + restore := jbig2Decode + defer func() { jbig2Decode = restore }() + jbig2Decode = func(data, globals []byte) (image.Image, error) { + return nil, errors.New("resource budget exceeded") + } + d := jbig2Page(t, jbig2Ink, 16, 8, nil) + 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 although the decoder refused", ink) + } +} + +func TestAJBIG2DecoderThatReturnsNothingTakesTheImageWithIt(t *testing.T) { + restore := jbig2Decode + defer func() { jbig2Decode = restore }() + jbig2Decode = func(data, globals []byte) (image.Image, error) { return nil, nil } + d := jbig2Page(t, jbig2Ink, 16, 8, nil) + 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 although the decoder returned no picture", ink) + } +} + +// seenGlobals draws a page and reports the globals the decoder was handed, so +// a test can check the plumbing without needing a stream split into two parts. +func seenGlobals(t *testing.T, parms reader.Object) []byte { + t.Helper() + restore := jbig2Decode + defer func() { jbig2Decode = restore }() + var got []byte + var called bool + jbig2Decode = func(data, globals []byte) (image.Image, error) { + got, called = globals, true + return restore(data, globals) + } + d := jbig2Page(t, jbig2Ink, 16, 8, reader.Dict{"DecodeParms": parms}) + if _, err := Page(d, 1, Options{Scale: 1}); err != nil { + t.Fatal(err) + } + if !called { + t.Fatal("the decoder was never called") + } + return got +} + +func TestTheSharedSegmentsAreHandedToTheDecoder(t *testing.T) { + // An encoder puts the symbol dictionary several pages draw from in a + // globals stream, and the pages are undecodable without it. /DecodeParms + // runs parallel to /Filter, so it is one dictionary or an array with one + // entry per filter in the chain, and both forms occur. + globals := &reader.Stream{Dict: reader.Dict{}, Raw: []byte("shared segments")} + for _, tc := range []struct { + name string + parms func(*reader.Stream) reader.Object + }{ + {"one dictionary", func(s *reader.Stream) reader.Object { + return reader.Dict{"JBIG2Globals": s} + }}, + {"an array with one entry per filter", func(s *reader.Stream) reader.Object { + return reader.Array{reader.Null{}, reader.Dict{"JBIG2Globals": s}} + }}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := string(seenGlobals(t, tc.parms(globals))); got != "shared segments" { + t.Errorf("the decoder was handed %q, not the globals stream", got) + } + }) + } +} + +func TestDecodeParametersThatNameNoSharedSegments(t *testing.T) { + // Every one of these is a stream that decodes on its own, which is what + // all 403 of the corpus's masks turned out to be. None of them is a + // reason to refuse the image. + broken := &reader.Stream{Dict: reader.Dict{"Filter": reader.Name("FlateDecode")}, + Raw: []byte("not deflated")} + for _, tc := range []struct { + name string + parms reader.Object + }{ + {"no decode parameters at all", nil}, + {"a dictionary naming none", reader.Dict{"K": reader.Integer(0)}}, + {"an array naming none", reader.Array{reader.Dict{"K": reader.Integer(0)}}}, + {"an array entry that is not a dictionary", reader.Array{reader.Integer(1)}}, + {"globals that are not a stream", reader.Dict{"JBIG2Globals": reader.Integer(1)}}, + {"globals whose own filter breaks", reader.Dict{"JBIG2Globals": broken}}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := seenGlobals(t, tc.parms); got != nil { + t.Errorf("the decoder was handed %q, not nothing", got) + } + }) + } +} diff --git a/jpx_test.go b/jpx_test.go index b97c712..d7ba184 100644 --- a/jpx_test.go +++ b/jpx_test.go @@ -112,40 +112,3 @@ func TestTheSizeComesFromThePictureNotTheDictionary(t *testing.T) { t.Error("a dictionary that lies about the size stopped the page being drawn") } } - -func TestAFormatNothingHereDecodesIsNotDrawn(t *testing.T) { - // JPEG 2000 used to be the example of this. What is left is JBIG2, which - // 12 of 250 court filings and 1 of 222 scanned books carry — a real gap, - // and one that draws nothing rather than drawing noise. - wr := reader.NewWriter("1.7") - pagesRef := wr.Reserve() - img := wr.Add(&reader.Stream{Dict: reader.Dict{ - "Type": reader.Name("XObject"), "Subtype": reader.Name("Image"), - "Width": reader.Integer(8), "Height": reader.Integer(8), - "ColorSpace": reader.Name("DeviceGray"), "BitsPerComponent": reader.Integer(1), - "Filter": reader.Name("JBIG2Decode"), - }, Raw: make([]byte, 8)}) - pageRef := wr.Add(reader.Dict{"Type": reader.Name("Page"), "Parent": pagesRef, - "MediaBox": nums(0, 0, 40, 40), - "Resources": reader.Dict{"XObject": reader.Dict{"S": img}}, - "Contents": wr.Add(&reader.Stream{Dict: reader.Dict{}, - Raw: []byte("q 40 0 0 40 0 0 cm /S Do Q")})}) - wr.Put(pagesRef, reader.Dict{"Type": reader.Name("Pages"), - "Kids": reader.Array{pageRef}, "Count": reader.Integer(1)}) - out, err := wr.Finish(reader.Dict{"Root": wr.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) - } - pic, err := Page(d, 1, Options{Scale: 1}) - if err != nil { - t.Fatal(err) - } - if ink := inked(pic); ink != 0 { - t.Errorf("%d pixels drawn from a format nothing here decodes", ink) - } -}