From 5f3cbd9f33b7d2fa18dcb4747a620d3de4ea7861 Mon Sep 17 00:00:00 2001 From: Louis <70532216+l0uisgrange@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:23:15 +0200 Subject: [PATCH 01/11] added function --- color/src/lib.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/color/src/lib.rs b/color/src/lib.rs index a7fbb20..f8603dd 100644 --- a/color/src/lib.rs +++ b/color/src/lib.rs @@ -189,6 +189,15 @@ impl AlphaColor { let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), 1.]; Self::new(components) } + + /// Create a color from a hexadecimal value. + pub fn from_hex(hex: &str) -> Self { + let components = match parse_color(hex) { + Ok(c) => c, + Err(_) => return AlphaColor::WHITE, + }; + Self::new(components.components) + } } impl OpaqueColor { @@ -197,6 +206,15 @@ impl OpaqueColor { let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b)]; Self::new(components) } + + /// Create a color from a hexadecimal value. + pub fn from_hex(hex: &str) -> Self { + let components = match parse_color(hex) { + Ok(c) => c, + Err(_) => return OpaqueColor::WHITE, + }; + Self::new([components.components[0], components.components[1], components.components[2]]) + } } impl PremulColor { From 0633eb11d66b8216e94a395c0b683a689656c529 Mon Sep 17 00:00:00 2001 From: Louis <70532216+l0uisgrange@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:24:53 +0200 Subject: [PATCH 02/11] formatting --- color/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/color/src/lib.rs b/color/src/lib.rs index f8603dd..94a1f71 100644 --- a/color/src/lib.rs +++ b/color/src/lib.rs @@ -213,7 +213,11 @@ impl OpaqueColor { Ok(c) => c, Err(_) => return OpaqueColor::WHITE, }; - Self::new([components.components[0], components.components[1], components.components[2]]) + Self::new([ + components.components[0], + components.components[1], + components.components[2], + ]) } } From 4b66c10934f5b75e99a562de3133d820fd0a6e72 Mon Sep 17 00:00:00 2001 From: Louis <70532216+l0uisgrange@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:27:58 +0200 Subject: [PATCH 03/11] fixed CI --- color/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/color/src/lib.rs b/color/src/lib.rs index 94a1f71..1e76220 100644 --- a/color/src/lib.rs +++ b/color/src/lib.rs @@ -194,7 +194,7 @@ impl AlphaColor { pub fn from_hex(hex: &str) -> Self { let components = match parse_color(hex) { Ok(c) => c, - Err(_) => return AlphaColor::WHITE, + Err(_) => return Self::WHITE, }; Self::new(components.components) } @@ -211,7 +211,7 @@ impl OpaqueColor { pub fn from_hex(hex: &str) -> Self { let components = match parse_color(hex) { Ok(c) => c, - Err(_) => return OpaqueColor::WHITE, + Err(_) => return Self::WHITE, }; Self::new([ components.components[0], From 79ea2530a264cf35cbbaca8c6952ad687c901003 Mon Sep 17 00:00:00 2001 From: Louis <70532216+l0uisgrange@users.noreply.github.com> Date: Fri, 1 May 2026 13:05:49 +0200 Subject: [PATCH 04/11] fixed `from_hex` --- color/src/lib.rs | 29 +++++++++++++---------------- color/src/parse.rs | 4 ++-- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/color/src/lib.rs b/color/src/lib.rs index 1e76220..4a83b8f 100644 --- a/color/src/lib.rs +++ b/color/src/lib.rs @@ -109,6 +109,7 @@ mod impl_bytemuck; #[cfg(all(not(feature = "std"), not(test)))] mod floatfuncs; +use crate::parse::{color_from_4bit_hex, get_4bit_hex_channels}; pub use chromaticity::Chromaticity; pub use color::{AlphaColor, HueDirection, OpaqueColor, PremulColor}; pub use colorspace::{ @@ -191,12 +192,12 @@ impl AlphaColor { } /// Create a color from a hexadecimal value. - pub fn from_hex(hex: &str) -> Self { - let components = match parse_color(hex) { - Ok(c) => c, - Err(_) => return Self::WHITE, - }; - Self::new(components.components) + pub const fn from_hex(hex: &str) -> Result { + let bit_hex = get_4bit_hex_channels(hex); + match bit_hex { + Ok((_, channels)) => Ok(color_from_4bit_hex(channels)), + Err(e) => Err(ParseError::UnknownColorComponent), + } } } @@ -208,16 +209,12 @@ impl OpaqueColor { } /// Create a color from a hexadecimal value. - pub fn from_hex(hex: &str) -> Self { - let components = match parse_color(hex) { - Ok(c) => c, - Err(_) => return Self::WHITE, - }; - Self::new([ - components.components[0], - components.components[1], - components.components[2], - ]) + pub const fn from_hex(hex: &str) -> Result { + let bit_hex = get_4bit_hex_channels(hex); + match bit_hex { + Ok((_, channels)) => Ok(color_from_4bit_hex(channels).discard_alpha()), + Err(e) => Err(ParseError::UnknownColorComponent), + } } } diff --git a/color/src/parse.rs b/color/src/parse.rs index 61e2cbc..659b5b0 100644 --- a/color/src/parse.rs +++ b/color/src/parse.rs @@ -616,7 +616,7 @@ impl FromStr for PremulColor { /// /// 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; @@ -651,7 +651,7 @@ const fn hex_from_ascii_byte(b: u8) -> Result { } } -const fn color_from_4bit_hex(components: [u8; 8]) -> AlphaColor { +pub(crate) const fn color_from_4bit_hex(components: [u8; 8]) -> AlphaColor { let [r0, r1, g0, g1, b0, b1, a0, a1] = components; AlphaColor::from_rgba8( (r0 << 4) | r1, From 1bebf63af8bd6900b1c689bdbe219289cfa2d3d8 Mon Sep 17 00:00:00 2001 From: Louis <70532216+l0uisgrange@users.noreply.github.com> Date: Fri, 1 May 2026 13:08:06 +0200 Subject: [PATCH 05/11] oops forgot a fix --- color/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/color/src/lib.rs b/color/src/lib.rs index 4a83b8f..8dcf822 100644 --- a/color/src/lib.rs +++ b/color/src/lib.rs @@ -196,7 +196,7 @@ impl AlphaColor { let bit_hex = get_4bit_hex_channels(hex); match bit_hex { Ok((_, channels)) => Ok(color_from_4bit_hex(channels)), - Err(e) => Err(ParseError::UnknownColorComponent), + Err(e) => Err(e), } } } @@ -213,7 +213,7 @@ impl OpaqueColor { let bit_hex = get_4bit_hex_channels(hex); match bit_hex { Ok((_, channels)) => Ok(color_from_4bit_hex(channels).discard_alpha()), - Err(e) => Err(ParseError::UnknownColorComponent), + Err(e) => Err(e), } } } From c5a35db35d21df821865dd1e13283ccdcfe2291c Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Wed, 6 May 2026 10:15:37 +1000 Subject: [PATCH 06/11] Expand `from_hex` to handle leading `#` and improve docs --- color/src/lib.rs | 195 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 189 insertions(+), 6 deletions(-) diff --git a/color/src/lib.rs b/color/src/lib.rs index 8dcf822..b66a31f 100644 --- a/color/src/lib.rs +++ b/color/src/lib.rs @@ -191,11 +191,61 @@ impl AlphaColor { Self::new(components) } - /// Create a color from a hexadecimal value. - pub const fn from_hex(hex: &str) -> Result { + /// Create an sRGB color from a hexadecimal string, such as `"#8a2be2"` (). + /// + /// 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 = 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 { + 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"` (). + /// + /// 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 { + // 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((_, channels)) => Ok(color_from_4bit_hex(channels)), + Ok((count, channels)) => { + if count != hex.len() { + return Err(ParseError::ExpectedEndOfString); + } + Ok(color_from_4bit_hex(channels)) + } Err(e) => Err(e), } } @@ -208,11 +258,64 @@ impl OpaqueColor { Self::new(components) } - /// Create a color from a hexadecimal value. - pub const fn from_hex(hex: &str) -> Result { + /// Create an sRGB color from a hexadecimal string, such as `"#8a2be2"` (). + /// + /// 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 = 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"` (). + /// + /// 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 { + // 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((_, channels)) => Ok(color_from_4bit_hex(channels).discard_alpha()), + Ok((count, channels)) => { + if count != hex.len() { + return Err(ParseError::ExpectedEndOfString); + } + if count != 3 && count != 6 { + return Err(ParseError::WrongNumberOfHexDigits); + } + Ok(color_from_4bit_hex(channels).discard_alpha()) + } Err(e) => Err(e), } } @@ -242,3 +345,83 @@ impl PremulColor { fn ensure_libm_dependency_used() -> f32 { libm::sqrtf(4_f32) } + +#[cfg(test)] +mod tests { + use super::*; + + const ALPHA_FROM_HEX_IS_CONST: AlphaColor = AlphaColor::from_hex("#8a2be2"); + #[test] + 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::::try_from_hex("#gg0000").is_err()); + // 5 digit color isn't defined. + assert!(AlphaColor::::try_from_hex("#12345").is_err()); + assert!(AlphaColor::::try_from_hex("").is_err()); + } + + const OPAQUE_FROM_HEX_IS_CONST: OpaqueColor = 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 short_alpha = OpaqueColor::from_hex("#abcd"); + let long_alpha = OpaqueColor::from_hex("#aabbccdd"); + assert_eq!(short_alpha, long_alpha); + + let lower = OpaqueColor::from_hex("#8a2be28f"); + let upper = OpaqueColor::from_hex("#8A2BE28F"); + 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()); + } +} From 5b8b1b9f65abac5853deb185715ef13ccaaedd3e Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Wed, 6 May 2026 10:26:37 +1000 Subject: [PATCH 07/11] Fixup test --- color/src/lib.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/color/src/lib.rs b/color/src/lib.rs index b66a31f..0dba272 100644 --- a/color/src/lib.rs +++ b/color/src/lib.rs @@ -404,12 +404,8 @@ mod tests { let long = OpaqueColor::from_hex("#aabbcc"); assert_eq!(short, long); - let short_alpha = OpaqueColor::from_hex("#abcd"); - let long_alpha = OpaqueColor::from_hex("#aabbccdd"); - assert_eq!(short_alpha, long_alpha); - - let lower = OpaqueColor::from_hex("#8a2be28f"); - let upper = OpaqueColor::from_hex("#8A2BE28F"); + let lower = OpaqueColor::from_hex("#8a2be2"); + let upper = OpaqueColor::from_hex("#8A2BE2"); assert_eq!(lower, upper); } #[test] From ab28c5653ef885957d0f990b82aeb33a7ce3167f Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Wed, 6 May 2026 10:30:30 +1000 Subject: [PATCH 08/11] Also update the changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a4080d..6017ac5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ This release has an [MSRV][] of 1.86. ### Added +- `AlphaColor::::from_hex` and `OpaqueColor::::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 @@ -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 @@ -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 From 11830d2c169972b3f207bdee29063d81877f873c Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Thu, 21 May 2026 14:46:28 +0200 Subject: [PATCH 09/11] Add to `Rgba8` as well --- color/src/lib.rs | 36 +++++------------ color/src/parse.rs | 11 ++++++ color/src/rgba8.rs | 98 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 117 insertions(+), 28 deletions(-) diff --git a/color/src/lib.rs b/color/src/lib.rs index 0dba272..00e1497 100644 --- a/color/src/lib.rs +++ b/color/src/lib.rs @@ -215,38 +215,17 @@ impl AlphaColor { /// /// 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!() - } - } + 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"` (). /// /// 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 { - // 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(color_from_4bit_hex(channels)) - } - Err(e) => Err(e), + pub const fn try_from_hex(hex: &str) -> Result { + match Rgba8::try_from_hex(hex) { + Ok(it) => Ok(Self::from_rgba8(it.r, it.g, it.b, it.a)), + Err(err) => Err(err), } } } @@ -351,6 +330,7 @@ mod tests { use super::*; const ALPHA_FROM_HEX_IS_CONST: AlphaColor = AlphaColor::from_hex("#8a2be2"); + #[test] fn alpha_from_hex() { let color = AlphaColor::from_hex("#8a2be2"); @@ -387,6 +367,7 @@ mod tests { } const OPAQUE_FROM_HEX_IS_CONST: OpaqueColor = OpaqueColor::from_hex("#8a2be2"); + #[test] fn opaque_from_hex() { let color = OpaqueColor::from_hex("#8a2be2"); @@ -408,6 +389,7 @@ mod tests { let upper = OpaqueColor::from_hex("#8A2BE2"); assert_eq!(lower, upper); } + #[test] fn opaque_try_from_hex_errors() { // 'g' is not a valid hex digit diff --git a/color/src/parse.rs b/color/src/parse.rs index 659b5b0..bb3b12b 100644 --- a/color/src/parse.rs +++ b/color/src/parse.rs @@ -9,6 +9,7 @@ use core::fmt; use core::str; use core::str::FromStr; +use crate::Rgba8; use crate::{ AlphaColor, ColorSpace, ColorSpaceTag, DynamicColor, Flags, Missing, OpaqueColor, PremulColor, Srgb, @@ -661,6 +662,16 @@ pub(crate) const fn color_from_4bit_hex(components: [u8; 8]) -> AlphaColor ) } +pub(crate) const fn rgba_from_4bit_hex(components: [u8; 8]) -> Rgba8 { + let [r0, r1, g0, g1, b0, b1, a0, a1] = components; + Rgba8 { + r: (r0 << 4) | r1, + g: (g0 << 4) | g1, + b: (b0 << 4) | b1, + a: (a0 << 4) | a1, + } +} + impl FromStr for ColorSpaceTag { type Err = ParseError; diff --git a/color/src/rgba8.rs b/color/src/rgba8.rs index 2aab5ec..416558e 100644 --- a/color/src/rgba8.rs +++ b/color/src/rgba8.rs @@ -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. /// @@ -62,6 +65,65 @@ 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"` (). + /// + /// 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 = 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 { + 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"` (). + /// + /// 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 { + // 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 for AlphaColor { @@ -275,4 +337,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()); + } } From 1fb5dd03af065ee77ab209a02dfb028dee3d20a1 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Thu, 21 May 2026 15:02:46 +0200 Subject: [PATCH 10/11] Fixup the duplication --- color/src/lib.rs | 5 +++-- color/src/parse.rs | 13 +------------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/color/src/lib.rs b/color/src/lib.rs index 00e1497..ac61741 100644 --- a/color/src/lib.rs +++ b/color/src/lib.rs @@ -109,7 +109,7 @@ mod impl_bytemuck; #[cfg(all(not(feature = "std"), not(test)))] mod floatfuncs; -use crate::parse::{color_from_4bit_hex, get_4bit_hex_channels}; +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::{ @@ -293,7 +293,8 @@ impl OpaqueColor { if count != 3 && count != 6 { return Err(ParseError::WrongNumberOfHexDigits); } - Ok(color_from_4bit_hex(channels).discard_alpha()) + let this = rgba_from_4bit_hex(channels); + Ok(Self::from_rgb8(this.r, this.g, this.b)) } Err(e) => Err(e), } diff --git a/color/src/parse.rs b/color/src/parse.rs index bb3b12b..5a03735 100644 --- a/color/src/parse.rs +++ b/color/src/parse.rs @@ -12,7 +12,6 @@ use core::str::FromStr; use crate::Rgba8; use crate::{ AlphaColor, ColorSpace, ColorSpaceTag, DynamicColor, Flags, Missing, OpaqueColor, PremulColor, - Srgb, }; // TODO: maybe include string offset @@ -509,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); @@ -652,16 +651,6 @@ const fn hex_from_ascii_byte(b: u8) -> Result { } } -pub(crate) const fn color_from_4bit_hex(components: [u8; 8]) -> AlphaColor { - 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, - ) -} - pub(crate) const fn rgba_from_4bit_hex(components: [u8; 8]) -> Rgba8 { let [r0, r1, g0, g1, b0, b1, a0, a1] = components; Rgba8 { From dafa94d5aa0ffc1cd2992c2450af667a99838eef Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Thu, 21 May 2026 15:15:03 +0200 Subject: [PATCH 11/11] Fixup docs --- color/src/rgba8.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/color/src/rgba8.rs b/color/src/rgba8.rs index 416558e..5b699d6 100644 --- a/color/src/rgba8.rs +++ b/color/src/rgba8.rs @@ -69,21 +69,22 @@ impl Rgba8 { /// Create an sRGB color from a hexadecimal string, such as `"#8a2be2"` (). /// /// 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. + /// 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`], which covers a wider variety of input forms in CSS syntax, or + /// [`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::{AlphaColor, Srgb}; - /// const BUTTON_COLOR: AlphaColor = AlphaColor::from_hex("#8a2be2"); + /// # use color::Rgba8; + /// const BUTTON_COLOR: Rgba8 = Rgba8::from_hex("#8a2be2"); /// ``` /// /// # Panics