1use std::collections::HashMap;
11
12use lopdf::{Dictionary, Document, Object};
13
14use super::function::Function;
15use super::objects::{as_dict, as_stream, deref, get, get_int, name, nums, resource};
16
17#[derive(Debug, Clone)]
18pub enum ColorSpace {
19 DeviceGray,
20 DeviceRGB,
21 DeviceCMYK,
22 Lab {
24 wp: [f64; 3],
25 range: [f64; 4],
26 },
27 Indexed {
28 base: Box<ColorSpace>,
29 hival: usize,
30 lookup: Vec<u8>,
31 },
32 Separation {
34 n: usize,
35 alt: Box<ColorSpace>,
36 tint: Option<Function>,
37 is_none: bool,
39 is_all: bool,
41 },
42 Pattern(Option<Box<ColorSpace>>),
45}
46
47impl ColorSpace {
48 pub fn components(&self) -> usize {
49 match self {
50 ColorSpace::DeviceGray => 1,
51 ColorSpace::DeviceRGB => 3,
52 ColorSpace::DeviceCMYK => 4,
53 ColorSpace::Lab { .. } => 3,
54 ColorSpace::Indexed { .. } => 1,
55 ColorSpace::Separation { n, .. } => *n,
56 ColorSpace::Pattern(_) => 1,
57 }
58 }
59
60 pub fn initial(&self) -> Vec<f64> {
63 match self {
64 ColorSpace::DeviceCMYK => vec![0.0, 0.0, 0.0, 1.0],
65 ColorSpace::Separation { n, .. } => vec![1.0; *n],
66 ColorSpace::Lab { .. } => vec![0.0, 0.0, 0.0],
67 ColorSpace::Indexed { .. } => vec![0.0],
68 other => vec![0.0; other.components()],
69 }
70 }
71
72 pub fn default_decode(&self, bpc: u32) -> Vec<f64> {
76 match self {
77 ColorSpace::Indexed { .. } => vec![0.0, ((1u64 << bpc) - 1) as f64],
78 ColorSpace::Lab { range, .. } => {
79 vec![0.0, 100.0, range[0], range[1], range[2], range[3]]
80 }
81 other => (0..other.components()).flat_map(|_| [0.0, 1.0]).collect(),
82 }
83 }
84
85 pub fn parse(doc: &Document, obj: &Object, res: Option<&Dictionary>) -> Option<ColorSpace> {
88 Self::parse_depth(doc, obj, res, 0)
89 }
90
91 fn parse_depth(
92 doc: &Document,
93 obj: &Object,
94 res: Option<&Dictionary>,
95 depth: usize,
96 ) -> Option<ColorSpace> {
97 if depth > 6 {
98 return None;
99 }
100 match deref(doc, obj) {
101 Object::Name(n) => match n.as_slice() {
102 b"DeviceGray" | b"G" | b"CalGray" => Some(ColorSpace::DeviceGray),
103 b"DeviceRGB" | b"RGB" | b"CalRGB" => Some(ColorSpace::DeviceRGB),
104 b"DeviceCMYK" | b"CMYK" => Some(ColorSpace::DeviceCMYK),
105 b"Pattern" => Some(ColorSpace::Pattern(None)),
106 b"Indexed" | b"I" => None,
107 other => {
108 let o = resource(doc, res, b"ColorSpace", other)?;
109 Self::parse_depth(doc, o, None, depth + 1)
112 }
113 },
114 Object::Array(a) => {
115 let fam = a.first().map(|o| deref(doc, o)).and_then(name)?;
116 match fam {
117 b"DeviceGray" | b"G" | b"CalGray" => Some(ColorSpace::DeviceGray),
118 b"DeviceRGB" | b"RGB" | b"CalRGB" => Some(ColorSpace::DeviceRGB),
119 b"DeviceCMYK" | b"CMYK" => Some(ColorSpace::DeviceCMYK),
120 b"Lab" => {
121 let d = a.get(1).and_then(|o| as_dict(doc, o));
122 let wp = d
123 .and_then(|d| get(doc, d, b"WhitePoint"))
124 .and_then(|o| nums(doc, o))
125 .filter(|v| v.len() == 3)
126 .map(|v| [v[0], v[1], v[2]])
127 .unwrap_or([0.9505, 1.0, 1.089]);
128 let range = d
129 .and_then(|d| get(doc, d, b"Range"))
130 .and_then(|o| nums(doc, o))
131 .filter(|v| v.len() == 4)
132 .map(|v| [v[0], v[1], v[2], v[3]])
133 .unwrap_or([-100.0, 100.0, -100.0, 100.0]);
134 Some(ColorSpace::Lab { wp, range })
135 }
136 b"ICCBased" => {
137 let s = a.get(1).and_then(|o| as_stream(doc, o));
138 let n = s.and_then(|s| get_int(doc, &s.dict, b"N"));
139 match n {
140 Some(1) => Some(ColorSpace::DeviceGray),
141 Some(4) => Some(ColorSpace::DeviceCMYK),
142 Some(3) => Some(ColorSpace::DeviceRGB),
143 _ => s
144 .and_then(|s| s.dict.get(b"Alternate").ok())
145 .and_then(|o| Self::parse_depth(doc, o, res, depth + 1))
146 .or(Some(ColorSpace::DeviceRGB)),
147 }
148 }
149 b"Indexed" | b"I" => {
150 if a.len() < 4 {
151 return None;
152 }
153 let base = Self::parse_depth(doc, &a[1], res, depth + 1)?;
154 let hival = deref(doc, &a[2]).as_i64().unwrap_or(0).clamp(0, 255) as usize;
155 let lookup = match deref(doc, &a[3]) {
156 Object::String(s, _) => s.clone(),
157 Object::Stream(s) => s.decompressed_content().ok()?,
158 _ => return None,
159 };
160 Some(ColorSpace::Indexed {
161 base: Box::new(base),
162 hival,
163 lookup,
164 })
165 }
166 b"Separation" | b"DeviceN" => {
167 let (n, names): (usize, Vec<Vec<u8>>) = match deref(doc, a.get(1)?) {
168 Object::Name(nm) => (1, vec![nm.clone()]),
169 Object::Array(arr) => (
170 arr.len().max(1),
171 arr.iter()
172 .filter_map(|o| name(deref(doc, o)).map(|n| n.to_vec()))
173 .collect(),
174 ),
175 _ => (1, Vec::new()),
176 };
177 let alt = a
178 .get(2)
179 .and_then(|o| Self::parse_depth(doc, o, res, depth + 1))
180 .unwrap_or(ColorSpace::DeviceGray);
181 let tint = a.get(3).and_then(|o| Function::parse(doc, o));
182 let is_none =
183 fam == b"Separation" && names.first().is_some_and(|n| n == b"None");
184 let is_all =
185 fam == b"Separation" && names.first().is_some_and(|n| n == b"All");
186 Some(ColorSpace::Separation {
187 n,
188 alt: Box::new(alt),
189 tint,
190 is_none,
191 is_all,
192 })
193 }
194 b"Pattern" => {
195 let base = a
196 .get(1)
197 .and_then(|o| Self::parse_depth(doc, o, res, depth + 1))
198 .map(Box::new);
199 Some(ColorSpace::Pattern(base))
200 }
201 b"DeviceRGBA" => Some(ColorSpace::DeviceRGB),
202 _ => None,
203 }
204 }
205 _ => None,
206 }
207 }
208
209 pub fn to_rgb(&self, v: &[f64]) -> Option<[f64; 3]> {
212 let c = |i: usize| v.get(i).copied().unwrap_or(0.0).clamp(0.0, 1.0);
213 match self {
214 ColorSpace::DeviceGray => {
215 let g = c(0);
216 Some([g, g, g])
217 }
218 ColorSpace::DeviceRGB => Some([c(0), c(1), c(2)]),
219 ColorSpace::DeviceCMYK => Some(cmyk_to_rgb(c(0), c(1), c(2), c(3))),
220 ColorSpace::Lab { wp, range } => {
221 let l = v.first().copied().unwrap_or(0.0).clamp(0.0, 100.0);
222 let a = v.get(1).copied().unwrap_or(0.0).clamp(range[0], range[1]);
223 let b = v.get(2).copied().unwrap_or(0.0).clamp(range[2], range[3]);
224 Some(lab_to_rgb(l, a, b, wp))
225 }
226 ColorSpace::Indexed {
227 base,
228 hival,
229 lookup,
230 } => {
231 let idx = v.first().copied().unwrap_or(0.0).round().max(0.0) as usize;
232 let idx = idx.min(*hival);
233 let n = base.components();
234 let comps: Vec<f64> = (0..n)
235 .map(|i| f64::from(*lookup.get(idx * n + i).unwrap_or(&0)) / 255.0)
236 .collect();
237 let comps = match &**base {
239 ColorSpace::Lab { range, .. } => vec![
240 comps[0] * 100.0,
241 range[0] + comps.get(1).copied().unwrap_or(0.0) * (range[1] - range[0]),
242 range[2] + comps.get(2).copied().unwrap_or(0.0) * (range[3] - range[2]),
243 ],
244 _ => comps,
245 };
246 base.to_rgb(&comps)
247 }
248 ColorSpace::Separation {
249 n,
250 alt,
251 tint,
252 is_none,
253 is_all,
254 } => {
255 if *is_none {
256 return None;
257 }
258 if *is_all {
259 let g = 1.0 - c(0);
260 return Some([g, g, g]);
261 }
262 match tint {
263 Some(f) => {
264 let inputs: Vec<f64> = (0..*n).map(c).collect();
265 let out = f.eval(&inputs);
266 alt.to_rgb(&out)
267 }
268 None => {
271 let g = 1.0 - c(0);
272 Some([g, g, g])
273 }
274 }
275 }
276 ColorSpace::Pattern(_) => Some([0.0, 0.0, 0.0]),
277 }
278 }
279}
280
281fn lab_to_rgb(l: f64, a: f64, b: f64, wp: &[f64; 3]) -> [f64; 3] {
283 let m = (l + 16.0) / 116.0;
284 let ll = m + a / 500.0;
285 let n = m - b / 200.0;
286 let g = |x: f64| {
287 if x >= 6.0 / 29.0 {
288 x * x * x
289 } else {
290 108.0 / 841.0 * (x - 4.0 / 29.0)
291 }
292 };
293 let x = wp[0] * g(ll);
294 let y = wp[1] * g(m);
295 let z = wp[2] * g(n);
296 let r = 3.2406 * x - 1.5372 * y - 0.4986 * z;
298 let gg = -0.9689 * x + 1.8758 * y + 0.0415 * z;
299 let bb = 0.0557 * x - 0.2040 * y + 1.0570 * z;
300 [linear_to_srgb(r), linear_to_srgb(gg), linear_to_srgb(bb)]
301}
302
303fn srgb_to_linear(v: f64) -> f64 {
304 if v <= 0.04045 {
305 v / 12.92
306 } else {
307 ((v + 0.055) / 1.055).powf(2.4)
308 }
309}
310
311fn linear_to_srgb(v: f64) -> f64 {
312 let v = v.clamp(0.0, 1.0);
313 if v <= 0.0031308 {
314 12.92 * v
315 } else {
316 1.055 * v.powf(1.0 / 2.4) - 0.055
317 }
318}
319
320struct CmykModel {
321 encoded_positive: [[f64; 3]; 8],
322 negative: [[f64; 3]; 8],
323 yule_nielsen: [f64; 3],
324 ink_tone: [f64; 3],
325 black: [[f64; 3]; 9],
326}
327
328fn cmyk_model() -> &'static CmykModel {
329 static MODEL: std::sync::OnceLock<CmykModel> = std::sync::OnceLock::new();
330 MODEL.get_or_init(|| {
331 let yule_nielsen = [1.553623, 1.348579, 1.536033];
332 let corner: [[f64; 3]; 8] = [
333 [1.000000, 1.000000, 1.000000], [-0.179803, 0.419937, 0.868289], [0.869084, -0.001406, 0.268871], [0.039807, 0.023510, 0.277328], [1.052223, 0.853703, 0.007227], [-0.074325, 0.403406, 0.078689], [0.879638, 0.012292, 0.011953], [0.043065, 0.031199, 0.032550], ];
342 let mut encoded_positive = [[0.0; 3]; 8];
343 let mut negative = [[0.0; 3]; 8];
344 for i in 0..8 {
345 for j in 0..3 {
346 let v = corner[i][j];
347 encoded_positive[i][j] = v.max(0.0).powf(1.0 / yule_nielsen[j]);
348 negative[i][j] = v.min(0.0);
349 }
350 }
351 let ramp: [[f64; 3]; 9] = [
352 [255.0, 255.0, 255.0],
353 [225.0, 226.0, 228.0],
354 [199.0, 200.0, 202.0],
355 [173.0, 174.0, 178.0],
356 [147.0, 149.0, 152.0],
357 [123.0, 125.0, 128.0],
358 [99.0, 99.0, 102.0],
359 [69.0, 70.0, 71.0],
360 [35.0, 31.0, 32.0],
361 ];
362 let mut black = [[0.0; 3]; 9];
363 for i in 0..9 {
364 for j in 0..3 {
365 black[i][j] = srgb_to_linear(ramp[i][j] / 255.0);
366 }
367 }
368 CmykModel {
369 encoded_positive,
370 negative,
371 yule_nielsen,
372 ink_tone: [1.176647, 1.212366, 1.145622],
373 black,
374 }
375 })
376}
377
378pub fn cmyk_to_rgb(c: f64, m: f64, y: f64, k: f64) -> [f64; 3] {
380 let model = cmyk_model();
381 let clamp = |v: f64| v.clamp(0.0, 1.0);
382 let area = [
383 clamp(c).powf(1.0 / model.ink_tone[0]),
384 clamp(m).powf(1.0 / model.ink_tone[1]),
385 clamp(y).powf(1.0 / model.ink_tone[2]),
386 ];
387 let mut weight = [0.0; 8];
388 for (i, w) in weight.iter_mut().enumerate() {
389 *w = (if i & 1 != 0 { area[0] } else { 1.0 - area[0] })
390 * (if i & 2 != 0 { area[1] } else { 1.0 - area[1] })
391 * (if i & 4 != 0 { area[2] } else { 1.0 - area[2] });
392 }
393 let kx = clamp(k) * 8.0;
394 let k0 = (kx as usize).min(7);
395 let kf = kx - k0 as f64;
396 let mut out = [0.0; 3];
397 for (j, o) in out.iter_mut().enumerate() {
398 let mut mixed = 0.0;
399 let mut offset = 0.0;
400 for (i, wt) in weight.iter().enumerate() {
401 mixed += wt * model.encoded_positive[i][j];
402 offset += wt * model.negative[i][j];
403 }
404 let linear = mixed.max(0.0).powf(model.yule_nielsen[j]) + offset;
405 let transmittance = model.black[k0][j] * (1.0 - kf) + model.black[k0 + 1][j] * kf;
406 *o = linear_to_srgb(linear * transmittance);
407 }
408 out
409}
410
411#[derive(Default)]
414pub struct CmykCache {
415 map: HashMap<u32, [u8; 3]>,
416}
417
418impl CmykCache {
419 pub fn rgb8(&mut self, c: u8, m: u8, y: u8, k: u8) -> [u8; 3] {
420 let key = u32::from_be_bytes([c, m, y, k]);
421 if let Some(v) = self.map.get(&key) {
422 return *v;
423 }
424 let rgb = cmyk_to_rgb(
425 f64::from(c) / 255.0,
426 f64::from(m) / 255.0,
427 f64::from(y) / 255.0,
428 f64::from(k) / 255.0,
429 );
430 let out = [to_u8(rgb[0]), to_u8(rgb[1]), to_u8(rgb[2])];
431 if self.map.len() < 1 << 16 {
432 self.map.insert(key, out);
433 }
434 out
435 }
436}
437
438pub fn to_u8(v: f64) -> u8 {
440 (v.clamp(0.0, 1.0) * 255.0).round() as u8
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn cmyk_black_is_docling_parses_warm_black() {
449 let k = cmyk_to_rgb(0.0, 0.0, 0.0, 1.0);
450 assert_eq!([to_u8(k[0]), to_u8(k[1]), to_u8(k[2])], [35, 31, 32]);
451 let w = cmyk_to_rgb(0.0, 0.0, 0.0, 0.0);
452 assert_eq!([to_u8(w[0]), to_u8(w[1]), to_u8(w[2])], [255, 255, 255]);
453 let c = cmyk_to_rgb(1.0, 0.0, 0.0, 0.0);
454 assert!(c[2] > c[0], "cyan is blue-ish: {c:?}");
455 }
456
457 #[test]
458 fn lab_white_and_black() {
459 let w = lab_to_rgb(100.0, 0.0, 0.0, &[0.9505, 1.0, 1.089]);
460 assert!(w.iter().all(|v| *v > 0.97), "{w:?}");
461 let k = lab_to_rgb(0.0, 0.0, 0.0, &[0.9505, 1.0, 1.089]);
462 assert!(k.iter().all(|v| *v < 0.02), "{k:?}");
463 }
464}