cartography 0.11.0

Cartography is a map rendering library for Geographic features expressed using [georust](https://georust.org/) libraries.
Documentation
//! Hex color parsing utilities

use crate::{
  Result,
  styling::{Rgba, rgba},
};

/// Parse a CSS-style hex color string to Rgba
///
/// Supports both 6-digit (#rrggbb) and 8-digit (#rrggbbaa) formats.
///
/// # Arguments
///
/// * `color_str` - A hex color string starting with '#'
///
/// # Returns
///
/// An Rgba color with values in [0.0, 1.0]
///
/// # Errors
///
/// Returns an error if the format is invalid or hex values are out of range.
pub fn parse_hex_color(color_str: &str) -> Result<Rgba>
{
  if !color_str.starts_with('#')
  {
    return Err(anyhow::anyhow!("Color must start with '#'"));
  }

  let hex_str = &color_str[1..];

  match hex_str.len()
  {
    6 => parse_6digit_hex(hex_str),
    8 => parse_8digit_hex(hex_str),
    _ => Err(anyhow::anyhow!(
      "Color must be 6 or 8 hex digits, got {}",
      hex_str.len()
    )),
  }
}

fn parse_6digit_hex(hex_str: &str) -> Result<Rgba>
{
  let r = parse_hex_byte(&hex_str[0..2])?;
  let g = parse_hex_byte(&hex_str[2..4])?;
  let b = parse_hex_byte(&hex_str[4..6])?;

  Ok(rgba(
    r as f32 / 255.0,
    g as f32 / 255.0,
    b as f32 / 255.0,
    1.0,
  ))
}

fn parse_8digit_hex(hex_str: &str) -> Result<Rgba>
{
  let r = parse_hex_byte(&hex_str[0..2])?;
  let g = parse_hex_byte(&hex_str[2..4])?;
  let b = parse_hex_byte(&hex_str[4..6])?;
  let a = parse_hex_byte(&hex_str[6..8])?;

  Ok(rgba(
    r as f32 / 255.0,
    g as f32 / 255.0,
    b as f32 / 255.0,
    a as f32 / 255.0,
  ))
}

fn parse_hex_byte(hex_str: &str) -> Result<u8>
{
  u8::from_str_radix(hex_str, 16).map_err(|_| anyhow::anyhow!("Invalid hex value: {}", hex_str))
}

#[cfg(test)]
mod tests
{
  use super::*;

  #[test]
  fn test_parse_red()
  {
    let color = parse_hex_color("#ff0000").unwrap();
    assert!((color.red() - 1.0).abs() < 0.01);
    assert!((color.green() - 0.0).abs() < 0.01);
    assert!((color.blue() - 0.0).abs() < 0.01);
    assert!((color.alpha() - 1.0).abs() < 0.01);
  }

  #[test]
  fn test_parse_black()
  {
    let color = parse_hex_color("#000000").unwrap();
    assert!((color.red() - 0.0).abs() < 0.01);
    assert!((color.green() - 0.0).abs() < 0.01);
    assert!((color.blue() - 0.0).abs() < 0.01);
    assert!((color.alpha() - 1.0).abs() < 0.01);
  }

  #[test]
  fn test_parse_white()
  {
    let color = parse_hex_color("#ffffff").unwrap();
    assert!((color.red() - 1.0).abs() < 0.01);
    assert!((color.green() - 1.0).abs() < 0.01);
    assert!((color.blue() - 1.0).abs() < 0.01);
    assert!((color.alpha() - 1.0).abs() < 0.01);
  }

  #[test]
  fn test_parse_with_alpha()
  {
    let color = parse_hex_color("#ff0000ff").unwrap();
    assert!((color.red() - 1.0).abs() < 0.01);
    assert!((color.green() - 0.0).abs() < 0.01);
    assert!((color.blue() - 0.0).abs() < 0.01);
    assert!((color.alpha() - 1.0).abs() < 0.01);
  }

  #[test]
  fn test_parse_with_half_alpha()
  {
    let color = parse_hex_color("#ff000080").unwrap();
    assert!((color.red() - 1.0).abs() < 0.01);
    assert!((color.green() - 0.0).abs() < 0.01);
    assert!((color.blue() - 0.0).abs() < 0.01);
    let expected_alpha = 0x80 as f32 / 255.0;
    assert!((color.alpha() - expected_alpha).abs() < 0.01);
  }

  #[test]
  fn test_no_hash_prefix()
  {
    assert!(parse_hex_color("ff0000").is_err());
  }

  #[test]
  fn test_invalid_length()
  {
    assert!(parse_hex_color("#fff").is_err());
    assert!(parse_hex_color("#fffff").is_err());
    assert!(parse_hex_color("#fffffffff").is_err());
  }

  #[test]
  fn test_invalid_hex_digits()
  {
    assert!(parse_hex_color("#gggggg").is_err());
    assert!(parse_hex_color("#zzzzzzz").is_err());
  }

  #[test]
  fn test_case_insensitive()
  {
    let color1 = parse_hex_color("#FF0000").unwrap();
    let color2 = parse_hex_color("#ff0000").unwrap();
    assert!((color1.red() - color2.red()).abs() < 0.01);
    assert!((color1.green() - color2.green()).abs() < 0.01);
    assert!((color1.blue() - color2.blue()).abs() < 0.01);
  }
}