// 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 [导出目录]") 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) }