Skip to content

A form XObject with INCOMPLETE /Resources shadows the page's: four of six images are not drawn #102

Closed

Description

@tannevaled

What happens

qpdf's own fixture form-xobjects-some-resources2.pdf — the name is the whole
story — draws six 15×15 images on page 1 through form XObjects, some of which
carry their own /Resources and some of which carry only part of what they use.

page 1
images pdfimages -list takes out 6 (objects 10, 12, 14, 16, 18, 20)
images render.Images returns 2 (objects 10, 12)

It is not only the extraction API. Through go-pdfkit/conformance's compare
against pdftoppm at 72 dpi, on a corpus of these files alone:

pixels differing 8.54%
mean abs difference 10.936 levels
worst square mean 130.27 levels
worst pixel 254

A worst square mean of 130 is not resampling. Four of the six stamps are
missing from the page we draw.
Both form-xobjects-some-resources2.pdf and
its -out.pdf sibling are affected; a third fixture,
nested-form-xobjects-inline-images-ii-some.pdf, differs by 0.03%.

Where

Both the drawing path and the extraction path handle an absent
/Resources and neither handles an incomplete one.

xobject.go, drawForm:

resources, ok := r.doc.GetDict(stream.Dict, "Resources")
if !ok {
	resources = parent
}

images.go, imagesDrawn:

inner, ok := r.doc.GetDict(st.Dict, "Resources")
if !ok {
	inner = res
}

The comment beside the second one says "A form with no resources of its own
draws against the ones in force where it was drawn"
— which is exactly right
and exactly half the rule. A form that has /Resources but whose /XObject
(or /Font, /ColorSpace, /Pattern, /Shading, /ExtGState) does not hold
the name being used replaces the parent dictionary wholesale, so the name
resolves to nothing and drawXObject returns 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. poppler does that — hence six images against our two — and qpdf ships
this fixture precisely to exercise it.

What a fix looks like

A name is looked up in the form's own category first and then in the enclosing
one, per category rather than per dictionary. The chain has to be the enclosing
dictionaries, not the page's alone, since forms nest; maxFormDepth already
bounds how deep that goes.

How to see it

pdfimages -list -f 1 -l 1 qpdf_qtest_qpdf_form-xobjects-some-resources2.pdf   # 6 rows

against render.Images(d, 1) returning 2. In the conformance corpus the file is
pdfforms/gh-qpdf/qpdf_qtest_qpdf_form-xobjects-some-resources2.pdf; upstream it
is qpdf/qtest/qpdf/form-xobjects-some-resources2.pdf.

Found by a counter added in
conformance#92 for the
pictures the reference extracts and we do not: of gh-qpdf's 13 such rows,
three are inline images and ten are this.

🤖 Generated with Claude Code

Activity

  1. added a commit that references this issue on Oct 6, 2026
    4e54b0b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions