use std::{
fs,
io::{self, Read},
path::{Path, PathBuf},
};
use blake3::Hash;
use image::ImageError;
use lofty::{error::LoftyError, picture::Picture};
use lunar_lib::{
database::{DatabaseError, Db, DbIdExt},
log::trace,
paths::cache_dir,
};
use selene_common::art::ImageSize;
use serde::{Deserialize, Serialize};
use crate::{database::Selene, library::Id};
#[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 mime_type(&self) -> &'static str {
match self {
ImageFormat::Png => "image/png",
ImageFormat::Jpeg => "image/jpeg",
ImageFormat::Tiff => "image/tiff",
ImageFormat::Bmp => "image/bmp",
ImageFormat::Gif => "image/gif",
}
}
#[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,
}
impl lunar_lib::database::DatabaseEntry for ImageArt {
type DbInner = Selene;
const VERSION_NUMBER: u32 = 1;
const TREE_NAME: &str = "art";
}
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 load_from_cache(
id: Id<Self>,
size: ImageSize,
db: &Db<Selene>,
) -> Result<Option<PathBuf>, DatabaseError> {
let (x, y) = size.size();
let path = cache_dir().join(format!("art/{id}_{x}_{y}.jpg"));
if path.try_exists()? {
return Ok(Some(path));
}
let Some(art) = id.db_get(db)? else {
return Ok(None);
};
let path = art.cache_file(size)?;
Ok(Some(path))
}
pub fn cache_file(&self, size: ImageSize) -> io::Result<PathBuf> {
let (x, y) = size.size();
let path = cache_dir().join(format!("art/{}_{x}_{y}.jpg", self.hash));
if path.try_exists()? {
return Ok(path);
}
trace!("Starting image encoding for {}", path.display());
fs::create_dir_all(
path.parent()
.expect("Files must always live in a directory"),
)?;
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())
}
#[must_use]
pub fn format(&self) -> ImageFormat {
self.format
}
}