1#![allow(
17 clippy::indexing_slicing,
18 reason = "3x3 matrices and fixed-size lookup tables indexed within their bounds"
19)]
20
21use otf_pixels_core::{
22 ChannelLayout, ImageDescriptor, Op, PixelFormat, PixelsError, Region, Result, SampleKind, Tile,
23 TileMut,
24};
25use std::sync::{Arc, OnceLock};
26
27const D50: [f64; 3] = [0.9642, 1.0, 0.8249];
29const ENCODE_TABLE: usize = 1 << 12;
31
32#[derive(Debug, Clone, PartialEq)]
34enum Curve {
35 Gamma(f64),
36 Table(Vec<f64>),
37 Parametric(u16, [f64; 7]),
39}
40
41impl Curve {
42 fn eval(&self, x: f64) -> f64 {
44 let y = match self {
45 Self::Gamma(g) => x.powf(*g),
46 Self::Table(table) => {
47 let last = table.len().saturating_sub(1);
48 let at = x.clamp(0.0, 1.0) * last as f64;
49 let i = (at.floor() as usize).min(last.saturating_sub(1));
50 let (a, b) = (
51 table.get(i).copied().unwrap_or(0.0),
52 table.get(i + 1).copied().unwrap_or(1.0),
53 );
54 a + (b - a) * (at - i as f64)
55 }
56 Self::Parametric(kind, [g, a, b, c, d, e, f]) => match kind {
57 0 => x.powf(*g),
58 1 => {
59 if x >= -b / a {
60 (a * x + b).powf(*g)
61 } else {
62 0.0
63 }
64 }
65 2 => {
66 if x >= -b / a {
67 (a * x + b).powf(*g) + c
68 } else {
69 *c
70 }
71 }
72 3 => {
73 if x >= *d {
74 (a * x + b).powf(*g)
75 } else {
76 c * x
77 }
78 }
79 _ => {
80 if x >= *d {
81 (a * x + b).powf(*g) + e
82 } else {
83 c * x + f
84 }
85 }
86 },
87 };
88 if y.is_nan() { 0.0 } else { y.clamp(0.0, 1.0) }
89 }
90}
91
92fn srgb_encode(linear: f64) -> f64 {
94 if linear <= 0.003_130_8 {
95 12.92 * linear
96 } else {
97 1.055 * linear.powf(1.0 / 2.4) - 0.055
98 }
99}
100
101fn srgb_decode(value: f64) -> f64 {
103 if value <= 0.040_45 {
104 value / 12.92
105 } else {
106 ((value + 0.055) / 1.055).powf(2.4)
107 }
108}
109
110type Matrix = [[f64; 3]; 3];
111
112fn mat_mul(a: &Matrix, b: &Matrix) -> Matrix {
113 let mut out = [[0.0; 3]; 3];
114 for (i, row) in out.iter_mut().enumerate() {
115 for (j, cell) in row.iter_mut().enumerate() {
116 *cell = (0..3).map(|k| a[i][k] * b[k][j]).sum();
117 }
118 }
119 out
120}
121
122fn mat_vec(a: &Matrix, v: [f64; 3]) -> [f64; 3] {
123 [0, 1, 2].map(|i| a[i][0] * v[0] + a[i][1] * v[1] + a[i][2] * v[2])
124}
125
126fn mat_inv(m: &Matrix) -> Option<Matrix> {
127 let [[a, b, c], [d, e, f], [g, h, i]] = *m;
128 let det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
129 if det.abs() < 1e-12 {
130 return None;
131 }
132 Some([
133 [
134 (e * i - f * h) / det,
135 (c * h - b * i) / det,
136 (b * f - c * e) / det,
137 ],
138 [
139 (f * g - d * i) / det,
140 (a * i - c * g) / det,
141 (c * d - a * f) / det,
142 ],
143 [
144 (d * h - e * g) / det,
145 (b * g - a * h) / det,
146 (a * e - b * d) / det,
147 ],
148 ])
149}
150
151fn srgb_colorants() -> Matrix {
154 const BRADFORD: Matrix = [
155 [0.8951, 0.2664, -0.1614],
156 [-0.7502, 1.7135, 0.0367],
157 [0.0389, -0.0685, 1.0296],
158 ];
159 let xyz = |x: f64, y: f64| [x / y, 1.0, (1.0 - x - y) / y];
160 let primaries = [xyz(0.64, 0.33), xyz(0.30, 0.60), xyz(0.15, 0.06)];
161 let m: Matrix = [0, 1, 2].map(|i| [0, 1, 2].map(|j| primaries[j][i]));
162 let white = xyz(0.3127, 0.3290);
163 let Some(inverse) = mat_inv(&m) else {
164 return [[0.0; 3]; 3];
165 };
166 let s = mat_vec(&inverse, white);
167 let rgb_to_xyz: Matrix = [0, 1, 2].map(|i| [0, 1, 2].map(|j| m[i][j] * s[j]));
168 let (from, to) = (mat_vec(&BRADFORD, white), mat_vec(&BRADFORD, D50));
169 let scale: Matrix =
170 [0, 1, 2].map(|i| [0, 1, 2].map(|j| if i == j { to[i] / from[i] } else { 0.0 }));
171 let Some(bradford_inverse) = mat_inv(&BRADFORD) else {
172 return [[0.0; 3]; 3];
173 };
174 mat_mul(
175 &bradford_inverse,
176 &mat_mul(&scale, &mat_mul(&BRADFORD, &rgb_to_xyz)),
177 )
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
182#[non_exhaustive]
183pub enum Unconvertible {
184 Malformed(&'static str),
186 Unsupported(String),
189}
190
191#[derive(Debug, Clone)]
193#[non_exhaustive]
194pub enum Conversion {
195 Convert(ToSrgb),
197 AlreadySrgb,
199 Keep(Unconvertible),
201}
202
203#[derive(Debug, Clone)]
205pub struct ToSrgb {
206 shared: Arc<Shared>,
207}
208
209#[derive(Debug)]
210struct Shared {
211 curves: Vec<Curve>,
213 matrix: Matrix,
215 lut8: Vec<[f32; 256]>,
217 lut16: OnceLock<Vec<Vec<f32>>>,
219 encode: Vec<f32>,
221}
222
223struct Tags<'a> {
225 data: &'a [u8],
226 entries: Vec<([u8; 4], usize, usize)>,
227}
228
229fn be32(data: &[u8], at: usize) -> Option<u32> {
230 Some(u32::from_be_bytes(data.get(at..at + 4)?.try_into().ok()?))
231}
232
233fn be16(data: &[u8], at: usize) -> Option<u16> {
234 Some(u16::from_be_bytes(data.get(at..at + 2)?.try_into().ok()?))
235}
236
237fn s15(data: &[u8], at: usize) -> Option<f64> {
238 Some(f64::from(be32(data, at)? as i32) / 65536.0)
239}
240
241impl<'a> Tags<'a> {
242 fn parse(data: &'a [u8]) -> std::result::Result<Self, Unconvertible> {
243 let malformed = Unconvertible::Malformed;
244 if data.len() < 132 || data.get(36..40) != Some(b"acsp") {
245 return Err(malformed("not an ICC profile"));
246 }
247 let count = be32(data, 128).ok_or(malformed("truncated tag table"))? as usize;
248 if count > 1024 || 132 + count * 12 > data.len() {
249 return Err(malformed("tag table runs past the profile"));
250 }
251 let mut entries = Vec::with_capacity(count);
252 for i in 0..count {
253 let at = 132 + i * 12;
254 let sig: [u8; 4] = data
255 .get(at..at + 4)
256 .and_then(|s| s.try_into().ok())
257 .ok_or(malformed("truncated tag entry"))?;
258 let offset = be32(data, at + 4).ok_or(malformed("truncated tag entry"))? as usize;
259 let size = be32(data, at + 8).ok_or(malformed("truncated tag entry"))? as usize;
260 if offset.checked_add(size).is_none_or(|end| end > data.len()) {
261 return Err(malformed("a tag runs past the profile"));
262 }
263 entries.push((sig, offset, size));
264 }
265 Ok(Self { data, entries })
266 }
267
268 fn get(&self, sig: &[u8; 4]) -> Option<&'a [u8]> {
269 let &(_, offset, size) = self.entries.iter().find(|(s, _, _)| s == sig)?;
270 self.data.get(offset..offset + size)
271 }
272
273 fn xyz(&self, sig: &[u8; 4]) -> std::result::Result<[f64; 3], Unconvertible> {
274 let tag = self.get(sig).ok_or_else(|| missing(sig))?;
275 if tag.get(..4) != Some(b"XYZ ") {
276 return Err(Unconvertible::Malformed("a colorant is not an XYZType"));
277 }
278 let v = |i: usize| s15(tag, 8 + 4 * i).ok_or(Unconvertible::Malformed("truncated XYZType"));
279 Ok([v(0)?, v(1)?, v(2)?])
280 }
281
282 fn curve(&self, sig: &[u8; 4]) -> std::result::Result<Curve, Unconvertible> {
283 let malformed = Unconvertible::Malformed;
284 let tag = self.get(sig).ok_or_else(|| missing(sig))?;
285 match tag.get(..4) {
286 Some(b"curv") => {
287 let n = be32(tag, 8).ok_or(malformed("truncated curv"))? as usize;
288 match n {
289 0 => Ok(Curve::Gamma(1.0)),
290 1 => Ok(Curve::Gamma(
291 f64::from(be16(tag, 12).ok_or(malformed("truncated curv"))?) / 256.0,
292 )),
293 _ => {
294 let table: Option<Vec<f64>> = (0..n)
295 .map(|i| be16(tag, 12 + 2 * i).map(|v| f64::from(v) / 65535.0))
296 .collect();
297 Ok(Curve::Table(
298 table.ok_or(malformed("truncated curv table"))?,
299 ))
300 }
301 }
302 }
303 Some(b"para") => {
304 let kind = be16(tag, 8).ok_or(malformed("truncated para"))?;
305 let count = match kind {
306 0 => 1,
307 1 => 3,
308 2 => 4,
309 3 => 5,
310 4 => 7,
311 _ => return Err(malformed("unknown parametric curve type")),
312 };
313 let mut params = [0.0; 7];
314 for (i, p) in params.iter_mut().enumerate().take(count) {
315 *p = s15(tag, 12 + 4 * i).ok_or(malformed("truncated para"))?;
316 }
317 if kind != 0 && params[1] == 0.0 {
318 return Err(malformed("parametric curve with a zero slope"));
319 }
320 Ok(Curve::Parametric(kind, params))
321 }
322 _ => Err(malformed("a tone curve is neither curv nor para")),
323 }
324 }
325}
326
327fn missing(sig: &[u8; 4]) -> Unconvertible {
328 Unconvertible::Unsupported(format!(
329 "the profile has no `{}` tag, so it is not a matrix/TRC profile",
330 String::from_utf8_lossy(sig)
331 ))
332}
333
334impl ToSrgb {
335 #[must_use]
341 pub fn from_profile(profile: &[u8]) -> Conversion {
342 match Self::build(profile) {
343 Ok(Some(op)) => Conversion::Convert(op),
344 Ok(None) => Conversion::AlreadySrgb,
345 Err(why) => Conversion::Keep(why),
346 }
347 }
348
349 fn build(profile: &[u8]) -> std::result::Result<Option<Self>, Unconvertible> {
350 let tags = Tags::parse(profile)?;
351 let space = profile.get(16..20).unwrap_or_default();
352 let pcs = profile.get(20..24).unwrap_or_default();
353 if pcs != b"XYZ " {
354 return Err(Unconvertible::Unsupported(
355 "the profile connects through Lab, not XYZ".into(),
356 ));
357 }
358 let (curves, matrix) = match space {
359 b"RGB " => {
360 let columns = [tags.xyz(b"rXYZ")?, tags.xyz(b"gXYZ")?, tags.xyz(b"bXYZ")?];
361 let source: Matrix = [0, 1, 2].map(|i| [0, 1, 2].map(|j| columns[j][i]));
362 let to_srgb = mat_inv(&srgb_colorants())
363 .ok_or(Unconvertible::Malformed("sRGB colorants are singular"))?;
364 let matrix = mat_mul(&to_srgb, &source);
365 let curves = vec![
366 tags.curve(b"rTRC")?,
367 tags.curve(b"gTRC")?,
368 tags.curve(b"bTRC")?,
369 ];
370 (curves, matrix)
371 }
372 b"GRAY" => (
373 vec![tags.curve(b"kTRC")?],
374 [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
375 ),
376 other => {
377 return Err(Unconvertible::Unsupported(format!(
378 "a `{}` colour space profile",
379 String::from_utf8_lossy(other).trim_end()
380 )));
381 }
382 };
383 if is_srgb(&curves, &matrix) {
384 return Ok(None);
385 }
386 let lut8 = curves
387 .iter()
388 .map(|curve| std::array::from_fn(|i| curve.eval(i as f64 / 255.0) as f32))
389 .collect();
390 let encode = (0..ENCODE_TABLE)
391 .map(|i| srgb_encode(i as f64 / (ENCODE_TABLE - 1) as f64) as f32)
392 .collect();
393 Ok(Some(Self {
394 shared: Arc::new(Shared {
395 curves,
396 matrix,
397 lut8,
398 lut16: OnceLock::new(),
399 encode,
400 }),
401 }))
402 }
403
404 #[must_use]
406 pub fn is_grey(&self) -> bool {
407 self.shared.curves.len() == 1
408 }
409
410 #[must_use]
413 pub fn applies_to(&self, format: PixelFormat) -> bool {
414 let grey = matches!(
415 format.layout(),
416 ChannelLayout::Gray | ChannelLayout::GrayAlpha
417 );
418 grey == self.is_grey()
419 }
420}
421
422fn is_srgb(curves: &[Curve], matrix: &Matrix) -> bool {
425 let identity =
426 (0..3).all(|i| (0..3).all(|j| (matrix[i][j] - f64::from(u8::from(i == j))).abs() < 2e-3));
427 let srgb_curve = |curve: &Curve| {
428 (0..=32).all(|k| {
429 let x = f64::from(k) / 32.0;
430 (curve.eval(x) - srgb_decode(x)).abs() < 1e-3
431 })
432 };
433 identity && curves.iter().all(srgb_curve)
434}
435
436impl Shared {
437 fn encode(&self, linear: f32) -> f32 {
439 let at = linear.clamp(0.0, 1.0) * (ENCODE_TABLE - 1) as f32;
440 let i = (at as usize).min(ENCODE_TABLE - 2);
441 let (a, b) = (
442 self.encode.get(i).copied().unwrap_or(0.0),
443 self.encode.get(i + 1).copied().unwrap_or(1.0),
444 );
445 a + (b - a) * (at - i as f32)
446 }
447
448 fn convert(&self, linear: [f32; 3]) -> [f32; 3] {
450 let m = &self.matrix;
451 [0, 1, 2].map(|i| {
452 let v = m[i][0] as f32 * linear[0]
453 + m[i][1] as f32 * linear[1]
454 + m[i][2] as f32 * linear[2];
455 self.encode(v)
456 })
457 }
458
459 fn lut16(&self) -> &[Vec<f32>] {
460 self.lut16.get_or_init(|| {
461 self.curves
462 .iter()
463 .map(|curve| {
464 (0..=u16::MAX)
465 .map(|i| curve.eval(f64::from(i) / 65535.0) as f32)
466 .collect()
467 })
468 .collect()
469 })
470 }
471}
472
473impl Op for ToSrgb {
474 fn name(&self) -> &'static str {
475 "to_srgb"
476 }
477
478 fn rescaled(&self) -> Option<Arc<dyn Op>> {
480 Some(Arc::new(self.clone()))
481 }
482
483 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor> {
484 let [input] = inputs else {
485 return Err(PixelsError::graph("to_srgb takes exactly one input"));
486 };
487 if !self.applies_to(input.pixel) {
488 return Err(PixelsError::invalid_argument(
489 "profile",
490 format!(
491 "a {} profile cannot describe {} pixels",
492 if self.is_grey() { "grey" } else { "RGB" },
493 input.pixel
494 ),
495 ));
496 }
497 Ok(*input)
498 }
499
500 fn input_regions(&self, output: Region, _inputs: &[ImageDescriptor]) -> Result<Vec<Region>> {
501 Ok(vec![output])
502 }
503
504 fn compute(&self, inputs: &[Tile<'_>], output: &mut TileMut<'_>) -> Result<()> {
505 let [input] = inputs else {
506 return Err(PixelsError::graph("to_srgb takes exactly one input"));
507 };
508 let format = output.pixel();
509 let region = output.region();
510 let channels = format.channels();
511 let colour = if self.is_grey() { 1 } else { 3 };
512 let shared = &*self.shared;
513 for y in region.y..region.y + region.height {
514 let Some(source) = input.row(y) else { continue };
515 let Some(target) = output.row_mut(y) else {
516 continue;
517 };
518 match format.sample_kind() {
519 SampleKind::U8 => {
520 for (from, to) in source
521 .chunks_exact(channels)
522 .zip(target.chunks_exact_mut(channels))
523 {
524 let linear: [f32; 3] = std::array::from_fn(|c| {
525 let lut = shared.lut8.get(c.min(colour - 1));
526 let v = from.get(c.min(colour - 1)).copied().unwrap_or(0);
527 lut.and_then(|l| l.get(usize::from(v)))
528 .copied()
529 .unwrap_or(0.0)
530 });
531 let out = if colour == 1 {
532 [shared.encode(linear[0]); 3]
533 } else {
534 shared.convert(linear)
535 };
536 for (c, slot) in to.iter_mut().enumerate() {
537 *slot = if c < colour {
538 (out[c] * 255.0 + 0.5) as u8
539 } else {
540 from.get(c).copied().unwrap_or(255)
541 };
542 }
543 }
544 }
545 SampleKind::U16 => {
546 let lut = shared.lut16();
547 let bytes = channels * 2;
548 for (from, to) in source
549 .chunks_exact(bytes)
550 .zip(target.chunks_exact_mut(bytes))
551 {
552 let sample = |c: usize| {
553 u16::from_ne_bytes([
554 from.get(2 * c).copied().unwrap_or(0),
555 from.get(2 * c + 1).copied().unwrap_or(0),
556 ])
557 };
558 let linear: [f32; 3] = std::array::from_fn(|c| {
559 let c = c.min(colour - 1);
560 lut.get(c)
561 .and_then(|l| l.get(usize::from(sample(c))))
562 .copied()
563 .unwrap_or(0.0)
564 });
565 let out = if colour == 1 {
566 [shared.encode(linear[0]); 3]
567 } else {
568 shared.convert(linear)
569 };
570 for c in 0..channels {
571 let value = if let Some(v) = out.get(c).filter(|_| c < colour) {
572 (v * 65535.0 + 0.5) as u16
573 } else {
574 sample(c)
575 };
576 if let Some(slot) = to.get_mut(2 * c..2 * c + 2) {
577 slot.copy_from_slice(&value.to_ne_bytes());
578 }
579 }
580 }
581 }
582 SampleKind::F32 => {
583 let bytes = channels * 4;
584 for (from, to) in source
585 .chunks_exact(bytes)
586 .zip(target.chunks_exact_mut(bytes))
587 {
588 let sample = |c: usize| {
589 let mut b = [0_u8; 4];
590 if let Some(s) = from.get(4 * c..4 * c + 4) {
591 b.copy_from_slice(s);
592 }
593 f32::from_ne_bytes(b)
594 };
595 let linear: [f32; 3] = std::array::from_fn(|c| {
596 let c = c.min(colour - 1);
597 shared
598 .curves
599 .get(c)
600 .map_or(0.0, |curve| curve.eval(f64::from(sample(c))) as f32)
601 });
602 let out = if colour == 1 {
603 [srgb_encode(f64::from(linear[0])) as f32; 3]
604 } else {
605 let m = &shared.matrix;
606 [0, 1, 2].map(|i| {
607 let v = (0..3).map(|k| m[i][k] * f64::from(linear[k])).sum::<f64>();
608 srgb_encode(v.clamp(0.0, 1.0)) as f32
609 })
610 };
611 for c in 0..channels {
612 let value = out
613 .get(c)
614 .filter(|_| c < colour)
615 .copied()
616 .unwrap_or_else(|| sample(c));
617 if let Some(slot) = to.get_mut(4 * c..4 * c + 4) {
618 slot.copy_from_slice(&value.to_ne_bytes());
619 }
620 }
621 }
622 }
623 }
624 }
625 Ok(())
626 }
627}
628
629#[cfg(test)]
630#[allow(
631 clippy::unwrap_used,
632 clippy::indexing_slicing,
633 reason = "tests operate on known-good values"
634)]
635mod tests {
636 use super::*;
637
638 fn profile(space: &[u8; 4], pcs: &[u8; 4], tags: &[(&[u8; 4], Vec<u8>)]) -> Vec<u8> {
640 let mut header = vec![0_u8; 128];
641 header[16..20].copy_from_slice(space);
642 header[20..24].copy_from_slice(pcs);
643 header[36..40].copy_from_slice(b"acsp");
644 let mut table = (tags.len() as u32).to_be_bytes().to_vec();
645 let mut data = Vec::new();
646 let base = 128 + 4 + 12 * tags.len();
647 for (sig, body) in tags {
648 table.extend_from_slice(*sig);
649 table.extend_from_slice(&((base + data.len()) as u32).to_be_bytes());
650 table.extend_from_slice(&(body.len() as u32).to_be_bytes());
651 data.extend_from_slice(body);
652 while data.len() % 4 != 0 {
653 data.push(0);
654 }
655 }
656 [header, table, data].concat()
657 }
658
659 fn xyz(v: [f64; 3]) -> Vec<u8> {
660 let mut out = b"XYZ \0\0\0\0".to_vec();
661 for x in v {
662 out.extend_from_slice(&((x * 65536.0).round() as i32).to_be_bytes());
663 }
664 out
665 }
666
667 fn gamma(g: f64) -> Vec<u8> {
668 let mut out = b"curv\0\0\0\0\0\0\0\x01".to_vec();
669 out.extend_from_slice(&((g * 256.0).round() as u16).to_be_bytes());
670 out
671 }
672
673 fn para(kind: u16, params: &[f64]) -> Vec<u8> {
674 let mut out = b"para\0\0\0\0".to_vec();
675 out.extend_from_slice(&kind.to_be_bytes());
676 out.extend_from_slice(&[0, 0]);
677 for p in params {
678 out.extend_from_slice(&((p * 65536.0).round() as i32).to_be_bytes());
679 }
680 out
681 }
682
683 fn rgb_profile(trc: Vec<u8>) -> Vec<u8> {
684 let m = srgb_colorants();
685 profile(
686 b"RGB ",
687 b"XYZ ",
688 &[
689 (b"rXYZ", xyz([m[0][0], m[1][0], m[2][0]])),
690 (b"gXYZ", xyz([m[0][1], m[1][1], m[2][1]])),
691 (b"bXYZ", xyz([m[0][2], m[1][2], m[2][2]])),
692 (b"rTRC", trc.clone()),
693 (b"gTRC", trc.clone()),
694 (b"bTRC", trc),
695 ],
696 )
697 }
698
699 #[test]
700 fn srgb_colorants_match_the_published_d50_values() {
701 let m = srgb_colorants();
703 let published = [
704 [0.4361, 0.3851, 0.1431],
705 [0.2225, 0.7169, 0.0606],
706 [0.0139, 0.0971, 0.7141],
707 ];
708 for i in 0..3 {
709 for j in 0..3 {
710 assert!(
711 (m[i][j] - published[i][j]).abs() < 2e-4,
712 "{i}{j}: {}",
713 m[i][j]
714 );
715 }
716 }
717 }
718
719 #[test]
720 fn an_srgb_profile_is_recognised_and_left_alone() {
721 let srgb = rgb_profile(para(
722 3,
723 &[2.4, 1.0 / 1.055, 0.055 / 1.055, 1.0 / 12.92, 0.04045],
724 ));
725 assert!(matches!(
726 ToSrgb::from_profile(&srgb),
727 Conversion::AlreadySrgb
728 ));
729 assert!(matches!(
731 ToSrgb::from_profile(&rgb_profile(gamma(1.8))),
732 Conversion::Convert(_)
733 ));
734 }
735
736 #[test]
737 fn parametric_curves_follow_icc_table_68() {
738 let curve = |kind, params: [f64; 7]| Curve::Parametric(kind, params);
739 assert!((curve(0, [2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]).eval(0.5) - 0.25).abs() < 1e-12);
740 let t1 = curve(1, [1.0, 2.0, -0.5, 0.0, 0.0, 0.0, 0.0]);
742 assert_eq!(t1.eval(0.2), 0.0);
743 assert!((t1.eval(0.5) - 0.5).abs() < 1e-12);
744 assert!((curve(2, [1.0, 2.0, -0.5, 0.1, 0.0, 0.0, 0.0]).eval(0.2) - 0.1).abs() < 1e-12);
746 let t3 = curve(
748 3,
749 [
750 2.4,
751 1.0 / 1.055,
752 0.055 / 1.055,
753 1.0 / 12.92,
754 0.04045,
755 0.0,
756 0.0,
757 ],
758 );
759 for k in 0..=20 {
760 let x = f64::from(k) / 20.0;
761 assert!((t3.eval(x) - srgb_decode(x)).abs() < 1e-9);
762 }
763 let t4 = curve(4, [1.0, 1.0, 0.0, 0.5, 0.5, 0.1, 0.05]);
765 assert!((t4.eval(0.2) - 0.15).abs() < 1e-12);
766 assert!((t4.eval(0.8) - 0.9).abs() < 1e-12);
767 let table = Curve::Table(vec![0.0, 0.25, 1.0]);
769 assert!((table.eval(0.25) - 0.125).abs() < 1e-12);
770 assert_eq!((table.eval(0.0), table.eval(1.0)), (0.0, 1.0));
771 }
772
773 #[test]
774 fn unconvertible_profiles_are_kept_not_refused() {
775 let cmyk = profile(b"CMYK", b"Lab ", &[]);
776 assert!(matches!(
777 ToSrgb::from_profile(&cmyk),
778 Conversion::Keep(Unconvertible::Unsupported(_))
779 ));
780 let lab_pcs = profile(b"RGB ", b"Lab ", &[]);
781 assert!(matches!(
782 ToSrgb::from_profile(&lab_pcs),
783 Conversion::Keep(Unconvertible::Unsupported(_))
784 ));
785 let lut_only = profile(b"RGB ", b"XYZ ", &[(b"A2B0", b"mft2\0\0\0\0".to_vec())]);
787 assert!(matches!(
788 ToSrgb::from_profile(&lut_only),
789 Conversion::Keep(Unconvertible::Unsupported(_))
790 ));
791 for broken in [&b"not a profile"[..], &[0_u8; 200][..]] {
792 assert!(matches!(
793 ToSrgb::from_profile(broken),
794 Conversion::Keep(Unconvertible::Malformed(_))
795 ));
796 }
797 let mut truncated = rgb_profile(gamma(2.2));
799 truncated.truncate(truncated.len() - 4);
800 assert!(matches!(
801 ToSrgb::from_profile(&truncated),
802 Conversion::Keep(Unconvertible::Malformed(_))
803 ));
804 }
805
806 #[test]
807 fn grey_profiles_convert_grey_only() {
808 let grey = profile(b"GRAY", b"XYZ ", &[(b"kTRC", gamma(1.0))]);
809 let Conversion::Convert(op) = ToSrgb::from_profile(&grey) else {
810 unreachable!("a grey kTRC profile converts")
811 };
812 assert!(op.applies_to(PixelFormat::Gray8) && op.applies_to(PixelFormat::GrayA8));
813 assert!(!op.applies_to(PixelFormat::Rgb8));
814 let encoded = (op.shared.encode(op.shared.lut8[0][128]) * 255.0 + 0.5) as u8;
816 assert_eq!(encoded, 188);
817 }
818}