1use rpdfium_graphics::Color;
12use rpdfium_page::color_space::{ColorSpaceParams, ResolvedColorSpace};
13use rpdfium_page::function;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
17pub struct RgbaColor {
18 pub r: u8,
20 pub g: u8,
22 pub b: u8,
24 pub a: u8,
26}
27
28impl RgbaColor {
29 pub const WHITE: Self = Self {
31 r: 255,
32 g: 255,
33 b: 255,
34 a: 255,
35 };
36
37 pub const BLACK: Self = Self {
39 r: 0,
40 g: 0,
41 b: 0,
42 a: 255,
43 };
44
45 pub const TRANSPARENT: Self = Self {
47 r: 0,
48 g: 0,
49 b: 0,
50 a: 0,
51 };
52
53 pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
55 Self { r, g, b, a }
56 }
57
58 pub fn from_pdf_color(color: &Color, alpha: f32) -> Self {
66 let a = (alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
67 match color.components.len() {
68 1 => {
69 let v = (color.components[0].clamp(0.0, 1.0) * 255.0).round() as u8;
70 Self {
71 r: v,
72 g: v,
73 b: v,
74 a,
75 }
76 }
77 3 => Self {
78 r: (color.components[0].clamp(0.0, 1.0) * 255.0).round() as u8,
79 g: (color.components[1].clamp(0.0, 1.0) * 255.0).round() as u8,
80 b: (color.components[2].clamp(0.0, 1.0) * 255.0).round() as u8,
81 a,
82 },
83 4 => {
84 let c = color.components[0].clamp(0.0, 1.0);
85 let m = color.components[1].clamp(0.0, 1.0);
86 let y_val = color.components[2].clamp(0.0, 1.0);
87 let k = color.components[3].clamp(0.0, 1.0);
88 let (r, g, b) = crate::cfx_cmyk_to_srgb::adobe_cmyk_f32_to_srgb(c, m, y_val, k);
89 Self { r, g, b, a }
90 }
91 _ => Self {
92 r: 0,
93 g: 0,
94 b: 0,
95 a,
96 },
97 }
98 }
99
100 pub fn from_resolved_color(color: &Color, cs: &ResolvedColorSpace, alpha: f32) -> Self {
106 let a_byte = (alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
107
108 match &cs.params {
109 ColorSpaceParams::Device => Self::from_pdf_color(color, alpha),
110
111 ColorSpaceParams::CalGray { gamma, .. } => {
112 let v = color
113 .components
114 .first()
115 .copied()
116 .unwrap_or(0.0)
117 .clamp(0.0, 1.0);
118 let v_linear = v.powf(*gamma);
119 let c = (v_linear.clamp(0.0, 1.0) * 255.0).round() as u8;
120 Self {
121 r: c,
122 g: c,
123 b: c,
124 a: a_byte,
125 }
126 }
127
128 ColorSpaceParams::CalRGB {
129 gamma,
130 matrix,
131 white_point,
132 } => {
133 let r_in = color
134 .components
135 .first()
136 .copied()
137 .unwrap_or(0.0)
138 .clamp(0.0, 1.0);
139 let g_in = color
140 .components
141 .get(1)
142 .copied()
143 .unwrap_or(0.0)
144 .clamp(0.0, 1.0);
145 let b_in = color
146 .components
147 .get(2)
148 .copied()
149 .unwrap_or(0.0)
150 .clamp(0.0, 1.0);
151
152 let a_lin = r_in.powf(gamma[0]);
154 let b_lin = g_in.powf(gamma[1]);
155 let c_lin = b_in.powf(gamma[2]);
156
157 let x = matrix[0] * a_lin + matrix[3] * b_lin + matrix[6] * c_lin;
159 let y = matrix[1] * a_lin + matrix[4] * b_lin + matrix[7] * c_lin;
160 let z = matrix[2] * a_lin + matrix[5] * b_lin + matrix[8] * c_lin;
161
162 let (r_out, g_out, b_out) = xyz_to_srgb(x, y, z, white_point);
164 Self {
165 r: (r_out * 255.0).round() as u8,
166 g: (g_out * 255.0).round() as u8,
167 b: (b_out * 255.0).round() as u8,
168 a: a_byte,
169 }
170 }
171
172 ColorSpaceParams::Lab { range, .. } => {
173 let l_star = color
174 .components
175 .first()
176 .copied()
177 .unwrap_or(0.0)
178 .clamp(0.0, 100.0);
179 let a_star = color
180 .components
181 .get(1)
182 .copied()
183 .unwrap_or(0.0)
184 .clamp(range[0], range[1]);
185 let b_star = color
186 .components
187 .get(2)
188 .copied()
189 .unwrap_or(0.0)
190 .clamp(range[2], range[3]);
191
192 let m = (l_star + 16.0) / 116.0;
196 let l = m + a_star / 500.0;
197 let n = m - b_star / 200.0;
198
199 let x = if l < 0.2069 {
200 0.957 * 0.12842 * (l - 0.1379)
201 } else {
202 0.957 * l * l * l
203 };
204 let y = if m < 0.2069 {
205 0.12842 * (m - 0.1379)
206 } else {
207 m * m * m
208 };
209 let z = if n < 0.2069 {
210 1.0889 * 0.12842 * (n - 0.1379)
211 } else {
212 1.0889 * n * n * n
213 };
214
215 let (r_out, g_out, b_out) = xyz_to_srgb(x, y, z, &[0.957, 1.0, 1.0889]);
216 Self {
217 r: (r_out * 255.0).round() as u8,
218 g: (g_out * 255.0).round() as u8,
219 b: (b_out * 255.0).round() as u8,
220 a: a_byte,
221 }
222 }
223
224 ColorSpaceParams::ICCBased {
225 n_components,
226 alternate,
227 icc_profile_data,
228 } => {
229 if let Some(profile_data) = icc_profile_data {
231 if let Some((r, g, b)) = icc_transform(color, profile_data, *n_components) {
232 return Self { r, g, b, a: a_byte };
233 }
234 }
235 Self::from_resolved_color(color, alternate, alpha)
236 }
237
238 ColorSpaceParams::Indexed {
239 base,
240 hival,
241 lookup,
242 } => {
243 let index = color.components.first().copied().unwrap_or(0.0) as u16;
244 let index = index.min(*hival);
245 let n = base.component_count() as usize;
246 let start = index as usize * n;
247 let ranges = base.component_ranges();
251 let mut components = Vec::with_capacity(n);
252 for i in 0..n {
253 let byte = lookup.get(start + i).copied().unwrap_or(0) as f32;
254 let (min_val, max_val) = ranges.get(i).copied().unwrap_or((0.0, 1.0));
255 components.push(min_val + (byte / 255.0) * (max_val - min_val));
256 }
257 let base_color = Color { components };
258 Self::from_resolved_color(&base_color, base, alpha)
259 }
260
261 ColorSpaceParams::Separation {
262 alternate, tint_fn, ..
263 } => {
264 let tint = color.components.first().copied().unwrap_or(0.0);
265 let alt_components = function::evaluate(tint_fn, &[tint]);
266 let alt_color = Color {
267 components: alt_components,
268 };
269 Self::from_resolved_color(&alt_color, alternate, alpha)
270 }
271
272 ColorSpaceParams::DeviceN {
273 alternate, tint_fn, ..
274 } => {
275 let alt_components = function::evaluate(tint_fn, &color.components);
276 let alt_color = Color {
277 components: alt_components,
278 };
279 Self::from_resolved_color(&alt_color, alternate, alpha)
280 }
281 }
282 }
283}
284
285fn icc_transform(color: &Color, profile_data: &[u8], n_components: u8) -> Option<(u8, u8, u8)> {
289 use std::sync::Arc;
290
291 let src_profile = moxcms::ColorProfile::new_from_slice(profile_data).ok()?;
292 let dst_profile = moxcms::ColorProfile::new_srgb();
293 let src_layout = match n_components {
294 1 => moxcms::Layout::Gray,
295 3 => moxcms::Layout::Rgb,
296 4 => moxcms::Layout::Rgba,
298 _ => return None,
299 };
300 let executor: Arc<moxcms::Transform8BitExecutor> = src_profile
301 .create_transform_8bit(
302 src_layout,
303 &dst_profile,
304 moxcms::Layout::Rgb,
305 moxcms::TransformOptions::default(),
306 )
307 .ok()?;
308 let src_bytes: Vec<u8> = color
309 .components
310 .iter()
311 .map(|c| (c.clamp(0.0, 1.0) * 255.0).round() as u8)
312 .collect();
313 let mut dst = [0u8; 3];
314 executor.transform(&src_bytes, &mut dst).ok()?;
315 Some((dst[0], dst[1], dst[2]))
316}
317
318#[rustfmt::skip]
325static SRGB_SAMPLES_1: [u8; 192] = [
326 0, 3, 6, 10, 13, 15, 18, 20, 22, 23, 25, 27, 28, 30, 31,
327 32, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
328 48, 49, 49, 50, 51, 52, 53, 53, 54, 55, 56, 56, 57, 58, 58,
329 59, 60, 61, 61, 62, 62, 63, 64, 64, 65, 66, 66, 67, 67, 68,
330 68, 69, 70, 70, 71, 71, 72, 72, 73, 73, 74, 74, 75, 76, 76,
331 77, 77, 78, 78, 79, 79, 79, 80, 80, 81, 81, 82, 82, 83, 83,
332 84, 84, 85, 85, 85, 86, 86, 87, 87, 88, 88, 88, 89, 89, 90,
333 90, 91, 91, 91, 92, 92, 93, 93, 93, 94, 94, 95, 95, 95, 96,
334 96, 97, 97, 97, 98, 98, 98, 99, 99, 99, 100, 100, 101, 101, 101,
335 102, 102, 102, 103, 103, 103, 104, 104, 104, 105, 105, 106, 106, 106, 107,
336 107, 107, 108, 108, 108, 109, 109, 109, 110, 110, 110, 110, 111, 111, 111,
337 112, 112, 112, 113, 113, 113, 114, 114, 114, 115, 115, 115, 115, 116, 116,
338 116, 117, 117, 117, 118, 118, 118, 118, 119, 119, 119, 120,
339];
340
341#[rustfmt::skip]
343static SRGB_SAMPLES_2: [u8; 208] = [
344 120, 121, 122, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135,
345 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 148, 149,
346 150, 151, 152, 153, 154, 155, 155, 156, 157, 158, 159, 159, 160, 161, 162,
347 163, 163, 164, 165, 166, 167, 167, 168, 169, 170, 170, 171, 172, 173, 173,
348 174, 175, 175, 176, 177, 178, 178, 179, 180, 180, 181, 182, 182, 183, 184,
349 185, 185, 186, 187, 187, 188, 189, 189, 190, 190, 191, 192, 192, 193, 194,
350 194, 195, 196, 196, 197, 197, 198, 199, 199, 200, 200, 201, 202, 202, 203,
351 203, 204, 205, 205, 206, 206, 207, 208, 208, 209, 209, 210, 210, 211, 212,
352 212, 213, 213, 214, 214, 215, 215, 216, 216, 217, 218, 218, 219, 219, 220,
353 220, 221, 221, 222, 222, 223, 223, 224, 224, 225, 226, 226, 227, 227, 228,
354 228, 229, 229, 230, 230, 231, 231, 232, 232, 233, 233, 234, 234, 235, 235,
355 236, 236, 237, 237, 238, 238, 238, 239, 239, 240, 240, 241, 241, 242, 242,
356 243, 243, 244, 244, 245, 245, 246, 246, 246, 247, 247, 248, 248, 249, 249,
357 250, 250, 251, 251, 251, 252, 252, 253, 253, 254, 254, 255, 255,
358];
359
360fn rgb_conversion(v: f32) -> f32 {
362 let v = v.clamp(0.0, 1.0);
363 let scale = (v * 1023.0) as usize;
364 if scale < 192 {
365 SRGB_SAMPLES_1[scale] as f32 / 255.0
366 } else {
367 let idx = scale / 4 - 48;
368 SRGB_SAMPLES_2[idx.min(SRGB_SAMPLES_2.len() - 1)] as f32 / 255.0
369 }
370}
371
372fn xyz_to_srgb(x: f32, y: f32, z: f32, _white_point: &[f32; 3]) -> (f32, f32, f32) {
377 let r_lin = 3.2410 * x - 1.5374 * y - 0.4986 * z;
378 let g_lin = -0.9692 * x + 1.8760 * y + 0.0416 * z;
379 let b_lin = 0.0556 * x - 0.2040 * y + 1.0570 * z;
380 (
381 rgb_conversion(r_lin),
382 rgb_conversion(g_lin),
383 rgb_conversion(b_lin),
384 )
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use rpdfium_graphics::Color;
391
392 #[test]
393 fn test_gray_to_rgba() {
394 let color = Color::gray(0.5);
395 let rgba = RgbaColor::from_pdf_color(&color, 1.0);
396 assert_eq!(rgba.r, 128);
398 assert_eq!(rgba.g, 128);
399 assert_eq!(rgba.b, 128);
400 assert_eq!(rgba.a, 255);
401 }
402
403 #[test]
404 fn test_rgb_to_rgba() {
405 let color = Color::rgb(1.0, 0.0, 0.0);
406 let rgba = RgbaColor::from_pdf_color(&color, 1.0);
407 assert_eq!(rgba.r, 255);
408 assert_eq!(rgba.g, 0);
409 assert_eq!(rgba.b, 0);
410 assert_eq!(rgba.a, 255);
411 }
412
413 #[test]
414 fn test_cmyk_to_rgba() {
415 let color = Color::cmyk(1.0, 0.0, 0.0, 0.0);
417 let rgba = RgbaColor::from_pdf_color(&color, 1.0);
418 assert_eq!(rgba.r, 0);
419 assert_eq!(rgba.g, 174);
420 assert_eq!(rgba.b, 239);
421 assert_eq!(rgba.a, 255);
422 }
423
424 #[test]
425 fn test_cmyk_black() {
426 let color = Color::cmyk(0.0, 0.0, 0.0, 1.0);
428 let rgba = RgbaColor::from_pdf_color(&color, 1.0);
429 assert_eq!(rgba.r, 35);
430 assert_eq!(rgba.g, 31);
431 assert_eq!(rgba.b, 32);
432 }
433
434 #[test]
435 fn test_alpha_scaling() {
436 let color = Color::gray(1.0);
437 let rgba = RgbaColor::from_pdf_color(&color, 0.5);
438 assert_eq!(rgba.a, 128);
440 }
441
442 #[test]
443 fn test_clamping_out_of_range() {
444 let color = Color {
445 components: vec![1.5, -0.5, 2.0],
446 };
447 let rgba = RgbaColor::from_pdf_color(&color, 1.5);
448 assert_eq!(rgba.r, 255);
449 assert_eq!(rgba.g, 0);
450 assert_eq!(rgba.b, 255);
451 assert_eq!(rgba.a, 255);
452 }
453
454 #[test]
455 fn test_constants() {
456 assert_eq!(RgbaColor::WHITE, RgbaColor::new(255, 255, 255, 255));
457 assert_eq!(RgbaColor::BLACK, RgbaColor::new(0, 0, 0, 255));
458 assert_eq!(RgbaColor::TRANSPARENT, RgbaColor::new(0, 0, 0, 0));
459 }
460
461 #[test]
464 fn test_icc_based_no_profile_falls_back_to_alternate() {
465 let cs = ResolvedColorSpace {
466 family: rpdfium_graphics::ColorSpaceFamily::ICCBased,
467 params: ColorSpaceParams::ICCBased {
468 n_components: 3,
469 alternate: Box::new(ResolvedColorSpace::device_rgb()),
470 icc_profile_data: None,
471 },
472 };
473 let color = Color::rgb(1.0, 0.0, 0.0);
474 let rgba = RgbaColor::from_resolved_color(&color, &cs, 1.0);
475 assert_eq!(rgba.r, 255);
476 assert_eq!(rgba.g, 0);
477 assert_eq!(rgba.b, 0);
478 }
479
480 #[test]
481 fn test_icc_based_garbage_profile_falls_back() {
482 let cs = ResolvedColorSpace {
483 family: rpdfium_graphics::ColorSpaceFamily::ICCBased,
484 params: ColorSpaceParams::ICCBased {
485 n_components: 3,
486 alternate: Box::new(ResolvedColorSpace::device_rgb()),
487 icc_profile_data: Some(vec![0, 1, 2, 3]),
488 },
489 };
490 let color = Color::rgb(0.0, 1.0, 0.0);
491 let rgba = RgbaColor::from_resolved_color(&color, &cs, 1.0);
492 assert_eq!(rgba.r, 0);
494 assert_eq!(rgba.g, 255);
495 assert_eq!(rgba.b, 0);
496 }
497
498 #[test]
499 fn test_icc_transform_unknown_components_returns_none() {
500 let color = Color {
501 components: vec![0.5, 0.5],
502 };
503 let result = icc_transform(&color, &[0, 1, 2, 3], 2);
505 assert!(result.is_none());
506 }
507
508 #[test]
509 fn test_icc_transform_gray_layout() {
510 let color = Color::gray(0.5);
512 let result = icc_transform(&color, &[0, 1, 2, 3], 1);
513 assert!(result.is_none());
515 }
516
517 #[test]
518 fn test_icc_transform_cmyk_layout() {
519 let color = Color::cmyk(0.0, 0.0, 0.0, 0.0);
521 let result = icc_transform(&color, &[0, 1, 2, 3], 4);
522 assert!(result.is_none());
524 }
525
526 #[test]
529 fn test_from_pdf_color_gray_negative() {
530 let color = Color {
531 components: vec![-0.5],
532 };
533 let rgba = RgbaColor::from_pdf_color(&color, 1.0);
534 assert_eq!(rgba.r, 0);
535 assert_eq!(rgba.g, 0);
536 assert_eq!(rgba.b, 0);
537 }
538
539 #[test]
540 fn test_from_pdf_color_gray_over_one() {
541 let color = Color {
542 components: vec![1.5],
543 };
544 let rgba = RgbaColor::from_pdf_color(&color, 1.0);
545 assert_eq!(rgba.r, 255);
546 assert_eq!(rgba.g, 255);
547 assert_eq!(rgba.b, 255);
548 }
549
550 #[test]
551 fn test_from_pdf_color_rgb_mixed_bounds() {
552 let color = Color {
553 components: vec![-0.1, 0.5, 1.2],
554 };
555 let rgba = RgbaColor::from_pdf_color(&color, 1.0);
556 assert_eq!(rgba.r, 0);
557 assert_eq!(rgba.g, 128);
558 assert_eq!(rgba.b, 255);
559 }
560
561 #[test]
562 fn test_from_pdf_color_rgb_all_negative() {
563 let color = Color {
564 components: vec![-1.0, -2.0, -3.0],
565 };
566 let rgba = RgbaColor::from_pdf_color(&color, 1.0);
567 assert_eq!(rgba.r, 0);
568 assert_eq!(rgba.g, 0);
569 assert_eq!(rgba.b, 0);
570 }
571
572 #[test]
573 fn test_from_pdf_color_rgb_all_over() {
574 let color = Color {
575 components: vec![2.0, 3.0, 4.0],
576 };
577 let rgba = RgbaColor::from_pdf_color(&color, 1.0);
578 assert_eq!(rgba.r, 255);
579 assert_eq!(rgba.g, 255);
580 assert_eq!(rgba.b, 255);
581 }
582
583 #[test]
584 fn test_from_pdf_color_cmyk_negative_values() {
585 let color = Color {
587 components: vec![-0.5, -0.1, 0.0, 0.0],
588 };
589 let rgba = RgbaColor::from_pdf_color(&color, 1.0);
590 assert_eq!(rgba.r, 255);
591 assert_eq!(rgba.g, 255);
592 assert_eq!(rgba.b, 255);
593 }
594
595 #[test]
596 fn test_from_pdf_color_cmyk_over_one() {
597 let color = Color {
599 components: vec![2.0, 2.0, 2.0, 2.0],
600 };
601 let rgba = RgbaColor::from_pdf_color(&color, 1.0);
602 assert_eq!(rgba.r, 0);
603 assert_eq!(rgba.g, 0);
604 assert_eq!(rgba.b, 0);
605 }
606
607 #[test]
608 fn test_from_pdf_color_cmyk_mixed_clamping() {
609 let color = Color {
611 components: vec![-0.1, 0.5, 1.5, 0.3],
612 };
613 let rgba = RgbaColor::from_pdf_color(&color, 1.0);
614 assert_eq!(rgba.r, 183);
616 assert_eq!(rgba.g, 109);
617 assert_eq!(rgba.b, 17);
618 }
619
620 #[test]
621 fn test_from_pdf_color_alpha_negative() {
622 let color = Color::gray(0.5);
623 let rgba = RgbaColor::from_pdf_color(&color, -1.0);
624 assert_eq!(rgba.a, 0);
625 }
626
627 #[test]
628 fn test_from_pdf_color_alpha_over_one() {
629 let color = Color::gray(0.5);
630 let rgba = RgbaColor::from_pdf_color(&color, 2.5);
631 assert_eq!(rgba.a, 255);
632 }
633
634 #[test]
635 fn test_from_pdf_color_zero_components() {
636 let color = Color { components: vec![] };
637 let rgba = RgbaColor::from_pdf_color(&color, 0.8);
638 assert_eq!(rgba.r, 0);
640 assert_eq!(rgba.g, 0);
641 assert_eq!(rgba.b, 0);
642 assert_eq!(rgba.a, 204); }
644
645 #[test]
646 fn test_from_pdf_color_five_components() {
647 let color = Color {
648 components: vec![0.1, 0.2, 0.3, 0.4, 0.5],
649 };
650 let rgba = RgbaColor::from_pdf_color(&color, 1.0);
651 assert_eq!(rgba.r, 0);
653 assert_eq!(rgba.g, 0);
654 assert_eq!(rgba.b, 0);
655 assert_eq!(rgba.a, 255);
656 }
657
658 #[test]
659 fn test_cmyk_stress_low_values() {
660 let color = Color::cmyk(0.001, 0.002, 0.003, 0.0);
662 let rgba = RgbaColor::from_pdf_color(&color, 1.0);
663 assert_eq!(rgba.r, 254);
665 assert_eq!(rgba.g, 254);
666 assert_eq!(rgba.b, 253);
667 }
668
669 #[test]
670 fn test_cmyk_half_key() {
671 let color = Color::cmyk(0.0, 0.0, 0.0, 0.5);
673 let rgba = RgbaColor::from_pdf_color(&color, 1.0);
674 assert_eq!(rgba.r, 147);
676 assert_eq!(rgba.g, 149);
677 assert_eq!(rgba.b, 152);
678 }
679}