1use crate::cache::{Cache, CacheKey};
4use crate::function::Function;
5use log::warn;
6use moxcms::{
7 ColorProfile, DataColorSpace, Layout, RenderingIntent, Transform8BitExecutor,
8 TransformF32BitExecutor, TransformOptions, Xyzd,
9};
10use pdf_syntax::object;
11use pdf_syntax::object::Array;
12use pdf_syntax::object::Dict;
13use pdf_syntax::object::Name;
14use pdf_syntax::object::Object;
15use pdf_syntax::object::Stream;
16use pdf_syntax::object::dict::keys::*;
17use smallvec::{SmallVec, ToSmallVec, smallvec};
18use std::fmt::{Debug, Formatter};
19use std::ops::Deref;
20use std::sync::{Arc, OnceLock};
21
22static DEFAULT_CMYK_PROFILE: OnceLock<Option<ICCProfile>> = OnceLock::new();
25
26fn default_cmyk_profile() -> Option<&'static ICCProfile> {
27 DEFAULT_CMYK_PROFILE
28 .get_or_init(|| ICCProfile::new(include_bytes!("../assets/CGATS001Compat-v2-micro.icc"), 4))
29 .as_ref()
30}
31
32pub type ColorComponents = SmallVec<[f32; 4]>;
34
35#[derive(Debug, Copy, Clone)]
37pub struct AlphaColor {
38 components: [f32; 4],
39}
40
41impl AlphaColor {
42 pub const BLACK: Self = Self::new([0., 0., 0., 1.]);
44
45 pub const TRANSPARENT: Self = Self::new([0., 0., 0., 0.]);
47
48 pub const WHITE: Self = Self::new([1., 1., 1., 1.]);
50
51 pub const fn new(components: [f32; 4]) -> Self {
53 Self { components }
54 }
55
56 pub const fn from_rgb8(r: u8, g: u8, b: u8) -> Self {
58 let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), 1.];
59 Self::new(components)
60 }
61
62 pub fn premultiplied(&self) -> [f32; 4] {
64 [
65 self.components[0] * self.components[3],
66 self.components[1] * self.components[3],
67 self.components[2] * self.components[3],
68 self.components[3],
69 ]
70 }
71
72 pub const fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
74 let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), u8_to_f32(a)];
75 Self::new(components)
76 }
77
78 pub fn to_rgba8(&self) -> [u8; 4] {
80 [
81 (self.components[0] * 255.0 + 0.5) as u8,
82 (self.components[1] * 255.0 + 0.5) as u8,
83 (self.components[2] * 255.0 + 0.5) as u8,
84 (self.components[3] * 255.0 + 0.5) as u8,
85 ]
86 }
87
88 pub fn components(&self) -> [f32; 4] {
90 self.components
91 }
92}
93
94const fn u8_to_f32(x: u8) -> f32 {
95 x as f32 * (1.0 / 255.0)
96}
97
98#[derive(Debug, Clone)]
99pub(crate) enum ColorSpaceType {
100 DeviceCmyk,
101 DeviceGray,
102 DeviceRgb,
103 Pattern(ColorSpace),
104 Indexed(Indexed),
105 ICCBased(ICCProfile),
106 CalGray(CalGray),
107 CalRgb(CalRgb),
108 Lab(Lab),
109 Separation(Separation),
110 DeviceN(DeviceN),
111}
112
113impl ColorSpaceType {
114 fn new(object: Object<'_>, cache: &Cache) -> Option<Self> {
115 Self::new_inner(object, cache)
116 }
117
118 fn new_inner(object: Object<'_>, cache: &Cache) -> Option<Self> {
119 if let Some(name) = object.clone().into_name() {
120 return Self::new_from_name(name.clone());
121 } else if let Some(color_array) = object.clone().into_array() {
122 let mut iter = color_array.clone().flex_iter();
123 let name = iter.next::<Name>()?;
124
125 match name.deref() {
126 ICC_BASED => {
127 let icc_stream = iter.next::<Stream<'_>>()?;
128 let dict = icc_stream.dict();
129 let num_components = dict.get::<usize>(N);
134
135 return cache.get_or_insert_with(icc_stream.cache_key(), || {
136 let from_icc = num_components.and_then(|n| {
140 icc_stream.decoded().ok().as_ref().and_then(|decoded| {
141 ICCProfile::new(decoded, n).map(|icc| {
142 if icc.is_srgb() {
147 Self::DeviceRgb
148 } else {
149 Self::ICCBased(icc)
150 }
151 })
152 })
153 });
154
155 from_icc
156 .or_else(|| {
157 dict.get::<Object<'_>>(ALTERNATE)
158 .and_then(|o| Self::new(o, cache))
159 })
160 .or_else(|| match num_components {
161 Some(1) => Some(Self::DeviceGray),
162 Some(3) => Some(Self::DeviceRgb),
163 Some(4) => Some(Self::DeviceCmyk),
164 _ => None,
165 })
166 });
167 }
168 CALCMYK => return Some(Self::DeviceCmyk),
169 CALGRAY => {
170 let cal_dict = iter.next::<Dict<'_>>()?;
171 return Some(Self::CalGray(CalGray::new(&cal_dict)?));
172 }
173 CALRGB => {
174 let cal_dict = iter.next::<Dict<'_>>()?;
175 return Some(Self::CalRgb(CalRgb::new(&cal_dict)?));
176 }
177 DEVICE_RGB | RGB => return Some(Self::DeviceRgb),
178 DEVICE_GRAY | G => return Some(Self::DeviceGray),
179 DEVICE_CMYK | CMYK => return Some(Self::DeviceCmyk),
180 LAB => {
181 let lab_dict = iter.next::<Dict<'_>>()?;
182 return Some(Self::Lab(Lab::new(&lab_dict)?));
183 }
184 INDEXED | I => {
185 return Some(Self::Indexed(Indexed::new(&color_array, cache)?));
186 }
187 SEPARATION => {
188 return Some(Self::Separation(Separation::new(&color_array, cache)?));
189 }
190 DEVICE_N => {
191 return Some(Self::DeviceN(DeviceN::new(&color_array, cache)?));
192 }
193 PATTERN => {
194 let cs = iter
197 .next::<Object<'_>>()
198 .and_then(|o| ColorSpace::new(o, cache))
199 .unwrap_or(ColorSpace::device_rgb());
200 return Some(Self::Pattern(cs));
201 }
202 _ => {
203 warn!("unsupported color space: {}", name.as_str());
204 return None;
205 }
206 }
207 }
208
209 None
210 }
211
212 fn new_from_name(name: Name) -> Option<Self> {
213 match name.deref() {
214 DEVICE_RGB | RGB => Some(Self::DeviceRgb),
215 DEVICE_GRAY | G => Some(Self::DeviceGray),
216 DEVICE_CMYK | CMYK => Some(Self::DeviceCmyk),
217 CALCMYK => Some(Self::DeviceCmyk),
218 PATTERN => Some(Self::Pattern(ColorSpace::device_rgb())),
219 _ => None,
220 }
221 }
222}
223
224#[derive(Debug, Clone)]
226pub struct ColorSpace(Arc<ColorSpaceType>);
227
228impl ColorSpace {
229 pub(crate) fn new(object: Object<'_>, cache: &Cache) -> Option<Self> {
231 Some(Self(Arc::new(ColorSpaceType::new(object, cache)?)))
232 }
233
234 pub(crate) fn new_from_name(name: Name) -> Option<Self> {
236 ColorSpaceType::new_from_name(name).map(|c| Self(Arc::new(c)))
237 }
238
239 pub(crate) fn device_gray() -> Self {
241 Self(Arc::new(ColorSpaceType::DeviceGray))
242 }
243
244 pub(crate) fn device_rgb() -> Self {
246 Self(Arc::new(ColorSpaceType::DeviceRgb))
247 }
248
249 pub(crate) fn device_cmyk() -> Self {
251 Self(Arc::new(ColorSpaceType::DeviceCmyk))
252 }
253
254 pub fn is_device_rgb(&self) -> bool {
256 matches!(*self.0, ColorSpaceType::DeviceRgb)
257 }
258
259 pub fn is_device_cmyk(&self) -> bool {
261 matches!(*self.0, ColorSpaceType::DeviceCmyk)
262 }
263
264 pub(crate) fn pattern() -> Self {
266 Self(Arc::new(ColorSpaceType::Pattern(Self::device_gray())))
267 }
268
269 pub(crate) fn pattern_cs(&self) -> Option<Self> {
270 match self.0.as_ref() {
271 ColorSpaceType::Pattern(cs) => Some(cs.clone()),
272 _ => None,
273 }
274 }
275
276 pub(crate) fn is_pattern(&self) -> bool {
278 matches!(self.0.as_ref(), ColorSpaceType::Pattern(_))
279 }
280
281 pub(crate) fn is_indexed(&self) -> bool {
283 matches!(self.0.as_ref(), ColorSpaceType::Indexed(_))
284 }
285
286 pub(crate) fn default_decode_arr(&self, n: f32) -> SmallVec<[(f32, f32); 4]> {
288 match self.0.as_ref() {
289 ColorSpaceType::DeviceCmyk => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
290 ColorSpaceType::DeviceGray => smallvec![(0.0, 1.0)],
291 ColorSpaceType::DeviceRgb => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
292 ColorSpaceType::ICCBased(i) => smallvec![(0.0, 1.0); i.0.number_components],
293 ColorSpaceType::CalGray(_) => smallvec![(0.0, 1.0)],
294 ColorSpaceType::CalRgb(_) => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
295 ColorSpaceType::Lab(l) => smallvec![
296 (0.0, 100.0),
297 (l.range[0], l.range[1]),
298 (l.range[2], l.range[3]),
299 ],
300 ColorSpaceType::Indexed(_) => smallvec![(0.0, 2.0_f32.powf(n) - 1.0)],
301 ColorSpaceType::Separation(_) => smallvec![(0.0, 1.0)],
302 ColorSpaceType::DeviceN(d) => smallvec![(0.0, 1.0); d.num_components as usize],
303 ColorSpaceType::Pattern(_) => smallvec![(0.0, 1.0)],
305 }
306 }
307
308 pub(crate) fn initial_color(&self) -> ColorComponents {
310 match self.0.as_ref() {
311 ColorSpaceType::DeviceCmyk => smallvec![0.0, 0.0, 0.0, 1.0],
312 ColorSpaceType::DeviceGray => smallvec![0.0],
313 ColorSpaceType::DeviceRgb => smallvec![0.0, 0.0, 0.0],
314 ColorSpaceType::ICCBased(icc) => match icc.0.number_components {
315 1 => smallvec![0.0],
316 3 => smallvec![0.0, 0.0, 0.0],
317 4 => smallvec![0.0, 0.0, 0.0, 1.0],
318 _ => unreachable!(),
319 },
320 ColorSpaceType::CalGray(_) => smallvec![0.0],
321 ColorSpaceType::CalRgb(_) => smallvec![0.0, 0.0, 0.0],
322 ColorSpaceType::Lab(_) => smallvec![0.0, 0.0, 0.0],
323 ColorSpaceType::Indexed(_) => smallvec![0.0],
324 ColorSpaceType::Separation(_) => smallvec![1.0],
325 ColorSpaceType::Pattern(c) => c.initial_color(),
326 ColorSpaceType::DeviceN(d) => smallvec![1.0; d.num_components as usize],
327 }
328 }
329
330 pub(crate) fn num_components(&self) -> u8 {
332 match self.0.as_ref() {
333 ColorSpaceType::DeviceCmyk => 4,
334 ColorSpaceType::DeviceGray => 1,
335 ColorSpaceType::DeviceRgb => 3,
336 ColorSpaceType::ICCBased(icc) => icc.0.number_components as u8,
337 ColorSpaceType::CalGray(_) => 1,
338 ColorSpaceType::CalRgb(_) => 3,
339 ColorSpaceType::Lab(_) => 3,
340 ColorSpaceType::Indexed(_) => 1,
341 ColorSpaceType::Separation(_) => 1,
342 ColorSpaceType::Pattern(p) => p.num_components(),
343 ColorSpaceType::DeviceN(d) => d.num_components,
344 }
345 }
346
347 pub fn to_rgba(&self, c: &[f32], opacity: f32, manual_scale: bool) -> AlphaColor {
349 self.to_alpha_color(c, opacity, manual_scale)
350 .unwrap_or(AlphaColor::BLACK)
351 }
352}
353
354impl ToRgb for ColorSpace {
355 fn convert_f32(&self, input: &[f32], output: &mut [u8], manual_scale: bool) -> Option<()> {
356 match self.0.as_ref() {
357 ColorSpaceType::DeviceCmyk => {
358 if let Some(profile) = default_cmyk_profile() {
364 return profile.convert_f32(input, output, manual_scale);
365 }
366 for (input, output) in input.chunks_exact(4).zip(output.chunks_exact_mut(3)) {
368 let (c, m, y, k) = (input[0], input[1], input[2], input[3]);
369 output[0] = f32_to_u8(1.0 - (c + k).min(1.0));
370 output[1] = f32_to_u8(1.0 - (m + k).min(1.0));
371 output[2] = f32_to_u8(1.0 - (y + k).min(1.0));
372 }
373 Some(())
374 }
375 ColorSpaceType::DeviceGray => {
376 let converted = input.iter().copied().map(f32_to_u8).collect::<Vec<_>>();
377
378 for (input, output) in converted.iter().zip(output.chunks_exact_mut(3)) {
379 output.copy_from_slice(&[*input, *input, *input]);
380 }
381
382 Some(())
383 }
384 ColorSpaceType::DeviceRgb => {
385 for (input, output) in input.iter().copied().zip(output) {
386 *output = f32_to_u8(input);
387 }
388
389 Some(())
390 }
391 ColorSpaceType::Pattern(i) => i.convert_f32(input, output, manual_scale),
392 ColorSpaceType::Indexed(i) => i.convert_f32(input, output, manual_scale),
393 ColorSpaceType::ICCBased(i) => i.convert_f32(input, output, manual_scale),
394 ColorSpaceType::CalGray(i) => i.convert_f32(input, output, manual_scale),
395 ColorSpaceType::CalRgb(i) => i.convert_f32(input, output, manual_scale),
396 ColorSpaceType::Lab(i) => i.convert_f32(input, output, manual_scale),
397 ColorSpaceType::Separation(i) => i.convert_f32(input, output, manual_scale),
398 ColorSpaceType::DeviceN(i) => i.convert_f32(input, output, manual_scale),
399 }
400 }
401
402 fn supports_u8(&self) -> bool {
403 match self.0.as_ref() {
404 ColorSpaceType::DeviceCmyk => true,
405 ColorSpaceType::DeviceGray => true,
406 ColorSpaceType::DeviceRgb => true,
407 ColorSpaceType::Pattern(i) => i.supports_u8(),
408 ColorSpaceType::Indexed(i) => i.supports_u8(),
409 ColorSpaceType::ICCBased(i) => i.supports_u8(),
410 ColorSpaceType::CalGray(i) => i.supports_u8(),
411 ColorSpaceType::CalRgb(i) => i.supports_u8(),
412 ColorSpaceType::Lab(i) => i.supports_u8(),
413 ColorSpaceType::Separation(i) => i.supports_u8(),
414 ColorSpaceType::DeviceN(i) => i.supports_u8(),
415 }
416 }
417
418 fn convert_u8(&self, input: &[u8], output: &mut [u8]) -> Option<()> {
419 match self.0.as_ref() {
420 ColorSpaceType::DeviceCmyk => {
421 if let Some(profile) = default_cmyk_profile() {
423 return profile.convert_u8(input, output);
424 }
425 for (input, output) in input.chunks_exact(4).zip(output.chunks_exact_mut(3)) {
427 let (c, m, y, k) = (
428 input[0] as u16,
429 input[1] as u16,
430 input[2] as u16,
431 input[3] as u16,
432 );
433 output[0] = (255u16.saturating_sub(c + k)) as u8;
434 output[1] = (255u16.saturating_sub(m + k)) as u8;
435 output[2] = (255u16.saturating_sub(y + k)) as u8;
436 }
437 Some(())
438 }
439 ColorSpaceType::DeviceGray => {
440 for (input, output) in input.iter().zip(output.chunks_exact_mut(3)) {
441 output.copy_from_slice(&[*input, *input, *input]);
442 }
443
444 Some(())
445 }
446 ColorSpaceType::DeviceRgb => {
447 output.copy_from_slice(input);
448
449 Some(())
450 }
451 ColorSpaceType::Pattern(i) => i.convert_u8(input, output),
452 ColorSpaceType::Indexed(i) => i.convert_u8(input, output),
453 ColorSpaceType::ICCBased(i) => i.convert_u8(input, output),
454 ColorSpaceType::CalGray(i) => i.convert_u8(input, output),
455 ColorSpaceType::CalRgb(i) => i.convert_u8(input, output),
456 ColorSpaceType::Lab(i) => i.convert_u8(input, output),
457 ColorSpaceType::Separation(i) => i.convert_u8(input, output),
458 ColorSpaceType::DeviceN(i) => i.convert_u8(input, output),
459 }
460 }
461
462 fn is_none(&self) -> bool {
463 match self.0.as_ref() {
464 ColorSpaceType::Separation(s) => s.is_none(),
465 ColorSpaceType::DeviceN(d) => d.is_none(),
466 _ => false,
467 }
468 }
469}
470
471#[derive(Debug, Clone)]
472pub(crate) struct CalGray {
473 white_point: [f32; 3],
474 black_point: [f32; 3],
475 gamma: f32,
476}
477
478impl CalGray {
480 fn new(dict: &Dict<'_>) -> Option<Self> {
481 let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]);
482 let black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]);
483 let gamma = dict.get::<f32>(GAMMA).unwrap_or(1.0);
484
485 Some(Self {
486 white_point,
487 black_point,
488 gamma,
489 })
490 }
491}
492
493impl ToRgb for CalGray {
494 fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
495 for (input, output) in input.iter().copied().zip(output.chunks_exact_mut(3)) {
496 let g = self.gamma;
497 let (_xw, yw, _zw) = {
498 let wp = self.white_point;
499 (wp[0], wp[1], wp[2])
500 };
501 let (_xb, _yb, _zb) = {
502 let bp = self.black_point;
503 (bp[0], bp[1], bp[2])
504 };
505
506 let a = input;
507 let ag = a.powf(g);
508 let l = yw * ag;
509 let val = (0.0_f32.max(295.8 * l.powf(0.333_333_34) - 40.8) + 0.5) as u8;
510
511 output.copy_from_slice(&[val, val, val]);
512 }
513
514 Some(())
515 }
516}
517
518#[derive(Debug, Clone)]
519pub(crate) struct CalRgb {
520 white_point: [f32; 3],
521 black_point: [f32; 3],
522 matrix: [f32; 9],
523 gamma: [f32; 3],
524}
525
526impl CalRgb {
531 fn new(dict: &Dict<'_>) -> Option<Self> {
532 let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]);
533 let black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]);
534 let matrix = dict
535 .get::<[f32; 9]>(MATRIX)
536 .unwrap_or([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]);
537 let gamma = dict.get::<[f32; 3]>(GAMMA).unwrap_or([1.0, 1.0, 1.0]);
538
539 Some(Self {
540 white_point,
541 black_point,
542 matrix,
543 gamma,
544 })
545 }
546
547 const BRADFORD_SCALE_MATRIX: [f32; 9] = [
548 0.8951, 0.2664, -0.1614, -0.7502, 1.7135, 0.0367, 0.0389, -0.0685, 1.0296,
549 ];
550
551 const BRADFORD_SCALE_INVERSE_MATRIX: [f32; 9] = [
552 0.9869929, -0.1470543, 0.1599627, 0.4323053, 0.5183603, 0.0492912, -0.0085287, 0.0400428,
553 0.9684867,
554 ];
555
556 const SRGB_D65_XYZ_TO_RGB_MATRIX: [f32; 9] = [
557 3.2404542, -1.5371385, -0.4985314, -0.969_266, 1.8760108, 0.0415560, 0.0556434, -0.2040259,
558 1.0572252,
559 ];
560
561 const FLAT_WHITEPOINT: [f32; 3] = [1.0, 1.0, 1.0];
562 const D65_WHITEPOINT: [f32; 3] = [0.95047, 1.0, 1.08883];
563
564 fn decode_l_constant() -> f32 {
565 ((8.0_f32 + 16.0) / 116.0).powi(3) / 8.0
566 }
567
568 fn srgb_transfer_function(color: f32) -> f32 {
569 if color <= 0.0031308 {
570 (12.92 * color).clamp(0.0, 1.0)
571 } else if color >= 0.99554525 {
572 1.0
573 } else {
574 ((1.0 + 0.055) * color.powf(1.0 / 2.4) - 0.055).clamp(0.0, 1.0)
575 }
576 }
577
578 fn matrix_product(a: &[f32; 9], b: &[f32; 3]) -> [f32; 3] {
579 [
580 a[0] * b[0] + a[1] * b[1] + a[2] * b[2],
581 a[3] * b[0] + a[4] * b[1] + a[5] * b[2],
582 a[6] * b[0] + a[7] * b[1] + a[8] * b[2],
583 ]
584 }
585
586 fn to_flat(source_white_point: &[f32; 3], lms: &[f32; 3]) -> [f32; 3] {
587 [
588 lms[0] / source_white_point[0],
589 lms[1] / source_white_point[1],
590 lms[2] / source_white_point[2],
591 ]
592 }
593
594 fn to_d65(source_white_point: &[f32; 3], lms: &[f32; 3]) -> [f32; 3] {
595 [
596 lms[0] * Self::D65_WHITEPOINT[0] / source_white_point[0],
597 lms[1] * Self::D65_WHITEPOINT[1] / source_white_point[1],
598 lms[2] * Self::D65_WHITEPOINT[2] / source_white_point[2],
599 ]
600 }
601
602 fn decode_l(l: f32) -> f32 {
603 if l < 0.0 {
604 -Self::decode_l(-l)
605 } else if l > 8.0 {
606 ((l + 16.0) / 116.0).powi(3)
607 } else {
608 l * Self::decode_l_constant()
609 }
610 }
611
612 fn compensate_black_point(source_bp: &[f32; 3], xyz_flat: &[f32; 3]) -> [f32; 3] {
613 if source_bp == &[0.0, 0.0, 0.0] {
614 return *xyz_flat;
615 }
616
617 let zero_decode_l = Self::decode_l(0.0);
618
619 let mut out = [0.0; 3];
620 for i in 0..3 {
621 let src = Self::decode_l(source_bp[i]);
622 let scale = (1.0 - zero_decode_l) / (1.0 - src);
623 let offset = 1.0 - scale;
624 out[i] = xyz_flat[i] * scale + offset;
625 }
626
627 out
628 }
629
630 fn normalize_white_point_to_flat(
631 &self,
632 source_white_point: &[f32; 3],
633 xyz: &[f32; 3],
634 ) -> [f32; 3] {
635 if source_white_point[0] == 1.0 && source_white_point[2] == 1.0 {
636 return *xyz;
637 }
638 let lms = Self::matrix_product(&Self::BRADFORD_SCALE_MATRIX, xyz);
639 let lms_flat = Self::to_flat(source_white_point, &lms);
640 Self::matrix_product(&Self::BRADFORD_SCALE_INVERSE_MATRIX, &lms_flat)
641 }
642
643 fn normalize_white_point_to_d65(
644 &self,
645 source_white_point: &[f32; 3],
646 xyz: &[f32; 3],
647 ) -> [f32; 3] {
648 let lms = Self::matrix_product(&Self::BRADFORD_SCALE_MATRIX, xyz);
649 let lms_d65 = Self::to_d65(source_white_point, &lms);
650 Self::matrix_product(&Self::BRADFORD_SCALE_INVERSE_MATRIX, &lms_d65)
651 }
652}
653
654impl ToRgb for CalRgb {
655 fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
656 for (input, output) in input.chunks_exact(3).zip(output.chunks_exact_mut(3)) {
657 let input = [
658 input[0].clamp(0.0, 1.0),
659 input[1].clamp(0.0, 1.0),
660 input[2].clamp(0.0, 1.0),
661 ];
662
663 let [r, g, b] = input;
664 let [gr, gg, gb] = self.gamma;
665 let [agr, bgg, cgb] = [
666 if r == 1.0 { 1.0 } else { r.powf(gr) },
667 if g == 1.0 { 1.0 } else { g.powf(gg) },
668 if b == 1.0 { 1.0 } else { b.powf(gb) },
669 ];
670
671 let m = &self.matrix;
672 let x = m[0] * agr + m[3] * bgg + m[6] * cgb;
673 let y = m[1] * agr + m[4] * bgg + m[7] * cgb;
674 let z = m[2] * agr + m[5] * bgg + m[8] * cgb;
675 let xyz = [x, y, z];
676
677 let xyz_flat = self.normalize_white_point_to_flat(&self.white_point, &xyz);
678 let xyz_black = Self::compensate_black_point(&self.black_point, &xyz_flat);
679 let xyz_d65 = self.normalize_white_point_to_d65(&Self::FLAT_WHITEPOINT, &xyz_black);
680 let srgb_xyz = Self::matrix_product(&Self::SRGB_D65_XYZ_TO_RGB_MATRIX, &xyz_d65);
681
682 output.copy_from_slice(&[
683 (Self::srgb_transfer_function(srgb_xyz[0]) * 255.0 + 0.5) as u8,
684 (Self::srgb_transfer_function(srgb_xyz[1]) * 255.0 + 0.5) as u8,
685 (Self::srgb_transfer_function(srgb_xyz[2]) * 255.0 + 0.5) as u8,
686 ]);
687 }
688
689 Some(())
690 }
691}
692
693#[derive(Debug, Clone)]
694pub(crate) struct Lab {
695 range: [f32; 4],
696 profile: ICCProfile,
697}
698
699impl Lab {
700 fn new(dict: &Dict<'_>) -> Option<Self> {
701 let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]);
702 let _black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]);
704 let range = dict
705 .get::<[f32; 4]>(RANGE)
706 .unwrap_or([-100.0, 100.0, -100.0, 100.0]);
707
708 let mut profile = ColorProfile::new_from_slice(include_bytes!("../assets/LAB.icc")).ok()?;
709 profile.white_point = Xyzd::new(
710 white_point[0] as f64,
711 white_point[1] as f64,
712 white_point[2] as f64,
713 );
714
715 let profile = ICCProfile::new_from_src_profile(
716 profile, false,
717 false, 3,
721 )?;
722
723 Some(Self { range, profile })
724 }
725}
726
727impl ToRgb for Lab {
728 fn convert_f32(&self, input: &[f32], output: &mut [u8], manual_scale: bool) -> Option<()> {
729 if !manual_scale {
730 let input = input
734 .chunks_exact(3)
735 .flat_map(|i| {
736 let l = i[0] / 100.0;
737 let a = (i[1] + 128.0) / 255.0;
738 let b = (i[2] + 128.0) / 255.0;
739
740 [l, a, b]
741 })
742 .collect::<Vec<_>>();
743
744 self.profile.convert_f32(&input, output, manual_scale)
745 } else {
746 self.profile.convert_f32(input, output, manual_scale)
747 }
748 }
749}
750
751#[derive(Debug, Clone)]
752pub(crate) struct Indexed {
753 values: Vec<Vec<f32>>,
754 hival: u8,
755 base: Box<ColorSpace>,
756}
757
758impl Indexed {
759 fn new(array: &Array<'_>, cache: &Cache) -> Option<Self> {
760 let mut iter = array.flex_iter();
761 let _ = iter.next::<Name>()?;
763 let base_color_space = ColorSpace::new(iter.next::<Object<'_>>()?, cache)?;
764 let hival = iter.next::<u8>()?;
765
766 let values = {
767 let data = iter
768 .next::<Stream<'_>>()
769 .and_then(|s| s.decoded().ok())
770 .or_else(|| iter.next::<object::String>().map(|s| s.to_vec()))?;
771
772 let num_components = base_color_space.num_components();
773
774 let mut byte_iter = data.iter().copied();
775
776 let mut vals = vec![];
777 for _ in 0..=hival {
778 let mut temp = vec![];
779
780 for _ in 0..num_components {
781 temp.push(byte_iter.next()? as f32 / 255.0);
782 }
783
784 vals.push(temp);
785 }
786
787 vals
788 };
789
790 Some(Self {
791 values,
792 hival,
793 base: Box::new(base_color_space),
794 })
795 }
796}
797
798impl ToRgb for Indexed {
799 fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
800 let mut indexed = vec![0.0; input.len() * self.base.num_components() as usize];
801
802 for (input, output) in input
803 .iter()
804 .copied()
805 .zip(indexed.chunks_exact_mut(self.base.num_components() as usize))
806 {
807 let idx = (input.clamp(0.0, self.hival as f32) + 0.5) as usize;
808 output.copy_from_slice(&self.values[idx]);
809 }
810
811 self.base.convert_f32(&indexed, output, true)
812 }
813}
814
815#[derive(Debug, Clone)]
816pub(crate) struct Separation {
817 alternate_space: ColorSpace,
818 tint_transform: Function,
819 is_none_separation: bool,
820}
821
822impl Separation {
823 fn new(array: &Array<'_>, cache: &Cache) -> Option<Self> {
824 let mut iter = array.flex_iter();
825 let _ = iter.next::<Name>()?;
827 let name = iter.next::<Name>()?;
828 let alternate_space = ColorSpace::new(iter.next::<Object<'_>>()?, cache)?;
829 let tint_transform = Function::new(&iter.next::<Object<'_>>()?)?;
830 let is_none_separation = name.as_str() == "None";
837
838 Some(Self {
839 alternate_space,
840 tint_transform,
841 is_none_separation,
842 })
843 }
844}
845
846impl ToRgb for Separation {
847 fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
848 let evaluated = input
849 .iter()
850 .flat_map(|n| {
851 self.tint_transform
852 .eval(smallvec![*n])
853 .unwrap_or(self.alternate_space.initial_color())
854 })
855 .collect::<Vec<_>>();
856 self.alternate_space.convert_f32(&evaluated, output, false)
857 }
858
859 fn is_none(&self) -> bool {
860 self.is_none_separation
861 }
862}
863
864#[derive(Debug, Clone)]
865pub(crate) struct DeviceN {
866 alternate_space: ColorSpace,
867 num_components: u8,
868 tint_transform: Function,
869 is_none: bool,
870}
871
872impl DeviceN {
873 fn new(array: &Array<'_>, cache: &Cache) -> Option<Self> {
874 let mut iter = array.flex_iter();
875 let _ = iter.next::<Name>()?;
877 let names = iter.next::<Array<'_>>()?.iter::<Name>().collect::<Vec<_>>();
878 let num_components = u8::try_from(names.len()).ok()?;
879 let all_none = names.iter().all(|n| n.as_str() == "None");
885 let alternate_space = ColorSpace::new(iter.next::<Object<'_>>()?, cache)?;
886 let tint_transform = Function::new(&iter.next::<Object<'_>>()?)?;
887
888 if num_components == 0 {
889 return None;
890 }
891
892 Some(Self {
893 alternate_space,
894 num_components,
895 tint_transform,
896 is_none: all_none,
897 })
898 }
899}
900
901impl ToRgb for DeviceN {
902 fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
903 let evaluated = input
904 .chunks_exact(self.num_components as usize)
905 .flat_map(|n| {
906 self.tint_transform
907 .eval(n.to_smallvec())
908 .unwrap_or(self.alternate_space.initial_color())
909 })
910 .collect::<Vec<_>>();
911 self.alternate_space.convert_f32(&evaluated, output, false)
912 }
913
914 fn is_none(&self) -> bool {
915 self.is_none
916 }
917}
918
919struct ICCColorRepr {
920 transform_u8: Box<Transform8BitExecutor>,
921 transform_f32: Box<TransformF32BitExecutor>,
922 number_components: usize,
923 is_srgb: bool,
924 is_lab: bool,
925}
926
927#[derive(Clone)]
928pub(crate) struct ICCProfile(Arc<ICCColorRepr>);
929
930impl Debug for ICCProfile {
931 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
932 write!(f, "ICCColor {{..}}")
933 }
934}
935
936impl ICCProfile {
937 fn new(profile: &[u8], number_components: usize) -> Option<Self> {
938 let src_profile = ColorProfile::new_from_slice(profile).ok()?;
939
940 let profile_components = match src_profile.color_space {
948 DataColorSpace::Gray => 1,
949 DataColorSpace::Rgb
950 | DataColorSpace::Lab
951 | DataColorSpace::Luv
952 | DataColorSpace::Xyz
953 | DataColorSpace::YCbr
954 | DataColorSpace::Yxy
955 | DataColorSpace::Hsv
956 | DataColorSpace::Hls
957 | DataColorSpace::Cmy
958 | DataColorSpace::Color3 => 3,
959 DataColorSpace::Cmyk | DataColorSpace::Color4 => 4,
960 _ => {
961 warn!(
962 "unsupported ICC profile color space {:?}",
963 src_profile.color_space
964 );
965 return None;
966 }
967 };
968
969 if number_components != profile_components {
970 warn!(
971 "ICCBased /N={} does not match embedded ICC profile component count {}; using profile",
972 number_components, profile_components
973 );
974 }
975
976 const SRGB_MARKER: &[u8] = b"sRGB";
977
978 let is_srgb = profile
979 .get(52..56)
980 .map(|device_model| device_model == SRGB_MARKER)
981 .unwrap_or(false);
982 let is_lab = src_profile.color_space == DataColorSpace::Lab;
983
984 Self::new_from_src_profile(src_profile, is_srgb, is_lab, profile_components)
985 }
986
987 fn new_from_src_profile(
988 src_profile: ColorProfile,
989 is_srgb: bool,
990 is_lab: bool,
991 number_components: usize,
992 ) -> Option<Self> {
993 let dest_profile = ColorProfile::new_srgb();
994
995 let src_layout = match number_components {
1003 1 => Layout::Gray,
1004 3 => Layout::Rgb,
1005 4 => Layout::Rgba, _ => {
1007 warn!("unsupported number of components {number_components} for ICC profile");
1008
1009 return None;
1010 }
1011 };
1012
1013 let intents_to_try = [
1030 RenderingIntent::RelativeColorimetric,
1031 RenderingIntent::Perceptual,
1032 RenderingIntent::Saturation,
1033 ];
1034 let mut u8_transform = None;
1035 let mut f32_transform = None;
1036 for intent in intents_to_try {
1037 let options = TransformOptions {
1038 rendering_intent: intent,
1039 ..TransformOptions::default()
1040 };
1041 let u8_ok = src_profile
1042 .create_transform_8bit(src_layout, &dest_profile, Layout::Rgb, options)
1043 .ok();
1044 let f32_ok = src_profile
1045 .create_transform_f32(src_layout, &dest_profile, Layout::Rgb, options)
1046 .ok();
1047 if let (Some(u), Some(f)) = (u8_ok, f32_ok) {
1048 u8_transform = Some(u);
1049 f32_transform = Some(f);
1050 break;
1051 }
1052 }
1053 let u8_transform = u8_transform?;
1054 let f32_transform = f32_transform?;
1055
1056 Some(Self(Arc::new(ICCColorRepr {
1057 transform_u8: u8_transform,
1058 transform_f32: f32_transform,
1059 number_components,
1060 is_srgb,
1061 is_lab,
1062 })))
1063 }
1064
1065 fn is_srgb(&self) -> bool {
1066 self.0.is_srgb
1067 }
1068
1069 fn is_lab(&self) -> bool {
1070 self.0.is_lab
1071 }
1072}
1073
1074impl ToRgb for ICCProfile {
1075 fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
1076 let mut temp = vec![0.0_f32; output.len()];
1077
1078 if self.is_lab() {
1079 let scaled = input
1081 .chunks_exact(3)
1082 .flat_map(|i| {
1083 [
1084 i[0] * (1.0 / 100.0),
1085 (i[1] + 128.0) * (1.0 / 255.0),
1086 (i[2] + 128.0) * (1.0 / 255.0),
1087 ]
1088 })
1089 .collect::<Vec<_>>();
1090 self.0.transform_f32.transform(&scaled, &mut temp).ok()?;
1091 } else {
1092 self.0.transform_f32.transform(input, &mut temp).ok()?;
1093 };
1094
1095 for (input, output) in temp.iter().zip(output.iter_mut()) {
1096 *output = (input * 255.0 + 0.5) as u8;
1097 }
1098
1099 Some(())
1100 }
1101
1102 fn supports_u8(&self) -> bool {
1103 true
1104 }
1105
1106 fn convert_u8(&self, input: &[u8], output: &mut [u8]) -> Option<()> {
1107 if self.is_srgb() && input.len() == output.len() {
1114 output.copy_from_slice(input);
1115 } else {
1116 self.0.transform_u8.transform(input, output).ok()?;
1117 }
1118
1119 Some(())
1120 }
1121}
1122
1123#[inline(always)]
1124fn f32_to_u8(val: f32) -> u8 {
1125 (val * 255.0 + 0.5) as u8
1126}
1127
1128#[derive(Debug, Clone)]
1129pub struct Color {
1131 color_space: ColorSpace,
1132 components: ColorComponents,
1133 opacity: f32,
1134}
1135
1136impl Color {
1137 pub(crate) fn new(color_space: ColorSpace, components: ColorComponents, opacity: f32) -> Self {
1138 Self {
1139 color_space,
1140 components,
1141 opacity,
1142 }
1143 }
1144
1145 pub fn to_rgba(&self) -> AlphaColor {
1147 self.color_space
1148 .to_rgba(&self.components, self.opacity, false)
1149 }
1150
1151 pub fn from_rgba(rgba: AlphaColor) -> Self {
1153 let c = rgba.components();
1154 Self {
1155 color_space: ColorSpace::device_rgb(),
1156 components: smallvec![c[0], c[1], c[2]],
1157 opacity: c[3],
1158 }
1159 }
1160
1161 pub fn from_device_rgb(r: f32, g: f32, b: f32) -> Self {
1163 Self {
1164 color_space: ColorSpace::device_rgb(),
1165 components: smallvec![r, g, b],
1166 opacity: 1.0,
1167 }
1168 }
1169
1170 pub fn from_device_rgb_with_opacity(r: f32, g: f32, b: f32, opacity: f32) -> Self {
1172 Self {
1173 color_space: ColorSpace::device_rgb(),
1174 components: smallvec![r, g, b],
1175 opacity,
1176 }
1177 }
1178
1179 pub fn opacity(&self) -> f32 {
1181 self.opacity
1182 }
1183
1184 pub fn is_device_cmyk(&self) -> bool {
1186 self.color_space.is_device_cmyk()
1187 }
1188
1189 pub fn device_cmyk_components(&self) -> Option<[f32; 4]> {
1191 if !self.color_space.is_device_cmyk() || self.components.len() != 4 {
1192 return None;
1193 }
1194
1195 Some([
1196 self.components[0],
1197 self.components[1],
1198 self.components[2],
1199 self.components[3],
1200 ])
1201 }
1202}
1203
1204pub(crate) trait ToRgb {
1205 fn convert_f32(&self, input: &[f32], output: &mut [u8], manual_scale: bool) -> Option<()>;
1206 fn supports_u8(&self) -> bool {
1207 false
1208 }
1209 fn convert_u8(&self, _: &[u8], _: &mut [u8]) -> Option<()> {
1210 unimplemented!();
1211 }
1212 fn is_none(&self) -> bool {
1213 false
1214 }
1215 fn to_alpha_color(
1216 &self,
1217 input: &[f32],
1218 mut opacity: f32,
1219 manual_scale: bool,
1220 ) -> Option<AlphaColor> {
1221 let mut output = [0; 3];
1222 self.convert_f32(input, &mut output, manual_scale)?;
1223
1224 if self.is_none() {
1229 opacity = 0.0;
1230 }
1231
1232 Some(AlphaColor::from_rgba8(
1233 output[0],
1234 output[1],
1235 output[2],
1236 (opacity * 255.0 + 0.5) as u8,
1237 ))
1238 }
1239}
1240
1241#[cfg(test)]
1242mod tests {
1243 use super::*;
1244 use pdf_syntax::object::{Array, FromBytes};
1245
1246 fn separation_array(ink_name: &str) -> Vec<u8> {
1250 format!(
1251 "[/Separation /{ink_name} /DeviceGray \
1252 << /FunctionType 2 /Domain [0 1] /C0 [1] /C1 [0] /N 1 >> ]",
1253 ink_name = ink_name
1254 )
1255 .into_bytes()
1256 }
1257
1258 fn make_separation(ink_name: &str) -> Option<Separation> {
1259 let bytes = separation_array(ink_name);
1260 let array = Array::from_bytes(&bytes)?;
1261 let cache = Cache::new();
1262 Separation::new(&array, &cache)
1263 }
1264
1265 #[test]
1267 fn none_ink_is_suppressed() {
1268 let sep = make_separation("None").expect("should parse");
1269 assert!(sep.is_none(), "ink name 'None' must be suppressed");
1270 }
1271
1272 #[test]
1274 fn pantone_ink_is_not_suppressed() {
1275 let sep = make_separation("PANTONE#20123#20CVC").expect("should parse");
1277 assert!(!sep.is_none(), "PANTONE spot colour must not be suppressed");
1278 }
1279
1280 #[test]
1282 fn all_ink_is_not_suppressed() {
1283 let sep = make_separation("All").expect("should parse");
1284 assert!(!sep.is_none(), "'All' separation must not be suppressed");
1285 }
1286
1287 #[test]
1289 fn pantone_produces_visible_color() {
1290 let sep = make_separation("PANTONE#20123#20CVC").expect("should parse");
1291 let cs = ColorSpace(Arc::new(ColorSpaceType::Separation(sep)));
1292 let color = cs.to_rgba(&[1.0], 1.0, false);
1294 assert!(
1295 color.to_rgba8()[3] > 0,
1296 "PANTONE ink at tint=1.0 must be opaque"
1297 );
1298 }
1299
1300 #[test]
1302 fn none_produces_transparent_color() {
1303 let sep = make_separation("None").expect("should parse");
1304 let cs = ColorSpace(Arc::new(ColorSpaceType::Separation(sep)));
1305 let color = cs.to_rgba(&[1.0], 1.0, false);
1306 assert_eq!(
1307 color.to_rgba8()[3],
1308 0,
1309 "Separation/None ink must be fully transparent"
1310 );
1311 }
1312}