use std::{
fs::{self},
io::{self, BufWriter},
path::{Path, PathBuf},
};
use blake3::Hash;
use image::{DynamicImage, ImageError, imageops::FilterType};
use lofty::{
error::LoftyError,
picture::{Picture, PictureType},
};
use lunar_lib::trace;
use serde::{Deserialize, Serialize};
use crate::{cache_dir, utils::hash_file};
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct ImageArt {
hash: Hash,
source: PathBuf,
}
pub trait CacheableArt {
fn cache_file(&self, x: u32, y: u32) -> io::Result<PathBuf>;
fn cache_image(&self, x: u32, y: u32) -> io::Result<(DynamicImage, PathBuf)>;
fn hash(&self) -> Hash;
}
impl ImageArt {
pub(crate) fn from_raw(source: PathBuf, hash: Hash) -> Self {
Self { hash, source }
}
pub fn from_file(source: impl Into<PathBuf>) -> io::Result<Self> {
let source = source.into();
let hash = hash_file(&source)?;
Ok(Self { hash, source })
}
#[must_use]
pub fn hash(&self) -> Hash {
self.hash
}
#[must_use]
pub fn source(&self) -> &Path {
&self.source
}
pub fn to_picture(&self) -> Result<Picture, LoftyError> {
let image = image::open(&self.source).expect("Image art should be pre-validated");
let mut reader = io::Cursor::new(image.into_bytes());
let mut picture = Picture::from_reader(&mut reader)?;
picture.set_pic_type(PictureType::CoverFront);
Ok(picture)
}
}
impl CacheableArt for ImageArt {
fn cache_file(&self, x: u32, y: u32) -> io::Result<PathBuf> {
let cover_dir = cache_dir().join("covers");
let cache_file = cover_dir.join(format!("{id}_{x}_{x}.jpeg", id = self.hash()));
if cache_file.exists() {
Ok(cache_file)
} else {
self.cache_image(x, y).map(|(_, p)| p)
}
}
fn cache_image(&self, x: u32, y: u32) -> io::Result<(DynamicImage, PathBuf)> {
let cover_dir = cache_dir().join("covers");
let cache_file = cover_dir.join(format!("{id}_{x}_{x}.jpeg", id = self.hash()));
if cache_file.exists() {
let image = image::open(&cache_file).expect("Cache art was modified or corrupted");
return Ok((image, cache_file));
}
let image = image::open(&self.source)
.expect("Image art should be pre-validated")
.resize_to_fill(x, y, FilterType::Lanczos3);
cache_image(&image, &cache_file)?;
Ok((image, cache_file))
}
fn hash(&self) -> Hash {
self.hash
}
}
pub fn hash_image(source: impl AsRef<Path>) -> Result<Hash, ImageError> {
let image = image::open(source)?;
Ok(blake3::hash(image.as_bytes()))
}
pub fn cache_image(image: &DynamicImage, path: impl AsRef<Path>) -> io::Result<()> {
let path = path.as_ref();
trace!("Starting image encoding for {}", path.display());
fs::create_dir_all(path.parent().unwrap())?;
let writer = BufWriter::new(
fs::OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(path)?,
);
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(writer, 80);
encoder.encode_image(image).map_err(io::Error::other)?;
trace!("Finished image encoding for {}", path.display());
Ok(())
}