#![forbid(unsafe_code)]
#![warn(clippy::perf)]
#![warn(clippy::pedantic)]
#![warn(missing_docs)]
#![allow(clippy::module_name_repetitions)]
#![doc = include_str!("../README.md")]
use std::fmt::Debug;
use std::io::ErrorKind;
use std::num::ParseFloatError;
use std::path::Path;
use std::thread::sleep;
use std::time::Duration;
use std::{fs, io};
use anyhow::Result;
use headless_chrome::types::PrintToPdfOptions;
use headless_chrome::{Browser, LaunchOptions};
use humantime::format_duration;
use log::{debug, info};
use thiserror::Error;
mod cli;
pub use cli::*;
#[derive(Error, Debug)]
pub enum Error {
#[error(
"Invalid paper size {0}, expected a value in A4, Letter, A3, Tabloid, A2, A1, A0, A5, A6"
)]
InvalidPaperSize(String),
#[error("Invalid margin definition, expected 1, 2, or 4 value, got {0}")]
InvalidMarginDefinition(String),
#[error("Invalid margin value: {0}")]
InvalidMarginValue(ParseFloatError),
#[error("Oops, an error occurs with headless chrome: {0}")]
HeadlessChromeError(String),
#[error("Oops, an error occurs with IO")]
IoError {
#[from]
source: io::Error,
},
}
impl From<ParseFloatError> for Error {
fn from(source: ParseFloatError) -> Self {
Error::InvalidMarginValue(source)
}
}
impl From<anyhow::Error> for Error {
fn from(source: anyhow::Error) -> Self {
Error::HeadlessChromeError(source.to_string())
}
}
pub fn run(opt: &Options) -> Result<(), Error> {
let input = dunce::canonicalize(opt.input())?;
let output = if let Some(path) = opt.output() {
path.clone()
} else {
let mut path = opt.input().clone();
path.set_extension("pdf");
path
};
html_to_pdf(input, output, opt.into(), opt.into(), opt.wait())?;
Ok(())
}
pub fn html_to_pdf<I, O>(
input: I,
output: O,
pdf_options: PrintToPdfOptions,
launch_options: LaunchOptions,
wait: Option<Duration>,
) -> Result<(), Error>
where
I: AsRef<Path> + Debug,
O: AsRef<Path> + Debug,
{
let os = input
.as_ref()
.as_os_str()
.to_str()
.ok_or_else(|| io::Error::from(ErrorKind::InvalidInput))?;
let input = format!("file://{os}");
info!("Input file: {input}");
let local_pdf = print_to_pdf(&input, pdf_options, launch_options, wait)?;
info!("Output file: {:?}", output.as_ref());
fs::write(output.as_ref(), local_pdf)?;
Ok(())
}
fn print_to_pdf(
file_path: &str,
pdf_options: PrintToPdfOptions,
launch_options: LaunchOptions,
wait: Option<Duration>,
) -> Result<Vec<u8>> {
let browser = Browser::new(launch_options)?;
let tab = browser.new_tab()?;
let tab = tab.navigate_to(file_path)?.wait_until_navigated()?;
if let Some(wait) = wait {
info!("Waiting {} before export to PDF", format_duration(wait));
sleep(wait);
}
debug!("Using PDF options: {:?}", pdf_options);
let bytes = tab.print_to_pdf(Some(pdf_options))?;
Ok(bytes)
}