1use crate::{
6 Cached,
7 embedded_assets::{EmbeddedAssetsError, EmbeddedAssetsResult},
8};
9use proc_macro2::TokenStream;
10use quote::{ToTokens, TokenStreamExt, quote};
11use std::{ffi::OsStr, io::Cursor, path::Path};
12
13pub(crate) enum IconFormat {
15 Raw,
17
18 Image { width: u32, height: u32 },
20}
21
22pub struct CachedIcon {
23 cache: Cached,
24 format: IconFormat,
25 root: TokenStream,
26}
27
28impl CachedIcon {
29 pub fn new(root: &TokenStream, icon: &Path) -> EmbeddedAssetsResult<Self> {
30 match icon.extension().map(OsStr::to_string_lossy).as_deref() {
31 Some("png") => Self::new_png(root, icon),
32 Some("ico") => Self::new_ico(root, icon),
33 unknown => Err(EmbeddedAssetsError::InvalidImageExtension {
34 extension: unknown.unwrap_or_default().into(),
35 path: icon.to_path_buf(),
36 }),
37 }
38 }
39
40 pub fn new_raw(root: &TokenStream, icon: &Path) -> EmbeddedAssetsResult<Self> {
42 let buf = Self::open(icon);
43 Cached::try_from(buf).map(|cache| Self {
44 cache,
45 root: root.clone(),
46 format: IconFormat::Raw,
47 })
48 }
49
50 pub fn new_ico(root: &TokenStream, icon: &Path) -> EmbeddedAssetsResult<Self> {
52 let buf = Self::open(icon);
53
54 let icon_dir = ico::IconDir::read(Cursor::new(&buf))
55 .unwrap_or_else(|e| panic!("failed to parse icon {}: {}", icon.display(), e));
56
57 let entry = largest_ico_entry(&icon_dir)
58 .unwrap_or_else(|| panic!("icon {} has no entries", icon.display()));
59 let rgba = entry
60 .decode()
61 .unwrap_or_else(|e| panic!("failed to decode icon {}: {}", icon.display(), e))
62 .rgba_data()
63 .to_vec();
64
65 Cached::try_from(rgba).map(|cache| Self {
66 cache,
67 root: root.clone(),
68 format: IconFormat::Image {
69 width: entry.width(),
70 height: entry.height(),
71 },
72 })
73 }
74
75 pub fn new_png(root: &TokenStream, icon: &Path) -> EmbeddedAssetsResult<Self> {
77 let buf = Self::open(icon);
78 let decoder = png::Decoder::new(Cursor::new(&buf));
79 let mut reader = decoder
80 .read_info()
81 .unwrap_or_else(|e| panic!("failed to read icon {}: {}", icon.display(), e));
82
83 if reader.output_color_type().0 != png::ColorType::Rgba {
84 panic!("icon {} is not RGBA", icon.display());
85 }
86
87 let mut rgba = Vec::with_capacity(reader.output_buffer_size().unwrap());
88 while let Ok(Some(row)) = reader.next_row() {
89 rgba.extend(row.data());
90 }
91
92 Cached::try_from(rgba).map(|cache| Self {
93 cache,
94 root: root.clone(),
95 format: IconFormat::Image {
96 width: reader.info().width,
97 height: reader.info().height,
98 },
99 })
100 }
101
102 fn open(path: &Path) -> Vec<u8> {
103 std::fs::read(path).unwrap_or_else(|e| panic!("failed to open icon {}: {}", path.display(), e))
104 }
105}
106
107fn largest_ico_entry(icon_dir: &ico::IconDir) -> Option<&ico::IconDirEntry> {
112 icon_dir
113 .entries()
114 .iter()
115 .max_by_key(|e| (e.width() * e.height(), e.bits_per_pixel()))
116}
117
118impl ToTokens for CachedIcon {
119 fn to_tokens(&self, tokens: &mut TokenStream) {
120 let root = &self.root;
121 let cache = &self.cache;
122 let raw = quote!(::std::include_bytes!(#cache));
123 tokens.append_all(match self.format {
124 IconFormat::Raw => raw,
125 IconFormat::Image { width, height } => {
126 quote!(#root::image::Image::new(#raw, #width, #height))
127 }
128 })
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::largest_ico_entry;
135 use ico::{IconDir, IconDirEntry, IconImage, ResourceType};
136 use std::io::Cursor;
137
138 fn image(size: u32, alpha: u8) -> IconImage {
139 let rgba = [255, 0, 0, alpha].repeat((size * size) as usize);
140 IconImage::from_rgba_data(size, size, rgba)
141 }
142
143 fn icon_dir(sizes: &[u32]) -> IconDir {
146 let mut dir = IconDir::new(ResourceType::Icon);
147 for &size in sizes {
148 let entry = if size == 256 {
149 IconDirEntry::encode_as_png(&image(size, 255)).unwrap()
150 } else {
151 IconDirEntry::encode_as_bmp(&image(size, 255)).unwrap()
152 };
153 dir.add_entry(entry);
154 }
155 let mut buf = Vec::new();
156 dir.write(&mut buf).unwrap();
157 IconDir::read(Cursor::new(buf)).unwrap()
158 }
159
160 #[test]
161 fn picks_largest_entry_regardless_of_order() {
162 let dir = icon_dir(&[32, 16, 24, 48, 64, 256]);
164 let entry = largest_ico_entry(&dir).unwrap();
165 assert_eq!((entry.width(), entry.height()), (256, 256));
166 assert!(entry.is_png());
167
168 for sizes in [&[64, 48, 16][..], &[16, 64, 48], &[64]] {
170 let dir = icon_dir(sizes);
171 let entry = largest_ico_entry(&dir).unwrap();
172 assert_eq!((entry.width(), entry.height()), (64, 64), "{sizes:?}");
173 }
174 }
175
176 #[test]
177 fn picks_deepest_entry_for_equal_sizes() {
178 let shallow = IconDirEntry::encode_as_bmp(&image(32, 255)).unwrap();
180 let deep = IconDirEntry::encode_as_bmp(&image(32, 128)).unwrap();
181 assert!(shallow.bits_per_pixel() < deep.bits_per_pixel());
182 let deep_bpp = deep.bits_per_pixel();
183
184 for entries in [[shallow.clone(), deep.clone()], [deep, shallow]] {
185 let mut dir = IconDir::new(ResourceType::Icon);
186 for entry in entries {
187 dir.add_entry(entry);
188 }
189 let entry = largest_ico_entry(&dir).unwrap();
190 assert_eq!((entry.width(), entry.height()), (32, 32));
191 assert_eq!(entry.bits_per_pixel(), deep_bpp);
192 }
193 }
194
195 #[test]
196 fn selected_entry_decodes_at_its_own_size() {
197 let dir = icon_dir(&[16, 256]);
198 let entry = largest_ico_entry(&dir).unwrap();
199 let decoded = entry.decode().unwrap();
200 assert_eq!((decoded.width(), decoded.height()), (256, 256));
201 assert_eq!(decoded.rgba_data().len(), 256 * 256 * 4);
202 }
203
204 #[test]
205 fn no_entries() {
206 assert!(largest_ico_entry(&IconDir::new(ResourceType::Icon)).is_none());
207 }
208}