Files
oxpoe 59145910b5 Ikon: cari per domain/URL gambar + semua format dikonversi ke PNG
- Dialog ikon: nama aplikasi -> Iconify; domain (mikrotik.com) -> ikon resmi situs
  (manifest 512, apple-touch-icon, favicon) + layanan favicon; URL gambar -> langsung
- Contoh kasus: 'winbox' cuma punya 1 ikon tidak nyambung di Iconify, tapi
  'mikrotik.com' memberi ikon resminya (5 pilihan, semuanya PNG)
- Decoder ICO: entri BMP 24/32-bit (favicon.ico lama) ikut didukung, bukan cuma PNG
- FromURL/Fetch selalu mengembalikan PNG; hasil raster/konversi seragam 512px
- Tes: deteksi URL gambar/domain, daftar ikon situs, decode ICO BMP, konversi format
2026-10-13 08:00:12 +07:00

110 lines
3.0 KiB
Go

package icon
import (
"bytes"
"encoding/binary"
"errors"
"image"
"image/color"
"image/png"
)
// decodeICO mengambil gambar terbesar di dalam file .ico.
//
// ICO bisa berisi PNG (modern) atau DIB/BMP (format lama, masih sering dipakai
// favicon). Keduanya didukung: BMP 32-bit pakai alpha, 24-bit pakai AND mask.
// Entri dengan palet (<= 8-bit) tidak didukung.
func decodeICO(data []byte) (image.Image, error) {
if pngData := largestPNGFromICO(data); pngData != nil {
return png.Decode(bytes.NewReader(pngData))
}
if len(data) < 22 || data[0] != 0 || data[1] != 0 || data[2] != 1 || data[3] != 0 {
return nil, errors.New("bukan file ICO")
}
count := int(data[4]) | int(data[5])<<8
var best image.Image
bestSize := 0
for i := 0; i < count; i++ {
off := 6 + i*16
if off+16 > len(data) {
break
}
width := int(data[off])
if width == 0 {
width = 256
}
size := int(data[off+8]) | int(data[off+9])<<8 | int(data[off+10])<<16 | int(data[off+11])<<24
start := int(data[off+12]) | int(data[off+13])<<8 | int(data[off+14])<<16 | int(data[off+15])<<24
if size <= 0 || start <= 0 || start+size > len(data) {
continue
}
if width*width <= bestSize {
continue
}
img, err := decodeDIB(data[start : start+size])
if err != nil {
continue
}
best, bestSize = img, width*width
}
if best == nil {
return nil, errors.New("tidak ada gambar yang bisa dibaca di dalam .ico")
}
return best, nil
}
// decodeDIB membaca gambar DIB/BMP di dalam entri ICO.
func decodeDIB(dib []byte) (image.Image, error) {
if len(dib) < 40 {
return nil, errors.New("entri ICO terlalu pendek")
}
headerSize := int(binary.LittleEndian.Uint32(dib[0:4]))
width := int(int32(binary.LittleEndian.Uint32(dib[4:8])))
height := int(int32(binary.LittleEndian.Uint32(dib[8:12]))) / 2 // tinggi DIB = XOR + AND mask
bitCount := int(binary.LittleEndian.Uint16(dib[14:16]))
compression := int(binary.LittleEndian.Uint32(dib[16:20]))
if compression != 0 {
return nil, errors.New("entri ICO terkompresi tidak didukung")
}
if width <= 0 || height <= 0 || width > 512 || height > 512 {
return nil, errors.New("ukuran entri ICO tidak wajar")
}
if bitCount != 24 && bitCount != 32 {
return nil, errors.New("hanya ICO 24/32-bit yang didukung")
}
stride := (width*bitCount/8 + 3) & ^3
start := headerSize
if start+stride*height > len(dib) {
return nil, errors.New("data piksel ICO terpotong")
}
img := image.NewNRGBA(image.Rect(0, 0, width, height))
allTransparent := bitCount == 32
for y := 0; y < height; y++ {
row := dib[start+(height-1-y)*stride:] // DIB disimpan dari bawah ke atas
for x := 0; x < width; x++ {
px := row[x*bitCount/8:]
var alpha uint8 = 0xFF
if bitCount == 32 {
alpha = px[3]
if alpha != 0 {
allTransparent = false
}
}
img.SetNRGBA(x, y, color.NRGBA{R: px[2], G: px[1], B: px[0], A: alpha})
}
}
// sebagian ICO 32-bit tidak mengisi kanal alpha sama sekali
if allTransparent {
for i := 3; i < len(img.Pix); i += 4 {
img.Pix[i] = 0xFF
}
}
return img, nil
}