Skip to main content

img_gen_renderer/generator/
mod.rs

1use 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/// A class to represent an image generator.
24///
25/// The given `Layout` describes how to generate the `Image`.
26///
27/// This struct caches the font database and font source client to avoid
28/// re-initializing them on every `render()` call.
29#[cfg_attr(feature = "pyo3", pyclass(module = "img_gen", from_py_object))]
30#[derive(Clone)]
31pub struct Generator {
32    /// Search paths used to resolve input images, SVG files, or font files.
33    pub external_resource_paths: Vec<PathBuf>,
34    fontdb: Arc<fontdb::Database>,
35    fontsource_client: FontSourceClient,
36    /// The root directory used for renderer cache data such as downloaded fonts.
37    pub cache_root: PathBuf,
38}
39
40impl Generator {
41    /// Create a new `Generator` with the given image search paths.
42    ///
43    /// This initializes the shared font database and font source client once.
44    /// Both are cached in-memory and reused across all subsequent renders.
45    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                // Keep font downloads under img-gen's app cache by default.
51                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    /// Render the `Image` described by the `Generator`'s `Layout`.
67    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        // Create Options from the cached fontdb. This is cheap since we're just
74        // cloning Arcs and reusing the pre-loaded font database.
75        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/// A class to represent an Image object.
91///
92/// This class cannot be constructed from python, rather it is returned from
93/// `Generator.render()`
94#[cfg_attr(feature = "pyo3", pyclass(module = "img_gen", from_py_object))]
95#[derive(Clone)]
96pub struct Image {
97    /// The raw RGBA pixel buffer for the rendered image.
98    pub data: RgbaImage,
99}
100
101impl Image {
102    /// Save the image to a file.
103    ///
104    /// Does not support SVG output.
105    /// The file format is inferred from the file extension in the given `name`.
106    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    /// Get a byte array of the image data.
117    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    /// Get the SHA256 hash of the image data as a hex string.
127    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}