Skip to main content

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}