use crate::cache::{Cache, CacheKey};
use crate::function::Function;
use log::warn;
use moxcms::{
ColorProfile, DataColorSpace, Layout, RenderingIntent, Transform8BitExecutor,
TransformF32BitExecutor, TransformOptions, Xyzd,
};
use pdf_syntax::object;
use pdf_syntax::object::Array;
use pdf_syntax::object::Dict;
use pdf_syntax::object::Name;
use pdf_syntax::object::Object;
use pdf_syntax::object::Stream;
use pdf_syntax::object::dict::keys::*;
use smallvec::{SmallVec, ToSmallVec, smallvec};
use std::fmt::{Debug, Formatter};
use std::ops::Deref;
use std::sync::{Arc, OnceLock};
static DEFAULT_CMYK_PROFILE: OnceLock<Option<ICCProfile>> = OnceLock::new();
fn default_cmyk_profile() -> Option<&'static ICCProfile> {
DEFAULT_CMYK_PROFILE
.get_or_init(|| ICCProfile::new(include_bytes!("../assets/CGATS001Compat-v2-micro.icc"), 4))
.as_ref()
}
pub type ColorComponents = SmallVec<[f32; 4]>;
#[derive(Debug, Copy, Clone)]
pub struct AlphaColor {
components: [f32; 4],
}
impl AlphaColor {
pub const BLACK: Self = Self::new([0., 0., 0., 1.]);
pub const TRANSPARENT: Self = Self::new([0., 0., 0., 0.]);
pub const WHITE: Self = Self::new([1., 1., 1., 1.]);
pub const fn new(components: [f32; 4]) -> Self {
Self { components }
}
pub const fn from_rgb8(r: u8, g: u8, b: u8) -> Self {
let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), 1.];
Self::new(components)
}
pub fn premultiplied(&self) -> [f32; 4] {
[
self.components[0] * self.components[3],
self.components[1] * self.components[3],
self.components[2] * self.components[3],
self.components[3],
]
}
pub const fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), u8_to_f32(a)];
Self::new(components)
}
pub fn to_rgba8(&self) -> [u8; 4] {
[
(self.components[0] * 255.0 + 0.5) as u8,
(self.components[1] * 255.0 + 0.5) as u8,
(self.components[2] * 255.0 + 0.5) as u8,
(self.components[3] * 255.0 + 0.5) as u8,
]
}
pub fn components(&self) -> [f32; 4] {
self.components
}
}
const fn u8_to_f32(x: u8) -> f32 {
x as f32 * (1.0 / 255.0)
}
#[derive(Debug, Clone)]
pub(crate) enum ColorSpaceType {
DeviceCmyk,
DeviceGray,
DeviceRgb,
Pattern(ColorSpace),
Indexed(Indexed),
ICCBased(ICCProfile),
CalGray(CalGray),
CalRgb(CalRgb),
Lab(Lab),
Separation(Separation),
DeviceN(DeviceN),
}
impl ColorSpaceType {
fn new(object: Object<'_>, cache: &Cache) -> Option<Self> {
Self::new_inner(object, cache)
}
fn new_inner(object: Object<'_>, cache: &Cache) -> Option<Self> {
if let Some(name) = object.clone().into_name() {
return Self::new_from_name(name.clone());
} else if let Some(color_array) = object.clone().into_array() {
let mut iter = color_array.clone().flex_iter();
let name = iter.next::<Name>()?;
match name.deref() {
ICC_BASED => {
let icc_stream = iter.next::<Stream<'_>>()?;
let dict = icc_stream.dict();
let num_components = dict.get::<usize>(N);
return cache.get_or_insert_with(icc_stream.cache_key(), || {
let from_icc = num_components.and_then(|n| {
icc_stream.decoded().ok().as_ref().and_then(|decoded| {
ICCProfile::new(decoded, n).map(|icc| {
if icc.is_srgb() {
Self::DeviceRgb
} else {
Self::ICCBased(icc)
}
})
})
});
from_icc
.or_else(|| {
dict.get::<Object<'_>>(ALTERNATE)
.and_then(|o| Self::new(o, cache))
})
.or_else(|| match num_components {
Some(1) => Some(Self::DeviceGray),
Some(3) => Some(Self::DeviceRgb),
Some(4) => Some(Self::DeviceCmyk),
_ => None,
})
});
}
CALCMYK => return Some(Self::DeviceCmyk),
CALGRAY => {
let cal_dict = iter.next::<Dict<'_>>()?;
return Some(Self::CalGray(CalGray::new(&cal_dict)?));
}
CALRGB => {
let cal_dict = iter.next::<Dict<'_>>()?;
return Some(Self::CalRgb(CalRgb::new(&cal_dict)?));
}
DEVICE_RGB | RGB => return Some(Self::DeviceRgb),
DEVICE_GRAY | G => return Some(Self::DeviceGray),
DEVICE_CMYK | CMYK => return Some(Self::DeviceCmyk),
LAB => {
let lab_dict = iter.next::<Dict<'_>>()?;
return Some(Self::Lab(Lab::new(&lab_dict)?));
}
INDEXED | I => {
return Some(Self::Indexed(Indexed::new(&color_array, cache)?));
}
SEPARATION => {
return Some(Self::Separation(Separation::new(&color_array, cache)?));
}
DEVICE_N => {
return Some(Self::DeviceN(DeviceN::new(&color_array, cache)?));
}
PATTERN => {
let cs = iter
.next::<Object<'_>>()
.and_then(|o| ColorSpace::new(o, cache))
.unwrap_or(ColorSpace::device_rgb());
return Some(Self::Pattern(cs));
}
_ => {
warn!("unsupported color space: {}", name.as_str());
return None;
}
}
}
None
}
fn new_from_name(name: Name) -> Option<Self> {
match name.deref() {
DEVICE_RGB | RGB => Some(Self::DeviceRgb),
DEVICE_GRAY | G => Some(Self::DeviceGray),
DEVICE_CMYK | CMYK => Some(Self::DeviceCmyk),
CALCMYK => Some(Self::DeviceCmyk),
PATTERN => Some(Self::Pattern(ColorSpace::device_rgb())),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct ColorSpace(Arc<ColorSpaceType>);
impl ColorSpace {
pub(crate) fn new(object: Object<'_>, cache: &Cache) -> Option<Self> {
Some(Self(Arc::new(ColorSpaceType::new(object, cache)?)))
}
pub(crate) fn new_from_name(name: Name) -> Option<Self> {
ColorSpaceType::new_from_name(name).map(|c| Self(Arc::new(c)))
}
pub(crate) fn device_gray() -> Self {
Self(Arc::new(ColorSpaceType::DeviceGray))
}
pub(crate) fn device_rgb() -> Self {
Self(Arc::new(ColorSpaceType::DeviceRgb))
}
pub(crate) fn device_cmyk() -> Self {
Self(Arc::new(ColorSpaceType::DeviceCmyk))
}
pub fn is_device_rgb(&self) -> bool {
matches!(*self.0, ColorSpaceType::DeviceRgb)
}
pub fn is_device_cmyk(&self) -> bool {
matches!(*self.0, ColorSpaceType::DeviceCmyk)
}
pub(crate) fn pattern() -> Self {
Self(Arc::new(ColorSpaceType::Pattern(Self::device_gray())))
}
pub(crate) fn pattern_cs(&self) -> Option<Self> {
match self.0.as_ref() {
ColorSpaceType::Pattern(cs) => Some(cs.clone()),
_ => None,
}
}
pub(crate) fn is_pattern(&self) -> bool {
matches!(self.0.as_ref(), ColorSpaceType::Pattern(_))
}
pub(crate) fn is_indexed(&self) -> bool {
matches!(self.0.as_ref(), ColorSpaceType::Indexed(_))
}
pub(crate) fn default_decode_arr(&self, n: f32) -> SmallVec<[(f32, f32); 4]> {
match self.0.as_ref() {
ColorSpaceType::DeviceCmyk => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
ColorSpaceType::DeviceGray => smallvec![(0.0, 1.0)],
ColorSpaceType::DeviceRgb => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
ColorSpaceType::ICCBased(i) => smallvec![(0.0, 1.0); i.0.number_components],
ColorSpaceType::CalGray(_) => smallvec![(0.0, 1.0)],
ColorSpaceType::CalRgb(_) => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
ColorSpaceType::Lab(l) => smallvec![
(0.0, 100.0),
(l.range[0], l.range[1]),
(l.range[2], l.range[3]),
],
ColorSpaceType::Indexed(_) => smallvec![(0.0, 2.0_f32.powf(n) - 1.0)],
ColorSpaceType::Separation(_) => smallvec![(0.0, 1.0)],
ColorSpaceType::DeviceN(d) => smallvec![(0.0, 1.0); d.num_components as usize],
ColorSpaceType::Pattern(_) => smallvec![(0.0, 1.0)],
}
}
pub(crate) fn initial_color(&self) -> ColorComponents {
match self.0.as_ref() {
ColorSpaceType::DeviceCmyk => smallvec![0.0, 0.0, 0.0, 1.0],
ColorSpaceType::DeviceGray => smallvec![0.0],
ColorSpaceType::DeviceRgb => smallvec![0.0, 0.0, 0.0],
ColorSpaceType::ICCBased(icc) => match icc.0.number_components {
1 => smallvec![0.0],
3 => smallvec![0.0, 0.0, 0.0],
4 => smallvec![0.0, 0.0, 0.0, 1.0],
_ => unreachable!(),
},
ColorSpaceType::CalGray(_) => smallvec![0.0],
ColorSpaceType::CalRgb(_) => smallvec![0.0, 0.0, 0.0],
ColorSpaceType::Lab(_) => smallvec![0.0, 0.0, 0.0],
ColorSpaceType::Indexed(_) => smallvec![0.0],
ColorSpaceType::Separation(_) => smallvec![1.0],
ColorSpaceType::Pattern(c) => c.initial_color(),
ColorSpaceType::DeviceN(d) => smallvec![1.0; d.num_components as usize],
}
}
pub(crate) fn num_components(&self) -> u8 {
match self.0.as_ref() {
ColorSpaceType::DeviceCmyk => 4,
ColorSpaceType::DeviceGray => 1,
ColorSpaceType::DeviceRgb => 3,
ColorSpaceType::ICCBased(icc) => icc.0.number_components as u8,
ColorSpaceType::CalGray(_) => 1,
ColorSpaceType::CalRgb(_) => 3,
ColorSpaceType::Lab(_) => 3,
ColorSpaceType::Indexed(_) => 1,
ColorSpaceType::Separation(_) => 1,
ColorSpaceType::Pattern(p) => p.num_components(),
ColorSpaceType::DeviceN(d) => d.num_components,
}
}
pub fn to_rgba(&self, c: &[f32], opacity: f32, manual_scale: bool) -> AlphaColor {
self.to_alpha_color(c, opacity, manual_scale)
.unwrap_or(AlphaColor::BLACK)
}
}
impl ToRgb for ColorSpace {
fn convert_f32(&self, input: &[f32], output: &mut [u8], manual_scale: bool) -> Option<()> {
match self.0.as_ref() {
ColorSpaceType::DeviceCmyk => {
if let Some(profile) = default_cmyk_profile() {
return profile.convert_f32(input, output, manual_scale);
}
for (input, output) in input.chunks_exact(4).zip(output.chunks_exact_mut(3)) {
let (c, m, y, k) = (input[0], input[1], input[2], input[3]);
output[0] = f32_to_u8(1.0 - (c + k).min(1.0));
output[1] = f32_to_u8(1.0 - (m + k).min(1.0));
output[2] = f32_to_u8(1.0 - (y + k).min(1.0));
}
Some(())
}
ColorSpaceType::DeviceGray => {
let converted = input.iter().copied().map(f32_to_u8).collect::<Vec<_>>();
for (input, output) in converted.iter().zip(output.chunks_exact_mut(3)) {
output.copy_from_slice(&[*input, *input, *input]);
}
Some(())
}
ColorSpaceType::DeviceRgb => {
for (input, output) in input.iter().copied().zip(output) {
*output = f32_to_u8(input);
}
Some(())
}
ColorSpaceType::Pattern(i) => i.convert_f32(input, output, manual_scale),
ColorSpaceType::Indexed(i) => i.convert_f32(input, output, manual_scale),
ColorSpaceType::ICCBased(i) => i.convert_f32(input, output, manual_scale),
ColorSpaceType::CalGray(i) => i.convert_f32(input, output, manual_scale),
ColorSpaceType::CalRgb(i) => i.convert_f32(input, output, manual_scale),
ColorSpaceType::Lab(i) => i.convert_f32(input, output, manual_scale),
ColorSpaceType::Separation(i) => i.convert_f32(input, output, manual_scale),
ColorSpaceType::DeviceN(i) => i.convert_f32(input, output, manual_scale),
}
}
fn supports_u8(&self) -> bool {
match self.0.as_ref() {
ColorSpaceType::DeviceCmyk => true,
ColorSpaceType::DeviceGray => true,
ColorSpaceType::DeviceRgb => true,
ColorSpaceType::Pattern(i) => i.supports_u8(),
ColorSpaceType::Indexed(i) => i.supports_u8(),
ColorSpaceType::ICCBased(i) => i.supports_u8(),
ColorSpaceType::CalGray(i) => i.supports_u8(),
ColorSpaceType::CalRgb(i) => i.supports_u8(),
ColorSpaceType::Lab(i) => i.supports_u8(),
ColorSpaceType::Separation(i) => i.supports_u8(),
ColorSpaceType::DeviceN(i) => i.supports_u8(),
}
}
fn convert_u8(&self, input: &[u8], output: &mut [u8]) -> Option<()> {
match self.0.as_ref() {
ColorSpaceType::DeviceCmyk => {
if let Some(profile) = default_cmyk_profile() {
return profile.convert_u8(input, output);
}
for (input, output) in input.chunks_exact(4).zip(output.chunks_exact_mut(3)) {
let (c, m, y, k) = (
input[0] as u16,
input[1] as u16,
input[2] as u16,
input[3] as u16,
);
output[0] = (255u16.saturating_sub(c + k)) as u8;
output[1] = (255u16.saturating_sub(m + k)) as u8;
output[2] = (255u16.saturating_sub(y + k)) as u8;
}
Some(())
}
ColorSpaceType::DeviceGray => {
for (input, output) in input.iter().zip(output.chunks_exact_mut(3)) {
output.copy_from_slice(&[*input, *input, *input]);
}
Some(())
}
ColorSpaceType::DeviceRgb => {
output.copy_from_slice(input);
Some(())
}
ColorSpaceType::Pattern(i) => i.convert_u8(input, output),
ColorSpaceType::Indexed(i) => i.convert_u8(input, output),
ColorSpaceType::ICCBased(i) => i.convert_u8(input, output),
ColorSpaceType::CalGray(i) => i.convert_u8(input, output),
ColorSpaceType::CalRgb(i) => i.convert_u8(input, output),
ColorSpaceType::Lab(i) => i.convert_u8(input, output),
ColorSpaceType::Separation(i) => i.convert_u8(input, output),
ColorSpaceType::DeviceN(i) => i.convert_u8(input, output),
}
}
fn is_none(&self) -> bool {
match self.0.as_ref() {
ColorSpaceType::Separation(s) => s.is_none(),
ColorSpaceType::DeviceN(d) => d.is_none(),
_ => false,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct CalGray {
white_point: [f32; 3],
black_point: [f32; 3],
gamma: f32,
}
impl CalGray {
fn new(dict: &Dict<'_>) -> Option<Self> {
let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]);
let black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]);
let gamma = dict.get::<f32>(GAMMA).unwrap_or(1.0);
Some(Self {
white_point,
black_point,
gamma,
})
}
}
impl ToRgb for CalGray {
fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
for (input, output) in input.iter().copied().zip(output.chunks_exact_mut(3)) {
let g = self.gamma;
let (_xw, yw, _zw) = {
let wp = self.white_point;
(wp[0], wp[1], wp[2])
};
let (_xb, _yb, _zb) = {
let bp = self.black_point;
(bp[0], bp[1], bp[2])
};
let a = input;
let ag = a.powf(g);
let l = yw * ag;
let val = (0.0_f32.max(295.8 * l.powf(0.333_333_34) - 40.8) + 0.5) as u8;
output.copy_from_slice(&[val, val, val]);
}
Some(())
}
}
#[derive(Debug, Clone)]
pub(crate) struct CalRgb {
white_point: [f32; 3],
black_point: [f32; 3],
matrix: [f32; 9],
gamma: [f32; 3],
}
impl CalRgb {
fn new(dict: &Dict<'_>) -> Option<Self> {
let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]);
let black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]);
let matrix = dict
.get::<[f32; 9]>(MATRIX)
.unwrap_or([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]);
let gamma = dict.get::<[f32; 3]>(GAMMA).unwrap_or([1.0, 1.0, 1.0]);
Some(Self {
white_point,
black_point,
matrix,
gamma,
})
}
const BRADFORD_SCALE_MATRIX: [f32; 9] = [
0.8951, 0.2664, -0.1614, -0.7502, 1.7135, 0.0367, 0.0389, -0.0685, 1.0296,
];
const BRADFORD_SCALE_INVERSE_MATRIX: [f32; 9] = [
0.9869929, -0.1470543, 0.1599627, 0.4323053, 0.5183603, 0.0492912, -0.0085287, 0.0400428,
0.9684867,
];
const SRGB_D65_XYZ_TO_RGB_MATRIX: [f32; 9] = [
3.2404542, -1.5371385, -0.4985314, -0.969_266, 1.8760108, 0.0415560, 0.0556434, -0.2040259,
1.0572252,
];
const FLAT_WHITEPOINT: [f32; 3] = [1.0, 1.0, 1.0];
const D65_WHITEPOINT: [f32; 3] = [0.95047, 1.0, 1.08883];
fn decode_l_constant() -> f32 {
((8.0_f32 + 16.0) / 116.0).powi(3) / 8.0
}
fn srgb_transfer_function(color: f32) -> f32 {
if color <= 0.0031308 {
(12.92 * color).clamp(0.0, 1.0)
} else if color >= 0.99554525 {
1.0
} else {
((1.0 + 0.055) * color.powf(1.0 / 2.4) - 0.055).clamp(0.0, 1.0)
}
}
fn matrix_product(a: &[f32; 9], b: &[f32; 3]) -> [f32; 3] {
[
a[0] * b[0] + a[1] * b[1] + a[2] * b[2],
a[3] * b[0] + a[4] * b[1] + a[5] * b[2],
a[6] * b[0] + a[7] * b[1] + a[8] * b[2],
]
}
fn to_flat(source_white_point: &[f32; 3], lms: &[f32; 3]) -> [f32; 3] {
[
lms[0] / source_white_point[0],
lms[1] / source_white_point[1],
lms[2] / source_white_point[2],
]
}
fn to_d65(source_white_point: &[f32; 3], lms: &[f32; 3]) -> [f32; 3] {
[
lms[0] * Self::D65_WHITEPOINT[0] / source_white_point[0],
lms[1] * Self::D65_WHITEPOINT[1] / source_white_point[1],
lms[2] * Self::D65_WHITEPOINT[2] / source_white_point[2],
]
}
fn decode_l(l: f32) -> f32 {
if l < 0.0 {
-Self::decode_l(-l)
} else if l > 8.0 {
((l + 16.0) / 116.0).powi(3)
} else {
l * Self::decode_l_constant()
}
}
fn compensate_black_point(source_bp: &[f32; 3], xyz_flat: &[f32; 3]) -> [f32; 3] {
if source_bp == &[0.0, 0.0, 0.0] {
return *xyz_flat;
}
let zero_decode_l = Self::decode_l(0.0);
let mut out = [0.0; 3];
for i in 0..3 {
let src = Self::decode_l(source_bp[i]);
let scale = (1.0 - zero_decode_l) / (1.0 - src);
let offset = 1.0 - scale;
out[i] = xyz_flat[i] * scale + offset;
}
out
}
fn normalize_white_point_to_flat(
&self,
source_white_point: &[f32; 3],
xyz: &[f32; 3],
) -> [f32; 3] {
if source_white_point[0] == 1.0 && source_white_point[2] == 1.0 {
return *xyz;
}
let lms = Self::matrix_product(&Self::BRADFORD_SCALE_MATRIX, xyz);
let lms_flat = Self::to_flat(source_white_point, &lms);
Self::matrix_product(&Self::BRADFORD_SCALE_INVERSE_MATRIX, &lms_flat)
}
fn normalize_white_point_to_d65(
&self,
source_white_point: &[f32; 3],
xyz: &[f32; 3],
) -> [f32; 3] {
let lms = Self::matrix_product(&Self::BRADFORD_SCALE_MATRIX, xyz);
let lms_d65 = Self::to_d65(source_white_point, &lms);
Self::matrix_product(&Self::BRADFORD_SCALE_INVERSE_MATRIX, &lms_d65)
}
}
impl ToRgb for CalRgb {
fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
for (input, output) in input.chunks_exact(3).zip(output.chunks_exact_mut(3)) {
let input = [
input[0].clamp(0.0, 1.0),
input[1].clamp(0.0, 1.0),
input[2].clamp(0.0, 1.0),
];
let [r, g, b] = input;
let [gr, gg, gb] = self.gamma;
let [agr, bgg, cgb] = [
if r == 1.0 { 1.0 } else { r.powf(gr) },
if g == 1.0 { 1.0 } else { g.powf(gg) },
if b == 1.0 { 1.0 } else { b.powf(gb) },
];
let m = &self.matrix;
let x = m[0] * agr + m[3] * bgg + m[6] * cgb;
let y = m[1] * agr + m[4] * bgg + m[7] * cgb;
let z = m[2] * agr + m[5] * bgg + m[8] * cgb;
let xyz = [x, y, z];
let xyz_flat = self.normalize_white_point_to_flat(&self.white_point, &xyz);
let xyz_black = Self::compensate_black_point(&self.black_point, &xyz_flat);
let xyz_d65 = self.normalize_white_point_to_d65(&Self::FLAT_WHITEPOINT, &xyz_black);
let srgb_xyz = Self::matrix_product(&Self::SRGB_D65_XYZ_TO_RGB_MATRIX, &xyz_d65);
output.copy_from_slice(&[
(Self::srgb_transfer_function(srgb_xyz[0]) * 255.0 + 0.5) as u8,
(Self::srgb_transfer_function(srgb_xyz[1]) * 255.0 + 0.5) as u8,
(Self::srgb_transfer_function(srgb_xyz[2]) * 255.0 + 0.5) as u8,
]);
}
Some(())
}
}
#[derive(Debug, Clone)]
pub(crate) struct Lab {
range: [f32; 4],
profile: ICCProfile,
}
impl Lab {
fn new(dict: &Dict<'_>) -> Option<Self> {
let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]);
let _black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]);
let range = dict
.get::<[f32; 4]>(RANGE)
.unwrap_or([-100.0, 100.0, -100.0, 100.0]);
let mut profile = ColorProfile::new_from_slice(include_bytes!("../assets/LAB.icc")).ok()?;
profile.white_point = Xyzd::new(
white_point[0] as f64,
white_point[1] as f64,
white_point[2] as f64,
);
let profile = ICCProfile::new_from_src_profile(
profile, false,
false, 3,
)?;
Some(Self { range, profile })
}
}
impl ToRgb for Lab {
fn convert_f32(&self, input: &[f32], output: &mut [u8], manual_scale: bool) -> Option<()> {
if !manual_scale {
let input = input
.chunks_exact(3)
.flat_map(|i| {
let l = i[0] / 100.0;
let a = (i[1] + 128.0) / 255.0;
let b = (i[2] + 128.0) / 255.0;
[l, a, b]
})
.collect::<Vec<_>>();
self.profile.convert_f32(&input, output, manual_scale)
} else {
self.profile.convert_f32(input, output, manual_scale)
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct Indexed {
values: Vec<Vec<f32>>,
hival: u8,
base: Box<ColorSpace>,
}
impl Indexed {
fn new(array: &Array<'_>, cache: &Cache) -> Option<Self> {
let mut iter = array.flex_iter();
let _ = iter.next::<Name>()?;
let base_color_space = ColorSpace::new(iter.next::<Object<'_>>()?, cache)?;
let hival = iter.next::<u8>()?;
let values = {
let data = iter
.next::<Stream<'_>>()
.and_then(|s| s.decoded().ok())
.or_else(|| iter.next::<object::String>().map(|s| s.to_vec()))?;
let num_components = base_color_space.num_components();
let mut byte_iter = data.iter().copied();
let mut vals = vec![];
for _ in 0..=hival {
let mut temp = vec![];
for _ in 0..num_components {
temp.push(byte_iter.next()? as f32 / 255.0);
}
vals.push(temp);
}
vals
};
Some(Self {
values,
hival,
base: Box::new(base_color_space),
})
}
}
impl ToRgb for Indexed {
fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
let mut indexed = vec![0.0; input.len() * self.base.num_components() as usize];
for (input, output) in input
.iter()
.copied()
.zip(indexed.chunks_exact_mut(self.base.num_components() as usize))
{
let idx = (input.clamp(0.0, self.hival as f32) + 0.5) as usize;
output.copy_from_slice(&self.values[idx]);
}
self.base.convert_f32(&indexed, output, true)
}
}
#[derive(Debug, Clone)]
pub(crate) struct Separation {
alternate_space: ColorSpace,
tint_transform: Function,
is_none_separation: bool,
}
impl Separation {
fn new(array: &Array<'_>, cache: &Cache) -> Option<Self> {
let mut iter = array.flex_iter();
let _ = iter.next::<Name>()?;
let name = iter.next::<Name>()?;
let alternate_space = ColorSpace::new(iter.next::<Object<'_>>()?, cache)?;
let tint_transform = Function::new(&iter.next::<Object<'_>>()?)?;
let is_none_separation = name.as_str() == "None";
Some(Self {
alternate_space,
tint_transform,
is_none_separation,
})
}
}
impl ToRgb for Separation {
fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
let evaluated = input
.iter()
.flat_map(|n| {
self.tint_transform
.eval(smallvec![*n])
.unwrap_or(self.alternate_space.initial_color())
})
.collect::<Vec<_>>();
self.alternate_space.convert_f32(&evaluated, output, false)
}
fn is_none(&self) -> bool {
self.is_none_separation
}
}
#[derive(Debug, Clone)]
pub(crate) struct DeviceN {
alternate_space: ColorSpace,
num_components: u8,
tint_transform: Function,
is_none: bool,
}
impl DeviceN {
fn new(array: &Array<'_>, cache: &Cache) -> Option<Self> {
let mut iter = array.flex_iter();
let _ = iter.next::<Name>()?;
let names = iter.next::<Array<'_>>()?.iter::<Name>().collect::<Vec<_>>();
let num_components = u8::try_from(names.len()).ok()?;
let all_none = names.iter().all(|n| n.as_str() == "None");
let alternate_space = ColorSpace::new(iter.next::<Object<'_>>()?, cache)?;
let tint_transform = Function::new(&iter.next::<Object<'_>>()?)?;
if num_components == 0 {
return None;
}
Some(Self {
alternate_space,
num_components,
tint_transform,
is_none: all_none,
})
}
}
impl ToRgb for DeviceN {
fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
let evaluated = input
.chunks_exact(self.num_components as usize)
.flat_map(|n| {
self.tint_transform
.eval(n.to_smallvec())
.unwrap_or(self.alternate_space.initial_color())
})
.collect::<Vec<_>>();
self.alternate_space.convert_f32(&evaluated, output, false)
}
fn is_none(&self) -> bool {
self.is_none
}
}
struct ICCColorRepr {
transform_u8: Box<Transform8BitExecutor>,
transform_f32: Box<TransformF32BitExecutor>,
number_components: usize,
is_srgb: bool,
is_lab: bool,
}
#[derive(Clone)]
pub(crate) struct ICCProfile(Arc<ICCColorRepr>);
impl Debug for ICCProfile {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "ICCColor {{..}}")
}
}
impl ICCProfile {
fn new(profile: &[u8], number_components: usize) -> Option<Self> {
let src_profile = ColorProfile::new_from_slice(profile).ok()?;
let profile_components = match src_profile.color_space {
DataColorSpace::Gray => 1,
DataColorSpace::Rgb
| DataColorSpace::Lab
| DataColorSpace::Luv
| DataColorSpace::Xyz
| DataColorSpace::YCbr
| DataColorSpace::Yxy
| DataColorSpace::Hsv
| DataColorSpace::Hls
| DataColorSpace::Cmy
| DataColorSpace::Color3 => 3,
DataColorSpace::Cmyk | DataColorSpace::Color4 => 4,
_ => {
warn!(
"unsupported ICC profile color space {:?}",
src_profile.color_space
);
return None;
}
};
if number_components != profile_components {
warn!(
"ICCBased /N={} does not match embedded ICC profile component count {}; using profile",
number_components, profile_components
);
}
const SRGB_MARKER: &[u8] = b"sRGB";
let is_srgb = profile
.get(52..56)
.map(|device_model| device_model == SRGB_MARKER)
.unwrap_or(false);
let is_lab = src_profile.color_space == DataColorSpace::Lab;
Self::new_from_src_profile(src_profile, is_srgb, is_lab, profile_components)
}
fn new_from_src_profile(
src_profile: ColorProfile,
is_srgb: bool,
is_lab: bool,
number_components: usize,
) -> Option<Self> {
let dest_profile = ColorProfile::new_srgb();
let src_layout = match number_components {
1 => Layout::Gray,
3 => Layout::Rgb,
4 => Layout::Rgba, _ => {
warn!("unsupported number of components {number_components} for ICC profile");
return None;
}
};
let intents_to_try = [
RenderingIntent::RelativeColorimetric,
RenderingIntent::Perceptual,
RenderingIntent::Saturation,
];
let mut u8_transform = None;
let mut f32_transform = None;
for intent in intents_to_try {
let options = TransformOptions {
rendering_intent: intent,
..TransformOptions::default()
};
let u8_ok = src_profile
.create_transform_8bit(src_layout, &dest_profile, Layout::Rgb, options)
.ok();
let f32_ok = src_profile
.create_transform_f32(src_layout, &dest_profile, Layout::Rgb, options)
.ok();
if let (Some(u), Some(f)) = (u8_ok, f32_ok) {
u8_transform = Some(u);
f32_transform = Some(f);
break;
}
}
let u8_transform = u8_transform?;
let f32_transform = f32_transform?;
Some(Self(Arc::new(ICCColorRepr {
transform_u8: u8_transform,
transform_f32: f32_transform,
number_components,
is_srgb,
is_lab,
})))
}
fn is_srgb(&self) -> bool {
self.0.is_srgb
}
fn is_lab(&self) -> bool {
self.0.is_lab
}
}
impl ToRgb for ICCProfile {
fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> {
let mut temp = vec![0.0_f32; output.len()];
if self.is_lab() {
let scaled = input
.chunks_exact(3)
.flat_map(|i| {
[
i[0] * (1.0 / 100.0),
(i[1] + 128.0) * (1.0 / 255.0),
(i[2] + 128.0) * (1.0 / 255.0),
]
})
.collect::<Vec<_>>();
self.0.transform_f32.transform(&scaled, &mut temp).ok()?;
} else {
self.0.transform_f32.transform(input, &mut temp).ok()?;
};
for (input, output) in temp.iter().zip(output.iter_mut()) {
*output = (input * 255.0 + 0.5) as u8;
}
Some(())
}
fn supports_u8(&self) -> bool {
true
}
fn convert_u8(&self, input: &[u8], output: &mut [u8]) -> Option<()> {
if self.is_srgb() && input.len() == output.len() {
output.copy_from_slice(input);
} else {
self.0.transform_u8.transform(input, output).ok()?;
}
Some(())
}
}
#[inline(always)]
fn f32_to_u8(val: f32) -> u8 {
(val * 255.0 + 0.5) as u8
}
#[derive(Debug, Clone)]
pub struct Color {
color_space: ColorSpace,
components: ColorComponents,
opacity: f32,
}
impl Color {
pub(crate) fn new(color_space: ColorSpace, components: ColorComponents, opacity: f32) -> Self {
Self {
color_space,
components,
opacity,
}
}
pub fn to_rgba(&self) -> AlphaColor {
self.color_space
.to_rgba(&self.components, self.opacity, false)
}
pub fn from_rgba(rgba: AlphaColor) -> Self {
let c = rgba.components();
Self {
color_space: ColorSpace::device_rgb(),
components: smallvec![c[0], c[1], c[2]],
opacity: c[3],
}
}
pub fn from_device_rgb(r: f32, g: f32, b: f32) -> Self {
Self {
color_space: ColorSpace::device_rgb(),
components: smallvec![r, g, b],
opacity: 1.0,
}
}
pub fn from_device_rgb_with_opacity(r: f32, g: f32, b: f32, opacity: f32) -> Self {
Self {
color_space: ColorSpace::device_rgb(),
components: smallvec![r, g, b],
opacity,
}
}
pub fn opacity(&self) -> f32 {
self.opacity
}
pub fn is_device_cmyk(&self) -> bool {
self.color_space.is_device_cmyk()
}
pub fn device_cmyk_components(&self) -> Option<[f32; 4]> {
if !self.color_space.is_device_cmyk() || self.components.len() != 4 {
return None;
}
Some([
self.components[0],
self.components[1],
self.components[2],
self.components[3],
])
}
}
pub(crate) trait ToRgb {
fn convert_f32(&self, input: &[f32], output: &mut [u8], manual_scale: bool) -> Option<()>;
fn supports_u8(&self) -> bool {
false
}
fn convert_u8(&self, _: &[u8], _: &mut [u8]) -> Option<()> {
unimplemented!();
}
fn is_none(&self) -> bool {
false
}
fn to_alpha_color(
&self,
input: &[f32],
mut opacity: f32,
manual_scale: bool,
) -> Option<AlphaColor> {
let mut output = [0; 3];
self.convert_f32(input, &mut output, manual_scale)?;
if self.is_none() {
opacity = 0.0;
}
Some(AlphaColor::from_rgba8(
output[0],
output[1],
output[2],
(opacity * 255.0 + 0.5) as u8,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use pdf_syntax::object::{Array, FromBytes};
fn separation_array(ink_name: &str) -> Vec<u8> {
format!(
"[/Separation /{ink_name} /DeviceGray \
<< /FunctionType 2 /Domain [0 1] /C0 [1] /C1 [0] /N 1 >> ]",
ink_name = ink_name
)
.into_bytes()
}
fn make_separation(ink_name: &str) -> Option<Separation> {
let bytes = separation_array(ink_name);
let array = Array::from_bytes(&bytes)?;
let cache = Cache::new();
Separation::new(&array, &cache)
}
#[test]
fn none_ink_is_suppressed() {
let sep = make_separation("None").expect("should parse");
assert!(sep.is_none(), "ink name 'None' must be suppressed");
}
#[test]
fn pantone_ink_is_not_suppressed() {
let sep = make_separation("PANTONE#20123#20CVC").expect("should parse");
assert!(!sep.is_none(), "PANTONE spot colour must not be suppressed");
}
#[test]
fn all_ink_is_not_suppressed() {
let sep = make_separation("All").expect("should parse");
assert!(!sep.is_none(), "'All' separation must not be suppressed");
}
#[test]
fn pantone_produces_visible_color() {
let sep = make_separation("PANTONE#20123#20CVC").expect("should parse");
let cs = ColorSpace(Arc::new(ColorSpaceType::Separation(sep)));
let color = cs.to_rgba(&[1.0], 1.0, false);
assert!(
color.to_rgba8()[3] > 0,
"PANTONE ink at tint=1.0 must be opaque"
);
}
#[test]
fn none_produces_transparent_color() {
let sep = make_separation("None").expect("should parse");
let cs = ColorSpace(Arc::new(ColorSpaceType::Separation(sep)));
let color = cs.to_rgba(&[1.0], 1.0, false);
assert_eq!(
color.to_rgba8()[3],
0,
"Separation/None ink must be fully transparent"
);
}
}