prettypretty/core/space.rs
1#[cfg(feature = "pyffi")]
2use pyo3::prelude::*;
3
4/// The enumeration of supported color spaces.
5///
6/// # RGB
7///
8/// This crate supports several RGB color spaces, each in its gamma-corrected
9/// and its linear form. From smallest to largest gamut, they are:
10///
11/// * [sRGB](https://en.wikipedia.org/wiki/SRGB), which has long served as the
12/// default color space for the web.
13/// * [Display P3](https://en.wikipedia.org/wiki/DCI-P3), which is
14/// well-positioned to become sRGB's successor.
15/// * [Rec. 2020](https://en.wikipedia.org/wiki/Rec._2020), which is the
16/// standard color space for ultra-high-definition (UDH) video and, when it
17/// comes to display hardware, currently aspirational.
18///
19/// For all three color spaces as well as all three linear versions, in-gamut
20/// coordinates range from 0 to 1, inclusive.
21///
22/// # The Oklab Variations
23///
24/// This crate supports the
25/// [Oklab/Oklch](https://bottosson.github.io/posts/oklab/) and
26/// [Oklrab/Oklrch](https://bottosson.github.io/posts/colorpicker/#intermission---a-new-lightness-estimate-for-oklab)
27/// color spaces. All four are variations of the same perceptually uniform color
28/// space, which, like CIELAB, uses one coordinate for lightness and two
29/// coordinates for "colorness."
30///
31/// Oklab and Oklch reflect the original design. They improve on CIELAB by using
32/// the D65 standard illuminant (not the print-oriented D50), which is also used
33/// by sRGB and Display P3. They further improve on CIELAB by avoiding visible
34/// distortions around the blues. However, they also regress, as their lightness
35/// L is visibly biased towards dark tones. Oklrab and Oklrch, which were
36/// introduced nine months after Oklab/Oklch, feature a revised lightness Lr
37/// that closely resembles CIELAB's uniform lightness.
38///
39/// Oklab/Oklrab use Cartesian coordinates a, b for colorness—with the a axis
40/// varying red/green and the b axis varying blue/yellow. Because they use
41/// Cartesian coordinates, computing color difference in Oklab/Oklrab is
42/// straight-forward: It simply is the Euclidian distance. In contrast,
43/// Oklch/Oklrch use polar coordinates C/h—with C expressing chroma and h or
44/// also hº expressing hue. That makes both color spaces well-suited to
45/// synthesizing and modifying colors.
46///
47/// Compared to the most other conversions between color spaces, conversions
48/// between the four Oklab variations are mathematically simpler and may not
49/// involve all coordinates. After all, there are four three-dimensional color
50/// spaces but only six distinct quantities:
51///
52/// | Color space | Lightness | Colorness 1 | Colorness 2 |
53/// | ----------- | :-------: | :---------: | :---------: |
54/// | Oklab | L | a | b |
55/// | Oklch | L | C | hº |
56/// | Oklrab | Lr | a | b |
57/// | Oklrch | Lr | C | hº |
58///
59/// Valid coordinates observe the following invariants:
60///
61/// * The (revised) lightness for all four color spaces is limited to `0..=1`.
62/// * The a/b coordinates for Oklab/Oklrab have no set limits, but in practice
63/// can be bounded `-0.4..=0.4`.
64/// * The chroma for Oklch/Oklrch must be non-negative and in practice can be
65/// bounded `0..=0.4`.
66/// * The hue for Oklch/Oklrch may be not-a-number, which indicates a
67/// powerless component, i.e., gray tone. In that case, the chroma must
68/// necessarily be zero.
69///
70/// Fundamentally, Oklab and Oklch are the *same* color space, only using
71/// different coordinate systems. Of course, that also is the case for Oklrab
72/// and Oklrch. The chroma bond corresponds to a circle with radius 0.4 that is
73/// centered at the origin. The a/b bounds correspond to a square with sides 0.8
74/// that is also centered at the origin. The circle just fits into the square
75/// and covers an area of π×0.4². Meanwhile, the square covers an area of
76/// (2×0.4)², i.e., it is 4/π or 1.273 times larger. In other words, the a/b
77/// bounds are somewhat looser than the chroma bound.
78///
79/// There may or may not be another, still outstanding issue with Oklrab, namely
80/// that a and b need [to be scaled by a factor of around
81/// 2.1](https://github.com/w3c/csswg-drafts/issues/6642#issuecomment-945714988).
82///
83/// There also is an extended Oklab, which behaves better for [imaginary
84/// colors](https://github.com/w3c/csswg-drafts/issues/9449). As shown in the
85/// [corresponding
86/// notebook](https://colab.research.google.com/drive/1_uoLM95LJKTiI7MECG_PjBrd32v-3W3o),
87/// the implementation compresses and shifts the LMS coordinates during
88/// conversion.
89///
90/// # XYZ
91///
92/// [XYZ](https://en.wikipedia.org/wiki/CIE_1931_color_space) serves as
93/// foundational color space. Notably, all conversions between unrelated color
94/// spaces go through XYZ. Since sRGB, Display P3, and Oklab use the [D65
95/// standard illuminant](https://en.wikipedia.org/wiki/Standard_illuminant),
96/// this crate uses XYZ with D65 as its reference color space. But XYZ with the
97/// D50 standard illuminant is available, too. Chromatic adaptation between the
98/// two versions of XYZ uses the (linear) Bradford method.
99#[cfg_attr(
100 feature = "pyffi",
101 pyclass(eq, eq_int, frozen, hash, module = "prettypretty.color")
102)]
103#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
104pub enum ColorSpace {
105 Srgb,
106 LinearSrgb,
107 DisplayP3,
108 LinearDisplayP3,
109 Rec2020,
110 LinearRec2020,
111 Oklab,
112 Oklch,
113 Oklrab,
114 Oklrch,
115 Xyz,
116 XyzD50,
117}
118
119#[cfg_attr(feature = "pyffi", pymethods)]
120impl ColorSpace {
121 /// Determine whether this color space is polar.
122 ///
123 /// Oklch and Oklrch currently are the only polar color spaces.
124 pub const fn is_polar(&self) -> bool {
125 matches!(*self, Self::Oklch | Self::Oklrch)
126 }
127
128 /// Determine whether this color space is XYZ.
129 pub const fn is_xyz(&self) -> bool {
130 matches!(self, &Self::Xyz)
131 }
132
133 /// Determine whether this color space is RGB.
134 ///
135 /// RGB color spaces are additive and have red, green, and blue coordinates.
136 /// In-gamut colors have coordinates in unit range `0..=1`.
137 pub const fn is_rgb(&self) -> bool {
138 use ColorSpace::*;
139 matches!(
140 *self,
141 Srgb | LinearSrgb | DisplayP3 | LinearDisplayP3 | Rec2020 | LinearRec2020
142 )
143 }
144
145 /// Determine whether this color space is one of the Oklab variations.
146 pub const fn is_ok(&self) -> bool {
147 use ColorSpace::*;
148 matches!(*self, Oklab | Oklch | Oklrab | Oklrch)
149 }
150
151 /// Determine whether this color space is bounded.
152 ///
153 /// XYZ and the Oklab variations are *unbounded* and hence can model any
154 /// color. By contrast, RGB color spaces are *bounded*, with coordinates
155 /// of in-gamut colors ranging `0..=1`.
156 pub const fn is_bounded(&self) -> bool {
157 self.is_rgb()
158 }
159
160 /// Create an iterator over this color space's gamut boundaries. <i
161 /// class=gamut-only>Gamut only</i>
162 ///
163 /// For bounded or RGB color spaces, this method returns an iterator that
164 /// traces the boundaries of the color space's gamut. As described in detail
165 /// for [`GamutTraversal`](crate::gamut::GamutTraversal), the iterator does
166 /// so by yielding [`GamutTraversalStep`](crate::gamut::GamutTraversalStep)s
167 /// that trace paths along the edges of this color space's RGB cube.
168 ///
169 /// Altogether, the iterator yields steps for a closed path covering six
170 /// edges followed by another six paths each covering one edge. Each step
171 /// includes exactly one in-gamut color that also is in this color space.
172 /// There are `edge_length` steps per edge, though the first path yields
173 /// corners other than the blue primary only once.
174 ///
175 /// If this color space is not bounded or the segment size is 0 or 1, this
176 /// method returns `None`.
177 #[cfg(feature = "gamut")]
178 pub fn gamut(&self, edge_length: usize) -> Option<crate::gamut::GamutTraversal> {
179 crate::gamut::GamutTraversal::new(*self, edge_length)
180 }
181
182 /// Create a human-readable representation for this color space. <i
183 /// class=python-only>Python only!</i>
184 #[cfg(feature = "pyffi")]
185 pub fn __str__(&self) -> String {
186 format!("{}", self)
187 }
188}
189
190impl core::fmt::Display for ColorSpace {
191 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
192 use ColorSpace::*;
193
194 let s = match *self {
195 Srgb => "sRGB",
196 LinearSrgb => "linear sRGB",
197 DisplayP3 => "Display P3",
198 LinearDisplayP3 => "linear Display P3",
199 Rec2020 => "Rec. 2020",
200 LinearRec2020 => "linear Rec. 2020",
201 Oklab => "Oklab",
202 Oklrab => "Oklrab",
203 Oklch => "Oklch",
204 Oklrch => "Oklrch",
205 Xyz => "XYZ D65",
206 XyzD50 => "XYZ D50",
207 };
208
209 f.write_str(s)
210 }
211}