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
6 changes: 3 additions & 3 deletions colour.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 4 additions & 0 deletions exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
133 changes: 133 additions & 0 deletions formresources_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
12 changes: 10 additions & 2 deletions images.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions optional.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
Expand Down
12 changes: 6 additions & 6 deletions pattern.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
8 changes: 2 additions & 6 deletions space.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
60 changes: 60 additions & 0 deletions state.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down Expand Up @@ -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
}
5 changes: 2 additions & 3 deletions text.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions xobject.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading