1use std::{borrow::Cow, fmt};
7
8use crate::{
9 color::tf::{hlg_to_scene, linear_to_pq_precise, pq_to_linear_precise},
10 error::{Error, Result},
11 headers::color_encoding::{
12 ColorEncoding, ColorSpace, Primaries, RenderingIntent, TransferFunction, WhitePoint,
13 },
14 util::{Matrix3x3, Vector3, inv_3x3_matrix, mul_3x3_matrix, mul_3x3_vector},
15};
16
17const K_BRADFORD: Matrix3x3<f64> = [
19 [0.8951, 0.2664, -0.1614],
20 [-0.7502, 1.7135, 0.0367],
21 [0.0389, -0.0685, 1.0296],
22];
23
24const K_BRADFORD_INV: Matrix3x3<f64> = [
25 [0.9869929, -0.1470543, 0.1599627],
26 [0.4323053, 0.5183603, 0.0492912],
27 [-0.0085287, 0.0400428, 0.9684867],
28];
29
30pub fn compute_md5(data: &[u8]) -> [u8; 16] {
31 let mut sum = [0u8; 16];
32 let mut data64 = data.to_vec();
33 data64.push(128);
34
35 let extra = (64 - ((data64.len() + 8) & 63)) & 63;
37 data64.resize(data64.len() + extra, 0);
38
39 let bit_len = (data.len() as u64) << 3;
41 for i in (0..64).step_by(8) {
42 data64.push((bit_len >> i) as u8);
43 }
44
45 const SINEPARTS: [u32; 64] = [
46 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613,
47 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193,
48 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d,
49 0x02441453, 0xd8a1e681, 0xe7d3fbc8, 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
50 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122,
51 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa,
52 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, 0xf4292244,
53 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
54 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb,
55 0xeb86d391,
56 ];
57
58 const SHIFT: [u32; 64] = [
59 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5,
60 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10,
61 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
62 ];
63
64 let mut a0: u32 = 0x67452301;
65 let mut b0: u32 = 0xefcdab89;
66 let mut c0: u32 = 0x98badcfe;
67 let mut d0: u32 = 0x10325476;
68
69 for i in (0..data64.len()).step_by(64) {
70 let mut a = a0;
71 let mut b = b0;
72 let mut c = c0;
73 let mut d = d0;
74
75 for j in 0..64 {
76 let (f, g) = if j < 16 {
77 ((b & c) | ((!b) & d), j)
78 } else if j < 32 {
79 ((d & b) | ((!d) & c), (5 * j + 1) & 0xf)
80 } else if j < 48 {
81 (b ^ c ^ d, (3 * j + 5) & 0xf)
82 } else {
83 (c ^ (b | (!d)), (7 * j) & 0xf)
84 };
85
86 let dg0 = data64[i + g * 4] as u32;
87 let dg1 = data64[i + g * 4 + 1] as u32;
88 let dg2 = data64[i + g * 4 + 2] as u32;
89 let dg3 = data64[i + g * 4 + 3] as u32;
90 let u = dg0 | (dg1 << 8) | (dg2 << 16) | (dg3 << 24);
91
92 let f = f.wrapping_add(a).wrapping_add(SINEPARTS[j]).wrapping_add(u);
93 a = d;
94 d = c;
95 c = b;
96 b = b.wrapping_add(f.rotate_left(SHIFT[j]));
97 }
98
99 a0 = a0.wrapping_add(a);
100 b0 = b0.wrapping_add(b);
101 c0 = c0.wrapping_add(c);
102 d0 = d0.wrapping_add(d);
103 }
104
105 sum[0] = a0 as u8;
106 sum[1] = (a0 >> 8) as u8;
107 sum[2] = (a0 >> 16) as u8;
108 sum[3] = (a0 >> 24) as u8;
109 sum[4] = b0 as u8;
110 sum[5] = (b0 >> 8) as u8;
111 sum[6] = (b0 >> 16) as u8;
112 sum[7] = (b0 >> 24) as u8;
113 sum[8] = c0 as u8;
114 sum[9] = (c0 >> 8) as u8;
115 sum[10] = (c0 >> 16) as u8;
116 sum[11] = (c0 >> 24) as u8;
117 sum[12] = d0 as u8;
118 sum[13] = (d0 >> 8) as u8;
119 sum[14] = (d0 >> 16) as u8;
120 sum[15] = (d0 >> 24) as u8;
121 sum
122}
123
124#[allow(clippy::too_many_arguments)]
125pub(crate) fn primaries_to_xyz(
126 rx: f32,
127 ry: f32,
128 gx: f32,
129 gy: f32,
130 bx: f32,
131 by: f32,
132 wx: f32,
133 wy: f32,
134) -> Result<Matrix3x3<f64>, Error> {
135 if !((0.0..=1.0).contains(&wx) && (wy > 0.0 && wy <= 1.0)) {
137 return Err(Error::IccInvalidWhitePoint(
138 wx,
139 wy,
140 "White point coordinates out of range ([0,1] for x, (0,1] for y)".to_string(),
141 ));
142 }
143 let rz = 1.0 - rx as f64 - ry as f64;
154 let gz = 1.0 - gx as f64 - gy as f64;
155 let bz = 1.0 - bx as f64 - by as f64;
156 let p_matrix = [
157 [rx as f64, gx as f64, bx as f64],
158 [ry as f64, gy as f64, by as f64],
159 [rz, gz, bz],
160 ];
161
162 let p_inv_matrix = inv_3x3_matrix(&p_matrix)?;
163
164 let x_over_y_wp = wx as f64 / wy as f64;
167 let z_over_y_wp = (1.0 - wx as f64 - wy as f64) / wy as f64;
168
169 if !x_over_y_wp.is_finite() || !z_over_y_wp.is_finite() {
170 return Err(Error::IccInvalidWhitePoint(
171 wx,
172 wy,
173 "Calculated X/Y or Z/Y for white point is not finite.".to_string(),
174 ));
175 }
176 let white_point_xyz_vec: Vector3<f64> = [x_over_y_wp, 1.0, z_over_y_wp];
177
178 let s_vec = mul_3x3_vector(&p_inv_matrix, &white_point_xyz_vec);
181
182 let s_diag_matrix = [
184 [s_vec[0], 0.0, 0.0],
185 [0.0, s_vec[1], 0.0],
186 [0.0, 0.0, s_vec[2]],
187 ];
188 let result_matrix = mul_3x3_matrix(&p_matrix, &s_diag_matrix);
190
191 Ok(result_matrix)
192}
193
194pub(crate) fn adapt_to_xyz_d50(wx: f32, wy: f32) -> Result<Matrix3x3<f64>, Error> {
195 if !((0.0..=1.0).contains(&wx) && (wy > 0.0 && wy <= 1.0)) {
196 return Err(Error::IccInvalidWhitePoint(
197 wx,
198 wy,
199 "White point coordinates out of range ([0,1] for x, (0,1] for y)".to_string(),
200 ));
201 }
202
203 let x_over_y = wx as f64 / wy as f64;
205 let z_over_y = (1.0 - wx as f64 - wy as f64) / wy as f64;
206
207 if !x_over_y.is_finite() || !z_over_y.is_finite() {
209 return Err(Error::IccInvalidWhitePoint(
210 wx,
211 wy,
212 "Calculated X/Y or Z/Y for white point is not finite.".to_string(),
213 ));
214 }
215 let w: Vector3<f64> = [x_over_y, 1.0, z_over_y];
216
217 let w50: Vector3<f64> = [0.96422, 1.0, 0.82521];
220
221 let lms_source = mul_3x3_vector(&K_BRADFORD, &w);
223 let lms_d50 = mul_3x3_vector(&K_BRADFORD, &w50);
224
225 if lms_source.contains(&0.0) {
227 return Err(Error::IccInvalidWhitePoint(
228 wx,
229 wy,
230 "LMS components for source white point are zero, leading to division by zero."
231 .to_string(),
232 ));
233 }
234
235 let mut a_diag_matrix: Matrix3x3<f64> = [[0.0; 3]; 3];
237 for i in 0..3 {
238 a_diag_matrix[i][i] = lms_d50[i] / lms_source[i];
239 if !a_diag_matrix[i][i].is_finite() {
240 return Err(Error::IccInvalidWhitePoint(
241 wx,
242 wy,
243 format!("Diagonal adaptation matrix component {i} is not finite."),
244 ));
245 }
246 }
247
248 let b_matrix = mul_3x3_matrix(&a_diag_matrix, &K_BRADFORD);
250 let final_adaptation_matrix = mul_3x3_matrix(&K_BRADFORD_INV, &b_matrix);
251
252 Ok(final_adaptation_matrix)
253}
254
255#[allow(clippy::too_many_arguments)]
256pub(crate) fn primaries_to_xyz_d50(
257 rx: f32,
258 ry: f32,
259 gx: f32,
260 gy: f32,
261 bx: f32,
262 by: f32,
263 wx: f32,
264 wy: f32,
265) -> Result<Matrix3x3<f64>, Error> {
266 let rgb_to_xyz_native_wp_matrix = primaries_to_xyz(rx, ry, gx, gy, bx, by, wx, wy)?;
268
269 let adaptation_to_d50_matrix = adapt_to_xyz_d50(wx, wy)?;
271 let result_matrix = mul_3x3_matrix(&adaptation_to_d50_matrix, &rgb_to_xyz_native_wp_matrix);
278
279 Ok(result_matrix)
280}
281
282#[allow(clippy::too_many_arguments)]
283fn create_icc_rgb_matrix(
284 rx: f32,
285 ry: f32,
286 gx: f32,
287 gy: f32,
288 bx: f32,
289 by: f32,
290 wx: f32,
291 wy: f32,
292) -> Result<Matrix3x3<f32>, Error> {
293 let result_f64 = primaries_to_xyz_d50(rx, ry, gx, gy, bx, by, wx, wy)?;
295 Ok(std::array::from_fn(|r_idx| {
296 std::array::from_fn(|c_idx| result_f64[r_idx][c_idx] as f32)
297 }))
298}
299
300#[derive(Clone, Debug, PartialEq)]
301pub enum JxlWhitePoint {
302 D65,
303 E,
304 DCI,
305 Chromaticity { wx: f32, wy: f32 },
306}
307
308impl fmt::Display for JxlWhitePoint {
309 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310 match self {
311 JxlWhitePoint::D65 => f.write_str("D65"),
312 JxlWhitePoint::E => f.write_str("EER"),
313 JxlWhitePoint::DCI => f.write_str("DCI"),
314 JxlWhitePoint::Chromaticity { wx, wy } => write!(f, "{wx:.7};{wy:.7}"),
315 }
316 }
317}
318
319impl JxlWhitePoint {
320 pub fn to_xy_coords(&self) -> (f32, f32) {
321 match self {
322 JxlWhitePoint::Chromaticity { wx, wy } => (*wx, *wy),
323 JxlWhitePoint::D65 => (0.3127, 0.3290),
324 JxlWhitePoint::DCI => (0.314, 0.351),
325 JxlWhitePoint::E => (1.0 / 3.0, 1.0 / 3.0),
326 }
327 }
328}
329
330#[derive(Clone, Debug, PartialEq)]
331pub enum JxlPrimaries {
332 SRGB,
333 BT2100,
334 P3,
335 Chromaticities {
336 rx: f32,
337 ry: f32,
338 gx: f32,
339 gy: f32,
340 bx: f32,
341 by: f32,
342 },
343}
344
345impl fmt::Display for JxlPrimaries {
346 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347 match self {
348 JxlPrimaries::SRGB => f.write_str("SRG"),
349 JxlPrimaries::BT2100 => f.write_str("202"),
350 JxlPrimaries::P3 => f.write_str("DCI"),
351 JxlPrimaries::Chromaticities {
352 rx,
353 ry,
354 gx,
355 gy,
356 bx,
357 by,
358 } => write!(f, "{rx:.7},{ry:.7};{gx:.7},{gy:.7};{bx:.7},{by:.7}"),
359 }
360 }
361}
362
363impl JxlPrimaries {
364 pub fn to_xy_coords(&self) -> [(f32, f32); 3] {
365 match self {
366 JxlPrimaries::Chromaticities {
367 rx,
368 ry,
369 gx,
370 gy,
371 bx,
372 by,
373 } => [(*rx, *ry), (*gx, *gy), (*bx, *by)],
374 JxlPrimaries::SRGB => [
375 (0.639_998_7, 0.330_010_15),
377 (0.300_003_8, 0.600_003_36),
379 (0.150_002_05, 0.059_997_204),
381 ],
383 JxlPrimaries::BT2100 => [
384 (0.708, 0.292), (0.170, 0.797), (0.131, 0.046), ],
388 JxlPrimaries::P3 => [
389 (0.680, 0.320), (0.265, 0.690), (0.150, 0.060), ],
393 }
394 }
395}
396
397#[derive(Clone, Debug, PartialEq)]
398pub enum JxlTransferFunction {
399 BT709,
400 Linear,
401 SRGB,
402 PQ,
403 DCI,
404 HLG,
405 Gamma(f32),
406}
407
408impl fmt::Display for JxlTransferFunction {
409 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410 match self {
411 JxlTransferFunction::BT709 => f.write_str("709"),
412 JxlTransferFunction::Linear => f.write_str("Lin"),
413 JxlTransferFunction::SRGB => f.write_str("SRG"),
414 JxlTransferFunction::PQ => f.write_str("PeQ"),
415 JxlTransferFunction::DCI => f.write_str("DCI"),
416 JxlTransferFunction::HLG => f.write_str("HLG"),
417 JxlTransferFunction::Gamma(g) => write!(f, "g{g:.7}"),
418 }
419 }
420}
421
422#[derive(Clone, Debug, PartialEq)]
423pub enum JxlColorEncoding {
424 RgbColorSpace {
425 white_point: JxlWhitePoint,
426 primaries: JxlPrimaries,
427 transfer_function: JxlTransferFunction,
428 rendering_intent: RenderingIntent,
429 },
430 GrayscaleColorSpace {
431 white_point: JxlWhitePoint,
432 transfer_function: JxlTransferFunction,
433 rendering_intent: RenderingIntent,
434 },
435 XYB {
436 rendering_intent: RenderingIntent,
437 },
438}
439
440impl fmt::Display for JxlColorEncoding {
441 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
442 match self {
443 Self::RgbColorSpace { .. } => f.write_str("RGB"),
444 Self::GrayscaleColorSpace { .. } => f.write_str("Gra"),
445 Self::XYB { .. } => f.write_str("XYB"),
446 }
447 }
448}
449
450impl JxlColorEncoding {
451 pub fn from_internal(internal: &ColorEncoding) -> Result<Self> {
452 let rendering_intent = internal.rendering_intent;
453 if internal.color_space == ColorSpace::XYB {
454 if rendering_intent != RenderingIntent::Perceptual {
455 return Err(Error::InvalidRenderingIntent);
456 }
457 return Ok(Self::XYB { rendering_intent });
458 }
459
460 let white_point = match internal.white_point {
461 WhitePoint::D65 => JxlWhitePoint::D65,
462 WhitePoint::E => JxlWhitePoint::E,
463 WhitePoint::DCI => JxlWhitePoint::DCI,
464 WhitePoint::Custom => {
465 let (wx, wy) = internal.white.as_f32_coords();
466 JxlWhitePoint::Chromaticity { wx, wy }
467 }
468 };
469 let transfer_function = if internal.tf.have_gamma {
470 JxlTransferFunction::Gamma(internal.tf.gamma())
471 } else {
472 match internal.tf.transfer_function {
473 TransferFunction::BT709 => JxlTransferFunction::BT709,
474 TransferFunction::Linear => JxlTransferFunction::Linear,
475 TransferFunction::SRGB => JxlTransferFunction::SRGB,
476 TransferFunction::PQ => JxlTransferFunction::PQ,
477 TransferFunction::DCI => JxlTransferFunction::DCI,
478 TransferFunction::HLG => JxlTransferFunction::HLG,
479 TransferFunction::Unknown => {
480 return Err(Error::InvalidColorEncoding);
481 }
482 }
483 };
484
485 if internal.color_space == ColorSpace::Gray {
486 return Ok(Self::GrayscaleColorSpace {
487 white_point,
488 transfer_function,
489 rendering_intent,
490 });
491 }
492
493 let primaries = match internal.primaries {
494 Primaries::SRGB => JxlPrimaries::SRGB,
495 Primaries::BT2100 => JxlPrimaries::BT2100,
496 Primaries::P3 => JxlPrimaries::P3,
497 Primaries::Custom => {
498 let (rx, ry) = internal.custom_primaries[0].as_f32_coords();
499 let (gx, gy) = internal.custom_primaries[1].as_f32_coords();
500 let (bx, by) = internal.custom_primaries[2].as_f32_coords();
501 JxlPrimaries::Chromaticities {
502 rx,
503 ry,
504 gx,
505 gy,
506 bx,
507 by,
508 }
509 }
510 };
511
512 match internal.color_space {
513 ColorSpace::Gray | ColorSpace::XYB => unreachable!(),
514 ColorSpace::RGB => Ok(Self::RgbColorSpace {
515 white_point,
516 primaries,
517 transfer_function,
518 rendering_intent,
519 }),
520 ColorSpace::Unknown => Err(Error::InvalidColorSpace),
521 }
522 }
523
524 fn create_icc_cicp_tag_data(&self, tags_data: &mut Vec<u8>) -> Result<Option<TagInfo>, Error> {
525 let JxlColorEncoding::RgbColorSpace {
526 white_point,
527 primaries,
528 transfer_function,
529 ..
530 } = self
531 else {
532 return Ok(None);
533 };
534
535 let primaries_val: u8 = match (white_point, primaries) {
537 (JxlWhitePoint::D65, JxlPrimaries::SRGB) => 1,
538 (JxlWhitePoint::D65, JxlPrimaries::BT2100) => 9,
539 (JxlWhitePoint::D65, JxlPrimaries::P3) => 12,
540 (JxlWhitePoint::DCI, JxlPrimaries::P3) => 11,
541 _ => return Ok(None),
542 };
543
544 let tf_val = match transfer_function {
545 JxlTransferFunction::BT709 => 1,
546 JxlTransferFunction::Linear => 8,
547 JxlTransferFunction::SRGB => 13,
548 JxlTransferFunction::PQ => 16,
549 JxlTransferFunction::DCI => 17,
550 JxlTransferFunction::HLG => 18,
551 JxlTransferFunction::Gamma(_) => return Ok(None),
553 };
554
555 let signature = b"cicp";
556 let start_offset = tags_data.len() as u32;
557 tags_data.extend_from_slice(signature);
558 let data_len = tags_data.len();
559 tags_data.resize(tags_data.len() + 4, 0);
560 write_u32_be(tags_data, data_len, 0)?;
561 tags_data.push(primaries_val);
562 tags_data.push(tf_val);
563 tags_data.push(0);
565 tags_data.push(1);
567
568 Ok(Some(TagInfo {
569 signature: *signature,
570 offset_in_tags_blob: start_offset,
571 size_unpadded: 12,
572 }))
573 }
574
575 fn can_tone_map_for_icc(&self) -> bool {
576 let JxlColorEncoding::RgbColorSpace {
577 white_point,
578 primaries,
579 transfer_function,
580 ..
581 } = self
582 else {
583 return false;
584 };
585 if let JxlPrimaries::Chromaticities { .. } = primaries {
601 return false;
602 }
603
604 matches!(
605 transfer_function,
606 JxlTransferFunction::PQ | JxlTransferFunction::HLG
607 ) && (*white_point == JxlWhitePoint::D65
608 || (*white_point == JxlWhitePoint::DCI && *primaries == JxlPrimaries::P3))
609 }
610
611 pub fn get_color_encoding_description(&self) -> String {
612 if let Some(common_name) = match self {
614 JxlColorEncoding::RgbColorSpace {
615 white_point: JxlWhitePoint::D65,
616 primaries: JxlPrimaries::SRGB,
617 transfer_function: JxlTransferFunction::SRGB,
618 rendering_intent: RenderingIntent::Perceptual,
619 } => Some("sRGB"),
620 JxlColorEncoding::RgbColorSpace {
621 white_point: JxlWhitePoint::D65,
622 primaries: JxlPrimaries::P3,
623 transfer_function: JxlTransferFunction::SRGB,
624 rendering_intent: RenderingIntent::Perceptual,
625 } => Some("DisplayP3"),
626 JxlColorEncoding::RgbColorSpace {
627 white_point: JxlWhitePoint::D65,
628 primaries: JxlPrimaries::BT2100,
629 transfer_function: JxlTransferFunction::PQ,
630 rendering_intent: RenderingIntent::Relative,
631 } => Some("Rec2100PQ"),
632 JxlColorEncoding::RgbColorSpace {
633 white_point: JxlWhitePoint::D65,
634 primaries: JxlPrimaries::BT2100,
635 transfer_function: JxlTransferFunction::HLG,
636 rendering_intent: RenderingIntent::Relative,
637 } => Some("Rec2100HLG"),
638 _ => None,
639 } {
640 return common_name.to_string();
641 }
642
643 let mut d = String::with_capacity(64);
645
646 match self {
647 JxlColorEncoding::RgbColorSpace {
648 white_point,
649 primaries,
650 transfer_function,
651 rendering_intent,
652 } => {
653 d.push_str("RGB_");
654 d.push_str(&white_point.to_string());
655 d.push('_');
656 d.push_str(&primaries.to_string());
657 d.push('_');
658 d.push_str(&rendering_intent.to_string());
659 d.push('_');
660 d.push_str(&transfer_function.to_string());
661 }
662 JxlColorEncoding::GrayscaleColorSpace {
663 white_point,
664 transfer_function,
665 rendering_intent,
666 } => {
667 d.push_str("Gra_");
668 d.push_str(&white_point.to_string());
669 d.push('_');
670 d.push_str(&rendering_intent.to_string());
671 d.push('_');
672 d.push_str(&transfer_function.to_string());
673 }
674 JxlColorEncoding::XYB { rendering_intent } => {
675 d.push_str("XYB_");
676 d.push_str(&rendering_intent.to_string());
677 }
678 }
679
680 d
681 }
682
683 fn create_icc_header(&self) -> Result<Vec<u8>, Error> {
684 let mut header_data = vec![0u8; 128];
685
686 write_u32_be(&mut header_data, 0, 0)?;
688 const CMM_TAG: &str = "jxl ";
689 write_icc_tag(&mut header_data, 4, CMM_TAG)?;
691
692 write_u32_be(&mut header_data, 8, 0x04400000u32)?;
695
696 let profile_class_str = match self {
697 JxlColorEncoding::XYB { .. } => "scnr",
698 _ => "mntr",
699 };
700 write_icc_tag(&mut header_data, 12, profile_class_str)?;
701
702 let data_color_space_str = match self {
704 JxlColorEncoding::GrayscaleColorSpace { .. } => "GRAY",
705 _ => "RGB ",
706 };
707 write_icc_tag(&mut header_data, 16, data_color_space_str)?;
708
709 const K_ENABLE_3D_ICC_TONEMAPPING: bool = true;
713 if K_ENABLE_3D_ICC_TONEMAPPING && self.can_tone_map_for_icc() {
714 write_icc_tag(&mut header_data, 20, "Lab ")?;
715 } else {
716 write_icc_tag(&mut header_data, 20, "XYZ ")?;
717 }
718
719 write_u16_be(&mut header_data, 24, 2019)?; write_u16_be(&mut header_data, 26, 12)?; write_u16_be(&mut header_data, 28, 1)?; write_u16_be(&mut header_data, 30, 0)?; write_u16_be(&mut header_data, 32, 0)?; write_u16_be(&mut header_data, 34, 0)?; write_icc_tag(&mut header_data, 36, "acsp")?;
728 write_icc_tag(&mut header_data, 40, "APPL")?;
729
730 write_u32_be(&mut header_data, 44, 0)?;
732 write_u32_be(&mut header_data, 48, 0)?;
734 write_u32_be(&mut header_data, 52, 0)?;
736 write_u32_be(&mut header_data, 56, 0)?;
738 write_u32_be(&mut header_data, 60, 0)?;
739
740 let rendering_intent = match self {
742 JxlColorEncoding::RgbColorSpace {
743 rendering_intent, ..
744 }
745 | JxlColorEncoding::GrayscaleColorSpace {
746 rendering_intent, ..
747 }
748 | JxlColorEncoding::XYB { rendering_intent } => rendering_intent,
749 };
750 write_u32_be(&mut header_data, 64, *rendering_intent as u32)?;
751
752 write_u32_be(&mut header_data, 68, 0x0000F6D6)?;
754 write_u32_be(&mut header_data, 72, 0x00010000)?;
755 write_u32_be(&mut header_data, 76, 0x0000D32D)?;
756
757 write_icc_tag(&mut header_data, 80, CMM_TAG)?;
759
760 Ok(header_data)
766 }
767
768 pub fn maybe_create_profile(&self) -> Result<Option<Vec<u8>>, Error> {
769 if let JxlColorEncoding::XYB { rendering_intent } = self
770 && *rendering_intent != RenderingIntent::Perceptual
771 {
772 return Err(Error::InvalidRenderingIntent);
773 }
774 let header = self.create_icc_header()?;
775 let mut tags_data: Vec<u8> = Vec::new();
776 let mut collected_tags: Vec<TagInfo> = Vec::new();
777
778 let description_string = self.get_color_encoding_description();
780
781 let desc_tag_start_offset = tags_data.len() as u32; create_icc_mluc_tag(&mut tags_data, &description_string)?;
783 let desc_tag_unpadded_size = (tags_data.len() as u32) - desc_tag_start_offset;
784 pad_to_4_byte_boundary(&mut tags_data);
785 collected_tags.push(TagInfo {
786 signature: *b"desc",
787 offset_in_tags_blob: desc_tag_start_offset,
788 size_unpadded: desc_tag_unpadded_size,
789 });
790
791 let copyright_string = "CC0";
793 let cprt_tag_start_offset = tags_data.len() as u32;
794 create_icc_mluc_tag(&mut tags_data, copyright_string)?;
795 let cprt_tag_unpadded_size = (tags_data.len() as u32) - cprt_tag_start_offset;
796 pad_to_4_byte_boundary(&mut tags_data);
797 collected_tags.push(TagInfo {
798 signature: *b"cprt",
799 offset_in_tags_blob: cprt_tag_start_offset,
800 size_unpadded: cprt_tag_unpadded_size,
801 });
802
803 match self {
804 JxlColorEncoding::GrayscaleColorSpace { white_point, .. } => {
805 let (wx, wy) = white_point.to_xy_coords();
806 collected_tags.push(create_icc_xyz_tag(
807 &mut tags_data,
808 &cie_xyz_from_white_cie_xy(wx, wy)?,
809 )?);
810 }
811 _ => {
812 const D50: [f32; 3] = [0.964203f32, 1.0, 0.824905];
814 collected_tags.push(create_icc_xyz_tag(&mut tags_data, &D50)?);
815 }
816 }
817 pad_to_4_byte_boundary(&mut tags_data);
818 if !matches!(self, JxlColorEncoding::GrayscaleColorSpace { .. }) {
819 let (wx, wy) = match self {
820 JxlColorEncoding::GrayscaleColorSpace { .. } => unreachable!(),
821 JxlColorEncoding::RgbColorSpace { white_point, .. } => white_point.to_xy_coords(),
822 JxlColorEncoding::XYB { .. } => JxlWhitePoint::D65.to_xy_coords(),
823 };
824 let chad_matrix_f64 = adapt_to_xyz_d50(wx, wy)?;
825 let chad_matrix = std::array::from_fn(|r_idx| {
826 std::array::from_fn(|c_idx| chad_matrix_f64[r_idx][c_idx] as f32)
827 });
828 collected_tags.push(create_icc_chad_tag(&mut tags_data, &chad_matrix)?);
829 pad_to_4_byte_boundary(&mut tags_data);
830 }
831
832 if let JxlColorEncoding::RgbColorSpace {
833 white_point,
834 primaries,
835 ..
836 } = self
837 {
838 if let Some(tag_info) = self.create_icc_cicp_tag_data(&mut tags_data)? {
839 collected_tags.push(tag_info);
840 }
844
845 let primaries_coords = primaries.to_xy_coords();
847 let (rx, ry) = primaries_coords[0];
848 let (gx, gy) = primaries_coords[1];
849 let (bx, by) = primaries_coords[2];
850 let (wx, wy) = white_point.to_xy_coords();
851
852 let m = create_icc_rgb_matrix(rx, ry, gx, gy, bx, by, wx, wy)?;
854
855 let r_xyz = [m[0][0], m[1][0], m[2][0]];
857 let g_xyz = [m[0][1], m[1][1], m[2][1]];
858 let b_xyz = [m[0][2], m[1][2], m[2][2]];
859
860 let create_xyz_type_tag_data =
862 |tags: &mut Vec<u8>, xyz: &[f32; 3]| -> Result<u32, Error> {
863 let start_offset = tags.len();
864 tags.extend_from_slice(b"XYZ ");
866 tags.extend_from_slice(&0u32.to_be_bytes());
867 for &val in xyz {
868 append_s15_fixed_16(tags, val)?;
869 }
870 Ok((tags.len() - start_offset) as u32)
871 };
872
873 let r_xyz_tag_start_offset = tags_data.len() as u32;
875 let r_xyz_tag_unpadded_size = create_xyz_type_tag_data(&mut tags_data, &r_xyz)?;
876 pad_to_4_byte_boundary(&mut tags_data);
877 collected_tags.push(TagInfo {
878 signature: *b"rXYZ", offset_in_tags_blob: r_xyz_tag_start_offset,
880 size_unpadded: r_xyz_tag_unpadded_size,
881 });
882
883 let g_xyz_tag_start_offset = tags_data.len() as u32;
885 let g_xyz_tag_unpadded_size = create_xyz_type_tag_data(&mut tags_data, &g_xyz)?;
886 pad_to_4_byte_boundary(&mut tags_data);
887 collected_tags.push(TagInfo {
888 signature: *b"gXYZ",
889 offset_in_tags_blob: g_xyz_tag_start_offset,
890 size_unpadded: g_xyz_tag_unpadded_size,
891 });
892
893 let b_xyz_tag_start_offset = tags_data.len() as u32;
895 let b_xyz_tag_unpadded_size = create_xyz_type_tag_data(&mut tags_data, &b_xyz)?;
896 pad_to_4_byte_boundary(&mut tags_data);
897 collected_tags.push(TagInfo {
898 signature: *b"bXYZ",
899 offset_in_tags_blob: b_xyz_tag_start_offset,
900 size_unpadded: b_xyz_tag_unpadded_size,
901 });
902 }
903 if self.can_tone_map_for_icc() {
904 if let JxlColorEncoding::RgbColorSpace {
906 white_point,
907 primaries,
908 transfer_function,
909 ..
910 } = self
911 {
912 let a2b0_start = tags_data.len() as u32;
913 create_icc_lut_atob_tag_for_hdr(
914 transfer_function,
915 primaries,
916 white_point,
917 &mut tags_data,
918 )?;
919 pad_to_4_byte_boundary(&mut tags_data);
920 let a2b0_size = (tags_data.len() as u32) - a2b0_start;
921 collected_tags.push(TagInfo {
922 signature: *b"A2B0",
923 offset_in_tags_blob: a2b0_start,
924 size_unpadded: a2b0_size,
925 });
926
927 let b2a0_start = tags_data.len() as u32;
929 create_icc_noop_btoa_tag(&mut tags_data)?;
930 pad_to_4_byte_boundary(&mut tags_data);
931 let b2a0_size = (tags_data.len() as u32) - b2a0_start;
932 collected_tags.push(TagInfo {
933 signature: *b"B2A0",
934 offset_in_tags_blob: b2a0_start,
935 size_unpadded: b2a0_size,
936 });
937 }
938 } else {
939 match self {
940 JxlColorEncoding::XYB { .. } => {
941 let a2b0_start = tags_data.len() as u32;
943 create_icc_lut_atob_tag_for_xyb(&mut tags_data)?;
944 pad_to_4_byte_boundary(&mut tags_data);
945 let a2b0_size = (tags_data.len() as u32) - a2b0_start;
946 collected_tags.push(TagInfo {
947 signature: *b"A2B0",
948 offset_in_tags_blob: a2b0_start,
949 size_unpadded: a2b0_size,
950 });
951
952 let b2a0_start = tags_data.len() as u32;
954 create_icc_noop_btoa_tag(&mut tags_data)?;
955 pad_to_4_byte_boundary(&mut tags_data);
956 let b2a0_size = (tags_data.len() as u32) - b2a0_start;
957 collected_tags.push(TagInfo {
958 signature: *b"B2A0",
959 offset_in_tags_blob: b2a0_start,
960 size_unpadded: b2a0_size,
961 });
962 }
963 JxlColorEncoding::RgbColorSpace {
964 transfer_function, ..
965 }
966 | JxlColorEncoding::GrayscaleColorSpace {
967 transfer_function, ..
968 } => {
969 let trc_tag_start_offset = tags_data.len() as u32;
970 let trc_tag_unpadded_size = match transfer_function {
971 JxlTransferFunction::Gamma(g) => {
972 let gamma = 1.0 / g;
974 create_icc_curv_para_tag(&mut tags_data, &[gamma], 0)?
975 }
976 JxlTransferFunction::SRGB => {
977 const PARAMS: [f32; 5] =
979 [2.4, 1.0 / 1.055, 0.055 / 1.055, 1.0 / 12.92, 0.04045];
980 create_icc_curv_para_tag(&mut tags_data, &PARAMS, 3)?
981 }
982 JxlTransferFunction::BT709 => {
983 const PARAMS: [f32; 5] =
985 [1.0 / 0.45, 1.0 / 1.099, 0.099 / 1.099, 1.0 / 4.5, 0.081];
986 create_icc_curv_para_tag(&mut tags_data, &PARAMS, 3)?
987 }
988 JxlTransferFunction::Linear => {
989 const PARAMS: [f32; 5] = [1.0, 1.0, 0.0, 1.0, 0.0];
991 create_icc_curv_para_tag(&mut tags_data, &PARAMS, 3)?
992 }
993 JxlTransferFunction::DCI => {
994 const PARAMS: [f32; 5] = [2.6, 1.0, 0.0, 1.0, 0.0];
996 create_icc_curv_para_tag(&mut tags_data, &PARAMS, 3)?
997 }
998 JxlTransferFunction::HLG | JxlTransferFunction::PQ => {
999 let params = create_table_curve(64, transfer_function, false)?;
1000 create_icc_curv_para_tag(&mut tags_data, params.as_slice(), 3)?
1001 }
1002 };
1003 pad_to_4_byte_boundary(&mut tags_data);
1004
1005 match self {
1006 JxlColorEncoding::GrayscaleColorSpace { .. } => {
1007 collected_tags.push(TagInfo {
1009 signature: *b"kTRC",
1010 offset_in_tags_blob: trc_tag_start_offset,
1011 size_unpadded: trc_tag_unpadded_size,
1012 });
1013 }
1014 _ => {
1015 collected_tags.push(TagInfo {
1018 signature: *b"rTRC",
1019 offset_in_tags_blob: trc_tag_start_offset,
1020 size_unpadded: trc_tag_unpadded_size,
1021 });
1022 collected_tags.push(TagInfo {
1023 signature: *b"gTRC",
1024 offset_in_tags_blob: trc_tag_start_offset, size_unpadded: trc_tag_unpadded_size, });
1027 collected_tags.push(TagInfo {
1028 signature: *b"bTRC",
1029 offset_in_tags_blob: trc_tag_start_offset, size_unpadded: trc_tag_unpadded_size, });
1032 }
1033 }
1034 }
1035 }
1036 }
1037
1038 let mut tag_table_bytes: Vec<u8> = Vec::new();
1040 tag_table_bytes.extend_from_slice(&(collected_tags.len() as u32).to_be_bytes());
1042
1043 let header_size = header.len() as u32;
1044 let tag_table_on_disk_size = 4 + (collected_tags.len() as u32 * 12);
1046
1047 for tag_info in &collected_tags {
1048 tag_table_bytes.extend_from_slice(&tag_info.signature);
1049 let final_profile_offset_for_tag =
1051 header_size + tag_table_on_disk_size + tag_info.offset_in_tags_blob;
1052 tag_table_bytes.extend_from_slice(&final_profile_offset_for_tag.to_be_bytes());
1053 tag_table_bytes.extend_from_slice(&tag_info.size_unpadded.to_be_bytes());
1061 }
1066
1067 let mut final_icc_profile_data: Vec<u8> =
1069 Vec::with_capacity(header.len() + tag_table_bytes.len() + tags_data.len());
1070 final_icc_profile_data.extend_from_slice(&header);
1071 final_icc_profile_data.extend_from_slice(&tag_table_bytes);
1072 final_icc_profile_data.extend_from_slice(&tags_data);
1073
1074 let total_profile_size = final_icc_profile_data.len() as u32;
1076 write_u32_be(&mut final_icc_profile_data, 0, total_profile_size)?;
1077
1078 let mut final_icc_profile_data: Vec<u8> =
1080 Vec::with_capacity(header.len() + tag_table_bytes.len() + tags_data.len());
1081 final_icc_profile_data.extend_from_slice(&header);
1082 final_icc_profile_data.extend_from_slice(&tag_table_bytes);
1083 final_icc_profile_data.extend_from_slice(&tags_data);
1084
1085 let total_profile_size = final_icc_profile_data.len() as u32;
1087 write_u32_be(&mut final_icc_profile_data, 0, total_profile_size)?;
1088
1089 let mut profile_for_checksum = final_icc_profile_data.clone();
1092
1093 if profile_for_checksum.len() >= 84 {
1094 profile_for_checksum[44..48].fill(0);
1096 profile_for_checksum[64..68].fill(0);
1098 }
1100
1101 let checksum = compute_md5(&profile_for_checksum);
1103
1104 if final_icc_profile_data.len() >= 100 {
1107 final_icc_profile_data[84..100].copy_from_slice(&checksum);
1108 }
1109
1110 Ok(Some(final_icc_profile_data))
1111 }
1112
1113 pub fn srgb(grayscale: bool) -> Self {
1114 if grayscale {
1115 JxlColorEncoding::GrayscaleColorSpace {
1116 white_point: JxlWhitePoint::D65,
1117 transfer_function: JxlTransferFunction::SRGB,
1118 rendering_intent: RenderingIntent::Relative,
1119 }
1120 } else {
1121 JxlColorEncoding::RgbColorSpace {
1122 white_point: JxlWhitePoint::D65,
1123 primaries: JxlPrimaries::SRGB,
1124 transfer_function: JxlTransferFunction::SRGB,
1125 rendering_intent: RenderingIntent::Relative,
1126 }
1127 }
1128 }
1129
1130 pub fn linear_srgb(grayscale: bool) -> Self {
1134 if grayscale {
1135 JxlColorEncoding::GrayscaleColorSpace {
1136 white_point: JxlWhitePoint::D65,
1137 transfer_function: JxlTransferFunction::Linear,
1138 rendering_intent: RenderingIntent::Relative,
1139 }
1140 } else {
1141 JxlColorEncoding::RgbColorSpace {
1142 white_point: JxlWhitePoint::D65,
1143 primaries: JxlPrimaries::SRGB,
1144 transfer_function: JxlTransferFunction::Linear,
1145 rendering_intent: RenderingIntent::Relative,
1146 }
1147 }
1148 }
1149
1150 pub fn with_linear_tf(&self) -> Self {
1153 match self {
1154 JxlColorEncoding::RgbColorSpace {
1155 white_point,
1156 primaries,
1157 rendering_intent,
1158 ..
1159 } => JxlColorEncoding::RgbColorSpace {
1160 white_point: white_point.clone(),
1161 primaries: primaries.clone(),
1162 transfer_function: JxlTransferFunction::Linear,
1163 rendering_intent: *rendering_intent,
1164 },
1165 JxlColorEncoding::GrayscaleColorSpace {
1166 white_point,
1167 rendering_intent,
1168 ..
1169 } => JxlColorEncoding::GrayscaleColorSpace {
1170 white_point: white_point.clone(),
1171 transfer_function: JxlTransferFunction::Linear,
1172 rendering_intent: *rendering_intent,
1173 },
1174 JxlColorEncoding::XYB { .. } => Self::linear_srgb(false),
1175 }
1176 }
1177
1178 pub fn channels(&self) -> usize {
1181 match self {
1182 JxlColorEncoding::RgbColorSpace { .. } => 3,
1183 JxlColorEncoding::GrayscaleColorSpace { .. } => 1,
1184 JxlColorEncoding::XYB { .. } => 3,
1185 }
1186 }
1187}
1188
1189#[derive(Clone, Debug, PartialEq)]
1190pub enum JxlColorProfile {
1191 Icc(Vec<u8>),
1192 Simple(JxlColorEncoding),
1193}
1194
1195impl JxlColorProfile {
1196 pub fn as_icc(&self) -> Cow<'_, Vec<u8>> {
1202 match self {
1203 Self::Icc(x) => Cow::Borrowed(x),
1204 Self::Simple(encoding) => Cow::Owned(encoding.maybe_create_profile().unwrap().unwrap()),
1205 }
1206 }
1207
1208 pub fn try_as_icc(&self) -> Option<Cow<'_, Vec<u8>>> {
1212 match self {
1213 Self::Icc(x) => Some(Cow::Borrowed(x)),
1214 Self::Simple(encoding) => encoding
1215 .maybe_create_profile()
1216 .ok()
1217 .flatten()
1218 .map(Cow::Owned),
1219 }
1220 }
1221
1222 pub fn same_color_encoding(&self, other: &Self) -> bool {
1228 match (self, other) {
1229 (Self::Simple(a), Self::Simple(b)) => {
1230 use JxlColorEncoding::*;
1231 match (a, b) {
1232 (
1233 RgbColorSpace {
1234 white_point: wp_a,
1235 primaries: prim_a,
1236 transfer_function: tf_a,
1237 ..
1238 },
1239 RgbColorSpace {
1240 white_point: wp_b,
1241 primaries: prim_b,
1242 transfer_function: tf_b,
1243 ..
1244 },
1245 ) => wp_a == wp_b && prim_a == prim_b && tf_a == tf_b,
1246 (
1247 GrayscaleColorSpace {
1248 white_point: wp_a,
1249 transfer_function: tf_a,
1250 ..
1251 },
1252 GrayscaleColorSpace {
1253 white_point: wp_b,
1254 transfer_function: tf_b,
1255 ..
1256 },
1257 ) => wp_a == wp_b && tf_a == tf_b,
1258 _ => false,
1260 }
1261 }
1262 (Self::Icc(a), Self::Icc(b)) => a == b && !self.is_cmyk(),
1269 _ => false,
1271 }
1272 }
1273
1274 pub fn transfer_function(&self) -> Option<&JxlTransferFunction> {
1277 match self {
1278 Self::Simple(JxlColorEncoding::RgbColorSpace {
1279 transfer_function, ..
1280 })
1281 | Self::Simple(JxlColorEncoding::GrayscaleColorSpace {
1282 transfer_function, ..
1283 }) => Some(transfer_function),
1284 _ => None,
1285 }
1286 }
1287
1288 pub fn can_output_to(&self) -> bool {
1294 match self {
1295 Self::Icc(_) => false,
1296 Self::Simple(JxlColorEncoding::RgbColorSpace { .. }) => true,
1297 Self::Simple(JxlColorEncoding::GrayscaleColorSpace { white_point, .. }) => {
1298 *white_point == JxlWhitePoint::D65
1300 }
1301 Self::Simple(JxlColorEncoding::XYB { .. }) => {
1302 false
1304 }
1305 }
1306 }
1307
1308 pub fn with_linear_tf(&self) -> Option<Self> {
1313 match self {
1314 Self::Icc(_) => None,
1315 Self::Simple(encoding) => Some(Self::Simple(encoding.with_linear_tf())),
1316 }
1317 }
1318
1319 pub fn channels(&self) -> usize {
1324 match self {
1325 Self::Simple(enc) => enc.channels(),
1326 Self::Icc(icc_data) => {
1327 if icc_data.len() >= 20 {
1329 match &icc_data[16..20] {
1330 b"GRAY" => 1,
1331 b"RGB " => 3,
1332 b"CMYK" => 4,
1333 _ => 3, }
1335 } else {
1336 3 }
1338 }
1339 }
1340 }
1341
1342 pub fn is_cmyk(&self) -> bool {
1347 match self {
1348 Self::Simple(_) => false, Self::Icc(icc_data) => {
1350 if icc_data.len() >= 20 {
1352 &icc_data[16..20] == b"CMYK"
1353 } else {
1354 false
1355 }
1356 }
1357 }
1358 }
1359}
1360
1361impl fmt::Display for JxlColorProfile {
1362 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1363 match self {
1364 Self::Icc(_) => f.write_str("ICC"),
1365 Self::Simple(enc) => write!(f, "{}", enc),
1366 }
1367 }
1368}
1369
1370pub trait JxlCmsTransformer {
1371 fn do_transform(&mut self, input: &[f32], output: &mut [f32]) -> Result<()>;
1376
1377 fn do_transform_inplace(&mut self, inout: &mut [f32]) -> Result<()>;
1382}
1383
1384pub trait JxlCms {
1385 fn initialize_transforms(
1391 &self,
1392 n: usize,
1393 max_pixels_per_transform: usize,
1394 input: JxlColorProfile,
1395 output: JxlColorProfile,
1396 intensity_target: f32,
1397 ) -> Result<(usize, Vec<Box<dyn JxlCmsTransformer + Send + Sync>>)>;
1398}
1399
1400fn write_u32_be(slice: &mut [u8], pos: usize, value: u32) -> Result<(), Error> {
1402 if pos.checked_add(4).is_none_or(|end| end > slice.len()) {
1403 return Err(Error::IccWriteOutOfBounds);
1404 }
1405 slice[pos..pos + 4].copy_from_slice(&value.to_be_bytes());
1406 Ok(())
1407}
1408
1409fn write_u16_be(slice: &mut [u8], pos: usize, value: u16) -> Result<(), Error> {
1411 if pos.checked_add(2).is_none_or(|end| end > slice.len()) {
1412 return Err(Error::IccWriteOutOfBounds);
1413 }
1414 slice[pos..pos + 2].copy_from_slice(&value.to_be_bytes());
1415 Ok(())
1416}
1417
1418fn write_icc_tag(slice: &mut [u8], pos: usize, tag_str: &str) -> Result<(), Error> {
1420 if tag_str.len() != 4 || !tag_str.is_ascii() {
1421 return Err(Error::IccInvalidTagString(tag_str.to_string()));
1422 }
1423 if pos.checked_add(4).is_none_or(|end| end > slice.len()) {
1424 return Err(Error::IccWriteOutOfBounds);
1425 }
1426 slice[pos..pos + 4].copy_from_slice(tag_str.as_bytes());
1427 Ok(())
1428}
1429
1430fn create_icc_mluc_tag(tags: &mut Vec<u8>, text: &str) -> Result<(), Error> {
1435 if !text.is_ascii() {
1438 return Err(Error::IccMlucTextNotAscii(text.to_string()));
1439 }
1440 tags.extend_from_slice(b"mluc");
1442 tags.extend_from_slice(&0u32.to_be_bytes());
1444 tags.extend_from_slice(&1u32.to_be_bytes());
1446 tags.extend_from_slice(&12u32.to_be_bytes());
1449 tags.extend_from_slice(b"en");
1451 tags.extend_from_slice(b"US");
1453 let string_actual_byte_length = text.len() * 2;
1456 tags.extend_from_slice(&(string_actual_byte_length as u32).to_be_bytes());
1457 tags.extend_from_slice(&28u32.to_be_bytes());
1460 for ascii_char_code in text.as_bytes() {
1463 tags.push(0u8);
1464 tags.push(*ascii_char_code);
1465 }
1466
1467 Ok(())
1468}
1469
1470struct TagInfo {
1471 signature: [u8; 4],
1472 offset_in_tags_blob: u32,
1474 size_unpadded: u32,
1476}
1477
1478fn pad_to_4_byte_boundary(data: &mut Vec<u8>) {
1479 data.resize(data.len().next_multiple_of(4), 0u8);
1480}
1481
1482fn append_s15_fixed_16(tags_data: &mut Vec<u8>, value: f32) -> Result<(), Error> {
1486 if !(value.is_finite() && (-32767.995..=32767.995).contains(&value)) {
1490 return Err(Error::IccValueOutOfRangeS15Fixed16(value));
1491 }
1492
1493 let scaled_value = (value * 65536.0).round();
1495 let int_value = scaled_value as i32;
1497 tags_data.extend_from_slice(&int_value.to_be_bytes());
1498 Ok(())
1499}
1500
1501fn create_icc_xyz_tag(tags_data: &mut Vec<u8>, xyz_color: &[f32; 3]) -> Result<TagInfo, Error> {
1504 let start_offset = tags_data.len() as u32;
1506 let signature = b"XYZ ";
1507 tags_data.extend_from_slice(signature);
1508
1509 tags_data.extend_from_slice(&0u32.to_be_bytes());
1511
1512 for &val in xyz_color {
1514 append_s15_fixed_16(tags_data, val)?;
1515 }
1516
1517 Ok(TagInfo {
1518 signature: *b"wtpt",
1519 offset_in_tags_blob: start_offset,
1520 size_unpadded: (tags_data.len() as u32) - start_offset,
1521 })
1522}
1523
1524fn create_icc_chad_tag(
1525 tags_data: &mut Vec<u8>,
1526 chad_matrix: &Matrix3x3<f32>,
1527) -> Result<TagInfo, Error> {
1528 let signature = b"sf32";
1530 let start_offset = tags_data.len() as u32;
1531 tags_data.extend_from_slice(signature);
1532
1533 tags_data.extend_from_slice(&0u32.to_be_bytes());
1535
1536 for row_array in chad_matrix.iter() {
1539 for &value in row_array.iter() {
1540 append_s15_fixed_16(tags_data, value)?;
1541 }
1542 }
1543 Ok(TagInfo {
1544 signature: *b"chad",
1545 offset_in_tags_blob: start_offset,
1546 size_unpadded: (tags_data.len() as u32) - start_offset,
1547 })
1548}
1549
1550fn cie_xyz_from_white_cie_xy(wx: f32, wy: f32) -> Result<[f32; 3], Error> {
1552 if wy.abs() < 1e-12 {
1554 return Err(Error::IccInvalidWhitePointY(wy));
1555 }
1556 let factor = 1.0 / wy;
1557 let x_val = wx * factor;
1558 let y_val = 1.0f32;
1559 let z_val = (1.0 - wx - wy) * factor;
1560 Ok([x_val, y_val, z_val])
1561}
1562
1563fn create_icc_curv_para_tag(
1566 tags_data: &mut Vec<u8>,
1567 params: &[f32],
1568 curve_type: u16,
1569) -> Result<u32, Error> {
1570 let start_offset = tags_data.len();
1571 tags_data.extend_from_slice(b"para");
1573 tags_data.extend_from_slice(&0u32.to_be_bytes());
1575 tags_data.extend_from_slice(&curve_type.to_be_bytes());
1577 tags_data.extend_from_slice(&0u16.to_be_bytes());
1579 for ¶m in params {
1581 append_s15_fixed_16(tags_data, param)?;
1582 }
1583 Ok((tags_data.len() - start_offset) as u32)
1584}
1585
1586fn display_from_encoded_pq(display_intensity_target: f32, mut e: f64) -> f64 {
1587 const M1: f64 = 2610.0 / 16384.0;
1588 const M2: f64 = (2523.0 / 4096.0) * 128.0;
1589 const C1: f64 = 3424.0 / 4096.0;
1590 const C2: f64 = (2413.0 / 4096.0) * 32.0;
1591 const C3: f64 = (2392.0 / 4096.0) * 32.0;
1592 if e == 0.0 {
1594 return 0.0;
1595 }
1596
1597 let original_sign = e.signum();
1600 e = e.abs();
1601
1602 let xp = e.powf(1.0 / M2);
1604 let num = (xp - C1).max(0.0);
1605 let den = C2 - C3 * xp;
1606
1607 debug_assert!(den != 0.0, "PQ transfer function denominator is zero.");
1611
1612 let d = (num / den).powf(1.0 / M1);
1613
1614 debug_assert!(
1616 d >= 0.0,
1617 "PQ intermediate value `d` should not be negative."
1618 );
1619
1620 let scaled_d = d * (10000.0 / display_intensity_target as f64);
1623
1624 scaled_d.copysign(original_sign)
1626}
1627
1628#[allow(non_camel_case_types)]
1641struct TF_HLG;
1642
1643impl TF_HLG {
1644 const A: f64 = 0.17883277;
1646 const RA: f64 = 1.0 / Self::A;
1647 const B: f64 = 1.0 - 4.0 * Self::A;
1648 const C: f64 = 0.5599107295;
1649 const INV_12: f64 = 1.0 / 12.0;
1650
1651 #[inline]
1657 fn display_from_encoded(e: f64) -> f64 {
1658 Self::inv_oetf(e)
1659 }
1660
1661 #[inline]
1666 #[allow(dead_code)]
1667 fn encoded_from_display(d: f64) -> f64 {
1668 Self::oetf(d)
1669 }
1670
1671 fn oetf(mut s: f64) -> f64 {
1673 if s == 0.0 {
1674 return 0.0;
1675 }
1676 let original_sign = s.signum();
1677 s = s.abs();
1678
1679 let e = if s <= Self::INV_12 {
1680 (3.0 * s).sqrt()
1681 } else {
1682 Self::A * (12.0 * s - Self::B).ln() + Self::C
1683 };
1684
1685 debug_assert!(e > 0.0);
1687
1688 e.copysign(original_sign)
1689 }
1690
1691 fn inv_oetf(mut e: f64) -> f64 {
1693 if e == 0.0 {
1694 return 0.0;
1695 }
1696 let original_sign = e.signum();
1697 e = e.abs();
1698
1699 let s = if e <= 0.5 {
1700 e * e * (1.0 / 3.0)
1702 } else {
1703 (((e - Self::C) * Self::RA).exp() + Self::B) * Self::INV_12
1704 };
1705
1706 debug_assert!(s >= 0.0);
1708
1709 s.copysign(original_sign)
1710 }
1711}
1712
1713fn create_table_curve(
1726 n: usize,
1727 tf: &JxlTransferFunction,
1728 _tone_map: bool,
1729) -> Result<Vec<f32>, Error> {
1730 if n > 4096 {
1733 return Err(Error::IccTableSizeExceeded(n));
1734 }
1735
1736 if !matches!(tf, JxlTransferFunction::PQ | JxlTransferFunction::HLG) {
1737 return Err(Error::IccUnsupportedTransferFunction);
1738 }
1739
1740 const PQ_INTENSITY_TARGET: f64 = 10000.0;
1742
1743 let mut table = Vec::with_capacity(n);
1744 for i in 0..n {
1745 let x = i as f64 / (n - 1) as f64;
1747
1748 let y = match tf {
1751 JxlTransferFunction::HLG => TF_HLG::display_from_encoded(x),
1752 JxlTransferFunction::PQ => {
1753 display_from_encoded_pq(PQ_INTENSITY_TARGET as f32, x) / PQ_INTENSITY_TARGET
1756 }
1757 _ => unreachable!(), };
1759
1760 let y_clamped = y.clamp(0.0, 1.0);
1770
1771 table.push(y_clamped as f32);
1773 }
1774
1775 Ok(table)
1776}
1777
1778struct Rec2408ToneMapper {
1785 source_range: (f32, f32), target_range: (f32, f32),
1787 luminances: [f32; 3], pq_mastering_min: f32,
1791 #[allow(dead_code)] pq_mastering_max: f32,
1793 pq_mastering_range: f32,
1794 inv_pq_mastering_range: f32,
1795 min_lum: f32,
1796 max_lum: f32,
1797 ks: f32,
1798 inv_one_minus_ks: f32,
1799 normalizer: f32,
1800 inv_target_peak: f32,
1801}
1802
1803impl Rec2408ToneMapper {
1804 fn new(source_range: (f32, f32), target_range: (f32, f32), luminances: [f32; 3]) -> Self {
1805 let pq_mastering_min = Self::linear_to_pq(source_range.0);
1806 let pq_mastering_max = Self::linear_to_pq(source_range.1);
1807 let pq_mastering_range = pq_mastering_max - pq_mastering_min;
1808 let inv_pq_mastering_range = 1.0 / pq_mastering_range;
1809
1810 let min_lum =
1811 (Self::linear_to_pq(target_range.0) - pq_mastering_min) * inv_pq_mastering_range;
1812 let max_lum =
1813 (Self::linear_to_pq(target_range.1) - pq_mastering_min) * inv_pq_mastering_range;
1814 let ks = 1.5 * max_lum - 0.5;
1815
1816 Self {
1817 source_range,
1818 target_range,
1819 luminances,
1820 pq_mastering_min,
1821 pq_mastering_max,
1822 pq_mastering_range,
1823 inv_pq_mastering_range,
1824 min_lum,
1825 max_lum,
1826 ks,
1827 inv_one_minus_ks: 1.0 / (1.0 - ks).max(1e-6),
1828 normalizer: source_range.1 / target_range.1,
1829 inv_target_peak: 1.0 / target_range.1,
1830 }
1831 }
1832
1833 fn linear_to_pq(luminance: f32) -> f32 {
1836 let mut val = [luminance / 10000.0]; linear_to_pq_precise(10000.0, &mut val);
1838 val[0]
1839 }
1840
1841 fn pq_to_linear(encoded: f32) -> f32 {
1844 let mut val = [encoded];
1845 pq_to_linear_precise(10000.0, &mut val);
1846 val[0] * 10000.0
1847 }
1848
1849 fn t(&self, a: f32) -> f32 {
1850 (a - self.ks) * self.inv_one_minus_ks
1851 }
1852
1853 fn p(&self, b: f32) -> f32 {
1854 let t_b = self.t(b);
1855 let t_b_2 = t_b * t_b;
1856 let t_b_3 = t_b_2 * t_b;
1857 (2.0 * t_b_3 - 3.0 * t_b_2 + 1.0) * self.ks
1858 + (t_b_3 - 2.0 * t_b_2 + t_b) * (1.0 - self.ks)
1859 + (-2.0 * t_b_3 + 3.0 * t_b_2) * self.max_lum
1860 }
1861
1862 fn tone_map(&self, rgb: &mut [f32; 3]) {
1864 let luminance = self.source_range.1
1865 * (self.luminances[0] * rgb[0]
1866 + self.luminances[1] * rgb[1]
1867 + self.luminances[2] * rgb[2]);
1868
1869 let normalized_pq = ((Self::linear_to_pq(luminance) - self.pq_mastering_min)
1870 * self.inv_pq_mastering_range)
1871 .min(1.0);
1872
1873 let e2 = if normalized_pq < self.ks {
1874 normalized_pq
1875 } else {
1876 self.p(normalized_pq)
1877 };
1878
1879 let one_minus_e2 = 1.0 - e2;
1880 let one_minus_e2_2 = one_minus_e2 * one_minus_e2;
1881 let one_minus_e2_4 = one_minus_e2_2 * one_minus_e2_2;
1882 let e3 = self.min_lum * one_minus_e2_4 + e2;
1883 let e4 = e3 * self.pq_mastering_range + self.pq_mastering_min;
1884 let d4 = Self::pq_to_linear(e4);
1885 let new_luminance = d4.clamp(0.0, self.target_range.1);
1886
1887 let min_luminance = 1e-6;
1888 let use_cap = luminance <= min_luminance;
1889 let ratio = new_luminance / luminance.max(min_luminance);
1890 let cap = new_luminance * self.inv_target_peak;
1891 let multiplier = ratio * self.normalizer;
1892
1893 for c in rgb.iter_mut() {
1894 *c = if use_cap { cap } else { *c * multiplier };
1895 }
1896 }
1897}
1898
1899fn apply_hlg_ootf(rgb: &mut [f32; 3], target_luminance: f32, luminances: [f32; 3]) {
1903 let system_gamma = 1.2_f32 * 1.111_f32.powf((target_luminance / 1e3).log2());
1906 let exp = system_gamma - 1.0;
1907
1908 if exp.abs() < 0.1 {
1909 return;
1910 }
1911
1912 let mixed = rgb[0] * luminances[0] + rgb[1] * luminances[1] + rgb[2] * luminances[2];
1914 let mult = crate::util::fast_powf(mixed, exp);
1915 rgb[0] *= mult;
1916 rgb[1] *= mult;
1917 rgb[2] *= mult;
1918}
1919
1920fn gamut_map(rgb: &mut [f32; 3], luminances: &[f32; 3], preserve_saturation: f32) {
1922 let luminance = luminances[0] * rgb[0] + luminances[1] * rgb[1] + luminances[2] * rgb[2];
1923
1924 let mut gray_mix_saturation = 0.0_f32;
1925 let mut gray_mix_luminance = 0.0_f32;
1926
1927 for &val in rgb.iter() {
1928 let val_minus_gray = val - luminance;
1929 let inv_val_minus_gray = if val_minus_gray == 0.0 {
1930 1.0
1931 } else {
1932 1.0 / val_minus_gray
1933 };
1934 let val_over_val_minus_gray = val * inv_val_minus_gray;
1935
1936 if val_minus_gray < 0.0 {
1937 gray_mix_saturation = gray_mix_saturation.max(val_over_val_minus_gray);
1938 }
1939
1940 gray_mix_luminance = gray_mix_luminance.max(if val_minus_gray <= 0.0 {
1941 gray_mix_saturation
1942 } else {
1943 val_over_val_minus_gray - inv_val_minus_gray
1944 });
1945 }
1946
1947 let gray_mix = (preserve_saturation * (gray_mix_saturation - gray_mix_luminance)
1948 + gray_mix_luminance)
1949 .clamp(0.0, 1.0);
1950
1951 for val in rgb.iter_mut() {
1952 *val = gray_mix * (luminance - *val) + *val;
1953 }
1954
1955 let max_clr = rgb[0].max(rgb[1]).max(rgb[2]).max(1.0);
1956 let normalizer = 1.0 / max_clr;
1957 for v in rgb.iter_mut() {
1958 *v *= normalizer;
1959 }
1960}
1961
1962fn tone_map_pixel(
1964 transfer_function: &JxlTransferFunction,
1965 primaries: &JxlPrimaries,
1966 white_point: &JxlWhitePoint,
1967 input: [f32; 3],
1968) -> Result<[u8; 3], Error> {
1969 let primaries_coords = primaries.to_xy_coords();
1971 let (rx, ry) = primaries_coords[0];
1972 let (gx, gy) = primaries_coords[1];
1973 let (bx, by) = primaries_coords[2];
1974 let (wx, wy) = white_point.to_xy_coords();
1975
1976 let primaries_xyz = primaries_to_xyz(rx, ry, gx, gy, bx, by, wx, wy)?;
1978
1979 let luminances = [
1981 primaries_xyz[1][0] as f32,
1982 primaries_xyz[1][1] as f32,
1983 primaries_xyz[1][2] as f32,
1984 ];
1985
1986 let mut linear = match transfer_function {
1988 JxlTransferFunction::PQ => {
1989 [
1991 Rec2408ToneMapper::pq_to_linear(input[0]) / 10000.0,
1992 Rec2408ToneMapper::pq_to_linear(input[1]) / 10000.0,
1993 Rec2408ToneMapper::pq_to_linear(input[2]) / 10000.0,
1994 ]
1995 }
1996 JxlTransferFunction::HLG => {
1997 let mut vals = [input[0], input[1], input[2]];
1999 hlg_to_scene(&mut vals);
2000 vals
2001 }
2002 _ => return Err(Error::IccUnsupportedTransferFunction),
2003 };
2004
2005 match transfer_function {
2007 JxlTransferFunction::PQ => {
2008 let tone_mapper = Rec2408ToneMapper::new(
2009 (0.0, 10000.0), (0.0, 250.0), luminances,
2012 );
2013 tone_mapper.tone_map(&mut linear);
2014 }
2015 JxlTransferFunction::HLG => {
2016 apply_hlg_ootf(&mut linear, 80.0, luminances);
2018 }
2019 _ => {}
2020 }
2021
2022 gamut_map(&mut linear, &luminances, 0.3);
2024
2025 let chad = adapt_to_xyz_d50(wx, wy)?;
2027
2028 let to_xyzd50_f64 = mul_3x3_matrix(&chad, &primaries_xyz);
2031
2032 let to_xyzd50: [[f32; 3]; 3] =
2034 std::array::from_fn(|r| std::array::from_fn(|c| to_xyzd50_f64[r][c] as f32));
2035
2036 let xyz = [
2038 linear[0] * to_xyzd50[0][0] + linear[1] * to_xyzd50[0][1] + linear[2] * to_xyzd50[0][2],
2039 linear[0] * to_xyzd50[1][0] + linear[1] * to_xyzd50[1][1] + linear[2] * to_xyzd50[1][2],
2040 linear[0] * to_xyzd50[2][0] + linear[1] * to_xyzd50[2][1] + linear[2] * to_xyzd50[2][2],
2041 ];
2042
2043 const XN: f32 = 0.964212;
2046 const YN: f32 = 1.0;
2047 const ZN: f32 = 0.825188;
2048 const DELTA: f32 = 6.0 / 29.0;
2049
2050 let lab_f = |x: f32| -> f32 {
2051 if x <= DELTA * DELTA * DELTA {
2052 x * (1.0 / (3.0 * DELTA * DELTA)) + 4.0 / 29.0
2053 } else {
2054 x.cbrt()
2055 }
2056 };
2057
2058 let f_x = lab_f(xyz[0] / XN);
2059 let f_y = lab_f(xyz[1] / YN);
2060 let f_z = lab_f(xyz[2] / ZN);
2061
2062 Ok([
2067 (255.0 * (1.16 * f_y - 0.16).clamp(0.0, 1.0)).round() as u8,
2068 (128.0 + (500.0 * (f_x - f_y)).clamp(-128.0, 127.0)).round() as u8,
2069 (128.0 + (200.0 * (f_y - f_z)).clamp(-128.0, 127.0)).round() as u8,
2070 ])
2071}
2072
2073fn create_icc_lut_atob_tag_for_xyb(tags: &mut Vec<u8>) -> Result<(), Error> {
2075 use super::xyb_constants::*;
2076 use byteorder::{BigEndian, WriteBytesExt};
2077
2078 tags.extend_from_slice(b"mAB ");
2080 tags.write_u32::<BigEndian>(0)
2082 .map_err(|_| Error::InvalidIccStream)?;
2083 tags.push(3);
2085 tags.push(3);
2087 tags.write_u16::<BigEndian>(0)
2089 .map_err(|_| Error::InvalidIccStream)?;
2090
2091 tags.write_u32::<BigEndian>(32)
2094 .map_err(|_| Error::InvalidIccStream)?;
2095 tags.write_u32::<BigEndian>(244)
2097 .map_err(|_| Error::InvalidIccStream)?;
2098 tags.write_u32::<BigEndian>(148)
2100 .map_err(|_| Error::InvalidIccStream)?;
2101 tags.write_u32::<BigEndian>(80)
2103 .map_err(|_| Error::InvalidIccStream)?;
2104 tags.write_u32::<BigEndian>(32)
2106 .map_err(|_| Error::InvalidIccStream)?;
2107
2108 for _ in 0..3 {
2112 create_icc_curv_para_tag(tags, &[1.0], 0)?;
2113 }
2114
2115 for i in 0..16 {
2118 tags.push(if i < 3 { 2 } else { 0 });
2119 }
2120 tags.push(2);
2122 tags.push(0);
2124 tags.write_u16::<BigEndian>(0)
2125 .map_err(|_| Error::InvalidIccStream)?;
2126
2127 let cube = unscaled_a2b_cube_full();
2129 for row_x in &cube {
2130 for row_y in row_x {
2131 for out_f in row_y {
2132 for &val_f in out_f {
2133 let val = (65535.0 * val_f).round().clamp(0.0, 65535.0) as u16;
2134 tags.write_u16::<BigEndian>(val)
2135 .map_err(|_| Error::InvalidIccStream)?;
2136 }
2137 }
2138 }
2139 }
2140
2141 let scale = xyb_scale();
2145 for i in 0..3 {
2146 let b = -XYB_OFFSET[i] - NEG_OPSIN_ABSORBANCE_BIAS_RGB[i].cbrt();
2147 let params = [
2148 3.0, 1.0 / scale[i], b, 0.0, (-b * scale[i]).max(0.0), ];
2154 create_icc_curv_para_tag(tags, ¶ms, 3)?;
2155 }
2156
2157 for v in XYB_ICC_MATRIX {
2160 append_s15_fixed_16(tags, v as f32)?;
2161 }
2162
2163 for i in 0..3 {
2165 let mut intercept: f64 = 0.0;
2166 for j in 0..3 {
2167 intercept += XYB_ICC_MATRIX[i * 3 + j] * (NEG_OPSIN_ABSORBANCE_BIAS_RGB[j] as f64);
2168 }
2169 append_s15_fixed_16(tags, intercept as f32)?;
2170 }
2171
2172 Ok(())
2173}
2174
2175fn create_icc_lut_atob_tag_for_hdr(
2177 transfer_function: &JxlTransferFunction,
2178 primaries: &JxlPrimaries,
2179 white_point: &JxlWhitePoint,
2180 tags: &mut Vec<u8>,
2181) -> Result<(), Error> {
2182 const LUT_DIM: usize = 9; tags.extend_from_slice(b"mft1");
2186 tags.extend_from_slice(&0u32.to_be_bytes());
2188 tags.push(3);
2190 tags.push(3);
2192 tags.push(LUT_DIM as u8);
2194 tags.push(0);
2196
2197 for i in 0..3 {
2199 for j in 0..3 {
2200 let val: f32 = if i == j { 1.0 } else { 0.0 };
2201 append_s15_fixed_16(tags, val)?;
2202 }
2203 }
2204
2205 for _ in 0..3 {
2207 for i in 0..256 {
2208 tags.push(i as u8);
2209 }
2210 }
2211
2212 for ix in 0..LUT_DIM {
2214 for iy in 0..LUT_DIM {
2215 for ib in 0..LUT_DIM {
2216 let input = [
2217 ix as f32 / (LUT_DIM - 1) as f32,
2218 iy as f32 / (LUT_DIM - 1) as f32,
2219 ib as f32 / (LUT_DIM - 1) as f32,
2220 ];
2221 let pcslab = tone_map_pixel(transfer_function, primaries, white_point, input)?;
2222 tags.extend_from_slice(&pcslab);
2223 }
2224 }
2225 }
2226
2227 for _ in 0..3 {
2229 for i in 0..256 {
2230 tags.push(i as u8);
2231 }
2232 }
2233
2234 Ok(())
2235}
2236
2237fn create_icc_noop_btoa_tag(tags: &mut Vec<u8>) -> Result<(), Error> {
2239 tags.extend_from_slice(b"mBA ");
2241 tags.extend_from_slice(&0u32.to_be_bytes());
2243 tags.push(3);
2245 tags.push(3);
2247 tags.extend_from_slice(&0u16.to_be_bytes());
2249 tags.extend_from_slice(&32u32.to_be_bytes());
2251 tags.extend_from_slice(&0u32.to_be_bytes());
2253 tags.extend_from_slice(&0u32.to_be_bytes());
2255 tags.extend_from_slice(&0u32.to_be_bytes());
2257 tags.extend_from_slice(&0u32.to_be_bytes());
2259
2260 for _ in 0..3 {
2263 create_icc_curv_para_tag(tags, &[1.0], 0)?;
2264 }
2265
2266 Ok(())
2267}
2268
2269#[cfg(test)]
2270mod test {
2271 use super::*;
2272
2273 #[test]
2274 fn test_md5() {
2275 let test_cases = vec![
2277 ("", "d41d8cd98f00b204e9800998ecf8427e"),
2278 (
2279 "The quick brown fox jumps over the lazy dog",
2280 "9e107d9d372bb6826bd81d3542a419d6",
2281 ),
2282 ("abc", "900150983cd24fb0d6963f7d28e17f72"),
2283 ("message digest", "f96b697d7cb7938d525a2f31aaf161d0"),
2284 (
2285 "abcdefghijklmnopqrstuvwxyz",
2286 "c3fcd3d76192e4007dfb496cca67e13b",
2287 ),
2288 (
2289 "12345678901234567890123456789012345678901234567890123456789012345678901234567890",
2290 "57edf4a22be3c955ac49da2e2107b67a",
2291 ),
2292 ];
2293
2294 for (input, expected) in test_cases {
2295 let hash = compute_md5(input.as_bytes());
2296 let hex: String = hash.iter().map(|e| format!("{:02x}", e)).collect();
2297 assert_eq!(hex, expected, "Failed for input: '{}'", input);
2298 }
2299 }
2300
2301 #[test]
2302 fn test_description() {
2303 assert_eq!(
2304 JxlColorEncoding::srgb(false).get_color_encoding_description(),
2305 "RGB_D65_SRG_Rel_SRG"
2306 );
2307 assert_eq!(
2308 JxlColorEncoding::srgb(true).get_color_encoding_description(),
2309 "Gra_D65_Rel_SRG"
2310 );
2311 assert_eq!(
2312 JxlColorEncoding::RgbColorSpace {
2313 white_point: JxlWhitePoint::D65,
2314 primaries: JxlPrimaries::BT2100,
2315 transfer_function: JxlTransferFunction::Gamma(1.7),
2316 rendering_intent: RenderingIntent::Relative
2317 }
2318 .get_color_encoding_description(),
2319 "RGB_D65_202_Rel_g1.7000000"
2320 );
2321 assert_eq!(
2322 JxlColorEncoding::RgbColorSpace {
2323 white_point: JxlWhitePoint::D65,
2324 primaries: JxlPrimaries::P3,
2325 transfer_function: JxlTransferFunction::SRGB,
2326 rendering_intent: RenderingIntent::Perceptual
2327 }
2328 .get_color_encoding_description(),
2329 "DisplayP3"
2330 );
2331 }
2332
2333 #[test]
2334 fn test_rec2408_tone_mapper() {
2335 let luminances = [0.2627, 0.6780, 0.0593]; let tone_mapper = Rec2408ToneMapper::new((0.0, 10000.0), (0.0, 250.0), luminances);
2338
2339 let mut rgb = [0.8, 0.8, 0.8]; tone_mapper.tone_map(&mut rgb);
2342 assert!(rgb[0] >= 0.0 && rgb[0] <= 1.0, "R out of range: {}", rgb[0]);
2344 assert!(rgb[1] >= 0.0 && rgb[1] <= 1.0, "G out of range: {}", rgb[1]);
2345 assert!(rgb[2] >= 0.0 && rgb[2] <= 1.0, "B out of range: {}", rgb[2]);
2346
2347 let mut rgb_dark = [0.1, 0.1, 0.1];
2349 tone_mapper.tone_map(&mut rgb_dark);
2350 assert!(
2351 rgb_dark[0] >= 0.0 && rgb_dark[0] <= 1.0,
2352 "R out of range: {}",
2353 rgb_dark[0]
2354 );
2355 }
2356
2357 #[test]
2358 fn test_hlg_ootf() {
2359 let luminances = [0.2627, 0.6780, 0.0593];
2360
2361 let mut rgb = [0.5, 0.5, 0.5];
2362 apply_hlg_ootf(&mut rgb, 80.0, luminances);
2363 assert!(rgb[0] >= 0.0, "R should be non-negative");
2365 assert!(rgb[1] >= 0.0, "G should be non-negative");
2366 assert!(rgb[2] >= 0.0, "B should be non-negative");
2367 }
2368
2369 #[test]
2370 fn test_gamut_map() {
2371 let luminances = [0.2627, 0.6780, 0.0593];
2372
2373 let mut rgb = [-0.1, 0.5, 0.5];
2375 gamut_map(&mut rgb, &luminances, 0.3);
2376 assert!(rgb[0] >= 0.0, "R should be non-negative after gamut map");
2378 assert!(rgb[1] >= 0.0, "G should be non-negative after gamut map");
2379 assert!(rgb[2] >= 0.0, "B should be non-negative after gamut map");
2380
2381 let mut rgb_valid = [0.5, 0.3, 0.2];
2383 gamut_map(&mut rgb_valid, &luminances, 0.3);
2384 assert!(rgb_valid[0] >= 0.0 && rgb_valid[0] <= 1.0);
2385 assert!(rgb_valid[1] >= 0.0 && rgb_valid[1] <= 1.0);
2386 assert!(rgb_valid[2] >= 0.0 && rgb_valid[2] <= 1.0);
2387 }
2388
2389 #[test]
2390 fn test_tone_map_pixel_pq() {
2391 let result = tone_map_pixel(
2392 &JxlTransferFunction::PQ,
2393 &JxlPrimaries::BT2100,
2394 &JxlWhitePoint::D65,
2395 [0.5, 0.5, 0.5],
2396 );
2397 assert!(result.is_ok());
2398 let lab = result.unwrap();
2399 assert!(lab[0] > 0, "L* should be positive for non-black input");
2401 assert!(
2403 (lab[1] as i32 - 128).abs() < 10,
2404 "a* should be near neutral"
2405 );
2406 assert!(
2407 (lab[2] as i32 - 128).abs() < 10,
2408 "b* should be near neutral"
2409 );
2410 }
2411
2412 #[test]
2413 fn test_tone_map_pixel_hlg() {
2414 let result = tone_map_pixel(
2415 &JxlTransferFunction::HLG,
2416 &JxlPrimaries::BT2100,
2417 &JxlWhitePoint::D65,
2418 [0.5, 0.5, 0.5],
2419 );
2420 assert!(result.is_ok());
2421 let lab = result.unwrap();
2422 assert!(lab[0] > 0, "L* should be positive for non-black input");
2424 assert!(
2426 (lab[1] as i32 - 128).abs() < 10,
2427 "a* should be near neutral"
2428 );
2429 assert!(
2430 (lab[2] as i32 - 128).abs() < 10,
2431 "b* should be near neutral"
2432 );
2433 }
2434
2435 #[test]
2436 fn test_hdr_icc_profile_generation_pq() {
2437 let encoding = JxlColorEncoding::RgbColorSpace {
2441 white_point: JxlWhitePoint::D65,
2442 primaries: JxlPrimaries::BT2100,
2443 transfer_function: JxlTransferFunction::PQ,
2444 rendering_intent: RenderingIntent::Relative,
2445 };
2446
2447 assert!(encoding.can_tone_map_for_icc());
2448
2449 let result = encoding.maybe_create_profile();
2450 assert!(result.is_ok(), "Profile creation should succeed");
2451 let profile_opt = result.unwrap();
2452 assert!(profile_opt.is_some(), "Profile should be generated for PQ");
2453
2454 let profile = profile_opt.unwrap();
2455 assert!(profile.len() > 128, "Profile should have header + tags");
2457
2458 assert_eq!(
2460 &profile[20..24],
2461 b"Lab ",
2462 "PCS should be Lab for HDR profiles"
2463 );
2464
2465 assert!(
2467 profile.windows(4).any(|w| w == b"mft1"),
2468 "Profile should contain mft1 tag (A2B0)"
2469 );
2470 assert!(
2471 profile.windows(4).any(|w| w == b"mBA "),
2472 "Profile should contain mBA tag (B2A0)"
2473 );
2474
2475 assert!(
2477 profile.windows(4).any(|w| w == b"cicp"),
2478 "Profile should contain cicp tag"
2479 );
2480 }
2481
2482 #[test]
2483 fn test_hdr_icc_profile_generation_hlg() {
2484 let encoding = JxlColorEncoding::RgbColorSpace {
2486 white_point: JxlWhitePoint::D65,
2487 primaries: JxlPrimaries::BT2100,
2488 transfer_function: JxlTransferFunction::HLG,
2489 rendering_intent: RenderingIntent::Relative,
2490 };
2491
2492 assert!(encoding.can_tone_map_for_icc());
2493
2494 let result = encoding.maybe_create_profile();
2495 assert!(result.is_ok(), "Profile creation should succeed");
2496 let profile_opt = result.unwrap();
2497 assert!(profile_opt.is_some(), "Profile should be generated for HLG");
2498
2499 let profile = profile_opt.unwrap();
2500 assert!(profile.len() > 128, "Profile should have header + tags");
2501 }
2502
2503 #[test]
2504 fn test_pq_eotf_inv_eotf_roundtrip() {
2505 let test_values: [f32; 5] = [0.0, 100.0, 1000.0, 5000.0, 10000.0];
2507 for &luminance in &test_values {
2508 let encoded = Rec2408ToneMapper::linear_to_pq(luminance);
2509 let decoded = Rec2408ToneMapper::pq_to_linear(encoded);
2510 let diff = (luminance - decoded).abs();
2511 assert!(
2512 diff < 1.0,
2513 "Roundtrip failed for {}: got {}, diff {}",
2514 luminance,
2515 decoded,
2516 diff
2517 );
2518 }
2519 }
2520
2521 #[test]
2522 fn test_can_tone_map_for_icc() {
2523 let pq_bt2100 = JxlColorEncoding::RgbColorSpace {
2525 white_point: JxlWhitePoint::D65,
2526 primaries: JxlPrimaries::BT2100,
2527 transfer_function: JxlTransferFunction::PQ,
2528 rendering_intent: RenderingIntent::Relative,
2529 };
2530 assert!(pq_bt2100.can_tone_map_for_icc());
2531
2532 let hlg_bt2100 = JxlColorEncoding::RgbColorSpace {
2534 white_point: JxlWhitePoint::D65,
2535 primaries: JxlPrimaries::BT2100,
2536 transfer_function: JxlTransferFunction::HLG,
2537 rendering_intent: RenderingIntent::Relative,
2538 };
2539 assert!(hlg_bt2100.can_tone_map_for_icc());
2540
2541 let srgb = JxlColorEncoding::srgb(false);
2543 assert!(!srgb.can_tone_map_for_icc());
2544
2545 let custom_pq = JxlColorEncoding::RgbColorSpace {
2547 white_point: JxlWhitePoint::D65,
2548 primaries: JxlPrimaries::Chromaticities {
2549 rx: 0.7,
2550 ry: 0.3,
2551 gx: 0.2,
2552 gy: 0.8,
2553 bx: 0.15,
2554 by: 0.05,
2555 },
2556 transfer_function: JxlTransferFunction::PQ,
2557 rendering_intent: RenderingIntent::Relative,
2558 };
2559 assert!(!custom_pq.can_tone_map_for_icc());
2560 }
2561
2562 #[test]
2564 fn test_hdr_pq_file_icc_profile() {
2565 use crate::api::{JxlDecoder, JxlDecoderOptions, ProcessingResult};
2566
2567 let data = std::fs::read("resources/test/hdr_pq_test.jxl")
2568 .expect("Failed to read hdr_pq_test.jxl - run from jxl crate directory");
2569
2570 let options = JxlDecoderOptions::default();
2571 let decoder = JxlDecoder::new(options);
2572 let mut input: &[u8] = &data;
2573
2574 let decoder_info = match decoder.process(&mut input).unwrap() {
2575 ProcessingResult::Complete { result } => result,
2576 _ => panic!("Expected complete decoding"),
2577 };
2578
2579 let color_profile = decoder_info.output_color_profile();
2581
2582 let icc = color_profile.try_as_icc();
2584 assert!(
2585 icc.is_some(),
2586 "Should generate ICC profile for HDR PQ content"
2587 );
2588
2589 let profile = icc.unwrap();
2590 assert_eq!(&profile[20..24], b"Lab ", "PCS should be Lab for HDR");
2592 assert!(
2594 profile.windows(4).any(|w| w == b"A2B0"),
2595 "Should have A2B0 tag"
2596 );
2597 assert!(
2599 profile.windows(4).any(|w| w == b"B2A0"),
2600 "Should have B2A0 tag"
2601 );
2602 }
2603
2604 #[test]
2606 fn test_hdr_hlg_file_icc_profile() {
2607 use crate::api::{JxlDecoder, JxlDecoderOptions, ProcessingResult};
2608
2609 let data = std::fs::read("resources/test/hdr_hlg_test.jxl")
2610 .expect("Failed to read hdr_hlg_test.jxl - run from jxl crate directory");
2611
2612 let options = JxlDecoderOptions::default();
2613 let decoder = JxlDecoder::new(options);
2614 let mut input: &[u8] = &data;
2615
2616 let decoder_info = match decoder.process(&mut input).unwrap() {
2617 ProcessingResult::Complete { result } => result,
2618 _ => panic!("Expected complete decoding"),
2619 };
2620
2621 let color_profile = decoder_info.output_color_profile();
2623
2624 let icc = color_profile.try_as_icc();
2626 assert!(
2627 icc.is_some(),
2628 "Should generate ICC profile for HDR HLG content"
2629 );
2630
2631 let profile = icc.unwrap();
2632 assert_eq!(&profile[20..24], b"Lab ", "PCS should be Lab for HDR");
2634 assert!(
2636 profile.windows(4).any(|w| w == b"A2B0"),
2637 "Should have A2B0 tag"
2638 );
2639 }
2640
2641 #[test]
2642 fn test_same_color_encoding_identical() {
2643 let srgb1 = JxlColorProfile::Simple(JxlColorEncoding::RgbColorSpace {
2645 white_point: JxlWhitePoint::D65,
2646 primaries: JxlPrimaries::SRGB,
2647 transfer_function: JxlTransferFunction::SRGB,
2648 rendering_intent: RenderingIntent::Relative,
2649 });
2650 let srgb2 = JxlColorProfile::Simple(JxlColorEncoding::RgbColorSpace {
2651 white_point: JxlWhitePoint::D65,
2652 primaries: JxlPrimaries::SRGB,
2653 transfer_function: JxlTransferFunction::SRGB,
2654 rendering_intent: RenderingIntent::Relative,
2655 });
2656 assert!(srgb1.same_color_encoding(&srgb2));
2657 }
2658
2659 #[test]
2660 fn test_same_color_encoding_different_transfer() {
2661 let srgb_gamma = JxlColorProfile::Simple(JxlColorEncoding::RgbColorSpace {
2663 white_point: JxlWhitePoint::D65,
2664 primaries: JxlPrimaries::SRGB,
2665 transfer_function: JxlTransferFunction::SRGB,
2666 rendering_intent: RenderingIntent::Relative,
2667 });
2668 let srgb_linear = JxlColorProfile::Simple(JxlColorEncoding::RgbColorSpace {
2669 white_point: JxlWhitePoint::D65,
2670 primaries: JxlPrimaries::SRGB,
2671 transfer_function: JxlTransferFunction::Linear,
2672 rendering_intent: RenderingIntent::Relative,
2673 });
2674 assert!(!srgb_gamma.same_color_encoding(&srgb_linear));
2675 assert!(!srgb_linear.same_color_encoding(&srgb_gamma));
2676 }
2677
2678 #[test]
2679 fn test_same_color_encoding_different_primaries() {
2680 let srgb = JxlColorProfile::Simple(JxlColorEncoding::RgbColorSpace {
2682 white_point: JxlWhitePoint::D65,
2683 primaries: JxlPrimaries::SRGB,
2684 transfer_function: JxlTransferFunction::SRGB,
2685 rendering_intent: RenderingIntent::Relative,
2686 });
2687 let p3 = JxlColorProfile::Simple(JxlColorEncoding::RgbColorSpace {
2688 white_point: JxlWhitePoint::D65,
2689 primaries: JxlPrimaries::P3,
2690 transfer_function: JxlTransferFunction::SRGB,
2691 rendering_intent: RenderingIntent::Relative,
2692 });
2693 assert!(!srgb.same_color_encoding(&p3));
2694 assert!(!p3.same_color_encoding(&srgb));
2695 }
2696
2697 #[test]
2698 fn test_same_color_encoding_different_white_point() {
2699 let d65 = JxlColorProfile::Simple(JxlColorEncoding::RgbColorSpace {
2701 white_point: JxlWhitePoint::D65,
2702 primaries: JxlPrimaries::SRGB,
2703 transfer_function: JxlTransferFunction::SRGB,
2704 rendering_intent: RenderingIntent::Relative,
2705 });
2706 let dci = JxlColorProfile::Simple(JxlColorEncoding::RgbColorSpace {
2707 white_point: JxlWhitePoint::DCI,
2708 primaries: JxlPrimaries::SRGB,
2709 transfer_function: JxlTransferFunction::SRGB,
2710 rendering_intent: RenderingIntent::Relative,
2711 });
2712 assert!(!d65.same_color_encoding(&dci));
2713 }
2714
2715 #[test]
2716 fn test_same_color_encoding_grayscale() {
2717 let gray_srgb = JxlColorProfile::Simple(JxlColorEncoding::GrayscaleColorSpace {
2719 white_point: JxlWhitePoint::D65,
2720 transfer_function: JxlTransferFunction::SRGB,
2721 rendering_intent: RenderingIntent::Relative,
2722 });
2723 let gray_linear = JxlColorProfile::Simple(JxlColorEncoding::GrayscaleColorSpace {
2724 white_point: JxlWhitePoint::D65,
2725 transfer_function: JxlTransferFunction::Linear,
2726 rendering_intent: RenderingIntent::Relative,
2727 });
2728 assert!(!gray_srgb.same_color_encoding(&gray_linear));
2729 let gray_srgb2 = JxlColorProfile::Simple(JxlColorEncoding::GrayscaleColorSpace {
2731 white_point: JxlWhitePoint::D65,
2732 transfer_function: JxlTransferFunction::SRGB,
2733 rendering_intent: RenderingIntent::Relative,
2734 });
2735 assert!(gray_srgb.same_color_encoding(&gray_srgb2));
2736 }
2737
2738 #[test]
2739 fn test_same_color_encoding_icc_profile() {
2740 let srgb = JxlColorProfile::Simple(JxlColorEncoding::RgbColorSpace {
2741 white_point: JxlWhitePoint::D65,
2742 primaries: JxlPrimaries::SRGB,
2743 transfer_function: JxlTransferFunction::SRGB,
2744 rendering_intent: RenderingIntent::Relative,
2745 });
2746 let icc_a = JxlColorProfile::Icc(vec![0u8; 100]);
2747 let icc_b = JxlColorProfile::Icc(vec![0u8; 100]); let icc_c = JxlColorProfile::Icc(vec![1u8; 100]); assert!(!srgb.same_color_encoding(&icc_a));
2752 assert!(!icc_a.same_color_encoding(&srgb));
2753
2754 assert!(icc_a.same_color_encoding(&icc_a));
2756 assert!(icc_a.same_color_encoding(&icc_b));
2757
2758 assert!(!icc_a.same_color_encoding(&icc_c));
2760
2761 let mut cmyk_data = vec![0u8; 100];
2763 cmyk_data[16..20].copy_from_slice(b"CMYK");
2764 let cmyk_a = JxlColorProfile::Icc(cmyk_data.clone());
2765 let cmyk_b = JxlColorProfile::Icc(cmyk_data);
2766 assert!(!cmyk_a.same_color_encoding(&cmyk_b));
2767 }
2768
2769 #[test]
2770 fn test_same_color_encoding_rgb_vs_grayscale() {
2771 let rgb = JxlColorProfile::Simple(JxlColorEncoding::RgbColorSpace {
2773 white_point: JxlWhitePoint::D65,
2774 primaries: JxlPrimaries::SRGB,
2775 transfer_function: JxlTransferFunction::SRGB,
2776 rendering_intent: RenderingIntent::Relative,
2777 });
2778 let gray = JxlColorProfile::Simple(JxlColorEncoding::GrayscaleColorSpace {
2779 white_point: JxlWhitePoint::D65,
2780 transfer_function: JxlTransferFunction::SRGB,
2781 rendering_intent: RenderingIntent::Relative,
2782 });
2783 assert!(!rgb.same_color_encoding(&gray));
2784 assert!(!gray.same_color_encoding(&rgb));
2785 }
2786
2787 #[test]
2789 fn test_xyb_icc_profile_generation() {
2790 let xyb = JxlColorProfile::Simple(JxlColorEncoding::XYB {
2791 rendering_intent: RenderingIntent::Perceptual,
2792 });
2793
2794 let icc = xyb.try_as_icc().expect("XYB should generate ICC profile");
2795 assert!(!icc.is_empty());
2796 assert!(icc.windows(4).any(|w| w == b"mAB "));
2797 assert!(icc.windows(4).any(|w| w == b"mBA "));
2798 }
2799}