mondrian_charts/
chart_to_image.rs

1use crate::{chart_to_svg, ChartOptions, MondrianChart};
2use image::{EncodableLayout, RgbaImage};
3use resvg::tiny_skia::{Color, Pixmap};
4use resvg::usvg::{self, TreeTextToPath};
5use std::io::Cursor;
6use thiserror::Error;
7
8pub use image::{ImageError, ImageFormat};
9pub use resvg::usvg::fontdb;
10
11#[derive(Debug, Error)]
12pub enum ImageRenderingError {
13    #[error("Error writing the image format")]
14    ImageError(ImageError),
15    #[error("Invalid color string")]
16    InvalidColor { color: String },
17    #[error("Could not create a buffer for the given image dimensions")]
18    InvalidDimensions,
19    #[error("SVG representation was invalid")]
20    InvalidSvg(String),
21    #[error("{0}")]
22    Other(String),
23}
24
25/// Options for generating an image from a Mondrian chart.
26pub struct ImageOptions {
27    /// The font database to be used for rendering ticks.
28    ///
29    /// Currently, only the `sans-serif` font needs to be initialized.
30    pub fonts: fontdb::Database,
31
32    /// The file format to use for the generated image.
33    pub format: ImageFormat,
34
35    /// Background color to render the chart on.
36    ///
37    /// Must be a valid CSS color string.
38    pub background_color: String,
39}
40
41pub fn chart_to_image<'a, S, P>(
42    chart: &'a MondrianChart<S, P>,
43    chart_options: &ChartOptions<'a, S>,
44    image_options: &ImageOptions,
45) -> Result<Vec<u8>, ImageRenderingError> {
46    let svg = chart_to_svg(chart, chart_options);
47
48    let image = render_svg_to_buffer(&svg, chart_options, image_options)?;
49
50    // Give the buffer an initial capacity that matches the raw image size. This
51    // probably allocates more memory than necessary, but it's better than
52    // growing the buffer many times.
53    let mut buffer = Vec::with_capacity(image.as_bytes().len());
54    image
55        .write_to(&mut Cursor::new(&mut buffer), image_options.format)
56        .map_err(ImageRenderingError::ImageError)?;
57
58    Ok(buffer)
59}
60
61fn render_svg_to_buffer<S>(
62    svg: &str,
63    chart_options: &ChartOptions<S>,
64    image_options: &ImageOptions,
65) -> Result<image::RgbaImage, ImageRenderingError> {
66    let &ChartOptions { width, height, .. } = chart_options;
67
68    let background_color = parse_color(&image_options.background_color)?;
69    let mut pixels =
70        Pixmap::new(width as u32, height as u32).ok_or(ImageRenderingError::InvalidDimensions)?;
71    pixels.fill(background_color);
72
73    let mut tree: usvg::Tree =
74        usvg::TreeParsing::from_data(svg.as_bytes(), &usvg::Options::default())
75            .map_err(|error| ImageRenderingError::InvalidSvg(error.to_string()))?;
76
77    tree.convert_text(&image_options.fonts);
78    resvg::Tree::from_usvg(&tree).render(usvg::Transform::identity(), &mut pixels.as_mut());
79
80    let img = RgbaImage::from_vec(width as u32, height as u32, pixels.take()).ok_or(
81        ImageRenderingError::Other("Could not create ImageBuffer from bytes".to_owned()),
82    )?;
83
84    Ok(img)
85}
86
87fn parse_color(string: &str) -> Result<Color, ImageRenderingError> {
88    csscolorparser::parse(string)
89        .ok()
90        .and_then(|csscolorparser::Color { r, g, b, a }| {
91            Color::from_rgba(r as f32, g as f32, b as f32, a as f32)
92        })
93        .ok_or_else(|| ImageRenderingError::InvalidColor {
94            color: string.to_owned(),
95        })
96}