返回 DeepSeek-Reasonix
appicon_asset_test.go
根目录 / desktop / appicon_asset_test.go
1 package main
2
3 import (
4 "bytes"
5 "encoding/binary"
6 "fmt"
7 "image"
8 "image/color"
9 "image/png"
10 "os"
11 "testing"
12 )
13
14 func TestAppIconPNGUsesBlueFullCanvasRoundedBackground(t *testing.T) {
15 f, err := os.Open("build/appicon.png")
16 if err != nil {
17 t.Fatal(err)
18 }
19 defer f.Close()
20
21 img, err := png.Decode(f)
22 if err != nil {
23 t.Fatal(err)
24 }
25
26 assertFullCanvasRoundedIcon(t, img, 1024)
27 }
28
29 func TestWindowsICOUsesBlueFullCanvasRoundedBackground(t *testing.T) {
30 for _, size := range []int{16, 24, 32, 48, 64, 256} {
31 t.Run(fmt.Sprintf("%dx%d", size, size), func(t *testing.T) {
32 img := decodeICOImage(t, "build/windows/icon.ico", size)
33 assertFullCanvasRoundedIcon(t, img, size)
34 })
35 }
36 }
37
38 func TestDarwinICNSUsesMacOSIconSafeArea(t *testing.T) {
39 img := decodeICNSImage(t, "build/darwin/icon.icns", "ic10")
40 if got, want := img.Bounds(), image.Rect(0, 0, 1024, 1024); got != want {
41 t.Fatalf("macOS ic10 frame bounds = %v, want %v", got, want)
42 }
43 if got, want := alphaBounds(img), image.Rect(100, 100, 924, 924); got != want {
44 t.Fatalf("macOS icon visible bounds = %v, want %v", got, want)
45 }
46 }
47
48 func assertFullCanvasRoundedIcon(t *testing.T, img image.Image, size int) {
49 t.Helper()
50
51 bounds := img.Bounds()
52 if bounds.Dx() != size || bounds.Dy() != size {
53 t.Fatalf("app icon must be square, got %dx%d", bounds.Dx(), bounds.Dy())
54 }
55
56 corners := []struct {
57 name string
58 x int
59 y int
60 }{
61 {"top-left", bounds.Min.X, bounds.Min.Y},
62 {"top-right", bounds.Max.X - 1, bounds.Min.Y},
63 {"bottom-left", bounds.Min.X, bounds.Max.Y - 1},
64 {"bottom-right", bounds.Max.X - 1, bounds.Max.Y - 1},
65 }
66 for _, corner := range corners {
67 _, _, _, a := img.At(corner.x, corner.y).RGBA()
68 if a > 0xff {
69 t.Fatalf("%s corner must be transparent, alpha=%d", corner.name, a)
70 }
71 }
72
73 _, _, _, centerAlpha := img.At(bounds.Min.X+bounds.Dx()/2, bounds.Min.Y+bounds.Dy()/2).RGBA()
74 if centerAlpha == 0 {
75 t.Fatal("app icon center must contain visible artwork")
76 }
77
78 edgePoints := []struct {
79 name string
80 x int
81 y int
82 }{
83 {"top", bounds.Min.X + bounds.Dx()/2, bounds.Min.Y},
84 {"right", bounds.Max.X - 1, bounds.Min.Y + bounds.Dy()/2},
85 {"bottom", bounds.Min.X + bounds.Dx()/2, bounds.Max.Y - 1},
86 {"left", bounds.Min.X, bounds.Min.Y + bounds.Dy()/2},
87 }
88 for _, point := range edgePoints {
89 _, _, _, a := img.At(point.x, point.y).RGBA()
90 if a == 0 {
91 t.Fatalf("%s edge must contain visible rounded-rect background", point.name)
92 }
93 assertReasonixBlue(t, point.name, img.At(point.x, point.y))
94 }
95 }
96
97 func assertReasonixBlue(t *testing.T, name string, colorValue color.Color) {
98 t.Helper()
99
100 r16, g16, b16, _ := colorValue.RGBA()
101 r, g, b := uint8(r16>>8), uint8(g16>>8), uint8(b16>>8)
102 if !near(r, 0x01, 2) || !near(g, 0x53, 2) || !near(b, 0xe5, 2) {
103 t.Fatalf("%s edge must use Reasonix blue background, got #%02x%02x%02x", name, r, g, b)
104 }
105 }
106
107 func near(got, want uint8, tolerance uint8) bool {
108 if got > want {
109 return got-want <= tolerance
110 }
111 return want-got <= tolerance
112 }
113
114 func alphaBounds(img image.Image) image.Rectangle {
115 bounds := img.Bounds()
116 visible := image.Rectangle{}
117 found := false
118 for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
119 for x := bounds.Min.X; x < bounds.Max.X; x++ {
120 _, _, _, a := img.At(x, y).RGBA()
121 if a == 0 {
122 continue
123 }
124 if !found {
125 visible = image.Rect(x, y, x+1, y+1)
126 found = true
127 continue
128 }
129 if x < visible.Min.X {
130 visible.Min.X = x
131 }
132 if y < visible.Min.Y {
133 visible.Min.Y = y
134 }
135 if x >= visible.Max.X {
136 visible.Max.X = x + 1
137 }
138 if y >= visible.Max.Y {
139 visible.Max.Y = y + 1
140 }
141 }
142 }
143 return visible
144 }
145
146 func decodeICNSImage(t *testing.T, path, iconType string) image.Image {
147 t.Helper()
148
149 data, err := os.ReadFile(path)
150 if err != nil {
151 t.Fatal(err)
152 }
153 if len(data) < 8 || string(data[:4]) != "icns" {
154 t.Fatal("invalid ICNS header")
155 }
156 if declaredSize := int(binary.BigEndian.Uint32(data[4:8])); declaredSize != len(data) {
157 t.Fatalf("ICNS size = %d, want %d", declaredSize, len(data))
158 }
159
160 for offset := 8; offset < len(data); {
161 if offset+8 > len(data) {
162 t.Fatalf("truncated ICNS entry header at offset %d", offset)
163 }
164 entryType := string(data[offset : offset+4])
165 entrySize := int(binary.BigEndian.Uint32(data[offset+4 : offset+8]))
166 if entrySize < 8 || offset+entrySize > len(data) {
167 t.Fatalf("invalid ICNS entry %q size %d at offset %d", entryType, entrySize, offset)
168 }
169 if entryType == iconType {
170 img, err := png.Decode(bytes.NewReader(data[offset+8 : offset+entrySize]))
171 if err != nil {
172 t.Fatalf("decode ICNS entry %q: %v", iconType, err)
173 }
174 return img
175 }
176 offset += entrySize
177 }
178
179 t.Fatalf("ICNS is missing %q image", iconType)
180 return nil
181 }
182
183 func decodeICOImage(t *testing.T, path string, size int) image.Image {
184 t.Helper()
185
186 data, err := os.ReadFile(path)
187 if err != nil {
188 t.Fatal(err)
189 }
190 r := bytes.NewReader(data)
191
192 var header struct {
193 Reserved uint16
194 Type uint16
195 Count uint16
196 }
197 if err := binary.Read(r, binary.LittleEndian, &header); err != nil {
198 t.Fatal(err)
199 }
200 if header.Reserved != 0 || header.Type != 1 {
201 t.Fatalf("invalid ICO header: reserved=%d type=%d", header.Reserved, header.Type)
202 }
203
204 type iconEntry struct {
205 Width uint8
206 Height uint8
207 ColorCount uint8
208 Reserved uint8
209 Planes uint16
210 BitCount uint16
211 BytesInRes uint32
212 ImageOffset uint32
213 }
214
215 entries := make([]iconEntry, header.Count)
216 for i := range entries {
217 if err := binary.Read(r, binary.LittleEndian, &entries[i]); err != nil {
218 t.Fatal(err)
219 }
220 }
221
222 expectedSizes := map[int]bool{16: false, 24: false, 32: false, 48: false, 64: false, 256: false}
223 targetIndex := -1
224 for i, entry := range entries {
225 width := int(entry.Width)
226 height := int(entry.Height)
227 if width == 0 {
228 width = 256
229 }
230 if height == 0 {
231 height = 256
232 }
233 if width != height {
234 t.Fatalf("ICO image must be square, got %dx%d", width, height)
235 }
236 if _, ok := expectedSizes[width]; ok {
237 expectedSizes[width] = true
238 }
239 if width == size {
240 targetIndex = i
241 }
242 }
243 for expectedSize, found := range expectedSizes {
244 if !found {
245 t.Fatalf("ICO is missing %dx%d image", expectedSize, expectedSize)
246 }
247 }
248 if targetIndex < 0 {
249 t.Fatalf("ICO is missing %dx%d image", size, size)
250 }
251
252 entry := entries[targetIndex]
253 end := int(entry.ImageOffset + entry.BytesInRes)
254 if end > len(data) {
255 t.Fatalf("ICO image offset exceeds file size: offset=%d size=%d file=%d", entry.ImageOffset, entry.BytesInRes, len(data))
256 }
257 img, err := png.Decode(bytes.NewReader(data[entry.ImageOffset:end]))
258 if err != nil {
259 t.Fatal(err)
260 }
261 return img
262 }
263
263 lines GO