use crate::prelude::*;
use base64::prelude::*;
use beet_core::prelude::*;
use serde_json::json;
#[derive(Debug, Clone)]
pub struct PdfOptions {
pub background: bool,
pub margin: PdfMargin,
pub orientation: PdfOrientation,
pub page_size: PdfPageSize,
pub scale: f64,
pub shrink_to_fit: bool,
pub page_ranges: Vec<String>,
}
impl Default for PdfOptions {
fn default() -> Self {
Self {
background: true,
margin: default(),
orientation: PdfOrientation::Portrait,
page_size: PdfPageSize::a4(),
scale: 1.0,
page_ranges: Vec::new(),
shrink_to_fit: true,
}
}
}
#[derive(Debug, Clone)]
pub struct PdfMargin {
pub top: f64,
pub bottom: f64,
pub left: f64,
pub right: f64,
}
impl Default for PdfMargin {
fn default() -> Self {
Self {
top: 1.0,
bottom: 1.0,
left: 1.0,
right: 1.0,
}
}
}
impl PdfMargin {
pub fn none() -> Self {
Self {
top: 0.0,
bottom: 0.0,
left: 0.0,
right: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub enum PdfOrientation {
Portrait,
Landscape,
}
#[derive(Debug, Clone)]
pub struct PdfPageSize {
pub width: f64,
pub height: f64,
}
impl PdfPageSize {
pub fn a4() -> Self {
Self {
width: 21.0,
height: 29.7,
}
}
pub fn letter() -> Self {
Self {
width: 21.59,
height: 27.94,
}
}
pub fn legal() -> Self {
Self {
width: 21.59,
height: 35.56,
}
}
pub fn custom(width: f64, height: f64) -> Self { Self { width, height } }
}
impl Page {
pub async fn export_pdf(&self) -> Result<Vec<u8>> {
self.export_pdf_with_options(&PdfOptions::default()).await
}
pub async fn export_pdf_with_options(
&self,
options: &PdfOptions,
) -> Result<Vec<u8>> {
let response = self
.session
.command(
"browsingContext.print",
json!({
"context": self.context_id,
"background": options.background,
"margin": {
"top": options.margin.top,
"bottom": options.margin.bottom,
"left": options.margin.left,
"right": options.margin.right
},
"orientation": match options.orientation {
PdfOrientation::Portrait => "portrait",
PdfOrientation::Landscape => "landscape",
},
"page": {
"width": options.page_size.width,
"height": options.page_size.height
},
"pageRanges": options.page_ranges,
"scale": options.scale,
"shrinkToFit": options.shrink_to_fit
}),
)
.await?;
let data_base64 = response
.pointer("/result/data")
.and_then(|v| v.as_str())
.ok_or_else(|| bevyhow!("missing PDF data in response"))?;
BASE64_STANDARD
.decode(data_base64)
.map_err(|e| bevyhow!("failed to decode base64 PDF data: {}", e))
}
}
#[cfg(test)]
mod test {
use crate::prelude::*;
use beet_core::prelude::*;
#[beet_core::test]
async fn export_pdf_generates_valid_pdf() {
App::default()
.run_io_task_local(async move {
let (proc, page) =
Page::visit("https://example.com").await.unwrap();
let pdf_bytes = page.export_pdf().await.unwrap();
pdf_bytes.len().xpect_greater_than(0);
let header = std::str::from_utf8(&pdf_bytes[..4]).unwrap();
header.xpect_eq("%PDF");
page.kill().await.unwrap();
proc.kill().unwrap();
})
.await;
}
#[beet_core::test]
async fn export_pdf_with_custom_options() {
App::default()
.run_io_task_local(async move {
let (proc, page) =
Page::visit("https://example.com").await.unwrap();
let options = PdfOptions {
background: false,
margin: PdfMargin {
top: 2.0,
bottom: 2.0,
left: 1.5,
right: 1.5,
},
orientation: PdfOrientation::Landscape,
page_size: PdfPageSize::letter(),
scale: 0.8,
shrink_to_fit: false,
page_ranges: vec!["1".to_string()],
};
let pdf_bytes =
page.export_pdf_with_options(&options).await.unwrap();
pdf_bytes.len().xpect_greater_than(0);
let header = std::str::from_utf8(&pdf_bytes[..4]).unwrap();
header.xpect_eq("%PDF");
page.kill().await.unwrap();
proc.kill().unwrap();
})
.await;
}
}