img_gen_renderer/generator/
mod.rs1use directories::ProjectDirs;
2use fontsource_downloader::FontSourceClient;
3#[cfg(feature = "pyo3")]
4use pyo3::prelude::*;
5
6use image::RgbaImage;
7use resvg::usvg::{Options, fontdb};
8use sha2::{Digest, Sha256};
9use std::{
10 borrow::Cow,
11 io::Read,
12 path::{Path, PathBuf},
13 sync::Arc,
14};
15
16use crate::{
17 ImgGenRendererError, Layout, Result,
18 validators::{HEIGHT, WIDTH},
19};
20mod renderer;
21use renderer::Renderer;
22
23#[cfg_attr(feature = "pyo3", pyclass(module = "img_gen", from_py_object))]
30#[derive(Clone)]
31pub struct Generator {
32 pub external_resource_paths: Vec<PathBuf>,
34 fontdb: Arc<fontdb::Database>,
35 fontsource_client: FontSourceClient,
36 pub cache_root: PathBuf,
38}
39
40impl Generator {
41 pub fn new(external_resource_paths: Vec<PathBuf>, cache_root: Option<PathBuf>) -> Result<Self> {
46 let fontdb = fontdb::Database::new();
47
48 let cache_root = cache_root
49 .or_else(|| {
50 ProjectDirs::from("", "2bndy5", "img-gen")
52 .map(|dirs| dirs.cache_dir().to_path_buf())
53 })
54 .unwrap_or_else(|| PathBuf::from(".img-gen-cache"));
55
56 let fontsource_client = FontSourceClient::with_cache_root(&cache_root)?;
57
58 Ok(Generator {
59 external_resource_paths,
60 fontdb: Arc::new(fontdb),
61 fontsource_client,
62 cache_root,
63 })
64 }
65
66 pub async fn render(&self, layout: Layout) -> Result<Image> {
68 let mut canvas = RgbaImage::new(
69 layout.size.width.unwrap_or(WIDTH).get(),
70 layout.size.height.unwrap_or(HEIGHT).get(),
71 );
72
73 let opt = Options {
76 fontdb: self.fontdb.clone(),
77 ..Default::default()
78 };
79
80 let mut renderer =
81 Renderer::new(opt, &self.fontsource_client, &self.external_resource_paths);
82 for layer in &layout.layers {
83 renderer.render_layer(layer, &mut canvas).await?;
84 }
85 renderer.render_debug(&layout, &mut canvas).await?;
86 Ok(Image { data: canvas })
87 }
88}
89
90#[cfg_attr(feature = "pyo3", pyclass(module = "img_gen", from_py_object))]
95#[derive(Clone)]
96pub struct Image {
97 pub data: RgbaImage,
99}
100
101impl Image {
102 pub fn save<P: AsRef<Path>>(&self, name: P) -> Result<()> {
107 let name = name.as_ref();
108 self.data
109 .save(name)
110 .map_err(|source| ImgGenRendererError::SaveImageFailed {
111 path: name.to_string_lossy().into_owned(),
112 source,
113 })
114 }
115
116 pub fn get_bytes(&'_ self) -> Result<Cow<'_, [u8]>> {
118 let bytes: Vec<u8> = self
119 .data
120 .bytes()
121 .collect::<std::io::Result<Vec<u8>>>()
122 .map_err(|source| ImgGenRendererError::CollectImageBytesFailed { source })?;
123 Ok(Cow::Owned(bytes))
124 }
125
126 pub fn get_sha256(&self) -> Result<String> {
128 let mut hash_gen = Sha256::new();
129 hash_gen.update(self.get_bytes()?);
130 let digest = hash_gen.finalize();
131 Ok(digest
132 .as_slice()
133 .iter()
134 .map(|byte| format!("{byte:02x}"))
135 .collect())
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 #![allow(clippy::unwrap_used)]
142
143 use super::{Image, RgbaImage};
144
145 #[test]
146 fn hash() {
147 let buffer = RgbaImage::new(50, 50);
148 let img = Image { data: buffer };
149 let sha256 = img.get_sha256().unwrap();
150 assert_eq!(
151 sha256,
152 "95b532cc4381affdff0d956e12520a04129ed49d37e154228368fe5621f0b9a2"
153 );
154 }
155}