pub extern crate ikon;
pub use ikon::{image, resvg::{self, usvg, raqote}, encode::{Encode, Save, EncodingError}, Image};
use ikon::{encode, image::DynamicImage, AsSize};
use std::{
convert::TryFrom,
collections::{hash_map::{HashMap, Entry}, btree_set::BTreeSet},
fs::{DirBuilder, File},
io::{self, Write},
path::{Path, PathBuf},
fmt::{self, Display, Formatter}
};
use entries::{Entries, Sizes};
use serialize::{LinkTag, ManifestIcon};
mod entries;
mod serialize;
#[cfg(test)]
mod test;
pub mod resample {
pub use ikon::resample::{nearest, linear, cubic, ResampleError};
pub(crate) use ikon::resample::apply;
}
const LINK_TAG_CAPACITY: usize = 80;
const MANIFEST_ICON_CAPACITY: usize = 90;
macro_rules! path {
($path: expr) => {
PathBuf::from($path)
};
($format: expr, $($arg: expr),*) => {
PathBuf::from(format!($format, $($arg),*))
};
}
#[derive(Clone, Debug)]
pub struct Favicon {
pngs: HashMap<u32, Vec<u8>>,
svgs: HashMap<Vec<u8>, Vec<u32>>,
svg_entries: BTreeSet<u32>,
include_apple_touch_helper: bool,
include_pwa_helper: bool
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Size(pub u16);
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum Rel {
Icon,
AppleTouchIcon
}
impl Favicon {
#[inline]
pub fn apple_touch(&mut self, b: bool) -> &mut Self {
self.include_apple_touch_helper = b;
self
}
#[inline]
pub fn web_app(&mut self, b: bool) -> &mut Self {
self.include_pwa_helper = b;
self
}
pub fn html_helper(&self) -> io::Result<Vec<u8>> {
let mut output = Vec::with_capacity(LINK_TAG_CAPACITY * self.len());
for (id, (sizes, _)) in self.entries().enumerate() {
write!(output, "{}\n", LinkTag::icon(sizes, id))?;
}
if self.include_apple_touch_helper {
for (id, (sizes, _)) in self.entries().enumerate() {
write!(output, "{}\n", LinkTag::apple_touch(sizes, id))?;
}
}
Ok(output)
}
pub fn manisfest(&self) -> io::Result<Vec<u8>> {
let mut output = Vec::with_capacity(MANIFEST_ICON_CAPACITY * self.len());
write!(output, "{{\n icons: [\n")?;
for (id, (sizes, _)) in self.entries().enumerate() {
write!(output, "{},\n", ManifestIcon::from(sizes, id))?;
}
write!(output, " ]\n}}")?;
Ok(output)
}
fn entries(&self) -> Entries<'_> {
let mut entries = Vec::with_capacity(self.len());
for (&size, buf) in &self.pngs {
entries.push((Sizes::Png(size), buf));
}
for (buf, sizes) in &self.svgs {
entries.push((Sizes::Svg(sizes), buf));
}
Entries::from(entries)
}
#[inline]
fn add_raster(
&mut self,
source: &DynamicImage,
key: Size
) -> Result<&mut Self, EncodingError<Size>> {
let size = key.as_size();
if self.svg_entries.contains(&size) {
return Err(EncodingError::AlreadyIncluded(key));
}
if let Entry::Vacant(entry) = self.pngs.entry(size) {
let buf = encode::png(source)?;
entry.insert(buf);
Ok(self)
} else {
Err(EncodingError::AlreadyIncluded(key))
}
}
#[inline]
fn add_svg(
&mut self,
svg: &usvg::Tree,
key: Size
) -> Result<&mut Self, EncodingError<Size>> {
let size = key.as_size();
if self.pngs.contains_key(&size) || !self.svg_entries.insert(size) {
Err(EncodingError::AlreadyIncluded(key))
} else {
let buf = encode::svg(svg);
let entry = self.svgs.entry(buf).or_insert(Vec::with_capacity(1));
entry.push(size);
entry.sort();
Ok(self)
}
}
}
impl Encode for Favicon {
type Key = Size;
fn with_capacity(capacity: usize) -> Self {
Favicon {
pngs: HashMap::with_capacity(capacity),
svgs: HashMap::new(),
svg_entries: BTreeSet::new(),
include_apple_touch_helper: false,
include_pwa_helper: false
}
}
fn len(&self) -> usize {
self.pngs.len() + self.svg_entries.len()
}
fn add_entry<F: FnMut(&DynamicImage, u32) -> io::Result<DynamicImage>>(
&mut self,
filter: F,
source: &Image,
key: Self::Key,
) -> Result<&mut Self, EncodingError<Self::Key>> {
match source {
Image::Raster(img) => self.add_raster(&resample::apply(filter, img, key.as_size())?, key),
Image::Svg(svg) => self.add_svg(&svg, key)
}
}
}
impl Save for Favicon {
fn save<P: AsRef<Path>>(&mut self, base_path: &P) -> io::Result<&mut Self> {
if !base_path.as_ref().is_dir() {
return Err(io::Error::from(io::ErrorKind::InvalidInput));
}
let container = base_path.as_ref().join("icons/");
if !container.exists() {
let mut builder = DirBuilder::new();
builder.recursive(true).create(container)?;
}
for (id, (sizes, buf)) in self.entries().enumerate() {
let path = path!("icons/favicon{}.{}", id, sizes.extension());
save_file(buf.as_ref(), base_path, &path)?;
}
let mut helper = self.html_helper()?;
if self.include_pwa_helper {
write!(helper, "<link rel=\"serialize\" href=\"helper.webmanifest\">\n")?;
let manifest = self.manisfest()?;
save_file(manifest.as_ref(), base_path, &"helper.webmanifest")?;
}
save_file(helper.as_ref(), base_path, &"helper.html")?;
Ok(self)
}
}
impl AsSize for Size {
fn as_size(&self) -> u32 {
if self.0 == 0 {
65536
} else {
self.0 as u32
}
}
}
impl Display for Rel {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::AppleTouchIcon => write!(f, "apple-touch-icon-precomposed"),
Self::Icon => write!(f, "icon")
}
}
}
impl TryFrom<u32> for Size {
type Error = io::Error;
fn try_from(val: u32) -> io::Result<Self> {
match val {
65536 => Ok(Size(0)),
0 => Err(io::Error::from(io::ErrorKind::InvalidInput)),
n if n < 65536 => Ok(Size(n as u16)),
_ => Err(io::Error::from(io::ErrorKind::InvalidInput))
}
}
}
#[inline]
fn save_file<P1: AsRef<Path>, P2: AsRef<Path>>(
data: &[u8],
base_path: &P1,
path: &P2,
) -> io::Result<()> {
let path = base_path.as_ref().join(path);
let mut file = File::create(path)?;
file.write_all(data)
}