use std::{
fs,
io::{self, Read},
path::{Path, PathBuf},
};
use blake3::Hash;
use image::ImageError;
use lofty::{error::LoftyError, picture::Picture};
use lunar_lib::{
log::trace,
paths::{cache_dir, data_dir},
};
use serde::{Deserialize, Serialize};
use crate::library::Id;
#[cfg(feature = "database-impls")]
lunar_lib::define_db!(Images {
fn path() -> Option<PathBuf> {
Some(data_dir().join("image_data"))
}
});
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
pub enum ImageFormat {
Png,
Jpeg,
Tiff,
Bmp,
Gif,
}
impl From<ImageFormat> for lofty::picture::MimeType {
fn from(value: ImageFormat) -> Self {
match value {
ImageFormat::Png => lofty::picture::MimeType::Png,
ImageFormat::Jpeg => lofty::picture::MimeType::Jpeg,
ImageFormat::Tiff => lofty::picture::MimeType::Tiff,
ImageFormat::Bmp => lofty::picture::MimeType::Bmp,
ImageFormat::Gif => lofty::picture::MimeType::Gif,
}
}
}
impl From<ImageFormat> for image::ImageFormat {
fn from(value: ImageFormat) -> Self {
match value {
ImageFormat::Png => image::ImageFormat::Png,
ImageFormat::Jpeg => image::ImageFormat::Jpeg,
ImageFormat::Tiff => image::ImageFormat::Tiff,
ImageFormat::Bmp => image::ImageFormat::Bmp,
ImageFormat::Gif => image::ImageFormat::Gif,
}
}
}
impl ImageFormat {
#[must_use]
pub fn try_from_mime_type(value: lofty::picture::MimeType) -> Option<Self> {
let format = match value {
lofty::picture::MimeType::Png => ImageFormat::Png,
lofty::picture::MimeType::Jpeg => ImageFormat::Jpeg,
lofty::picture::MimeType::Tiff => ImageFormat::Tiff,
lofty::picture::MimeType::Bmp => ImageFormat::Bmp,
lofty::picture::MimeType::Gif => ImageFormat::Gif,
_ => return None,
};
Some(format)
}
fn try_from_image_image_format(value: image::ImageFormat) -> Option<Self> {
let format = match value {
image::ImageFormat::Png => ImageFormat::Png,
image::ImageFormat::Jpeg => ImageFormat::Jpeg,
image::ImageFormat::Tiff => ImageFormat::Tiff,
image::ImageFormat::Bmp => ImageFormat::Bmp,
image::ImageFormat::Gif => ImageFormat::Gif,
_ => return None,
};
Some(format)
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub struct ImageArt {
hash: Hash,
data: Vec<u8>,
format: ImageFormat,
}
#[cfg(feature = "database-impls")]
impl lunar_lib::database::DatabaseEntry for ImageArt {
type DbInner = Images;
const VERSION_NUMBER: u32 = 1;
const TREE_NAME: &str = "images";
}
impl ImageArt {
#[must_use]
pub fn try_from_picture(picture: Picture) -> Option<Self> {
let Some(mime_type) = picture.mime_type() else {
return None;
};
Some(Self {
hash: blake3::hash(picture.data()),
format: ImageFormat::try_from_mime_type(mime_type.clone())?,
data: picture.into_data(),
})
}
pub fn from_image_file(path: impl AsRef<Path>) -> Result<Self, ImageError> {
let path = path.as_ref();
let reader = io::BufReader::new(fs::File::open(path)?);
let image = image::ImageReader::new(reader).with_guessed_format()?;
let format = image.format().expect("Format was just guessed");
let format = ImageFormat::try_from_image_image_format(format).ok_or(io::Error::new(
io::ErrorKind::InvalidData,
"invalid image format",
))?;
let mut data = Vec::new();
image.into_inner().read_to_end(&mut data)?;
Ok(Self {
hash: blake3::hash(&data),
data,
format,
})
}
pub fn to_picture(&self) -> Result<Picture, LoftyError> {
let mut cursor = io::Cursor::new(&self.data);
let mut picture = Picture::from_reader(&mut cursor)?;
picture.set_pic_type(lofty::picture::PictureType::CoverFront);
Ok(picture)
}
pub fn cache_file(&self, x: u32, y: u32) -> io::Result<PathBuf> {
let path = cache_dir().join(format!("art/{}_{x}_{y}.jpg", self.hash));
trace!("Starting image encoding for {}", path.display());
fs::create_dir_all(path.parent().unwrap())?;
let writer = io::BufWriter::new(
fs::OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(&path)?,
);
let image = image::load_from_memory_with_format(&self.data, self.format.into())
.expect("Stored image data should always be valid");
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(path)
}
#[must_use]
pub fn id(&self) -> Id<ImageArt> {
Id::from(*self.hash.as_bytes())
}
}