Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This release has an [MSRV][] of 1.86.

### Added

- `AlphaColor::<Srgb>::from_hex` and `OpaqueColor::<Srgb>::from_hex`, which can be used to convert const colors from an in-editor color picker. ([#217][] by [@l0uisgrange][] and [@DJMcNab][])
- Add a `const` `DynamicColor::new` constructor for convenience, taking a color space tag and color components, and setting default `Flags`. ([#219][] by [@tomcur][])

### Changed
Expand Down Expand Up @@ -175,6 +176,7 @@ This is the initial release.
[@ajakubowicz-canva]: https://github.com/ajakubowicz-canva
[@alvinisspicy]: https://github.com/alvinisspicy
[@DJMcNab]: https://github.com/DJMcNab
[@l0uisgrange]: https://github.com/l0uisgrange
[@LaurenzV]: https://github.com/LaurenzV
[@MightyBurger]: https://github.com/MightyBurger
[@raphlinus]: https://github.com/raphlinus
Expand Down Expand Up @@ -234,6 +236,7 @@ This is the initial release.
[#202]: https://github.com/linebender/color/pull/202
[#210]: https://github.com/linebender/color/pull/210
[#211]: https://github.com/linebender/color/pull/211
[#217]: https://github.com/linebender/color/pull/217
[#218]: https://github.com/linebender/color/pull/218
[#219]: https://github.com/linebender/color/pull/219

Expand Down
181 changes: 181 additions & 0 deletions color/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ mod impl_bytemuck;
#[cfg(all(not(feature = "std"), not(test)))]
mod floatfuncs;

use crate::parse::{get_4bit_hex_channels, rgba_from_4bit_hex};
pub use chromaticity::Chromaticity;
pub use color::{AlphaColor, HueDirection, OpaqueColor, PremulColor};
pub use colorspace::{
Expand Down Expand Up @@ -189,6 +190,44 @@ impl AlphaColor<Srgb> {
let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), 1.];
Self::new(components)
}

/// Create an sRGB color from a hexadecimal string, such as `"#8a2be2"` (<span style="background-color:#8a2be2;padding:0 0.7em;border:1px solid"></span>).
///
/// Certain code editors may provide a color picker for input strings of this format, making this method
/// preferable to [`from_rgb8`](`Self::from_rgb8`) or [`from_rgba8`](`Self::from_rgba8`) for colors which may need to be experimented with.
///
/// The leading `#` in the input is optional, but it is recommended to include it.
/// The input is provided in RGBA order, and valid inputs are of the form `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`.
/// `A-F` in the input string may be upper or lowercase.
///
/// This function is designed for use in const contexts; for user-provided values, you can use
/// [`parse_color`], which covers a wider variety of input forms in CSS syntax, or
/// [`try_from_hex`](Self::try_from_hex) which returns an error instead of panicking.
///
/// # Example
///
/// ```
/// # use color::{AlphaColor, Srgb};
/// const BUTTON_COLOR: AlphaColor<Srgb> = AlphaColor::from_hex("#8a2be2");
/// ```
///
/// # Panics
///
/// If the input string contains anything other than an optional `#` and 3, 4, 6, or 8 hexadecimal digits.
pub const fn from_hex(hex: &str) -> Self {
let this = Rgba8::from_hex(hex);
Self::from_rgba8(this.r, this.g, this.b, this.a)
}

/// Create a color from a hexadecimal string, such as `"#8a2be2"` (<span style="background-color:#8a2be2;padding:0 0.7em;border:1px solid"></span>).
///
/// Same as [`from_hex`](Self::from_hex), but returns an error in cases where that method panics.
pub const fn try_from_hex(hex: &str) -> Result<Self, ParseError> {
match Rgba8::try_from_hex(hex) {
Ok(it) => Ok(Self::from_rgba8(it.r, it.g, it.b, it.a)),
Err(err) => Err(err),
}
}
}

impl OpaqueColor<Srgb> {
Expand All @@ -197,6 +236,69 @@ impl OpaqueColor<Srgb> {
let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b)];
Self::new(components)
}

/// Create an sRGB color from a hexadecimal string, such as `"#8a2be2"` (<span style="background-color:#8a2be2;padding:0 0.7em;border:1px solid"></span>).
///
/// Certain code editors may provide a color picker for input strings of this format, making this method
/// preferable to [`from_rgb8`](`Self::from_rgb8`) for colors which may need to be experimented with.
///
/// The leading `#` in the input is optional, but it is recommended to include it.
/// The input is provided in RGBA order, and valid inputs are of the form `#RGB`, `#RRGGBB` or `#RRGGBB`.
/// `A-F` in the input string may be upper or lowercase.
///
/// This function is designed for use in const contexts; for user-provided values, you can use
/// [`parse_color`], which covers a wider variety of input forms in CSS syntax, or
/// [`try_from_hex`](Self::try_from_hex) which returns an error instead of panicking.
///
/// # Example
///
/// ```
/// # use color::{OpaqueColor, Srgb};
/// const BUTTON_COLOR: OpaqueColor<Srgb> = OpaqueColor::from_hex("#8a2be2");
/// ```
///
/// # Panics
///
/// If the input string contains anything other than an optional `#` and 3, or 6 hexadecimal digits.
pub const fn from_hex(hex: &str) -> Self {
match Self::try_from_hex(hex) {
Ok(color) => color,
Err(ParseError::WrongNumberOfHexDigits) => {
panic!("An invalid number of hexadecimal digits was provided.");
}
Err(ParseError::ExpectedEndOfString) => {
panic!("Input to from_hex contains characters after hexadecimal digits.");
}
Err(_) => {
unreachable!()
}
}
}

/// Create a color from a hexadecimal string, such as `"#8a2be2"` (<span style="background-color:#8a2be2;padding:0 0.7em;border:1px solid"></span>).
///
/// Same as [`from_hex`](Self::from_hex), but returns an error in cases where that method panics.
pub const fn try_from_hex(mut hex: &str) -> Result<Self, ParseError> {
// Strip an optional '#' from the start. We can't use `strip_prefix` as it isn't const.
if !hex.is_empty() && hex.as_bytes()[0] == b'#' {
hex = hex.split_at(1).1;
}

let bit_hex = get_4bit_hex_channels(hex);
match bit_hex {
Ok((count, channels)) => {
if count != hex.len() {
return Err(ParseError::ExpectedEndOfString);
}
if count != 3 && count != 6 {
return Err(ParseError::WrongNumberOfHexDigits);
}
let this = rgba_from_4bit_hex(channels);
Ok(Self::from_rgb8(this.r, this.g, this.b))
}
Err(e) => Err(e),
}
}
}

impl PremulColor<Srgb> {
Expand All @@ -223,3 +325,82 @@ impl PremulColor<Srgb> {
fn ensure_libm_dependency_used() -> f32 {
libm::sqrtf(4_f32)
}

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

const ALPHA_FROM_HEX_IS_CONST: AlphaColor<Srgb> = AlphaColor::from_hex("#8a2be2");

#[test]
Comment thread
DJMcNab marked this conversation as resolved.
fn alpha_from_hex() {
let color = AlphaColor::from_hex("#8a2be2");
assert_eq!(
color.to_rgba8(),
Rgba8::from_u8_array([0x8a, 0x2b, 0xe2, 0xff])
);
assert_eq!(color, ALPHA_FROM_HEX_IS_CONST);

let with = AlphaColor::from_hex("#aabbcc");
let without = AlphaColor::from_hex("aabbcc");
assert_eq!(with, without);

let short = AlphaColor::from_hex("#abc");
let long = AlphaColor::from_hex("#aabbcc");
assert_eq!(short, long);

let short_alpha = AlphaColor::from_hex("#abcd");
let long_alpha = AlphaColor::from_hex("#aabbccdd");
assert_eq!(short_alpha, long_alpha);

let lower = AlphaColor::from_hex("#8a2be28f");
let upper = AlphaColor::from_hex("#8A2BE28F");
assert_eq!(lower, upper);
}

#[test]
fn alpha_try_from_hex_errors() {
// 'g' is not a valid hex digit
assert!(AlphaColor::<Srgb>::try_from_hex("#gg0000").is_err());
// 5 digit color isn't defined.
assert!(AlphaColor::<Srgb>::try_from_hex("#12345").is_err());
assert!(AlphaColor::<Srgb>::try_from_hex("").is_err());
}

const OPAQUE_FROM_HEX_IS_CONST: OpaqueColor<Srgb> = OpaqueColor::from_hex("#8a2be2");

#[test]
fn opaque_from_hex() {
let color = OpaqueColor::from_hex("#8a2be2");
assert_eq!(
color.to_rgba8(),
Rgba8::from_u8_array([0x8a, 0x2b, 0xe2, 0xff])
);
assert_eq!(color, OPAQUE_FROM_HEX_IS_CONST);

let with = OpaqueColor::from_hex("#aabbcc");
let without = OpaqueColor::from_hex("aabbcc");
assert_eq!(with, without);

let short = OpaqueColor::from_hex("#abc");
let long = OpaqueColor::from_hex("#aabbcc");
assert_eq!(short, long);

let lower = OpaqueColor::from_hex("#8a2be2");
let upper = OpaqueColor::from_hex("#8A2BE2");
assert_eq!(lower, upper);
}

#[test]
fn opaque_try_from_hex_errors() {
// 'g' is not a valid hex digit
assert!(OpaqueColor::try_from_hex("#gg0000").is_err());
// 5 digit color isn't defined.
assert!(OpaqueColor::try_from_hex("#12345").is_err());
// 4 digit color isn't allowed for an opaque color.
assert!(OpaqueColor::try_from_hex("#123f").is_err());
// 8 digit color isn't allowed for an opaque color.
assert!(OpaqueColor::try_from_hex("#12233480").is_err());
assert!(OpaqueColor::try_from_hex("").is_err());
}
}
20 changes: 10 additions & 10 deletions color/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ use core::fmt;
use core::str;
use core::str::FromStr;

use crate::Rgba8;
use crate::{
AlphaColor, ColorSpace, ColorSpaceTag, DynamicColor, Flags, Missing, OpaqueColor, PremulColor,
Srgb,
};

// TODO: maybe include string offset
Expand Down Expand Up @@ -508,7 +508,7 @@ pub fn parse_color_prefix(s: &str) -> Result<(usize, DynamicColor), ParseError>

if let Some(stripped) = s.strip_prefix('#') {
let (ix, channels) = get_4bit_hex_channels(stripped)?;
let color = color_from_4bit_hex(channels);
let color = rgba_from_4bit_hex(channels).into();
// Hex colors are seen as if they are generated from the named `rgb()` color space
// function.
let mut color = DynamicColor::from_alpha_color(color);
Expand Down Expand Up @@ -616,7 +616,7 @@ impl<CS: ColorSpace> FromStr for PremulColor<CS> {
///
/// Returns the parsed channels and the byte offset to the remainder of the string (i.e., the
/// number of hex characters parsed).
const fn get_4bit_hex_channels(hex_str: &str) -> Result<(usize, [u8; 8]), ParseError> {
pub(crate) const fn get_4bit_hex_channels(hex_str: &str) -> Result<(usize, [u8; 8]), ParseError> {
let mut hex = [0; 8];

let mut i = 0;
Expand Down Expand Up @@ -651,14 +651,14 @@ const fn hex_from_ascii_byte(b: u8) -> Result<u8, ()> {
}
}

const fn color_from_4bit_hex(components: [u8; 8]) -> AlphaColor<Srgb> {
pub(crate) const fn rgba_from_4bit_hex(components: [u8; 8]) -> Rgba8 {
let [r0, r1, g0, g1, b0, b1, a0, a1] = components;
AlphaColor::from_rgba8(
(r0 << 4) | r1,
(g0 << 4) | g1,
(b0 << 4) | b1,
(a0 << 4) | a1,
)
Rgba8 {
r: (r0 << 4) | r1,
g: (g0 << 4) | g1,
b: (b0 << 4) | b1,
a: (a0 << 4) | a1,
}
}

impl FromStr for ColorSpaceTag {
Expand Down
99 changes: 98 additions & 1 deletion color/src/rgba8.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Copyright 2024 the Color Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

use crate::{AlphaColor, PremulColor, Srgb};
use crate::{
AlphaColor, ParseError, PremulColor, Srgb,
parse::{get_4bit_hex_channels, rgba_from_4bit_hex},
};

/// A packed representation of sRGB colors.
///
Expand Down Expand Up @@ -62,6 +65,66 @@ impl Rgba8 {
pub const fn from_u32(packed_bytes: u32) -> Self {
Self::from_u8_array(u32::to_ne_bytes(packed_bytes))
}

/// Create an sRGB color from a hexadecimal string, such as `"#8a2be2"` (<span style="background-color:#8a2be2;padding:0 0.7em;border:1px solid"></span>).
///
/// Certain code editors may provide a color picker for input strings of this format, making this method
/// preferable to [`from_u8_array`](`Self::from_u8_array`) or [`from_u8_array`](`Self::from_u8_array`) for colors
/// which may need to be experimented with.
///
/// The leading `#` in the input is optional, but it is recommended to include it.
/// The input is provided in RGBA order, and valid inputs are of the form `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`.
/// `A-F` in the input string may be upper or lowercase.
///
/// This function is designed for use in const contexts; for user-provided values, you can use
/// [`parse_color`](crate::parse_color), which covers a wider variety of input forms in CSS syntax, or
/// [`try_from_hex`](Self::try_from_hex) which returns an error instead of panicking.
///
/// # Example
///
/// ```
/// # use color::Rgba8;
/// const BUTTON_COLOR: Rgba8 = Rgba8::from_hex("#8a2be2");
/// ```
///
/// # Panics
///
/// If the input string contains anything other than an optional `#` and 3, 4, 6, or 8 hexadecimal digits.
pub const fn from_hex(hex: &str) -> Self {
match Self::try_from_hex(hex) {
Ok(color) => color,
Err(ParseError::WrongNumberOfHexDigits) => {
panic!("An invalid number of hexadecimal digits was provided.");
}
Err(ParseError::ExpectedEndOfString) => {
panic!("Input to from_hex contains characters after hexadecimal digits.");
}
Err(_) => {
unreachable!()
}
}
}

/// Create a color from a hexadecimal string, such as `"#8a2be2"` (<span style="background-color:#8a2be2;padding:0 0.7em;border:1px solid"></span>).
///
/// Same as [`from_hex`](Self::from_hex), but returns an error in cases where that method panics.
pub const fn try_from_hex(mut hex: &str) -> Result<Self, ParseError> {
// Strip an optional '#' from the start. We can't use `strip_prefix` as it isn't const.
if !hex.is_empty() && hex.as_bytes()[0] == b'#' {
hex = hex.split_at(1).1;
}

let bit_hex = get_4bit_hex_channels(hex);
match bit_hex {
Ok((count, channels)) => {
if count != hex.len() {
return Err(ParseError::ExpectedEndOfString);
}
Ok(rgba_from_4bit_hex(channels))
}
Err(e) => Err(e),
}
}
}

impl From<Rgba8> for AlphaColor<Srgb> {
Expand Down Expand Up @@ -275,4 +338,38 @@ mod tests {
let p = [0xaa, 0xbb, 0xcc, 0xff];
assert_eq!(PremulRgba8::from_u8_array(p), bytemuck::cast(p));
}

const RGBA8_FROM_HEX_IS_CONST: Rgba8 = Rgba8::from_hex("#8a2be2");

#[test]
fn rgba8_from_hex() {
let color = Rgba8::from_hex("#8a2be2");
assert_eq!(color, Rgba8::from_u8_array([0x8a, 0x2b, 0xe2, 0xff]));
assert_eq!(color, RGBA8_FROM_HEX_IS_CONST);

let with = Rgba8::from_hex("#aabbcc");
let without = Rgba8::from_hex("aabbcc");
assert_eq!(with, without);

let short = Rgba8::from_hex("#abc");
let long = Rgba8::from_hex("#aabbcc");
assert_eq!(short, long);

let short_alpha = Rgba8::from_hex("#abcd");
let long_alpha = Rgba8::from_hex("#aabbccdd");
assert_eq!(short_alpha, long_alpha);

let lower = Rgba8::from_hex("#8a2be28f");
let upper = Rgba8::from_hex("#8A2BE28F");
assert_eq!(lower, upper);
}

#[test]
fn rgba8_try_from_hex_errors() {
// 'g' is not a valid hex digit
assert!(Rgba8::try_from_hex("#gg0000").is_err());
// 5 digit color isn't defined.
assert!(Rgba8::try_from_hex("#12345").is_err());
assert!(Rgba8::try_from_hex("").is_err());
}
}
Loading