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 }