1use crate::graphics::Color;
8use crate::objects::{Dictionary, Object};
9
10#[derive(Debug, Clone)]
12pub struct SeparationColorSpace {
13 pub colorant_name: String,
15 pub alternate_space: AlternateColorSpace,
17 pub tint_transform: TintTransform,
19}
20
21#[derive(Debug, Clone)]
23pub enum AlternateColorSpace {
24 DeviceGray,
26 DeviceRGB,
28 DeviceCMYK,
30 Lab {
32 white_point: [f64; 3],
33 black_point: [f64; 3],
34 range: [f64; 4],
35 },
36}
37
38impl AlternateColorSpace {
39 pub fn to_pdf_object(&self) -> Object {
41 match self {
42 AlternateColorSpace::DeviceGray => Object::Name("DeviceGray".to_string()),
43 AlternateColorSpace::DeviceRGB => Object::Name("DeviceRGB".to_string()),
44 AlternateColorSpace::DeviceCMYK => Object::Name("DeviceCMYK".to_string()),
45 AlternateColorSpace::Lab {
46 white_point,
47 black_point,
48 range,
49 } => {
50 let mut dict = Dictionary::new();
51 dict.set(
52 "WhitePoint",
53 Object::Array(white_point.iter().map(|&v| Object::Real(v)).collect()),
54 );
55 dict.set(
56 "BlackPoint",
57 Object::Array(black_point.iter().map(|&v| Object::Real(v)).collect()),
58 );
59 dict.set(
60 "Range",
61 Object::Array(range.iter().map(|&v| Object::Real(v)).collect()),
62 );
63
64 Object::Array(vec![
65 Object::Name("Lab".to_string()),
66 Object::Dictionary(dict),
67 ])
68 }
69 }
70 }
71
72 pub fn num_components(&self) -> usize {
74 match self {
75 AlternateColorSpace::DeviceGray => 1,
76 AlternateColorSpace::DeviceRGB => 3,
77 AlternateColorSpace::DeviceCMYK => 4,
78 AlternateColorSpace::Lab { .. } => 3,
79 }
80 }
81}
82
83#[derive(Debug, Clone)]
85pub enum TintTransform {
86 Linear {
88 min_values: Vec<f64>,
89 max_values: Vec<f64>,
90 },
91 Exponential {
93 gamma: f64,
94 min_values: Vec<f64>,
95 max_values: Vec<f64>,
96 },
97 Custom {
99 domain: [f64; 2],
100 range: Vec<f64>,
101 function_type: u8,
102 function_data: Vec<u8>,
103 },
104 Sampled {
106 samples: Vec<Vec<f64>>,
107 domain: [f64; 2],
108 range: Vec<f64>,
109 },
110}
111
112impl TintTransform {
113 pub fn linear(min_values: Vec<f64>, max_values: Vec<f64>) -> Self {
115 TintTransform::Linear {
116 min_values,
117 max_values,
118 }
119 }
120
121 pub fn exponential(gamma: f64, min_values: Vec<f64>, max_values: Vec<f64>) -> Self {
123 TintTransform::Exponential {
124 gamma,
125 min_values,
126 max_values,
127 }
128 }
129
130 pub fn apply(&self, tint: f64) -> Vec<f64> {
132 let tint = tint.clamp(0.0, 1.0);
133
134 match self {
135 TintTransform::Linear {
136 min_values,
137 max_values,
138 } => min_values
139 .iter()
140 .zip(max_values.iter())
141 .map(|(&min, &max)| min + tint * (max - min))
142 .collect(),
143 TintTransform::Exponential {
144 gamma,
145 min_values,
146 max_values,
147 } => {
148 let t = tint.powf(*gamma);
149 min_values
150 .iter()
151 .zip(max_values.iter())
152 .map(|(&min, &max)| min + t * (max - min))
153 .collect()
154 }
155 TintTransform::Sampled { samples, .. } => {
156 if samples.is_empty() {
158 return vec![];
159 }
160
161 let index = (tint * (samples.len() - 1) as f64) as usize;
162 let index = index.min(samples.len() - 1);
163 samples[index].clone()
164 }
165 TintTransform::Custom { .. } => {
166 vec![tint]
168 }
169 }
170 }
171
172 pub fn to_pdf_dict(&self) -> Dictionary {
174 let mut dict = Dictionary::new();
175
176 match self {
177 TintTransform::Linear {
178 min_values,
179 max_values,
180 } => {
181 dict.set("FunctionType", Object::Integer(2));
182 dict.set(
183 "Domain",
184 Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
185 );
186 dict.set(
187 "C0",
188 Object::Array(min_values.iter().map(|&v| Object::Real(v)).collect()),
189 );
190 dict.set(
191 "C1",
192 Object::Array(max_values.iter().map(|&v| Object::Real(v)).collect()),
193 );
194 dict.set("N", Object::Real(1.0));
195 }
196 TintTransform::Exponential {
197 gamma,
198 min_values,
199 max_values,
200 } => {
201 dict.set("FunctionType", Object::Integer(2));
202 dict.set(
203 "Domain",
204 Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
205 );
206 dict.set(
207 "C0",
208 Object::Array(min_values.iter().map(|&v| Object::Real(v)).collect()),
209 );
210 dict.set(
211 "C1",
212 Object::Array(max_values.iter().map(|&v| Object::Real(v)).collect()),
213 );
214 dict.set("N", Object::Real(*gamma));
215 }
216 TintTransform::Sampled {
217 samples,
218 domain,
219 range,
220 } => {
221 dict.set("FunctionType", Object::Integer(0));
222 dict.set(
223 "Domain",
224 Object::Array(vec![Object::Real(domain[0]), Object::Real(domain[1])]),
225 );
226 dict.set(
227 "Range",
228 Object::Array(range.iter().map(|&v| Object::Real(v)).collect()),
229 );
230 dict.set(
231 "Size",
232 Object::Array(vec![Object::Integer(samples.len() as i64)]),
233 );
234 dict.set("BitsPerSample", Object::Integer(8));
235
236 let mut data = Vec::new();
238 for sample in samples {
239 for &value in sample {
240 data.push((value * 255.0) as u8);
241 }
242 }
243 dict.set("Length", Object::Integer(data.len() as i64));
245 }
246 TintTransform::Custom {
247 domain,
248 range,
249 function_type,
250 ..
251 } => {
252 dict.set("FunctionType", Object::Integer(*function_type as i64));
253 dict.set(
254 "Domain",
255 Object::Array(vec![Object::Real(domain[0]), Object::Real(domain[1])]),
256 );
257 dict.set(
258 "Range",
259 Object::Array(range.iter().map(|&v| Object::Real(v)).collect()),
260 );
261 }
262 }
263
264 dict
265 }
266}
267
268impl SeparationColorSpace {
269 pub fn new(
271 colorant_name: impl Into<String>,
272 alternate_space: AlternateColorSpace,
273 tint_transform: TintTransform,
274 ) -> Self {
275 Self {
276 colorant_name: colorant_name.into(),
277 alternate_space,
278 tint_transform,
279 }
280 }
281
282 pub fn rgb_separation(colorant_name: impl Into<String>, r: f64, g: f64, b: f64) -> Self {
284 Self::new(
285 colorant_name,
286 AlternateColorSpace::DeviceRGB,
287 TintTransform::linear(vec![1.0, 1.0, 1.0], vec![r, g, b]),
288 )
289 }
290
291 pub fn cmyk_separation(
293 colorant_name: impl Into<String>,
294 c: f64,
295 m: f64,
296 y: f64,
297 k: f64,
298 ) -> Self {
299 Self::new(
300 colorant_name,
301 AlternateColorSpace::DeviceCMYK,
302 TintTransform::linear(vec![0.0, 0.0, 0.0, 0.0], vec![c, m, y, k]),
303 )
304 }
305
306 pub fn to_pdf_array(&self) -> Vec<Object> {
308 vec![
309 Object::Name("Separation".to_string()),
310 Object::Name(self.colorant_name.clone()),
311 self.alternate_space.to_pdf_object(),
312 Object::Dictionary(self.tint_transform.to_pdf_dict()),
313 ]
314 }
315
316 pub fn apply_tint(&self, tint: f64) -> Vec<f64> {
318 self.tint_transform.apply(tint)
319 }
320
321 pub fn tint_to_rgb(&self, tint: f64) -> Color {
323 let values = self.apply_tint(tint);
324
325 match &self.alternate_space {
326 AlternateColorSpace::DeviceGray => {
327 let gray = values.first().copied().unwrap_or(0.0);
328 Color::rgb(gray, gray, gray)
329 }
330 AlternateColorSpace::DeviceRGB => Color::rgb(
331 values.first().copied().unwrap_or(0.0),
332 values.get(1).copied().unwrap_or(0.0),
333 values.get(2).copied().unwrap_or(0.0),
334 ),
335 AlternateColorSpace::DeviceCMYK => {
336 let c = values.first().copied().unwrap_or(0.0);
338 let m = values.get(1).copied().unwrap_or(0.0);
339 let y = values.get(2).copied().unwrap_or(0.0);
340 let k = values.get(3).copied().unwrap_or(0.0);
341
342 Color::rgb(
343 (1.0 - c) * (1.0 - k),
344 (1.0 - m) * (1.0 - k),
345 (1.0 - y) * (1.0 - k),
346 )
347 }
348 AlternateColorSpace::Lab { .. } => {
349 Color::rgb(
351 values.first().copied().unwrap_or(0.0) / 100.0,
352 (values.get(1).copied().unwrap_or(0.0) + 128.0) / 255.0,
353 (values.get(2).copied().unwrap_or(0.0) + 128.0) / 255.0,
354 )
355 }
356 }
357 }
358}
359
360pub struct SpotColors;
362
363impl SpotColors {
364 pub fn pantone_185c() -> SeparationColorSpace {
366 SeparationColorSpace::cmyk_separation("PANTONE 185 C", 0.0, 0.91, 0.76, 0.0)
367 }
368
369 pub fn pantone_286c() -> SeparationColorSpace {
371 SeparationColorSpace::cmyk_separation("PANTONE 286 C", 1.0, 0.66, 0.0, 0.0)
372 }
373
374 pub fn pantone_376c() -> SeparationColorSpace {
376 SeparationColorSpace::cmyk_separation("PANTONE 376 C", 0.5, 0.0, 1.0, 0.0)
377 }
378
379 pub fn gold() -> SeparationColorSpace {
381 SeparationColorSpace::rgb_separation("Gold", 1.0, 0.843, 0.0)
382 }
383
384 pub fn silver() -> SeparationColorSpace {
386 SeparationColorSpace::rgb_separation("Silver", 0.753, 0.753, 0.753)
387 }
388
389 pub fn varnish() -> SeparationColorSpace {
391 SeparationColorSpace::new(
392 "Varnish",
393 AlternateColorSpace::DeviceGray,
394 TintTransform::linear(vec![1.0], vec![0.9]),
395 )
396 }
397}
398
399#[derive(Debug, Clone)]
401pub struct SeparationColor {
402 pub color_space: SeparationColorSpace,
404 pub tint: f64,
406}
407
408impl SeparationColor {
409 pub fn new(color_space: SeparationColorSpace, tint: f64) -> Self {
411 Self {
412 color_space,
413 tint: tint.clamp(0.0, 1.0),
414 }
415 }
416
417 pub fn get_alternate_values(&self) -> Vec<f64> {
419 self.color_space.apply_tint(self.tint)
420 }
421
422 pub fn to_rgb(&self) -> Color {
424 self.color_space.tint_to_rgb(self.tint)
425 }
426
427 pub fn colorant_name(&self) -> &str {
429 &self.color_space.colorant_name
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn test_separation_color_space_creation() {
439 let sep = SeparationColorSpace::new(
440 "MySpotColor",
441 AlternateColorSpace::DeviceRGB,
442 TintTransform::linear(vec![1.0, 1.0, 1.0], vec![1.0, 0.0, 0.0]),
443 );
444
445 assert_eq!(sep.colorant_name, "MySpotColor");
446 assert!(matches!(
447 sep.alternate_space,
448 AlternateColorSpace::DeviceRGB
449 ));
450 }
451
452 #[test]
453 fn test_rgb_separation() {
454 let sep = SeparationColorSpace::rgb_separation("Red", 1.0, 0.0, 0.0);
455
456 assert_eq!(sep.colorant_name, "Red");
457 let values = sep.apply_tint(1.0);
458 assert_eq!(values, vec![1.0, 0.0, 0.0]);
459
460 let values_half = sep.apply_tint(0.5);
461 assert_eq!(values_half, vec![1.0, 0.5, 0.5]);
462 }
463
464 #[test]
465 fn test_cmyk_separation() {
466 let sep = SeparationColorSpace::cmyk_separation("Cyan", 1.0, 0.0, 0.0, 0.0);
467
468 assert_eq!(sep.colorant_name, "Cyan");
469 let values = sep.apply_tint(1.0);
470 assert_eq!(values, vec![1.0, 0.0, 0.0, 0.0]);
471 }
472
473 #[test]
474 fn test_tint_transform_linear() {
475 let transform = TintTransform::linear(vec![0.0, 0.0, 0.0], vec![1.0, 0.5, 0.25]);
476
477 let values = transform.apply(0.0);
478 assert_eq!(values, vec![0.0, 0.0, 0.0]);
479
480 let values = transform.apply(1.0);
481 assert_eq!(values, vec![1.0, 0.5, 0.25]);
482
483 let values = transform.apply(0.5);
484 assert_eq!(values, vec![0.5, 0.25, 0.125]);
485 }
486
487 #[test]
488 fn test_tint_transform_exponential() {
489 let transform = TintTransform::exponential(2.0, vec![0.0], vec![1.0]);
490
491 let values = transform.apply(0.5);
492 assert_eq!(values[0], 0.25); }
494
495 #[test]
496 fn test_alternate_color_space_components() {
497 assert_eq!(AlternateColorSpace::DeviceGray.num_components(), 1);
498 assert_eq!(AlternateColorSpace::DeviceRGB.num_components(), 3);
499 assert_eq!(AlternateColorSpace::DeviceCMYK.num_components(), 4);
500
501 let lab = AlternateColorSpace::Lab {
502 white_point: [0.95, 1.0, 1.09],
503 black_point: [0.0, 0.0, 0.0],
504 range: [-100.0, 100.0, -100.0, 100.0],
505 };
506 assert_eq!(lab.num_components(), 3);
507 }
508
509 #[test]
510 fn test_separation_to_pdf_array() {
511 let sep = SeparationColorSpace::rgb_separation("TestColor", 0.5, 0.5, 1.0);
512 let pdf_array = sep.to_pdf_array();
513
514 assert_eq!(pdf_array.len(), 4);
515 assert_eq!(pdf_array[0], Object::Name("Separation".to_string()));
516 assert_eq!(pdf_array[1], Object::Name("TestColor".to_string()));
517 }
518
519 #[test]
520 fn test_tint_to_rgb() {
521 let sep = SeparationColorSpace::rgb_separation("Purple", 0.5, 0.0, 0.5);
522 let color = sep.tint_to_rgb(1.0);
523
524 assert_eq!(color.r(), 0.5);
525 assert_eq!(color.g(), 0.0);
526 assert_eq!(color.b(), 0.5);
527 }
528
529 #[test]
530 fn test_spot_colors() {
531 let pantone_red = SpotColors::pantone_185c();
532 assert_eq!(pantone_red.colorant_name, "PANTONE 185 C");
533
534 let gold = SpotColors::gold();
535 assert_eq!(gold.colorant_name, "Gold");
536
537 let varnish = SpotColors::varnish();
538 assert_eq!(varnish.colorant_name, "Varnish");
539 }
540
541 #[test]
542 fn test_separation_color() {
543 let color_space = SeparationColorSpace::rgb_separation("Blue", 0.0, 0.0, 1.0);
544 let color = SeparationColor::new(color_space, 0.75);
545
546 assert_eq!(color.tint, 0.75);
547 assert_eq!(color.colorant_name(), "Blue");
548
549 let alt_values = color.get_alternate_values();
550 assert_eq!(alt_values[0], 0.25); assert_eq!(alt_values[1], 0.25); assert_eq!(alt_values[2], 1.0); }
554
555 #[test]
556 fn test_tint_clamping() {
557 let color_space = SeparationColorSpace::rgb_separation("Test", 1.0, 0.0, 0.0);
558 let color = SeparationColor::new(color_space, 1.5); assert_eq!(color.tint, 1.0);
561
562 let color2 = SeparationColor::new(
563 SeparationColorSpace::rgb_separation("Test2", 1.0, 0.0, 0.0),
564 -0.5, );
566 assert_eq!(color2.tint, 0.0);
567 }
568}