From 7871d3f870f038d146363fa731a2ed7d1872b0fb Mon Sep 17 00:00:00 2001 From: tannevaled Date: Thu, 27 Aug 2026 14:56:21 +0200 Subject: [PATCH] deps: the same file must draw the same pixels every time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reader v0.4.2 stopped letting Go's map iteration order decide what an inline image says. A dictionary may carry both spellings of a key and disagree with itself — /W 20 beside /Width 10, /CS /RGB beside /ColorSpace /3chanRGB — and expanding it in one pass over the map let the randomised order pick the winner. The consequence landed here: eight renders of safedocs' Inline_Image_Abbreviations fixture through this package gave five different pictures. before 79b9edef 79b9edef b2594aef 6bf89747 6bf89747 544c8e6b 79b9edef 43030bf3 after 87f31291 x 8 Nothing in this suite could have seen that. Every test here draws a page once, and non-determinism is invisible to a suite that never repeats itself. determinism_test.go repeats itself: it builds a one-page file whose inline image says /W 2 and /Width 1, and /H 1 and /Height 2 — the two readings give a 2x1 image and a 1x2 one, which do not cover the same pixels — and draws it forty times, requiring every draw to match the first. Against reader v0.4.1 it fails on the second draw. The image is built in the test rather than committed, so nobody else's PDF enters the repository. HOW MUCH OF THE EARLIER MEASUREMENT THIS PUTS IN DOUBT Little, and the amount is measured rather than assumed. Rendering the first page of 3 668 corpus files three times over — 1 633 real forms, 1 400 arXiv papers, all 635 vendor fixtures — turns up exactly one file that disagrees with itself, and it is that fixture. So the corpus figures in the v0.8.0 notes stand, with one page of noise in them. Co-Authored-By: Claude Opus 5 --- determinism_test.go | 67 +++++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +-- 3 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 determinism_test.go diff --git a/determinism_test.go b/determinism_test.go new file mode 100644 index 0000000..178b376 --- /dev/null +++ b/determinism_test.go @@ -0,0 +1,67 @@ +package render + +import ( + "crypto/sha256" + "testing" + + "github.com/go-pdfkit/reader" +) + +// TestTheSameFileDrawsTheSamePixelsEveryTime guards the dependency, not this +// package's own code. +// +// An inline image dictionary may carry both spellings of a key — /W beside +// /Width, /CS beside /ColorSpace — and disagree with itself. reader before +// v0.4.2 expanded such a dictionary in one pass over its map, so the winner +// was whichever Go's randomised iteration order yielded last, and the same +// page drew a different picture on different runs of the same binary. Eight +// renders of safedocs' Inline_Image_Abbreviations fixture gave five different +// answers. +// +// Nothing in this package's own tests could see that: every one of them draws +// a page once. Non-determinism is invisible to a suite that never repeats +// itself, which is why this test repeats itself. +// +// The image is built here rather than committed, so nobody else's PDF enters +// the repository. It says /W 2 and /Width 1, and /H 1 and /Height 2: the two +// readings give a 2x1 image and a 1x2 one, which do not cover the same pixels. +func TestTheSameFileDrawsTheSamePixelsEveryTime(t *testing.T) { + w := reader.NewWriter("1.7") + pagesRef := w.Reserve() + content := "q 20 0 0 20 0 0 cm " + + "BI /W 2 /Width 1 /H 1 /Height 2 /CS /G /ColorSpace /DeviceGray /BPC 8 " + + "ID \x00\xff EI Q" + pageRef := w.Add(reader.Dict{"Type": reader.Name("Page"), "Parent": pagesRef, + "MediaBox": nums(0, 0, 20, 20), + "Contents": w.Add(&reader.Stream{Dict: reader.Dict{}, Raw: []byte(content)})}) + w.Put(pagesRef, reader.Dict{"Type": reader.Name("Pages"), + "Kids": reader.Array{pageRef}, "Count": reader.Integer(1)}) + out, err := w.Finish(reader.Dict{"Root": w.Add(reader.Dict{ + "Type": reader.Name("Catalog"), "Pages": pagesRef})}) + if err != nil { + t.Fatal(err) + } + + var first string + for i := 0; i < 40; i++ { + d, err := reader.Open(out) + if err != nil { + t.Fatal(err) + } + img, err := Page(d, 1, Options{Scale: 1}) + if err != nil { + t.Fatal(err) + } + h := sha256.Sum256(img.Pix) + got := string(h[:]) + if i == 0 { + first = got + continue + } + if got != first { + t.Fatalf("draw %d differs from the first: the same file drew "+ + "different pixels, so something in the read path depends on "+ + "map iteration order", i) + } + } +} diff --git a/go.mod b/go.mod index bfc683a..4f5c08b 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/go-gfx/gfx v0.10.0 github.com/go-opentype/fonts v0.9.0 github.com/go-opentype/opentype v0.10.0 - github.com/go-pdfkit/reader v0.4.1 + github.com/go-pdfkit/reader v0.4.2 ) require github.com/go-pdfkit/pdffont v0.3.0 diff --git a/go.sum b/go.sum index c500cb1..96588d0 100644 --- a/go.sum +++ b/go.sum @@ -6,5 +6,5 @@ github.com/go-opentype/opentype v0.10.0 h1:cZVMZ3RVkcijXmxlmpqyVkjlNc33aSB2ugKVW github.com/go-opentype/opentype v0.10.0/go.mod h1:AOixevJf7XQaH7+WG+OMIOZEbYPXfMqklVk26Y6YTUU= github.com/go-pdfkit/pdffont v0.3.0 h1:G5DKcAmsZJ0e17QhSrcUaL7PKEXKjUx4P/iGMl7bAbI= github.com/go-pdfkit/pdffont v0.3.0/go.mod h1:bfmNLna1l1CljNX/Utg55YzFylovfmI7sJnvgA3bzKI= -github.com/go-pdfkit/reader v0.4.1 h1:pRxFqRjsn7H/VsGfWb9nYWyFuDgTU2Pjmoq/f5mgVq4= -github.com/go-pdfkit/reader v0.4.1/go.mod h1:fQFOVfCMUui1AdvD4qhimdyvvNr9KvvJ1S7IuKZjyV8= +github.com/go-pdfkit/reader v0.4.2 h1:0/qPShLzdG/roH4i13dwDTHnS7mEBVTaCBGwCZQhOH0= +github.com/go-pdfkit/reader v0.4.2/go.mod h1:fQFOVfCMUui1AdvD4qhimdyvvNr9KvvJ1S7IuKZjyV8=