- 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
110 lines
3.0 KiB
Go
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
|
|
}
|