1use crate::error::PdfError;
8use crate::objects::{PdfDict, PdfObj};
9use crate::resolver::Resolver;
10
11const MAX_FUNCTION_DEPTH: u32 = 32;
17
18#[derive(Clone, Debug)]
20pub enum PdfFunction {
21 Sampled {
23 domain: Vec<[f64; 2]>,
24 range: Vec<[f64; 2]>,
25 size: Vec<u32>,
26 bps: u32,
27 encode: Vec<[f64; 2]>,
28 decode: Vec<[f64; 2]>,
29 samples: Vec<f64>,
30 n_outputs: usize,
31 },
32 Exponential {
34 domain: Vec<[f64; 2]>,
35 range: Vec<[f64; 2]>,
36 c0: Vec<f64>,
37 c1: Vec<f64>,
38 n: f64,
39 },
40 Stitching {
42 domain: Vec<[f64; 2]>,
43 range: Vec<[f64; 2]>,
44 functions: Vec<PdfFunction>,
45 bounds: Vec<f64>,
46 encode: Vec<[f64; 2]>,
47 },
48 Calculator {
50 domain: Vec<[f64; 2]>,
51 range: Vec<[f64; 2]>,
52 tokens: Vec<CalcToken>,
53 },
54 Composite { functions: Vec<PdfFunction> },
57}
58
59#[derive(Clone, Debug)]
61pub enum CalcToken {
62 Number(f64),
63 Bool(bool),
64 Add,
66 Sub,
67 Mul,
68 Div,
69 Idiv,
70 Mod,
71 Neg,
72 Abs,
73 Ceiling,
74 Floor,
75 Round,
76 Truncate,
77 Sqrt,
78 Exp,
79 Ln,
80 Log,
81 Sin,
82 Cos,
83 Atan,
84 Eq,
86 Ne,
87 Gt,
88 Ge,
89 Lt,
90 Le,
91 And,
92 Or,
93 Xor,
94 Not,
95 Bitshift,
96 Dup,
98 Exch,
99 Pop,
100 Copy,
101 Index,
102 Roll,
103 If(Vec<CalcToken>),
105 IfElse(Vec<CalcToken>, Vec<CalcToken>),
106 Cvi,
108 Cvr,
109 True,
110 False,
111}
112
113impl PdfFunction {
114 pub fn parse(obj: &PdfObj, resolver: &Resolver) -> Result<Self, PdfError> {
116 Self::parse_guarded(obj, resolver, &mut Vec::new(), 0)
117 }
118
119 fn parse_guarded(
132 obj: &PdfObj,
133 resolver: &Resolver,
134 active: &mut Vec<u32>,
135 depth: u32,
136 ) -> Result<Self, PdfError> {
137 if depth >= MAX_FUNCTION_DEPTH {
138 return Err(PdfError::NestingTooDeep {
139 context: "function",
140 limit: MAX_FUNCTION_DEPTH,
141 });
142 }
143 if let PdfObj::Ref(num, gen_num) = obj {
144 if active.contains(num) {
145 return Err(PdfError::CircularReference(*num, *gen_num));
146 }
147 active.push(*num);
148 let result = Self::parse_resolved(obj, resolver, active, depth);
149 active.pop();
150 return result;
151 }
152 Self::parse_resolved(obj, resolver, active, depth)
153 }
154
155 fn parse_resolved(
157 obj: &PdfObj,
158 resolver: &Resolver,
159 active: &mut Vec<u32>,
160 depth: u32,
161 ) -> Result<Self, PdfError> {
162 let resolved = resolver.deref(obj)?;
163 let dict = resolved
164 .as_dict()
165 .ok_or(PdfError::Other("function is not a dict/stream".into()))?;
166
167 let fn_type =
168 dict.get_int(b"FunctionType")
169 .ok_or(PdfError::Other("function missing FunctionType".into()))? as i32;
170
171 let domain = parse_domain_range(dict, b"Domain")?;
172 let range = parse_domain_range(dict, b"Range").unwrap_or_default();
173
174 match fn_type {
175 0 => Self::parse_sampled(dict, obj, domain, range, resolver),
176 2 => Self::parse_exponential(dict, domain, range),
177 3 => Self::parse_stitching(dict, domain, range, resolver, active, depth),
178 4 => Self::parse_calculator(obj, domain, range, resolver),
179 _ => Err(PdfError::Other(format!(
180 "unsupported function type {fn_type}"
181 ))),
182 }
183 }
184
185 pub fn evaluate(&self, inputs: &[f64]) -> Vec<f64> {
187 match self {
188 Self::Sampled {
189 domain,
190 range,
191 size,
192 encode,
193 decode,
194 samples,
195 n_outputs,
196 ..
197 } => evaluate_sampled(
198 inputs, domain, range, size, encode, decode, samples, *n_outputs,
199 ),
200 Self::Exponential {
201 domain,
202 range,
203 c0,
204 c1,
205 n,
206 } => evaluate_exponential(inputs, domain, range, c0, c1, *n),
207 Self::Stitching {
208 domain,
209 range,
210 functions,
211 bounds,
212 encode,
213 } => evaluate_stitching(inputs, domain, range, functions, bounds, encode),
214 Self::Calculator {
215 domain,
216 range,
217 tokens,
218 } => evaluate_calculator(inputs, domain, range, tokens),
219 Self::Composite { functions } => {
220 let mut result = Vec::new();
221 for f in functions {
222 result.extend(f.evaluate(inputs));
223 }
224 result
225 }
226 }
227 }
228
229 pub fn composite(functions: Vec<PdfFunction>) -> Self {
231 Self::Composite { functions }
232 }
233
234 pub fn domain_0(&self) -> [f64; 2] {
236 let d = match self {
237 Self::Sampled { domain, .. }
238 | Self::Exponential { domain, .. }
239 | Self::Stitching { domain, .. }
240 | Self::Calculator { domain, .. } => domain,
241 Self::Composite { functions } => {
242 return functions.first().map_or([0.0, 1.0], |f| f.domain_0());
243 }
244 };
245 d.first().copied().unwrap_or([0.0, 1.0])
246 }
247
248 pub fn n_outputs(&self) -> usize {
250 match self {
251 Self::Sampled { n_outputs, .. } => *n_outputs,
252 Self::Exponential { c0, .. } => c0.len(),
253 Self::Stitching {
254 range, functions, ..
255 } => {
256 if !range.is_empty() {
257 range.len()
258 } else if let Some(f) = functions.first() {
259 f.n_outputs()
260 } else {
261 1
262 }
263 }
264 Self::Calculator { range, .. } => range.len(),
265 Self::Composite { functions } => functions.iter().map(|f| f.n_outputs()).sum(),
266 }
267 }
268
269 pub fn discontinuity_positions(&self) -> Vec<f64> {
272 let mut positions = Vec::new();
273 self.collect_discontinuities(&mut positions);
274 positions
275 }
276
277 fn collect_discontinuities(&self, out: &mut Vec<f64>) {
278 match self {
279 Self::Stitching {
280 domain,
281 bounds,
282 functions,
283 encode,
284 ..
285 } => {
286 let d = domain.first().copied().unwrap_or([0.0, 1.0]);
287 for &b in bounds {
289 if b > d[0] && b < d[1] {
290 out.push(b);
291 }
292 }
293 for (k, f) in functions.iter().enumerate() {
296 let sub_discs = f.discontinuity_positions();
297 if sub_discs.is_empty() {
298 continue;
299 }
300 let enc = encode.get(k).copied().unwrap_or([0.0, 1.0]);
301 let d_lo = if k == 0 { d[0] } else { bounds[k - 1] };
302 let d_hi = if k >= bounds.len() { d[1] } else { bounds[k] };
303 for sd in sub_discs {
304 if (enc[1] - enc[0]).abs() < 1e-15 {
310 continue;
311 }
312 let x = d_lo + (sd - enc[0]) * (d_hi - d_lo) / (enc[1] - enc[0]);
313 if x > d[0] && x < d[1] {
314 out.push(x);
315 }
316 }
317 }
318 }
319 Self::Composite { functions } => {
320 for f in functions {
321 f.collect_discontinuities(out);
322 }
323 }
324 _ => {}
325 }
326 }
327
328 pub fn min_samples(&self) -> usize {
333 match self {
334 Self::Sampled { size, .. } => size.first().copied().unwrap_or(0) as usize,
335 Self::Stitching { functions, .. } => {
336 functions.iter().map(|f| f.min_samples().max(2)).sum()
337 }
338 Self::Composite { functions } => {
339 functions.iter().map(|f| f.min_samples()).max().unwrap_or(0)
340 }
341 _ => 0,
342 }
343 }
344
345 fn parse_sampled(
346 dict: &PdfDict,
347 obj: &PdfObj,
348 domain: Vec<[f64; 2]>,
349 range: Vec<[f64; 2]>,
350 resolver: &Resolver,
351 ) -> Result<Self, PdfError> {
352 let size: Vec<u32> = dict
353 .get_array(b"Size")
354 .ok_or(PdfError::Other("sampled function missing Size".into()))?
355 .iter()
356 .filter_map(|o| o.as_int().map(|n| n as u32))
357 .collect();
358
359 let bps = dict
360 .get_int(b"BitsPerSample")
361 .ok_or(PdfError::Other("missing BitsPerSample".into()))? as u32;
362
363 let n_outputs = range.len();
364
365 let encode = if let Ok(enc) = parse_domain_range(dict, b"Encode") {
366 enc
367 } else {
368 size.iter().map(|s| [0.0, (*s as f64) - 1.0]).collect()
369 };
370
371 let decode = if let Ok(dec) = parse_domain_range(dict, b"Decode") {
372 dec
373 } else {
374 range.clone()
375 };
376
377 let data = resolver.stream_data_from_obj(obj)?;
379 let max_val = ((1u64 << bps) - 1) as f64;
380 let total_samples: usize = size.iter().map(|s| *s as usize).product::<usize>() * n_outputs;
381 let mut samples = Vec::with_capacity(total_samples);
382
383 let mut bit_offset = 0usize;
384 for _ in 0..total_samples {
385 let byte_idx = bit_offset / 8;
386 let bit_idx = bit_offset % 8;
387 let mut val = 0u64;
388 let mut bits_left = bps;
389 let mut cur_byte = byte_idx;
390 let mut cur_bit = bit_idx;
391
392 while bits_left > 0 && cur_byte < data.len() {
393 let avail = 8 - cur_bit as u32;
394 let take = bits_left.min(avail);
395 let shift = avail - take;
396 let mask = ((1u64 << take) - 1) << shift;
397 val = (val << take) | ((data[cur_byte] as u64 & mask) >> shift);
398 bits_left -= take;
399 cur_bit = 0;
400 cur_byte += 1;
401 }
402
403 samples.push(val as f64 / max_val);
404 bit_offset += bps as usize;
405 }
406
407 Ok(Self::Sampled {
408 domain,
409 range,
410 size,
411 bps,
412 encode,
413 decode,
414 samples,
415 n_outputs,
416 })
417 }
418
419 fn parse_exponential(
420 dict: &PdfDict,
421 domain: Vec<[f64; 2]>,
422 range: Vec<[f64; 2]>,
423 ) -> Result<Self, PdfError> {
424 let n = dict
425 .get_f64(b"N")
426 .ok_or(PdfError::Other("exponential function missing N".into()))?;
427
428 let n_outputs = if !range.is_empty() { range.len() } else { 1 };
429
430 let c0 = dict
431 .get_array(b"C0")
432 .map(|arr| arr.iter().filter_map(|o| o.as_f64()).collect())
433 .unwrap_or_else(|| vec![0.0; n_outputs]);
434
435 let c1 = dict
436 .get_array(b"C1")
437 .map(|arr| arr.iter().filter_map(|o| o.as_f64()).collect())
438 .unwrap_or_else(|| vec![1.0; n_outputs]);
439
440 Ok(Self::Exponential {
441 domain,
442 range,
443 c0,
444 c1,
445 n,
446 })
447 }
448
449 fn parse_stitching(
450 dict: &PdfDict,
451 domain: Vec<[f64; 2]>,
452 range: Vec<[f64; 2]>,
453 resolver: &Resolver,
454 active: &mut Vec<u32>,
455 depth: u32,
456 ) -> Result<Self, PdfError> {
457 let fn_arr = if let Some(arr) = dict.get_array(b"Functions") {
459 arr.to_vec()
460 } else if let Some(obj) = dict.get(b"Functions") {
461 match resolver.deref(obj)? {
462 PdfObj::Array(arr) => arr,
463 _ => {
464 return Err(PdfError::Other(
465 "stitching Functions is not an array".into(),
466 ));
467 }
468 }
469 } else {
470 return Err(PdfError::Other("stitching missing Functions".into()));
471 };
472
473 let mut functions = Vec::with_capacity(fn_arr.len());
474 for fn_obj in &fn_arr {
475 functions.push(PdfFunction::parse_guarded(
476 fn_obj,
477 resolver,
478 active,
479 depth + 1,
480 )?);
481 }
482
483 let bounds_arr = if let Some(arr) = dict.get_array(b"Bounds") {
484 arr.to_vec()
485 } else if let Some(obj) = dict.get(b"Bounds") {
486 match resolver.deref(obj)? {
487 PdfObj::Array(arr) => arr,
488 _ => Vec::new(),
489 }
490 } else {
491 return Err(PdfError::Other("stitching missing Bounds".into()));
492 };
493 let bounds: Vec<f64> = bounds_arr.iter().filter_map(|o| o.as_f64()).collect();
494
495 let encode = parse_domain_range_resolved(dict, b"Encode", resolver)
496 .or_else(|_| parse_domain_range(dict, b"Encode"))
497 .unwrap_or_else(|_| functions.iter().map(|_| [0.0, 1.0]).collect());
498
499 Ok(Self::Stitching {
500 domain,
501 range,
502 functions,
503 bounds,
504 encode,
505 })
506 }
507
508 fn parse_calculator(
509 obj: &PdfObj,
510 domain: Vec<[f64; 2]>,
511 range: Vec<[f64; 2]>,
512 resolver: &Resolver,
513 ) -> Result<Self, PdfError> {
514 let data = resolver.stream_data_from_obj(obj)?;
515 let code = std::str::from_utf8(&data)
516 .map_err(|_| PdfError::Other("calculator function: invalid UTF-8".into()))?;
517 let tokens = parse_calc_tokens(code)?;
518 Ok(Self::Calculator {
519 domain,
520 range,
521 tokens,
522 })
523 }
524}
525
526fn parse_domain_range(dict: &PdfDict, key: &[u8]) -> Result<Vec<[f64; 2]>, PdfError> {
529 let arr = dict
530 .get_array(key)
531 .ok_or_else(|| PdfError::Other(format!("missing /{}", String::from_utf8_lossy(key))))?;
532 let vals: Vec<f64> = arr.iter().filter_map(|o| o.as_f64()).collect();
533 Ok(vals
534 .chunks(2)
535 .map(|c| [c[0], c.get(1).copied().unwrap_or(c[0])])
536 .collect())
537}
538
539fn parse_domain_range_resolved(
541 dict: &PdfDict,
542 key: &[u8],
543 resolver: &Resolver,
544) -> Result<Vec<[f64; 2]>, PdfError> {
545 if dict.get_array(key).is_some() {
546 return parse_domain_range(dict, key);
547 }
548 let obj = dict
549 .get(key)
550 .ok_or_else(|| PdfError::Other(format!("missing /{}", String::from_utf8_lossy(key))))?;
551 let resolved = resolver.deref(obj)?;
552 let arr = match &resolved {
553 PdfObj::Array(a) => a,
554 _ => {
555 return Err(PdfError::Other(format!(
556 "/{} is not an array",
557 String::from_utf8_lossy(key)
558 )));
559 }
560 };
561 let vals: Vec<f64> = arr.iter().filter_map(|o| o.as_f64()).collect();
562 Ok(vals
563 .chunks(2)
564 .map(|c| [c[0], c.get(1).copied().unwrap_or(c[0])])
565 .collect())
566}
567
568fn clamp(x: f64, lo: f64, hi: f64) -> f64 {
571 x.max(lo).min(hi)
572}
573
574fn interpolate(x: f64, x_min: f64, x_max: f64, y_min: f64, y_max: f64) -> f64 {
575 if (x_max - x_min).abs() < 1e-30 {
576 return y_min;
577 }
578 y_min + (x - x_min) * (y_max - y_min) / (x_max - x_min)
579}
580
581#[allow(clippy::too_many_arguments)]
582fn evaluate_sampled(
583 inputs: &[f64],
584 domain: &[[f64; 2]],
585 range: &[[f64; 2]],
586 size: &[u32],
587 encode: &[[f64; 2]],
588 decode: &[[f64; 2]],
589 samples: &[f64],
590 n_outputs: usize,
591) -> Vec<f64> {
592 let n_inputs = domain.len();
593
594 let mut encoded = Vec::with_capacity(n_inputs);
596 for i in 0..n_inputs.min(inputs.len()) {
597 let x = clamp(inputs[i], domain[i][0], domain[i][1]);
598 let e = interpolate(x, domain[i][0], domain[i][1], encode[i][0], encode[i][1]);
599 let e = clamp(e, 0.0, (size[i] as f64) - 1.0);
600 encoded.push(e);
601 }
602
603 if n_inputs == 1 && !encoded.is_empty() {
605 let e = encoded[0];
606 let i0 = e.floor() as usize;
607 let i1 = (i0 + 1).min(size[0] as usize - 1);
608 let frac = e - e.floor();
609
610 let mut result = Vec::with_capacity(n_outputs);
611 for j in 0..n_outputs {
612 let s0 = samples.get(i0 * n_outputs + j).copied().unwrap_or(0.0);
613 let s1 = samples.get(i1 * n_outputs + j).copied().unwrap_or(0.0);
614 let val = s0 + frac * (s1 - s0);
615 let decoded = if j < decode.len() {
616 interpolate(val, 0.0, 1.0, decode[j][0], decode[j][1])
617 } else {
618 val
619 };
620 let clamped = if j < range.len() {
621 clamp(decoded, range[j][0], range[j][1])
622 } else {
623 decoded
624 };
625 result.push(clamped);
626 }
627 return result;
628 }
629
630 let n = n_inputs.min(encoded.len());
633
634 let mut i0s = Vec::with_capacity(n);
636 let mut fracs = Vec::with_capacity(n);
637 for dim in 0..n {
638 let e = encoded[dim];
639 let lo = e.floor() as usize;
640 let lo = lo.min(size[dim] as usize - 2); i0s.push(lo);
642 fracs.push(e - lo as f64);
643 }
644
645 let mut strides = vec![0usize; n];
648 strides[0] = n_outputs;
649 for dim in 1..n {
650 strides[dim] = strides[dim - 1] * size[dim - 1] as usize;
651 }
652
653 let n_corners = 1usize << n;
655 let mut result = vec![0.0f64; n_outputs];
656 for corner in 0..n_corners {
657 let mut weight = 1.0f64;
658 let mut index = 0usize;
659 for dim in 0..n {
660 if corner & (1 << dim) != 0 {
661 weight *= fracs[dim];
662 index += (i0s[dim] + 1) * strides[dim];
663 } else {
664 weight *= 1.0 - fracs[dim];
665 index += i0s[dim] * strides[dim];
666 }
667 }
668 for (j, r) in result.iter_mut().enumerate() {
669 *r += weight * samples.get(index + j).copied().unwrap_or(0.0);
670 }
671 }
672
673 for j in 0..n_outputs {
675 if j < decode.len() {
676 result[j] = interpolate(result[j], 0.0, 1.0, decode[j][0], decode[j][1]);
677 }
678 if j < range.len() {
679 result[j] = clamp(result[j], range[j][0], range[j][1]);
680 }
681 }
682 result
683}
684
685fn evaluate_exponential(
686 inputs: &[f64],
687 domain: &[[f64; 2]],
688 range: &[[f64; 2]],
689 c0: &[f64],
690 c1: &[f64],
691 n: f64,
692) -> Vec<f64> {
693 let x = if !inputs.is_empty() && !domain.is_empty() {
694 clamp(inputs[0], domain[0][0], domain[0][1])
695 } else {
696 0.0
697 };
698
699 let x_n = x.powf(n);
700 let mut result = Vec::with_capacity(c0.len());
701 for i in 0..c0.len() {
702 let val = c0[i] + x_n * (c1.get(i).copied().unwrap_or(1.0) - c0[i]);
703 let clamped = if i < range.len() {
704 clamp(val, range[i][0], range[i][1])
705 } else {
706 val
707 };
708 result.push(clamped);
709 }
710 result
711}
712
713fn evaluate_stitching(
714 inputs: &[f64],
715 domain: &[[f64; 2]],
716 range: &[[f64; 2]],
717 functions: &[PdfFunction],
718 bounds: &[f64],
719 encode: &[[f64; 2]],
720) -> Vec<f64> {
721 if functions.is_empty() {
722 return vec![0.0];
723 }
724
725 let x = if !inputs.is_empty() && !domain.is_empty() {
726 clamp(inputs[0], domain[0][0], domain[0][1])
727 } else {
728 0.0
729 };
730
731 let mut k = 0;
733 for (i, &b) in bounds.iter().enumerate() {
734 if x < b {
735 k = i;
736 break;
737 }
738 k = i + 1;
739 }
740 k = k.min(functions.len() - 1);
741
742 let d_lo = if k == 0 {
744 domain.first().map(|d| d[0]).unwrap_or(0.0)
745 } else {
746 bounds[k - 1]
747 };
748 let d_hi = if k >= bounds.len() {
749 domain.first().map(|d| d[1]).unwrap_or(1.0)
750 } else {
751 bounds[k]
752 };
753
754 let enc = encode.get(k).copied().unwrap_or([0.0, 1.0]);
756 let x_enc = interpolate(x, d_lo, d_hi, enc[0], enc[1]);
757
758 let mut result = functions[k].evaluate(&[x_enc]);
759
760 for (i, val) in result.iter_mut().enumerate() {
762 if i < range.len() {
763 *val = clamp(*val, range[i][0], range[i][1]);
764 }
765 }
766 result
767}
768
769fn evaluate_calculator(
770 inputs: &[f64],
771 domain: &[[f64; 2]],
772 range: &[[f64; 2]],
773 tokens: &[CalcToken],
774) -> Vec<f64> {
775 let mut stack: Vec<f64> = Vec::with_capacity(16);
777 for (i, &x) in inputs.iter().enumerate() {
778 let clamped = if i < domain.len() {
779 clamp(x, domain[i][0], domain[i][1])
780 } else {
781 x
782 };
783 stack.push(clamped);
784 }
785
786 execute_calc_tokens(&mut stack, tokens);
787
788 let n_out = range.len();
790 let mut result = Vec::with_capacity(n_out);
791 for i in 0..n_out {
792 let val = if i < stack.len() {
793 stack[stack.len() - n_out + i]
794 } else {
795 0.0
796 };
797 result.push(clamp(val, range[i][0], range[i][1]));
798 }
799 result
800}
801
802fn execute_calc_tokens(stack: &mut Vec<f64>, tokens: &[CalcToken]) {
803 for token in tokens {
804 match token {
805 CalcToken::Number(n) => stack.push(*n),
806 CalcToken::Bool(b) => stack.push(if *b { 1.0 } else { 0.0 }),
807 CalcToken::True => stack.push(1.0),
808 CalcToken::False => stack.push(0.0),
809
810 CalcToken::Add => bin_op(stack, |a, b| a + b),
812 CalcToken::Sub => bin_op(stack, |a, b| a - b),
813 CalcToken::Mul => bin_op(stack, |a, b| a * b),
814 CalcToken::Div => bin_op(stack, |a, b| if b != 0.0 { a / b } else { 0.0 }),
815 CalcToken::Idiv => bin_op(stack, |a, b| {
816 if b != 0.0 {
817 ((a as i64) / (b as i64)) as f64
818 } else {
819 0.0
820 }
821 }),
822 CalcToken::Mod => bin_op(stack, |a, b| {
823 if b != 0.0 {
824 ((a as i64) % (b as i64)) as f64
825 } else {
826 0.0
827 }
828 }),
829 CalcToken::Neg => un_op(stack, |a| -a),
830 CalcToken::Abs => un_op(stack, |a| a.abs()),
831 CalcToken::Ceiling => un_op(stack, |a| a.ceil()),
832 CalcToken::Floor => un_op(stack, |a| a.floor()),
833 CalcToken::Round => un_op(stack, |a| a.round()),
834 CalcToken::Truncate => un_op(stack, |a| a.trunc()),
835 CalcToken::Sqrt => un_op(stack, |a| a.sqrt()),
836 CalcToken::Exp => bin_op(stack, |a, b| a.powf(b)),
837 CalcToken::Ln => un_op(stack, |a| a.ln()),
838 CalcToken::Log => un_op(stack, |a| a.log10()),
839 CalcToken::Sin => un_op(stack, |a| a.to_radians().sin()),
840 CalcToken::Cos => un_op(stack, |a| a.to_radians().cos()),
841 CalcToken::Atan => bin_op(stack, |a, b| {
842 let deg = a.atan2(b).to_degrees();
843 if deg < 0.0 { deg + 360.0 } else { deg }
844 }),
845
846 CalcToken::Eq => bin_op(stack, |a, b| if (a - b).abs() < 1e-10 { 1.0 } else { 0.0 }),
848 CalcToken::Ne => bin_op(stack, |a, b| if (a - b).abs() >= 1e-10 { 1.0 } else { 0.0 }),
849 CalcToken::Gt => bin_op(stack, |a, b| if a > b { 1.0 } else { 0.0 }),
850 CalcToken::Ge => bin_op(stack, |a, b| if a >= b { 1.0 } else { 0.0 }),
851 CalcToken::Lt => bin_op(stack, |a, b| if a < b { 1.0 } else { 0.0 }),
852 CalcToken::Le => bin_op(stack, |a, b| if a <= b { 1.0 } else { 0.0 }),
853 CalcToken::And => bin_op(stack, |a, b| ((a as i64) & (b as i64)) as f64),
854 CalcToken::Or => bin_op(stack, |a, b| ((a as i64) | (b as i64)) as f64),
855 CalcToken::Xor => bin_op(stack, |a, b| ((a as i64) ^ (b as i64)) as f64),
856 CalcToken::Not => un_op(stack, |a| if a == 0.0 { 1.0 } else { 0.0 }),
857 CalcToken::Bitshift => bin_op(stack, |a, b| {
858 let n = a as i64;
859 let shift = b as i32;
860 if shift > 0 {
861 (n << shift) as f64
862 } else {
863 (n >> (-shift)) as f64
864 }
865 }),
866
867 CalcToken::Dup => {
869 if let Some(&top) = stack.last() {
870 stack.push(top);
871 }
872 }
873 CalcToken::Exch => {
874 let len = stack.len();
875 if len >= 2 {
876 stack.swap(len - 1, len - 2);
877 }
878 }
879 CalcToken::Pop => {
880 stack.pop();
881 }
882 CalcToken::Copy => {
883 if let Some(&n) = stack.last() {
884 stack.pop();
885 let n = n as usize;
886 let len = stack.len();
887 if n <= len {
888 let items: Vec<f64> = stack[len - n..].to_vec();
889 stack.extend_from_slice(&items);
890 }
891 }
892 }
893 CalcToken::Index => {
894 if let Some(&n) = stack.last() {
895 stack.pop();
896 let idx = n as usize;
897 let len = stack.len();
898 if idx < len {
899 stack.push(stack[len - 1 - idx]);
900 }
901 }
902 }
903 CalcToken::Roll => {
904 let len = stack.len();
905 if len >= 2 {
906 let j = stack.pop().unwrap() as i32;
907 let n = stack.pop().unwrap() as usize;
908 if n > 0 && n <= stack.len() {
909 let start = stack.len() - n;
910 let j = ((j % n as i32) + n as i32) as usize % n;
911 let mut temp: Vec<f64> = stack[start..].to_vec();
912 temp.rotate_right(j);
913 stack[start..].copy_from_slice(&temp);
914 }
915 }
916 }
917
918 CalcToken::If(body) => {
920 if let Some(&cond) = stack.last() {
921 stack.pop();
922 if cond != 0.0 {
923 execute_calc_tokens(stack, body);
924 }
925 }
926 }
927 CalcToken::IfElse(if_body, else_body) => {
928 if let Some(&cond) = stack.last() {
929 stack.pop();
930 if cond != 0.0 {
931 execute_calc_tokens(stack, if_body);
932 } else {
933 execute_calc_tokens(stack, else_body);
934 }
935 }
936 }
937
938 CalcToken::Cvi => un_op(stack, |a| a.trunc()),
940 CalcToken::Cvr => {} }
942 }
943}
944
945fn bin_op(stack: &mut Vec<f64>, f: impl FnOnce(f64, f64) -> f64) {
946 if stack.len() >= 2 {
947 let b = stack.pop().unwrap();
948 let a = stack.pop().unwrap();
949 stack.push(f(a, b));
950 }
951}
952
953fn un_op(stack: &mut Vec<f64>, f: impl FnOnce(f64) -> f64) {
954 if let Some(a) = stack.pop() {
955 stack.push(f(a));
956 }
957}
958
959fn parse_calc_tokens(code: &str) -> Result<Vec<CalcToken>, PdfError> {
962 let code = code.trim();
963 let code = if code.starts_with('{') && code.ends_with('}') {
965 &code[1..code.len() - 1]
966 } else {
967 code
968 };
969
970 parse_token_sequence(code, 0)
971}
972
973const MAX_CALC_DEPTH: u32 = 64;
980
981fn parse_token_sequence(code: &str, depth: u32) -> Result<Vec<CalcToken>, PdfError> {
985 if depth >= MAX_CALC_DEPTH {
986 return Err(PdfError::NestingTooDeep {
987 context: "calculator function procedure",
988 limit: MAX_CALC_DEPTH,
989 });
990 }
991 let mut tokens = Vec::new();
992 let mut chars = code.chars().peekable();
993
994 while let Some(&ch) = chars.peek() {
995 if ch.is_whitespace() {
996 chars.next();
997 continue;
998 }
999
1000 if ch == '{' {
1001 chars.next();
1002 let body = collect_brace_body(&mut chars)?;
1004 let body_tokens = parse_token_sequence(&body, depth + 1)?;
1005
1006 while chars.peek().is_some_and(|c| c.is_whitespace()) {
1009 chars.next();
1010 }
1011
1012 let saved: String = chars.clone().collect();
1014 if saved.starts_with('{') {
1015 chars.next(); let else_body = collect_brace_body(&mut chars)?;
1018 let else_tokens = parse_token_sequence(&else_body, depth + 1)?;
1019 while chars.peek().is_some_and(|c| c.is_whitespace()) {
1021 chars.next();
1022 }
1023 let word = collect_word(&mut chars);
1025 if word == "ifelse" {
1026 tokens.push(CalcToken::IfElse(body_tokens, else_tokens));
1027 } else {
1028 tokens.push(CalcToken::If(body_tokens));
1030 tokens.push(CalcToken::If(else_tokens));
1031 if let Some(tok) = word_to_token(&word) {
1032 tokens.push(tok);
1033 }
1034 }
1035 } else {
1036 let word = collect_word(&mut chars);
1037 if word == "if" {
1038 tokens.push(CalcToken::If(body_tokens));
1039 } else {
1040 tokens.push(CalcToken::If(body_tokens));
1042 if let Some(tok) = word_to_token(&word) {
1043 tokens.push(tok);
1044 }
1045 }
1046 }
1047 continue;
1048 }
1049
1050 let word = collect_word(&mut chars);
1052 if word.is_empty() {
1053 chars.next(); continue;
1055 }
1056
1057 if let Ok(n) = word.parse::<f64>() {
1059 tokens.push(CalcToken::Number(n));
1060 } else if let Some(tok) = word_to_token(&word) {
1061 tokens.push(tok);
1062 }
1063 }
1065
1066 Ok(tokens)
1067}
1068
1069fn collect_brace_body(
1070 chars: &mut std::iter::Peekable<std::str::Chars>,
1071) -> Result<String, PdfError> {
1072 let mut body = String::new();
1073 let mut depth = 1;
1074 for ch in chars.by_ref() {
1075 if ch == '{' {
1076 depth += 1;
1077 body.push(ch);
1078 } else if ch == '}' {
1079 depth -= 1;
1080 if depth == 0 {
1081 return Ok(body);
1082 }
1083 body.push(ch);
1084 } else {
1085 body.push(ch);
1086 }
1087 }
1088 Err(PdfError::Other(
1089 "unterminated { in calculator function".into(),
1090 ))
1091}
1092
1093fn collect_word(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
1094 let mut word = String::new();
1095 while let Some(&ch) = chars.peek() {
1096 if ch.is_whitespace() || ch == '{' || ch == '}' {
1097 break;
1098 }
1099 word.push(ch);
1100 chars.next();
1101 }
1102 word
1103}
1104
1105fn word_to_token(word: &str) -> Option<CalcToken> {
1106 Some(match word {
1107 "add" => CalcToken::Add,
1108 "sub" => CalcToken::Sub,
1109 "mul" => CalcToken::Mul,
1110 "div" => CalcToken::Div,
1111 "idiv" => CalcToken::Idiv,
1112 "mod" => CalcToken::Mod,
1113 "neg" => CalcToken::Neg,
1114 "abs" => CalcToken::Abs,
1115 "ceiling" => CalcToken::Ceiling,
1116 "floor" => CalcToken::Floor,
1117 "round" => CalcToken::Round,
1118 "truncate" => CalcToken::Truncate,
1119 "sqrt" => CalcToken::Sqrt,
1120 "exp" => CalcToken::Exp,
1121 "ln" => CalcToken::Ln,
1122 "log" => CalcToken::Log,
1123 "sin" => CalcToken::Sin,
1124 "cos" => CalcToken::Cos,
1125 "atan" => CalcToken::Atan,
1126 "eq" => CalcToken::Eq,
1127 "ne" => CalcToken::Ne,
1128 "gt" => CalcToken::Gt,
1129 "ge" => CalcToken::Ge,
1130 "lt" => CalcToken::Lt,
1131 "le" => CalcToken::Le,
1132 "and" => CalcToken::And,
1133 "or" => CalcToken::Or,
1134 "xor" => CalcToken::Xor,
1135 "not" => CalcToken::Not,
1136 "bitshift" => CalcToken::Bitshift,
1137 "dup" => CalcToken::Dup,
1138 "exch" => CalcToken::Exch,
1139 "pop" => CalcToken::Pop,
1140 "copy" => CalcToken::Copy,
1141 "index" => CalcToken::Index,
1142 "roll" => CalcToken::Roll,
1143 "cvi" => CalcToken::Cvi,
1144 "cvr" => CalcToken::Cvr,
1145 "true" => CalcToken::True,
1146 "false" => CalcToken::False,
1147 "if" | "ifelse" => return None, _ => return None,
1149 })
1150}
1151
1152#[cfg(test)]
1153mod tests {
1154 use super::*;
1155
1156 #[test]
1157 fn exponential_function() {
1158 let f = PdfFunction::Exponential {
1159 domain: vec![[0.0, 1.0]],
1160 range: vec![[0.0, 1.0], [0.0, 1.0], [0.0, 1.0]],
1161 c0: vec![1.0, 0.0, 0.0],
1162 c1: vec![0.0, 0.0, 1.0],
1163 n: 1.0,
1164 };
1165 let result = f.evaluate(&[0.0]);
1166 assert_eq!(result, vec![1.0, 0.0, 0.0]);
1167
1168 let result = f.evaluate(&[1.0]);
1169 assert_eq!(result, vec![0.0, 0.0, 1.0]);
1170
1171 let result = f.evaluate(&[0.5]);
1172 assert!((result[0] - 0.5).abs() < 1e-10);
1173 }
1174
1175 #[test]
1176 fn calculator_simple() {
1177 let tokens = parse_calc_tokens("{ 2 mul }").unwrap();
1178 let f = PdfFunction::Calculator {
1179 domain: vec![[0.0, 1.0]],
1180 range: vec![[0.0, 2.0]],
1181 tokens,
1182 };
1183 let result = f.evaluate(&[0.5]);
1184 assert!((result[0] - 1.0).abs() < 1e-10);
1185 }
1186
1187 #[test]
1188 fn devicen_duotone_black_green_diag() {
1189 let code = "{1.000000 3 1 roll 1.000000 3 1 roll 1.000000 3 1 roll 1 index 1.000000 \
1190cvr exch sub 3 1 roll 6 -1 roll 1 index 0.500000 mul 1.000000 cvr \
1191exch sub mul 1.000000 cvr exch sub 6 1 roll 5 -1 roll 1 index \
11920.000000 mul 1.000000 cvr exch sub mul 1.000000 cvr exch sub 5 1 roll 4 \
1193-1 roll 1 index 1.000000 mul 1.000000 cvr exch sub mul 1.000000 cvr exch sub \
11944 1 roll 3 -1 roll 1 index 0.000000 mul 1.000000 cvr exch sub mul \
11951.000000 cvr exch sub 3 1 roll pop pop }";
1196 let tokens = parse_calc_tokens(code).unwrap();
1197 let f = PdfFunction::Calculator {
1198 domain: vec![[0.0, 1.0], [0.0, 1.0]],
1199 range: vec![[0.0, 1.0], [0.0, 1.0], [0.0, 1.0], [0.0, 1.0]],
1200 tokens,
1201 };
1202 for (b, g, label) in [
1203 (0.0, 0.0, "white"),
1204 (1.0, 0.0, "black only"),
1205 (0.0, 1.0, "green only"),
1206 (1.0, 1.0, "both full"),
1207 (0.5, 0.5, "both half"),
1208 ] {
1209 let r = f.evaluate(&[b, g]);
1210 eprintln!("{label} (b={b}, g={g}) -> CMYK={r:?}");
1211 }
1212 let r = f.evaluate(&[1.0, 0.0]);
1214 assert!(
1215 (r[0]).abs() < 1e-6
1216 && (r[1]).abs() < 1e-6
1217 && (r[2]).abs() < 1e-6
1218 && (r[3] - 1.0).abs() < 1e-6,
1219 "black-only got CMYK={r:?}"
1220 );
1221 let r = f.evaluate(&[0.0, 1.0]);
1222 assert!(
1223 (r[0] - 0.5).abs() < 1e-6
1224 && (r[1]).abs() < 1e-6
1225 && (r[2] - 1.0).abs() < 1e-6
1226 && (r[3]).abs() < 1e-6,
1227 "green-only got CMYK={r:?}"
1228 );
1229 }
1230
1231 #[test]
1232 fn devicen_cyan_green_gradient() {
1233 let code = "{0 index 1.000000 cvr exch sub 3 1 roll 1.000000 3 1 roll 1.000000 3 \
12341 roll 1.000000 3 1 roll 6 -1 roll 2 index 0.500000 mul 1.000000 cvr \
1235exch sub mul 1.000000 cvr exch sub 6 1 roll 5 -1 roll 2 index \
12360.000000 mul 1.000000 cvr exch sub mul 1.000000 cvr exch sub 5 1 roll 4 \
1237-1 roll 2 index 1.000000 mul 1.000000 cvr exch sub mul 1.000000 cvr exch sub \
12384 1 roll 3 -1 roll 2 index 0.000000 mul 1.000000 cvr exch sub mul \
12391.000000 cvr exch sub 3 1 roll pop pop }";
1240 let tokens = parse_calc_tokens(code).unwrap();
1241 let f = PdfFunction::Calculator {
1242 domain: vec![[0.0, 1.0], [0.0, 1.0]],
1243 range: vec![[0.0, 1.0], [0.0, 1.0], [0.0, 1.0], [0.0, 1.0]],
1244 tokens,
1245 };
1246 for (g, c, label) in [
1247 (0.0, 0.0, "white"),
1248 (1.0, 0.0, "green only"),
1249 (0.0, 1.0, "cyan only"),
1250 (0.5, 0.5, "both half"),
1251 ] {
1252 let r = f.evaluate(&[g, c]);
1253 eprintln!("{label} (g={g}, c={c}) -> CMYK={r:?}");
1254 }
1255 let r = f.evaluate(&[1.0, 0.0]);
1256 assert!(
1257 (r[0] - 0.5).abs() < 1e-6 && (r[2] - 1.0).abs() < 1e-6,
1258 "green-only: CMYK={r:?}"
1259 );
1260 let r = f.evaluate(&[0.0, 1.0]);
1261 assert!(
1262 (r[0] - 1.0).abs() < 1e-6 && (r[1]).abs() < 1e-6 && (r[2]).abs() < 1e-6,
1263 "cyan-only: CMYK={r:?}"
1264 );
1265 }
1266
1267 #[test]
1268 fn devicen_duotone_via_tint_table() {
1269 use std::sync::Arc;
1270 use stet_graphics::device::TintLookupTable;
1271 let code = "{1.000000 3 1 roll 1.000000 3 1 roll 1.000000 3 1 roll 1 index 1.000000 \
1272cvr exch sub 3 1 roll 6 -1 roll 1 index 0.500000 mul 1.000000 cvr \
1273exch sub mul 1.000000 cvr exch sub 6 1 roll 5 -1 roll 1 index \
12740.000000 mul 1.000000 cvr exch sub mul 1.000000 cvr exch sub 5 1 roll 4 \
1275-1 roll 1 index 1.000000 mul 1.000000 cvr exch sub mul 1.000000 cvr exch sub \
12764 1 roll 3 -1 roll 1 index 0.000000 mul 1.000000 cvr exch sub mul \
12771.000000 cvr exch sub 3 1 roll pop pop }";
1278 let tokens = parse_calc_tokens(code).unwrap();
1279 let f = PdfFunction::Calculator {
1280 domain: vec![[0.0, 1.0], [0.0, 1.0]],
1281 range: vec![[0.0, 1.0], [0.0, 1.0], [0.0, 1.0], [0.0, 1.0]],
1282 tokens,
1283 };
1284 let n_inputs = 2usize;
1286 let n_out = 4usize;
1287 let spd = 64u32;
1288 let total: usize = (spd as usize).pow(n_inputs as u32);
1289 let mut data = Vec::with_capacity(total * n_out);
1290 let mut inputs = vec![0.0f64; n_inputs];
1291 for idx in 0..total {
1292 let mut rem = idx;
1293 for d in (0..n_inputs).rev() {
1294 inputs[d] = (rem % spd as usize) as f64 / (spd - 1) as f64;
1295 rem /= spd as usize;
1296 }
1297 let out = f.evaluate(&inputs);
1298 for j in 0..n_out {
1299 data.push(out.get(j).copied().unwrap_or(0.0) as f32);
1300 }
1301 }
1302 let table = TintLookupTable {
1303 num_inputs: n_inputs as u32,
1304 num_outputs: n_out as u32,
1305 samples_per_dim: spd,
1306 data,
1307 };
1308 let mut out = vec![0.0f32; 4];
1309 for (b, g, label) in [
1310 (0.0, 0.0, "white"),
1311 (1.0, 0.0, "black only"),
1312 (0.0, 1.0, "green only"),
1313 (1.0, 1.0, "both full"),
1314 (0.5, 0.5, "both half"),
1315 ] {
1316 table.lookup_nd(&[b as f32, g as f32], &mut out);
1317 eprintln!("table {label} (b={b}, g={g}) -> CMYK={out:?}");
1318 }
1319 table.lookup_nd(&[0.0, 1.0], &mut out);
1321 assert!(
1322 (out[0] - 0.5).abs() < 0.02,
1323 "green-only via table: C={out:?}"
1324 );
1325 assert!(
1326 (out[2] - 1.0).abs() < 0.02,
1327 "green-only via table: Y={out:?}"
1328 );
1329 }
1330}