use std::sync::Arc;
use anyhow::{Result, bail};
use crate::adapters::chrome::{Open, PrintToPdfParams, Profile, Session};
use crate::drive::http;
use devpage_core::document::{Document, Kind, Paper, Pdf};
pub async fn print(document: Arc<Document>) -> Result<Vec<u8>> {
let params = match &document.kind {
Kind::Doc { pdf, .. } => doc_params(pdf),
Kind::Slides { .. } => PrintToPdfParams {
print_background: Some(true),
prefer_css_page_size: Some(true),
paper_width: Some(20.0),
paper_height: Some(11.25),
margin_top: Some(0.0),
margin_bottom: Some(0.0),
margin_left: Some(0.0),
margin_right: Some(0.0),
..Default::default()
},
Kind::Site { .. } => bail!("pdf prints a doc or slides, not a site"),
Kind::Design => bail!("pdf prints a doc or slides, not a design"),
};
let path = format!("{}?print", document.entry());
let router = http::router(http::App::printing(document));
let session = Session::start(Profile::Shot, router).await?;
let printed = async {
let tab = session
.open(
&path,
Open {
shot: true,
paper: true,
..Open::default()
},
)
.await?;
let bytes = tab.print(params).await;
tab.close().await;
bytes
}
.await;
session.close().await;
let bytes = printed?;
if bytes.is_empty() {
bail!("Chrome printed nothing");
}
Ok(bytes)
}
pub(super) fn doc_params(pdf: &Pdf) -> PrintToPdfParams {
let template = |text: &str| {
let text = text
.replace('&', "&")
.replace('<', "<")
.replace("{page}", "<span class=\"pageNumber\"></span>")
.replace("{pages}", "<span class=\"totalPages\"></span>");
format!(
"<div style=\"font: 9px -apple-system, Helvetica, sans-serif; color: #6e6e73; width: 100%; text-align: center;\">{text}</div>"
)
};
let (width, height) = match pdf.paper {
Paper::A4 => (8.27, 11.69),
Paper::Letter => (8.5, 11.0),
};
PrintToPdfParams {
display_header_footer: Some(true),
header_template: Some(template(pdf.header.as_deref().unwrap_or(""))),
footer_template: Some(template(&pdf.footer)),
print_background: Some(true),
prefer_css_page_size: Some(true),
paper_width: Some(width),
paper_height: Some(height),
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use devpage_core::folder::ServedFolder;
#[test]
fn a_docs_print_call_carries_its_paper_header_and_footer() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let document = devpage_core::document::Document::load(
&root.join("tests/fixtures/sync-design"),
&ServedFolder::new(root).unwrap(),
)
.unwrap();
let devpage_core::document::Kind::Doc { pdf, .. } = &document.kind else {
panic!("the fixture is a doc")
};
let params = doc_params(pdf);
assert_eq!(
(params.paper_width, params.paper_height),
(Some(8.27), Some(11.69))
);
assert!(
params
.header_template
.unwrap()
.contains("Offline sync — draft")
);
let footer = params.footer_template.unwrap();
assert!(
footer.contains(
"Page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span>"
),
"{footer}"
);
}
#[tokio::test]
async fn a_site_is_refused() {
let dir = std::env::temp_dir().join(format!("devpage-pdf-site-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("README.md"), "# Home\n").unwrap();
std::fs::write(
dir.join("devpage.yml"),
"kind: site\ntitle: Home\nroot: README.md\n",
)
.unwrap();
let site = Arc::new(Document::load(&dir, &ServedFolder::new(&dir).unwrap()).unwrap());
let error = print(site).await.unwrap_err();
assert_eq!(error.to_string(), "pdf prints a doc or slides, not a site");
}
}