From b6155ab006bb8058367b0327ba447239b239892c Mon Sep 17 00:00:00 2001 From: Andrei <8067229+andrei-ng@users.noreply.github.com> Date: Sat, 21 Dec 2024 11:24:21 +0100 Subject: [PATCH] allow users to set Kaleido path via envionment variable (#262) * allow users to set Kaleido path via envionment variable - introduced a new feature to allow users to download Kaleido at compile time when the applications are targeted for the host machine - this can be overriden by the runtime environment variable * add no-sanbox arg to Kaleido process - something fishy is happening in the CI, without this argument empty files are generated because of chromium security issues - print stderr of Kaleido to console Signed-off-by: Andrei Gherghescu <8067229+andrei-ng@users.noreply.github.com> --- README.md | 41 ++++++++++---- examples/kaleido/Cargo.toml | 7 ++- examples/kaleido/src/main.rs | 12 +++-- plotly/Cargo.toml | 8 ++- plotly/src/plot.rs | 14 ++--- plotly_kaleido/Cargo.toml | 14 +++-- plotly_kaleido/build.rs | 83 ++++++++++++++++++----------- plotly_kaleido/src/lib.rs | 100 +++++++++++++++++++++++------------ 8 files changed, 185 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 884af831..cd2c4883 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ * [Introduction](#introduction) * [Basic Usage](#basic-usage) * [Exporting an Interactive Plot](#exporting-an-interactive-plot) - * [Exporting a Static Image](#exporting-a-static-image) + * [Exporting Static Images with Kaleido](#exporting-static-images-with-kaleido) * [Usage Within a Wasm Environment](#usage-within-a-wasm-environment) * [Crate Feature Flags](#crate-feature-flags) * [Contributing](#contributing) @@ -96,10 +96,30 @@ If you only want to view the plot in the browser quickly, use the `Plot.show()` plot.show(); // The default web browser will open, displaying an interactive plot ``` -## Exporting a Static Image +## Exporting Static Images with Kaleido -To save a plot as a static image, the `kaleido` feature is required: +To save a plot as a static image, the `kaleido` feature is required as well as installing an **external dependency**. +### Kaleido external dependency + +When developing applications for your host, enabling both `kaleido` and `kaleido_download` features will ensure that the `kaleido` binary is downloaded for your system's architecture at compile time. After download, it is unpacked into a specific path, e.g., on Linux this is `/home/USERNAME/.config/kaleido`. With these two features enabled, static images can be exported as described in the next section as long as the application run on the same host where where this crate was compiled on. + +When the applications developed with `plotly.rs` are intended for other targets or when the user wants to control where the `kaleido` binary is installed then Kaleido must be manually downloaded and installed. Setting the environment variable `KALEIDO_PATH=/path/installed/kaleido/` will ensure that applications that were built with the `kaleido` feature enabled can locate the `kaleido` executable and use it to generate static images. + +Kaleido binaries are available on Github [release page](https://github.com/plotly/Kaleido/releases). It currently supports Linux(`x86_64`), Windows(`x86_64`) and MacOS(`x86_64`/`aarch64`). + +## Exporting a Static Images + +Enable the `kaleido` feature and opt in for automatic downloading of the `kaleido` binaries by doing the following + +```toml +# Cargo.toml + +[dependencies] +plotly = { version = "0.11", features = ["kaleido", "kaleido_download"] } +``` + +Alternatively, enable only the `kaleido` feature and manually install Kaleido. ```toml # Cargo.toml @@ -107,7 +127,7 @@ To save a plot as a static image, the `kaleido` feature is required: plotly = { version = "0.11", features = ["kaleido"] } ``` -With this feature enabled, plots can be saved as any of `png`, `jpeg`, `webp`, `svg`, `pdf` and `eps`. Note that the plot will be a static image, i.e. they will be non-interactive. +With the feature enabled, plots can be saved as any of `png`, `jpeg`, `webp`, `svg`, `pdf` and `eps`. Note that the plot will be a static image, i.e. they will be non-interactive. Exporting a simple plot looks like this: @@ -121,12 +141,6 @@ plot.add_trace(trace); plot.write_image("out.png", ImageFormat::PNG, 800, 600, 1.0); ``` -### _Kaleido dependency_ - -On your host, when building this project with the `kaleido` feature enabled the Kaleido binary is downloaded automatically for your system's architecture at compile time from the official Kaleido [release page](https://github.com/plotly/Kaleido/releases). This library currently supports `x86_64` on Linux and Windows, and both `x86_64` and `aarch64` on macOS. - -When building application for other targets that depend on this feature, the `Kaleido` binary will need to be installed manually on the target machine. Currently, the location where the binary is expected is hardcoded depending on the target OS. E.g., on Linux this defaults to `~/.config/kaleido`. This is defined in source code [here](https://github.com/plotly/plotly.rs/blob/1405731b5121c1343b491e307222a21ef4becc5e/plotly_kaleido/src/lib.rs#L89) - ## Usage Within a Wasm Environment Using `Plotly.rs` in a Wasm-based frontend framework is possible by enabling the `wasm` feature: @@ -198,6 +212,13 @@ The following feature flags are available: Adds plot save functionality to the following formats: `png`, `jpeg`, `webp`, `svg`, `pdf` and `eps`. +Requires `Kaleido` to have been previously installed on the host machine. See the following feature flag and [Kaleido external dependency](#kaleido-external-dependency). + +### `kaleido_download` + +Enable download and install of Kaleido binary at build time from [Kaleido releases](https://github.com/plotly/Kaleido/releases/) on the host machine. +See [Kaleido external dependency](#kaleido-external-dependency) for more details. + ### `plotly_image` Adds trait implementations so that `image::RgbImage` and `image::RgbaImage` can be used more directly with the `plotly::Image` trace. diff --git a/examples/kaleido/Cargo.toml b/examples/kaleido/Cargo.toml index 4d3bc714..5dc65140 100644 --- a/examples/kaleido/Cargo.toml +++ b/examples/kaleido/Cargo.toml @@ -1,8 +1,11 @@ [package] name = "kaleido" version = "0.1.0" -authors = ["Michael Freeborn "] +authors = [ + "Michael Freeborn ", + "Andrei Gherghescu andrei-ng@protonmail.com", +] edition = "2021" [dependencies] -plotly = { path = "../../plotly", features = ["kaleido"] } \ No newline at end of file +plotly = { path = "../../plotly", features = ["kaleido", "kaleido_download"] } diff --git a/examples/kaleido/src/main.rs b/examples/kaleido/src/main.rs index 02d9e300..b2d1b827 100644 --- a/examples/kaleido/src/main.rs +++ b/examples/kaleido/src/main.rs @@ -5,15 +5,21 @@ fn main() { let trace = Scatter::new(vec![0, 1, 2], vec![2, 1, 0]); plot.add_trace(trace); - // Adjust these arguments to set the image format, width and height of the + // Adjust these arguments to set the width and height of the // output image. let filename = "out"; - let image_format = ImageFormat::PNG; let width = 800; let height = 600; let scale = 1.0; // The image will be saved to format!("{filename}.{image_format}") relative to // the current working directory. - plot.write_image(filename, image_format, width, height, scale); + plot.write_image(filename, ImageFormat::EPS, width, height, scale); + plot.write_image(filename, ImageFormat::JPEG, width, height, scale); + plot.write_image(filename, ImageFormat::PDF, width, height, scale); + plot.write_image(filename, ImageFormat::PNG, width, height, scale); + plot.write_image(filename, ImageFormat::SVG, width, height, scale); + plot.write_image(filename, ImageFormat::WEBP, width, height, scale); + + let _svg_string = plot.to_svg(width, height, scale); } diff --git a/plotly/Cargo.toml b/plotly/Cargo.toml index 3ba11657..9a03aaa0 100644 --- a/plotly/Cargo.toml +++ b/plotly/Cargo.toml @@ -15,10 +15,12 @@ exclude = ["target/*"] [features] kaleido = ["plotly_kaleido"] +kaleido_download = ["plotly_kaleido/download"] + plotly_ndarray = ["ndarray"] plotly_image = ["image"] -# Embed JavaScript into library and templates for offline use plotly_embed_js = [] + wasm = ["getrandom", "js-sys", "wasm-bindgen", "wasm-bindgen-futures"] with-axum = ["rinja/with-axum", "rinja_axum"] @@ -48,6 +50,8 @@ image = "0.25" itertools = ">=0.10, <0.14" itertools-num = "0.1" ndarray = "0.16" -plotly_kaleido = { version = "0.11", path = "../plotly_kaleido" } +plotly_kaleido = { version = "0.11", path = "../plotly_kaleido", features = [ + "download", +] } rand_distr = "0.4" base64 = "0.22" diff --git a/plotly/src/plot.rs b/plotly/src/plot.rs index 22478b97..717cee02 100644 --- a/plotly/src/plot.rs +++ b/plotly/src/plot.rs @@ -749,7 +749,7 @@ mod tests { assert!(!dst.exists()); } - #[cfg(target_os = "linux")] + #[cfg(not(target_os = "macos"))] #[test] #[cfg(feature = "kaleido")] fn test_save_to_png() { @@ -761,7 +761,7 @@ mod tests { assert!(!dst.exists()); } - #[cfg(target_os = "linux")] + #[cfg(not(target_os = "macos"))] #[test] #[cfg(feature = "kaleido")] fn test_save_to_jpeg() { @@ -773,7 +773,7 @@ mod tests { assert!(!dst.exists()); } - #[cfg(target_os = "linux")] + #[cfg(not(target_os = "macos"))] #[test] #[cfg(feature = "kaleido")] fn test_save_to_svg() { @@ -797,7 +797,7 @@ mod tests { assert!(!dst.exists()); } - #[cfg(target_os = "linux")] + #[cfg(not(target_os = "macos"))] #[test] #[cfg(feature = "kaleido")] fn test_save_to_pdf() { @@ -809,7 +809,7 @@ mod tests { assert!(!dst.exists()); } - #[cfg(target_os = "linux")] + #[cfg(not(target_os = "macos"))] #[test] #[cfg(feature = "kaleido")] fn test_save_to_webp() { @@ -821,8 +821,8 @@ mod tests { assert!(!dst.exists()); } - #[cfg(target_os = "linux")] #[test] + #[cfg(not(target_os = "macos"))] #[cfg(feature = "kaleido")] fn test_image_to_base64() { let plot = create_test_plot(); @@ -849,8 +849,8 @@ mod tests { assert!(image_base64.is_empty()); } - #[cfg(target_os = "linux")] #[test] + #[cfg(not(target_os = "macos"))] #[cfg(feature = "kaleido")] fn test_image_to_svg_string() { let plot = create_test_plot(); diff --git a/plotly_kaleido/Cargo.toml b/plotly_kaleido/Cargo.toml index 7e225564..5a1829ba 100644 --- a/plotly_kaleido/Cargo.toml +++ b/plotly_kaleido/Cargo.toml @@ -2,7 +2,10 @@ name = "plotly_kaleido" version = "0.11.0" description = "Additional output format support for plotly using Kaleido" -authors = ["Ioannis Giagkiozis "] +authors = [ + "Ioannis Giagkiozis ", + "Andrei Gherghescu andrei-ng@protonmail.com", +] license = "MIT" readme = "README.md" workspace = ".." @@ -14,12 +17,17 @@ keywords = ["plot", "chart", "plotly", "ndarray"] exclude = ["target/*", "kaleido/*", "examples/*"] +[features] +download = [] + [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -base64 = "0.22" dunce = "1.0" -directories = ">=4, <6" +base64 = "0.22" + +[dev-dependencies] +plotly_kaleido = { version = "0.11", path = ".", features = ["download"] } [build-dependencies] zip = "2.1" diff --git a/plotly_kaleido/build.rs b/plotly_kaleido/build.rs index cf47e82d..2e16fc26 100644 --- a/plotly_kaleido/build.rs +++ b/plotly_kaleido/build.rs @@ -30,15 +30,12 @@ const KALEIDO_URL: &str = const KALEIDO_URL: &str = "https://github.com/plotly/Kaleido/releases/download/v0.2.1/kaleido_mac_arm64.zip"; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "macos"))] const KALEIDO_BIN: &str = "kaleido"; #[cfg(target_os = "windows")] const KALEIDO_BIN: &str = "kaleido.exe"; -#[cfg(target_os = "macos")] -const KALEIDO_BIN: &str = "kaleido"; - fn extract_zip(p: &Path, zip_file: &Path) -> Result<()> { let file = fs::File::open(zip_file).unwrap(); let mut archive = zip::ZipArchive::new(file).unwrap(); @@ -95,35 +92,57 @@ fn extract_zip(p: &Path, zip_file: &Path) -> Result<()> { } fn main() -> Result<()> { - let project_dirs = ProjectDirs::from("org", "plotly", "kaleido") - .expect("Could not create plotly_kaleido config directory."); - let dst: PathBuf = project_dirs.config_dir().into(); + if cfg!(feature = "download") { + let project_dirs = ProjectDirs::from("org", "plotly", "kaleido") + .expect("Could not create Kaleido config directory path."); + let dst: PathBuf = project_dirs.config_dir().into(); + + let kaleido_binary = dst.join("bin").join(KALEIDO_BIN); + + println!("cargo:rerun-if-changed=src/lib.rs"); + println!( + "cargo::rerun-if-changed={}", + kaleido_binary.to_string_lossy() + ); + + println!( + "cargo:rustc-env=KALEIDO_COMPILE_TIME_DLD_PATH={}", + dst.to_string_lossy() + ); + + if kaleido_binary.exists() { + return Ok(()); + } - let kaleido_binary = dst.join("bin").join(KALEIDO_BIN); - if kaleido_binary.exists() { - return Ok(()); + let msg = format!( + "Downloaded Plotly Kaleido from {KALEIDO_URL} to '{}'", + dst.to_string_lossy() + ); + println!("cargo::warning={msg}"); + + let p = PathBuf::from(env::var("OUT_DIR").unwrap()); + let kaleido_zip_file = p.join("kaleido.zip"); + + let mut cmd = Command::new("cargo") + .args(["install", "ruget"]) + .spawn() + .unwrap(); + cmd.wait()?; + + let mut cmd = Command::new("ruget") + .args([ + KALEIDO_URL, + "-o", + kaleido_zip_file.as_path().to_str().unwrap(), + ]) + .spawn() + .unwrap(); + cmd.wait()?; + + extract_zip(&dst, &kaleido_zip_file)?; + } else { + let msg = "'download' feature disabled. Please install Kaleido manually and make the environment variable 'KALEIDO_PATH' point to it.".to_string(); + println!("cargo::warning={msg}"); } - - let p = PathBuf::from(env::var("OUT_DIR").unwrap()); - let kaleido_zip_file = p.join("kaleido.zip"); - - let mut cmd = Command::new("cargo") - .args(["install", "ruget"]) - .spawn() - .unwrap(); - cmd.wait()?; - - let mut cmd = Command::new("ruget") - .args([ - KALEIDO_URL, - "-o", - kaleido_zip_file.as_path().to_str().unwrap(), - ]) - .spawn() - .unwrap(); - cmd.wait()?; - - extract_zip(&dst, &kaleido_zip_file)?; - println!("cargo:rerun-if-changed=src/lib.rs"); Ok(()) } diff --git a/plotly_kaleido/src/lib.rs b/plotly_kaleido/src/lib.rs index 09e0759a..f18dfb0d 100644 --- a/plotly_kaleido/src/lib.rs +++ b/plotly_kaleido/src/lib.rs @@ -17,7 +17,6 @@ use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use base64::{engine::general_purpose, Engine as _}; -use directories::ProjectDirs; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -77,49 +76,59 @@ pub struct Kaleido { } impl Kaleido { + const KALEIDO_PATH_ENV: &str = "KALEIDO_PATH"; + pub fn new() -> Kaleido { - let path = match Kaleido::binary_path() { - Ok(path) => path, - Err(msg) => panic!("{}", msg), + use std::env; + + let path = match env::var(Self::KALEIDO_PATH_ENV) { + Ok(runtime_env) => runtime_env, + Err(runtime_env_err) => match option_env!("KALEIDO_COMPILE_TIME_DLD_PATH") { + Some(compile_time_path) => compile_time_path.to_string(), + None => { + println!("{}: {}", Self::KALEIDO_PATH_ENV, runtime_env_err); + println!("Use `kaleido_download` feature to automatically download, install and use Kaleido when targeting applications that run on the host machine."); + println!("Use `{}` environment variable when targeting applications intended to run on different machines. Manually install Kaleido on the target machine and point {} to the installation location.", Self::KALEIDO_PATH_ENV, Self::KALEIDO_PATH_ENV + ); + std::process::exit(1); + } + }, }; - Kaleido { cmd_path: path } - } + let path = match Kaleido::binary_path(&path) { + Ok(kaleido_path) => kaleido_path, + Err(msg) => panic!("Failed tu use Kaleido binary at {} due to {}", path, msg), + }; - fn root_dir() -> Result { - let project_dirs = ProjectDirs::from("org", "plotly", "kaleido") - .expect("Could not create plotly_kaleido config directory."); - Ok(project_dirs.config_dir().into()) + Kaleido { cmd_path: path } } - #[cfg(target_os = "linux")] - fn binary_path() -> Result { - let mut p = Kaleido::root_dir()?; - p = p.join("kaleido").canonicalize().unwrap(); + fn binary_path(dld_path: &str) -> Result { + let mut p = PathBuf::from(dld_path); + p = Self::os_binary_path(p); if !p.exists() { return Err("could not find kaleido executable in path"); } Ok(p) } - #[cfg(target_os = "macos")] - fn binary_path() -> Result { - let mut p = Kaleido::root_dir()?; - p = p.join("kaleido").canonicalize().unwrap(); - if !p.exists() { - return Err("could not find kaleido executable in path"); + #[cfg(any(target_os = "linux", target_os = "macos"))] + fn os_binary_path(path: PathBuf) -> PathBuf { + match path.join("kaleido").canonicalize() { + Ok(v) => v, + Err(e) => { + println!( + "Failed to find Kaleido binary at '{}': {e}", + path.to_string_lossy() + ); + panic!("{e}"); + } } - Ok(p) } #[cfg(target_os = "windows")] - fn binary_path() -> Result { - let mut p = Kaleido::root_dir()?; - p = p.join("kaleido.cmd"); - if !p.exists() { - return Err("could not find kaleido executable in path"); - } - Ok(p) + fn os_binary_path(path: PathBuf) -> PathBuf { + path.join("kaleido.cmd") } /// Generate a static image from a Plotly graph and save it to a file @@ -188,12 +197,23 @@ impl Kaleido { "--disable-dev-shm-usage", "--disable-software-rasterizer", "--single-process", + "--disable-gpu", + "--no-sandbox", ]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() - .expect("failed to spawn Kaleido binary"); + .unwrap_or_else(|_| { + panic!( + "{}", + format!( + "failed to spawn Kaleido binary at {}", + self.cmd_path.to_string_lossy() + ) + .to_string() + ) + }); { let plot_data = PlotData::new(plotly_data, format, width, height, scale).to_json(); @@ -217,6 +237,16 @@ impl Kaleido { } } + // Don't eat up Kaleido/Chromium errors but show them in the terminal + println!("Kaleido failed to generate static image for format: {format}."); + println!("Kaleido stderr output:"); + let stderr = process.stderr.take().unwrap(); + let stderr_lines = BufReader::new(stderr).lines(); + for line in stderr_lines { + let line = line.unwrap(); + eprintln!("{}", line); + } + Ok(String::default()) } } @@ -279,7 +309,7 @@ mod tests { } // This seems to fail unpredictably on MacOs. - #[cfg(target_os = "linux")] + #[cfg(not(target_os = "macos"))] #[test] fn test_save_png() { let test_plot = create_test_plot(); @@ -291,7 +321,7 @@ mod tests { } // This seems to fail unpredictably on MacOs. - #[cfg(target_os = "linux")] + #[cfg(not(target_os = "macos"))] #[test] fn test_save_jpeg() { let test_plot = create_test_plot(); @@ -303,7 +333,7 @@ mod tests { } // This seems to fail unpredictably on MacOs. - #[cfg(target_os = "linux")] + #[cfg(not(target_os = "macos"))] #[test] fn test_save_webp() { let test_plot = create_test_plot(); @@ -315,7 +345,7 @@ mod tests { } // This seems to fail unpredictably on MacOs. - #[cfg(target_os = "linux")] + #[cfg(not(target_os = "macos"))] #[test] fn test_save_svg() { let test_plot = create_test_plot(); @@ -327,7 +357,7 @@ mod tests { } // This seems to fail unpredictably on MacOs. - #[cfg(target_os = "linux")] + #[cfg(not(target_os = "macos"))] #[test] fn test_save_pdf() { let test_plot = create_test_plot(); @@ -338,7 +368,7 @@ mod tests { assert!(std::fs::remove_file(dst.as_path()).is_ok()); } - // This doesn't work for some reason + // This generates empty eps files for some reason #[test] #[ignore] fn test_save_eps() {