Skip to main content

chromashift/
transfer.rs

1/// The transfer function (a.k.a. "gamma") pairing a linear light value with its encoded form.
2///
3/// Every variant is defined for the whole real line by mirroring the curve about zero, as CSS Color 4 requires for
4/// out-of-gamut channels.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Transfer {
7	Linear,
8	Srgb,
9	A98Rgb,
10	ProphotoRgb,
11	/// The ITU-R BT.2020 OETF, which is what browser engines currently render `rec2020` with. CSS Color 4 now specifies
12	/// the BT.1886 EOTF, a plain 2.4 gamma, instead (<https://github.com/w3c/csswg-drafts/issues/12574>); the
13	/// `rec2020-bt1886` feature switches to that.
14	Rec2020,
15}
16
17impl Transfer {
18	const REC2020_ALPHA: f64 = 1.09929682680944;
19	const REC2020_BETA: f64 = 0.018053968510807;
20
21	/// Encodes a linear light value, e.g. linear sRGB 0.2140 -> sRGB 0.5.
22	pub fn encode(self, linear: f64) -> f64 {
23		let abs = linear.abs();
24		match self {
25			Self::Linear => linear,
26			Self::Srgb => {
27				if abs <= 0.0031308 {
28					linear * 12.92
29				} else {
30					linear.signum() * (1.055 * abs.powf(1.0 / 2.4) - 0.055)
31				}
32			}
33			Self::A98Rgb => linear.signum() * abs.powf(256.0 / 563.0),
34			Self::ProphotoRgb => {
35				if abs >= 1.0 / 512.0 {
36					linear.signum() * abs.powf(1.0 / 1.8)
37				} else {
38					linear * 16.0
39				}
40			}
41			Self::Rec2020 => {
42				if cfg!(feature = "rec2020-bt1886") {
43					linear.signum() * abs.powf(1.0 / 2.4)
44				} else if abs >= Self::REC2020_BETA {
45					linear.signum() * (Self::REC2020_ALPHA * abs.powf(0.45) - (Self::REC2020_ALPHA - 1.0))
46				} else {
47					linear * 4.5
48				}
49			}
50		}
51	}
52
53	/// Decodes an encoded value back to linear light, e.g. sRGB 0.5 -> linear sRGB 0.2140.
54	pub fn decode(self, encoded: f64) -> f64 {
55		let abs = encoded.abs();
56		match self {
57			Self::Linear => encoded,
58			Self::Srgb => {
59				if abs > 0.04045 {
60					encoded.signum() * ((abs + 0.055) / 1.055).powf(2.4)
61				} else {
62					encoded / 12.92
63				}
64			}
65			Self::A98Rgb => encoded.signum() * abs.powf(563.0 / 256.0),
66			Self::ProphotoRgb => {
67				if abs >= 16.0 / 512.0 {
68					encoded.signum() * abs.powf(1.8)
69				} else {
70					encoded / 16.0
71				}
72			}
73			Self::Rec2020 => {
74				if cfg!(feature = "rec2020-bt1886") {
75					encoded.signum() * abs.powf(2.4)
76				} else if abs >= Self::REC2020_BETA * 4.5 {
77					encoded.signum() * ((abs + (Self::REC2020_ALPHA - 1.0)) / Self::REC2020_ALPHA).powf(1.0 / 0.45)
78				} else {
79					encoded / 4.5
80				}
81			}
82		}
83	}
84
85	/// Encodes three linear light channels, e.g. the red, green and blue of an RGB space.
86	pub fn encode_all(self, linear: [f64; 3]) -> [f64; 3] {
87		[self.encode(linear[0]), self.encode(linear[1]), self.encode(linear[2])]
88	}
89
90	/// Decodes three encoded channels back to linear light.
91	pub fn decode_all(self, encoded: [f64; 3]) -> [f64; 3] {
92		[self.decode(encoded[0]), self.decode(encoded[1]), self.decode(encoded[2])]
93	}
94}