use crate::error::ColorFormatError;
use crate::{ColorSpace, Float};
fn parse_hashed(s: &str) -> Result<[u8; 3], ColorFormatError> {
if !s.starts_with('#') {
return Err(ColorFormatError::UnknownFormat);
} else if s.len() != 4 && s.len() != 7 {
return Err(ColorFormatError::UnexpectedCharacters);
}
fn parse_coordinate(s: &str, index: usize) -> Result<u8, ColorFormatError> {
let factor = s.len() / 3;
let t = s
.get((1 + factor * index)..=(factor * (index + 1)))
.ok_or(ColorFormatError::UnexpectedCharacters)?;
let n = u8::from_str_radix(t, 16).map_err(|_| ColorFormatError::MalformedHex)?;
Ok(if factor == 1 { 16 * n + n } else { n })
}
let c1 = parse_coordinate(s, 0)?;
let c2 = parse_coordinate(s, 1)?;
let c3 = parse_coordinate(s, 2)?;
Ok([c1, c2, c3])
}
pub(crate) fn parse_x(s: &str) -> Result<[Float; 3], ColorFormatError> {
if !s.starts_with("rgb:") {
return Err(ColorFormatError::UnknownFormat);
}
fn parse_coordinate(s: Option<&str>, _: usize) -> Result<Float, ColorFormatError> {
let t = s.ok_or(ColorFormatError::MissingCoordinate)?;
if t.is_empty() {
return Err(ColorFormatError::MissingCoordinate);
} else if 4 < t.len() {
return Err(ColorFormatError::OversizedCoordinate);
}
let n = u16::from_str_radix(t, 16).map_err(|_| ColorFormatError::MalformedHex)?;
Ok(n as Float / (16_u32.pow(t.len() as u32) - 1) as Float)
}
let mut iter = s.strip_prefix("rgb:").expect("was just tested").split('/');
let c1 = parse_coordinate(iter.next(), 0)?;
let c2 = parse_coordinate(iter.next(), 1)?;
let c3 = parse_coordinate(iter.next(), 2)?;
if iter.next().is_some() {
return Err(ColorFormatError::TooManyCoordinates);
}
Ok([c1, c2, c3])
}
const COLOR_SPACES: [(&str, ColorSpace); 11] = [
("srgb", ColorSpace::Srgb),
("linear-srgb", ColorSpace::LinearSrgb),
("display-p3", ColorSpace::DisplayP3),
("--linear-display-p3", ColorSpace::LinearDisplayP3),
("rec2020", ColorSpace::Rec2020),
("--linear-rec2020", ColorSpace::LinearRec2020),
("--oklrab", ColorSpace::Oklrab),
("--oklrch", ColorSpace::Oklrch),
("xyz", ColorSpace::Xyz),
("xyz-d65", ColorSpace::Xyz),
("xyz-d50", ColorSpace::XyzD50),
];
fn parse_css(s: &str) -> Result<(ColorSpace, [Float; 3]), ColorFormatError> {
use ColorSpace::*;
let (space, rest) = s
.strip_prefix("oklab")
.map(|r| (Some(Oklab), r))
.or_else(|| s.strip_prefix("oklch").map(|r| (Some(Oklch), r)))
.or_else(|| s.strip_prefix("color").map(|r| (None, r)))
.ok_or(ColorFormatError::UnknownFormat)?;
let rest = rest
.trim_start()
.strip_prefix('(')
.ok_or(ColorFormatError::NoOpeningParenthesis)
.and_then(|rest| {
rest.strip_suffix(')')
.ok_or(ColorFormatError::NoClosingParenthesis)
})?;
let (space, body) = if let Some(s) = space {
(s, rest) } else {
let rest = rest.trim_start();
COLOR_SPACES
.iter()
.filter_map(|&(p, s)| rest.strip_prefix(p).map(|r| (s, r)))
.next() .ok_or(ColorFormatError::UnknownColorSpace)?
};
#[inline]
fn parse_coordinate(s: Option<&str>, _: usize) -> Result<Float, ColorFormatError> {
s.ok_or(ColorFormatError::MissingCoordinate)
.and_then(|t| t.parse().map_err(|_| ColorFormatError::MalformedFloat))
}
let mut iter = body.split_whitespace();
let c1 = parse_coordinate(iter.next(), 0)?;
let c2 = parse_coordinate(iter.next(), 1)?;
let c3 = parse_coordinate(iter.next(), 2)?;
if iter.next().is_some() {
return Err(ColorFormatError::TooManyCoordinates);
}
Ok((space, [c1, c2, c3]))
}
pub(crate) fn parse(s: &str) -> Result<(ColorSpace, [Float; 3]), ColorFormatError> {
let lowercase = s.trim().to_ascii_lowercase(); let s = lowercase.as_str();
if s.starts_with('#') {
let [c1, c2, c3] = parse_hashed(s)?;
Ok((
ColorSpace::Srgb,
[
c1 as Float / 255.0,
c2 as Float / 255.0,
c3 as Float / 255.0,
],
))
} else if s.starts_with("rgb:") {
Ok((ColorSpace::Srgb, parse_x(s)?))
} else {
parse_css(s)
}
}
fn css_prefix(space: ColorSpace) -> &'static str {
use ColorSpace::*;
match space {
Srgb => "color(srgb ",
LinearSrgb => "color(linear-srgb ",
DisplayP3 => "color(display-p3 ",
LinearDisplayP3 => "color(--linear-display-p3 ",
Rec2020 => "color(rec2020 ",
LinearRec2020 => "color(--linear-rec2020 ",
Oklab => "oklab(",
Oklch => "oklch(",
Oklrab => "color(--oklrab ",
Oklrch => "color(--oklrch ",
Xyz => "color(xyz ",
XyzD50 => "color(xyz-d50 ",
}
}
pub(crate) fn format(
space: ColorSpace,
coordinates: &[Float; 3],
f: &mut core::fmt::Formatter<'_>,
) -> core::fmt::Result {
f.write_fmt(format_args!("{}", css_prefix(space)))?;
let mut factor = (10.0 as Float).powi(f.precision().unwrap_or(5) as i32);
for (index, coordinate) in coordinates.iter().enumerate() {
if space.is_polar() && index == 2 {
factor /= 100.0;
}
if coordinate.is_nan() {
f.write_str("none")?;
} else {
let c = (coordinate * factor).round() / factor;
if c == c.trunc() {
f.write_fmt(format_args!("{:.0}", c))?;
} else {
f.write_fmt(format_args!("{}", c))?;
}
}
if index < 2 {
f.write_str(" ")?;
}
}
f.write_str(")")
}
#[cfg(test)]
mod test {
use super::{parse, parse_css, parse_hashed, parse_x, ColorFormatError};
use crate::ColorSpace::*;
use crate::Float;
#[test]
fn test_parse_hashed() -> Result<(), ColorFormatError> {
assert_eq!(parse_hashed("#123")?, [0x11_u8, 0x22, 0x33]);
assert_eq!(parse_hashed("#112233")?, [0x11_u8, 0x22, 0x33]);
assert_eq!(parse_hashed("fff"), Err(ColorFormatError::UnknownFormat));
assert_eq!(
parse_hashed("#ff"),
Err(ColorFormatError::UnexpectedCharacters)
);
assert_eq!(
parse_hashed("#💩00"),
Err(ColorFormatError::UnexpectedCharacters)
);
let result = parse_hashed("#0g0");
assert!(matches!(result, Err(ColorFormatError::MalformedHex)));
let result = parse_hashed("#00g");
assert!(matches!(result, Err(ColorFormatError::MalformedHex)));
Ok(())
}
#[test]
fn test_parse_x() -> Result<(), ColorFormatError> {
assert_eq!(
parse_x("rgb:a/bb/ccc")?,
[
0xa as Float / 0xf as Float,
0xbb as Float / 0xff as Float,
0xccc as Float / 0xfff as Float
]
);
assert_eq!(
parse_x("rgb:0123/4567/89ab")?,
[
0x123 as Float / 0xffff as Float,
0x4567 as Float / 0xffff as Float,
0x89ab as Float / 0xffff as Float
]
);
assert_eq!(
parse_x("rgbi:0.1/0.1/0.1"),
Err(ColorFormatError::UnknownFormat)
);
assert_eq!(parse_x("rgb:0"), Err(ColorFormatError::MissingCoordinate));
assert_eq!(
parse_x("rgb:0//2"),
Err(ColorFormatError::MissingCoordinate)
);
assert_eq!(
parse_x("rgb:1/12345/1"),
Err(ColorFormatError::OversizedCoordinate)
);
assert_eq!(
parse_x("rgb:1/2/3/4"),
Err(ColorFormatError::TooManyCoordinates)
);
let result = parse_x("rgb:f/g/f");
assert!(matches!(result, Err(ColorFormatError::MalformedHex)));
assert_eq!(
parse(" RGB:00/55/aa ")?,
(
Srgb,
[0.0 as Float, 0.33333333333333333, 0.6666666666666666]
)
);
Ok(())
}
#[test]
fn test_parse_css() {
assert_eq!(parse_css("oklab(0 0 0)"), Ok((Oklab, [0.0, 0.0, 0.0])));
assert_eq!(
parse_css("color(xyz 1 1 1)"),
Ok((Xyz, [1.0, 1.0, 1.0]))
);
assert_eq!(
parse_css("color( --oklrch 1 1 1)"),
Ok((Oklrch, [1.0, 1.0, 1.0]))
);
assert_eq!(
parse_css("color ( --linear-display-p3 1 1.123 0.3333 )"),
Ok((LinearDisplayP3, [1.0, 1.123, 0.3333]))
);
assert_eq!(
parse_css("whatever(1 1 1)"),
Err(ColorFormatError::UnknownFormat)
);
assert_eq!(
parse_css("colorsrgb 1 1 1)"),
Err(ColorFormatError::NoOpeningParenthesis)
);
assert_eq!(
parse_css("color(srgb 1 1 1"),
Err(ColorFormatError::NoClosingParenthesis)
);
assert_eq!(
parse_css("color(nemo 1 1 1)"),
Err(ColorFormatError::UnknownColorSpace)
);
assert!(matches!(
parse_css("color(srgb abc 1 1)"),
Err(ColorFormatError::MalformedFloat)
));
assert_eq!(
parse_css("color(srgb 1)"),
Err(ColorFormatError::MissingCoordinate)
);
assert_eq!(
parse_css("color(srgb 1 1 1 1)"),
Err(ColorFormatError::TooManyCoordinates)
);
assert_eq!(
parse(" COLOR( --linear-display-p3 1 1.123 0.3333 ) "),
Ok((LinearDisplayP3, [1.0, 1.123, 0.3333]))
);
assert_eq!(
parse(" color( --Linear-Display-P3 1 1.123 0.3333 ) "),
Ok((LinearDisplayP3, [1.0, 1.123, 0.3333]))
);
}
#[test]
fn test_format() {
use crate::Color;
let clr = Color::srgb(0.3, 0.336, 0.123456);
assert_eq!(clr.to_string(), "color(srgb 0.3 0.336 0.12346)");
assert_eq!(format!("{:.2}", clr), "color(srgb 0.3 0.34 0.12)");
assert_eq!(Color::oklab(1.0, 0.0, 0.0).to_string(), "oklab(1 0 0)");
assert_eq!(
Color::oklch(0.5, 0.1, 167.0).to_string(),
"oklch(0.5 0.1 167)"
);
assert_eq!(
Color::oklrch(0.5, 0.1, 167.0).to_string(),
"color(--oklrch 0.5 0.1 167)"
);
}
}