package artwork
import (
"image"
"image/color"
"image/jpeg"
"image/png"
"os"
"path/filepath"
"testing"
)
// picture draws something with structure, so a difference hash has real
// gradients to work with rather than one flat colour.
func picture(n int, variant int) image.Image {
img := image.NewRGBA(image.Rect(0, 0, n, n))
for y := 0; y < n; y++ {
for x := 0; x < n; x++ {
r := uint8((x * 255 / n) % 256)
g := uint8((y * 255 / n) % 256)
b := uint8(0)
switch variant {
case 1:
b = uint8((x + y) * 255 / (2 * n))
case 2:
// a visibly different picture: a checker pattern with the
// gradients reversed, so it differs in both directions
if (x/(n/8)+y/(n/8))%2 == 0 {
r, g = 255-r, 255-g
}
b = 200
}
img.Set(x, y, color.RGBA{r, g, b, 255})
}
}
return img
}
func savePNG(t *testing.T, path string, img image.Image) {
t.Helper()
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()
if err := png.Encode(f, img); err != nil {
t.Fatal(err)
}
}
func saveJPEG(t *testing.T, path string, img image.Image) {
t.Helper()
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()
if err := jpeg.Encode(f, img, nil); err != nil {
t.Fatal(err)
}
}
// The same artwork at two sizes or in another format is one picture, and the
// point of the change is that the picker should say so.
func TestVisualHashSeesTheSamePictureAtAnotherSize(t *testing.T) {
dir := t.TempDir()
small := filepath.Join(dir, "small.png")
large := filepath.Join(dir, "large.png")
reencoded := filepath.Join(dir, "reencoded.jpg")
savePNG(t, small, picture(300, 1))
savePNG(t, large, picture(1200, 1))
saveJPEG(t, reencoded, picture(800, 1))
hs, ok := visualHash(small)
if !ok {
t.Fatal("could not hash the small image")
}
hl, _ := visualHash(large)
hr, _ := visualHash(reencoded)
if !hs.near(hl) {
t.Errorf("300px and 1200px versions of one picture read as different")
}
if !hs.near(hr) {
t.Errorf("a jpeg re-encode read as a different picture")
}
}
func TestVisualHashSeparatesDifferentPictures(t *testing.T) {
dir := t.TempDir()
a := filepath.Join(dir, "a.png")
b := filepath.Join(dir, "b.png")
savePNG(t, a, picture(400, 1))
savePNG(t, b, picture(400, 2))
ha, _ := visualHash(a)
hb, _ := visualHash(b)
if ha.near(hb) {
t.Errorf("two different pictures were read as the same")
}
}
func TestVisualHashOnSomethingThatIsNotAnImage(t *testing.T) {
dir := t.TempDir()
junk := filepath.Join(dir, "cover.jpg")
if err := os.WriteFile(junk, []byte("not an image"), 0o600); err != nil {
t.Fatal(err)
}
if _, ok := visualHash(junk); ok {
t.Error("nonsense should not produce a hash")
}
if _, ok := visualHash(filepath.Join(dir, "missing.png")); ok {
t.Error("a missing file should not produce a hash")
}
}
// The picker offered the same sleeve several times at several sizes. Dedupe
// works on what the picture looks like, so those collapse to one entry.
func TestDedupeCollapsesTheSameArtworkAtDifferentSizes(t *testing.T) {
dir := t.TempDir()
var in []Candidate
for i, n := range []int{600, 1200, 800} {
p := filepath.Join(dir, string(rune('a'+i))+".png")
savePNG(t, p, picture(n, 1))
in = append(in, Candidate{
Source: SourceCoverArt, Label: "coverartarchive",
Path: p, Width: n, Height: n,
})
}
// and a genuinely different cover, which must survive
other := filepath.Join(dir, "other.png")
savePNG(t, other, picture(1000, 2))
in = append(in, Candidate{Source: SourceDeezer, Label: "deezer", Path: other, Width: 1000, Height: 1000})
got := dedupe(in)
if len(got) != 2 {
t.Fatalf("deduped to %d candidates, want 2: %s", len(got), describe(got))
}
if got[0].Width != 600 {
t.Errorf("kept the %dpx copy; the first one should win", got[0].Width)
}
}
func describe(cs []Candidate) string {
s := ""
for _, c := range cs {
s += c.Path + " "
}
return s
}