use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;
use clap::Parser;
use headless_chrome::types::PrintToPdfOptions;
use headless_chrome::LaunchOptions;
use humantime::parse_duration;
use crate::Error;
#[derive(Debug, Parser)]
#[clap(version)]
pub struct Options {
pub input: PathBuf,
#[clap(short, long)]
pub output: Option<PathBuf>,
#[clap(long)]
pub landscape: bool,
#[clap(long)]
pub background: bool,
#[clap(long, value_parser = parse_duration)]
pub wait: Option<Duration>,
#[clap(long)]
pub header: Option<String>,
#[clap(long)]
pub footer: Option<String>,
#[clap(long)]
pub paper: Option<PaperSize>,
#[clap(long)]
pub scale: Option<f64>,
#[clap(long)]
pub range: Option<String>,
#[clap(long)]
pub margin: Option<Margin>,
#[clap(long)]
pub disable_sandbox: bool,
}
impl From<&Options> for PrintToPdfOptions {
fn from(opt: &Options) -> Self {
PrintToPdfOptions {
landscape: Some(opt.landscape),
display_header_footer: Some(opt.header.is_some() || opt.footer.is_some()),
print_background: Some(opt.background),
scale: opt.scale,
paper_width: opt.paper.map(|p| p.dimensions().0),
paper_height: opt.paper.map(|p| p.dimensions().1),
margin_top: opt.margin.as_ref().map(|m| m.top),
margin_bottom: opt.margin.as_ref().map(|m| m.bottom),
margin_left: opt.margin.as_ref().map(|m| m.left),
margin_right: opt.margin.as_ref().map(|m| m.right),
page_ranges: opt.range.clone(),
header_template: opt.header.clone(),
footer_template: opt.footer.clone(),
..Default::default()
}
}
}
impl From<&Options> for LaunchOptions<'_> {
fn from(opt: &Options) -> Self {
LaunchOptions {
sandbox: !opt.disable_sandbox,
idle_browser_timeout: opt.wait.unwrap_or_default().max(Duration::from_secs(30)),
..Default::default()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::Display)]
pub enum PaperSize {
A0,
A1,
A2,
A3,
A4,
A5,
A6,
Letter,
Legal,
Tabloid,
}
impl PaperSize {
#[must_use]
pub fn dimensions(self) -> (f64, f64) {
match self {
Self::A0 => (33.1, 46.8),
Self::A1 => (23.4, 33.1),
Self::A2 => (16.5, 23.4),
Self::A3 => (11.7, 16.5),
Self::A4 => (8.27, 11.7),
Self::A5 => (5.83, 8.27),
Self::A6 => (4.13, 5.83),
Self::Letter => (8.5, 11.0),
Self::Legal => (8.5, 17.0),
Self::Tabloid => (11.0, 17.0),
}
}
#[must_use]
pub fn paper_width(self) -> f64 {
self.dimensions().0
}
#[must_use]
pub fn paper_height(self) -> f64 {
self.dimensions().1
}
}
impl FromStr for PaperSize {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"a0" => Ok(Self::A0),
"a1" => Ok(Self::A1),
"a2" => Ok(Self::A2),
"a3" => Ok(Self::A3),
"a4" => Ok(Self::A4),
"a5" => Ok(Self::A5),
"a6" => Ok(Self::A6),
"letter" => Ok(Self::Letter),
"legal" => Ok(Self::Legal),
"tabloid" => Ok(Self::Tabloid),
_ => Err(Error::InvalidPaperSize {
size: s.to_string(),
}),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Margin {
pub top: f64,
pub right: f64,
pub bottom: f64,
pub left: f64,
}
impl FromStr for Margin {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let values: Vec<&str> = s.split(' ').filter(|s| !s.is_empty()).collect();
match values.len() {
1 => {
let all = s.parse::<f64>()?;
Ok(Self {
top: all,
right: all,
bottom: all,
left: all,
})
}
2 => {
let v = values[0].parse::<f64>()?;
let h = values[1].parse::<f64>()?;
Ok(Self {
top: v,
right: h,
bottom: v,
left: h,
})
}
4 => Ok(Self {
top: values[0].parse::<f64>()?,
right: values[1].parse::<f64>()?,
bottom: values[2].parse::<f64>()?,
left: values[3].parse::<f64>()?,
}),
_ => Err(Error::InvalidMarginDefinition {
margin: s.to_string(),
}),
}
}
}
#[cfg(test)]
mod tests {
use assert2::check;
use rstest::rstest;
use super::*;
#[rstest]
#[case::a0("a0", PaperSize::A0)]
#[case::a1("A1", PaperSize::A1)]
#[case::a2("A2", PaperSize::A2)]
#[case::a3("A3", PaperSize::A3)]
#[case::a4("A4", PaperSize::A4)]
#[case::a5("A5", PaperSize::A5)]
#[case::a6("A6", PaperSize::A6)]
#[case::letter("letter", PaperSize::Letter)]
#[case::legal("Legal", PaperSize::Legal)]
#[case::tabloid("Tabloid", PaperSize::Tabloid)]
fn should_parse_valid_paper_size(#[case] value: &str, #[case] expected: PaperSize) {
let result = value
.parse::<PaperSize>()
.expect("should parse valid paper size");
check!(result == expected);
}
#[test]
fn should_reject_invalid_paper_size() {
let value = "plop";
let result = value.parse::<PaperSize>();
check!(let Err(Error::InvalidPaperSize { .. }) = result);
}
#[test]
fn should_parse_valid_margin_all() {
let m = "0.4".parse::<Margin>().expect("should parse margin");
check!(m.top == 0.4);
check!(m.right == 0.4);
check!(m.bottom == 0.4);
check!(m.left == 0.4);
}
#[test]
fn should_parse_valid_margin_vh() {
let m = "0.4 0.7".parse::<Margin>().expect("should parse margin");
check!(m.top == 0.4);
check!(m.bottom == 0.4);
check!(m.right == 0.7);
check!(m.left == 0.7);
}
#[test]
fn should_parse_valid_margin_trbl() {
let m = "0.2 0.3 0.4 0.5"
.parse::<Margin>()
.expect("should parse margin");
check!(m.top == 0.2);
check!(m.right == 0.3);
check!(m.bottom == 0.4);
check!(m.left == 0.5);
}
#[test]
fn should_reject_invalid_margin() {
let value = "0.2 0.3 0.4";
let result = value.parse::<Margin>();
check!(let Err(Error::InvalidMarginDefinition { .. }) = result);
}
#[test]
fn should_reject_invalid_margin_value() {
let value = "plop";
let result = value.parse::<Margin>();
check!(let Err(Error::InvalidMarginValue(_)) = result);
}
}