Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ jobs:
components: clippy
targets: wasm32-unknown-unknown
# lint plotly_static for all features
- run: cargo check -p plotly_static
- run: cargo test -p plotly_static build_without_driver_feature_returns_error
- run: cargo clippy -p plotly_static --features geckodriver,webdriver_download -- -D warnings -A deprecated
- run: cargo clippy -p plotly_static --features chromedriver,webdriver_download -- -D warnings -A deprecated
# lint the main library workspace for non-wasm target
Expand Down
1 change: 0 additions & 1 deletion plotly/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,5 @@ image = "0.25"
itertools = ">=0.10, <0.16"
itertools-num = "0.1"
ndarray = "0.17"
plotly_static = { path = "../plotly_static" }
rand_distr = "0.6"
base64 = "0.22"
1 change: 1 addition & 0 deletions plotly_static/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ serde_json = "1.0"

### Feature Flags

- To use static export at runtime, enable exactly one of the driver features below.
- `chromedriver`: Use Chromedriver and Chrome/Chromium browser for rendering and export
- `geckodriver`: Use Geckodriver Firefox browser for rendering for rendering and export
- `webdriver_download`: Auto-download the chosen WebDriver binary
Expand Down
9 changes: 5 additions & 4 deletions plotly_static/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
#![cfg_attr(
not(any(feature = "geckodriver", feature = "chromedriver")),
allow(unused)
)]

use std::env;
use std::fs;
use std::path::{Path, PathBuf};
Expand All @@ -12,10 +17,6 @@ use webdriver_downloader::prelude::*;
#[cfg(all(feature = "geckodriver", feature = "chromedriver"))]
compile_error!("Only one of 'geckodriver' or 'chromedriver' features can be enabled at a time.");

// Enforce that at least one driver feature is enabled
#[cfg(not(any(feature = "geckodriver", feature = "chromedriver")))]
compile_error!("At least one of 'geckodriver' or 'chromedriver' features must be enabled.");

#[cfg(target_os = "windows")]
const DRIVER_EXT: &str = ".exe";
#[cfg(not(target_os = "windows"))]
Expand Down
126 changes: 79 additions & 47 deletions plotly_static/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@
//!
//! ## Features and Dependencies
//!
//! ### Required Features
//! ### Driver Features
//!
//! You must enable one of the following features:
//! For compile time only, no specific feature must be provided. However, to use
//! static export at runtime, one of the below features must be enabled.
//!
//! - `chromedriver`: Use Chrome/Chromium for rendering
//! - `geckodriver`: Use Firefox for rendering
Expand Down Expand Up @@ -294,6 +295,7 @@ use fantoccini::{wd::Capabilities, Client, ClientBuilder};
#[cfg(not(any(test, feature = "debug")))]
use log::{debug, error, warn};
use serde::Serialize;
#[cfg(any(feature = "chromedriver", feature = "geckodriver"))]
use serde_json::map::Map as JsonMap;
use urlencoding::encode;
use webdriver::WebDriver;
Expand All @@ -303,6 +305,9 @@ use crate::template::{image_export_js_script, pdf_export_js_script};
mod template;
mod webdriver;

#[cfg(not(any(feature = "chromedriver", feature = "geckodriver")))]
const DRIVER_FEATURE_REQUIRED: &str = "Static image export at runtime requires enabling either the 'chromedriver' or 'geckodriver' feature.";

/// Supported image formats for static image export.
///
/// This enum defines all the image formats that can be exported from Plotly
Expand Down Expand Up @@ -648,6 +653,7 @@ impl StaticExporterBuilder {
}

/// Create a new WebDriver instance based on the spawn_webdriver flag
#[cfg(any(feature = "chromedriver", feature = "geckodriver"))]
fn create_webdriver(&self) -> Result<WebDriver> {
let port = self.webdriver_port;
let in_async = tokio::runtime::Handle::try_current().is_ok();
Expand Down Expand Up @@ -688,16 +694,22 @@ impl StaticExporterBuilder {
/// .expect("Failed to build AsyncStaticExporter");
/// ```
pub fn build_async(&self) -> Result<AsyncStaticExporter> {
let wd = self.create_webdriver()?;
Ok(AsyncStaticExporter {
webdriver_port: self.webdriver_port,
webdriver_url: self.webdriver_url.clone(),
webdriver: wd,
offline_mode: self.offline_mode,
pdf_export_timeout: self.pdf_export_timeout,
webdriver_browser_caps: self.webdriver_browser_caps.clone(),
webdriver_client: None,
})
#[cfg(not(any(feature = "chromedriver", feature = "geckodriver")))]
return Err(anyhow!(DRIVER_FEATURE_REQUIRED));

#[cfg(any(feature = "chromedriver", feature = "geckodriver"))]
{
let wd = self.create_webdriver()?;
Ok(AsyncStaticExporter {
webdriver_port: self.webdriver_port,
webdriver_url: self.webdriver_url.clone(),
webdriver: wd,
offline_mode: self.offline_mode,
pdf_export_timeout: self.pdf_export_timeout,
webdriver_browser_caps: self.webdriver_browser_caps.clone(),
webdriver_client: None,
})
}
}
}

Expand Down Expand Up @@ -927,6 +939,10 @@ pub struct AsyncStaticExporter {
pdf_export_timeout: u32,

/// Browser command-line flags (e.g., "--headless", "--no-sandbox")
#[cfg_attr(
not(any(feature = "chromedriver", feature = "geckodriver")),
allow(dead_code)
)]
webdriver_browser_caps: Vec<String>,

/// Cached WebDriver client for session reuse
Expand Down Expand Up @@ -1121,46 +1137,53 @@ impl AsyncStaticExporter {
}

fn build_webdriver_caps(&self) -> Result<Capabilities> {
// Define browser capabilities (copied to avoid reordering existing code)
let mut caps = JsonMap::new();
let mut browser_opts = JsonMap::new();
let browser_args = self.webdriver_browser_caps.clone();

browser_opts.insert("args".to_string(), serde_json::json!(browser_args));

// Add Chrome binary capability if BROWSER_PATH is set
#[cfg(feature = "chromedriver")]
if let Ok(chrome_path) = std::env::var("BROWSER_PATH") {
browser_opts.insert("binary".to_string(), serde_json::json!(chrome_path));
debug!("Added Chrome binary capability: {chrome_path}");
}
// Add Firefox binary capability if BROWSER_PATH is set
#[cfg(feature = "geckodriver")]
if let Ok(firefox_path) = std::env::var("BROWSER_PATH") {
browser_opts.insert("binary".to_string(), serde_json::json!(firefox_path));
debug!("Added Firefox binary capability: {firefox_path}");
}

// Add Firefox-specific preferences for CI environments
#[cfg(feature = "geckodriver")]
#[cfg(not(any(feature = "chromedriver", feature = "geckodriver")))]
{
let prefs = common::get_firefox_ci_preferences();
browser_opts.insert("prefs".to_string(), serde_json::json!(prefs));
debug!("Added Firefox preferences for CI compatibility");
Err(anyhow!(DRIVER_FEATURE_REQUIRED))
}
#[cfg(any(feature = "chromedriver", feature = "geckodriver"))]
{
// Define browser capabilities (copied to avoid reordering existing code)
let mut caps = JsonMap::new();
let mut browser_opts = JsonMap::new();
let browser_args = self.webdriver_browser_caps.clone();

browser_opts.insert("args".to_string(), serde_json::json!(browser_args));

// Add Chrome binary capability if BROWSER_PATH is set
#[cfg(feature = "chromedriver")]
if let Ok(chrome_path) = std::env::var("BROWSER_PATH") {
browser_opts.insert("binary".to_string(), serde_json::json!(chrome_path));
debug!("Added Chrome binary capability: {chrome_path}");
}
// Add Firefox binary capability if BROWSER_PATH is set
#[cfg(feature = "geckodriver")]
if let Ok(firefox_path) = std::env::var("BROWSER_PATH") {
browser_opts.insert("binary".to_string(), serde_json::json!(firefox_path));
debug!("Added Firefox binary capability: {firefox_path}");
}

caps.insert(
"browserName".to_string(),
serde_json::json!(get_browser_name()),
);
caps.insert(
get_options_key().to_string(),
serde_json::json!(browser_opts),
);
// Add Firefox-specific preferences for CI environments
#[cfg(feature = "geckodriver")]
{
let prefs = common::get_firefox_ci_preferences();
browser_opts.insert("prefs".to_string(), serde_json::json!(prefs));
debug!("Added Firefox preferences for CI compatibility");
}

caps.insert(
"browserName".to_string(),
serde_json::json!(get_browser_name()),
);
caps.insert(
get_options_key().to_string(),
serde_json::json!(browser_opts),
);

debug!("WebDriver capabilities: {caps:?}");
debug!("WebDriver capabilities: {caps:?}");

Ok(caps)
Ok(caps)
}
}

#[cfg(target_os = "windows")]
Expand Down Expand Up @@ -1334,6 +1357,15 @@ mod tests {
let _ = env_logger::try_init();
}

#[test]
#[cfg(not(any(feature = "chromedriver", feature = "geckodriver")))]
fn build_without_driver_feature_returns_error() {
match StaticExporterBuilder::default().build_async() {
Err(e) => assert_eq!(e.to_string(), DRIVER_FEATURE_REQUIRED),
Ok(_) => panic!("expected build to fail without a driver feature"),
}
}

// Helper to generate unique ports for parallel tests
#[cfg(not(feature = "debug"))]
fn get_unique_port() -> u32 {
Expand Down
Loading
Loading