1use crate::WarningSinkFn;
4use crate::cache::{Cache, CacheKey};
5use crate::function::Function;
6use crate::util::decode_or_warn;
7use log::warn;
8use moxcms::{
9 ColorProfile, DataColorSpace, Layout, RenderingIntent, Transform8BitExecutor,
10 TransformF32BitExecutor, TransformOptions, Xyzd,
11};
12use pdf_syntax::object;
13use pdf_syntax::object::Array;
14use pdf_syntax::object::Dict;
15use pdf_syntax::object::Name;
16use pdf_syntax::object::Object;
17use pdf_syntax::object::Stream;
18use pdf_syntax::object::dict::keys::*;
19use smallvec::{SmallVec, ToSmallVec, smallvec};
20use std::fmt::{Debug, Formatter};
21use std::ops::Deref;
22use std::sync::{Arc, OnceLock};
23
24static DEFAULT_CMYK_PROFILE: OnceLock<Option<ICCProfile>> = OnceLock::new();
27
28fn default_cmyk_profile() -> Option<&'static ICCProfile> {
29 DEFAULT_CMYK_PROFILE
30 .get_or_init(|| ICCProfile::new(include_bytes!("../assets/CGATS001Compat-v2-micro.icc"), 4))
31 .as_ref()
32}
33
34pub type ColorComponents = SmallVec<[f32; 4]>;
36
37#[derive(Debug, Copy, Clone)]
39pub struct AlphaColor {
40 components: [f32; 4],
41}
42
43impl AlphaColor {
44 pub const BLACK: Self = Self::new([0., 0., 0., 1.]);
46
47 pub const TRANSPARENT: Self = Self::new([0., 0., 0., 0.]);
49
50 pub const WHITE: Self = Self::new([1., 1., 1., 1.]);
52
53 pub const fn new(components: [f32; 4]) -> Self {
55 Self { components }
56 }
57
58 pub const fn from_rgb8(r: u8, g: u8, b: u8) -> Self {
60 let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), 1.];
61 Self::new(components)
62 }
63
64 pub fn premultiplied(&self) -> [f32; 4] {
66 [
67 self.components[0] * self.components[3],
68 self.components[1] * self.components[3],
69 self.components[2] * self.components[3],
70 self.components[3],
71 ]
72 }
73
74 pub const fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
76 let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), u8_to_f32(a)];
77 Self::new(components)
78 }
79
80 pub fn to_rgba8(&self) -> [u8; 4] {
82 [
83 (self.components[0] * 255.0 + 0.5) as u8,
84 (self.components[1] * 255.0 + 0.5) as u8,
85 (self.components[2] * 255.0 + 0.5) as u8,
86 (self.components[3] * 255.0 + 0.5) as u8,
87 ]
88 }
89
90 pub fn components(&self) -> [f32; 4] {
92 self.components
93 }
94}
95
96const fn u8_to_f32(x: u8) -> f32 {
97 x as f32 * (1.0 / 255.0)
98}
99
100#[derive(Debug, Clone)]
101pub(crate) enum ColorSpaceType {
102 DeviceCmyk,
103 DeviceGray,
104 DeviceRgb,
105 Pattern(ColorSpace),
106 Indexed(Indexed),
107 ICCBased(ICCProfile),
108 CalGray(CalGray),
109 CalRgb(CalRgb),
110 Lab(Lab),
111 Separation(Separation),
112 DeviceN(DeviceN),
113}
114
115impl ColorSpaceType {
116 fn new(object: Object<'_>, cache: &Cache, warning_sink: &WarningSinkFn) -> Option<Self> {
117 Self::new_inner(object, cache, warning_sink)
118 }
119
120 fn new_inner(object: Object<'_>, cache: &Cache, warning_sink: &WarningSinkFn) -> Option<Self> {
121 if let Some(name) = object.clone().into_name() {
122 return Self::new_from_name(name.clone());
123 } else if let Some(color_array) = object.clone().into_array() {
124 let mut iter = color_array.clone().flex_iter();
125 let name = iter.next::<Name>()?;
126
127 match name.deref() {
128 ICC_BASED => {
129 let icc_stream = iter.next::<Stream<'_>>()?;
130 let dict = icc_stream.dict();
131 let num_components = dict.get::<usize>(N);
136
137 return cache.get_or_insert_with(icc_stream.cache_key(), || {
138 let from_icc = num_components.and_then(|n| {
142 decode_or_warn(&icc_stream, warning_sink)
143 .as_ref()
144 .and_then(|decoded| {
145 ICCProfile::new(decoded, n).map(|icc| {
146 if icc.is_srgb() {
151 Self::DeviceRgb
152 } else {
153 Self::ICCBased(icc)
154 }
155 })
156 })
157 });
158
159 from_icc
160 .or_else(|| {
161 dict.get::<Object<'_>>(ALTERNATE)
162 .and_then(|o| Self::new(o, cache, warning_sink))
163 })
164 .or_else(|| match num_components {
165 Some(1) => Some(Self::DeviceGray),
166 Some(3) => Some(Self::DeviceRgb),
167 Some(4) => Some(Self::DeviceCmyk),
168 _ => None,
169 })
170 });
171 }
172 CALCMYK => return Some(Self::DeviceCmyk),
173 CALGRAY => {
174 let cal_dict = iter.next::<Dict<'_>>()?;
175 return Some(Self::CalGray(CalGray::new(&cal_dict)?));
176 }
177 CALRGB => {
178 let cal_dict = iter.next::<Dict<'_>>()?;
179 return Some(Self::CalRgb(CalRgb::new(&cal_dict)?));
180 }
181 DEVICE_RGB | RGB => return Some(Self::DeviceRgb),
182 DEVICE_GRAY | G => return Some(Self::DeviceGray),
183 DEVICE_CMYK | CMYK => return Some(Self::DeviceCmyk),
184 LAB => {
185 let lab_dict = iter.next::<Dict<'_>>()?;
186 return Some(Self::Lab(Lab::new(&lab_dict)?));
187 }
188 INDEXED | I => {
189 return Some(Self::Indexed(Indexed::new(
190 &color_array,
191 cache,
192 warning_sink,
193 )?));
194 }
195 SEPARATION => {
196 return Some(Self::Separation(Separation::new(
197 &color_array,
198 cache,
199 warning_sink,
200 )?));
201 }
202 DEVICE_N => {
203 return Some(Self::DeviceN(DeviceN::new(
204 &color_array,
205 cache,
206 warning_sink,
207 )?));
208 }
209 PATTERN => {
210 let cs = iter
213 .next::<Object<'_>>()
214 .and_then(|o| ColorSpace::new(o, cache, warning_sink))
215 .unwrap_or(ColorSpace::device_rgb());
216 return Some(Self::Pattern(cs));
217 }
218 _ => {
219 warn!("unsupported color space: {}", name.as_str());
220 return None;
221 }
222 }
223 }
224
225 None
226 }
227
228 fn new_from_name(name: Name) -> Option<Self> {
229 match name.deref() {
230 DEVICE_RGB | RGB => Some(Self::DeviceRgb),
231 DEVICE_GRAY | G => Some(Self::DeviceGray),
232 DEVICE_CMYK | CMYK => Some(Self::DeviceCmyk),
233 CALCMYK => Some(Self::DeviceCmyk),
234 PATTERN => Some(Self::Pattern(ColorSpace::device_rgb())),
235 _ => None,
236 }
237 }
238}
239
240#[derive(Debug, Clone)]
242pub struct ColorSpace(Arc<ColorSpaceType>);
243
244impl ColorSpace {
245 pub(crate) fn new(
247 object: Object<'_>,
248 cache: &Cache,
249 warning_sink: &WarningSinkFn,
250 ) -> Option<Self> {
251 Some(Self(Arc::new(ColorSpaceType::new(
252 object,
253 cache,
254 warning_sink,
255 )?)))
256 }
257
258 pub(crate) fn new_from_name(name: Name) -> Option<Self> {
260 ColorSpaceType::new_from_name(name).map(|c| Self(Arc::new(c)))
261 }
262
263 pub(crate) fn device_gray() -> Self {
265 Self(Arc::new(ColorSpaceType::DeviceGray))
266 }
267
268 pub(crate) fn device_rgb() -> Self {
270 Self(Arc::new(ColorSpaceType::DeviceRgb))
271 }
272
273 pub(crate) fn device_cmyk() -> Self {
275 Self(Arc::new(ColorSpaceType::DeviceCmyk))
276 }
277
278 pub fn is_device_rgb(&self) -> bool {
280 matches!(*self.0, ColorSpaceType::DeviceRgb)
281 }
282
283 pub fn is_device_cmyk(&self) -> bool {
285 matches!(*self.0, ColorSpaceType::DeviceCmyk)
286 }
287
288 pub(crate) fn pattern() -> Self {
290 Self(Arc::new(ColorSpaceType::Pattern(Self::device_gray())))
291 }
292
293 pub(crate) fn pattern_cs(&self) -> Option<Self> {
294 match self.0.as_ref() {
295 ColorSpaceType::Pattern(cs) => Some(cs.clone()),
296 _ => None,
297 }
298 }
299
300 pub(crate) fn is_pattern(&self) -> bool {
302 matches!(self.0.as_ref(), ColorSpaceType::Pattern(_))
303 }
304
305 pub(crate) fn is_indexed(&self) -> bool {
307 matches!(self.0.as_ref(), ColorSpaceType::Indexed(_))
308 }
309
310 pub(crate) fn default_decode_arr(&self, n: f32) -> SmallVec<[(f32, f32); 4]> {
312 match self.0.as_ref() {
313 ColorSpaceType::DeviceCmyk => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
314 ColorSpaceType::DeviceGray => smallvec![(0.0, 1.0)],
315 ColorSpaceType::DeviceRgb => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
316 ColorSpaceType::ICCBased(i) => smallvec![(0.0, 1.0); i.0.number_components],
317 ColorSpaceType::CalGray(_) => smallvec![(0.0, 1.0)],
318 ColorSpaceType::CalRgb(_) => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
319 ColorSpaceType::Lab(l) => smallvec![
320 (0.0, 100.0),
321 (l.range[0], l.range[1]),
322 (l.range[2], l.range[3]),
323 ],
324 ColorSpaceType::Indexed(_) => smallvec![(0.0, 2.0_f32.powf(n) - 1.0)],
325 ColorSpaceType::Separation(_) => smallvec![(0.0, 1.0)],
326 ColorSpaceType::DeviceN(d) => smallvec![(0.0, 1.0); d.num_components as usize],
327 ColorSpaceType::Pattern(_) => smallvec![(0.0, 1.0)],
329 }
330 }
331
332 pub(crate) fn initial_color(&self) -> ColorComponents {
334 match self.0.as_ref() {
335 ColorSpaceType::DeviceCmyk => smallvec![0.0, 0.0, 0.0, 1.0],
336 ColorSpaceType::DeviceGray => smallvec![0.0],
337 ColorSpaceType::DeviceRgb => smallvec![0.0, 0.0, 0.0],
338 ColorSpaceType::ICCBased(icc) => match icc.0.number_components {
339 1 => smallvec![0.0],
340 3 => smallvec![0.0, 0.0, 0.0],
341 4 => smallvec![0.0, 0.0, 0.0, 1.0],
342 _ => unreachable!(),
343 },
344 ColorSpaceType::CalGray(_) => smallvec![0.0],
345 ColorSpaceType::CalRgb(_) => smallvec![0.0, 0.0, 0.0],
346 ColorSpaceType::Lab(_) => smallvec![0.0, 0.0, 0.0],
347 ColorSpaceType::Indexed(_) => smallvec![0.0],
348 ColorSpaceType::Separation(_) => smallvec![1.0],
349 ColorSpaceType::Pattern(c) => c.initial_color(),
350 ColorSpaceType::DeviceN(d) => smallvec![1.0; d.num_components as usize],
351 }
352 }
353
354 pub(crate) fn num_components(&self) -> u8 {
356 match self.0.as_ref() {
357 ColorSpaceType::DeviceCmyk => 4,
358 ColorSpaceType::DeviceGray => 1,
359 ColorSpaceType::DeviceRgb => 3,
360 ColorSpaceType::ICCBased(icc) => icc.0.number_components as u8,
361 ColorSpaceType::CalGray(_) => 1,
362 ColorSpaceType::CalRgb(_) => 3,
363 ColorSpaceType::Lab(_) => 3,
364 ColorSpaceType::Indexed(_) => 1,
365 ColorSpaceType::Separation(_) => 1,
366 ColorSpaceType::Pattern(p) => p.num_components(),
367 ColorSpaceType::DeviceN(d) => d.num_components,
368 }
369 }
370
371 pub fn to_rgba(&self, c: &[f32], opacity: f32, manual_scale: bool) -> AlphaColor {
373 self.to_alpha_color(c, opacity, manual_scale)
374 .unwrap_or(AlphaColor::BLACK)
375 }
376}
377
378impl ToRgb for ColorSpace {
379 fn convert_f32(&self, input: &[f32], output: &mut [u8], manual_scale: bool) -> Option<()> {
380 match self.0.as_ref() {
381 ColorSpaceType::DeviceCmyk => {
382 if let Some(profile) = default_cmyk_profile() {
388 return profile.convert_f32(input, output, manual_scale);
389 }
390 for (input, output) in input.chunks_exact(4).zip(output.chunks_exact_mut(3)) {
392 let (c, m, y, k) = (input[0], input[1], input[2], input[3]);
393 output[0] = f32_to_u8(1.0 - (c + k).min(1.0));
394 output[1] = f32_to_u8(1.0 - (m + k).min(1.0));
395 output[2] = f32_to_u8(1.0 - (y + k).min(1.0));
396 }
397 Some(())
398 }
399 ColorSpaceType::DeviceGray => {
400 let converted = input.iter().copied().map(f32_to_u8).collect::<Vec<_>>();
401
402 for (input, output) in converted.iter().zip(output.chunks_exact_mut(3)) {
403 output.copy_from_slice(&[*input, *input, *input]);
404 }
405
406 Some(())
407 }
408 ColorSpaceType::DeviceRgb => {
409 for (input, output) in input.iter().copied().zip(output) {
410 *output = f32_to_u8(input);
411 }
412
413 Some(())
414 }
415 ColorSpaceType::Pattern(i) => i.convert_f32(input, output, manual_scale),
416 ColorSpaceType::Indexed(i) => i.convert_f32(input, output, manual_scale),
417 ColorSpaceType::ICCBased(i) => i.convert_f32(input, output, manual_scale),
418 ColorSpaceType::CalGray(i) => i.convert_f32(input, output, manual_scale),
419 ColorSpaceType::CalRgb(i) => i.convert_f32(input, output, manual_scale),
420 ColorSpaceType::Lab(i) => i.convert_f32(input, output, manual_scale),
421 ColorSpaceType::Separation(i) => i.convert_f32(input, output, manual_scale),
422 ColorSpaceType::DeviceN(i) => i.convert_f32(input, output, manual_scale),
423 }
424 }
425
426 fn supports_u8(&self) -> bool {
427 match self.0.as_ref() {
428 ColorSpaceType::DeviceCmyk => true,
429 ColorSpaceType::DeviceGray => true,
430 ColorSpaceType::DeviceRgb => true,
431 ColorSpaceType::Pattern(i) => i.supports_u8(),
432 ColorSpaceType::Indexed(i) => i.supports_u8(),
433 ColorSpaceType::ICCBased(i) => i.supports_u8(),
434 ColorSpaceType::CalGray(i) => i.supports_u8(),
435 ColorSpaceType::CalRgb(i) => i.supports_u8(),
436 ColorSpaceType::Lab(i) => i.supports_u8(),
437 ColorSpaceType::Separation(i) => i.supports_u8(),
438 ColorSpaceType::DeviceN(i) => i.supports_u8(),
439 }
440 }
441
442 fn convert_u8(&self, input: &[u8], output: &mut [u8]) -> Option<()> {
443 match self.0.as_ref() {
444 ColorSpaceType::DeviceCmyk => {
445 if let Some(profile) = default_cmyk_profile() {
447 return profile.convert_u8(input, output);
448 }
449 for (input, output) in input.chunks_exact(4).zip(output.chunks_exact_mut(3)) {
451 let (c, m, y, k) = (
452 input[0] as u16,
453 input[1] as u16,
454 input[2] as u16,
455 input[3] as u16,
456 );
457 output[0] = (255u16.saturating_sub(c + k)) as u8;
458 output[1] = (255u16.saturating_sub(m + k)) as u8;
459 output[2] = (255u16.saturating_sub(y + k)) as u8;
460 }
461 Some(())
462 }
463 ColorSpaceType::DeviceGray => {
464 for (input, output) in input.iter().zip(output.chunks_exact_mut(3)) {
465 output.copy_from_slice(&[*input, *input, *input]);
466 }
467
468 Some(())
469 }
470 ColorSpaceType::DeviceRgb => {
471 output.copy_from_slice(input);
472
473 Some(())
474 }
475 ColorSpaceType::Pattern(i) => i.convert_u8(input, output),
476 ColorSpaceType::Indexed(i) => i.convert_u8(input, output),
477 ColorSpaceType::ICCBased(i) => i.convert_u8(input, output),
478 ColorSpaceType::CalGray(i) => i.convert_u8(input, output),
479 ColorSpaceType::CalRgb(i) => i.convert_u8(input, output),
480 ColorSpaceType::Lab(i) => i.convert_u8(input, output),
481 ColorSpaceType::Separation(i) => i.convert_u8(input, output),
482 ColorSpaceType::DeviceN(i) => i.convert_u8(input, output),
483 }
484 }
485
486 fn is_none(&self) -> bool {
487 match self.0.as_ref() {
488 ColorSpaceType::Separation(s) => s.is_none(),
489 ColorSpaceType::DeviceN(d) => d.is_none(),
490 _ => false,
491 }
492 }
493}
494
495#[derive(Debug, Clone)]
496pub(crate) struct CalGray {
497 white_point: [f32; 3],
498 black_point: [f32; 3],
499 gamma: f32,
500}
501
502impl CalGray {
504 fn new(dict: &Dict<'_>) -> Option<Self> {
505 let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]);
506 let black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]);
507 let gamma = dict.get::<f32>(GAMMA).unwrap_or(1.0);
508
509 Some(Self {
510 white_point,
511 black_point,
512 gamma,
513 })
514 }
515}
516
517impl ToRgb for CalGray {
518 fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
519 for (input, output) in input.iter().copied().zip(output.chunks_exact_mut(3)) {
520 let g = self.gamma;
521 let (_xw, yw, _zw) = {
522 let wp = self.white_point;
523 (wp[0], wp[1], wp[2])
524 };
525 let (_xb, _yb, _zb) = {
526 let bp = self.black_point;
527 (bp[0], bp[1], bp[2])
528 };
529
530 let a = input;
531 let ag = a.powf(g);
532 let l = yw * ag;
533 let val = (0.0_f32.max(295.8 * l.powf(0.333_333_34) - 40.8) + 0.5) as u8;
534
535 output.copy_from_slice(&[val, val, val]);
536 }
537
538 Some(())
539 }
540}
541
542#[derive(Debug, Clone)]
543pub(crate) struct CalRgb {
544 white_point: [f32; 3],
545 black_point: [f32; 3],
546 matrix: [f32; 9],
547 gamma: [f32; 3],
548}
549
550impl CalRgb {
555 fn new(dict: &Dict<'_>) -> Option<Self> {
556 let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]);
557 let black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]);
558 let matrix = dict
559 .get::<[f32; 9]>(MATRIX)
560 .unwrap_or([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]);
561 let gamma = dict.get::<[f32; 3]>(GAMMA).unwrap_or([1.0, 1.0, 1.0]);
562
563 Some(Self {
564 white_point,
565 black_point,
566 matrix,
567 gamma,
568 })
569 }
570
571 const BRADFORD_SCALE_MATRIX: [f32; 9] = [
572 0.8951, 0.2664, -0.1614, -0.7502, 1.7135, 0.0367, 0.0389, -0.0685, 1.0296,
573 ];
574
575 const BRADFORD_SCALE_INVERSE_MATRIX: [f32; 9] = [
576 0.9869929, -0.1470543, 0.1599627, 0.4323053, 0.5183603, 0.0492912, -0.0085287, 0.0400428,
577 0.9684867,
578 ];
579
580 const SRGB_D65_XYZ_TO_RGB_MATRIX: [f32; 9] = [
581 3.2404542, -1.5371385, -0.4985314, -0.969_266, 1.8760108, 0.0415560, 0.0556434, -0.2040259,
582 1.0572252,
583 ];
584
585 const FLAT_WHITEPOINT: [f32; 3] = [1.0, 1.0, 1.0];
586 const D65_WHITEPOINT: [f32; 3] = [0.95047, 1.0, 1.08883];
587
588 fn decode_l_constant() -> f32 {
589 ((8.0_f32 + 16.0) / 116.0).powi(3) / 8.0
590 }
591
592 fn srgb_transfer_function(color: f32) -> f32 {
593 if color <= 0.0031308 {
594 (12.92 * color).clamp(0.0, 1.0)
595 } else if color >= 0.99554525 {
596 1.0
597 } else {
598 ((1.0 + 0.055) * color.powf(1.0 / 2.4) - 0.055).clamp(0.0, 1.0)
599 }
600 }
601
602 fn matrix_product(a: &[f32; 9], b: &[f32; 3]) -> [f32; 3] {
603 [
604 a[0] * b[0] + a[1] * b[1] + a[2] * b[2],
605 a[3] * b[0] + a[4] * b[1] + a[5] * b[2],
606 a[6] * b[0] + a[7] * b[1] + a[8] * b[2],
607 ]
608 }
609
610 fn to_flat(source_white_point: &[f32; 3], lms: &[f32; 3]) -> [f32; 3] {
611 [
612 lms[0] / source_white_point[0],
613 lms[1] / source_white_point[1],
614 lms[2] / source_white_point[2],
615 ]
616 }
617
618 fn to_d65(source_white_point: &[f32; 3], lms: &[f32; 3]) -> [f32; 3] {
619 [
620 lms[0] * Self::D65_WHITEPOINT[0] / source_white_point[0],
621 lms[1] * Self::D65_WHITEPOINT[1] / source_white_point[1],
622 lms[2] * Self::D65_WHITEPOINT[2] / source_white_point[2],
623 ]
624 }
625
626 fn decode_l(l: f32) -> f32 {
627 if l < 0.0 {
628 -Self::decode_l(-l)
629 } else if l > 8.0 {
630 ((l + 16.0) / 116.0).powi(3)
631 } else {
632 l * Self::decode_l_constant()
633 }
634 }
635
636 fn compensate_black_point(source_bp: &[f32; 3], xyz_flat: &[f32; 3]) -> [f32; 3] {
637 if source_bp == &[0.0, 0.0, 0.0] {
638 return *xyz_flat;
639 }
640
641 let zero_decode_l = Self::decode_l(0.0);
642
643 let mut out = [0.0; 3];
644 for i in 0..3 {
645 let src = Self::decode_l(source_bp[i]);
646 let scale = (1.0 - zero_decode_l) / (1.0 - src);
647 let offset = 1.0 - scale;
648 out[i] = xyz_flat[i] * scale + offset;
649 }
650
651 out
652 }
653
654 fn normalize_white_point_to_flat(
655 &self,
656 source_white_point: &[f32; 3],
657 xyz: &[f32; 3],
658 ) -> [f32; 3] {
659 if source_white_point[0] == 1.0 && source_white_point[2] == 1.0 {
660 return *xyz;
661 }
662 let lms = Self::matrix_product(&Self::BRADFORD_SCALE_MATRIX, xyz);
663 let lms_flat = Self::to_flat(source_white_point, &lms);
664 Self::matrix_product(&Self::BRADFORD_SCALE_INVERSE_MATRIX, &lms_flat)
665 }
666
667 fn normalize_white_point_to_d65(
668 &self,
669 source_white_point: &[f32; 3],
670 xyz: &[f32; 3],
671 ) -> [f32; 3] {
672 let lms = Self::matrix_product(&Self::BRADFORD_SCALE_MATRIX, xyz);
673 let lms_d65 = Self::to_d65(source_white_point, &lms);
674 Self::matrix_product(&Self::BRADFORD_SCALE_INVERSE_MATRIX, &lms_d65)
675 }
676}
677
678impl ToRgb for CalRgb {
679 fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
680 for (input, output) in input.chunks_exact(3).zip(output.chunks_exact_mut(3)) {
681 let input = [
682 input[0].clamp(0.0, 1.0),
683 input[1].clamp(0.0, 1.0),
684 input[2].clamp(0.0, 1.0),
685 ];
686
687 let [r, g, b] = input;
688 let [gr, gg, gb] = self.gamma;
689 let [agr, bgg, cgb] = [
690 if r == 1.0 { 1.0 } else { r.powf(gr) },
691 if g == 1.0 { 1.0 } else { g.powf(gg) },
692 if b == 1.0 { 1.0 } else { b.powf(gb) },
693 ];
694
695 let m = &self.matrix;
696 let x = m[0] * agr + m[3] * bgg + m[6] * cgb;
697 let y = m[1] * agr + m[4] * bgg + m[7] * cgb;
698 let z = m[2] * agr + m[5] * bgg + m[8] * cgb;
699 let xyz = [x, y, z];
700
701 let xyz_flat = self.normalize_white_point_to_flat(&self.white_point, &xyz);
702 let xyz_black = Self::compensate_black_point(&self.black_point, &xyz_flat);
703 let xyz_d65 = self.normalize_white_point_to_d65(&Self::FLAT_WHITEPOINT, &xyz_black);
704 let srgb_xyz = Self::matrix_product(&Self::SRGB_D65_XYZ_TO_RGB_MATRIX, &xyz_d65);
705
706 output.copy_from_slice(&[
707 (Self::srgb_transfer_function(srgb_xyz[0]) * 255.0 + 0.5) as u8,
708 (Self::srgb_transfer_function(srgb_xyz[1]) * 255.0 + 0.5) as u8,
709 (Self::srgb_transfer_function(srgb_xyz[2]) * 255.0 + 0.5) as u8,
710 ]);
711 }
712
713 Some(())
714 }
715}
716
717#[derive(Debug, Clone)]
718pub(crate) struct Lab {
719 range: [f32; 4],
720 profile: ICCProfile,
721}
722
723impl Lab {
724 fn new(dict: &Dict<'_>) -> Option<Self> {
725 let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]);
726 let _black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]);
728 let range = dict
729 .get::<[f32; 4]>(RANGE)
730 .unwrap_or([-100.0, 100.0, -100.0, 100.0]);
731
732 let mut profile = ColorProfile::new_from_slice(include_bytes!("../assets/LAB.icc")).ok()?;
733 profile.white_point = Xyzd::new(
734 white_point[0] as f64,
735 white_point[1] as f64,
736 white_point[2] as f64,
737 );
738
739 let profile = ICCProfile::new_from_src_profile(
740 profile, false,
741 false, 3,
745 )?;
746
747 Some(Self { range, profile })
748 }
749}
750
751impl ToRgb for Lab {
752 fn convert_f32(&self, input: &[f32], output: &mut [u8], manual_scale: bool) -> Option<()> {
753 if !manual_scale {
754 let input = input
758 .chunks_exact(3)
759 .flat_map(|i| {
760 let l = i[0] / 100.0;
761 let a = (i[1] + 128.0) / 255.0;
762 let b = (i[2] + 128.0) / 255.0;
763
764 [l, a, b]
765 })
766 .collect::<Vec<_>>();
767
768 self.profile.convert_f32(&input, output, manual_scale)
769 } else {
770 self.profile.convert_f32(input, output, manual_scale)
771 }
772 }
773}
774
775#[derive(Debug, Clone)]
776pub(crate) struct Indexed {
777 values: Vec<Vec<f32>>,
778 hival: u8,
779 base: Box<ColorSpace>,
780}
781
782impl Indexed {
783 fn new(array: &Array<'_>, cache: &Cache, warning_sink: &WarningSinkFn) -> Option<Self> {
784 let mut iter = array.flex_iter();
785 let _ = iter.next::<Name>()?;
787 let base_color_space = ColorSpace::new(iter.next::<Object<'_>>()?, cache, warning_sink)?;
788 let hival = iter.next::<u32>()?.min(u8::MAX as u32) as u8;
794
795 let values = {
796 let data = iter
797 .next::<Stream<'_>>()
798 .and_then(|s| decode_or_warn(&s, warning_sink))
799 .or_else(|| iter.next::<object::String>().map(|s| s.to_vec()))?;
800
801 let num_components = base_color_space.num_components();
802
803 let mut byte_iter = data.iter().copied();
804
805 let mut vals = vec![];
806 for _ in 0..=hival {
807 let mut temp = vec![];
808
809 for _ in 0..num_components {
810 temp.push(byte_iter.next()? as f32 / 255.0);
811 }
812
813 vals.push(temp);
814 }
815
816 vals
817 };
818
819 Some(Self {
820 values,
821 hival,
822 base: Box::new(base_color_space),
823 })
824 }
825}
826
827impl ToRgb for Indexed {
828 fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
829 let mut indexed = vec![0.0; input.len() * self.base.num_components() as usize];
830
831 for (input, output) in input
832 .iter()
833 .copied()
834 .zip(indexed.chunks_exact_mut(self.base.num_components() as usize))
835 {
836 let idx = (input.clamp(0.0, self.hival as f32) + 0.5) as usize;
837 output.copy_from_slice(&self.values[idx]);
838 }
839
840 self.base.convert_f32(&indexed, output, true)
841 }
842}
843
844#[derive(Debug, Clone)]
845pub(crate) struct Separation {
846 alternate_space: ColorSpace,
847 tint_transform: Function,
848 is_none_separation: bool,
849}
850
851impl Separation {
852 fn new(array: &Array<'_>, cache: &Cache, warning_sink: &WarningSinkFn) -> Option<Self> {
853 let mut iter = array.flex_iter();
854 let _ = iter.next::<Name>()?;
856 let name = iter.next::<Name>()?;
857 let alternate_space = ColorSpace::new(iter.next::<Object<'_>>()?, cache, warning_sink)?;
858 let tint_transform = Function::new(&iter.next::<Object<'_>>()?)?;
859 let is_none_separation = name.as_str() == "None";
866
867 Some(Self {
868 alternate_space,
869 tint_transform,
870 is_none_separation,
871 })
872 }
873}
874
875impl ToRgb for Separation {
876 fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
877 let evaluated = input
878 .iter()
879 .flat_map(|n| {
880 self.tint_transform
881 .eval(smallvec![*n])
882 .unwrap_or(self.alternate_space.initial_color())
883 })
884 .collect::<Vec<_>>();
885 self.alternate_space.convert_f32(&evaluated, output, false)
886 }
887
888 fn is_none(&self) -> bool {
889 self.is_none_separation
890 }
891}
892
893#[derive(Debug, Clone)]
894pub(crate) struct DeviceN {
895 alternate_space: ColorSpace,
896 num_components: u8,
897 tint_transform: Function,
898 is_none: bool,
899}
900
901impl DeviceN {
902 fn new(array: &Array<'_>, cache: &Cache, warning_sink: &WarningSinkFn) -> Option<Self> {
903 let mut iter = array.flex_iter();
904 let _ = iter.next::<Name>()?;
906 let names = iter.next::<Array<'_>>()?.iter::<Name>().collect::<Vec<_>>();
907 let num_components = u8::try_from(names.len()).ok()?;
908 let all_none = names.iter().all(|n| n.as_str() == "None");
914 let alternate_space = ColorSpace::new(iter.next::<Object<'_>>()?, cache, warning_sink)?;
915 let tint_transform = Function::new(&iter.next::<Object<'_>>()?)?;
916
917 if num_components == 0 {
918 return None;
919 }
920
921 Some(Self {
922 alternate_space,
923 num_components,
924 tint_transform,
925 is_none: all_none,
926 })
927 }
928}
929
930impl ToRgb for DeviceN {
931 fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
932 let evaluated = input
933 .chunks_exact(self.num_components as usize)
934 .flat_map(|n| {
935 self.tint_transform
936 .eval(n.to_smallvec())
937 .unwrap_or(self.alternate_space.initial_color())
938 })
939 .collect::<Vec<_>>();
940 self.alternate_space.convert_f32(&evaluated, output, false)
941 }
942
943 fn is_none(&self) -> bool {
944 self.is_none
945 }
946}
947
948struct ICCColorRepr {
949 transform_u8: Box<Transform8BitExecutor>,
950 transform_f32: Box<TransformF32BitExecutor>,
951 number_components: usize,
952 is_srgb: bool,
953 is_lab: bool,
954}
955
956#[derive(Clone)]
957pub(crate) struct ICCProfile(Arc<ICCColorRepr>);
958
959impl Debug for ICCProfile {
960 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
961 write!(f, "ICCColor {{..}}")
962 }
963}
964
965impl ICCProfile {
966 fn new(profile: &[u8], number_components: usize) -> Option<Self> {
967 let src_profile = ColorProfile::new_from_slice(profile).ok()?;
968
969 let profile_components = match src_profile.color_space {
977 DataColorSpace::Gray => 1,
978 DataColorSpace::Rgb
979 | DataColorSpace::Lab
980 | DataColorSpace::Luv
981 | DataColorSpace::Xyz
982 | DataColorSpace::YCbr
983 | DataColorSpace::Yxy
984 | DataColorSpace::Hsv
985 | DataColorSpace::Hls
986 | DataColorSpace::Cmy
987 | DataColorSpace::Color3 => 3,
988 DataColorSpace::Cmyk | DataColorSpace::Color4 => 4,
989 _ => {
990 warn!(
991 "unsupported ICC profile color space {:?}",
992 src_profile.color_space
993 );
994 return None;
995 }
996 };
997
998 if number_components != profile_components {
999 warn!(
1000 "ICCBased /N={} does not match embedded ICC profile component count {}; using profile",
1001 number_components, profile_components
1002 );
1003 }
1004
1005 const SRGB_MARKER: &[u8] = b"sRGB";
1006
1007 let is_srgb = profile
1008 .get(52..56)
1009 .map(|device_model| device_model == SRGB_MARKER)
1010 .unwrap_or(false);
1011 let is_lab = src_profile.color_space == DataColorSpace::Lab;
1012
1013 Self::new_from_src_profile(src_profile, is_srgb, is_lab, profile_components)
1014 }
1015
1016 fn new_from_src_profile(
1017 src_profile: ColorProfile,
1018 is_srgb: bool,
1019 is_lab: bool,
1020 number_components: usize,
1021 ) -> Option<Self> {
1022 let dest_profile = ColorProfile::new_srgb();
1023
1024 let src_layout = match number_components {
1032 1 => Layout::Gray,
1033 3 => Layout::Rgb,
1034 4 => Layout::Rgba, _ => {
1036 warn!("unsupported number of components {number_components} for ICC profile");
1037
1038 return None;
1039 }
1040 };
1041
1042 let intents_to_try = [
1059 RenderingIntent::RelativeColorimetric,
1060 RenderingIntent::Perceptual,
1061 RenderingIntent::Saturation,
1062 ];
1063 let mut u8_transform = None;
1064 let mut f32_transform = None;
1065 for intent in intents_to_try {
1066 let options = TransformOptions {
1067 rendering_intent: intent,
1068 prefer_fixed_point: false,
1086 ..TransformOptions::default()
1087 };
1088 let u8_ok = src_profile
1089 .create_transform_8bit(src_layout, &dest_profile, Layout::Rgb, options)
1090 .ok();
1091 let f32_ok = src_profile
1092 .create_transform_f32(src_layout, &dest_profile, Layout::Rgb, options)
1093 .ok();
1094 if let (Some(u), Some(f)) = (u8_ok, f32_ok) {
1095 u8_transform = Some(u);
1096 f32_transform = Some(f);
1097 break;
1098 }
1099 }
1100 let u8_transform = u8_transform?;
1101 let f32_transform = f32_transform?;
1102
1103 Some(Self(Arc::new(ICCColorRepr {
1104 transform_u8: u8_transform,
1105 transform_f32: f32_transform,
1106 number_components,
1107 is_srgb,
1108 is_lab,
1109 })))
1110 }
1111
1112 fn is_srgb(&self) -> bool {
1113 self.0.is_srgb
1114 }
1115
1116 fn is_lab(&self) -> bool {
1117 self.0.is_lab
1118 }
1119}
1120
1121impl ToRgb for ICCProfile {
1122 fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
1123 let mut temp = vec![0.0_f32; output.len()];
1124
1125 if self.is_lab() {
1126 let scaled = input
1128 .chunks_exact(3)
1129 .flat_map(|i| {
1130 [
1131 i[0] * (1.0 / 100.0),
1132 (i[1] + 128.0) * (1.0 / 255.0),
1133 (i[2] + 128.0) * (1.0 / 255.0),
1134 ]
1135 })
1136 .collect::<Vec<_>>();
1137 self.0.transform_f32.transform(&scaled, &mut temp).ok()?;
1138 } else {
1139 self.0.transform_f32.transform(input, &mut temp).ok()?;
1140 };
1141
1142 for (input, output) in temp.iter().zip(output.iter_mut()) {
1143 *output = (input * 255.0 + 0.5) as u8;
1144 }
1145
1146 Some(())
1147 }
1148
1149 fn supports_u8(&self) -> bool {
1150 true
1151 }
1152
1153 fn convert_u8(&self, input: &[u8], output: &mut [u8]) -> Option<()> {
1154 if self.is_srgb() && input.len() == output.len() {
1161 output.copy_from_slice(input);
1162 } else {
1163 self.0.transform_u8.transform(input, output).ok()?;
1164 }
1165
1166 Some(())
1167 }
1168}
1169
1170#[inline(always)]
1171fn f32_to_u8(val: f32) -> u8 {
1172 (val * 255.0 + 0.5) as u8
1173}
1174
1175#[derive(Debug, Clone)]
1176pub struct Color {
1178 color_space: ColorSpace,
1179 components: ColorComponents,
1180 opacity: f32,
1181}
1182
1183impl Color {
1184 pub(crate) fn new(color_space: ColorSpace, components: ColorComponents, opacity: f32) -> Self {
1185 Self {
1186 color_space,
1187 components,
1188 opacity,
1189 }
1190 }
1191
1192 pub fn to_rgba(&self) -> AlphaColor {
1194 self.color_space
1195 .to_rgba(&self.components, self.opacity, false)
1196 }
1197
1198 pub fn from_rgba(rgba: AlphaColor) -> Self {
1200 let c = rgba.components();
1201 Self {
1202 color_space: ColorSpace::device_rgb(),
1203 components: smallvec![c[0], c[1], c[2]],
1204 opacity: c[3],
1205 }
1206 }
1207
1208 pub fn from_device_rgb(r: f32, g: f32, b: f32) -> Self {
1210 Self {
1211 color_space: ColorSpace::device_rgb(),
1212 components: smallvec![r, g, b],
1213 opacity: 1.0,
1214 }
1215 }
1216
1217 pub fn from_device_rgb_with_opacity(r: f32, g: f32, b: f32, opacity: f32) -> Self {
1219 Self {
1220 color_space: ColorSpace::device_rgb(),
1221 components: smallvec![r, g, b],
1222 opacity,
1223 }
1224 }
1225
1226 pub fn opacity(&self) -> f32 {
1228 self.opacity
1229 }
1230
1231 pub fn is_device_cmyk(&self) -> bool {
1233 self.color_space.is_device_cmyk()
1234 }
1235
1236 pub fn device_cmyk_components(&self) -> Option<[f32; 4]> {
1238 if !self.color_space.is_device_cmyk() || self.components.len() != 4 {
1239 return None;
1240 }
1241
1242 Some([
1243 self.components[0],
1244 self.components[1],
1245 self.components[2],
1246 self.components[3],
1247 ])
1248 }
1249}
1250
1251pub(crate) trait ToRgb {
1252 fn convert_f32(&self, input: &[f32], output: &mut [u8], manual_scale: bool) -> Option<()>;
1253 fn supports_u8(&self) -> bool {
1254 false
1255 }
1256 fn convert_u8(&self, _: &[u8], _: &mut [u8]) -> Option<()> {
1257 unimplemented!();
1258 }
1259 fn is_none(&self) -> bool {
1260 false
1261 }
1262 fn to_alpha_color(
1263 &self,
1264 input: &[f32],
1265 mut opacity: f32,
1266 manual_scale: bool,
1267 ) -> Option<AlphaColor> {
1268 let mut output = [0; 3];
1269 self.convert_f32(input, &mut output, manual_scale)?;
1270
1271 if self.is_none() {
1276 opacity = 0.0;
1277 }
1278
1279 Some(AlphaColor::from_rgba8(
1280 output[0],
1281 output[1],
1282 output[2],
1283 (opacity * 255.0 + 0.5) as u8,
1284 ))
1285 }
1286}
1287
1288#[cfg(test)]
1289mod tests {
1290 use super::*;
1291 use pdf_syntax::object::{Array, FromBytes};
1292
1293 fn separation_array(ink_name: &str) -> Vec<u8> {
1297 format!(
1298 "[/Separation /{ink_name} /DeviceGray \
1299 << /FunctionType 2 /Domain [0 1] /C0 [1] /C1 [0] /N 1 >> ]",
1300 ink_name = ink_name
1301 )
1302 .into_bytes()
1303 }
1304
1305 fn make_separation(ink_name: &str) -> Option<Separation> {
1306 let bytes = separation_array(ink_name);
1307 let array = Array::from_bytes(&bytes)?;
1308 let cache = Cache::new();
1309 {
1310 let sink: WarningSinkFn = std::sync::Arc::new(|_: crate::InterpreterWarning| {});
1311 Separation::new(&array, &cache, &sink)
1312 }
1313 }
1314
1315 #[test]
1317 fn none_ink_is_suppressed() {
1318 let sep = make_separation("None").expect("should parse");
1319 assert!(sep.is_none(), "ink name 'None' must be suppressed");
1320 }
1321
1322 #[test]
1324 fn pantone_ink_is_not_suppressed() {
1325 let sep = make_separation("PANTONE#20123#20CVC").expect("should parse");
1327 assert!(!sep.is_none(), "PANTONE spot colour must not be suppressed");
1328 }
1329
1330 #[test]
1332 fn all_ink_is_not_suppressed() {
1333 let sep = make_separation("All").expect("should parse");
1334 assert!(!sep.is_none(), "'All' separation must not be suppressed");
1335 }
1336
1337 #[test]
1339 fn pantone_produces_visible_color() {
1340 let sep = make_separation("PANTONE#20123#20CVC").expect("should parse");
1341 let cs = ColorSpace(Arc::new(ColorSpaceType::Separation(sep)));
1342 let color = cs.to_rgba(&[1.0], 1.0, false);
1344 assert!(
1345 color.to_rgba8()[3] > 0,
1346 "PANTONE ink at tint=1.0 must be opaque"
1347 );
1348 }
1349
1350 #[test]
1352 fn none_produces_transparent_color() {
1353 let sep = make_separation("None").expect("should parse");
1354 let cs = ColorSpace(Arc::new(ColorSpaceType::Separation(sep)));
1355 let color = cs.to_rgba(&[1.0], 1.0, false);
1356 assert_eq!(
1357 color.to_rgba8()[3],
1358 0,
1359 "Separation/None ink must be fully transparent"
1360 );
1361 }
1362}
1363
1364#[cfg(test)]
1365mod indexed_hival {
1366 use super::*;
1367 use pdf_syntax::object::FromBytes;
1368 use pdf_syntax::object::array::Array;
1369
1370 fn colour_space(hival: u32, palette_len: usize) -> Option<ColorSpace> {
1376 let hex: String = "00".repeat(palette_len);
1377 let src = format!("[/Indexed /DeviceGray {hival} <{hex}>]");
1378 let array = Array::from_bytes(src.as_bytes())?;
1379 ColorSpace::new(
1380 pdf_syntax::object::Object::Array(array),
1381 &Cache::default(),
1382 &(std::sync::Arc::new(|_: crate::InterpreterWarning| {}) as WarningSinkFn),
1383 )
1384 }
1385
1386 #[test]
1400 fn the_clamp_lands_on_255_and_not_merely_somewhere() {
1401 let mut hex = "00".repeat(255);
1402 hex.push_str("ff");
1403 let src = format!("[/Indexed /DeviceGray 300 <{hex}>]");
1404 let array = Array::from_bytes(src.as_bytes()).expect("a well-formed array");
1405 let space = ColorSpace::new(
1406 pdf_syntax::object::Object::Array(array),
1407 &Cache::default(),
1408 &(std::sync::Arc::new(|_: crate::InterpreterWarning| {}) as WarningSinkFn),
1409 )
1410 .expect("the space is built; that is the other test");
1411
1412 let top = space.to_rgba(&[255.0], 1.0, false).to_rgba8();
1413 assert_eq!(
1414 [top[0], top[1], top[2]],
1415 [255, 255, 255],
1416 "index 255 did not resolve to the last palette entry, so /HiVal was \
1417 clamped below 255 and the top of the palette is unreachable"
1418 );
1419 }
1420
1421 #[test]
1428 fn an_out_of_range_hival_clamps_instead_of_dropping_the_space() {
1429 assert!(
1430 colour_space(300, 256).is_some(),
1431 "an out-of-range /HiVal discarded the indexed colour space, so the \
1432 image it belongs to renders as nothing"
1433 );
1434 }
1435
1436 #[test]
1439 fn an_ordinary_hival_still_builds_the_space() {
1440 assert!(colour_space(1, 2).is_some());
1441 }
1442}