use image::codecs::ico::{IcoEncoder, IcoFrame};
use image::codecs::png::PngEncoder;
use image::imageops::resize;
use image::io::Reader as ImageReader;
use image::{ColorType, DynamicImage, ImageEncoder};
use std::borrow::Cow;
use std::ffi::OsStr;
use std::fs::OpenOptions;
use std::io::Cursor;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::{env, io, iter};
use thiserror::Error;
pub use image::imageops::FilterType;
#[derive(Debug)]
pub struct IcoBuilder {
sizes: IconSizes,
source_files: Vec<PathBuf>,
filter_type: FilterType,
}
impl Default for IcoBuilder {
fn default() -> Self {
IcoBuilder {
sizes: Default::default(),
source_files: Default::default(),
filter_type: FilterType::Lanczos3,
}
}
}
impl IcoBuilder {
pub fn sizes(&mut self, sizes: impl Into<IconSizes>) -> &mut IcoBuilder {
self.sizes = sizes.into();
self
}
pub fn add_source_file(&mut self, source_file: impl AsRef<Path>) -> &mut IcoBuilder {
self.add_source_files(iter::once(source_file))
}
pub fn add_source_files(
&mut self,
source_files: impl IntoIterator<Item = impl AsRef<Path>>,
) -> &mut IcoBuilder {
self.source_files
.extend(source_files.into_iter().map(|f| f.as_ref().to_owned()));
self
}
pub fn filter_type(&mut self, filter_type: FilterType) -> &mut IcoBuilder {
self.filter_type = filter_type;
self
}
pub fn build_file(&self, output_file_path: impl AsRef<Path>) -> Result<()> {
let icons = decode_icons(&self.source_files)?;
let frames = create_ico_frames(&self.sizes, &icons, self.filter_type)?;
let file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&output_file_path)?;
IcoEncoder::new(file).encode_images(&frames)?;
Ok(())
}
pub fn build_file_cargo(&self, file_name: impl AsRef<OsStr>) -> Result<PathBuf> {
let out_dir = env::var_os("OUT_DIR").expect(
"OUT_DIR environment variable is required.\nHint: This function is intended to be used in Cargo build scripts.",
);
let output_path: PathBuf = [&out_dir, file_name.as_ref()].iter().collect();
for file in &self.source_files {
println!(
"cargo:rerun-if-changed={}",
file.to_str().expect("Path needs to be valid UTF-8")
)
}
self.build_file(&output_path)?;
Ok(output_path)
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
Image(#[from] image::ImageError),
#[error(transparent)]
Io(#[from] io::Error),
#[error("No icon in the sources is >= {0}px")]
MissingIconSize(u32),
#[error("Image {path} ({width} × {height}) is not a square")]
NonSquareImage {
path: PathBuf,
width: u32,
height: u32,
},
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub struct IconSizes(Cow<'static, [u32]>);
impl IconSizes {
pub const MINIMAL: Self = Self::new(&[16, 24, 32, 48, 256]);
pub const fn new(sizes: &'static [u32]) -> IconSizes {
Self(Cow::Borrowed(sizes))
}
}
impl Default for IconSizes {
fn default() -> Self {
IconSizes::MINIMAL
}
}
impl<'a, I> From<I> for IconSizes
where
I: IntoIterator<Item = &'a u32>,
{
fn from(value: I) -> Self {
IconSizes(value.into_iter().copied().collect::<Vec<_>>().into())
}
}
impl Deref for IconSizes {
type Target = [u32];
fn deref(&self) -> &Self::Target {
&self.0
}
}
fn decode_icons(
icon_sources: impl IntoIterator<Item = impl AsRef<Path>>,
) -> Result<Vec<DynamicImage>> {
icon_sources
.into_iter()
.map(|path| decode_icon(path.as_ref()))
.collect()
}
fn decode_icon(path: &Path) -> Result<DynamicImage> {
let image = ImageReader::open(path)?.decode()?;
if is_square(&image) {
Ok(image)
} else {
Err(Error::NonSquareImage {
path: path.to_owned(),
width: image.width(),
height: image.height(),
})
}
}
fn is_square(image: &DynamicImage) -> bool {
image.width() == image.height()
}
fn find_next_bigger_icon(icons: &[DynamicImage], size: u32) -> Result<&DynamicImage> {
icons
.iter()
.filter(|icon| icon.width() >= size)
.min_by_key(|icon| icon.width())
.ok_or(Error::MissingIconSize(size))
}
fn create_ico_frames(
sizes: &IconSizes,
icons: &[DynamicImage],
filter_type: FilterType,
) -> Result<Vec<IcoFrame<'static>>> {
sizes
.iter()
.copied()
.map(|size| create_ico_frame(icons, size, filter_type))
.collect()
}
fn create_ico_frame(
icons: &[DynamicImage],
size: u32,
filter_type: FilterType,
) -> Result<IcoFrame<'static>> {
let next_bigger_icon = find_next_bigger_icon(icons, size)?;
let resized = resize(next_bigger_icon, size, size, filter_type);
encode_ico_frame(resized.as_raw(), size)
}
fn encode_ico_frame(buffer: &[u8], size: u32) -> Result<IcoFrame<'static>> {
let color_type = ColorType::Rgba8;
let mut encoded = Vec::new();
PngEncoder::new(Cursor::new(&mut encoded)).write_image(buffer, size, size, color_type)?;
Ok(IcoFrame::with_encoded(encoded, size, size, color_type)?)
}