browsing 0.1.7

Browser automation: navigate, click, extract, screenshot. Standalone browser control via CDP.
Documentation
//! PDF generation via CDP Page.printToPDF

use crate::browser::views::PdfConfig;
use crate::error::Result;
use base64::{Engine as _, engine::general_purpose};
use std::sync::Arc;

/// Manages PDF generation from browser pages
#[derive(Debug, Clone)]
pub struct PdfManager {
    client: Arc<crate::browser::cdp::CdpClient>,
}

impl PdfManager {
    /// Create a new PDF manager
    pub fn new(client: Arc<crate::browser::cdp::CdpClient>) -> Self {
        Self { client }
    }

    /// Save the current page as a PDF, returning the raw bytes.
    ///
    /// Uses CDP `Page.printToPDF` to generate a PDF from the current page.
    /// Returns the PDF data as a `Vec<u8>` which can be written to disk.
    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)
    }

    /// Convenience: generate a PDF with default settings
    pub async fn print_to_pdf(&self) -> Result<Vec<u8>> {
        self.save_as_pdf(None).await
    }

    /// Save a PDF to a file path
    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"));
    }
}