pdfrum_page/color/
special.rs1use super::{ColorSpace, Rgb};
20use crate::function::Function;
21use std::sync::Arc;
22
23const SCRATCH_FLOOR: usize = 16;
26
27pub const MAX_PATTERN_COMPONENTS: usize = 16;
29
30#[derive(Debug, Clone, PartialEq)]
32pub struct Separation {
33 pub none: bool,
35 pub alternate: Option<Box<ColorSpace>>,
37 pub tint: Option<Arc<Function>>,
41}
42
43impl Separation {
44 #[must_use]
46 pub fn to_rgb(&self, comps: &[f32]) -> Option<Rgb> {
47 if self.none {
48 return None;
49 }
50 let alternate = self.alternate.as_ref()?;
51 let tint = comps.first().copied().unwrap_or(0.0);
52 let Some(func) = &self.tint else {
53 let broadcast = vec![tint; alternate.n_components()];
56 return Some(alternate.to_rgb(&broadcast));
57 };
58 let mut results = vec![0.0f32; func.output_count().max(SCRATCH_FLOOR)];
59 let produced = func.eval_into(&[tint], &mut results);
60 if produced == 0 {
61 return None;
62 }
63 Some(alternate.to_rgb(&results))
64 }
65}
66
67#[derive(Debug, Clone, PartialEq)]
69pub struct DeviceN {
70 pub names: Box<[pdfrum_object::Name]>,
72 pub alternate: Box<ColorSpace>,
74 pub tint: Arc<Function>,
76}
77
78impl DeviceN {
79 #[must_use]
81 pub fn to_rgb(&self, comps: &[f32]) -> Option<Rgb> {
82 let n = self.names.len();
83 let inputs: Vec<f32> = (0..n)
84 .map(|i| comps.get(i).copied().unwrap_or(0.0))
85 .collect();
86 let mut results = vec![0.0f32; self.tint.output_count().max(SCRATCH_FLOOR)];
87 let produced = self.tint.eval_into(&inputs, &mut results);
88 if produced == 0 {
89 return None;
90 }
91 Some(self.alternate.to_rgb(&results))
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Default)]
102pub struct PatternSpace {
103 pub base: Option<Box<ColorSpace>>,
105}
106
107impl PatternSpace {
108 #[must_use]
111 pub fn n_components(&self) -> usize {
112 self.base.as_ref().map_or(1, |b| b.n_components() + 1)
113 }
114
115 #[must_use]
120 pub fn to_rgb(&self, comps: &[f32]) -> Option<Rgb> {
121 let base = self.base.as_ref()?;
122 Some(base.to_rgb(comps))
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 #![allow(
132 clippy::unreadable_literal,
133 clippy::float_cmp,
134 clippy::indexing_slicing,
135 clippy::cast_precision_loss,
136 clippy::cast_possible_truncation,
137 reason = "test fixtures quote oracle vectors verbatim and compare exactly"
138 )]
139
140 use super::{DeviceN, PatternSpace, Separation};
141 use crate::color::ColorSpace;
142 use crate::function::{Exponential, Function};
143 use std::sync::Arc;
144
145 fn ramp3() -> Arc<Function> {
147 Arc::new(Function::Exponential(Exponential {
148 domain: Box::from(&[0.0f32, 1.0][..]),
149 range: Box::from(&[0.0f32, 1.0, 0.0, 1.0, 0.0, 1.0][..]),
150 c0: Box::from(&[0.0f32, 0.0, 0.0][..]),
151 c1: Box::from(&[1.0f32, 1.0, 1.0][..]),
152 exponent: 1.0,
153 orig_outputs: 3,
154 outputs: 3,
155 }))
156 }
157
158 #[test]
159 fn none_separations_paint_nothing() {
160 let sep = Separation {
161 none: true,
162 alternate: None,
163 tint: None,
164 };
165 assert!(sep.to_rgb(&[1.0]).is_none());
166 }
167
168 #[test]
169 fn a_separation_without_a_transform_broadcasts_its_tint() {
170 let sep = Separation {
171 none: false,
172 alternate: Some(Box::new(ColorSpace::DeviceRgb)),
173 tint: None,
174 };
175 let rgb = sep.to_rgb(&[0.25]).expect("colour");
176 assert!((rgb.r - 0.25).abs() < 1e-6);
177 assert!((rgb.g - 0.25).abs() < 1e-6);
178 assert!((rgb.b - 0.25).abs() < 1e-6);
179 }
180
181 #[test]
182 fn a_separation_with_a_transform_uses_it() {
183 let sep = Separation {
184 none: false,
185 alternate: Some(Box::new(ColorSpace::DeviceRgb)),
186 tint: Some(ramp3()),
187 };
188 let rgb = sep.to_rgb(&[0.5]).expect("colour");
189 assert!((rgb.r - 0.5).abs() < 1e-5);
190 }
191
192 #[test]
193 fn device_n_reads_exactly_its_name_count() {
194 let cs = DeviceN {
195 names: Box::from(&[pdfrum_object::Name::from("A")][..]),
196 alternate: Box::new(ColorSpace::DeviceRgb),
197 tint: ramp3(),
198 };
199 assert_eq!(cs.names.len(), 1);
200 let a = cs.to_rgb(&[0.5, 9.0, 9.0]).expect("colour");
202 let b = cs.to_rgb(&[0.5]).expect("colour");
203 assert!((a.r - b.r).abs() < 1e-6);
204 }
205
206 #[test]
207 fn a_pattern_space_without_a_base_has_one_component_and_no_colour() {
208 let cs = PatternSpace::default();
209 assert_eq!(cs.n_components(), 1);
210 assert!(cs.to_rgb(&[0.5]).is_none());
211 }
212
213 #[test]
214 fn a_pattern_space_with_a_base_adds_one_component() {
215 let cs = PatternSpace {
216 base: Some(Box::new(ColorSpace::DeviceCmyk)),
217 };
218 assert_eq!(cs.n_components(), 5);
219 assert!(cs.to_rgb(&[0.0, 0.0, 0.0, 0.0]).is_some());
220 }
221}