test(thumbhash): dither the fixtures so no DCT coefficient is degenerate
The gradient fixtures are smooth analytic ramps, and alpha.png's alpha channel varied along x only. Both make whole families of DCT coefficients mathematically zero: 13 of alpha.png's 38 AC nibbles, and at least one in every other ramp fixture. A zero coefficient normalizes to exactly the 0.5 midpoint, i.e. 15*f = 7.5, so which of nibble 7 or 8 it quantizes to is decided by ~1e-16 of float rounding noise. Those nibbles are therefore unstable across any two summation orders -- the vendored JS and the Go port already disagreed on solid.png for this reason -- and they carry no signal, so a bug that transposed two of them would be invisible. A deterministic +/-4 dither, plus an alpha ramp that varies in x and y, leaves every coefficient at least 4.8e-5 from the tie boundary: ~1e9 times the observed inter- implementation noise. solid.png and tiny.png regenerate byte-identical and keep their existing carve-outs.
BIN
core/artwork/thumbhash/testdata/alpha.png
vendored
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 24 KiB |
27
core/artwork/thumbhash/testdata/gen_fixtures.mjs
vendored
@ -39,7 +39,20 @@ const png = (w, h, rgba) => {
|
||||
])
|
||||
}
|
||||
|
||||
// A ramp has exactly-zero DCT coefficients, and a zero coefficient's nibble is decided by float
|
||||
// rounding noise, so no two summation orders agree on it. Dither gives every coefficient signal.
|
||||
let seed = 0
|
||||
const dither = () => {
|
||||
seed ^= seed << 13
|
||||
seed ^= seed >>> 17
|
||||
seed ^= seed << 5
|
||||
seed >>>= 0
|
||||
return (seed % 9) - 4
|
||||
}
|
||||
const shade = (v) => Math.max(0, Math.min(255, v + dither()))
|
||||
|
||||
const make = (w, h, fn) => {
|
||||
seed = 0x9e3779b9
|
||||
const rgba = new Uint8Array(w * h * 4)
|
||||
for (let y = 0; y < h; y++)
|
||||
for (let x = 0; x < w; x++) fn(rgba, (y * w + x) * 4, x, y, w, h)
|
||||
@ -47,9 +60,9 @@ const make = (w, h, fn) => {
|
||||
}
|
||||
|
||||
const gradient = (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] = shade(Math.floor((255 * x) / w))
|
||||
rgba[i + 1] = shade(Math.floor((255 * y) / h))
|
||||
rgba[i + 2] = shade(Math.floor((255 * (x + y)) / (w + h)))
|
||||
rgba[i + 3] = 255
|
||||
}
|
||||
const solid = (rgba, i) => {
|
||||
@ -61,10 +74,10 @@ const solid = (rgba, i) => {
|
||||
// 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)
|
||||
rgba[i] = shade(Math.floor((255 * x) / w))
|
||||
rgba[i + 1] = shade(Math.floor((255 * y) / h))
|
||||
rgba[i + 2] = shade(Math.floor((255 * (x + y)) / (w + h)))
|
||||
rgba[i + 3] = shade(Math.floor((255 * (x + 2 * y)) / (w + 2 * h)))
|
||||
}
|
||||
|
||||
const out = new URL('.', import.meta.url).pathname
|
||||
|
||||
8
core/artwork/thumbhash/testdata/golden.json
vendored
@ -1,8 +1,8 @@
|
||||
{
|
||||
"alpha.png": "JFiKBQw3s4ewiId3eBtPOfuEgHd4iIh4eA==",
|
||||
"landscape.png": "3wcOFJpwd3dxd3eHh3ePgAj4hw==",
|
||||
"portrait.png": "3/cNFBpxB4d3d3d4d3eAjwj3eA==",
|
||||
"alpha.png": "JOiFBQ4nkIexh3p4iA8uB+lYhIeAh3d4dw==",
|
||||
"landscape.png": "3wcOFJpwh4eBh3d4iIePgAj3hw==",
|
||||
"portrait.png": "3/cNFBqBB4iId4d3d4iAjwj4hw==",
|
||||
"solid.png": "HoUBBwB4eHeHd3hweId3h3h4B2+Ih4gA",
|
||||
"square.png": "HwgOBxpwd4dwd3h3h3eHd3d3+PiIgI8H",
|
||||
"square.png": "H/gNBxpwh4dwd3eIiHd3iHeHeJ+dcH8I",
|
||||
"tiny.png": "HoU9tx4I9wiIh4hwj3CI+AiIcH/494cP"
|
||||
}
|
||||
|
||||
BIN
core/artwork/thumbhash/testdata/landscape.png
vendored
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 20 KiB |
BIN
core/artwork/thumbhash/testdata/portrait.png
vendored
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 20 KiB |
BIN
core/artwork/thumbhash/testdata/square.png
vendored
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |