use async_trait::async_trait;
use chromiumoxide::Browser;
use chromiumoxide::browser::BrowserConfig;
use chromiumoxide::cdp::browser_protocol::page::PrintToPdfParams;
use std::sync::Arc;
use tokio::time::{timeout, Duration};
use tokio::task::JoinHandle;
use futures::StreamExt;
use tempfile::TempDir;
use crate::{
config::{ChromeConfig, Orientation, PdfConfig},
error::{Error, Result},
renderer::{PdfRenderer, ResolvedInput},
};
pub struct ChromeRenderer {
browser: Arc<Browser>,
_handler_task: JoinHandle<()>,
_profile_dir: TempDir,
}
impl ChromeRenderer {
pub async fn new(config: ChromeConfig) -> Result<Self> {
let profile_dir = TempDir::new()
.map_err(|e| Error::ChromeProcess(format!("Failed to create temp profile: {}", e)))?;
let mut builder = BrowserConfig::builder()
.user_data_dir(profile_dir.path());
if let Some(path) = config.binary_path {
builder = builder.chrome_executable(path);
}
if config.headless {
builder = builder.with_head();
}
if config.no_sandbox {
builder = builder.arg("--no-sandbox");
}
for arg in config.extra_args {
builder = builder.arg(arg);
}
let browser_config = builder.build()
.map_err(|e| Error::ChromeProcess(format!("Failed to build config: {}", e)))?;
let (browser, handler) = timeout(
Duration::from_secs(config.connection_timeout),
Browser::launch(browser_config)
)
.await
.map_err(|_| Error::Timeout(config.connection_timeout))?
.map_err(|e| Error::ChromeProcess(format!("Failed to launch Chrome: {}", e)))?;
let handler_task = tokio::spawn(async move {
let mut handler = handler;
while let Some(event) = handler.next().await {
if let Err(e) = event {
tracing::error!("Chrome handler error: {}", e);
break;
}
}
});
Ok(Self {
browser: Arc::new(browser),
_handler_task: handler_task,
_profile_dir: profile_dir,
})
}
fn build_print_params(config: &PdfConfig) -> PrintToPdfParams {
let (width, height) = config.page_size.dimensions_inches();
let (width, height) = match config.orientation {
Orientation::Portrait => (width, height),
Orientation::Landscape => (height, width),
};
PrintToPdfParams {
landscape: matches!(config.orientation, Orientation::Landscape).into(),
display_header_footer: Some(config.display_header_footer),
print_background: Some(config.print_background),
scale: Some(config.scale),
paper_width: Some(width),
paper_height: Some(height),
margin_top: Some(config.margins.top),
margin_bottom: Some(config.margins.bottom),
margin_left: Some(config.margins.left),
margin_right: Some(config.margins.right),
page_ranges: None,
header_template: config.header_template.clone(),
footer_template: config.footer_template.clone(),
generate_document_outline: Some(false),
generate_tagged_pdf: Some(false),
prefer_css_page_size: Some(config.prefer_css_page_size),
transfer_mode: None,
}
}
}
#[async_trait]
impl PdfRenderer for ChromeRenderer {
async fn render(&self, input: ResolvedInput, config: PdfConfig) -> Result<Vec<u8>> {
let page = self.browser.new_page("about:blank")
.await
.map_err(|e| Error::Protocol(format!("Failed to create page: {}", e)))?;
match input {
ResolvedInput::Html(html) => {
page.set_content(&html)
.await
.map_err(|e| Error::Protocol(format!("Failed to set content: {}", e)))?;
}
ResolvedInput::Url(url) => {
page.goto(&url)
.await
.map_err(|e| Error::Protocol(format!(
"Failed to navigate to URL '{}': {}",
url, e
)))?;
page.wait_for_navigation()
.await
.map_err(|e| Error::Navigation(format!(
"Failed to navigate to URL '{}': {}",
url, e
)))?;
}
}
let print_params = Self::build_print_params(&config);
let pdf_data = timeout(
Duration::from_secs(config.timeout_seconds),
page.pdf(print_params)
)
.await
.map_err(|_| Error::Timeout(config.timeout_seconds))?
.map_err(|e| Error::Protocol(format!("PDF generation failed: {}", e)))?;
let _ = page.close().await;
Ok(pdf_data)
}
fn name(&self) -> &str {
"ChromeRenderer"
}
}
impl Drop for ChromeRenderer {
fn drop(&mut self) {
}
}