use super::{ColorSpace, MAX_PATTERN_COMPONENTS, Rgb};
use pdfrum_object::Name;
use smallvec::SmallVec;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)]
pub struct ColorValue {
pub space: Option<Arc<ColorSpace>>,
pub components: SmallVec<[f32; 4]>,
pub pattern: Option<Box<PatternValue>>,
}
impl Default for ColorValue {
fn default() -> Self {
Self {
space: None,
components: SmallVec::from_slice(&[0.0]),
pattern: None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PatternValue {
pub name: Name,
pub components: SmallVec<[f32; 4]>,
pub loaded: Option<Arc<crate::pattern::Pattern>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum SetComponentsError {
#[error("need {needed} colour components, got {got}")]
TooFew {
needed: usize,
got: usize,
},
#[error("a pattern colour is not set by components")]
PatternSpace,
}
impl ColorValue {
pub fn set_space(&mut self, space: Arc<ColorSpace>) {
self.components = space.default_color().into();
self.pattern = None;
self.space = Some(space);
}
pub fn set_components(&mut self, values: &[f32]) -> Result<(), SetComponentsError> {
let space = self
.space
.get_or_insert_with(|| Arc::new(ColorSpace::DeviceGray));
if space.n_components() > values.len() {
return Err(SetComponentsError::TooFew {
needed: space.n_components(),
got: values.len(),
});
}
if matches!(**space, ColorSpace::Pattern(_)) {
return Err(SetComponentsError::PatternSpace);
}
self.components = SmallVec::from_slice(values);
Ok(())
}
pub fn set_stock(&mut self, space: ColorSpace, values: &[f32]) {
let space = Arc::new(space);
self.set_space(Arc::clone(&space));
if space.n_components() <= values.len() {
self.components = SmallVec::from_slice(values);
}
}
pub fn set_pattern(
&mut self,
name: Name,
values: &[f32],
loaded: Option<Arc<crate::pattern::Pattern>>,
) {
if values.len() > MAX_PATTERN_COMPONENTS {
return;
}
if !self.is_pattern() {
self.space = Some(Arc::new(ColorSpace::Pattern(Box::default())));
}
self.pattern = Some(Box::new(PatternValue {
name,
components: SmallVec::from_slice(values),
loaded,
}));
}
#[must_use]
pub fn to_rgb(&self) -> Option<Rgb> {
let space = self.space.as_ref()?;
if let ColorSpace::Pattern(pattern_space) = &**space {
let value = self.pattern.as_ref()?;
return pattern_space.to_rgb(&value.components);
}
space.try_to_rgb(&self.components)
}
#[must_use]
pub fn is_pattern(&self) -> bool {
self.space
.as_ref()
.is_some_and(|s| matches!(**s, ColorSpace::Pattern(_)))
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unreadable_literal,
clippy::float_cmp,
clippy::indexing_slicing,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "test fixtures quote oracle vectors verbatim and compare exactly"
)]
use super::{ColorSpace, ColorValue, SetComponentsError};
use pdfrum_object::Name;
use smallvec::SmallVec;
use std::sync::Arc;
#[test]
fn the_initial_colour_is_black_in_device_gray() {
let c = ColorValue::default();
assert_eq!(&c.components[..], &[0.0]);
assert!(c.space.is_none());
}
#[test]
fn installing_a_space_resets_the_components() {
let mut c = ColorValue::default();
c.set_stock(ColorSpace::DeviceRgb, &[1.0, 0.5, 0.25]);
assert_eq!(&c.components[..], &[1.0, 0.5, 0.25]);
c.set_space(Arc::new(ColorSpace::DeviceGray));
assert_eq!(&c.components[..], &[0.0]);
}
#[test]
fn a_pattern_space_refuses_component_operands() {
let mut c = ColorValue::default();
c.set_space(Arc::new(ColorSpace::Pattern(Box::default())));
let before = c.components.clone();
assert_eq!(
c.set_components(&[0.5]),
Err(SetComponentsError::PatternSpace)
);
assert_eq!(c.components, before);
}
#[test]
fn too_few_components_change_nothing_at_all() {
let mut c = ColorValue::default();
c.set_stock(ColorSpace::DeviceCmyk, &[0.1, 0.2, 0.3, 0.4]);
assert_eq!(
c.set_components(&[0.9, 0.9]),
Err(SetComponentsError::TooFew { needed: 4, got: 2 })
);
assert_eq!(&c.components[..], &[0.1, 0.2, 0.3, 0.4]);
assert!(c.set_components(&[0.5, 0.5, 0.5, 0.5]).is_ok());
assert_eq!(&c.components[..], &[0.5, 0.5, 0.5, 0.5]);
}
#[test]
fn components_with_no_space_install_device_gray() {
let mut c = ColorValue {
space: None,
components: SmallVec::new(),
pattern: None,
};
assert!(c.set_components(&[0.75]).is_ok());
assert_eq!(
c.space.as_deref(),
Some(&ColorSpace::DeviceGray),
"an unset space becomes DeviceGray"
);
}
#[test]
fn a_pattern_operand_vector_over_sixteen_is_refused_outright() {
let mut c = ColorValue::default();
c.set_space(Arc::new(ColorSpace::Pattern(Box::default())));
c.set_pattern(Name::from("P0"), &[0.5, 0.25], None);
c.set_pattern(Name::from("P1"), &[1.0; 17], None);
let p = c.pattern.as_ref().expect("pattern");
assert_eq!(p.name.as_bytes(), b"P0", "the whole call is refused");
assert_eq!(&p.components[..], &[0.5, 0.25]);
}
#[test]
fn installing_a_pattern_installs_the_pattern_space() {
let mut c = ColorValue::default();
assert!(!c.is_pattern());
c.set_pattern(Name::from("P1"), &[], None);
assert!(c.is_pattern(), "the stock /Pattern space is installed");
let mut with_base = ColorValue::default();
with_base.set_space(Arc::new(ColorSpace::Pattern(Box::new(
super::super::PatternSpace {
base: Some(Box::new(ColorSpace::DeviceRgb)),
},
))));
with_base.set_pattern(Name::from("P1"), &[1.0, 0.0, 0.0], None);
assert_eq!(
with_base.to_rgb().map(super::Rgb::to_bytes),
Some([255, 0, 0])
);
}
#[test]
fn a_pattern_colour_resolves_through_its_base_space() {
let mut c = ColorValue::default();
c.set_space(Arc::new(ColorSpace::Pattern(Box::new(
super::super::PatternSpace {
base: Some(Box::new(ColorSpace::DeviceGray)),
},
))));
assert!(c.is_pattern());
assert!(c.to_rgb().is_none());
c.set_pattern(Name::from("P0"), &[0.5], None);
let rgb = c.to_rgb().expect("colour");
assert!((rgb.r - 0.5).abs() < 1e-6);
}
}