fix(thumbhash): re-condition alpha fixture and assert solid.png header

alpha.png varied only alpha over a constant color, so compositing atop the
average canceled L/P/Q exactly like solid.png, leaving it just as float-noise
unstable and silently dropping alpha-path coverage. Vary R/G/B with position
too so alpha.png carries real signal and reproduces byte-exactly, then
replace the blanket solid.png skip with a precise assertion on its
well-conditioned header bytes and quantized-zero scales.

Also apply a range-over-int modernizer hint in reference_test.go.
This commit is contained in:
Deluan 2026-07-25 15:36:26 -04:00
parent 0c78c65b3c
commit 02d253b2b0
4 changed files with 26 additions and 12 deletions

View File

@ -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 {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 433 B

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -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)
}

View File

@ -1,5 +1,5 @@
{
"alpha.png": "HCmDBQA3j3mHeHeXiGCI94h3gHd4iIh4eA==",
"alpha.png": "JFiKBQw3s4ewiId3eBtPOfuEgHd4iIh4eA==",
"landscape.png": "3wcOFJpwd3dxd3eHh3ePgAj4hw==",
"portrait.png": "3/cNFBpxB4d3d3d4d3eAjwj3eA==",
"solid.png": "HoUBBwB4eHeHd3hweId3h3h4B2+Ih4gA",