#![allow(deprecated)]
use std::fs::File;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::vec;
#[cfg(any(test, feature = "debug"))]
use std::{println as error, println as warn, println as debug};
use anyhow::{anyhow, Context, Result};
use base64::{engine::general_purpose, Engine as _};
use fantoccini::{wd::Capabilities, Client, ClientBuilder};
#[cfg(not(any(test, feature = "debug")))]
use log::{debug, error, warn};
use serde::Serialize;
use serde_json::map::Map as JsonMap;
use urlencoding::encode;
use webdriver::WebDriver;
use crate::template::{image_export_js_script, pdf_export_js_script};
mod template;
mod webdriver;
#[derive(Debug, Clone, Serialize)]
#[allow(deprecated)]
pub enum ImageFormat {
PNG,
JPEG,
WEBP,
SVG,
PDF,
#[deprecated(
since = "0.13.0",
note = "Use SVG or PDF instead. EPS variant will be removed in version 0.14.0"
)]
EPS,
}
impl std::fmt::Display for ImageFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::PNG => "png",
Self::JPEG => "jpeg",
Self::WEBP => "webp",
Self::SVG => "svg",
Self::PDF => "pdf",
#[allow(deprecated)]
Self::EPS => "eps",
}
)
}
}
#[derive(Serialize)]
struct PlotData<'a> {
format: ImageFormat,
width: usize,
height: usize,
scale: f64,
data: &'a serde_json::Value,
}
pub struct StaticExporterBuilder {
webdriver_port: u32,
webdriver_url: String,
spawn_webdriver: bool,
offline_mode: bool,
pdf_export_timeout: u32,
webdriver_browser_caps: Vec<String>,
}
impl Default for StaticExporterBuilder {
fn default() -> Self {
Self {
webdriver_port: webdriver::WEBDRIVER_PORT,
webdriver_url: webdriver::WEBDRIVER_URL.to_string(),
spawn_webdriver: true,
offline_mode: false,
pdf_export_timeout: 150,
webdriver_browser_caps: {
#[cfg(feature = "chromedriver")]
{
crate::webdriver::chrome_default_caps()
.into_iter()
.map(|s| s.to_string())
.collect()
}
#[cfg(feature = "geckodriver")]
{
crate::webdriver::firefox_default_caps()
.into_iter()
.map(|s| s.to_string())
.collect()
}
#[cfg(not(any(feature = "chromedriver", feature = "geckodriver")))]
{
Vec::new()
}
},
}
}
}
impl StaticExporterBuilder {
pub fn webdriver_port(mut self, port: u32) -> Self {
self.webdriver_port = port;
self
}
pub fn webdriver_url(mut self, url: &str) -> Self {
self.webdriver_url = url.to_string();
self
}
pub fn spawn_webdriver(mut self, yes: bool) -> Self {
self.spawn_webdriver = yes;
self
}
pub fn offline_mode(mut self, yes: bool) -> Self {
self.offline_mode = yes;
self
}
pub fn pdf_export_timeout(mut self, timeout_ms: u32) -> Self {
self.pdf_export_timeout = timeout_ms;
self
}
pub fn webdriver_browser_caps(mut self, caps: Vec<String>) -> Self {
self.webdriver_browser_caps = caps;
self
}
pub fn build(&self) -> Result<StaticExporter> {
let runtime = std::sync::Arc::new(
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Failed to create Tokio runtime"),
);
let inner = Self::build_async(self)?;
Ok(StaticExporter { runtime, inner })
}
fn create_webdriver(&self) -> Result<WebDriver> {
let port = self.webdriver_port;
let in_async = tokio::runtime::Handle::try_current().is_ok();
let run_create_fn = |spawn: bool| -> Result<WebDriver> {
let work = move || {
if spawn {
WebDriver::connect_or_spawn(port)
} else {
WebDriver::new(port)
}
};
if in_async {
std::thread::spawn(work)
.join()
.map_err(|_| anyhow!("failed to join webdriver thread"))?
} else {
work()
}
};
run_create_fn(self.spawn_webdriver)
}
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,
})
}
}
pub struct StaticExporter {
runtime: std::sync::Arc<tokio::runtime::Runtime>,
inner: AsyncStaticExporter,
}
impl StaticExporter {
pub fn write_fig(
&mut self,
dst: &Path,
plot: &serde_json::Value,
format: ImageFormat,
width: usize,
height: usize,
scale: f64,
) -> Result<(), Box<dyn std::error::Error>> {
if tokio::runtime::Handle::try_current().is_ok() {
return Err(anyhow!(
"StaticExporter sync methods cannot be used inside an async context. \
Use StaticExporterBuilder::build_async() and the associated AsyncStaticExporter::write_fig(...)."
)
.into());
}
let rt = self.runtime.clone();
rt.block_on(
self.inner
.write_fig(dst, plot, format, width, height, scale),
)
}
pub fn write_to_string(
&mut self,
plot: &serde_json::Value,
format: ImageFormat,
width: usize,
height: usize,
scale: f64,
) -> Result<String, Box<dyn std::error::Error>> {
if tokio::runtime::Handle::try_current().is_ok() {
return Err(anyhow!(
"StaticExporter sync methods cannot be used inside an async context. \
Use StaticExporterBuilder::build_async() and the associated AsyncStaticExporter::write_to_string(...)."
)
.into());
}
let rt = self.runtime.clone();
rt.block_on(
self.inner
.write_to_string(plot, format, width, height, scale),
)
}
pub fn get_webdriver_diagnostics(&self) -> String {
self.inner.get_webdriver_diagnostics()
}
pub fn close(&mut self) {
let runtime = self.runtime.clone();
runtime.block_on(self.inner.close());
}
}
pub struct AsyncStaticExporter {
webdriver_port: u32,
webdriver_url: String,
webdriver: WebDriver,
offline_mode: bool,
pdf_export_timeout: u32,
webdriver_browser_caps: Vec<String>,
webdriver_client: Option<Client>,
}
impl AsyncStaticExporter {
pub async fn write_fig(
&mut self,
dst: &Path,
plot: &serde_json::Value,
format: ImageFormat,
width: usize,
height: usize,
scale: f64,
) -> Result<(), Box<dyn std::error::Error>> {
let mut dst = PathBuf::from(dst);
dst.set_extension(format.to_string());
let plot_data = PlotData {
format: format.clone(),
width,
height,
scale,
data: plot,
};
let image_data = self.static_export(&plot_data).await?;
let data = match format {
ImageFormat::SVG => image_data.as_bytes().to_vec(),
_ => general_purpose::STANDARD.decode(image_data)?,
};
let mut file = File::create(dst.as_path())?;
file.write_all(&data)?;
file.flush()?;
Ok(())
}
pub async fn write_to_string(
&mut self,
plot: &serde_json::Value,
format: ImageFormat,
width: usize,
height: usize,
scale: f64,
) -> Result<String, Box<dyn std::error::Error>> {
let plot_data = PlotData {
format,
width,
height,
scale,
data: plot,
};
let image_data = self.static_export(&plot_data).await?;
Ok(image_data)
}
pub async fn close(&mut self) {
if let Some(client) = self.webdriver_client.take() {
if let Err(e) = client.close().await {
error!("Failed to close WebDriver client: {e}");
}
}
if let Err(e) = self.webdriver.stop() {
error!("Failed to stop WebDriver: {e}");
}
}
pub fn get_webdriver_diagnostics(&self) -> String {
self.webdriver.get_diagnostics()
}
async fn static_export(&mut self, plot: &PlotData<'_>) -> Result<String> {
let html_content = template::get_html_body(self.offline_mode);
self.extract(&html_content, plot)
.await
.with_context(|| "Failed to extract static image from browser session")
}
async fn extract(&mut self, html_content: &str, plot: &PlotData<'_>) -> Result<String> {
let caps = self.build_webdriver_caps()?;
debug!(
"Use WebDriver and headless browser to export static plot (offline_mode={}, port={})",
self.offline_mode, self.webdriver_port
);
let webdriver_url = format!("{}:{}", self.webdriver_url, self.webdriver_port);
debug!("Connecting to WebDriver at {webdriver_url}");
let client = if let Some(ref client) = self.webdriver_client {
debug!("Reusing existing WebDriver session");
client.clone()
} else {
debug!("Creating new WebDriver session");
let new_client = ClientBuilder::native()
.capabilities(caps)
.connect(&webdriver_url)
.await
.with_context(|| "WebDriver session error")?;
self.webdriver_client = Some(new_client.clone());
new_client
};
let url = if self.offline_mode {
let temp_file = template::to_file(html_content)
.with_context(|| "Failed to create temporary HTML file")?;
format!("file://{}", temp_file.to_string_lossy())
} else {
format!("data:text/html,{}", encode(html_content))
};
client.goto(&url).await?;
#[cfg(target_os = "windows")]
Self::wait_for_document_ready(&client, std::time::Duration::from_secs(10)).await?;
#[cfg(target_os = "windows")]
Self::wait_for_plotly_container(&client, std::time::Duration::from_secs(10)).await?;
if !self.offline_mode {
#[cfg(target_os = "windows")]
Self::wait_for_plotly_loaded(&client, std::time::Duration::from_secs(15)).await?;
}
let (js_script, args) = match plot.format {
ImageFormat::PDF => {
let args = vec![
plot.data.clone(),
ImageFormat::SVG.to_string().into(),
plot.width.into(),
plot.height.into(),
plot.scale.into(),
];
(pdf_export_js_script(self.pdf_export_timeout), args)
}
_ => {
let args = vec![
plot.data.clone(),
plot.format.to_string().into(),
plot.width.into(),
plot.height.into(),
plot.scale.into(),
];
(image_export_js_script(), args)
}
};
let data = client.execute_async(&js_script, args).await?;
let result = data.as_str().ok_or(anyhow!(
"Failed to execute Plotly.toImage in browser session"
))?;
if let Some(err) = result.strip_prefix("ERROR:") {
return Err(anyhow!("JavaScript error during export: {err}"));
}
match plot.format {
ImageFormat::SVG => common::extract_plain(result, &plot.format),
ImageFormat::PNG | ImageFormat::JPEG | ImageFormat::WEBP | ImageFormat::PDF => {
common::extract_encoded(result, &plot.format)
}
#[allow(deprecated)]
ImageFormat::EPS => {
error!("EPS format is deprecated. Use SVG or PDF instead.");
common::extract_encoded(result, &plot.format)
}
}
}
fn build_webdriver_caps(&self) -> Result<Capabilities> {
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));
#[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}");
}
#[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}");
}
#[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:?}");
Ok(caps)
}
#[cfg(target_os = "windows")]
async fn wait_for_document_ready(client: &Client, timeout: std::time::Duration) -> Result<()> {
let start = std::time::Instant::now();
loop {
let state = client
.execute("return document.readyState;", vec![])
.await
.unwrap_or(serde_json::Value::Null);
if state.as_str().map(|s| s == "complete").unwrap_or(false) {
return Ok(());
}
if start.elapsed() > timeout {
return Err(anyhow!(
"Timeout waiting for document.readyState === 'complete'"
));
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
}
#[cfg(target_os = "windows")]
async fn wait_for_plotly_container(
client: &Client,
timeout: std::time::Duration,
) -> Result<()> {
let start = std::time::Instant::now();
loop {
let has_el = client
.execute(
"return !!document.getElementById('plotly-html-element');",
vec![],
)
.await
.unwrap_or(serde_json::Value::Bool(false));
if has_el.as_bool().unwrap_or(false) {
return Ok(());
}
}
if start.elapsed() > timeout {
return Err(anyhow!(
"Timeout waiting for #plotly-html-element to appear in DOM"
));
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
#[cfg(target_os = "windows")]
async fn wait_for_plotly_loaded(client: &Client, timeout: std::time::Duration) -> Result<()> {
let start = std::time::Instant::now();
loop {
let has_plotly = client
.execute("return !!window.Plotly;", vec![])
.await
.unwrap_or(serde_json::Value::Bool(false));
if has_plotly.as_bool().unwrap_or(false) {
return Ok(());
}
if start.elapsed() > timeout {
return Err(anyhow!("Timeout waiting for Plotly library to load"));
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
}
}
mod common {
use super::*;
pub(crate) fn extract_plain(payload: &str, format: &ImageFormat) -> Result<String> {
match payload.split_once(",") {
Some((type_info, data)) => {
extract_type_info(type_info, format);
let decoded = urlencoding::decode(data)?;
Ok(decoded.to_string())
}
None => Err(anyhow!("'src' attribute has invalid {format} data")),
}
}
pub(crate) fn extract_encoded(payload: &str, format: &ImageFormat) -> Result<String> {
match payload.split_once(";") {
Some((type_info, encoded_data)) => {
extract_type_info(type_info, format);
extract_encoded_data(encoded_data)
.ok_or(anyhow!("No valid image data found in 'src' attribute"))
}
None => Err(anyhow!("'src' attribute has invalid base64 data")),
}
}
pub(crate) fn extract_type_info(type_info: &str, format: &ImageFormat) {
let val = type_info.split_once("/").map(|d| d.1.to_string());
match val {
Some(ext) => {
if !ext.contains(&format.to_string()) {
error!("Requested ImageFormat '{format}', got '{ext}'");
}
}
None => warn!("Failed to extract static Image Format from 'src' attribute"),
}
}
pub(crate) fn extract_encoded_data(data: &str) -> Option<String> {
data.split_once(",").map(|d| d.1.to_string())
}
#[cfg(feature = "geckodriver")]
pub(crate) fn get_firefox_ci_preferences() -> serde_json::Map<String, serde_json::Value> {
let mut prefs = serde_json::Map::new();
prefs.insert(
"layers.acceleration.disabled".to_string(),
serde_json::json!(true),
);
prefs.insert("gfx.webrender.all".to_string(), serde_json::json!(false));
prefs.insert(
"gfx.webrender.software".to_string(),
serde_json::json!(true),
);
prefs.insert("webgl.disabled".to_string(), serde_json::json!(false));
prefs.insert("webgl.force-enabled".to_string(), serde_json::json!(true));
prefs.insert("webgl.enable-webgl2".to_string(), serde_json::json!(true));
prefs.insert(
"webgl.software-rendering".to_string(),
serde_json::json!(true),
);
prefs.insert(
"webgl.software-rendering.force".to_string(),
serde_json::json!(true),
);
prefs.insert(
"gfx.canvas.azure.accelerated".to_string(),
serde_json::json!(false),
);
prefs.insert(
"gfx.canvas.azure.accelerated-layers".to_string(),
serde_json::json!(false),
);
prefs.insert(
"gfx.content.azure.backends".to_string(),
serde_json::json!("cairo"),
);
prefs.insert("gfx.2d.force-enabled".to_string(), serde_json::json!(true));
prefs.insert("gfx.2d.force-software".to_string(), serde_json::json!(true));
prefs
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use std::sync::atomic::{AtomicU32, Ordering};
use super::*;
fn init() {
let _ = env_logger::try_init();
}
#[cfg(not(feature = "debug"))]
fn get_unique_port() -> u32 {
static PORT_COUNTER: AtomicU32 = AtomicU32::new(4844);
PORT_COUNTER.fetch_add(1, Ordering::SeqCst)
}
#[cfg(feature = "debug")]
fn get_unique_port() -> u32 {
static PORT_COUNTER: AtomicU32 = AtomicU32::new(4844);
loop {
let p = PORT_COUNTER.fetch_add(1, Ordering::SeqCst);
if !webdriver::WebDriver::is_webdriver_running(p) {
return p;
}
}
}
fn create_test_plot() -> serde_json::Value {
serde_json::to_value(serde_json::json!(
{
"data": [
{
"name": "Surface",
"type": "surface",
"x": [
1.0,
2.0,
3.0
],
"y": [
4.0,
5.0,
6.0
],
"z": [
[
1.0,
2.0,
3.0
],
[
4.0,
5.0,
6.0
],
[
7.0,
8.0,
9.0
]
]
}
],
"layout": {
"autosize": false,
"width": 1200,
"height": 900,
"scene": {
"domain": {
"x": [0.15, 0.95],
"y": [0.15, 0.95]
},
"aspectmode": "data",
"aspectratio": {
"x": 1,
"y": 1,
"z": 1
},
"camera": {
"eye": {"x": 1.5, "y": 1.5, "z": 1.5}
}
},
"config": {
"responsive": false
},
},
}))
.unwrap()
}
#[test]
fn save_png() {
init();
let test_plot = create_test_plot();
let mut exporter = StaticExporterBuilder::default()
.spawn_webdriver(true)
.webdriver_port(get_unique_port())
.build()
.unwrap();
let dst = PathBuf::from("static_example.png");
exporter
.write_fig(dst.as_path(), &test_plot, ImageFormat::PNG, 1200, 900, 4.5)
.unwrap();
assert!(dst.exists());
let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
let file_size = metadata.len();
assert!(file_size > 0,);
#[cfg(not(feature = "debug"))]
assert!(std::fs::remove_file(dst.as_path()).is_ok());
exporter.close();
}
#[test]
fn save_jpeg() {
init();
let test_plot = create_test_plot();
let mut exporter = StaticExporterBuilder::default()
.spawn_webdriver(true)
.webdriver_port(get_unique_port())
.build()
.unwrap();
let dst = PathBuf::from("static_example.jpeg");
exporter
.write_fig(dst.as_path(), &test_plot, ImageFormat::JPEG, 1200, 900, 4.5)
.unwrap();
assert!(dst.exists());
let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
let file_size = metadata.len();
assert!(file_size > 0,);
#[cfg(not(feature = "debug"))]
assert!(std::fs::remove_file(dst.as_path()).is_ok());
exporter.close();
}
#[test]
fn save_svg() {
init();
let test_plot = create_test_plot();
let mut exporter = StaticExporterBuilder::default()
.spawn_webdriver(true)
.webdriver_port(get_unique_port())
.build()
.unwrap();
let dst = PathBuf::from("static_example.svg");
exporter
.write_fig(dst.as_path(), &test_plot, ImageFormat::SVG, 1200, 900, 4.5)
.unwrap();
assert!(dst.exists());
let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
let file_size = metadata.len();
assert!(file_size > 0,);
#[cfg(not(feature = "debug"))]
assert!(std::fs::remove_file(dst.as_path()).is_ok());
exporter.close();
}
#[test]
fn save_webp() {
init();
let test_plot = create_test_plot();
let mut exporter = StaticExporterBuilder::default()
.spawn_webdriver(true)
.webdriver_port(get_unique_port())
.build()
.unwrap();
let dst = PathBuf::from("static_example.webp");
exporter
.write_fig(dst.as_path(), &test_plot, ImageFormat::WEBP, 1200, 900, 4.5)
.unwrap();
assert!(dst.exists());
let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
let file_size = metadata.len();
assert!(file_size > 0,);
#[cfg(not(feature = "debug"))]
assert!(std::fs::remove_file(dst.as_path()).is_ok());
exporter.close();
}
#[tokio::test]
async fn save_png_async() {
init();
let test_plot = create_test_plot();
let mut exporter = StaticExporterBuilder::default()
.spawn_webdriver(true)
.webdriver_port(5444)
.build_async()
.unwrap();
let dst = PathBuf::from("static_example_async.png");
exporter
.write_fig(dst.as_path(), &test_plot, ImageFormat::PNG, 1200, 900, 4.5)
.await
.unwrap();
assert!(dst.exists());
let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
let file_size = metadata.len();
assert!(file_size > 0,);
#[cfg(not(feature = "debug"))]
assert!(std::fs::remove_file(dst.as_path()).is_ok());
exporter.close().await;
}
#[test]
fn save_pdf() {
init();
let test_plot = create_test_plot();
#[cfg(feature = "debug")]
let mut exporter = StaticExporterBuilder::default()
.spawn_webdriver(true)
.webdriver_port(get_unique_port())
.pdf_export_timeout(750)
.build()
.unwrap();
#[cfg(not(feature = "debug"))]
let mut exporter = StaticExporterBuilder::default()
.spawn_webdriver(true)
.webdriver_port(get_unique_port())
.build()
.unwrap();
let dst = PathBuf::from("static_example.pdf");
exporter
.write_fig(dst.as_path(), &test_plot, ImageFormat::PDF, 1200, 900, 4.5)
.unwrap();
assert!(dst.exists());
let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
let file_size = metadata.len();
assert!(file_size > 600000,);
#[cfg(not(feature = "debug"))]
assert!(std::fs::remove_file(dst.as_path()).is_ok());
exporter.close();
}
#[test]
fn save_jpeg_sequentially() {
init();
let test_plot = create_test_plot();
let mut exporter = StaticExporterBuilder::default()
.spawn_webdriver(true)
.webdriver_port(get_unique_port())
.build()
.unwrap();
let dst = PathBuf::from("static_example.jpeg");
exporter
.write_fig(dst.as_path(), &test_plot, ImageFormat::JPEG, 1200, 900, 4.5)
.unwrap();
assert!(dst.exists());
let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
let file_size = metadata.len();
assert!(file_size > 0,);
#[cfg(not(feature = "debug"))]
assert!(std::fs::remove_file(dst.as_path()).is_ok());
let dst = PathBuf::from("example2.jpeg");
exporter
.write_fig(dst.as_path(), &test_plot, ImageFormat::JPEG, 1200, 900, 4.5)
.unwrap();
assert!(dst.exists());
let metadata = std::fs::metadata(&dst).expect("Could not retrieve file metadata");
let file_size = metadata.len();
assert!(file_size > 0,);
#[cfg(not(feature = "debug"))]
assert!(std::fs::remove_file(dst.as_path()).is_ok());
exporter.close();
}
#[test]
#[cfg(feature = "chromedriver")]
fn test_webdriver_process_reuse() {
init();
let test_plot = create_test_plot();
let test_port = get_unique_port();
let mut exporter1 = StaticExporterBuilder::default()
.spawn_webdriver(true)
.webdriver_port(test_port)
.build()
.unwrap();
let dst1 = PathBuf::from("process_reuse_1.png");
exporter1
.write_fig(dst1.as_path(), &test_plot, ImageFormat::PNG, 800, 600, 1.0)
.unwrap();
assert!(dst1.exists());
#[cfg(not(feature = "debug"))]
assert!(std::fs::remove_file(dst1.as_path()).is_ok());
exporter1.close();
let mut exporter2 = StaticExporterBuilder::default()
.spawn_webdriver(true)
.webdriver_port(test_port)
.build()
.unwrap();
let dst2 = PathBuf::from("process_reuse_2.png");
exporter2
.write_fig(dst2.as_path(), &test_plot, ImageFormat::PNG, 800, 600, 1.0)
.unwrap();
assert!(dst2.exists());
#[cfg(not(feature = "debug"))]
assert!(std::fs::remove_file(dst2.as_path()).is_ok());
exporter2.close();
let mut exporter3 = StaticExporterBuilder::default()
.spawn_webdriver(true)
.webdriver_port(test_port)
.build()
.unwrap();
let dst3 = PathBuf::from("process_reuse_3.png");
exporter3
.write_fig(dst3.as_path(), &test_plot, ImageFormat::PNG, 800, 600, 1.0)
.unwrap();
assert!(dst3.exists());
#[cfg(not(feature = "debug"))]
assert!(std::fs::remove_file(dst3.as_path()).is_ok());
exporter3.close();
}
}
#[cfg(feature = "chromedriver")]
mod chrome {
pub fn get_browser_name() -> &'static str {
"chrome"
}
pub fn get_options_key() -> &'static str {
"goog:chromeOptions"
}
}
#[cfg(feature = "geckodriver")]
mod firefox {
pub fn get_browser_name() -> &'static str {
"firefox"
}
pub fn get_options_key() -> &'static str {
"moz:firefoxOptions"
}
}
#[cfg(feature = "chromedriver")]
use chrome::{get_browser_name, get_options_key};
#[cfg(feature = "geckodriver")]
use firefox::{get_browser_name, get_options_key};