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)]
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 Options {
#[must_use]
pub fn input(&self) -> &PathBuf {
&self.input
}
#[must_use]
pub fn output(&self) -> Option<&PathBuf> {
self.output.as_ref()
}
#[must_use]
pub fn landscape(&self) -> bool {
self.landscape
}
#[must_use]
pub fn background(&self) -> bool {
self.background
}
#[must_use]
pub fn wait(&self) -> Option<Duration> {
self.wait
}
#[must_use]
pub fn header(&self) -> Option<&String> {
self.header.as_ref()
}
#[must_use]
pub fn footer(&self) -> Option<&String> {
self.footer.as_ref()
}
#[must_use]
pub fn paper(&self) -> Option<&PaperSize> {
self.paper.as_ref()
}
#[must_use]
pub fn scale(&self) -> Option<f64> {
self.scale
}
#[must_use]
pub fn margin(&self) -> Option<&Margin> {
self.margin.as_ref()
}
#[must_use]
pub fn range(&self) -> Option<&String> {
self.range.as_ref()
}
#[must_use]
pub fn disable_sandbox(&self) -> bool {
self.disable_sandbox
}
}
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(PaperSize::paper_width),
paper_height: opt.paper().map(PaperSize::paper_height),
margin_top: opt.margin().map(Margin::margin_top),
margin_bottom: opt.margin().map(Margin::margin_bottom),
margin_left: opt.margin().map(Margin::margin_left),
margin_right: opt.margin().map(Margin::margin_right),
page_ranges: opt.range().cloned(),
ignore_invalid_page_ranges: None,
header_template: opt.header().cloned(),
footer_template: opt.footer().cloned(),
prefer_css_page_size: None,
transfer_mode: None,
}
}
}
impl From<&Options> for LaunchOptions<'_> {
fn from(opt: &Options) -> Self {
LaunchOptions {
sandbox: !opt.disable_sandbox(),
idle_browser_timeout: opt.wait().unwrap_or(Duration::from_secs(30)),
..Default::default()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PaperSize {
A0,
A1,
A2,
A3,
A4,
A5,
A6,
Letter,
Legal,
Tabloid,
}
impl PaperSize {
#[must_use]
pub fn paper_width(&self) -> f64 {
match self {
PaperSize::A0 => 33.1,
PaperSize::A1 => 23.4,
PaperSize::A2 => 16.5,
PaperSize::A3 => 11.7,
PaperSize::A4 => 8.27,
PaperSize::A5 => 5.83,
PaperSize::A6 => 4.13,
PaperSize::Letter | PaperSize::Legal => 8.5,
PaperSize::Tabloid => 11.0,
}
}
#[must_use]
pub fn paper_height(&self) -> f64 {
match self {
PaperSize::A0 => 46.8,
PaperSize::A1 => 33.1,
PaperSize::A2 => 23.4,
PaperSize::A3 => 16.5,
PaperSize::A4 => 11.7,
PaperSize::A5 => 8.27,
PaperSize::A6 => 5.83,
PaperSize::Letter => 11.0,
PaperSize::Legal | PaperSize::Tabloid => 17.0,
}
}
}
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(s.to_string())),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Margin {
All(f64),
VerticalHorizontal(f64, f64),
TopRightBottomLeft(f64, f64, f64, f64),
}
impl Margin {
#[must_use]
pub fn margin_top(&self) -> f64 {
match self {
Margin::All(f)
| Margin::VerticalHorizontal(f, _)
| Margin::TopRightBottomLeft(f, _, _, _) => *f,
}
}
#[must_use]
pub fn margin_right(&self) -> f64 {
match self {
Margin::All(f)
| Margin::VerticalHorizontal(_, f)
| Margin::TopRightBottomLeft(_, f, _, _) => *f,
}
}
#[must_use]
pub fn margin_bottom(&self) -> f64 {
match self {
Margin::All(f)
| Margin::VerticalHorizontal(f, _)
| Margin::TopRightBottomLeft(_, _, f, _) => *f,
}
}
#[must_use]
pub fn margin_left(&self) -> f64 {
match self {
Margin::All(f)
| Margin::VerticalHorizontal(_, f)
| Margin::TopRightBottomLeft(_, _, _, f) => *f,
}
}
}
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 value = s.parse::<f64>().map_err(Error::InvalidMarginValue)?;
Ok(Margin::All(value))
}
2 => {
let v = values[0]
.parse::<f64>()
.map_err(Error::InvalidMarginValue)?;
let h = values[1]
.parse::<f64>()
.map_err(Error::InvalidMarginValue)?;
Ok(Margin::VerticalHorizontal(v, h))
}
4 => {
let top = values[0]
.parse::<f64>()
.map_err(Error::InvalidMarginValue)?;
let right = values[1]
.parse::<f64>()
.map_err(Error::InvalidMarginValue)?;
let bottom = values[2]
.parse::<f64>()
.map_err(Error::InvalidMarginValue)?;
let left = values[2]
.parse::<f64>()
.map_err(Error::InvalidMarginValue)?;
Ok(Margin::TopRightBottomLeft(top, right, bottom, left))
}
_ => Err(Error::InvalidMarginDefinition(s.to_string())),
}
}
}
#[cfg(test)]
mod tests {
use assert2::{check, let_assert};
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>().unwrap();
check!(result == expected);
}
#[test]
fn should_reject_invalid_paper_size() {
let value = "plop";
let result = value.parse::<PaperSize>();
let_assert!(Err(Error::InvalidPaperSize(_)) = result);
}
#[test]
fn should_parse_valid_margin_all() {
let value = "0.4";
let result = value.parse::<Margin>();
let_assert!(Ok(Margin::All(_)) = result);
}
#[test]
fn should_parse_valid_margin_vh() {
let value = "0.4 0.7";
let result = value.parse::<Margin>();
let_assert!(Ok(Margin::VerticalHorizontal(_, _)) = result);
}
#[test]
fn should_parse_valid_margin_trbl() {
let value = "0.2 0.3 0.4 0.5";
let result = value.parse::<Margin>();
let_assert!(Ok(Margin::TopRightBottomLeft(_, _, _, _)) = result);
}
#[test]
fn should_reject_invalid_margin() {
let value = "0.2 0.3 0.4";
let result = value.parse::<Margin>();
let_assert!(Err(Error::InvalidMarginDefinition(_)) = result);
}
#[test]
fn should_reject_invalid_margin_value() {
let value = "plop";
let result = value.parse::<Margin>();
let_assert!(Err(Error::InvalidMarginValue(_)) = result);
}
}