1#[cfg(doctest)]
31#[doc = include_str!("../readme.md")]
32mod test_readme {}
33
34use image::codecs::ico::{IcoEncoder, IcoFrame};
35use image::codecs::png::PngEncoder;
36use image::imageops::resize;
37use image::io::Reader as ImageReader;
38use image::{DynamicImage, ExtendedColorType, ImageEncoder};
39use std::borrow::Cow;
40use std::ffi::OsStr;
41use std::fs::OpenOptions;
42use std::io::Cursor;
43use std::ops::Deref;
44use std::path::{Path, PathBuf};
45use std::{env, iter};
46
47mod error;
48pub use error::*;
49pub type Result<T> = std::result::Result<T, Error>;
50
51pub use image::imageops::FilterType;
52
53#[derive(Debug)]
56pub struct IcoBuilder {
57 sizes: IconSizes,
58 source_files: Vec<PathBuf>,
59 filter_type: FilterType,
60}
61
62impl Default for IcoBuilder {
63 fn default() -> Self {
64 IcoBuilder {
65 sizes: Default::default(),
66 source_files: Default::default(),
67 filter_type: FilterType::Lanczos3,
68 }
69 }
70}
71
72impl IcoBuilder {
73 pub fn sizes(&mut self, sizes: impl Into<IconSizes>) -> &mut IcoBuilder {
75 self.sizes = sizes.into();
76 self
77 }
78
79 pub fn add_source_file(&mut self, source_file: impl AsRef<Path>) -> &mut IcoBuilder {
92 self.add_source_files(iter::once(source_file))
93 }
94
95 pub fn add_source_files(
97 &mut self,
98 source_files: impl IntoIterator<Item = impl AsRef<Path>>,
99 ) -> &mut IcoBuilder {
100 self.source_files
101 .extend(source_files.into_iter().map(|f| f.as_ref().to_owned()));
102 self
103 }
104
105 pub fn filter_type(&mut self, filter_type: FilterType) -> &mut IcoBuilder {
107 self.filter_type = filter_type;
108 self
109 }
110
111 pub fn build_file(&self, output_file_path: impl AsRef<Path>) -> Result<()> {
113 let icons = decode_icons(&self.source_files)?;
114 let frames = create_ico_frames(&self.sizes, &icons, self.filter_type)?;
115
116 let file = OpenOptions::new()
117 .create(true)
118 .truncate(true)
119 .write(true)
120 .open(&output_file_path)?;
121 IcoEncoder::new(file).encode_images(&frames)?;
122
123 Ok(())
124 }
125
126 pub fn build_file_cargo(&self, file_name: impl AsRef<OsStr>) -> Result<PathBuf> {
131 let out_dir = env::var_os("OUT_DIR").expect(
132 "OUT_DIR environment variable is required.\nHint: This function is intended to be used in Cargo build scripts.",
133 );
134 let output_path: PathBuf = [&out_dir, file_name.as_ref()].iter().collect();
135
136 for file in &self.source_files {
137 println!(
138 "cargo:rerun-if-changed={}",
139 file.to_str().expect("Path needs to be valid UTF-8")
140 )
141 }
142
143 self.build_file(&output_path)?;
144
145 Ok(output_path)
146 }
147}
148
149#[derive(Debug)]
151pub struct IconSizes(Cow<'static, [u32]>);
152
153impl IconSizes {
154 pub const MINIMAL: Self = Self::new(&[16, 24, 32, 48, 256]);
158
159 pub const fn new(sizes: &'static [u32]) -> IconSizes {
160 Self(Cow::Borrowed(sizes))
161 }
162}
163
164impl Default for IconSizes {
165 fn default() -> Self {
166 IconSizes::MINIMAL
167 }
168}
169
170impl<'a, I> From<I> for IconSizes
171where
172 I: IntoIterator<Item = &'a u32>,
173{
174 fn from(value: I) -> Self {
175 IconSizes(value.into_iter().copied().collect::<Vec<_>>().into())
176 }
177}
178
179impl Deref for IconSizes {
180 type Target = [u32];
181
182 fn deref(&self) -> &Self::Target {
183 &self.0
184 }
185}
186
187fn decode_icons(
188 icon_sources: impl IntoIterator<Item = impl AsRef<Path>>,
189) -> Result<Vec<DynamicImage>> {
190 icon_sources
191 .into_iter()
192 .map(|path| decode_icon(path.as_ref()))
193 .collect()
194}
195
196fn decode_icon(path: &Path) -> Result<DynamicImage> {
197 let image = ImageReader::open(path)?.decode()?;
198
199 if is_square(&image) {
200 Ok(image)
201 } else {
202 Err(Error::NonSquareImage {
203 path: path.to_owned(),
204 width: image.width(),
205 height: image.height(),
206 })
207 }
208}
209
210fn is_square(image: &DynamicImage) -> bool {
211 image.width() == image.height()
212}
213
214fn find_next_bigger_icon(icons: &[DynamicImage], size: u32) -> Result<&DynamicImage> {
215 icons
216 .iter()
217 .filter(|icon| icon.width() >= size)
218 .min_by_key(|icon| icon.width())
219 .ok_or(Error::MissingIconSize(size))
220}
221
222fn create_ico_frames(
223 sizes: &IconSizes,
224 icons: &[DynamicImage],
225 filter_type: FilterType,
226) -> Result<Vec<IcoFrame<'static>>> {
227 sizes
228 .iter()
229 .copied()
230 .map(|size| create_ico_frame(icons, size, filter_type))
231 .collect()
232}
233
234fn create_ico_frame(
235 icons: &[DynamicImage],
236 size: u32,
237 filter_type: FilterType,
238) -> Result<IcoFrame<'static>> {
239 let next_bigger_icon = find_next_bigger_icon(icons, size)?;
240 let resized = resize(next_bigger_icon, size, size, filter_type);
241 encode_ico_frame(resized.as_raw(), size)
242}
243
244fn encode_ico_frame(buffer: &[u8], size: u32) -> Result<IcoFrame<'static>> {
245 let color_type = ExtendedColorType::Rgba8;
246 let mut encoded = Vec::new();
247 PngEncoder::new(Cursor::new(&mut encoded)).write_image(buffer, size, size, color_type)?;
248 Ok(IcoFrame::with_encoded(encoded, size, size, color_type)?)
249}