use anyhow::{Context, Result};
pub fn parse_page_size(size: &str) -> Result<(f64, f64)> {
match size.to_uppercase().as_str() {
"A3" => Ok((297.0, 420.0)),
"A4" => Ok((210.0, 297.0)),
"A5" => Ok((148.0, 210.0)),
"LETTER" => Ok((215.9, 279.4)),
"LEGAL" => Ok((215.9, 355.6)),
"TABLOID" => Ok((279.4, 431.8)),
_ => anyhow::bail!("Unknown page size: {size}. Use A3, A4, A5, Letter, Legal, or Tabloid"),
}
}
pub fn parse_length_mm(s: &str) -> Result<f64> {
let s = s.trim();
if let Some(v) = s.strip_suffix("mm") {
v.trim().parse().context("Invalid mm value")
} else if let Some(v) = s.strip_suffix("cm") {
let v: f64 = v.trim().parse().context("Invalid cm value")?;
Ok(v * 10.0)
} else if let Some(v) = s.strip_suffix("in") {
let v: f64 = v.trim().parse().context("Invalid in value")?;
Ok(v * 25.4)
} else if let Some(v) = s.strip_suffix("pt") {
let v: f64 = v.trim().parse().context("Invalid pt value")?;
Ok(v * 25.4 / 72.0)
} else if let Some(v) = s.strip_suffix("px") {
let v: f64 = v.trim().parse().context("Invalid px value")?;
Ok(v * 25.4 / 96.0)
} else {
s.parse().context("Invalid length value")
}
}
pub fn parse_margins(s: &str) -> Result<(f64, f64, f64, f64)> {
let parts: Vec<&str> = s.split(',').collect();
match parts.len() {
1 => {
let m = parse_length_mm(parts[0])?;
Ok((m, m, m, m))
}
2 => {
let v = parse_length_mm(parts[0])?;
let h = parse_length_mm(parts[1])?;
Ok((v, h, v, h))
}
4 => {
let top = parse_length_mm(parts[0])?;
let right = parse_length_mm(parts[1])?;
let bottom = parse_length_mm(parts[2])?;
let left = parse_length_mm(parts[3])?;
Ok((top, right, bottom, left))
}
_ => anyhow::bail!(
"Invalid margin format. Use 'value', 'vertical,horizontal', or 'top,right,bottom,left'"
),
}
}
#[allow(dead_code)] pub fn parse_timeout_ms(s: &str) -> Result<u32> {
let s = s.trim();
if let Some(v) = s.strip_suffix("ms") {
v.trim().parse().context("Invalid ms value")
} else if let Some(v) = s.strip_suffix('s') {
let v: u32 = v.trim().parse().context("Invalid s value")?;
Ok(v * 1000)
} else if let Some(v) = s.strip_suffix('m') {
let v: u32 = v.trim().parse().context("Invalid m value")?;
Ok(v * 60 * 1000)
} else {
s.parse().context("Invalid timeout value")
}
}
#[allow(dead_code)]
pub fn parse_coords(spec: &str, count: usize) -> Result<Vec<f64>> {
let coords: Vec<f64> = spec
.split(',')
.map(|s| s.trim().parse::<f64>())
.collect::<std::result::Result<_, _>>()
.context("Invalid coordinates")?;
if coords.len() != count {
anyhow::bail!("Expected {count} coordinate values, got {}", coords.len());
}
Ok(coords)
}
#[allow(dead_code)]
pub fn parse_page_coords(
spec: &str,
coord_count: usize,
format_hint: &str,
) -> Result<(u32, Vec<f64>)> {
let parts: Vec<&str> = spec.split(':').collect();
if parts.len() != 2 {
anyhow::bail!("Invalid format. Use: {format_hint}");
}
let page: u32 = parts[0].parse().context("Invalid page number")?;
let coords = parse_coords(parts[1], coord_count)?;
Ok((page, coords))
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)] pub enum PageSelection {
All,
Pages(Vec<u32>),
Range(u32, u32),
Odd,
Even,
First,
Last,
}
#[allow(dead_code)] pub fn parse_page_selection(s: Option<&str>) -> Result<PageSelection> {
match s {
None | Some("") => Ok(PageSelection::All),
Some("odd") => Ok(PageSelection::Odd),
Some("even") => Ok(PageSelection::Even),
Some("first") => Ok(PageSelection::First),
Some("last") => Ok(PageSelection::Last),
Some(pages_str) => {
if let Some((start, end)) = pages_str.split_once('-') {
let start: u32 = start
.trim()
.parse()
.context("Invalid start page in range")?;
let end: u32 = end.trim().parse().context("Invalid end page in range")?;
if start > end {
anyhow::bail!("Start page ({start}) must be <= end page ({end})");
}
Ok(PageSelection::Range(start, end))
} else {
let pages: Vec<u32> = pages_str
.split(',')
.map(|s| s.trim().parse::<u32>())
.collect::<std::result::Result<_, _>>()
.context("Invalid page numbers")?;
if pages.is_empty() {
anyhow::bail!("No page numbers specified");
}
Ok(PageSelection::Pages(pages))
}
}
}
}
#[allow(dead_code)] pub fn parse_color_hex(s: &str) -> Result<(u8, u8, u8)> {
match s.to_lowercase().as_str() {
"red" => Ok((255, 0, 0)),
"green" => Ok((0, 255, 0)),
"blue" => Ok((0, 0, 255)),
"black" => Ok((0, 0, 0)),
"white" => Ok((255, 255, 255)),
"gray" | "grey" => Ok((128, 128, 128)),
"yellow" => Ok((255, 255, 0)),
"cyan" => Ok((0, 255, 255)),
"magenta" => Ok((255, 0, 255)),
"orange" => Ok((255, 165, 0)),
"purple" => Ok((128, 0, 128)),
"pink" => Ok((255, 192, 203)),
hex => {
let hex = hex.strip_prefix('#').unwrap_or(hex);
if hex.len() != 6 {
anyhow::bail!("Invalid hex color format. Use #RRGGBB or RRGGBB");
}
let r =
u8::from_str_radix(&hex[0..2], 16).context("Invalid red component in hex color")?;
let g = u8::from_str_radix(&hex[2..4], 16)
.context("Invalid green component in hex color")?;
let b = u8::from_str_radix(&hex[4..6], 16)
.context("Invalid blue component in hex color")?;
Ok((r, g, b))
}
}
}
pub fn parse_colon_spec<'a>(
spec: &'a str,
min_parts: usize,
format_hint: &str,
) -> Result<Vec<&'a str>> {
let parts: Vec<&str> = spec.split(':').collect();
if parts.len() < min_parts {
anyhow::bail!("Invalid format. Expected: {format_hint}");
}
Ok(parts)
}
#[allow(dead_code)]
pub fn parse_page_spec<'a>(
spec: &'a str,
min_extra_parts: usize,
format_hint: &str,
) -> Result<(u32, Vec<&'a str>)> {
let parts = parse_colon_spec(spec, min_extra_parts + 1, format_hint)?;
let page: u32 = parts[0]
.parse()
.with_context(|| format!("Invalid page number: {}", parts[0]))?;
Ok((page, parts[1..].to_vec()))
}
#[allow(dead_code)] pub fn parse_rect_from_parts(parts: &[&str], start_index: usize) -> Result<(f32, f32, f32, f32)> {
if parts.len() < start_index + 4 {
anyhow::bail!("Not enough values for rectangle (need x, y, width, height)");
}
let pos_x: f32 = parts[start_index]
.parse()
.with_context(|| format!("Invalid x coordinate: {}", parts[start_index]))?;
let pos_y: f32 = parts[start_index + 1]
.parse()
.with_context(|| format!("Invalid y coordinate: {}", parts[start_index + 1]))?;
let width: f32 = parts[start_index + 2]
.parse()
.with_context(|| format!("Invalid width: {}", parts[start_index + 2]))?;
let height: f32 = parts[start_index + 3]
.parse()
.with_context(|| format!("Invalid height: {}", parts[start_index + 3]))?;
Ok((pos_x, pos_y, width, height))
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < 0.001
}
fn approx_eq_tuple(a: (f64, f64), b: (f64, f64)) -> bool {
approx_eq(a.0, b.0) && approx_eq(a.1, b.1)
}
fn approx_eq_quad(a: (f64, f64, f64, f64), b: (f64, f64, f64, f64)) -> bool {
approx_eq(a.0, b.0) && approx_eq(a.1, b.1) && approx_eq(a.2, b.2) && approx_eq(a.3, b.3)
}
fn approx_eq_vec(a: &[f64], b: &[f64]) -> bool {
a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| approx_eq(*x, *y))
}
#[test]
fn test_parse_page_size_valid() {
assert!(approx_eq_tuple(
parse_page_size("A4").unwrap(),
(210.0, 297.0)
));
assert!(approx_eq_tuple(
parse_page_size("a4").unwrap(),
(210.0, 297.0)
));
assert!(approx_eq_tuple(
parse_page_size("A3").unwrap(),
(297.0, 420.0)
));
assert!(approx_eq_tuple(
parse_page_size("A5").unwrap(),
(148.0, 210.0)
));
assert!(approx_eq_tuple(
parse_page_size("Letter").unwrap(),
(215.9, 279.4)
));
assert!(approx_eq_tuple(
parse_page_size("LEGAL").unwrap(),
(215.9, 355.6)
));
assert!(approx_eq_tuple(
parse_page_size("tabloid").unwrap(),
(279.4, 431.8)
));
}
#[test]
fn test_parse_page_size_invalid() {
assert!(parse_page_size("A6").is_err());
assert!(parse_page_size("").is_err());
assert!(parse_page_size("unknown").is_err());
}
#[test]
fn test_parse_length_mm_valid() {
assert!(approx_eq(parse_length_mm("10mm").unwrap(), 10.0));
assert!(approx_eq(parse_length_mm("10").unwrap(), 10.0));
assert!(approx_eq(parse_length_mm("1cm").unwrap(), 10.0));
assert!(approx_eq(parse_length_mm("1in").unwrap(), 25.4));
assert!((parse_length_mm("72pt").unwrap() - 25.4).abs() < 0.01);
assert!((parse_length_mm("96px").unwrap() - 25.4).abs() < 0.01);
}
#[test]
fn test_parse_length_mm_invalid() {
assert!(parse_length_mm("abc").is_err());
assert!(parse_length_mm("mm").is_err());
assert!(parse_length_mm("10xyz").is_err());
}
#[test]
fn test_parse_margins_single_value() {
assert!(approx_eq_quad(
parse_margins("10mm").unwrap(),
(10.0, 10.0, 10.0, 10.0)
));
}
#[test]
fn test_parse_margins_two_values() {
assert!(approx_eq_quad(
parse_margins("10mm,20mm").unwrap(),
(10.0, 20.0, 10.0, 20.0)
));
}
#[test]
fn test_parse_margins_four_values() {
assert!(approx_eq_quad(
parse_margins("10mm,20mm,30mm,40mm").unwrap(),
(10.0, 20.0, 30.0, 40.0)
));
}
#[test]
fn test_parse_margins_invalid() {
assert!(parse_margins("10mm,20mm,30mm").is_err()); assert!(parse_margins("10mm,20mm,30mm,40mm,50mm").is_err()); }
#[test]
fn test_parse_timeout_ms_valid() {
assert_eq!(parse_timeout_ms("1000ms").unwrap(), 1000);
assert_eq!(parse_timeout_ms("30s").unwrap(), 30000);
assert_eq!(parse_timeout_ms("1m").unwrap(), 60000);
assert_eq!(parse_timeout_ms("5000").unwrap(), 5000);
}
#[test]
fn test_parse_timeout_ms_invalid() {
assert!(parse_timeout_ms("abc").is_err());
assert!(parse_timeout_ms("10h").is_err()); }
#[test]
fn test_parse_coords_valid() {
assert!(approx_eq_vec(
&parse_coords("10,20,30,40", 4).unwrap(),
&[10.0, 20.0, 30.0, 40.0]
));
assert!(approx_eq_vec(
&parse_coords("1.5,2.5", 2).unwrap(),
&[1.5, 2.5]
));
}
#[test]
fn test_parse_coords_wrong_count() {
assert!(parse_coords("10,20", 4).is_err());
assert!(parse_coords("10,20,30,40,50", 4).is_err());
}
#[test]
fn test_parse_coords_invalid_number() {
assert!(parse_coords("10,abc,30,40", 4).is_err());
}
#[test]
fn test_parse_page_coords_valid() {
let (page, coords) = parse_page_coords("1:100,200,50,25", 4, "page:x,y,w,h").unwrap();
assert_eq!(page, 1);
assert_eq!(coords, vec![100.0, 200.0, 50.0, 25.0]);
}
#[test]
fn test_parse_page_coords_invalid_format() {
assert!(parse_page_coords("1", 4, "page:x,y,w,h").is_err());
assert!(parse_page_coords("1:2:3", 4, "page:x,y,w,h").is_err());
}
#[test]
fn test_parse_page_coords_invalid_page() {
assert!(parse_page_coords("abc:100,200,50,25", 4, "page:x,y,w,h").is_err());
}
#[test]
fn test_parse_page_selection_all() {
assert_eq!(parse_page_selection(None).unwrap(), PageSelection::All);
assert_eq!(parse_page_selection(Some("")).unwrap(), PageSelection::All);
}
#[test]
fn test_parse_page_selection_keywords() {
assert_eq!(
parse_page_selection(Some("odd")).unwrap(),
PageSelection::Odd
);
assert_eq!(
parse_page_selection(Some("even")).unwrap(),
PageSelection::Even
);
assert_eq!(
parse_page_selection(Some("first")).unwrap(),
PageSelection::First
);
assert_eq!(
parse_page_selection(Some("last")).unwrap(),
PageSelection::Last
);
}
#[test]
fn test_parse_page_selection_range() {
assert_eq!(
parse_page_selection(Some("1-5")).unwrap(),
PageSelection::Range(1, 5)
);
assert_eq!(
parse_page_selection(Some("10-20")).unwrap(),
PageSelection::Range(10, 20)
);
}
#[test]
fn test_parse_page_selection_list() {
assert_eq!(
parse_page_selection(Some("1,3,5")).unwrap(),
PageSelection::Pages(vec![1, 3, 5])
);
assert_eq!(
parse_page_selection(Some("2, 4, 6")).unwrap(),
PageSelection::Pages(vec![2, 4, 6])
);
}
#[test]
fn test_parse_page_selection_invalid() {
assert!(parse_page_selection(Some("5-1")).is_err()); assert!(parse_page_selection(Some("abc")).is_err());
}
#[test]
fn test_parse_color_hex_named() {
assert_eq!(parse_color_hex("red").unwrap(), (255, 0, 0));
assert_eq!(parse_color_hex("green").unwrap(), (0, 255, 0));
assert_eq!(parse_color_hex("blue").unwrap(), (0, 0, 255));
assert_eq!(parse_color_hex("black").unwrap(), (0, 0, 0));
assert_eq!(parse_color_hex("white").unwrap(), (255, 255, 255));
assert_eq!(parse_color_hex("gray").unwrap(), (128, 128, 128));
assert_eq!(parse_color_hex("grey").unwrap(), (128, 128, 128));
assert_eq!(parse_color_hex("yellow").unwrap(), (255, 255, 0));
}
#[test]
fn test_parse_color_hex_hex() {
assert_eq!(parse_color_hex("#FF0000").unwrap(), (255, 0, 0));
assert_eq!(parse_color_hex("00FF00").unwrap(), (0, 255, 0));
assert_eq!(parse_color_hex("#0000ff").unwrap(), (0, 0, 255));
}
#[test]
fn test_parse_color_hex_invalid() {
assert!(parse_color_hex("#FFF").is_err()); assert!(parse_color_hex("#GGGGGG").is_err()); assert!(parse_color_hex("notacolor").is_err());
}
#[test]
fn test_parse_colon_spec_valid() {
let parts = parse_colon_spec("a:b:c", 3, "x:y:z").unwrap();
assert_eq!(parts, vec!["a", "b", "c"]);
}
#[test]
fn test_parse_colon_spec_too_few() {
assert!(parse_colon_spec("a:b", 3, "x:y:z").is_err());
}
#[test]
fn test_parse_page_spec_valid() {
let (page, parts) = parse_page_spec("5:a:b:c", 3, "page:x:y:z").unwrap();
assert_eq!(page, 5);
assert_eq!(parts, vec!["a", "b", "c"]);
}
#[test]
fn test_parse_rect_from_parts_valid() {
let parts = vec!["1", "100", "200", "50", "25"];
let (x, y, w, h) = parse_rect_from_parts(&parts, 1).unwrap();
assert!(approx_eq(f64::from(x), 100.0));
assert!(approx_eq(f64::from(y), 200.0));
assert!(approx_eq(f64::from(w), 50.0));
assert!(approx_eq(f64::from(h), 25.0));
}
#[test]
fn test_parse_rect_from_parts_invalid() {
let parts = vec!["1", "100", "200"];
assert!(parse_rect_from_parts(&parts, 1).is_err());
}
}