use std::io::BufReader;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, ensure, Context, Result};
use once_cell::unsync::Lazy;
pub struct Loaded {
width: u32,
height: u32,
data: Vec<u32>,
}
pub enum IconInner {
Pending(PathBuf),
Failed,
Loaded(Loaded),
}
pub struct Icon {
inner: Lazy<Option<IconInner>, Box<dyn FnOnce() -> Option<IconInner>>>,
}
impl Icon {
pub fn new(path: impl Into<PathBuf>) -> Self {
let path = path.into();
Self {
inner: Lazy::new(Box::new(move || {
let mut inner = IconInner::new(path);
inner.load().map(|()| inner)
})),
}
}
pub fn as_image(&self) -> Option<raqote::Image> {
Lazy::force(&self.inner)
.as_ref()?
.loaded()
.map(|l| l.as_image())
}
}
impl Default for IconInner {
fn default() -> Self {
Self::Failed
}
}
impl IconInner {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self::Pending(path.into())
}
fn loaded(&self) -> Option<&Loaded> {
if let Self::Loaded(l) = self {
Some(l)
} else {
unreachable!()
}
}
fn load(&mut self) -> Option<()> {
let loaded = match self {
Self::Pending(path) => Loaded::load(path),
Self::Failed => return None,
Self::Loaded(_) => return Some(()),
};
if let Some(loaded) = loaded {
*self = Self::Loaded(loaded);
Some(())
} else {
*self = Self::Failed;
None
}
}
}
impl Loaded {
pub fn load(path: impl AsRef<Path>) -> Option<Self> {
let path = path.as_ref();
let failed_to_load = |e| log::info!("failed to load icon at path `{:?}`: {}", path, e);
match path.extension()?.to_str()? {
"png" => Self::from_png_path(path).map_err(failed_to_load).ok(),
"svg" => Self::from_svg_path(path).map_err(failed_to_load).ok(),
ext => {
log::info!("unsupported icon extension: {:?}", ext);
None
}
}
}
fn from_png_path(path: impl AsRef<Path>) -> Result<Self> {
let mut decoder = png::Decoder::new(BufReader::new(std::fs::File::open(path.as_ref())?));
decoder.set_transformations(png::Transformations::normalize_to_color8());
let mut reader = decoder
.read_info()
.map_err(|e| anyhow!("failed to read png info: {}", e))?;
let mut buf = vec![0; reader.output_buffer_size()];
let info = reader
.next_frame(&mut buf)
.map_err(|e| anyhow!("failed to read png frame: {}", e))?;
let buf = &buf[..info.buffer_size()];
let data = match info.color_type {
png::ColorType::Rgb => {
ensure!(buf.len() % 3 == 0, "corrupted icon file");
buf.chunks(3)
.map(|chunk| u32::from_be_bytes([0xff, chunk[0], chunk[1], chunk[2]]))
.collect()
}
png::ColorType::Rgba => rgba_to_argb(buf)?,
png::ColorType::GrayscaleAlpha => {
ensure!(buf.len() % 2 == 0, "corrupted icon file");
buf.chunks(2)
.map(|chunk| u32::from_be_bytes([chunk[1], chunk[0], chunk[0], chunk[0]]))
.collect()
}
png::ColorType::Grayscale => buf
.iter()
.copied()
.map(|pix| u32::from_be_bytes([0xff, pix, pix, pix]))
.collect(),
png::ColorType::Indexed => unreachable!("image shall be converted"),
};
Ok(Self {
width: info.width,
height: info.height,
data,
})
}
fn from_svg_path(path: impl AsRef<Path>) -> Result<Self> {
let opt = resvg::usvg::Options::default();
let data = std::fs::read(path.as_ref())
.with_context(|| format!("failed to open svg file: {:?}", path.as_ref()))?;
let tree = resvg::usvg::Tree::from_data(&data, &opt)
.map_err(|e| anyhow!("svg open error: {}", e))?;
let pixmap_size = tree.size().to_int_size();
let width = pixmap_size.width();
let height = pixmap_size.height();
let mut buf =
resvg::tiny_skia::Pixmap::new(width, height).context("invalid pixmap size")?;
resvg::render(
&tree,
resvg::tiny_skia::Transform::default(),
&mut buf.as_mut(),
);
Ok(Self {
width,
height,
data: rgba_to_argb(buf.data())?,
})
}
fn as_image(&self) -> raqote::Image {
raqote::Image {
width: self.width as i32,
height: self.height as i32,
data: self.data.as_slice(),
}
}
}
fn rgba_to_argb(buf: &[u8]) -> Result<Vec<u32>> {
ensure!(buf.len() % 4 == 0, "corrupted icon file");
let data = buf
.chunks(4)
.map(|chunk| {
let src = raqote::SolidSource::from_unpremultiplied_argb(
chunk[3], chunk[0], chunk[1], chunk[2],
);
let a = u32::from(src.a) << 24;
let r = u32::from(src.r) << 16;
let g = u32::from(src.g) << 8;
let b = u32::from(src.b);
a | r | g | b
})
.collect();
Ok(data)
}