171 lines
4.4 KiB
Go
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)
|
|
}
|