From b4c832ee606140686b57b2d0db7c2148a6519799 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sun, 4 Oct 2026 12:02:47 +0200 Subject: [PATCH 1/2] A form XObject's incomplete /Resources no longer shadows the enclosing ones qpdf's own fixture says it in its name. form-xobjects-some-resources2.pdf draws six 15x15 images on page 1 through forms, some of which carry only part of what they use, and this renderer drew TWO of them. images pdfimages -list takes out 6 (objects 10 12 14 16 18 20) images render.Images returned 2 (objects 10 12) It was not only the extraction API. Through go-pdfkit/conformance's compare against pdftoppm at 72 dpi, on these fixtures alone: before after pixels differing 8.54% 0.03% byte for byte 94.06% 99.56% mean |diff| 10.936 0.519 levels worst square mean 130.27 8.80 levels worst pixel 254 149 A worst square mean of 130 is not resampling; four of six stamps were missing from the page. Both paths had HALF the rule. drawForm and imagesDrawn each handled an ABSENT /Resources by falling back to the parent, and a present one replaced it wholesale -- so a name the form did not list resolved to nothing and drawXObject returned early. A form XObject's resource dictionary is not required to be complete, and a name it does not provide is resolved in the resources in force where the form was painted, which is what poppler does. So: renderer.resChain, the dictionaries in force outermost first, pushed by every stream that brings its own -- run() covers a form, a Type 3 glyph procedure, a pattern cell, an annotation's appearance and a soft mask; the extraction path pushes its own because it does not go through run(). The seven lookups that each read one category out of ONE dictionary -- XObject, Font, ColorSpace, Pattern, Shading, ExtGState, Properties -- go through named(), which tries the dictionary the caller holds and then walks outward. named() takes that dictionary rather than reading it off the chain, and that is not decoration: the first attempt dropped the parameter, and TestABDCThatNamesNoLayerHidesNothing failed at once because the tests call these functions directly. A lookup whose first dictionary is implicit is a lookup nobody can call. Checked in both directions. On the whole forms corpus, 2 268 documents in eighteen populations through conformance's images harness, exactly ONE population moves: gh-qpdf, pictures 63 -> 73, and the ten recovered land in (samples) direct as 51 of 51 exact and 51 of 51 byte-identical with poppler. They are not merely present; they are right. Nothing else in any population changes a cell. Mutation-checked: removing the walk fails the three inheritance tests, and dropping the caller's dictionary fails the one that calls directly. 100.0% of statements. Fixes #102. --- colour.go | 6 +- exec.go | 4 ++ formresources_test.go | 133 ++++++++++++++++++++++++++++++++++++++++++ images.go | 12 +++- optional.go | 6 +- pattern.go | 12 ++-- space.go | 8 +-- state.go | 60 +++++++++++++++++++ text.go | 5 +- xobject.go | 6 +- 10 files changed, 226 insertions(+), 26 deletions(-) create mode 100644 formresources_test.go diff --git a/colour.go b/colour.go index 83f9271..334f685 100644 --- a/colour.go +++ b/colour.go @@ -108,11 +108,11 @@ func (r *renderer) applyExtGState(g *gstate, operands []reader.Object, resources if !ok { return } - states, ok := r.doc.GetDict(resources, "ExtGState") - if !ok { + entry := r.named(resources, "ExtGState", name) + if entry == nil { return } - params, ok := r.doc.GetDict(states, name) + params, ok := reader.ToDict(resolve(r.doc, entry)) if !ok { return } diff --git a/exec.go b/exec.go index 7015372..d5e32ab 100644 --- a/exec.go +++ b/exec.go @@ -20,6 +20,10 @@ const ( // run executes a content stream against a graphics state. func (r *renderer) run(content []byte, resources reader.Dict, g gstate) { + // These are the innermost resources from here on, and a name they do not + // provide is resolved outward -- see renderer.resChain and named(). + r.resChain = append(r.resChain, resources) + defer func() { r.resChain = r.resChain[:len(r.resChain)-1] }() // Marked content does not reach across streams: a form's BDC has nothing // to do with the page's, and a stream that opens one and never closes it // must not leave the next stream hidden. diff --git a/formresources_test.go b/formresources_test.go new file mode 100644 index 0000000..0774eff --- /dev/null +++ b/formresources_test.go @@ -0,0 +1,133 @@ +package render + +import ( + "testing" + + "github.com/go-pdfkit/reader" +) + +// pageWhoseFormHasSomeResources builds a page that draws a form whose own +// /Resources is PRESENT and INCOMPLETE: it holds one entry of the category and +// not the one the form's content uses. +// +// That is the shape of qpdf's form-xobjects-some-resources2.pdf, which is in +// the conformance corpus and which this renderer drew two of six images of. +// See go-pdfkit/render#102. +func pageWhoseFormHasSomeResources(t *testing.T, formContent string, formRes, pageRes reader.Dict) *reader.Document { + t.Helper() + w := reader.NewWriter("1.7") + pagesRef := w.Reserve() + inner := w.Add(&reader.Stream{ + Dict: reader.Dict{"Type": reader.Name("XObject"), "Subtype": reader.Name("Form")}, + Raw: []byte("0 g 0 0 10 10 re f"), + }) + form := reader.Dict{"Type": reader.Name("XObject"), "Subtype": reader.Name("Form")} + if formRes != nil { + form["Resources"] = formRes + } + outer := w.Add(&reader.Stream{Dict: form, Raw: []byte(formContent)}) + res := reader.Dict{"XObject": reader.Dict{"F": outer, "G": inner}} + for k, v := range pageRes { + res[k] = v + } + page := w.Add(reader.Dict{ + "Type": reader.Name("Page"), "Parent": pagesRef, + "MediaBox": reader.Array{reader.Integer(0), reader.Integer(0), reader.Integer(40), reader.Integer(40)}, + "Contents": w.Add(&reader.Stream{Dict: reader.Dict{}, Raw: []byte("/F Do")}), + "Resources": res, + }) + w.Put(pagesRef, reader.Dict{"Type": reader.Name("Pages"), + "Kids": reader.Array{page}, "Count": reader.Integer(1)}) + root := w.Add(reader.Dict{"Type": reader.Name("Catalog"), "Pages": pagesRef}) + out, err := w.Finish(reader.Dict{"Root": root}) + if err != nil { + t.Fatal(err) + } + d, err := reader.Open(out) + if err != nil { + t.Fatal(err) + } + return d +} + +func TestAFormWithSomeResourcesInheritsTheRest(t *testing.T) { + // The form lists an /XObject of its own -- so the dictionary is there -- + // and does NOT list /G, which it draws. Before, a present /Resources + // replaced the page's wholesale and /G resolved to nothing: the renderer + // returned early and drew paper. + // + // The control is the SAME document with the form's own /XObject holding + // the name. If both drew ink the test would pass with the chain removed. + some := reader.Dict{"XObject": reader.Dict{"Unused": reader.Name("nothing")}} + d := pageWhoseFormHasSomeResources(t, "/G Do", some, nil) + wantBlack(t, draw(t, d, Options{}), 5, 35) + + // And the extraction path, which walks the content stream separately. + ims, err := Images(d, 1) + if err != nil { + t.Fatal(err) + } + if len(ims) != 0 { + t.Errorf("a form of forms yielded %d pictures", len(ims)) + } +} + +func TestANameNoDictionaryInForceProvidesDrawsNothing(t *testing.T) { + // The other direction, and the one that keeps the chain from making + // everything resolve to something: a name nowhere. + some := reader.Dict{"XObject": reader.Dict{"Unused": reader.Name("nothing")}} + d := pageWhoseFormHasSomeResources(t, "/Nowhere Do", some, nil) + wantWhite(t, draw(t, d, Options{}), 5, 35) + if ims, err := Images(d, 1); err != nil || len(ims) != 0 { + t.Errorf("got %d pictures, %v", len(ims), err) + } +} + +func TestAGraphicsStateNameNoDictionaryProvidesIsLeftAlone(t *testing.T) { + // gs names an /ExtGState, and the lookup for it went through the same + // change. A name nothing provides must leave the state as it was rather + // than take a nil for a dictionary. + some := reader.Dict{"XObject": reader.Dict{"Unused": reader.Name("nothing")}} + d := pageWhoseFormHasSomeResources(t, "/Nowhere gs 0 g 0 0 10 10 re f", some, nil) + wantBlack(t, draw(t, d, Options{}), 5, 35) +} + +func TestAGraphicsStateIsInheritedByAFormThatDoesNotListIt(t *testing.T) { + // The inheritance has to hold for every category the change touched, and + // /ExtGState is the one whose effect is visible in one pixel: half alpha + // over white paper is grey, not black. + page := reader.Dict{"ExtGState": reader.Dict{"Half": reader.Dict{"ca": reader.Real(0.5)}}} + some := reader.Dict{"XObject": reader.Dict{"Unused": reader.Name("nothing")}} + d := pageWhoseFormHasSomeResources(t, "/Half gs 0 g 0 0 10 10 re f", some, page) + img := draw(t, d, Options{}) + if isBlack(img, 5, 35) || isWhite(img, 5, 35) { + t.Errorf("the form did not inherit the page's /ExtGState: %s", pixel(img, 5, 35)) + } +} + +func TestADictionaryInForceWithoutTheCategoryIsSteppedOver(t *testing.T) { + // A form whose /Resources holds no /XObject AT ALL is not the absent case + // -- the dictionary is there, the category is not -- and the walk has to + // step over it rather than stop. + some := reader.Dict{"Font": reader.Dict{"Unused": reader.Name("nothing")}} + d := pageWhoseFormHasSomeResources(t, "/G Do", some, nil) + wantBlack(t, draw(t, d, Options{}), 5, 35) +} + +func TestAResourceThatIsThereAndIsNotWhatItShouldBe(t *testing.T) { + // Both lookups now answer "not there" with a nil and leave "there but + // wrong" to the type assertion after it. Before the change a missing name + // came back as /Null and took that same path, so these two branches were + // reached by the missing case and never by the wrong-type one. + some := reader.Dict{"XObject": reader.Dict{"Unused": reader.Name("nothing")}} + + // An /XObject entry that is not a stream. + d := pageWhoseFormHasSomeResources(t, "/Unused Do", some, nil) + wantWhite(t, draw(t, d, Options{}), 5, 35) + + // An /ExtGState entry that is not a dictionary, with ink after it so the + // page says whether the operator took the state down with it. + page := reader.Dict{"ExtGState": reader.Dict{"Bad": reader.Integer(1)}} + d = pageWhoseFormHasSomeResources(t, "/Bad gs 0 g 0 0 10 10 re f", some, page) + wantBlack(t, draw(t, d, Options{}), 5, 35) +} diff --git a/images.go b/images.go index d373434..a54eb98 100644 --- a/images.go +++ b/images.go @@ -292,7 +292,12 @@ func (r *renderer) imagesDrawn(content []byte, res reader.Dict, depth int) []Ima if depth > maxImageDepth { return nil } - xo, _ := reader.ToDict(resolve(r.doc, res.Get("XObject"))) + // The extraction path does not go through run(), so it pushes its own. + // Without this a form whose /Resources is present but INCOMPLETE loses + // every name it does not list: six images against two on qpdf's + // form-xobjects-some-resources2.pdf. See renderer.resChain. + r.resChain = append(r.resChain, res) + defer func() { r.resChain = r.resChain[:len(r.resChain)-1] }() // A stream that stops decoding part way is walked as far as it got, which // is what this package draws; the error says nothing the operators do not. ops, _ := reader.Operations(content) @@ -305,7 +310,10 @@ func (r *renderer) imagesDrawn(content []byte, res reader.Dict, depth int) []Ima if !ok { continue } - entry := xo.Get(name) + entry := r.named(res, "XObject", name) + if entry == nil { + continue + } st, ok := reader.ToStream(resolve(r.doc, entry)) if !ok { continue diff --git a/optional.go b/optional.go index b99cc5e..428d264 100644 --- a/optional.go +++ b/optional.go @@ -170,11 +170,11 @@ func (r *renderer) hiddenProperty(operands []reader.Object, resources reader.Dic return false } if name, ok := reader.ToName(operands[1]); ok { - props, ok := r.doc.GetDict(resources, "Properties") - if !ok { + entry := r.named(resources, "Properties", name) + if entry == nil { return false } - return r.oc.hidden(r.doc, props.Get(name)) + return r.oc.hidden(r.doc, entry) } return r.oc.hidden(r.doc, operands[1]) } diff --git a/pattern.go b/pattern.go index d333d49..2684bb9 100644 --- a/pattern.go +++ b/pattern.go @@ -32,11 +32,11 @@ type pattern struct { // readPattern reads a pattern out of the page's resources. func (r *renderer) readPattern(name reader.Name, resources reader.Dict) *pattern { - pats, ok := r.doc.GetDict(resources, "Pattern") - if !ok { + e := r.named(resources, "Pattern", name) + if e == nil { return nil } - entry := resolve(r.doc, pats.Get(name)) + entry := resolve(r.doc, e) dict, ok := reader.ToDict(entry) stream, isStream := reader.ToStream(entry) if isStream { @@ -153,11 +153,11 @@ func (r *renderer) drawShading(g *gstate, operands []reader.Object, resources re if !ok { return } - shadings, ok := r.doc.GetDict(resources, "Shading") - if !ok { + e := r.named(resources, "Shading", name) + if e == nil { return } - sh := r.readShading(shadings.Get(name), resources) + sh := r.readShading(e, resources) if sh == nil { return } diff --git a/space.go b/space.go index aa825b7..91f7998 100644 --- a/space.go +++ b/space.go @@ -106,12 +106,8 @@ func (r *renderer) colourSpace(o reader.Object, resources reader.Dict, depth int return s } // A name that is not a device space names one in the resources. - spaces, ok := r.doc.GetDict(resources, "ColorSpace") - if !ok { - return deviceGray - } - entry := spaces.Get(name) - if entry.Kind() == reader.KindNull { + entry := r.named(resources, "ColorSpace", name) + if entry == nil || entry.Kind() == reader.KindNull { return deviceGray } return r.colourSpace(entry, resources, depth+1) diff --git a/state.go b/state.go index f59fdc2..5a78611 100644 --- a/state.go +++ b/state.go @@ -75,6 +75,26 @@ type renderer struct { // depth bounds how far one form may draw another. depth int + // resChain is the resource dictionaries in force, outermost first. + // + // A form XObject's /Resources is not required to be COMPLETE, and a name + // it does not provide is resolved in the resources that were in force + // where the form was painted. This renderer had half of that rule: an + // ABSENT /Resources fell back to the parent, a present one replaced it + // wholesale, so a name the form did not list resolved to nothing and was + // silently not drawn. + // + // qpdf ships a fixture for exactly this, and it is in the conformance + // corpus: form-xobjects-some-resources2.pdf draws six 15x15 images and we + // drew TWO, 8.54% of the page's pixels away from poppler at a worst + // square mean of 130 levels. See go-pdfkit/render#102. + // + // Each stream that brings its own resources pushes them -- a form, a Type + // 3 glyph procedure, a pattern cell, an annotation's appearance, a soft + // mask -- and named() walks this innermost-first. maxFormDepth bounds how + // deep it goes. + resChain []reader.Dict + // drawingProcs holds the object numbers of the Type 3 glyph procedures // currently being drawn, so that one cannot draw itself. // @@ -490,3 +510,43 @@ func (r *renderer) salvaged(stream *reader.Stream) ([]byte, reader.Name, error) } return dec.Data, dec.Image, nil } + +// named resolves one name in one resource category, in the resources in force: +// the innermost dictionary first, then outward. +// +// It replaces seven lookups that each read the category out of ONE dictionary. +// Each of them is a place where a form with incomplete resources lost a name: +// XObject, Font, ColorSpace, Pattern, Shading, ExtGState and Properties. +// +// A nil comes back for a name no dictionary in force provides, which is what +// the callers already handled: the entry does not resolve and nothing is drawn. +// The dictionary the caller holds is tried FIRST and the chain behind it. It +// is passed rather than read off the chain so that the parameter every one of +// these functions already takes keeps meaning what it says -- a lookup whose +// first dictionary is implicit is a lookup nobody can call directly, and the +// tests do call them directly. +func (r *renderer) named(resources reader.Dict, category, name reader.Name) reader.Object { + if v := in(r.doc, resources, category, name); v != nil { + return v + } + for i := len(r.resChain) - 1; i >= 0; i-- { + if v := in(r.doc, r.resChain[i], category, name); v != nil { + return v + } + } + return nil +} + +// in looks one name up in one dictionary's category, and answers nil for +// "not there" -- which a present /Null is not. +func in(doc *reader.Document, resources reader.Dict, category, name reader.Name) reader.Object { + d, ok := doc.GetDict(resources, category) + if !ok { + return nil + } + v, present := d[name] + if !present { + return nil + } + return v +} diff --git a/text.go b/text.go index 0e82faf..cc37953 100644 --- a/text.go +++ b/text.go @@ -303,11 +303,10 @@ func (r *renderer) font(o reader.Object, resources reader.Dict) *pdfFont { if !ok { return nil } - fonts, ok := r.doc.GetDict(resources, "Font") - if !ok { + entry := r.named(resources, "Font", name) + if entry == nil { return nil } - entry := fonts.Get(name) if ref, ok := entry.(reader.Ref); ok { if f, ok := r.fonts[ref.Num]; ok { return f diff --git a/xobject.go b/xobject.go index 3c0cc2d..1392882 100644 --- a/xobject.go +++ b/xobject.go @@ -21,11 +21,11 @@ func (r *renderer) drawXObject(g *gstate, operands []reader.Object, resources re if !ok { return } - xobjects, ok := r.doc.GetDict(resources, "XObject") - if !ok { + entry := r.named(resources, "XObject", name) + if entry == nil { return } - stream, ok := reader.ToStream(resolve(r.doc, xobjects.Get(name))) + stream, ok := reader.ToStream(resolve(r.doc, entry)) if !ok { return } From 23435fe8b594abcdf706a76701018a271c3109f7 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sun, 4 Oct 2026 15:29:55 +0200 Subject: [PATCH 2/2] Images returns the inline ones, so 4 147 pictures of one corpus population can be compared at all Images returned nothing for a picture written into the content stream with BI, and said why: "they are named by no resource and are objects of nothing, so there is nothing for a tool that extracts a file's objects to hand back beside them." The first half is true and the conclusion was never measured. pdfimages EXTRACTS inline images and lists them with an object of 0. Measured over the conformance corpus's fr-cerfa, 450 forms, page 1 each: 4 147 pictures the reference got out and nothing of ours was ever compared with -- 2 614 of them stencils, 2 592 of them 16x16, one document holding 2 592. A defect in inline-image decoding was invisible to every figure in that baseline, because a picture nobody returns is a picture nobody compares. ONE ENTRY PER BI, which is the only unit that can be right: an inline image IS its draw, there is no object to collapse repeats onto, and pdfimages lists one row per draw as well. Object 0, which is what the file gives it and what pdfimages prints. The name is an ordinal, BI#1 upward across the whole call including across forms, because there is no resource name and a reader of a difference has to be able to say which picture it was. No mask is read beside it: an inline dictionary has no abbreviation for /SMask. Over the WHOLE forms corpus, 2 268 documents in eighteen populations, through conformance's images harness: pictures 5 917 -> 11 296 unseen 4 243 -> 80 paired by size 0 -> 5 368 The third line is the honest caveat and it is not a side effect: the pairing falls back to size exactly when an object number is missing, and until now the one class whose number is missing never reached it. That fallback has been in the harness for weeks with a share of zero. These 5 368 pictures are matched by size and order, which is weaker than by object, and conformance#92 made that share visible for this reason. In fr-cerfa 2 729 of the new pictures come out exact and byte-identical and none differs. The differing ones are three rows of ONE safedocs fixture, Inline_Image_Abbreviations_InlineAbbreviations.pdf, whose eight dictionaries each carry the abbreviation AND the long name with CONFLICTING values -- /W 20 /Width 10 /H 10 /Height 40 /BPC 8 /BitsPerComponent 4, with a comment reading "COMMENT OUT THIS LINE TO SEE EFFECT". poppler resolves those in favour of the long name, hence its 10x40 at 4 bits against our 20x10 at 8. reader's InlineImage.Expanded already documents that choice, cites Table 93 for it, and notes that pdf.js and MuPDF disagree with each other about it. Nothing here is a defect; it is an ambiguity that was decided on purpose and could not be seen until the pictures reached a comparison. Mutation-checked: dropping the ordinal gives "BI#0", advancing it before the decode succeeds leaves a gap a reader cannot account for, and skipping afford() lets a page past the budget come back without an error. 100.0% of statements. Based on #103, which the same counter found. Fixes #101. --- images.go | 60 ++++++++++++++++++++ inlineimages_test.go | 132 +++++++++++++++++++++++++++++++++++++++++++ state.go | 4 ++ 3 files changed, 196 insertions(+) create mode 100644 inlineimages_test.go diff --git a/images.go b/images.go index a54eb98..d2d5b57 100644 --- a/images.go +++ b/images.go @@ -258,6 +258,59 @@ func (r *renderer) decoded(name string, object int, st *reader.Stream, res reade return out } +// inlineImage reads one picture written into the content stream with BI. +// +// This used to return nothing, and said so: "they are named by no resource and +// are objects of nothing, so there is nothing for a tool that extracts a +// file's objects to hand back beside them." The first half is true and the +// conclusion was not measured. pdfimages EXTRACTS inline images and lists them +// with an object of 0, so there was something to hand back, and it was being +// compared against nothing: over the 450 forms of the conformance corpus's +// fr-cerfa, 4 147 pictures -- 2 614 of them stencils, 2 592 of them 16x16, one +// document holding 2 592. A defect in inline-image decoding was invisible to +// every figure in that baseline. See go-pdfkit/render#101. +// +// ONE ENTRY PER BI, which is the only unit that can be right here: an inline +// image IS its draw, there is no object to collapse repeats onto, and +// pdfimages lists one row per draw as well. A page of 2 592 of them returns +// 2 592 entries, which the budget covers -- 2 592 pictures of 16x16 is 663 552 +// pixels against a quarter of a billion. +// +// Object 0, because that is what the file gives it and what pdfimages prints: +// a harness pairing by object number then falls back to size, which is the +// case that fallback was written for and had never once been reached. +// +// The NAME is an ordinal, "BI#1" upward across the whole call, because there +// is no resource name to use and a reader of a difference needs to be able to +// say which one. It counts across forms as well as streams, so two inline +// images never share a name within one call to [Images]. +// +// No mask is read beside it. An inline image's dictionary has no abbreviation +// for /SMask and the long names are not permitted there, so there is nothing +// to follow. +func (r *renderer) inlineImage(im *reader.InlineImage, res reader.Dict) []Image { + dict := im.Expanded() + if !r.afford(dict) { + return nil + } + s := r.decodeBase(dict, im.Raw, res) + if s == nil { + return nil + } + r.inlines++ + stencil, _ := reader.ToBool(resolve(r.doc, dict.Get("ImageMask"))) + return []Image{{ + Name: fmt.Sprintf("BI#%d", r.inlines), + Object: 0, + Filter: imageFilterOf(r.doc, &reader.Stream{Dict: dict, Raw: im.Raw}), + Decoded: r.hasDecodeArray(dict), + Stencil: bool(stencil), + // Expanded for the reason decoded() gives: raster.Image.Pix is four + // bytes a pixel and callers read it directly. + Pic: pic(s), + }} +} + // 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 { @@ -303,6 +356,13 @@ func (r *renderer) imagesDrawn(content []byte, res reader.Dict, depth int) []Ima ops, _ := reader.Operations(content) var out []Image for _, op := range ops { + if op.Operator == "BI" && op.Image != nil { + out = append(out, r.inlineImage(op.Image, res)...) + if r.refused != nil { + return out + } + continue + } if op.Operator != "Do" || len(op.Operands) == 0 { continue } diff --git a/inlineimages_test.go b/inlineimages_test.go new file mode 100644 index 0000000..6a56b2c --- /dev/null +++ b/inlineimages_test.go @@ -0,0 +1,132 @@ +package render + +import ( + "fmt" + "strings" + "testing" + + "github.com/go-pdfkit/reader" +) + +// TestImagesReturnsTheInlineOnes is the measurement that made this change. +// +// Images returned nothing for a picture written into the content stream, and +// said why: it is the object of nothing. pdfimages extracts them and lists +// them with an object of 0, so over the 450 forms of the conformance corpus's +// fr-cerfa there were 4 147 pictures the reference got out and nothing of ours +// was ever compared with. See go-pdfkit/render#101. +func TestImagesReturnsTheInlineOnes(t *testing.T) { + content := "q 20 0 0 20 0 0 cm BI /W 2 /H 1 /BPC 8 /CS /G ID \x00\xff EI Q" + d := onePage(t, [4]float64{0, 0, 20, 20}, content, nil) + ims, err := Images(d, 1) + if err != nil { + t.Fatal(err) + } + if len(ims) != 1 { + t.Fatalf("got %d pictures", len(ims)) + } + im := ims[0] + // Object 0 is what the file gives it and what pdfimages prints, and it is + // what sends a harness pairing by object number to its size fallback. + if im.Object != 0 { + t.Errorf("object %d, want 0", im.Object) + } + // A name of its own, because there is no resource name and a reader of a + // difference has to be able to say which picture it was. + if im.Name != "BI#1" { + t.Errorf("name %q", im.Name) + } + if im.Pic.W != 2 || im.Pic.H != 1 { + t.Errorf("%dx%d, want 2x1", im.Pic.W, im.Pic.H) + } + if im.Stencil { + t.Error("an image was called a stencil") + } +} + +func TestEachInlineImageGetsItsOwnName(t *testing.T) { + // ONE ENTRY PER BI, and the ordinal counts across the whole call: an + // inline image is its own draw, there is no object to collapse repeats + // onto, and pdfimages lists one row per draw too. A page that stamps the + // same bytes three times is three rows on both sides. + one := "q 20 0 0 20 0 0 cm BI /W 2 /H 1 /BPC 8 /CS /G ID \x00\xff EI Q " + d := onePage(t, [4]float64{0, 0, 20, 20}, strings.Repeat(one, 3), nil) + ims, err := Images(d, 1) + if err != nil { + t.Fatal(err) + } + var names []string + for _, im := range ims { + names = append(names, im.Name) + } + if fmt.Sprint(names) != "[BI#1 BI#2 BI#3]" { + t.Errorf("names %v", names) + } +} + +func TestAnInlineImageInsideAFormIsReturnedToo(t *testing.T) { + // The ordinal has to count across streams, or two of them share a name + // within one call. A form draws one and the page draws one. + form := reader.Dict{} + d := pageWithForm(t, + "q 20 0 0 20 0 0 cm BI /W 2 /H 1 /BPC 8 /CS /G ID \x00\xff EI Q /F Do", + form, + "q 20 0 0 20 0 0 cm BI /W 1 /H 1 /BPC 8 /CS /G ID \x00 EI Q") + ims, err := Images(d, 1) + if err != nil { + t.Fatal(err) + } + if len(ims) != 2 { + t.Fatalf("got %d pictures: %+v", len(ims), ims) + } + if ims[0].Name != "BI#1" || ims[1].Name != "BI#2" { + t.Errorf("names %q and %q", ims[0].Name, ims[1].Name) + } +} + +func TestAnInlineStencilIsReportedAsOne(t *testing.T) { + // JBIG2 and CCITT stencils are most of what this change makes visible: + // 2 614 of fr-cerfa's 4 147 are stencils, 2 592 of them 16x16. + content := "q 20 0 0 20 0 0 cm BI /W 2 /H 1 /IM true ID \x40 EI Q" + d := onePage(t, [4]float64{0, 0, 20, 20}, content, nil) + ims, err := Images(d, 1) + if err != nil { + t.Fatal(err) + } + if len(ims) != 1 || !ims[0].Stencil { + t.Fatalf("got %+v", ims) + } +} + +func TestAnInlineImageThatDecodesToNothingIsNotReturned(t *testing.T) { + // Nothing comes back for a picture no codec here can read, which is the + // same answer Page gives by not drawing it, and the ordinal must not + // advance over it or the names of the ones that ARE returned have gaps a + // reader cannot account for. + content := "q 20 0 0 20 0 0 cm BI /W 0 /H 0 /BPC 8 /CS /G ID EI Q " + + "q 20 0 0 20 0 0 cm BI /W 2 /H 1 /BPC 8 /CS /G ID \x00\xff EI Q" + d := onePage(t, [4]float64{0, 0, 20, 20}, content, nil) + ims, err := Images(d, 1) + if err != nil { + t.Fatal(err) + } + if len(ims) != 1 { + t.Fatalf("got %d pictures: %+v", len(ims), ims) + } + if ims[0].Name != "BI#1" { + t.Errorf("the ordinal counted a picture that was not returned: %q", ims[0].Name) + } +} + +func TestAPageOfInlineImagesPastTheBudgetIsRefusedWhole(t *testing.T) { + // The budget is charged before the decode, as it is for an XObject, and a + // page that cannot be decoded whole comes back as nothing rather than as + // half of itself: half the pictures of a page read as the whole of them by + // anything counting. + one := fmt.Sprintf("q BI /W %d /H %d /BPC 8 /CS /G ID ", maxImagesPixels/2, 2) + content := one + strings.Repeat("\x00", 8) + " EI Q " + one + strings.Repeat("\x00", 8) + " EI Q" + d := onePage(t, [4]float64{0, 0, 20, 20}, content, nil) + if _, err := Images(d, 1); err == nil { + t.Error("a page past the budget came back without an error") + } +} diff --git a/state.go b/state.go index 5a78611..95b458f 100644 --- a/state.go +++ b/state.go @@ -95,6 +95,10 @@ type renderer struct { // deep it goes. resChain []reader.Dict + // inlines counts the inline images [Images] has returned, so that each + // gets a name of its own. There is no resource name to use. + inlines int + // drawingProcs holds the object numbers of the Type 3 glyph procedures // currently being drawn, so that one cannot draw itself. //