use directories::ProjectDirs;
use fontsource_downloader::FontSourceClient;
#[cfg(feature = "pyo3")]
use pyo3::prelude::*;
use image::RgbaImage;
use resvg::usvg::{Options, fontdb};
use sha2::{Digest, Sha256};
use std::{
borrow::Cow,
io::Read,
path::{Path, PathBuf},
sync::Arc,
};
use crate::{
ImgGenRendererError, Layout, Result,
validators::{HEIGHT, WIDTH},
};
mod renderer;
use renderer::Renderer;
#[cfg_attr(feature = "pyo3", pyclass(module = "img_gen", from_py_object))]
#[derive(Clone)]
pub struct Generator {
pub external_resource_paths: Vec<PathBuf>,
fontdb: Arc<fontdb::Database>,
fontsource_client: FontSourceClient,
pub cache_root: PathBuf,
}
impl Generator {
pub fn new(external_resource_paths: Vec<PathBuf>, cache_root: Option<PathBuf>) -> Result<Self> {
let fontdb = fontdb::Database::new();
let cache_root = cache_root
.or_else(|| {
ProjectDirs::from("", "2bndy5", "img-gen")
.map(|dirs| dirs.cache_dir().to_path_buf())
})
.unwrap_or_else(|| PathBuf::from(".img-gen-cache"));
let fontsource_client = FontSourceClient::with_cache_root(&cache_root)?;
Ok(Generator {
external_resource_paths,
fontdb: Arc::new(fontdb),
fontsource_client,
cache_root,
})
}
pub async fn render(&self, layout: Layout) -> Result<Image> {
let mut canvas = RgbaImage::new(
layout.size.width.unwrap_or(WIDTH).get(),
layout.size.height.unwrap_or(HEIGHT).get(),
);
let opt = Options {
fontdb: self.fontdb.clone(),
..Default::default()
};
let mut renderer =
Renderer::new(opt, &self.fontsource_client, &self.external_resource_paths);
for layer in &layout.layers {
renderer.render_layer(layer, &mut canvas).await?;
}
renderer.render_debug(&layout, &mut canvas).await?;
Ok(Image { data: canvas })
}
}
#[cfg_attr(feature = "pyo3", pyclass(module = "img_gen", from_py_object))]
#[derive(Clone)]
pub struct Image {
pub data: RgbaImage,
}
impl Image {
pub fn save<P: AsRef<Path>>(&self, name: P) -> Result<()> {
let name = name.as_ref();
self.data
.save(name)
.map_err(|source| ImgGenRendererError::SaveImageFailed {
path: name.to_string_lossy().into_owned(),
source,
})
}
pub fn get_bytes(&'_ self) -> Result<Cow<'_, [u8]>> {
let bytes: Vec<u8> = self
.data
.bytes()
.collect::<std::io::Result<Vec<u8>>>()
.map_err(|source| ImgGenRendererError::CollectImageBytesFailed { source })?;
Ok(Cow::Owned(bytes))
}
pub fn get_sha256(&self) -> Result<String> {
let mut hash_gen = Sha256::new();
hash_gen.update(self.get_bytes()?);
let digest = hash_gen.finalize();
Ok(digest
.as_slice()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::{Image, RgbaImage};
#[test]
fn hash() {
let buffer = RgbaImage::new(50, 50);
let img = Image { data: buffer };
let sha256 = img.get_sha256().unwrap();
assert_eq!(
sha256,
"95b532cc4381affdff0d956e12520a04129ed49d37e154228368fe5621f0b9a2"
);
}
}