09543d1e11
A pure-Go .icns writer (PNG-embedded entries from 16 to 1024) and a finder that digs the site's favicon out of a cloned mirror, preferring a large apple-touch icon and unwrapping a PNG-based .ico. These feed the bundle icon for the upcoming app formats. Adds golang.org/x/image for high-quality resampling.
136 lines
3.9 KiB
Go
136 lines
3.9 KiB
Go
package pack
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"image/color"
|
|
"image/png"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// pngBytes encodes a solid square as PNG, used to seed icon fixtures.
|
|
func pngBytes(t *testing.T, px int) []byte {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
if err := png.Encode(&buf, solid(px, px, color.RGBA{R: 0x10, G: 0x80, B: 0x40, A: 0xff})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// leWrite appends v to w in little-endian, swallowing the error a bytes.Buffer
|
|
// never returns. It keeps the .ico fixtures readable without an errcheck flag on
|
|
// every field.
|
|
func leWrite(w *bytes.Buffer, v any) { _ = binary.Write(w, binary.LittleEndian, v) }
|
|
|
|
// icoWithPNG wraps PNG payloads in a minimal .ico container so the ICO path is
|
|
// exercised without a binary fixture on disk.
|
|
func icoWithPNG(pngs [][]byte) []byte {
|
|
var dir, body bytes.Buffer
|
|
put := leWrite
|
|
put(&dir, uint16(0)) // reserved
|
|
put(&dir, uint16(1)) // type: icon
|
|
put(&dir, uint16(len(pngs))) // count
|
|
offset := 6 + len(pngs)*16
|
|
for i, p := range pngs {
|
|
// width/height bytes: 0 means 256; use a distinct small size per entry.
|
|
dim := byte(16 * (i + 1))
|
|
dir.WriteByte(dim) // width
|
|
dir.WriteByte(dim) // height
|
|
dir.WriteByte(0) // colours
|
|
dir.WriteByte(0) // reserved
|
|
put(&dir, uint16(1)) // planes
|
|
put(&dir, uint16(32)) // bit count
|
|
put(&dir, uint32(len(p))) // bytes in resource
|
|
put(&dir, uint32(offset)) // offset
|
|
offset += len(p)
|
|
body.Write(p)
|
|
}
|
|
return append(dir.Bytes(), body.Bytes()...)
|
|
}
|
|
|
|
func TestDecodeIconPNG(t *testing.T) {
|
|
p := filepath.Join(t.TempDir(), "favicon.png")
|
|
if err := os.WriteFile(p, pngBytes(t, 64), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
img, err := DecodeIcon(p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if img.Bounds().Dx() != 64 {
|
|
t.Errorf("decoded width = %d, want 64", img.Bounds().Dx())
|
|
}
|
|
}
|
|
|
|
func TestDecodeIconICOWithPNG(t *testing.T) {
|
|
// Two PNG entries; the decoder should pick the larger (second, 32px square).
|
|
ico := icoWithPNG([][]byte{pngBytes(t, 16), pngBytes(t, 32)})
|
|
p := filepath.Join(t.TempDir(), "favicon.ico")
|
|
if err := os.WriteFile(p, ico, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
img, err := DecodeIcon(p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if img.Bounds().Dx() != 32 {
|
|
t.Errorf("decoded width = %d, want the larger 32px entry", img.Bounds().Dx())
|
|
}
|
|
}
|
|
|
|
func TestDecodeIconBMPOnlyICOFails(t *testing.T) {
|
|
// A one-entry .ico whose payload is not a PNG (here just filler) must error
|
|
// so the caller falls back to the default icon.
|
|
var ico bytes.Buffer
|
|
leWrite(&ico, uint16(0))
|
|
leWrite(&ico, uint16(1))
|
|
leWrite(&ico, uint16(1))
|
|
ico.Write([]byte{32, 32, 0, 0})
|
|
leWrite(&ico, uint16(1))
|
|
leWrite(&ico, uint16(32))
|
|
leWrite(&ico, uint32(4))
|
|
leWrite(&ico, uint32(22))
|
|
ico.Write([]byte{0x42, 0x4d, 0x00, 0x00}) // "BM" DIB filler
|
|
p := filepath.Join(t.TempDir(), "favicon.ico")
|
|
if err := os.WriteFile(p, ico.Bytes(), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := DecodeIcon(p); err == nil {
|
|
t.Fatal("BMP-only .ico should fail to decode")
|
|
}
|
|
}
|
|
|
|
func TestFindIconPrefersAppleTouch(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// A tiny favicon at the root and a bigger apple-touch icon a level down.
|
|
if err := os.WriteFile(filepath.Join(dir, "favicon.png"), pngBytes(t, 16), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sub := filepath.Join(dir, "assets")
|
|
if err := os.MkdirAll(sub, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(sub, "apple-touch-icon.png"), pngBytes(t, 180), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
img, src, ok := FindIcon(dir)
|
|
if !ok {
|
|
t.Fatal("FindIcon found nothing")
|
|
}
|
|
if filepath.Base(src) != "apple-touch-icon.png" {
|
|
t.Errorf("picked %s, want apple-touch-icon.png", filepath.Base(src))
|
|
}
|
|
if img.Bounds().Dx() != 180 {
|
|
t.Errorf("width = %d, want 180", img.Bounds().Dx())
|
|
}
|
|
}
|
|
|
|
func TestFindIconNone(t *testing.T) {
|
|
if _, _, ok := FindIcon(t.TempDir()); ok {
|
|
t.Fatal("FindIcon should report nothing in an empty dir")
|
|
}
|
|
}
|