#![cfg_attr(not(feature = "std"), no_std)]
#![doc(html_root_url = "https://docs.rs/hex_color/3.0.0")]
#![cfg_attr(doc_cfg, feature(doc_cfg))]
#![deny(missing_docs)]
#![deny(clippy::pedantic)]
#![allow(clippy::many_single_char_names, clippy::similar_names)]
#[cfg(feature = "rand")]
mod rand;
#[cfg(feature = "serde")]
mod serde;
use core::fmt;
use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
use core::str::{Bytes, FromStr};
#[cfg(feature = "serde")]
#[doc(inline)]
pub use self::serde::{rgb, rgba, u24, u32};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct HexColor {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl HexColor {
pub const BLACK: HexColor = HexColor::rgb(0, 0, 0);
pub const BLUE: HexColor = HexColor::rgb(0, 0, 255);
pub const CLEAR: HexColor = HexColor::rgba(0, 0, 0, 0);
pub const CYAN: HexColor = HexColor::rgb(0, 255, 255);
pub const GRAY: HexColor = HexColor::achromatic(128);
pub const GREEN: HexColor = HexColor::rgb(0, 255, 0);
pub const GREY: HexColor = HexColor::achromatic(128);
pub const MAGENTA: HexColor = HexColor::rgb(255, 0, 255);
pub const MAX: HexColor = HexColor::from_u32(0xFFFF_FFFF);
pub const MIN: HexColor = HexColor::from_u32(0x0000_0000);
pub const RED: HexColor = HexColor::rgb(255, 0, 0);
pub const WHITE: HexColor = HexColor::rgb(255, 255, 255);
pub const YELLOW: HexColor = HexColor::rgb(255, 255, 0);
#[must_use]
#[inline]
pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> HexColor {
HexColor { r, g, b, a }
}
#[must_use]
#[inline]
pub const fn rgb(r: u8, g: u8, b: u8) -> HexColor {
HexColor { r, g, b, a: 255 }
}
#[must_use]
#[inline]
pub const fn achromatic(v: u8) -> HexColor {
HexColor::rgb(v, v, v)
}
#[cfg(all(feature = "rand", feature = "std"))]
#[cfg_attr(doc_cfg, doc(cfg(all(feature = "rand", feature = "std"))))]
#[must_use]
#[inline]
pub fn random_rgb() -> Self {
::rand::random()
}
#[cfg(all(feature = "rand", feature = "std"))]
#[cfg_attr(doc_cfg, doc(cfg(all(feature = "rand", feature = "std"))))]
#[must_use]
#[inline]
pub fn random_rgba() -> Self {
HexColor::random_rgb().with_a(::rand::random())
}
#[must_use]
unsafe fn parse_shorthand(bytes: &mut Bytes, has_alpha: bool) -> Option<HexColor> {
unsafe fn parse_single_hex_value(bytes: &mut Bytes) -> Option<u8> {
match bytes.next().unwrap_unchecked() {
b'0' => Some(0x00),
b'1' => Some(0x11),
b'2' => Some(0x22),
b'3' => Some(0x33),
b'4' => Some(0x44),
b'5' => Some(0x55),
b'6' => Some(0x66),
b'7' => Some(0x77),
b'8' => Some(0x88),
b'9' => Some(0x99),
b'a' | b'A' => Some(0xAA),
b'b' | b'B' => Some(0xBB),
b'c' | b'C' => Some(0xCC),
b'd' | b'D' => Some(0xDD),
b'e' | b'E' => Some(0xEE),
b'f' | b'F' => Some(0xFF),
_ => None,
}
}
let r = parse_single_hex_value(bytes)?;
let g = parse_single_hex_value(bytes)?;
let b = parse_single_hex_value(bytes)?;
let a = if has_alpha {
parse_single_hex_value(bytes)?
} else {
u8::MAX
};
Some(HexColor::rgba(r, g, b, a))
}
#[must_use]
unsafe fn parse_full(bytes: &mut Bytes, has_alpha: bool) -> Option<HexColor> {
const HEX_RADIX: u32 = 16;
unsafe fn parse_double_hex_value(bytes: &mut Bytes) -> Option<u8> {
let buf = [
bytes.next().unwrap_unchecked(),
bytes.next().unwrap_unchecked(),
];
let s = core::str::from_utf8_unchecked(&buf);
u8::from_str_radix(s, HEX_RADIX).ok()
}
let r = parse_double_hex_value(bytes)?;
let g = parse_double_hex_value(bytes)?;
let b = parse_double_hex_value(bytes)?;
let a = if has_alpha {
parse_double_hex_value(bytes)?
} else {
u8::MAX
};
Some(HexColor::rgba(r, g, b, a))
}
fn parse_internals(s: &str, mode: ParseMode) -> Result<HexColor, ParseHexColorError> {
macro_rules! err {
($variant:ident) => {{
return Err(ParseHexColorError::$variant);
}};
}
let mut bytes = s.bytes();
match bytes.next() {
Some(b'#') => {}
Some(_) => err!(InvalidFormat),
None => err!(Empty),
}
let has_alpha = matches!(s.len(), 5 | 9);
let opt = match (s.len(), mode) {
(4, ParseMode::Rgb | ParseMode::Any) | (5, ParseMode::Rgba | ParseMode::Any) => {
unsafe { HexColor::parse_shorthand(&mut bytes, has_alpha) }
}
(7, ParseMode::Rgb | ParseMode::Any) | (9, ParseMode::Rgba | ParseMode::Any) => {
unsafe { HexColor::parse_full(&mut bytes, has_alpha) }
}
_ => err!(InvalidFormat),
};
opt.ok_or(ParseHexColorError::InvalidDigit)
}
#[inline]
pub fn parse(s: &str) -> Result<HexColor, ParseHexColorError> {
HexColor::parse_internals(s, ParseMode::Any)
}
#[inline]
pub fn parse_rgb(s: &str) -> Result<HexColor, ParseHexColorError> {
HexColor::parse_internals(s, ParseMode::Rgb)
}
#[inline]
pub fn parse_rgba(s: &str) -> Result<HexColor, ParseHexColorError> {
HexColor::parse_internals(s, ParseMode::Rgba)
}
#[must_use]
#[inline]
#[track_caller]
pub const fn from_u24(n: u32) -> HexColor {
debug_assert!(n <= 0x00FF_FFFF);
let [_, r, g, b] = n.to_be_bytes();
HexColor::rgb(r, g, b)
}
#[must_use]
#[inline]
pub const fn from_u32(v: u32) -> HexColor {
let [r, g, b, a] = v.to_be_bytes();
HexColor::rgba(r, g, b, a)
}
#[inline]
#[must_use]
pub const fn to_u24(self) -> u32 {
let (r, g, b) = self.split_rgb();
u32::from_be_bytes([0x00, r, g, b])
}
#[must_use]
#[inline]
pub const fn to_u32(self) -> u32 {
let (r, g, b, a) = self.split_rgba();
u32::from_be_bytes([r, g, b, a])
}
#[must_use]
#[inline]
pub const fn to_be_bytes(self) -> [u8; 4] {
[self.r, self.g, self.b, self.a]
}
#[must_use]
#[inline]
pub const fn to_le_bytes(self) -> [u8; 4] {
[self.a, self.b, self.g, self.r]
}
#[must_use]
#[inline]
pub const fn with_r(self, r: u8) -> HexColor {
let (_, g, b, a) = self.split_rgba();
HexColor::rgba(r, g, b, a)
}
#[must_use]
#[inline]
pub const fn with_g(self, g: u8) -> HexColor {
let (r, _, b, a) = self.split_rgba();
HexColor::rgba(r, g, b, a)
}
#[must_use]
#[inline]
pub const fn with_b(self, b: u8) -> HexColor {
let (r, g, _, a) = self.split_rgba();
HexColor::rgba(r, g, b, a)
}
#[must_use]
#[inline]
pub const fn with_a(self, a: u8) -> HexColor {
let (r, g, b) = self.split_rgb();
HexColor::rgba(r, g, b, a)
}
#[must_use]
#[inline]
pub const fn split_rgba(self) -> (u8, u8, u8, u8) {
let HexColor { r, g, b, a } = self;
(r, g, b, a)
}
#[must_use]
#[inline]
pub const fn split_rgb(self) -> (u8, u8, u8) {
let HexColor { r, g, b, .. } = self;
(r, g, b)
}
#[must_use]
#[inline]
pub const fn display_rgb(self) -> Display {
Display::new(self).with_alpha(Alpha::Hidden)
}
#[must_use]
#[inline]
pub const fn display_rgba(self) -> Display {
Display::new(self).with_alpha(Alpha::Visible)
}
#[inline]
#[must_use]
#[track_caller]
pub const fn add(self, rhs: HexColor) -> HexColor {
let (r1, g1, b1) = self.split_rgb();
let (r2, g2, b2) = rhs.split_rgb();
HexColor::rgb(r1 + r2, g1 + g2, b1 + b2)
}
#[inline]
#[must_use]
pub const fn checked_add(self, rhs: HexColor) -> Option<HexColor> {
let (res, flag) = self.overflowing_add(rhs);
if flag {
None
} else {
Some(res)
}
}
#[inline]
#[must_use]
pub const fn overflowing_add(self, rhs: HexColor) -> (HexColor, bool) {
let (r1, g1, b1) = self.split_rgb();
let (r2, g2, b2) = rhs.split_rgb();
let (r, r_flag) = r1.overflowing_add(r2);
let (g, g_flag) = g1.overflowing_add(g2);
let (b, b_flag) = b1.overflowing_add(b2);
(HexColor::rgb(r, g, b), r_flag || g_flag || b_flag)
}
#[inline]
#[must_use]
pub const fn saturating_add(self, rhs: HexColor) -> HexColor {
let (r1, g1, b1) = self.split_rgb();
let (r2, g2, b2) = rhs.split_rgb();
HexColor::rgb(
r1.saturating_add(r2),
g1.saturating_add(g2),
b1.saturating_add(b2),
)
}
#[inline]
#[must_use]
pub const fn wrapping_add(self, rhs: HexColor) -> HexColor {
let (r1, g1, b1) = self.split_rgb();
let (r2, g2, b2) = rhs.split_rgb();
HexColor::rgb(
r1.wrapping_add(r2),
g1.wrapping_add(g2),
b1.wrapping_add(b2),
)
}
#[inline]
#[must_use]
#[track_caller]
pub const fn sub(self, rhs: HexColor) -> HexColor {
let (r1, g1, b1) = self.split_rgb();
let (r2, g2, b2) = rhs.split_rgb();
HexColor::rgb(r1 - r2, g1 - g2, b1 - b2)
}
#[inline]
#[must_use]
#[track_caller]
pub const fn sub_scalar(self, n: u8) -> HexColor {
self.sub(HexColor::achromatic(n))
}
#[inline]
#[must_use]
pub const fn checked_sub(self, rhs: HexColor) -> Option<HexColor> {
let (res, flag) = self.overflowing_sub(rhs);
if flag {
None
} else {
Some(res)
}
}
#[inline]
#[must_use]
pub const fn overflowing_sub(self, rhs: HexColor) -> (HexColor, bool) {
let (r1, g1, b1) = self.split_rgb();
let (r2, g2, b2) = rhs.split_rgb();
let (r, r_flag) = r1.overflowing_sub(r2);
let (g, g_flag) = g1.overflowing_sub(g2);
let (b, b_flag) = b1.overflowing_sub(b2);
(HexColor::rgb(r, g, b), r_flag || g_flag || b_flag)
}
#[inline]
#[must_use]
pub const fn saturating_sub(self, rhs: HexColor) -> HexColor {
let (r1, g1, b1) = self.split_rgb();
let (r2, g2, b2) = rhs.split_rgb();
HexColor::rgb(
r1.saturating_sub(r2),
g1.saturating_sub(g2),
b1.saturating_sub(b2),
)
}
#[inline]
#[must_use]
pub const fn wrapping_sub(self, rhs: HexColor) -> HexColor {
let (r1, g1, b1) = self.split_rgb();
let (r2, g2, b2) = rhs.split_rgb();
HexColor::rgb(
r1.wrapping_sub(r2),
g1.wrapping_sub(g2),
b1.wrapping_sub(b2),
)
}
#[inline]
#[must_use]
#[track_caller]
pub const fn mul(self, rhs: HexColor) -> HexColor {
let (r1, g1, b1) = self.split_rgb();
let (r2, g2, b2) = rhs.split_rgb();
HexColor::rgb(r1 * r2, g1 * g2, b1 * b2)
}
#[inline]
#[must_use]
pub const fn checked_mul(self, rhs: HexColor) -> Option<HexColor> {
let (res, flag) = self.overflowing_mul(rhs);
if flag {
None
} else {
Some(res)
}
}
#[inline]
#[must_use]
pub const fn overflowing_mul(self, rhs: HexColor) -> (HexColor, bool) {
let (r1, g1, b1) = self.split_rgb();
let (r2, g2, b2) = rhs.split_rgb();
let (r, r_flag) = r1.overflowing_mul(r2);
let (g, g_flag) = g1.overflowing_mul(g2);
let (b, b_flag) = b1.overflowing_mul(b2);
(HexColor::rgb(r, g, b), r_flag || g_flag || b_flag)
}
#[inline]
#[must_use]
pub const fn saturating_mul(self, rhs: HexColor) -> HexColor {
let (r1, g1, b1) = self.split_rgb();
let (r2, g2, b2) = rhs.split_rgb();
HexColor::rgb(
r1.saturating_mul(r2),
g1.saturating_mul(g2),
b1.saturating_mul(b2),
)
}
#[inline]
#[must_use]
pub const fn wrapping_mul(self, rhs: HexColor) -> HexColor {
let (r1, g1, b1) = self.split_rgb();
let (r2, g2, b2) = rhs.split_rgb();
HexColor::rgb(
r1.wrapping_mul(r2),
g1.wrapping_mul(g2),
b1.wrapping_mul(b2),
)
}
#[inline]
#[must_use]
#[track_caller]
pub const fn div(self, rhs: HexColor) -> HexColor {
let (r1, g1, b1) = self.split_rgb();
let (r2, g2, b2) = rhs.split_rgb();
HexColor::rgb(r1 / r2, g1 / g2, b1 / b2)
}
#[inline]
#[must_use]
pub const fn checked_div(self, rhs: HexColor) -> Option<HexColor> {
let (r1, g1, b1) = self.split_rgb();
let (r2, g2, b2) = rhs.split_rgb();
if r2 == 0 || g2 == 0 || b2 == 0 {
None
} else {
Some(HexColor::rgb(r1 / r2, g1 / g2, b1 / b2))
}
}
#[inline]
#[must_use]
#[track_caller]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn scale(self, f: f32) -> Self {
let (r, g, b, a) = self.split_rgba();
let r = (f32::from(r) * f).min(255.0).round() as u8;
let g = (f32::from(g) * f).min(255.0).round() as u8;
let b = (f32::from(b) * f).min(255.0).round() as u8;
HexColor::rgba(r, g, b, a)
}
#[inline]
#[must_use]
pub const fn invert(self) -> HexColor {
let (r, g, b, a) = self.split_rgba();
HexColor::rgba(0xFF - r, 0xFF - g, 0xFF - b, a)
}
}
impl Add for HexColor {
type Output = HexColor;
#[inline]
#[track_caller]
fn add(self, rhs: Self) -> Self::Output {
self.add(rhs)
}
}
impl AddAssign for HexColor {
#[inline]
#[track_caller]
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs;
}
}
impl Sub for HexColor {
type Output = HexColor;
#[inline]
#[track_caller]
fn sub(self, rhs: Self) -> Self::Output {
self.sub(rhs)
}
}
impl SubAssign for HexColor {
#[inline]
#[track_caller]
fn sub_assign(&mut self, rhs: Self) {
*self = *self - rhs;
}
}
impl Mul for HexColor {
type Output = HexColor;
#[inline]
#[track_caller]
fn mul(self, rhs: Self) -> Self::Output {
self.mul(rhs)
}
}
impl MulAssign for HexColor {
#[inline]
#[track_caller]
fn mul_assign(&mut self, rhs: Self) {
*self = *self * rhs;
}
}
impl Mul<f32> for HexColor {
type Output = HexColor;
#[inline]
#[track_caller]
fn mul(self, rhs: f32) -> Self::Output {
self.scale(rhs)
}
}
impl Mul<HexColor> for f32 {
type Output = HexColor;
#[inline]
#[track_caller]
fn mul(self, rhs: HexColor) -> Self::Output {
rhs.scale(self)
}
}
impl MulAssign<f32> for HexColor {
#[inline]
#[track_caller]
fn mul_assign(&mut self, rhs: f32) {
*self = *self * rhs;
}
}
impl Mul<f64> for HexColor {
type Output = HexColor;
#[inline]
#[track_caller]
#[allow(clippy::cast_possible_truncation)]
fn mul(self, rhs: f64) -> Self::Output {
self.scale(rhs as f32)
}
}
impl Mul<HexColor> for f64 {
type Output = HexColor;
#[inline]
#[track_caller]
fn mul(self, rhs: HexColor) -> Self::Output {
rhs * self
}
}
impl MulAssign<f64> for HexColor {
#[inline]
#[track_caller]
fn mul_assign(&mut self, rhs: f64) {
*self = *self * rhs;
}
}
impl Div for HexColor {
type Output = HexColor;
#[inline]
#[track_caller]
fn div(self, rhs: Self) -> Self::Output {
self.div(rhs)
}
}
impl DivAssign for HexColor {
#[inline]
#[track_caller]
fn div_assign(&mut self, rhs: Self) {
*self = *self / rhs;
}
}
impl From<(u8, u8, u8, u8)> for HexColor {
#[inline]
fn from((r, g, b, a): (u8, u8, u8, u8)) -> Self {
HexColor::rgba(r, g, b, a)
}
}
impl From<HexColor> for (u8, u8, u8, u8) {
fn from(hex_color: HexColor) -> Self {
hex_color.split_rgba()
}
}
impl From<(u8, u8, u8)> for HexColor {
#[inline]
fn from((r, g, b): (u8, u8, u8)) -> Self {
HexColor::rgb(r, g, b)
}
}
impl From<HexColor> for (u8, u8, u8) {
#[inline]
fn from(hex_color: HexColor) -> Self {
hex_color.split_rgb()
}
}
impl From<u32> for HexColor {
#[inline]
fn from(n: u32) -> Self {
HexColor::from_u32(n)
}
}
impl From<HexColor> for u32 {
#[inline]
fn from(hex_color: HexColor) -> Self {
hex_color.to_u32()
}
}
impl FromStr for HexColor {
type Err = ParseHexColorError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
HexColor::parse_internals(s, ParseMode::Any)
}
}
#[derive(Debug, Copy, Clone)]
enum ParseMode {
Any,
Rgb,
Rgba,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct Display {
color: HexColor,
alpha: Alpha,
case: Case,
}
impl Display {
#[must_use]
#[inline]
pub const fn new(color: HexColor) -> Self {
Self {
color,
alpha: Alpha::Hidden,
case: Case::Upper,
}
}
#[must_use]
#[inline]
pub const fn with_alpha(mut self, alpha: Alpha) -> Self {
self.alpha = alpha;
self
}
#[must_use]
#[inline]
pub const fn with_case(mut self, case: Case) -> Self {
self.case = case;
self
}
#[must_use]
#[inline]
pub const fn color(self) -> HexColor {
self.color
}
}
impl fmt::Display for Display {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (r, g, b) = self.color.split_rgb();
match self.case {
Case::Lower => write!(f, "#{r:02x}{g:02x}{b:02x}")?,
Case::Upper => write!(f, "#{r:02X}{g:02X}{b:02X}")?,
}
if self.alpha == Alpha::Visible {
let a = self.color.a;
match self.case {
Case::Lower => write!(f, "{a:02x}")?,
Case::Upper => write!(f, "{a:02X}")?,
}
}
Ok(())
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
pub enum Alpha {
#[default]
Hidden,
Visible,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
pub enum Case {
Lower,
#[default]
Upper,
}
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub enum ParseHexColorError {
Empty,
InvalidFormat,
InvalidDigit,
}
impl fmt::Display for ParseHexColorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let data = match self {
ParseHexColorError::Empty => "cannot parse hex color from empty string",
ParseHexColorError::InvalidFormat => "invalid hexadecimal color format",
ParseHexColorError::InvalidDigit => "invalid hexadecimal digit",
};
f.write_str(data)
}
}
#[cfg(feature = "std")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))]
impl std::error::Error for ParseHexColorError {}