v2 / vlib / v / builder / icon.v
257 lines · 237 sloc · 7.39 KB · bdc2b4357bd8710471957b96a2c54796fa1c4e08
Raw
1module builder
2
3import os
4
5const windows_icon_group_resource_id = 1
6const max_windows_icon_dimension = 256
7
8struct WindowsIconImage {
9 width u8
10 height u8
11 color_count u8
12 planes u16
13 bit_count u16
14 bytes_in_res u32
15 image_data []u8
16}
17
18struct WindowsIconSize {
19 width int
20 height int
21}
22
23fn (b &Builder) ensure_windows_icon_flag_is_valid() {
24 if b.pref.icon_path == '' {
25 return
26 }
27 if b.pref.os != .windows || b.pref.build_mode == .build_module || b.pref.is_o
28 || b.pref.is_shared {
29 verror('`-icon` is supported only when building Windows executables')
30 }
31 if b.pref.generate_c_project != '' || b.pref.out_name.ends_with('.c')
32 || b.pref.out_name.ends_with('.js') || b.pref.should_output_to_stdout() {
33 verror('`-icon` cannot be used when emitting generated C/JS output instead of a Windows executable')
34 }
35}
36
37fn (mut b Builder) prepare_cross_windows_icon_resource() !string {
38 if b.pref.icon_path == '' {
39 return ''
40 }
41 ico_path := b.prepare_windows_icon_ico_path()!
42 rc_path := b.get_vtmp_filename(b.pref.out_name, '.icon.rc')
43 obj_path := b.get_vtmp_filename(b.pref.out_name, '.icon.o')
44 os.write_file(rc_path, '1 ICON ${rc_quoted_string(ico_path)}\n')!
45 b.pref.cleanup_files << rc_path
46 b.pref.cleanup_files << obj_path
47 windres := b.find_windres()!
48 cmd := '${os.quoted_path(windres)} -i ${os.quoted_path(rc_path)} -o ${os.quoted_path(obj_path)} -O coff'
49 res := os.execute(cmd)
50 if res.exit_code != 0 {
51 return error('failed to compile Windows icon resource with `${windres}`: ${res.output.trim_space()}')
52 }
53 return obj_path
54}
55
56fn (mut b Builder) prepare_windows_icon_ico_path() !string {
57 if b.pref.icon_path == '' {
58 return ''
59 }
60 icon_path := os.real_path(b.pref.icon_path)
61 if !os.is_file(icon_path) {
62 return error('icon file `${icon_path}` does not exist')
63 }
64 match os.file_ext(icon_path).to_lower_ascii() {
65 '.ico' {
66 return icon_path
67 }
68 '.png' {
69 png_bytes := os.read_bytes(icon_path)!
70 ico_bytes := png_to_ico_bytes(png_bytes)!
71 ico_path := b.get_vtmp_filename(b.pref.out_name, '.icon.ico')
72 os.write_file_array(ico_path, ico_bytes)!
73 b.pref.cleanup_files << ico_path
74 return ico_path
75 }
76 else {
77 return error('`-icon` accepts only `.ico` or `.png` files')
78 }
79 }
80}
81
82fn (b &Builder) find_windres() !string {
83 compiler_dir := if b.pref.ccompiler.contains('/') || b.pref.ccompiler.contains('\\') {
84 os.dir(b.pref.ccompiler)
85 } else {
86 ''
87 }
88 compiler_name := executable_stem(os.file_name(b.pref.ccompiler))
89 mut candidates := []string{}
90 for suffix in ['-gcc', '-clang', '-cc', '-g++', '-clang++'] {
91 if compiler_name.ends_with(suffix) {
92 candidates << '${compiler_name[..compiler_name.len - suffix.len]}-windres'
93 }
94 }
95 candidates << 'windres'
96 candidates << 'llvm-windres'
97 for candidate in candidates {
98 for name in [candidate, '${candidate}.exe'] {
99 if compiler_dir != '' {
100 full_path := os.join_path(compiler_dir, name)
101 if os.is_file(full_path) {
102 return full_path
103 }
104 }
105 if resolved := os.find_abs_path_of_executable(name) {
106 return resolved
107 }
108 }
109 }
110 return error('could not find `windres`, which is needed for `-icon` while cross-compiling to Windows')
111}
112
113fn executable_stem(name string) string {
114 lower_name := name.to_lower_ascii()
115 if lower_name.ends_with('.exe') {
116 return name[..name.len - 4]
117 }
118 return name
119}
120
121fn rc_quoted_string(path string) string {
122 return '"' + path.replace('\\', '\\\\').replace('"', '\\"') + '"'
123}
124
125fn parse_ico_file(path string) ![]WindowsIconImage {
126 return parse_ico_bytes(os.read_bytes(path)!)
127}
128
129fn parse_ico_bytes(data []u8) ![]WindowsIconImage {
130 if data.len < 6 {
131 return error('invalid icon file: missing ICO header')
132 }
133 if read_le_u16(data, 0) != 0 || read_le_u16(data, 2) != 1 {
134 return error('invalid icon file: expected an ICO header')
135 }
136 image_count := int(read_le_u16(data, 4))
137 if image_count <= 0 {
138 return error('invalid icon file: no icon images were found')
139 }
140 if data.len < 6 + (image_count * 16) {
141 return error('invalid icon file: truncated icon directory')
142 }
143 mut images := []WindowsIconImage{cap: image_count}
144 for i := 0; i < image_count; i++ {
145 entry_offset := 6 + (i * 16)
146 image_size := int(read_le_u32(data, entry_offset + 8))
147 image_offset := int(read_le_u32(data, entry_offset + 12))
148 image_end := image_offset + image_size
149 if image_offset < 0 || image_size <= 0 || image_offset > data.len || image_end > data.len {
150 return error('invalid icon file: icon image ${i + 1} points outside the file')
151 }
152 images << WindowsIconImage{
153 width: data[entry_offset]
154 height: data[entry_offset + 1]
155 color_count: data[entry_offset + 2]
156 planes: read_le_u16(data, entry_offset + 4)
157 bit_count: read_le_u16(data, entry_offset + 6)
158 bytes_in_res: u32(image_size)
159 image_data: data[image_offset..image_end].clone()
160 }
161 }
162 return images
163}
164
165fn png_to_ico_bytes(png []u8) ![]u8 {
166 size := png_dimensions(png)!
167 mut ico := []u8{cap: 22 + png.len}
168 append_le_u16(mut ico, 0)
169 append_le_u16(mut ico, 1)
170 append_le_u16(mut ico, 1)
171 ico << u8(if size.width == max_windows_icon_dimension { 0 } else { size.width })
172 ico << u8(if size.height == max_windows_icon_dimension { 0 } else { size.height })
173 ico << u8(0)
174 ico << u8(0)
175 append_le_u16(mut ico, 1)
176 append_le_u16(mut ico, 32)
177 append_le_u32(mut ico, u32(png.len))
178 append_le_u32(mut ico, u32(22))
179 ico << png
180 return ico
181}
182
183fn png_dimensions(png []u8) !WindowsIconSize {
184 if png.len < 24 {
185 return error('invalid PNG icon file: missing PNG header')
186 }
187 if png[0] != 0x89 || png[1] != `P` || png[2] != `N` || png[3] != `G` || png[4] != 0x0d
188 || png[5] != 0x0a || png[6] != 0x1a || png[7] != 0x0a {
189 return error('invalid PNG icon file: bad PNG signature')
190 }
191 if png[12] != `I` || png[13] != `H` || png[14] != `D` || png[15] != `R` {
192 return error('invalid PNG icon file: missing IHDR chunk')
193 }
194 width := int(read_be_u32(png, 16))
195 height := int(read_be_u32(png, 20))
196 if width <= 0 || height <= 0 || width > max_windows_icon_dimension
197 || height > max_windows_icon_dimension {
198 return error('PNG icons must be between 1x1 and 256x256 pixels')
199 }
200 return WindowsIconSize{
201 width: width
202 height: height
203 }
204}
205
206fn build_group_icon_resource(images []WindowsIconImage) []u8 {
207 mut data := []u8{cap: 6 + (images.len * 14)}
208 append_le_u16(mut data, 0)
209 append_le_u16(mut data, 1)
210 append_le_u16(mut data, u16(images.len))
211 for i, image in images {
212 data << image.width
213 data << image.height
214 data << image.color_count
215 data << u8(0)
216 append_le_u16(mut data, image.planes)
217 append_le_u16(mut data, image.bit_count)
218 append_le_u32(mut data, image.bytes_in_res)
219 append_le_u16(mut data, u16(i + 1))
220 }
221 return data
222}
223
224fn append_le_u16(mut data []u8, value u16) {
225 data << u8(value & 0xff)
226 data << u8(value >> 8)
227}
228
229fn append_le_u32(mut data []u8, value u32) {
230 data << u8(value & 0xff)
231 data << u8((value >> 8) & 0xff)
232 data << u8((value >> 16) & 0xff)
233 data << u8(value >> 24)
234}
235
236@[direct_array_access; inline]
237fn read_le_u16(data []u8, offset int) u16 {
238 return u16(data[offset]) | (u16(data[offset + 1]) << 8)
239}
240
241@[direct_array_access; inline]
242fn read_le_u32(data []u8, offset int) u32 {
243 b0 := u32(data[offset])
244 b1 := u32(data[offset + 1])
245 b2 := u32(data[offset + 2])
246 b3 := u32(data[offset + 3])
247 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
248}
249
250@[direct_array_access; inline]
251fn read_be_u32(data []u8, offset int) u32 {
252 b0 := u32(data[offset])
253 b1 := u32(data[offset + 1])
254 b2 := u32(data[offset + 2])
255 b3 := u32(data[offset + 3])
256 return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3
257}
258