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