use crate::browser::views::PdfConfig;
use crate::error::Result;
use base64::{Engine as _, engine::general_purpose};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct PdfManager {
client: Arc<crate::browser::cdp::CdpClient>,
}
impl PdfManager {
pub fn new(client: Arc<crate::browser::cdp::CdpClient>) -> Self {
Self { client }
}
pub async fn save_as_pdf(&self, config: Option<&PdfConfig>) -> Result<Vec<u8>> {
let mut params = serde_json::json!({});
let cfg = config.map_or_else(PdfConfig::default, |c| c.clone());
if let Some(w) = cfg.paper_width {
params["paperWidth"] = serde_json::json!(w);
}
if let Some(h) = cfg.paper_height {
params["paperHeight"] = serde_json::json!(h);
}
if let Some(l) = cfg.landscape {
params["landscape"] = serde_json::json!(l);
}
if let Some(d) = cfg.display_header_footer {
params["displayHeaderFooter"] = serde_json::json!(d);
}
if let Some(p) = cfg.print_background {
params["printBackground"] = serde_json::json!(p);
}
if let Some(s) = cfg.scale {
params["scale"] = serde_json::json!(s);
}
if let Some(ref ranges) = cfg.page_ranges {
params["pageRanges"] = serde_json::json!(ranges);
}
if let Some(ref header) = cfg.header_template {
params["headerTemplate"] = serde_json::json!(header);
}
if let Some(ref footer) = cfg.footer_template {
params["footerTemplate"] = serde_json::json!(footer);
}
if let Some(pcss) = cfg.prefer_css_page_size {
params["preferCSSPageSize"] = serde_json::json!(pcss);
}
if let Some(ref m) = cfg.margins {
if let Some(t) = m.top {
params["marginTop"] = serde_json::json!(t);
}
if let Some(b) = m.bottom {
params["marginBottom"] = serde_json::json!(b);
}
if let Some(l) = m.left {
params["marginLeft"] = serde_json::json!(l);
}
if let Some(r) = m.right {
params["marginRight"] = serde_json::json!(r);
}
}
let result = self
.client
.send_command("Page.printToPDF", params)
.await?;
let data = result
.get("data")
.and_then(|v| v.as_str())
.ok_or_else(|| crate::error::BrowsingError::Browser("No PDF data in response".to_string()))?;
let pdf_bytes = general_purpose::STANDARD
.decode(data)
.map_err(|e| crate::error::BrowsingError::Browser(format!("PDF base64 decode failed: {e}")))?;
Ok(pdf_bytes)
}
pub async fn print_to_pdf(&self) -> Result<Vec<u8>> {
self.save_as_pdf(None).await
}
pub async fn save_to_file<P: AsRef<std::path::Path>>(
&self,
path: P,
config: Option<&PdfConfig>,
) -> Result<()> {
let bytes = self.save_as_pdf(config).await?;
std::fs::write(path, bytes)
.map_err(|e| crate::error::BrowsingError::Browser(format!("Write PDF file: {e}")))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::browser::views::PageMargins;
#[test]
fn test_pdf_config_default() {
let config = PdfConfig::default();
assert_eq!(config.landscape, Some(false));
assert_eq!(config.display_header_footer, Some(false));
assert_eq!(config.print_background, Some(false));
assert_eq!(config.prefer_css_page_size, Some(false));
assert!(config.paper_width.is_none());
}
#[test]
fn test_pdf_config_custom() {
let config = PdfConfig {
paper_width: Some(8.27),
paper_height: Some(11.69),
landscape: Some(true),
print_background: Some(true),
scale: Some(1.5),
margins: Some(PageMargins {
top: Some(0.5),
bottom: Some(0.5),
left: Some(1.0),
right: Some(1.0),
}),
page_ranges: Some("1-3".to_string()),
..Default::default()
};
assert_eq!(config.paper_width, Some(8.27));
assert_eq!(config.landscape, Some(true));
assert!(config.margins.is_some());
}
#[test]
fn test_page_margins_default() {
let margins = PageMargins::default();
assert!(margins.top.is_none());
assert!(margins.bottom.is_none());
assert!(margins.left.is_none());
assert!(margins.right.is_none());
}
#[test]
fn test_page_margins_serialization() {
let margins = PageMargins {
top: Some(0.5),
bottom: Some(0.5),
left: Some(1.0),
right: Some(1.0),
};
let json = serde_json::to_string(&margins).unwrap();
assert!(json.contains("0.5"));
assert!(json.contains("1.0"));
}
}