use deno_core::{v8, JsRuntime, RuntimeOptions};
use handlebars::Handlebars;
#[cfg(feature = "ssr-raster")]
use image::RgbaImage;
#[cfg(feature = "ssr-raster")]
use resvg::{tiny_skia::Pixmap, usvg};
#[cfg(feature = "ssr-raster")]
use std::io::Cursor;
#[cfg(feature = "ssr-raster")]
use std::sync::Arc;
use crate::{theme::Theme, Chart, EchartsError};
static CODE_TEMPLATE: &str = r#"
{{#if theme_source}}{{{ theme_source }}}{{/if}}
var chart = echarts.init(null, {{#if theme}}'{{ theme }}'{{else}}null{{/if}}, {
renderer: 'svg',
ssr: true,
width: {{ width }},
height: {{ height }}
});
chart.setOption({ animation: false, progressive: 0 });
chart.setOption({{{ chart_option }}});
chart.renderToSVGString();
"#;
#[cfg(feature = "ssr-raster")]
#[cfg_attr(docsrs, doc(cfg(feature = "ssr-raster")))]
pub use image::ImageFormat;
pub struct ImageRenderer {
js_runtime: JsRuntime,
#[cfg(feature = "ssr-raster")]
fontdb: Arc<usvg::fontdb::Database>,
theme: Theme,
width: u32,
height: u32,
}
impl ImageRenderer {
pub fn new(width: u32, height: u32) -> Self {
let mut runtime = JsRuntime::new(RuntimeOptions::default());
runtime
.execute_script(
"[runtime.js]",
include_str!("../asset/runtime.js").to_string(),
)
.unwrap();
runtime
.execute_script(
"[echarts.js]",
include_str!("../asset/echarts-5.5.1.min.js").to_string(),
)
.unwrap();
#[cfg(feature = "ssr-raster")]
let mut fontdb = usvg::fontdb::Database::default();
#[cfg(feature = "ssr-raster")]
fontdb.load_system_fonts();
#[cfg(all(
feature = "ssr-raster",
unix,
not(any(target_os = "macos", target_os = "android"))
))]
{
set_default_fonts(&mut fontdb);
}
Self {
js_runtime: runtime,
#[cfg(feature = "ssr-raster")]
fontdb: Arc::new(fontdb),
theme: Theme::Default,
width,
height,
}
}
pub fn theme(mut self, theme: Theme) -> Self {
self.theme = theme;
self
}
pub fn render(&mut self, chart: &Chart) -> Result<String, EchartsError> {
let (theme, theme_source) = self.theme.to_str();
let code = Handlebars::new()
.render_template(
CODE_TEMPLATE,
&serde_json::json!({
"theme": theme,
"theme_source": theme_source,
"width": self.width,
"height": self.height,
"chart_option": chart.to_string(),
}),
)
.expect("Failed to render template");
let result = self.js_runtime.execute_script("[anon]", code);
match result {
Ok(global) => {
let scope = &mut self.js_runtime.handle_scope();
let local = v8::Local::new(scope, global);
let value = serde_v8::from_v8::<serde_json::Value>(scope, local);
match value {
Ok(value) => Ok(value.as_str().unwrap().to_string()),
Err(error) => Err(EchartsError::JsRuntimeError(error.to_string())),
}
}
Err(error) => Err(EchartsError::JsRuntimeError(error.to_string())),
}
}
#[cfg(feature = "ssr-raster")]
#[cfg_attr(docsrs, doc(cfg(feature = "ssr-raster")))]
pub fn render_format(
&mut self,
image_format: ImageFormat,
chart: &Chart,
) -> Result<Vec<u8>, EchartsError> {
let svg = self.render(chart)?;
let img = self.render_svg_to_buf(&svg)?;
let estimated_capacity = self.width * self.height * 4 + 1024;
let mut buf = Vec::with_capacity(estimated_capacity as usize);
img.write_to(&mut Cursor::new(&mut buf), image_format)
.map_err(|error| EchartsError::ImageRenderingError(error.to_string()))?;
Ok(buf)
}
#[cfg(feature = "ssr-raster")]
#[cfg_attr(docsrs, doc(cfg(feature = "ssr-raster")))]
fn render_svg_to_buf(&mut self, svg: &str) -> Result<image::RgbaImage, EchartsError> {
let mut pixels =
Pixmap::new(self.width, self.height).ok_or(EchartsError::ImageRenderingError(
"Rendered image cannot be greater than i32::MAX/4".to_string(),
))?;
let options = usvg::Options {
fontdb: Arc::clone(&self.fontdb),
..Default::default()
};
let tree = usvg::Tree::from_data(svg.as_bytes(), &options)
.map_err(|error| EchartsError::ImageRenderingError(error.to_string()))?;
resvg::render(&tree, usvg::Transform::identity(), &mut pixels.as_mut());
let img = RgbaImage::from_vec(self.width, self.height, pixels.take()).ok_or(
EchartsError::ImageRenderingError(
"Could not create ImageBuffer from bytes".to_string(),
),
)?;
Ok(img)
}
pub fn save<P: AsRef<std::path::Path>>(
&mut self,
chart: &Chart,
path: P,
) -> Result<(), EchartsError> {
let svg = self.render(chart)?;
std::fs::write(path, svg)
.map_err(|error| EchartsError::ImageRenderingError(error.to_string()))
}
#[cfg(feature = "ssr-raster")]
#[cfg_attr(docsrs, doc(cfg(feature = "ssr-raster")))]
pub fn save_format<P: AsRef<std::path::Path>>(
&mut self,
image_format: ImageFormat,
chart: &Chart,
path: P,
) -> Result<(), EchartsError> {
let svg = self.render(chart)?;
let img = self.render_svg_to_buf(&svg)?;
img.save_with_format(path, image_format)
.map_err(|error| EchartsError::ImageRenderingError(error.to_string()))
}
}
#[cfg(all(
feature = "ssr-raster",
unix,
not(any(target_os = "macos", target_os = "android"))
))]
#[cfg_attr(docsrs, doc(cfg(feature = "ssr-raster")))]
fn set_default_fonts(fontdb: &mut usvg::fontdb::Database) {
let sans_serif_fonts = vec![
"DejaVu Sans",
"FreeSans",
"Liberation Sans",
"Arimo",
"Cantarell",
"Nimbus Sans",
];
let serif_fonts = vec![
"DejaVu Serif",
"FreeSerif",
"Liberation Serif",
"Tinos",
"Nimbus Roman",
];
let monospace_fonts = vec![
"DejaVu Sans Mono",
"FreeMono",
"Liberation Mono",
"Nimbus Mono",
];
for font in sans_serif_fonts {
if font_exists(fontdb, font) {
fontdb.set_sans_serif_family(font);
break;
}
}
for font in serif_fonts {
if font_exists(fontdb, font) {
fontdb.set_serif_family(font);
break;
}
}
for font in monospace_fonts {
if font_exists(fontdb, font) {
fontdb.set_monospace_family(font);
break;
}
}
}
#[cfg(all(
feature = "ssr-raster",
unix,
not(any(target_os = "macos", target_os = "android"))
))]
#[cfg_attr(docsrs, doc(cfg(feature = "ssr-raster")))]
fn font_exists(fontdb: &usvg::fontdb::Database, family: &str) -> bool {
fontdb
.query(&usvg::fontdb::Query {
families: &[usvg::fontdb::Family::Name(family)],
weight: usvg::fontdb::Weight(14),
stretch: usvg::fontdb::Stretch::Normal,
style: usvg::fontdb::Style::Normal,
})
.is_some()
}