diff --git a/core/artwork/thumbhash/reference_test.go b/core/artwork/thumbhash/reference_test.go index b3d7453c3..a23b95526 100644 --- a/core/artwork/thumbhash/reference_test.go +++ b/core/artwork/thumbhash/reference_test.go @@ -53,14 +53,10 @@ func loadGoldens() map[string]string { } var _ = Describe("reference port", func() { - // solid.png and alpha.png are exactly uniform in color, so their true DCT AC term is 0 and the - // hash instead encodes float rounding noise from cos(), where Go and V8 disagree in the last bit. - floatUnstable := map[string]bool{"solid.png": true, "alpha.png": true} - It("reproduces every golden vector", func() { for name, want := range loadGoldens() { - if floatUnstable[name] { - continue + if name == "solid.png" { + continue // see the dedicated header-only spec below } w, h, rgba := loadFixture(name) got := base64.StdEncoding.EncodeToString(referenceEncode(w, h, rgba)) @@ -68,6 +64,22 @@ var _ = Describe("reference port", func() { } }) + // solid.png is uniform, so its true AC term is 0 and the golden's AC nibbles are just float + // rounding noise from cos(); only the header (DC terms + scales, which quantize to 0) is well-defined. + It("reproduces the well-conditioned header of a uniform image", func() { + want, err := base64.StdEncoding.DecodeString(loadGoldens()["solid.png"]) + Expect(err).ToNot(HaveOccurred()) + w, h, rgba := loadFixture("solid.png") + got := referenceEncode(w, h, rgba) + Expect(got[:5]).To(Equal(want[:5]), "header bytes") + + header24 := int(got[0]) | int(got[1])<<8 | int(got[2])<<16 + header16 := int(got[3]) | int(got[4])<<8 + Expect((header24>>18)&31).To(Equal(0), "lScale") + Expect((header16>>3)&63).To(Equal(0), "pScale") + Expect((header16>>9)&63).To(Equal(0), "qScale") + }) + It("produces 24 bytes for a square opaque image", func() { w, h, rgba := loadFixture("square.png") Expect(referenceEncode(w, h, rgba)).To(HaveLen(24)) @@ -122,7 +134,7 @@ func referenceEncode(w, h int, rgba []byte) []byte { encodeChannel := func(channel []float64, nx, ny int) (dc float64, ac []float64, scale float64) { fx := make([]float64, w) - for cy := 0; cy < ny; cy++ { + for cy := range ny { for cx := 0; cx*ny < nx*(ny-cy); cx++ { f := 0.0 for x := range w { diff --git a/core/artwork/thumbhash/testdata/alpha.png b/core/artwork/thumbhash/testdata/alpha.png index 5770c696a..abcefdd5f 100644 Binary files a/core/artwork/thumbhash/testdata/alpha.png and b/core/artwork/thumbhash/testdata/alpha.png differ diff --git a/core/artwork/thumbhash/testdata/gen_fixtures.mjs b/core/artwork/thumbhash/testdata/gen_fixtures.mjs index f6c0bef16..68ddab959 100644 --- a/core/artwork/thumbhash/testdata/gen_fixtures.mjs +++ b/core/artwork/thumbhash/testdata/gen_fixtures.mjs @@ -58,10 +58,12 @@ const solid = (rgba, i) => { rgba[i + 2] = 180 rgba[i + 3] = 255 } -const alphaRamp = (rgba, i, x, y, w) => { - rgba[i] = 200 - rgba[i + 1] = 50 - rgba[i + 2] = 90 +// RGB must vary with position too: a constant color composited over its own average cancels to a +// flat L/P/Q (the bug this fixture exists to catch), so pair a color gradient with the alpha ramp. +const alphaRamp = (rgba, i, x, y, w, h) => { + rgba[i] = Math.floor((255 * x) / w) + rgba[i + 1] = Math.floor((255 * y) / h) + rgba[i + 2] = Math.floor((255 * (x + y)) / (w + h)) rgba[i + 3] = Math.floor((255 * x) / w) } diff --git a/core/artwork/thumbhash/testdata/golden.json b/core/artwork/thumbhash/testdata/golden.json index 83fc2fdde..e38b76638 100644 --- a/core/artwork/thumbhash/testdata/golden.json +++ b/core/artwork/thumbhash/testdata/golden.json @@ -1,5 +1,5 @@ { - "alpha.png": "HCmDBQA3j3mHeHeXiGCI94h3gHd4iIh4eA==", + "alpha.png": "JFiKBQw3s4ewiId3eBtPOfuEgHd4iIh4eA==", "landscape.png": "3wcOFJpwd3dxd3eHh3ePgAj4hw==", "portrait.png": "3/cNFBpxB4d3d3d4d3eAjwj3eA==", "solid.png": "HoUBBwB4eHeHd3hweId3h3h4B2+Ih4gA",