charming_fork_zephyr/renderer/
image_renderer.rs

1use std::io::Cursor;
2
3use deno_core::{v8, JsRuntime, RuntimeOptions};
4use handlebars::Handlebars;
5use image::RgbaImage;
6use resvg::{
7    tiny_skia::Pixmap,
8    usvg::{self, TreeTextToPath},
9};
10
11use crate::{theme::Theme, Chart, EchartsError};
12
13static CODE_TEMPLATE: &str = r#"
14{{#if theme_source}}{{{ theme_source }}}{{/if}}
15var chart = echarts.init(null, {{#if theme}}'{{ theme }}'{{else}}null{{/if}}, {
16    renderer: 'svg',
17    ssr: true,
18    width: {{ width }},
19    height: {{ height }}
20});
21
22chart.setOption({ animation: false });
23chart.setOption({{{ chart_option }}});
24chart.renderToSVGString();
25"#;
26
27pub use image::ImageFormat;
28
29pub struct ImageRenderer {
30    js_runtime: JsRuntime,
31    fontdb: usvg::fontdb::Database,
32    theme: Theme,
33    width: u32,
34    height: u32,
35}
36
37impl ImageRenderer {
38    pub fn new(width: u32, height: u32) -> Self {
39        let mut runtime = JsRuntime::new(RuntimeOptions::default());
40        runtime
41            .execute_script(
42                "[runtime.js]",
43                include_str!("../asset/runtime.js").to_string().into(),
44            )
45            .unwrap();
46        runtime
47            .execute_script(
48                "[echarts.js]",
49                include_str!("../asset/echarts-5.4.2.min.js")
50                    .to_string()
51                    .into(),
52            )
53            .unwrap();
54
55        let mut fontdb = usvg::fontdb::Database::default();
56        fontdb.load_system_fonts();
57
58        #[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
59        {
60            set_default_fonts(&mut fontdb);
61        }
62
63        Self {
64            js_runtime: runtime,
65            fontdb,
66            theme: Theme::Default,
67            width,
68            height,
69        }
70    }
71
72    pub fn theme(mut self, theme: Theme) -> Self {
73        self.theme = theme;
74        self
75    }
76
77    /// Render chart to an SVG String
78    pub fn render(&mut self, chart: &Chart) -> Result<String, EchartsError> {
79        let (theme, theme_source) = self.theme.to_str();
80        let code = Handlebars::new()
81            .render_template(
82                CODE_TEMPLATE,
83                &serde_json::json!({
84                    "theme": theme,
85                    "theme_source": theme_source,
86                    "width": self.width,
87                    "height": self.height,
88                    "chart_option": chart.to_string(),
89                }),
90            )
91            .expect("Failed to render template");
92        let result = self.js_runtime.execute_script("[anon]", code.into());
93
94        match result {
95            Ok(global) => {
96                let scope = &mut self.js_runtime.handle_scope();
97                let local = v8::Local::new(scope, global);
98                let value = serde_v8::from_v8::<serde_json::Value>(scope, local);
99
100                match value {
101                    Ok(value) => Ok(value.as_str().unwrap().to_string()),
102                    Err(error) => Err(EchartsError::JsRuntimeError(error.to_string())),
103                }
104            }
105            Err(error) => Err(EchartsError::JsRuntimeError(error.to_string())),
106        }
107    }
108
109    /// Render a chart to a given image format in bytes
110    pub fn render_format(
111        &mut self,
112        image_format: ImageFormat,
113        chart: &Chart,
114    ) -> Result<Vec<u8>, EchartsError> {
115        let svg = self.render(chart)?;
116
117        let img = self.render_svg_to_buf(&svg)?;
118
119        // give buf initial capacity of: width * height * num of channels for RGBA + room for headers/metadata
120        let estimated_capacity = self.width * self.height * 4 + 1024;
121        let mut buf = Vec::with_capacity(estimated_capacity as usize);
122        img.write_to(&mut Cursor::new(&mut buf), image_format)
123            .map_err(|error| EchartsError::ImageRenderingError(error.to_string()))?;
124        Ok(buf)
125    }
126
127    /// Given an svg str, render it into an [`image::ImageBuffer`]
128    fn render_svg_to_buf(&mut self, svg: &str) -> Result<image::RgbaImage, EchartsError> {
129        let mut pixels =
130            Pixmap::new(self.width, self.height).ok_or(EchartsError::ImageRenderingError(
131                "Rendered image cannot be greater than i32::MAX/4".to_string(),
132            ))?;
133
134        let mut tree: usvg::Tree =
135            usvg::TreeParsing::from_data(svg.as_bytes(), &usvg::Options::default())
136                .map_err(|error| EchartsError::ImageRenderingError(error.to_string()))?;
137
138        tree.convert_text(&self.fontdb);
139        resvg::Tree::from_usvg(&tree).render(usvg::Transform::identity(), &mut pixels.as_mut());
140
141        let img = RgbaImage::from_vec(self.width, self.height, pixels.take()).ok_or(
142            EchartsError::ImageRenderingError(
143                "Could not create ImageBuffer from bytes".to_string(),
144            ),
145        )?;
146
147        Ok(img)
148    }
149
150    /// Render and save chart as an SVG
151    pub fn save<P: AsRef<std::path::Path>>(
152        &mut self,
153        chart: &Chart,
154        path: P,
155    ) -> Result<(), EchartsError> {
156        let svg = self.render(chart)?;
157        std::fs::write(path, svg)
158            .map_err(|error| EchartsError::ImageRenderingError(error.to_string()))
159    }
160
161    /// Render and save chart as the given image format
162    pub fn save_format<P: AsRef<std::path::Path>>(
163        &mut self,
164        image_format: ImageFormat,
165        chart: &Chart,
166        path: P,
167    ) -> Result<(), EchartsError> {
168        let svg = self.render(chart)?;
169        let img = self.render_svg_to_buf(&svg)?;
170        img.save_with_format(path, image_format)
171            .map_err(|error| EchartsError::ImageRenderingError(error.to_string()))
172    }
173}
174
175#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
176fn set_default_fonts(fontdb: &mut usvg::fontdb::Database) {
177    let sans_serif_fonts = vec![
178        "DejaVu Sans",
179        "FreeSans",
180        "Liberation Sans",
181        "Arimo",
182        "Cantarell",
183        "Nimbus Sans",
184    ];
185
186    let serif_fonts = vec![
187        "DejaVu Serif",
188        "FreeSerif",
189        "Liberation Serif",
190        "Tinos",
191        "Nimbus Roman",
192    ];
193
194    let monospace_fonts = vec![
195        "DejaVu Sans Mono",
196        "FreeMono",
197        "Liberation Mono",
198        "Nimbus Mono",
199    ];
200
201    for font in sans_serif_fonts {
202        if font_exists(fontdb, font) {
203            fontdb.set_sans_serif_family(font);
204            break;
205        }
206    }
207
208    for font in serif_fonts {
209        if font_exists(fontdb, font) {
210            fontdb.set_serif_family(font);
211            break;
212        }
213    }
214
215    for font in monospace_fonts {
216        if font_exists(fontdb, font) {
217            fontdb.set_monospace_family(font);
218            break;
219        }
220    }
221}
222
223#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
224fn font_exists(fontdb: &usvg::fontdb::Database, family: &str) -> bool {
225    fontdb
226        .query(&usvg::fontdb::Query {
227            families: &[usvg::fontdb::Family::Name(family)],
228            weight: usvg::fontdb::Weight(14),
229            stretch: usvg::fontdb::Stretch::Normal,
230            style: usvg::fontdb::Style::Normal,
231        })
232        .is_some()
233}