Files
2026-10-11 12:56:38 +08:00

171 lines
4.4 KiB
Go

// icocheck:校验生成的 .ico 结构,并把它内嵌的每个尺寸导出成 PNG,
// 方便肉眼确认缩放质量、以及圆角有没有被 premultiplied alpha 处理对。
//
// 用法:
//
// go run ./tools/icocheck build/windows/icon.ico <导出目录>
package main
import (
"bytes"
"encoding/binary"
"fmt"
"image"
"image/png"
"os"
"path/filepath"
)
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "用法: go run ./tools/icocheck <icon.ico> [导出目录]")
os.Exit(1)
}
icoPath := os.Args[1]
outDir := ""
if len(os.Args) > 2 {
outDir = os.Args[2]
}
raw, err := os.ReadFile(icoPath)
if err != nil {
fail(err)
}
fmt.Printf("文件: %s (%d 字节)\n", icoPath, len(raw))
if len(raw) < 6 {
fail(fmt.Errorf("文件太短,不是 ICO"))
}
reserved := binary.LittleEndian.Uint16(raw[0:2])
typ := binary.LittleEndian.Uint16(raw[2:4])
count := int(binary.LittleEndian.Uint16(raw[4:6]))
fmt.Printf("ICONDIR: reserved=%d type=%d (1=icon) count=%d\n", reserved, typ, count)
if reserved != 0 || typ != 1 {
fail(fmt.Errorf("头部不对:reserved=%d type=%d", reserved, typ))
}
if 6+16*count > len(raw) {
fail(fmt.Errorf("目录表超出文件长度"))
}
if outDir != "" {
if err := os.MkdirAll(outDir, 0o755); err != nil {
fail(err)
}
}
fmt.Println()
fmt.Printf("%-10s %-6s %-9s %-9s %-7s %s\n", "尺寸", "编码", "数据长度", "偏移", "实际", "四角alpha(应≈0=圆角透明)")
for i := 0; i < count; i++ {
o := 6 + 16*i
w := int(raw[o])
h := int(raw[o+1])
dim := fmt.Sprintf("%dx%d", w, h)
if w == 0 && h == 0 {
dim = "256x256"
}
size := int(binary.LittleEndian.Uint32(raw[o+8 : o+12]))
off := int(binary.LittleEndian.Uint32(raw[o+12 : o+16]))
if off < 0 || size < 0 || off+size > len(raw) {
fail(fmt.Errorf("第 %d 项越界: off=%d size=%d 文件=%d", i, off, size, len(raw)))
}
body := raw[off : off+size]
var kind string
var img image.Image
switch {
case bytes.HasPrefix(body, []byte{0x89, 'P', 'N', 'G'}):
kind = "PNG"
im, err := png.Decode(bytes.NewReader(body))
if err != nil {
fail(fmt.Errorf("第 %d 项 PNG 解不开: %v", i, err))
}
img = im
case binary.LittleEndian.Uint32(body[0:4]) == 40:
kind = "DIB"
im, err := decodeDIB(body)
if err != nil {
fail(fmt.Errorf("第 %d 项 DIB 解不开: %v", i, err))
}
img = im
default:
fail(fmt.Errorf("第 %d 项既不是 PNG 也不是 DIB: % x", i, body[:min(4, len(body))]))
}
b := img.Bounds()
if b.Dx() != b.Dy() {
fail(fmt.Errorf("第 %d 项不是正方形: %dx%d", i, b.Dx(), b.Dy()))
}
if outDir != "" {
name := filepath.Join(outDir, fmt.Sprintf("ico-%03d-%s.png", b.Dx(), kind))
f, err := os.Create(name)
if err != nil {
fail(err)
}
if err := png.Encode(f, img); err != nil {
fail(err)
}
f.Close()
}
fmt.Printf("%-10s %-6s %-9d %-9d %-7s %v\n",
dim, kind, size, off,
fmt.Sprintf("%dx%d", b.Dx(), b.Dy()),
cornerAlpha(img))
}
fmt.Println()
fmt.Println("✓ 全部条目校验通过")
}
func cornerAlpha(im image.Image) [4]uint32 {
var out [4]uint32
b := im.Bounds()
pts := [][2]int{{b.Min.X, b.Min.Y}, {b.Max.X - 1, b.Min.Y}, {b.Min.X, b.Max.Y - 1}, {b.Max.X - 1, b.Max.Y - 1}}
for i, p := range pts {
_, _, _, a := im.At(p[0], p[1]).RGBA()
out[i] = a >> 8
}
return out
}
// decodeDIB 解 BITMAPINFOHEADER + 自下而上 BGRA + AND 掩码
func decodeDIB(b []byte) (image.Image, error) {
if len(b) < 40 {
return nil, fmt.Errorf("DIB 太短")
}
w := int(int32(binary.LittleEndian.Uint32(b[4:8])))
hFull := int(int32(binary.LittleEndian.Uint32(b[8:12])))
bits := binary.LittleEndian.Uint16(b[14:16])
h := hFull / 2 // biHeight 含 AND 掩码那一半
if w <= 0 || h <= 0 {
return nil, fmt.Errorf("DIB 尺寸非法 w=%d h=%d", w, h)
}
if bits != 32 {
return nil, fmt.Errorf("只支持 32bpp,实际 %d", bits)
}
img := image.NewNRGBA(image.Rect(0, 0, w, h))
xor := b[40:]
stride := w * 4
for row := 0; row < h; row++ {
srcRow := h - 1 - row // DIB 自下而上
for x := 0; x < w; x++ {
i := srcRow*stride + x*4
if i+4 > len(xor) {
return nil, fmt.Errorf("像素数据不足")
}
o := row*img.Stride + x*4
img.Pix[o+0] = xor[i+2] // R
img.Pix[o+1] = xor[i+1] // G
img.Pix[o+2] = xor[i+0] // B
img.Pix[o+3] = xor[i+3] // A
}
}
return img, nil
}
func fail(err error) {
fmt.Fprintln(os.Stderr, "✗ "+err.Error())
os.Exit(1)
}