use oxipdf_ir::color::Color;
use oxipdf_ir::style::typography::LineHeight;
use oxipdf_ir::units::{CalcExpr, Pt};
use oxipdf_ir::{Dimension, LengthPercentage};
pub(crate) fn parse_length(val: &str) -> Option<f64> {
let val = val.trim();
if val == "0" {
return Some(0.0);
}
if let Some(px) = val.strip_suffix("px") {
return px.trim().parse::<f64>().ok().map(|v| v * 0.75); }
if let Some(pt) = val.strip_suffix("pt") {
return pt.trim().parse::<f64>().ok();
}
if let Some(em) = val.strip_suffix("em") {
return em.trim().parse::<f64>().ok().map(|v| v * 12.0); }
if let Some(rem) = val.strip_suffix("rem") {
return rem.trim().parse::<f64>().ok().map(|v| v * 12.0);
}
if let Some(mm) = val.strip_suffix("mm") {
return mm.trim().parse::<f64>().ok().map(|v| v * 2.835);
}
if let Some(cm) = val.strip_suffix("cm") {
return cm.trim().parse::<f64>().ok().map(|v| v * 28.35);
}
val.parse::<f64>().ok()
}
pub(crate) fn parse_dimension(val: &str) -> Dimension {
let val = val.trim();
if val == "auto" {
return Dimension::Auto;
}
if let Some(d) = parse_dimension_func(val) {
return d;
}
if let Some(pct) = val.strip_suffix('%') {
if let Ok(v) = pct.trim().parse::<f64>() {
return Dimension::Percent(v / 100.0);
}
}
if let Some(pt) = parse_length(val) {
return Dimension::Length(Pt::new(pt));
}
Dimension::Auto
}
pub(crate) fn parse_length_percentage(val: &str) -> Option<LengthPercentage> {
let val = val.trim();
if let Some(lp) = parse_lp_func(val) {
return Some(lp);
}
if let Some(pct) = val.strip_suffix('%') {
if let Ok(v) = pct.trim().parse::<f64>() {
return Some(LengthPercentage::Percent(v / 100.0));
}
}
parse_length(val).map(|pt| LengthPercentage::Length(Pt::new(pt)))
}
fn parse_math_func<T>(
val: &str,
make_calc: fn(CalcExpr) -> T,
make_min: fn(CalcExpr, CalcExpr) -> T,
make_max: fn(CalcExpr, CalcExpr) -> T,
make_clamp: fn(CalcExpr, CalcExpr, CalcExpr) -> T,
) -> Option<T> {
if let Some(inner) = strip_func(val, "calc") {
return parse_calc_expr(inner).map(make_calc);
}
if let Some(inner) = strip_func(val, "min") {
let args = split_func_args(inner);
if args.len() == 2 {
let a = parse_calc_term(&args[0])?;
let b = parse_calc_term(&args[1])?;
return Some(make_min(a, b));
}
}
if let Some(inner) = strip_func(val, "max") {
let args = split_func_args(inner);
if args.len() == 2 {
let a = parse_calc_term(&args[0])?;
let b = parse_calc_term(&args[1])?;
return Some(make_max(a, b));
}
}
if let Some(inner) = strip_func(val, "clamp") {
let args = split_func_args(inner);
if args.len() == 3 {
let min = parse_calc_term(&args[0])?;
let v = parse_calc_term(&args[1])?;
let max = parse_calc_term(&args[2])?;
return Some(make_clamp(min, v, max));
}
}
None
}
fn parse_dimension_func(val: &str) -> Option<Dimension> {
parse_math_func(
val,
Dimension::Calc,
Dimension::Min,
Dimension::Max,
|min, v, max| Dimension::Clamp { min, val: v, max },
)
}
fn parse_lp_func(val: &str) -> Option<LengthPercentage> {
parse_math_func(
val,
LengthPercentage::Calc,
LengthPercentage::Min,
LengthPercentage::Max,
|min, v, max| LengthPercentage::Clamp { min, val: v, max },
)
}
fn parse_calc_expr(inner: &str) -> Option<CalcExpr> {
let inner = inner.trim();
if let Some((left, right)) = split_calc_operator(inner) {
let (right_str, negate) = if let Some(stripped) = right.strip_prefix('-') {
(stripped, true)
} else {
(right.as_str(), false)
};
let a = parse_calc_term(left.trim())?;
let mut b = parse_calc_term(right_str.trim())?;
if negate {
b.length = Pt::new(-b.length.get());
b.percent = -b.percent;
}
return Some(CalcExpr {
length: Pt::new(a.length.get() + b.length.get()),
percent: a.percent + b.percent,
});
}
parse_calc_term(inner)
}
fn split_calc_operator(s: &str) -> Option<(&str, String)> {
let bytes = s.as_bytes();
let mut i = 1; while i + 2 < bytes.len() {
if bytes[i] == b' ' {
let op_start = i + 1;
if op_start < bytes.len() && (bytes[op_start] == b'+' || bytes[op_start] == b'-') {
let op_char = bytes[op_start] as char;
let left = &s[..i];
let right = format!(
"{}{}",
if op_char == '-' { "-" } else { "" },
s[op_start + 1..].trim_start()
);
return Some((left, right));
}
}
i += 1;
}
None
}
fn parse_calc_term(val: &str) -> Option<CalcExpr> {
let val = val.trim();
if let Some(pct) = val.strip_suffix('%') {
let v = pct.trim().parse::<f64>().ok()?;
return Some(CalcExpr::from_percent(v / 100.0));
}
let pt = parse_length(val)?;
Some(CalcExpr::from_length(Pt::new(pt)))
}
fn strip_func<'a>(val: &'a str, name: &str) -> Option<&'a str> {
let val = val.trim();
let prefix = format!("{name}(");
let rest = val.strip_prefix(&prefix)?;
let inner = rest.strip_suffix(')')?;
Some(inner)
}
fn split_func_args(s: &str) -> Vec<String> {
let mut args = Vec::new();
let mut current = String::new();
let mut depth = 0u32;
for ch in s.chars() {
match ch {
'(' => {
depth += 1;
current.push(ch);
}
')' => {
depth = depth.saturating_sub(1);
current.push(ch);
}
',' if depth == 0 => {
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
args.push(trimmed);
}
current.clear();
}
_ => current.push(ch),
}
}
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
args.push(trimmed);
}
args
}
pub(crate) fn parse_color(val: &str) -> Option<Color> {
let val = val.trim();
if let Some(hex) = val.strip_prefix('#') {
return parse_hex_color(hex);
}
if let Some(inner) = val
.strip_prefix("rgb(")
.or_else(|| val.strip_prefix("rgba("))
{
let inner = inner.trim_end_matches(')');
let parts: Vec<&str> = inner.split([',', '/']).collect();
if parts.len() >= 3 {
let r = parse_color_component(parts[0])?;
let g = parse_color_component(parts[1])?;
let b = parse_color_component(parts[2])?;
let a = parts
.get(3)
.and_then(|s| parse_color_component(s))
.unwrap_or(1.0);
return Some(Color::rgba(r, g, b, a));
}
}
match val.to_lowercase().as_str() {
"black" => Some(Color::BLACK),
"white" => Some(Color::WHITE),
"red" => Some(Color::rgb(1.0, 0.0, 0.0)),
"green" => Some(Color::rgb(0.0, 0.502, 0.0)),
"blue" => Some(Color::rgb(0.0, 0.0, 1.0)),
"gray" | "grey" => Some(Color::rgb(0.502, 0.502, 0.502)),
"silver" => Some(Color::rgb(0.753, 0.753, 0.753)),
"navy" => Some(Color::rgb(0.0, 0.0, 0.502)),
"teal" => Some(Color::rgb(0.0, 0.502, 0.502)),
"maroon" => Some(Color::rgb(0.502, 0.0, 0.0)),
"purple" => Some(Color::rgb(0.502, 0.0, 0.502)),
"olive" => Some(Color::rgb(0.502, 0.502, 0.0)),
"orange" => Some(Color::rgb(1.0, 0.647, 0.0)),
"yellow" => Some(Color::rgb(1.0, 1.0, 0.0)),
"transparent" => Some(Color::TRANSPARENT),
_ => None,
}
}
fn parse_hex_color(hex: &str) -> Option<Color> {
match hex.len() {
3 => {
let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()? as f32 / 255.0;
let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()? as f32 / 255.0;
let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()? as f32 / 255.0;
Some(Color::rgb(r, g, b))
}
6 => {
let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.0;
let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.0;
let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.0;
Some(Color::rgb(r, g, b))
}
8 => {
let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.0;
let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.0;
let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.0;
let a = u8::from_str_radix(&hex[6..8], 16).ok()? as f32 / 255.0;
Some(Color::rgba(r, g, b, a))
}
_ => None,
}
}
fn parse_color_component(s: &str) -> Option<f32> {
let s = s.trim();
if let Some(pct) = s.strip_suffix('%') {
return pct.trim().parse::<f32>().ok().map(|v| v / 100.0);
}
let v = s.parse::<f32>().ok()?;
if v > 1.0 {
Some(v / 255.0) } else {
Some(v) }
}
pub(crate) fn parse_font_weight(val: &str) -> u16 {
match val.trim() {
"normal" => 400,
"bold" => 700,
"lighter" => 300,
"bolder" => 800,
n => n.parse().unwrap_or(400),
}
}
pub(crate) fn parse_font_family(val: &str) -> Vec<String> {
val.split(',')
.map(|s| s.trim().trim_matches('"').trim_matches('\'').to_string())
.filter(|s| !s.is_empty())
.collect()
}
pub(crate) fn parse_line_height(val: &str) -> Option<LineHeight> {
let val = val.trim();
if val == "normal" {
return Some(LineHeight::Normal);
}
if let Some(pt) = parse_length(val) {
return Some(LineHeight::Length(Pt::new(pt)));
}
val.parse::<f64>().ok().map(LineHeight::Number)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_hex_colors() {
assert!(parse_color("#ff0000").is_some());
assert!(parse_color("#f00").is_some());
assert!(parse_color("#ff000080").is_some());
}
#[test]
fn parse_named_colors() {
assert!(parse_color("black").is_some());
assert!(parse_color("transparent").is_some());
assert!(parse_color("unknowncolor").is_none());
}
#[test]
fn parse_rgb_function() {
let c = parse_color("rgb(255, 0, 0)").unwrap();
match c {
Color::Srgb { r, .. } => assert!((r - 1.0).abs() < 0.01),
_ => panic!("expected Srgb"),
}
}
#[test]
fn parse_lengths() {
assert!((parse_length("12pt").unwrap() - 12.0).abs() < 0.01);
assert!((parse_length("16px").unwrap() - 12.0).abs() < 0.01); assert!((parse_length("1em").unwrap() - 12.0).abs() < 0.01);
assert!((parse_length("0").unwrap()).abs() < 0.01);
assert!(parse_length("auto").is_none());
}
#[test]
fn parse_dimensions() {
assert!(matches!(parse_dimension("auto"), Dimension::Auto));
assert!(matches!(parse_dimension("50%"), Dimension::Percent(_)));
assert!(matches!(parse_dimension("100px"), Dimension::Length(_)));
}
#[test]
fn parse_calc_subtraction() {
let d = parse_dimension("calc(100% - 20px)");
if let Dimension::Calc(e) = d {
assert!((e.percent - 1.0).abs() < 0.01);
assert!((e.length.get() - (-15.0)).abs() < 0.01); } else {
panic!("expected Calc, got {d:?}");
}
}
#[test]
fn parse_calc_addition() {
let d = parse_dimension("calc(50% + 10pt)");
if let Dimension::Calc(e) = d {
assert!((e.percent - 0.5).abs() < 0.01);
assert!((e.length.get() - 10.0).abs() < 0.01);
} else {
panic!("expected Calc, got {d:?}");
}
}
#[test]
fn parse_min_function() {
let d = parse_dimension("min(300pt, 50%)");
assert!(matches!(d, Dimension::Min(_, _)), "expected Min, got {d:?}");
assert!((d.resolve(500.0).unwrap() - 250.0).abs() < 0.01);
}
#[test]
fn parse_max_function() {
let d = parse_dimension("max(100pt, 50%)");
assert!(matches!(d, Dimension::Max(_, _)), "expected Max, got {d:?}");
assert!((d.resolve(150.0).unwrap() - 100.0).abs() < 0.01);
}
#[test]
fn parse_clamp_function() {
let d = parse_dimension("clamp(100pt, 50%, 300pt)");
assert!(
matches!(d, Dimension::Clamp { .. }),
"expected Clamp, got {d:?}"
);
assert!((d.resolve(800.0).unwrap() - 300.0).abs() < 0.01);
}
#[test]
fn parse_calc_lp() {
let lp = parse_length_percentage("calc(100% - 10pt)");
assert!(lp.is_some());
if let Some(LengthPercentage::Calc(e)) = lp {
assert!((e.percent - 1.0).abs() < 0.01);
assert!((e.length.get() - (-10.0)).abs() < 0.01);
} else {
panic!("expected Calc LengthPercentage");
}
}
#[test]
fn parse_min_lp() {
let lp = parse_length_percentage("min(200pt, 80%)");
assert!(matches!(lp, Some(LengthPercentage::Min(_, _))));
}
}