Skip to main content

chromashift/
rgb.rs

1use crate::{Alpha, Matrix3, Number, RgbSpace, ToAlpha, XyzD50, XyzD65};
2use core::fmt;
3
4macro_rules! rgb_color {
5	($(#[$meta:meta])* $ty:ident, $css:literal, $space:ident, $xyz:ident) => {
6		rgb_color!($(#[$meta])* $ty, $space, $xyz);
7
8		impl fmt::Display for $ty {
9			fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10				let Self { red, green, blue, alpha } = *self;
11				write!(f, concat!("color(", $css, " {} {} {}{})"), Number(red), Number(green), Number(blue), Alpha(alpha))
12			}
13		}
14	};
15	($(#[$meta:meta])* $ty:ident, $space:ident, $xyz:ident) => {
16		$(#[$meta])*
17		/// The components are:
18		/// - Red - a number between 0.0 and 1.0
19		/// - Green - a number between 0.0 and 1.0
20		/// - Blue - a number between 0.0 and 1.0
21		/// - Alpha - a number between 0.0 and 100.0
22		///
23		/// Channels outside 0.0 to 1.0 are preserved, as CSS Color 4 requires; see
24		/// [`Gamut`](crate::Gamut) to detect and resolve them.
25		#[derive(Debug, Clone, Copy, PartialEq)]
26		pub struct $ty {
27			pub red: f64,
28			pub green: f64,
29			pub blue: f64,
30			pub alpha: f32,
31		}
32
33		impl $ty {
34			/// The colour space these channels are encoded in.
35			pub const SPACE: RgbSpace = RgbSpace::$space;
36
37			pub fn new(red: f64, green: f64, blue: f64, alpha: f32) -> Self {
38				Self { red, green, blue, alpha: alpha.clamp(0.0, 100.0) }
39			}
40		}
41
42		impl ToAlpha for $ty {
43			fn to_alpha(&self) -> f32 {
44				self.alpha
45			}
46		}
47
48		impl From<$xyz> for $ty {
49			fn from(value: $xyz) -> Self {
50				let $xyz { x, y, z, alpha } = value;
51				let [red, green, blue] = Self::SPACE.encoded_from_xyz([x / 100.0, y / 100.0, z / 100.0]);
52				Self::new(red, green, blue, alpha)
53			}
54		}
55
56		impl From<$ty> for $xyz {
57			fn from(value: $ty) -> Self {
58				let $ty { red, green, blue, alpha } = value;
59				let [x, y, z] = $ty::SPACE.xyz_from_encoded([red, green, blue]);
60				$xyz::new(x * 100.0, y * 100.0, z * 100.0, alpha)
61			}
62		}
63	};
64}
65
66rgb_color!(
67	/// A colour in the sRGB colour space.
68	Srgb,
69	SRGB,
70	XyzD65
71);
72
73rgb_color!(
74	/// A device independent expression of RGB: the sRGB primaries with no transfer function.
75	LinearRgb,
76	"srgb-linear",
77	SRGB_LINEAR,
78	XyzD65
79);
80
81rgb_color!(
82	/// A colour in the Adobe RGB (1998) colour space.
83	A98Rgb,
84	"a98-rgb",
85	A98_RGB,
86	XyzD65
87);
88
89rgb_color!(
90	/// A colour in the Display P3 colour space.
91	DisplayP3,
92	"display-p3",
93	DISPLAY_P3,
94	XyzD65
95);
96
97rgb_color!(
98	/// A colour in the ProPhoto RGB colour space (ROMM RGB).
99	ProphotoRgb,
100	"prophoto-rgb",
101	PROPHOTO_RGB,
102	XyzD50
103);
104
105rgb_color!(
106	/// A colour in the ITU-R BT.2020 colour space.
107	Rec2020,
108	"rec2020",
109	REC2020,
110	XyzD65
111);
112
113const P3_TO_SRGB: Matrix3 = Matrix3([
114	[3685649.0 / 3008840.0, -676809.0 / 3008840.0, 0.0],
115	[-5617931.0 / 133579120.0, 139197051.0 / 133579120.0, 0.0],
116	[-1323971.0 / 67420360.0, -1514763.0 / 19262960.0, 148092003.0 / 134840720.0],
117]);
118const SRGB_TO_P3: Matrix3 = Matrix3([
119	[2442703.0 / 2969989.0, 527286.0 / 2969989.0, 0.0],
120	[621563.0 / 18725049.0, 18103486.0 / 18725049.0, 0.0],
121	[281089.0 / 16454667.0, 10721482.0 / 148092003.0, 134840720.0 / 148092003.0],
122]);
123
124impl From<DisplayP3> for LinearRgb {
125	fn from(value: DisplayP3) -> Self {
126		let DisplayP3 { red, green, blue, alpha } = value;
127		let [red, green, blue] = P3_TO_SRGB.transform(DisplayP3::SPACE.transfer.decode_all([red, green, blue]));
128		LinearRgb::new(red, green, blue, alpha)
129	}
130}
131
132impl From<LinearRgb> for DisplayP3 {
133	fn from(value: LinearRgb) -> Self {
134		let LinearRgb { red, green, blue, alpha } = value;
135		let [red, green, blue] = DisplayP3::SPACE.transfer.encode_all(SRGB_TO_P3.transform([red, green, blue]));
136		DisplayP3::new(red, green, blue, alpha)
137	}
138}
139
140#[cfg(test)]
141mod tests {
142	use super::*;
143
144	#[test]
145	fn p3_srgb_matrices_are_products_of_the_css_color_4_matrices() {
146		for (matrix, product) in [
147			(P3_TO_SRGB, DisplayP3::SPACE.to_xyz.then(&LinearRgb::SPACE.from_xyz)),
148			(SRGB_TO_P3, LinearRgb::SPACE.to_xyz.then(&DisplayP3::SPACE.from_xyz)),
149		] {
150			for (got, want) in matrix.0.iter().flatten().zip(product.0.iter().flatten()) {
151				assert!((got - want).abs() < 1e-12, "{got} != {want}");
152			}
153		}
154	}
155}