charming_fork_zephyr/renderer/
image_renderer.rs1use 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 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 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 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 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 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 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}