use std::sync::Arc;
use rmcp::ErrorData;
use schemars::JsonSchema;
use serde::Deserialize;
use tokio::sync::Mutex;
use crate::errors::{McpServerError, map_error};
use crate::state::SessionState;
use crate::tools::common::{BlobOutput, blob_output, current_tab};
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PdfInput {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub landscape: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub print_background: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scale: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub paper_width: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub paper_height: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub margin_top: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub margin_bottom: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub margin_left: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub margin_right: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_ranges: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prefer_css_page_size: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub save_path: Option<String>,
}
pub async fn pdf(
state: Arc<Mutex<SessionState>>,
input: PdfInput,
) -> Result<BlobOutput, ErrorData> {
let s = state.lock().await;
let tab = current_tab(&s).await?;
let mut builder = tab.pdf_builder();
if let Some(v) = input.landscape {
builder = builder.landscape(v);
}
if let Some(v) = input.print_background {
builder = builder.print_background(v);
}
if let Some(v) = input.scale {
builder = builder.scale(v);
}
if let Some(v) = input.paper_width {
builder = builder.paper_width(v);
}
if let Some(v) = input.paper_height {
builder = builder.paper_height(v);
}
if let Some(v) = input.margin_top {
builder = builder.margin_top(v);
}
if let Some(v) = input.margin_bottom {
builder = builder.margin_bottom(v);
}
if let Some(v) = input.margin_left {
builder = builder.margin_left(v);
}
if let Some(v) = input.margin_right {
builder = builder.margin_right(v);
}
if let Some(v) = input.page_ranges {
builder = builder.page_ranges(v);
}
if let Some(v) = input.prefer_css_page_size {
builder = builder.prefer_css_page_size(v);
}
let bytes = builder
.bytes()
.await
.map_err(|e| map_error(McpServerError::from(e)))?;
blob_output(&bytes, input.save_path)
}
#[derive(Debug, Default, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SaveMhtmlInput {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub save_path: Option<String>,
}
pub async fn save_mhtml(
state: Arc<Mutex<SessionState>>,
input: SaveMhtmlInput,
) -> Result<BlobOutput, ErrorData> {
let s = state.lock().await;
let tab = current_tab(&s).await?;
let mhtml = tab
.snapshot_mhtml()
.await
.map_err(|e| map_error(McpServerError::from(e)))?;
blob_output(mhtml.as_bytes(), input.save_path)
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
#[tokio::test]
async fn pdf_with_no_browser_errors() {
let state = Arc::new(Mutex::new(SessionState::new()));
let err = pdf(
state,
PdfInput {
landscape: None,
print_background: None,
scale: None,
paper_width: None,
paper_height: None,
margin_top: None,
margin_bottom: None,
margin_left: None,
margin_right: None,
page_ranges: None,
prefer_css_page_size: None,
save_path: None,
},
)
.await
.expect_err("expected BrowserNotOpen");
assert!(err.message.contains("Browser not open"));
}
}