use super::calibrated_color::{CalGrayColorSpace, CalRgbColorSpace};
use super::color_profiles::{IccColorSpace, IccProfile};
use super::lab_color::LabColorSpace;
use crate::objects::{Dictionary, Object};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum PageColorSpace {
DeviceAlias(DeviceColorSpace),
Parameterised {
family: ParameterisedFamily,
params: Dictionary,
},
IccStream {
n: u8,
alternate: DeviceColorSpace,
profile_data: Arc<Vec<u8>>,
range: Option<Vec<f64>>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DeviceColorSpace {
Gray,
Rgb,
Cmyk,
Pattern,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParameterisedFamily {
CalGray,
CalRgb,
Lab,
IccBased,
}
impl DeviceColorSpace {
pub const fn pdf_name(self) -> &'static str {
match self {
DeviceColorSpace::Gray => "DeviceGray",
DeviceColorSpace::Rgb => "DeviceRGB",
DeviceColorSpace::Cmyk => "DeviceCMYK",
DeviceColorSpace::Pattern => "Pattern",
}
}
}
impl ParameterisedFamily {
pub const fn pdf_name(self) -> &'static str {
match self {
ParameterisedFamily::CalGray => "CalGray",
ParameterisedFamily::CalRgb => "CalRGB",
ParameterisedFamily::Lab => "Lab",
ParameterisedFamily::IccBased => "ICCBased",
}
}
}
impl PageColorSpace {
pub(crate) fn to_object(&self) -> Object {
match self {
PageColorSpace::DeviceAlias(device) => Object::Name(device.pdf_name().to_string()),
PageColorSpace::Parameterised { family, params } => Object::Array(vec![
Object::Name(family.pdf_name().to_string()),
Object::Dictionary(params.clone()),
]),
PageColorSpace::IccStream { .. } => {
unreachable!("IccStream must be emitted via icc_stream_parts, not to_object")
}
}
}
pub(crate) fn icc_stream_parts(&self) -> Option<(Dictionary, Vec<u8>)> {
match self {
PageColorSpace::IccStream {
n,
alternate,
profile_data,
range,
} => {
debug_assert!(
!matches!(alternate, DeviceColorSpace::Pattern),
"/Alternate must not be Pattern (ISO 32000-1 §8.6.5.5)"
);
let mut dict = Dictionary::new();
dict.set("N", Object::Integer(*n as i64));
dict.set("Alternate", Object::Name(alternate.pdf_name().to_string()));
if let Some(r) = range {
let default: Vec<f64> = (0..*n).flat_map(|_| [0.0, 1.0]).collect();
if *r != default {
dict.set(
"Range",
Object::Array(r.iter().map(|&x| Object::Real(x)).collect()),
);
}
}
Some((dict, (**profile_data).clone()))
}
_ => None,
}
}
}
impl From<&CalGrayColorSpace> for PageColorSpace {
fn from(cs: &CalGrayColorSpace) -> Self {
PageColorSpace::Parameterised {
family: ParameterisedFamily::CalGray,
params: cs.params_dictionary(),
}
}
}
impl From<&CalRgbColorSpace> for PageColorSpace {
fn from(cs: &CalRgbColorSpace) -> Self {
PageColorSpace::Parameterised {
family: ParameterisedFamily::CalRgb,
params: cs.params_dictionary(),
}
}
}
impl From<&LabColorSpace> for PageColorSpace {
fn from(cs: &LabColorSpace) -> Self {
PageColorSpace::Parameterised {
family: ParameterisedFamily::Lab,
params: cs.params_dictionary(),
}
}
}
impl From<&IccProfile> for PageColorSpace {
fn from(profile: &IccProfile) -> Self {
let alternate = match profile.color_space {
IccColorSpace::Gray => DeviceColorSpace::Gray,
IccColorSpace::Cmyk => DeviceColorSpace::Cmyk,
IccColorSpace::Rgb | IccColorSpace::Lab => DeviceColorSpace::Rgb,
IccColorSpace::Generic(n) => match n {
1 => DeviceColorSpace::Gray,
4 => DeviceColorSpace::Cmyk,
_ => DeviceColorSpace::Rgb,
},
};
PageColorSpace::IccStream {
n: profile.components,
alternate,
profile_data: Arc::new(profile.data.clone()),
range: profile.range.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn device_alias_to_object_is_name() {
let obj = PageColorSpace::DeviceAlias(DeviceColorSpace::Cmyk).to_object();
match obj {
Object::Name(n) => assert_eq!(n, "DeviceCMYK"),
other => panic!("expected Name(DeviceCMYK), got {other:?}"),
}
}
#[test]
fn parameterised_to_object_is_two_element_array() {
let mut params = Dictionary::new();
params.set("Gamma", Object::Real(2.2));
let obj = PageColorSpace::Parameterised {
family: ParameterisedFamily::CalGray,
params,
}
.to_object();
match obj {
Object::Array(a) => {
assert_eq!(a.len(), 2);
assert!(matches!(&a[0], Object::Name(n) if n == "CalGray"));
assert!(matches!(&a[1], Object::Dictionary(_)));
}
other => panic!("expected two-element array, got {other:?}"),
}
}
#[test]
fn device_pdf_name_covers_all_variants() {
assert_eq!(DeviceColorSpace::Gray.pdf_name(), "DeviceGray");
assert_eq!(DeviceColorSpace::Rgb.pdf_name(), "DeviceRGB");
assert_eq!(DeviceColorSpace::Cmyk.pdf_name(), "DeviceCMYK");
assert_eq!(DeviceColorSpace::Pattern.pdf_name(), "Pattern");
}
#[test]
fn parameterised_pdf_name_covers_all_variants() {
assert_eq!(ParameterisedFamily::CalGray.pdf_name(), "CalGray");
assert_eq!(ParameterisedFamily::CalRgb.pdf_name(), "CalRGB");
assert_eq!(ParameterisedFamily::Lab.pdf_name(), "Lab");
assert_eq!(ParameterisedFamily::IccBased.pdf_name(), "ICCBased");
}
}