first commit
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user