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
60 changes: 60 additions & 0 deletions images.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
132 changes: 132 additions & 0 deletions inlineimages_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
4 changes: 4 additions & 0 deletions state.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down
Loading