1extern crate alloc;
28
29pub fn linear_to_srgb(c: f32) -> f32 {
33 let c = c.clamp(0.0, 1.0);
34 if c <= 0.003_130_8 {
35 c * 12.92
36 } else {
37 1.055 * libm::powf(c, 1.0 / 2.4) - 0.055
38 }
39}
40
41pub fn srgb_to_linear(c: f32) -> f32 {
43 let c = c.clamp(0.0, 1.0);
44 if c <= 0.040_45 {
45 c / 12.92
46 } else {
47 libm::powf((c + 0.055) / 1.055, 2.4)
48 }
49}
50
51pub fn lab_to_xyz_d50(l: f32, a: f32, b: f32) -> (f32, f32, f32) {
55 const KAPPA: f32 = 24389.0 / 27.0;
57 const EPSILON: f32 = 216.0 / 24389.0;
58 const WHITE_D50: [f32; 3] = [0.964_2, 1.0, 0.824_9];
60
61 let fy = (l + 16.0) / 116.0;
62 let fx = a / 500.0 + fy;
63 let fz = fy - b / 200.0;
64
65 let cube = |t: f32| t * t * t;
66 let x = if cube(fx) > EPSILON {
67 cube(fx)
68 } else {
69 (116.0 * fx - 16.0) / KAPPA
70 };
71 let y = if l > KAPPA * EPSILON {
72 cube((l + 16.0) / 116.0)
73 } else {
74 l / KAPPA
75 };
76 let z = if cube(fz) > EPSILON {
77 cube(fz)
78 } else {
79 (116.0 * fz - 16.0) / KAPPA
80 };
81
82 (x * WHITE_D50[0], y * WHITE_D50[1], z * WHITE_D50[2])
83}
84
85pub fn d50_to_d65(x: f32, y: f32, z: f32) -> (f32, f32, f32) {
89 (
90 0.955_473_4 * x - 0.023_098_1 * y + 0.063_259_3 * z,
91 -0.028_369_7 * x + 1.009_943_9 * y + 0.021_041_0 * z,
92 0.012_314_2 * x - 0.020_483_0 * y + 1.330_889_0 * z,
93 )
94}
95
96pub fn xyz_d65_to_linear_srgb(x: f32, y: f32, z: f32) -> (f32, f32, f32) {
98 (
99 3.240_97 * x - 1.537_383 * y - 0.498_610_8 * z,
100 -0.969_243_6 * x + 1.875_967_5 * y + 0.041_555_1 * z,
101 0.055_630_8 * x - 0.203_976_9 * y + 1.056_971_5 * z,
102 )
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum ColorSpace {
108 Srgb,
110 SrgbLinear,
112 DisplayP3,
114 A98Rgb,
116 ProPhotoRgb,
118 Rec2020,
120 XyzD65,
122 XyzD50,
124}
125
126pub fn parse_color_space(name: &str) -> Option<ColorSpace> {
128 match name.trim() {
129 "srgb" => Some(ColorSpace::Srgb),
130 "srgb-linear" => Some(ColorSpace::SrgbLinear),
131 "display-p3" => Some(ColorSpace::DisplayP3),
132 "a98-rgb" => Some(ColorSpace::A98Rgb),
133 "prophoto-rgb" => Some(ColorSpace::ProPhotoRgb),
134 "rec2020" => Some(ColorSpace::Rec2020),
135 "xyz" | "xyz-d65" => Some(ColorSpace::XyzD65),
137 "xyz-d50" => Some(ColorSpace::XyzD50),
138 _ => None,
139 }
140}
141
142pub fn components_to_linear_srgb(space: ColorSpace, c1: f32, c2: f32, c3: f32) -> (f32, f32, f32) {
146 match space {
147 ColorSpace::Srgb => (srgb_to_linear(c1), srgb_to_linear(c2), srgb_to_linear(c3)),
148 ColorSpace::SrgbLinear => (c1, c2, c3),
149 ColorSpace::DisplayP3 => {
150 let (r, g, b) = (srgb_to_linear(c1), srgb_to_linear(c2), srgb_to_linear(c3));
152 (
154 1.224_940_2 * r - 0.224_940_18 * g,
155 -0.042_056_95 * r + 1.042_056_9 * g,
156 -0.019_637_39 * r - 0.078_636_04 * g + 1.098_273_4 * b,
157 )
158 }
159 ColorSpace::A98Rgb => {
160 let dec = |v: f32| -> f32 {
162 let s = if v < 0.0 { -1.0 } else { 1.0 };
163 s * libm::powf(libm::fabsf(v), 563.0 / 256.0)
164 };
165 let (r, g, b) = (dec(c1), dec(c2), dec(c3));
166 (
167 1.398_240_9 * r - 0.398_240_9 * g,
168 0.999_999_9 * g,
169 -0.043_306_2 * g + 1.043_306_2 * b,
170 )
171 }
172 ColorSpace::ProPhotoRgb => {
173 let dec = |v: f32| -> f32 {
175 let s = if v < 0.0 { -1.0 } else { 1.0 };
176 let a = libm::fabsf(v);
177 if a <= 16.0 / 512.0 {
178 v / 16.0
179 } else {
180 s * libm::powf(a, 1.8)
181 }
182 };
183 let (r, g, b) = (dec(c1), dec(c2), dec(c3));
184 let x = 0.797_675_3 * r + 0.135_192_2 * g + 0.031_353_6 * b;
186 let y = 0.288_040_4 * r + 0.711_874_2 * g + 0.000_085_4 * b;
187 let z = 0.825_210_5 * b;
188 let (x, y, z) = d50_to_d65(x, y, z);
190 xyz_d65_to_linear_srgb(x, y, z)
191 }
192 ColorSpace::Rec2020 => {
193 const A: f32 = 1.098_729_0;
195 const B: f32 = 0.018_053_97;
196 let dec = |v: f32| -> f32 {
197 let s = if v < 0.0 { -1.0 } else { 1.0 };
198 let a = libm::fabsf(v);
199 if a < B * 4.5 {
200 v / 4.5
201 } else {
202 s * libm::powf((a + A - 1.0) / A, 1.0 / 0.45)
203 }
204 };
205 let (r, g, b) = (dec(c1), dec(c2), dec(c3));
206 (
207 1.660_496_1 * r - 0.587_656_1 * g - 0.072_840_0 * b,
208 -0.124_547_4 * r + 1.132_895_9 * g - 0.008_348_5 * b,
209 -0.018_154_1 * r - 0.100_597_0 * g + 1.118_751_1 * b,
210 )
211 }
212 ColorSpace::XyzD65 => xyz_d65_to_linear_srgb(c1, c2, c3),
213 ColorSpace::XyzD50 => {
214 let (x, y, z) = d50_to_d65(c1, c2, c3);
215 xyz_d65_to_linear_srgb(x, y, z)
216 }
217 }
218}
219
220pub fn lab_to_linear_srgb(l: f32, a: f32, b: f32) -> (f32, f32, f32) {
222 let (x, y, z) = lab_to_xyz_d50(l, a, b);
223 let (x, y, z) = d50_to_d65(x, y, z);
224 xyz_d65_to_linear_srgb(x, y, z)
225}
226
227pub fn lch_to_lab(l: f32, c: f32, h_deg: f32) -> (f32, f32, f32) {
232 let rad = h_deg * core::f32::consts::PI / 180.0;
233 (l, c * libm::cosf(rad), c * libm::sinf(rad))
234}
235
236pub fn linear_srgb_to_rgb8(r: f32, g: f32, b: f32) -> (u32, u32, u32) {
241 let enc = |c: f32| -> u32 {
242 libm::roundf((linear_to_srgb(c) * 255.0).clamp(0.0, 255.0)) as u32
243 };
244 (enc(r), enc(g), enc(b))
245}