pub use ikon::{encode::{Encode, Save, EncodingError}, Image};
use ikon::{image::DynamicImage, AsSize};
use std::{
io::{self, BufWriter, Write},
path::{Path, PathBuf},
fs::{self, File, DirBuilder},
fmt::{self, Display, Formatter},
collections::HashMap
};
pub mod resample {
pub use ikon::resample::{ResampleError, cubic, linear, nearest};
}
#[cfg(test)]
mod test;
const MAX_SIZE: u32 = std::u16::MAX as u32 + 1;
macro_rules! path_buf {
($path:expr) => (PathBuf::from($path));
($($x:expr),*) => (PathBuf::from(format!($($x),*)));
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IconTheme {
name: String,
comment: String,
icons: HashMap<Dir, HashMap<String, ImageBuffer>>
}
impl IconTheme {
pub fn new(name: String, comment: String) -> Self {
Self::with_capacity(name, comment, 0)
}
pub fn with_capacity(
name: String,
comment: String,
capacity: usize
) -> Self {
Self { name, comment, icons: HashMap::with_capacity(capacity) }
}
}
impl Encode for IconTheme {
type Key = Icon;
#[inline]
fn len(&self) -> usize {
self.icons.len()
}
fn add_entry<
F: FnMut(&DynamicImage, (u32, u32)) -> io::Result<DynamicImage>
>(
&mut self,
filter: F,
source: &Image,
key: Icon
) -> Result<&mut Self, EncodingError<Icon>> {
let dir = self.icons.entry(key.dir).or_insert(HashMap::new());
if let None = dir.get(&key.name) {
match source {
Image::Raster(img) => {
let (x, y) = key.as_size();
let img = ikon::resample::apply(filter, img, (x, y))?;
let mut buff = Vec::with_capacity(x as usize * y as usize);
ikon::encode::png(&img, &mut buff)?;
let _ = dir.insert(key.name, ImageBuffer::Png(buff));
},
Image::Svg(svg) => {
let mut buff = Vec::new();
ikon::encode::svg(svg, &mut buff)?;
let _ = dir.insert(key.name, ImageBuffer::Svg(buff));
}
}
Ok(self)
} else {
Err(EncodingError::AlreadyIncluded(key))
}
}
}
impl Save for IconTheme {
fn save<P: AsRef<Path>>(&mut self, path: &P) -> io::Result<&mut Self> {
let mut builder = DirBuilder::new();
builder.recursive(true);
if !path.as_ref().exists() {
builder.create(&path)?;
}
let mut index = BufWriter::new(
File::create(path.as_ref().join("index.theme"))?
);
write!(
&mut index,
"[Icon Theme]\nName={}\nComment={}\nDirectories=",
self.name, self.comment
)?;
let mut dirs = self.icons.keys();
if let Some(first) = dirs.next() {
write!(&mut index, "{}", first.path().display())?;
while let Some(dir) = dirs.next() {
write!(&mut index, ",{}", dir.path().display())?;
}
}
write!(&mut index, "\n\n")?;
for (dir, names) in &self.icons {
let dir_path = dir.path();
write!(
&mut index,
"[{}]\nContext={}\n",
dir_path.display(),
dir.context
)?;
dir.size.write(&mut index)?;
if dir.scale != 1 {
write!(&mut index, "Scale={}\n", dir.scale)?;
}
write!(&mut index, "\n")?;
for (name, buff) in names {
let absolute_dir_path = path.as_ref().join(&dir_path);
if !absolute_dir_path.exists() {
builder.create(&absolute_dir_path)?;
}
match buff {
ImageBuffer::Png(raw) => {
fs::write(
absolute_dir_path.join(format!("{}.png", name)),
raw
)?;
},
ImageBuffer::Svg(raw) => {
fs::write(
absolute_dir_path.join(format!("{}.svg", name)),
raw
)?;
}
}
}
}
Ok(self)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Icon {
pub name: String,
pub(crate) dir: Dir
}
impl Icon {
#[inline]
pub fn application(name: String, size: Size, scale: usize) -> Self {
Self {
name,
dir: Dir { size, scale, context: Context::Apps }
}
}
#[inline]
pub fn mime_type(name: String, size: Size, scale: usize) -> Self {
Self {
name,
dir: Dir { size, scale, context: Context::Apps }
}
}
}
impl AsSize for Icon {
#[inline]
fn as_size(&self) -> (u32, u32) {
let size = match self.dir.size {
Size::Fixed(size) => size,
Size::Scalable { size, .. } => size,
Size::Threshold(size, _) => size
};
let size = size_to_u32(size);
(size, size)
}
}
#[derive(Copy, Clone, Debug, Hash)]
struct Dir {
pub(crate) size: Size,
pub(crate) context: Context,
pub(crate) scale: usize
}
impl Dir {
pub(crate) fn path(&self) -> PathBuf {
let c = self.context.path();
match self.size {
Size::Fixed(size) if self.scale == 1 => {
path_buf!("{0}x{0}/{1}", size, c)
},
Size::Fixed(size) => {
path_buf!("{0}x{0}@{1}/{2}", size, self.scale, c)
},
Size::Scalable { .. } => path_buf!("scalable/{0}", c),
Size::Threshold(_size, _t) => todo!()
}
}
}
impl PartialEq for Dir {
fn eq(&self, other: &Self) -> bool {
self.size == other.size
}
}
impl Eq for Dir {}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Size {
Fixed(u16),
Scalable { size: u16, max: u16, min: u16},
Threshold(u16, i16)
}
impl Size {
pub(crate) fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
match self {
Self::Fixed(size) => {
write!(w, "Size={}\nType=Fixed\n", size_to_u32(*size))
},
Self::Scalable { size, min, max, .. } => {
write!(
w,
"Size={}\nMinSize={}\nMaxSize={}\nType=Scalable\n",
size_to_u32(*size), min, max
)
},
Self::Threshold(size, t) => {
write!(w, "Size={}\nType=Threshold\n", size_to_u32(*size))?;
if *t != 2 {
write!(w, "Threshold={}\n", t)?;
}
Ok(())
}
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
enum Context {
Apps,
MimeTypes
}
impl Context {
pub(crate) fn path(&self) -> &'static str {
match self {
Self::Apps => "apps",
Self::MimeTypes => "mime_types"
}
}
}
impl Display for Context {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Apps => write!(f, "Applications"),
Self::MimeTypes => write!(f, "MimeTypes")
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
enum ImageBuffer {
Png(Vec<u8>),
Svg(Vec<u8>)
}
#[inline]
fn size_to_u32(size: u16) -> u32 {
if size == 0 {
MAX_SIZE
} else {
size as u32
}
}