1#[cfg(target_os = "linux")]
38use lcms2::{InfoType, Intent, Locale, PixelFormat, Profile, Tag, TagSignature, ToneCurve, Transform};
39use std::sync::Mutex;
40
41pub(crate) const CUBE: usize = 33;
43pub(crate) const CURVE: usize = 1024;
45
46#[derive(Clone, Copy, PartialEq, Eq, Debug)]
48pub(crate) enum Calmer {
49 Off,
50 Little,
52 More,
54}
55
56impl Calmer {
57 fn from_index(index: i32) -> Calmer {
60 match index {
61 1 => Calmer::Little,
62 2 => Calmer::More,
63 _ => Calmer::Off,
64 }
65 }
66
67 fn limit(self) -> Option<f64> {
70 match self {
71 Calmer::Off => None,
72 Calmer::Little => Some(0.12),
73 Calmer::More => Some(0.08),
74 }
75 }
76}
77
78pub(crate) struct Tables {
80 pub cube: Vec<f32>,
83 pub curve: Vec<f32>,
86}
87
88enum Screen {
90 Srgb,
92 #[cfg(target_os = "linux")]
95 Matrix { from_xyz: [[f64; 3]; 3], inverse: [ToneCurve; 3] },
96 #[cfg(target_os = "linux")]
99 Tables(Transform<[f32; 3], [f32; 3]>),
100}
101
102pub(crate) fn tables(profile: Option<&[u8]>, calmer: Calmer) -> Result<Option<Tables>, String> {
106 let screen = match profile {
107 Some(bytes) => screen_of(bytes)?,
108 None => Screen::Srgb,
109 };
110 if calmer == Calmer::Off && matches!(screen, Screen::Srgb) {
111 return Ok(None);
112 }
113 let made = make(&screen, calmer);
114 if calmer == Calmer::Off && is_identity(&made) {
115 return Ok(None);
116 }
117 Ok(Some(made))
118}
119
120#[cfg(not(target_os = "linux"))]
122fn screen_of(_bytes: &[u8]) -> Result<Screen, String> {
123 Err("colour profiles are read on Linux only".into())
124}
125
126#[cfg(target_os = "linux")]
128fn screen_of(bytes: &[u8]) -> Result<Screen, String> {
129 let profile = Profile::new_icc(bytes).map_err(|e| format!("the profile cannot be read: {e}"))?;
130 if profile.color_space() != lcms2::ColorSpaceSignature::RgbData {
131 return Err("not an RGB profile".into());
132 }
133 let has_tables = [TagSignature::BToA0Tag, TagSignature::BToA1Tag].iter().any(|&tag| profile.has_tag(tag));
136 if profile.is_matrix_shaper() && !has_tables {
137 let colorant = |tag| match profile.read_tag(tag) {
138 Tag::CIEXYZ(xyz) => Ok([xyz.X, xyz.Y, xyz.Z]),
139 _ => Err("a colorant is missing".to_string()),
140 };
141 let (r, g, b) = (colorant(TagSignature::RedColorantTag)?, colorant(TagSignature::GreenColorantTag)?, colorant(TagSignature::BlueColorantTag)?);
142 let to_xyz = [[r[0], g[0], b[0]], [r[1], g[1], b[1]], [r[2], g[2], b[2]]];
143 let from_xyz = invert(&to_xyz).ok_or("the profile's primaries are not independent")?;
144 let curve = |tag| match profile.read_tag(tag) {
145 Tag::ToneCurve(curve) => Ok(curve.reversed()),
146 _ => Err("a tone curve is missing".to_string()),
147 };
148 let inverse = [curve(TagSignature::RedTRCTag)?, curve(TagSignature::GreenTRCTag)?, curve(TagSignature::BlueTRCTag)?];
149 return Ok(Screen::Matrix { from_xyz, inverse });
150 }
151 let transform = Transform::new(&Profile::new_srgb(), PixelFormat::RGB_FLT, &profile, PixelFormat::RGB_FLT, Intent::RelativeColorimetric)
152 .map_err(|e| format!("no transform to the profile: {e}"))?;
153 Ok(Screen::Tables(transform))
154}
155
156fn make(screen: &Screen, calmer: Calmer) -> Tables {
158 let n = CUBE;
159 let mut cube = vec![1.0f32; n * n * n * 4];
160 #[cfg(target_os = "linux")]
162 let srgb_to_xyz = srgb_to_xyz();
163 #[cfg(target_os = "linux")]
164 let mut encoded = Vec::new();
165 for b in 0..n {
166 for g in 0..n {
167 for r in 0..n {
168 let numbers = [r, g, b].map(|i| i as f64 / (n - 1) as f64);
169 let mut linear = numbers.map(srgb_decode);
170 if let Some(limit) = calmer.limit() {
171 linear = soften(linear, limit);
172 }
173 let at = ((r + b * n) + g * n * n) * 4;
174 match screen {
175 Screen::Srgb => cube[at..at + 3].copy_from_slice(&linear.map(|v| v as f32)),
176 #[cfg(target_os = "linux")]
177 Screen::Matrix { from_xyz, .. } => {
178 let screen_linear = mul(from_xyz, &mul(&srgb_to_xyz, &linear));
179 cube[at..at + 3].copy_from_slice(&screen_linear.map(|v| v as f32));
180 }
181 #[cfg(target_os = "linux")]
182 Screen::Tables(_) => encoded.push(linear.map(|v| srgb_encode(v) as f32)),
183 }
184 }
185 }
186 }
187 #[cfg(target_os = "linux")]
190 if let Screen::Tables(transform) = screen {
191 let mut out = vec![[0.0f32; 3]; encoded.len()];
192 transform.transform_pixels(&encoded, &mut out);
193 let mut i = 0;
194 for b in 0..n {
195 for g in 0..n {
196 for r in 0..n {
197 let at = ((r + b * n) + g * n * n) * 4;
198 cube[at..at + 3].copy_from_slice(&out[i].map(|v| v.clamp(0.0, 1.0).powf(2.4)));
199 i += 1;
200 }
201 }
202 }
203 }
204 let mut curve = vec![1.0f32; CURVE * 4];
205 for j in 0..CURVE {
206 let linear = (j as f64 / (CURVE - 1) as f64).powf(2.4);
207 let numbers: [f64; 3] = match screen {
208 Screen::Srgb => [srgb_encode(linear); 3],
209 #[cfg(target_os = "linux")]
210 Screen::Matrix { inverse, .. } => [0, 1, 2].map(|c| f64::from(inverse[c].eval(linear as f32))),
211 #[cfg(target_os = "linux")]
212 Screen::Tables(_) => [linear.powf(1.0 / 2.4); 3],
213 };
214 curve[j * 4..j * 4 + 3].copy_from_slice(&numbers.map(|v| v.clamp(0.0, 1.0) as f32));
215 }
216 Tables { cube, curve }
217}
218
219fn is_identity(tables: &Tables) -> bool {
222 let n = CUBE;
223 (0..n).all(|b| {
224 (0..n).all(|g| {
225 (0..n).all(|r| {
226 let at = ((r + b * n) + g * n * n) * 4;
227 [r, g, b].iter().enumerate().all(|(c, &i)| {
228 let want = i as f64 / (n - 1) as f64;
229 let got = curve_at(tables, c, f64::from(tables.cube[at + c]));
230 (got - want).abs() < 0.2 / 255.0
231 })
232 })
233 })
234 })
235}
236
237fn curve_at(tables: &Tables, channel: usize, linear: f64) -> f64 {
239 let u = linear.clamp(0.0, 1.0).powf(1.0 / 2.4) * (CURVE - 1) as f64;
240 let j = (u.floor() as usize).min(CURVE - 2);
241 let f = u - j as f64;
242 let at = |k: usize| f64::from(tables.curve[k * 4 + channel]);
243 at(j) * (1.0 - f) + at(j + 1) * f
244}
245
246fn srgb_decode(v: f64) -> f64 {
248 if v <= 0.04045 { v / 12.92 } else { ((v + 0.055) / 1.055).powf(2.4) }
249}
250
251fn srgb_encode(v: f64) -> f64 {
253 let v = v.clamp(0.0, 1.0);
254 if v <= 0.003_130_8 { v * 12.92 } else { 1.055 * v.powf(1.0 / 2.4) - 0.055 }
255}
256
257#[cfg(target_os = "linux")]
260fn srgb_to_xyz() -> [[f64; 3]; 3] {
261 let srgb = Profile::new_srgb();
262 let colorant = |tag| match srgb.read_tag(tag) {
263 Tag::CIEXYZ(xyz) => [xyz.X, xyz.Y, xyz.Z],
264 _ => [0.0; 3],
265 };
266 let (r, g, b) = (colorant(TagSignature::RedColorantTag), colorant(TagSignature::GreenColorantTag), colorant(TagSignature::BlueColorantTag));
267 [[r[0], g[0], b[0]], [r[1], g[1], b[1]], [r[2], g[2], b[2]]]
268}
269
270const TO_LMS: [[f64; 3]; 3] = [[0.412_221_470_8, 0.536_332_536_3, 0.051_445_992_9], [0.211_903_498_2, 0.680_699_545_1, 0.107_396_956_6], [0.088_302_461_9, 0.281_718_837_6, 0.629_978_700_5]];
272const TO_LAB: [[f64; 3]; 3] = [[0.210_454_255_3, 0.793_617_785_0, -0.004_072_046_8], [1.977_998_495_1, -2.428_592_205_0, 0.450_593_709_9], [0.025_904_037_1, 0.782_771_766_2, -0.808_675_766_0]];
273const FROM_LAB: [[f64; 3]; 3] = [[1.0, 0.396_337_777_4, 0.215_803_757_3], [1.0, -0.105_561_345_8, -0.063_854_172_8], [1.0, -0.089_484_177_5, -1.291_485_548_0]];
274const FROM_LMS: [[f64; 3]; 3] = [[4.076_741_662_1, -3.307_711_591_3, 0.230_969_929_2], [-1.268_438_004_6, 2.609_757_401_1, -0.341_319_396_5], [-0.004_196_086_3, -0.703_418_614_7, 1.707_614_701_0]];
275
276fn oklab(linear: [f64; 3]) -> [f64; 3] {
278 mul(&TO_LAB, &mul(&TO_LMS, &linear).map(f64::cbrt))
279}
280
281fn from_oklab(lab: [f64; 3]) -> [f64; 3] {
283 mul(&FROM_LMS, &mul(&FROM_LAB, &lab).map(|v| v * v * v))
284}
285
286#[cfg(test)]
288fn chroma(linear: [f64; 3]) -> f64 {
289 let lab = oklab(linear);
290 lab[1].hypot(lab[2])
291}
292
293fn soften(linear: [f64; 3], limit: f64) -> [f64; 3] {
295 let mut lab = oklab(linear);
296 let c = lab[1].hypot(lab[2]);
297 if c > 1e-9 {
298 let k = limit * (c / limit).tanh() / c;
299 lab[1] *= k;
300 lab[2] *= k;
301 }
302 from_oklab(lab).map(|v| v.clamp(0.0, 1.0))
303}
304
305fn mul(m: &[[f64; 3]; 3], v: &[f64; 3]) -> [f64; 3] {
306 [0, 1, 2].map(|i| m[i][0] * v[0] + m[i][1] * v[1] + m[i][2] * v[2])
307}
308
309#[cfg(target_os = "linux")]
310fn invert(m: &[[f64; 3]; 3]) -> Option<[[f64; 3]; 3]> {
311 let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
312 if det.abs() < 1e-12 {
313 return None;
314 }
315 let c = |r: usize, k: usize| {
316 let (r1, r2, k1, k2) = ((r + 1) % 3, (r + 2) % 3, (k + 1) % 3, (k + 2) % 3);
317 m[r1][k1] * m[r2][k2] - m[r1][k2] * m[r2][k1]
318 };
319 Some([0, 1, 2].map(|i| [0, 1, 2].map(|k| c(k, i) / det)))
320}
321
322#[cfg(not(target_os = "linux"))]
325pub(crate) fn profile_name(_bytes: &[u8]) -> Option<String> {
326 None
327}
328
329#[cfg(target_os = "linux")]
331pub(crate) fn profile_name(bytes: &[u8]) -> Option<String> {
332 let profile = Profile::new_icc(bytes).ok()?;
333 profile.info(InfoType::Description, Locale::none()).map(|name| name.trim().to_string()).filter(|name| !name.is_empty())
334}
335
336#[derive(Clone, Debug, Default, PartialEq)]
339pub(crate) enum Said {
340 #[default]
342 Unknown,
343 Converted(String),
345 Desktop,
347 Plain,
349 Off,
351}
352
353static SAID: Mutex<Said> = Mutex::new(Said::Unknown);
354
355pub(crate) fn said() -> Said {
357 SAID.lock().map(|s| s.clone()).unwrap_or_default()
358}
359
360#[unsafe(no_mangle)]
371pub unsafe extern "C" fn sioul_colour_tables(profile: *const u8, length: usize, calmer: i32, cube_size: usize, cube: *mut f32, curve_size: usize, curve: *mut f32) -> i32 {
372 if cube_size != CUBE || curve_size != CURVE || cube.is_null() || curve.is_null() {
373 return -2;
374 }
375 let bytes = (!profile.is_null() && length > 0).then(|| unsafe { std::slice::from_raw_parts(profile, length) });
377 let made = std::panic::catch_unwind(|| tables(bytes, Calmer::from_index(calmer)));
379 match made {
380 Ok(Ok(Some(tables))) => {
381 unsafe {
383 std::ptr::copy_nonoverlapping(tables.cube.as_ptr(), cube, tables.cube.len());
384 std::ptr::copy_nonoverlapping(tables.curve.as_ptr(), curve, tables.curve.len());
385 }
386 1
387 }
388 Ok(Ok(None)) => 0,
389 Ok(Err(e)) => {
390 eprintln!("Sioul: the screen's colour profile: {e}");
391 -1
392 }
393 Err(_) => -1,
394 }
395}
396
397#[unsafe(no_mangle)]
404pub unsafe extern "C" fn sioul_colour_profile_name(profile: *const u8, length: usize, out: *mut u8, capacity: usize) -> usize {
405 if profile.is_null() || out.is_null() || capacity == 0 {
406 return 0;
407 }
408 let bytes = unsafe { std::slice::from_raw_parts(profile, length) };
410 let name = std::panic::catch_unwind(|| profile_name(bytes)).ok().flatten().unwrap_or_default();
411 let mut end = name.len().min(capacity - 1);
413 while !name.is_char_boundary(end) {
414 end -= 1;
415 }
416 unsafe {
418 std::ptr::copy_nonoverlapping(name.as_ptr(), out, end);
419 *out.add(end) = 0;
420 }
421 end
422}
423
424#[unsafe(no_mangle)]
432pub unsafe extern "C" fn sioul_colour_said(state: i32, name: *const std::ffi::c_char) {
433 let name = if name.is_null() { String::new() } else { unsafe { std::ffi::CStr::from_ptr(name) }.to_string_lossy().into_owned() };
435 let said = match state {
436 0 => Said::Converted(name),
437 1 => Said::Desktop,
438 3 => Said::Off,
439 _ => Said::Plain,
440 };
441 if let Ok(mut now) = SAID.lock() {
442 *now = said;
443 }
444}
445
446#[cfg(all(test, target_os = "linux"))]
448mod tests {
449 use super::*;
450 use lcms2::{CIExyY, CIExyYTRIPLE};
451
452 fn half(v: f32) -> f32 {
454 if v == 0.0 || !v.is_finite() {
455 return v;
456 }
457 let exponent = v.abs().log2().floor().max(-14.0);
458 let step = 2f32.powf(exponent - 10.0);
459 (v / step).round_ties_even() * step
460 }
461
462 fn shade(tables: &Tables, rgb: [u8; 3]) -> [f64; 3] {
466 let n = CUBE;
467 let cube: Vec<f64> = tables.cube.iter().map(|&v| f64::from(half(v))).collect();
468 let curve = Tables { cube: Vec::new(), curve: tables.curve.iter().map(|&v| half(v)).collect() };
469 let c = rgb.map(|v| f64::from(v) / 255.0 * (n - 1) as f64);
470 let (r0, g0, b0) = (c[0].floor().min((n - 2) as f64), c[1].floor().min((n - 2) as f64), c[2].floor().min((n - 2) as f64));
471 let (fr, fg, fb) = (c[0] - r0, c[1] - g0, c[2] - b0);
472 let texel = |r: usize, g: usize, b: usize, k: usize| cube[((r + b * n) + g * n * n) * 4 + k];
473 let (r0, g0, b0) = (r0 as usize, g0 as usize, b0 as usize);
474 [0, 1, 2].map(|k| {
475 let tile = |b: usize| {
476 let low = texel(r0, g0, b, k) * (1.0 - fr) + texel(r0 + 1, g0, b, k) * fr;
477 let high = texel(r0, g0 + 1, b, k) * (1.0 - fr) + texel(r0 + 1, g0 + 1, b, k) * fr;
478 low * (1.0 - fg) + high * fg
479 };
480 let linear = tile(b0) * (1.0 - fb) + tile(b0 + 1) * fb;
481 curve_at(&curve, k, linear) * 255.0
482 })
483 }
484
485 fn colours() -> Vec<[u8; 3]> {
487 let mut all = Vec::new();
488 for r in (0..=255).step_by(17) {
489 for g in (0..=255).step_by(17) {
490 for b in (0..=255).step_by(17) {
491 all.push([r as u8, g as u8, b as u8]);
492 }
493 }
494 }
495 all.extend([[0x25, 0xd3, 0x66], [0x58, 0x65, 0xf2], [0x42, 0x85, 0xf4], [0xff, 0x8c, 0x00], [0xff, 0x00, 0x33], [0x4c, 0x6b, 0x5c], [1, 2, 3], [252, 8, 4]]);
496 all
497 }
498
499 fn profile(white: (f64, f64), primaries: [(f64, f64); 3], curve: &ToneCurve) -> Vec<u8> {
501 let xy = |(x, y): (f64, f64)| CIExyY { x, y, Y: 1.0 };
502 let triple = CIExyYTRIPLE { Red: xy(primaries[0]), Green: xy(primaries[1]), Blue: xy(primaries[2]) };
503 Profile::new_rgb(&xy(white), &triple, &[curve, curve, curve]).unwrap().icc().unwrap()
504 }
505
506 fn adobe_like() -> Vec<u8> {
508 profile((0.3127, 0.3290), [(0.64, 0.33), (0.21, 0.71), (0.15, 0.06)], &ToneCurve::new(563.0 / 256.0))
509 }
510
511 fn table_curves() -> Vec<u8> {
514 let values: Vec<u16> = (0..256)
515 .map(|i| {
516 let x = f64::from(i) / 255.0;
517 let y = if x < 0.03 { x / 16.0 } else { ((x + 0.04) / 1.04).powf(2.4) };
518 (y * 65535.0).round() as u16
519 })
520 .collect();
521 profile((0.3127, 0.3290), [(0.68, 0.32), (0.20, 0.72), (0.15, 0.06)], &ToneCurve::new_tabulated(&values))
522 }
523
524 fn reference(bytes: &[u8], colours: &[[u8; 3]]) -> Vec<[f64; 3]> {
526 let screen = Profile::new_icc(bytes).unwrap();
527 let transform = Transform::new(&Profile::new_srgb(), PixelFormat::RGB_FLT, &screen, PixelFormat::RGB_FLT, Intent::RelativeColorimetric).unwrap();
528 let input: Vec<[f32; 3]> = colours.iter().map(|c| c.map(|v| f32::from(v) / 255.0)).collect();
529 let mut output = vec![[0.0f32; 3]; input.len()];
530 transform.transform_pixels(&input, &mut output);
531 output.iter().map(|o| o.map(|v| f64::from(v.clamp(0.0, 1.0)) * 255.0)).collect()
532 }
533
534 fn differences(tables: &Tables, bytes: &[u8]) -> (f64, f64) {
536 let colours = colours();
537 let want = reference(bytes, &colours);
538 let mut worst: f64 = 0.0;
539 let mut sum = 0.0;
540 for (colour, want) in colours.iter().zip(&want) {
541 let got = shade(tables, *colour);
542 for k in 0..3 {
543 let d = (got[k] - want[k]).abs();
544 worst = worst.max(d);
545 sum += d;
546 }
547 }
548 (worst, sum / (colours.len() * 3) as f64)
549 }
550
551 #[test]
552 fn a_screen_like_adobe_rgb_is_within_one_code_of_little_cms() {
553 let bytes = adobe_like();
554 let tables = tables(Some(&bytes), Calmer::Off).unwrap().expect("a wide screen changes colours");
555 let (worst, mean) = differences(&tables, &bytes);
556 assert!(worst <= 1.0 && mean < 0.1, "worst {worst:.3}, mean {mean:.3}");
557 }
558
559 #[test]
560 fn table_curves_are_within_one_code_of_little_cms() {
561 let bytes = table_curves();
562 let tables = tables(Some(&bytes), Calmer::Off).unwrap().unwrap();
563 let (worst, mean) = differences(&tables, &bytes);
564 assert!(worst <= 1.0 && mean < 0.1, "worst {worst:.3}, mean {mean:.3}");
565 }
566
567 #[test]
568 fn an_srgb_screen_changes_nothing() {
569 let srgb = Profile::new_srgb().icc().unwrap();
570 assert!(tables(Some(&srgb), Calmer::Off).unwrap().is_none());
571 assert!(tables(None, Calmer::Off).unwrap().is_none());
572 let curve = ToneCurve::new_parametric(4, &[2.4, 1.0 / 1.055, 0.055 / 1.055, 1.0 / 12.92, 0.04045]).unwrap();
574 let made = profile((0.3127, 0.3290), [(0.64, 0.33), (0.30, 0.60), (0.15, 0.06)], &curve);
575 assert!(tables(Some(&made), Calmer::Off).unwrap().is_none());
576 }
577
578 #[test]
579 fn unreadable_profiles_are_refused() {
580 assert!(tables(Some(b"not a profile"), Calmer::Off).is_err());
581 let grey = Profile::new_gray(&CIExyY { x: 0.3127, y: 0.3290, Y: 1.0 }, &ToneCurve::new(2.2)).unwrap().icc().unwrap();
582 assert!(tables(Some(&grey), Calmer::Off).is_err());
583 }
584
585 #[test]
586 fn a_profile_of_tables_goes_through_little_cms() {
587 let bytes = adobe_like();
591 let screen = screen_of(&bytes).unwrap();
592 assert!(matches!(screen, Screen::Matrix { .. }));
593 let transform = Transform::new(&Profile::new_srgb(), PixelFormat::RGB_FLT, &Profile::new_icc(&bytes).unwrap(), PixelFormat::RGB_FLT, Intent::RelativeColorimetric).unwrap();
594 let tables = make(&Screen::Tables(transform), Calmer::Off);
595 let (worst, mean) = differences(&tables, &bytes);
598 assert!(worst <= 4.0 && mean < 0.2, "worst {worst:.3}, mean {mean:.3}");
599 }
600
601 #[test]
602 fn calmer_colours_soften_loud_colours_and_keep_greys() {
603 let little = tables(None, Calmer::Little).unwrap().unwrap();
604 let more = tables(None, Calmer::More).unwrap().unwrap();
605 let linear = |rgb: [f64; 3]| rgb.map(|v| srgb_decode(v / 255.0));
606 let hue = |lin: [f64; 3]| {
607 let lab = oklab(lin);
608 lab[2].atan2(lab[1]).to_degrees()
609 };
610 for colour in colours() {
611 let before = linear(colour.map(f64::from));
612 for (tables, limit) in [(&little, 0.12), (&more, 0.08)] {
613 let after = linear(shade(tables, colour));
614 let (c0, c1) = (chroma(before), chroma(after));
615 assert!(c1 <= limit * (c0 / limit).tanh() + 0.004, "{colour:?}: {c0:.3} -> {c1:.3}");
617 let (l0, l1) = (oklab(before)[0], oklab(after)[0]);
619 assert!((l0 - l1).abs() < 0.01, "{colour:?}: lightness {l0:.3} -> {l1:.3}");
620 if c1 > 0.02 {
621 let turn = (hue(before) - hue(after) + 540.0) % 360.0 - 180.0;
622 assert!(turn.abs() < 2.0, "{colour:?}: hue turned {turn:.2}°");
623 }
624 }
625 if colour[0] == colour[1] && colour[1] == colour[2] {
627 for tables in [&little, &more] {
628 let got = shade(tables, colour);
629 assert!(got.iter().all(|v| (v - f64::from(colour[0])).abs() < 0.5), "{colour:?} -> {got:?}");
630 }
631 }
632 }
633 let red = linear([255.0, 0.0, 0.0]);
635 let soft = |t: &Tables| chroma(linear(shade(t, [255, 0, 0]))) / chroma(red);
636 assert!((0.4..0.55).contains(&soft(&little)) && soft(&more) < soft(&little), "{} {}", soft(&little), soft(&more));
637 }
638
639 #[test]
640 fn calmer_colours_on_a_wide_screen_are_both_at_once() {
641 let bytes = adobe_like();
643 let tables = tables(Some(&bytes), Calmer::Little).unwrap().unwrap();
644 let screen = Profile::new_icc(&bytes).unwrap();
645 let transform = Transform::new(&Profile::new_srgb(), PixelFormat::RGB_FLT, &screen, PixelFormat::RGB_FLT, Intent::RelativeColorimetric).unwrap();
646 let mut worst: f64 = 0.0;
647 for colour in colours() {
648 let soft = soften(colour.map(|v| srgb_decode(f64::from(v) / 255.0)), 0.12).map(|v| srgb_encode(v) as f32);
649 let mut want = [[0.0f32; 3]];
650 transform.transform_pixels(&[soft], &mut want);
651 let got = shade(&tables, colour);
652 for k in 0..3 {
653 worst = worst.max((got[k] - f64::from(want[0][k].clamp(0.0, 1.0)) * 255.0).abs());
654 }
655 }
656 assert!(worst <= 1.0, "worst {worst:.3}");
657 }
658
659 #[test]
660 fn the_c_functions_write_what_they_say() {
661 let bytes = adobe_like();
662 let mut cube = vec![0.0f32; CUBE * CUBE * CUBE * 4];
663 let mut curve = vec![0.0f32; CURVE * 4];
664 let written = unsafe { sioul_colour_tables(bytes.as_ptr(), bytes.len(), 0, CUBE, cube.as_mut_ptr(), CURVE, curve.as_mut_ptr()) };
666 assert_eq!(written, 1);
667 assert_eq!(cube[3], 1.0, "alpha is 1");
668 assert!(curve[(CURVE - 1) * 4] > 0.99);
669 assert_eq!(unsafe { sioul_colour_tables(std::ptr::null(), 0, 0, CUBE, cube.as_mut_ptr(), CURVE, curve.as_mut_ptr()) }, 0);
671 assert_eq!(unsafe { sioul_colour_tables(b"junk".as_ptr(), 4, 0, CUBE, cube.as_mut_ptr(), CURVE, curve.as_mut_ptr()) }, -1);
673 assert_eq!(unsafe { sioul_colour_tables(bytes.as_ptr(), bytes.len(), 0, 17, cube.as_mut_ptr(), CURVE, curve.as_mut_ptr()) }, -2);
675 let mut name = [0u8; 8];
676 let length = unsafe { sioul_colour_profile_name(bytes.as_ptr(), bytes.len(), name.as_mut_ptr(), name.len()) };
679 assert!(length <= 7 && name[length] == 0);
680 unsafe { sioul_colour_said(0, c"Screen".as_ptr()) };
682 assert_eq!(said(), Said::Converted("Screen".into()));
683 unsafe { sioul_colour_said(1, std::ptr::null()) };
685 assert_eq!(said(), Said::Desktop);
686 }
687}