Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Implement absolute color conversions and chromatic adaptation #139

Merged
merged 10 commits into from
Feb 13, 2025
3 changes: 3 additions & 0 deletions .typos.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# See the configuration reference at
# https://github.com/crate-ci/typos/blob/master/docs/reference.md

[default]
extend-ignore-re = ["colour-science"]

# Corrections take the form of a key/value pair. The key is the incorrect word
# and the value is the correct word. If the key and value are the same, the
# word is treated as always correct. If the value is an empty string, the word
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ You can find its changes [documented below](#023-2025-01-20).

This release has an [MSRV][] of 1.82.

### Added

* Support converting between color spaces without chromatic adaptation, thereby representing the same absolute color in the destination color space as in the source color space. ([#139][] by [@tomcur][])

**Note to `ColorSpace` implementers:** the `WHITE_POINT` associated constant is added to `ColorSpace`, defaulting to D65.
Implementations with a non-D65 white point should set this constant to get correct default absolute conversion behavior.
* Support manual chromatic adaptation of colors between arbitrary white point chromaticities. ([#139][] by [@tomcur][])

## [0.2.3][] (2025-01-20)

This release has an [MSRV][] of 1.82.
Expand Down Expand Up @@ -127,6 +135,7 @@ This is the initial release.
[#130]: https://github.com/linebender/color/pull/130
[#135]: https://github.com/linebender/color/pull/135
[#136]: https://github.com/linebender/color/pull/136
[#139]: https://github.com/linebender/color/pull/139

[Unreleased]: https://github.com/linebender/color/compare/v0.2.3...HEAD
[0.2.3]: https://github.com/linebender/color/releases/tag/v0.2.3
Expand Down
2 changes: 1 addition & 1 deletion color/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Simplifications include:
* Only handling 3-component color spaces (plus optional alpha).
* Choosing a fixed, curated set of color spaces for dynamic color types.
* Choosing linear sRGB as the central color space.
* Keeping white point implicit.
* Keeping white point implicit in the general conversion operations.

A number of other tasks are out of scope for this crate:
* Print color spaces (CMYK).
Expand Down
127 changes: 127 additions & 0 deletions color/src/chromaticity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2024 the Color Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

use crate::{matdiagmatmul, matmatmul, matvecmul};

/// CIE `xy` chromaticity, specifying a color in the XYZ color space, but not its luminosity.
///
/// An absolute color can be specified by adding a luminosity coordinate `Y` as in `xyY`. An `XYZ`
/// color can be calculated from `xyY` as follows.
///
/// ```text
/// X = Y/y * x
/// Y = Y
/// Z = Y/y * (1 - x - y)
/// ```
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Chromaticity {
/// The x-coordinate of the CIE `xy` chromaticity.
pub x: f32,

/// The y-coordinate of the CIE `xy` chromaticity.
pub y: f32,
}

impl Chromaticity {
/// The CIE D65 white point under the standard 2° observer.
///
/// This is a common white point for color spaces targeting monitors.
///
/// The white point's chromaticities are truncated to four digits here, as specified by the
/// CSS Color 4 specification, and following most color spaces using this white point.
pub const D65: Self = Self {
x: 0.3127,
y: 0.3290,
};

/// The CIE D50 white point under the standard 2° observer.
///
/// The white point's chromaticities are truncated to four digits here, as specified by the
/// CSS Color 4 specification, and following most color spaces using this white point.
pub const D50: Self = Self {
x: 0.3457,
y: 0.3585,
};

/// The [ACES white point][aceswp].
///
/// This is the reference white of [ACEScg](crate::AcesCg) and [ACES2065-1](crate::Aces2065_1).
/// The white point is near the D60 white point under the standard 2° observer.
///
/// [aceswp]: https://docs.acescentral.com/tb/white-point
pub const ACES: Self = Self {
x: 0.32168,
y: 0.33767,
};

/// Convert the `xy` chromaticities to XYZ, assuming `xyY` with `Y=1`.
pub(crate) const fn to_xyz(self) -> [f32; 3] {
let y_recip = 1. / self.y;
[self.x * y_recip, 1., (1. - self.x - self.y) * y_recip]
}

/// Calculate the 3x3 linear Bradford chromatic adaptation matrix from linear sRGB space.
///
/// This calculates the matrix going from a reference white of `self` to a reference white of
/// `to`.
pub(crate) const fn linear_srgb_chromatic_adaptation_matrix(self, to: Self) -> [[f32; 3]; 3] {
let bradford_source = matvecmul(&Self::XYZ_TO_BRADFORD, self.to_xyz());
let bradford_dest = matvecmul(&Self::XYZ_TO_BRADFORD, to.to_xyz());

matmatmul(
&matdiagmatmul(
&Self::BRADFORD_TO_SRGB,
[
bradford_dest[0] / bradford_source[0],
bradford_dest[1] / bradford_source[1],
bradford_dest[2] / bradford_source[2],
],
),
&Self::SRGB_TO_BRADFORD,
)
}

/// `XYZ_to_Bradford * lin_sRGB_to_XYZ`
const SRGB_TO_BRADFORD: [[f32; 3]; 3] = [
[
1_298_421_353. / 3_072_037_500.,
172_510_403. / 351_090_000.,
32_024_671. / 1_170_300_000.,
],
[
85_542_113. / 1_536_018_750.,
7_089_448_151. / 7_372_890_000.,
244_246_729. / 10_532_700_000.,
],
[
131_355_661. / 6_144_075_000.,
71_798_777. / 819_210_000.,
3_443_292_119. / 3_510_900_000.,
],
];

/// `XYZ_to_lin_sRGB * Bradford_to_XYZ`
const BRADFORD_TO_SRGB: [[f32; 3]; 3] = [
[
3_597_831_250_055_000. / 1_417_335_035_684_489.,
-1_833_298_161_702_000. / 1_417_335_035_684_489.,
-57_038_163_791_000. / 1_417_335_035_684_489.,
],
[
-4_593_417_841_453_000. / 31_461_687_363_220_151.,
35_130_825_086_032_200. / 31_461_687_363_220_151.,
-702_492_905_752_400. / 31_461_687_363_220_151.,
],
[
-191_861_334_350_000. / 4_536_975_728_019_583.,
-324_802_409_790_000. / 4_536_975_728_019_583.,
4_639_090_845_380_000. / 4_536_975_728_019_583.,
],
];

const XYZ_TO_BRADFORD: [[f32; 3]; 3] = [
[0.8951, 0.2664, -0.1614],
[-0.7502, 1.7135, 0.0367],
[0.0389, -0.0685, 1.0296],
];
}
Loading