diff --git a/plotly/Cargo.toml b/plotly/Cargo.toml
index d2408b3b..41867f34 100644
--- a/plotly/Cargo.toml
+++ b/plotly/Cargo.toml
@@ -50,3 +50,4 @@ itertools-num = "0.1.3"
ndarray = "0.16.0"
plotly_kaleido = { version = "0.10.0", path = "../plotly_kaleido" }
rand_distr = "0.4"
+base64 = "0.22"
diff --git a/plotly/src/plot.rs b/plotly/src/plot.rs
index f1d1b300..1bbaacb1 100644
--- a/plotly/src/plot.rs
+++ b/plotly/src/plot.rs
@@ -417,6 +417,52 @@ impl Plot {
.unwrap_or_else(|_| panic!("failed to export plot to {:?}", filename.as_ref()));
}
+ /// Convert the `Plot` to a static image and return the image as a `base64`
+ /// String Supported formats are [ImageFormat::JPEG], [ImageFormat::PNG]
+ /// and [ImageFormat::WEBP]
+ #[cfg(feature = "kaleido")]
+ pub fn to_base64(
+ &self,
+ format: ImageFormat,
+ width: usize,
+ height: usize,
+ scale: f64,
+ ) -> String {
+ match format {
+ ImageFormat::JPEG | ImageFormat::PNG | ImageFormat::WEBP => {
+ let kaleido = plotly_kaleido::Kaleido::new();
+ kaleido
+ .image_to_string(
+ &serde_json::to_value(self).unwrap(),
+ &format.to_string(),
+ width,
+ height,
+ scale,
+ )
+ .unwrap_or_else(|_| panic!("Kaleido failed to generate image"))
+ }
+ _ => {
+ eprintln!("Cannot generate base64 string for ImageFormat:{format}. Allowed formats are JPEG, PNG, WEBP");
+ String::default()
+ }
+ }
+ }
+
+ /// Convert the `Plot` to SVG and return it as a String.
+ #[cfg(feature = "kaleido")]
+ pub fn to_svg(&self, width: usize, height: usize, scale: f64) -> String {
+ let kaleido = plotly_kaleido::Kaleido::new();
+ kaleido
+ .image_to_string(
+ &serde_json::to_value(self).unwrap(),
+ "svg",
+ width,
+ height,
+ scale,
+ )
+ .unwrap_or_else(|_| panic!("Kaleido failed to generate image"))
+ }
+
fn render(&self) -> String {
let tmpl = PlotTemplate {
plot: self,
@@ -539,6 +585,7 @@ impl PartialEq for Plot {
mod tests {
use std::path::PathBuf;
+ use base64::{engine::general_purpose, Engine as _};
use serde_json::{json, to_value};
use super::*;
@@ -773,4 +820,47 @@ mod tests {
assert!(std::fs::remove_file(&dst).is_ok());
assert!(!dst.exists());
}
+
+ #[cfg(target_os = "linux")]
+ #[test]
+ #[cfg(feature = "kaleido")]
+ fn test_image_to_base64() {
+ let plot = create_test_plot();
+
+ let image_base64 = plot.to_base64(ImageFormat::PNG, 200, 150, 1.0);
+
+ assert!(!image_base64.is_empty());
+
+ let result_decoded = general_purpose::STANDARD.decode(image_base64).unwrap();
+ let expected = "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";
+ let expected_decoded = general_purpose::STANDARD.decode(expected).unwrap();
+
+ // Comparing the result seems to end up being a flaky test.
+ // Limit the comparison to the first characters;
+ // As image contents seem to be slightly inconsistent across platforms
+ assert_eq!(expected_decoded[..2], result_decoded[..2]);
+ }
+
+ #[test]
+ #[cfg(feature = "kaleido")]
+ fn test_image_to_base64_invalid_format() {
+ let plot = create_test_plot();
+ let image_base64 = plot.to_base64(ImageFormat::EPS, 200, 150, 1.0);
+ assert!(image_base64.is_empty());
+ }
+
+ #[cfg(target_os = "linux")]
+ #[test]
+ #[cfg(feature = "kaleido")]
+ fn test_image_to_svg_string() {
+ let plot = create_test_plot();
+ let image_svg = plot.to_svg(200, 150, 1.0);
+
+ assert!(!image_svg.is_empty());
+
+ let expected = "";
+ // Limit the test to the first LEN characters
+ const LEN: usize = 100;
+ assert_eq!(expected[..LEN], image_svg[..LEN]);
+ }
}
diff --git a/plotly_kaleido/src/lib.rs b/plotly_kaleido/src/lib.rs
index f6641c25..6b2fee7b 100644
--- a/plotly_kaleido/src/lib.rs
+++ b/plotly_kaleido/src/lib.rs
@@ -122,6 +122,7 @@ impl Kaleido {
Ok(p)
}
+ /// Generate a static image from a Plotly graph and save it to a file
pub fn save(
&self,
dst: &Path,
@@ -134,6 +135,44 @@ impl Kaleido {
let mut dst = PathBuf::from(dst);
dst.set_extension(format);
+ let image_data = self.convert(plotly_data, format, width, height, scale)?;
+ let data: Vec = match format {
+ "svg" | "eps" => image_data.as_bytes().to_vec(),
+ _ => general_purpose::STANDARD.decode(image_data).unwrap(),
+ };
+ let mut file = File::create(dst.as_path())?;
+ file.write_all(&data)?;
+ file.flush()?;
+
+ Ok(())
+ }
+
+ /// Generate a static image from a Plotly graph and return it as a String
+ /// The output may be base64 encoded or a plain text depending on the image
+ /// format provided as argument. SVG and EPS are returned in plain text
+ /// while JPEG, PNG, WEBP will be returned as a base64 encoded string.
+ pub fn image_to_string(
+ &self,
+ plotly_data: &Value,
+ format: &str,
+ width: usize,
+ height: usize,
+ scale: f64,
+ ) -> Result> {
+ let image_data = self.convert(plotly_data, format, width, height, scale)?;
+ Ok(image_data)
+ }
+
+ /// Convert the Plotly graph to a static image using Kaleido and return the
+ /// result as a String
+ pub fn convert(
+ &self,
+ plotly_data: &Value,
+ format: &str,
+ width: usize,
+ height: usize,
+ scale: f64,
+ ) -> Result> {
let p = self.cmd_path.as_path();
let p = p.to_str().unwrap();
let p = String::from(p);
@@ -168,17 +207,16 @@ impl Kaleido {
for line in output_lines.map_while(Result::ok) {
let res = KaleidoResult::from(line.as_str());
if let Some(image_data) = res.result {
- let data: Vec = match format {
- "svg" | "eps" => image_data.as_bytes().to_vec(),
- _ => general_purpose::STANDARD.decode(image_data).unwrap(),
- };
- let mut file = File::create(dst.as_path())?;
- file.write_all(&data)?;
- file.flush()?;
+ // TODO: this should be refactored
+ // The assumption is that KaleidoResult contains a single image.
+ // We should end the loop on the first valid one.
+ // If that is not the case, prior implementation would have returned the last
+ // valid image
+ return Ok(image_data);
}
}
- Ok(())
+ Ok(String::default())
}
}