Skip to main content

justpdf_core/
function.rs

1//! PDF Function evaluator.
2//!
3//! Supports all 4 PDF function types:
4//! - Type 0: Sampled function (lookup table)
5//! - Type 2: Exponential interpolation
6//! - Type 3: Stitching (piecewise)
7//! - Type 4: PostScript calculator
8//!
9//! Reference: PDF 2.0 spec, section 7.10
10
11use crate::object::{PdfDict, PdfObject};
12
13/// A resolved PDF function that can be evaluated.
14#[derive(Debug, Clone)]
15pub enum PdfFunction {
16    /// Type 2: Exponential interpolation.
17    Exponential {
18        domain: Vec<(f64, f64)>,
19        range: Vec<(f64, f64)>,
20        c0: Vec<f64>,
21        c1: Vec<f64>,
22        n: f64,
23    },
24    /// Type 3: Stitching of sub-functions.
25    Stitching {
26        domain: Vec<(f64, f64)>,
27        range: Vec<(f64, f64)>,
28        functions: Vec<PdfFunction>,
29        bounds: Vec<f64>,
30        encode: Vec<f64>,
31    },
32    /// Type 4: PostScript calculator.
33    PostScript {
34        domain: Vec<(f64, f64)>,
35        range: Vec<(f64, f64)>,
36        ops: Vec<PsOp>,
37    },
38}
39
40/// PostScript calculator operations.
41#[derive(Debug, Clone)]
42pub enum PsOp {
43    // Operand
44    Num(f64),
45    // Arithmetic
46    Add,
47    Sub,
48    Mul,
49    Div,
50    Idiv,
51    Mod,
52    Neg,
53    Abs,
54    Ceiling,
55    Floor,
56    Round,
57    Truncate,
58    Sqrt,
59    Exp,
60    Ln,
61    Log,
62    Sin,
63    Cos,
64    Atan,
65    // Comparison
66    Eq,
67    Ne,
68    Gt,
69    Ge,
70    Lt,
71    Le,
72    // Boolean
73    And,
74    Or,
75    Not,
76    Xor,
77    // Bitwise
78    Bitshift,
79    // Conditional
80    If(Vec<PsOp>),
81    IfElse(Vec<PsOp>, Vec<PsOp>),
82    // Stack
83    Dup,
84    Exch,
85    Pop,
86    Copy,
87    Index,
88    Roll,
89    // Conversion
90    Cvi,
91    Cvr,
92    True,
93    False,
94}
95
96impl PdfFunction {
97    /// Parse a PDF function from a function dictionary/stream.
98    pub fn parse(obj: &PdfObject) -> Option<Self> {
99        let dict = match obj {
100            PdfObject::Dict(d) => d,
101            PdfObject::Stream { dict, .. } => dict,
102            _ => return None,
103        };
104
105        let func_type = dict.get_i64(b"FunctionType")?;
106        let domain = parse_domain_range(dict, b"Domain");
107        let range = parse_domain_range(dict, b"Range");
108
109        match func_type {
110            2 => Self::parse_exponential(dict, domain, range),
111            3 => Self::parse_stitching(dict, domain, range),
112            4 => Self::parse_postscript(obj, domain, range),
113            _ => None,
114        }
115    }
116
117    fn parse_exponential(
118        dict: &PdfDict,
119        domain: Vec<(f64, f64)>,
120        range: Vec<(f64, f64)>,
121    ) -> Option<Self> {
122        let c0 = dict
123            .get_array(b"C0")
124            .map(|a| a.iter().filter_map(|o| o.as_f64()).collect())
125            .unwrap_or_else(|| vec![0.0]);
126        let c1 = dict
127            .get_array(b"C1")
128            .map(|a| a.iter().filter_map(|o| o.as_f64()).collect())
129            .unwrap_or_else(|| vec![1.0]);
130        let n = dict
131            .get(b"N")
132            .and_then(|o| o.as_f64())
133            .unwrap_or(1.0);
134
135        Some(PdfFunction::Exponential {
136            domain,
137            range,
138            c0,
139            c1,
140            n,
141        })
142    }
143
144    fn parse_stitching(
145        dict: &PdfDict,
146        domain: Vec<(f64, f64)>,
147        range: Vec<(f64, f64)>,
148    ) -> Option<Self> {
149        let bounds = dict
150            .get_array(b"Bounds")
151            .map(|a| a.iter().filter_map(|o| o.as_f64()).collect())
152            .unwrap_or_default();
153        let encode = dict
154            .get_array(b"Encode")
155            .map(|a| a.iter().filter_map(|o| o.as_f64()).collect())
156            .unwrap_or_default();
157
158        let functions: Vec<PdfFunction> = dict
159            .get_array(b"Functions")
160            .map(|arr| arr.iter().filter_map(|o| PdfFunction::parse(o)).collect())
161            .unwrap_or_default();
162
163        Some(PdfFunction::Stitching {
164            domain,
165            range,
166            functions,
167            bounds,
168            encode,
169        })
170    }
171
172    fn parse_postscript(
173        obj: &PdfObject,
174        domain: Vec<(f64, f64)>,
175        range: Vec<(f64, f64)>,
176    ) -> Option<Self> {
177        let stream_data = match obj {
178            PdfObject::Stream { data, .. } => data,
179            _ => return None,
180        };
181
182        let code = std::str::from_utf8(stream_data).ok()?;
183        let ops = parse_ps_code(code)?;
184
185        Some(PdfFunction::PostScript {
186            domain,
187            range,
188            ops,
189        })
190    }
191
192    /// Evaluate the function with given input values.
193    /// Returns output values.
194    pub fn evaluate(&self, input: &[f64]) -> Vec<f64> {
195        match self {
196            PdfFunction::Exponential {
197                domain,
198                range,
199                c0,
200                c1,
201                n,
202            } => {
203                let x = clamp_input(input.first().copied().unwrap_or(0.0), domain);
204                let out_len = c0.len().max(c1.len());
205                let mut result = Vec::with_capacity(out_len);
206                for i in 0..out_len {
207                    let a = c0.get(i).copied().unwrap_or(0.0);
208                    let b = c1.get(i).copied().unwrap_or(1.0);
209                    let val = a + x.powf(*n) * (b - a);
210                    result.push(clamp_output(val, range, i));
211                }
212                result
213            }
214            PdfFunction::Stitching {
215                domain,
216                range,
217                functions,
218                bounds,
219                encode,
220            } => {
221                if functions.is_empty() {
222                    return vec![0.0];
223                }
224                let x = clamp_input(input.first().copied().unwrap_or(0.0), domain);
225
226                // Find which sub-function to use
227                let mut idx = 0;
228                for (i, &b) in bounds.iter().enumerate() {
229                    if x < b {
230                        idx = i;
231                        break;
232                    }
233                    idx = i + 1;
234                }
235                idx = idx.min(functions.len() - 1);
236
237                // Encode x into sub-function's domain
238                let sub_domain_start = bounds.get(idx.wrapping_sub(1)).copied().unwrap_or_else(|| {
239                    domain.first().map(|d| d.0).unwrap_or(0.0)
240                });
241                let sub_domain_end = bounds.get(idx).copied().unwrap_or_else(|| {
242                    domain.first().map(|d| d.1).unwrap_or(1.0)
243                });
244
245                let enc_start = encode.get(idx * 2).copied().unwrap_or(0.0);
246                let enc_end = encode.get(idx * 2 + 1).copied().unwrap_or(1.0);
247
248                let denom = sub_domain_end - sub_domain_start;
249                let encoded = if denom.abs() > 1e-10 {
250                    enc_start + (x - sub_domain_start) / denom * (enc_end - enc_start)
251                } else {
252                    enc_start
253                };
254
255                let result = functions[idx].evaluate(&[encoded]);
256                result
257                    .iter()
258                    .enumerate()
259                    .map(|(i, &v)| clamp_output(v, range, i))
260                    .collect()
261            }
262            PdfFunction::PostScript {
263                domain,
264                range,
265                ops,
266            } => {
267                let mut stack: Vec<f64> = Vec::new();
268                // Push clamped inputs onto stack
269                for (i, &val) in input.iter().enumerate() {
270                    stack.push(clamp_input(val, &domain[i..i + 1].iter().copied().collect::<Vec<_>>()));
271                }
272
273                execute_ps_ops(&mut stack, ops);
274
275                // Clamp outputs
276                stack
277                    .iter()
278                    .enumerate()
279                    .map(|(i, &v)| clamp_output(v, range, i))
280                    .collect()
281            }
282        }
283    }
284}
285
286fn clamp_input(x: f64, domain: &[(f64, f64)]) -> f64 {
287    if let Some(&(lo, hi)) = domain.first() {
288        x.clamp(lo, hi)
289    } else {
290        x.clamp(0.0, 1.0)
291    }
292}
293
294fn clamp_output(val: f64, range: &[(f64, f64)], idx: usize) -> f64 {
295    if let Some(&(lo, hi)) = range.get(idx) {
296        val.clamp(lo, hi)
297    } else {
298        val
299    }
300}
301
302fn parse_domain_range(dict: &PdfDict, key: &[u8]) -> Vec<(f64, f64)> {
303    dict.get_array(key)
304        .map(|arr| {
305            arr.chunks(2)
306                .map(|pair| {
307                    let lo = pair.first().and_then(|o| o.as_f64()).unwrap_or(0.0);
308                    let hi = pair.get(1).and_then(|o| o.as_f64()).unwrap_or(1.0);
309                    (lo, hi)
310                })
311                .collect()
312        })
313        .unwrap_or_default()
314}
315
316// ---------------------------------------------------------------------------
317// PostScript calculator parser
318// ---------------------------------------------------------------------------
319
320fn parse_ps_code(code: &str) -> Option<Vec<PsOp>> {
321    // Strip outer braces { ... }
322    let code = code.trim();
323    let code = if code.starts_with('{') && code.ends_with('}') {
324        &code[1..code.len() - 1]
325    } else {
326        code
327    };
328
329    parse_ps_block(code)
330}
331
332fn parse_ps_block(code: &str) -> Option<Vec<PsOp>> {
333    let mut ops = Vec::new();
334    let tokens = tokenize_ps(code);
335    let mut i = 0;
336
337    while i < tokens.len() {
338        let token = &tokens[i];
339        i += 1;
340
341        match token.as_str() {
342            "{" => {
343                // Find matching closing brace
344                let (block, end) = extract_block(&tokens, i)?;
345                i = end;
346
347                // Check if followed by "if" or "ifelse"
348                if i < tokens.len() && tokens[i] == "if" {
349                    i += 1;
350                    let block_ops = parse_ps_block(&block)?;
351                    ops.push(PsOp::If(block_ops));
352                } else if i + 1 < tokens.len() && tokens[i] == "{" {
353                    // Could be: { block1 } { block2 } ifelse
354                    let (block2, end2) = extract_block(&tokens, i + 1)?;
355                    if end2 < tokens.len() && tokens[end2] == "ifelse" {
356                        let block1_ops = parse_ps_block(&block)?;
357                        let block2_ops = parse_ps_block(&block2)?;
358                        ops.push(PsOp::IfElse(block1_ops, block2_ops));
359                        i = end2 + 1;
360                    } else {
361                        // Just a block — push ops
362                        let block_ops = parse_ps_block(&block)?;
363                        ops.extend(block_ops);
364                    }
365                } else {
366                    let block_ops = parse_ps_block(&block)?;
367                    ops.extend(block_ops);
368                }
369            }
370            "add" => ops.push(PsOp::Add),
371            "sub" => ops.push(PsOp::Sub),
372            "mul" => ops.push(PsOp::Mul),
373            "div" => ops.push(PsOp::Div),
374            "idiv" => ops.push(PsOp::Idiv),
375            "mod" => ops.push(PsOp::Mod),
376            "neg" => ops.push(PsOp::Neg),
377            "abs" => ops.push(PsOp::Abs),
378            "ceiling" => ops.push(PsOp::Ceiling),
379            "floor" => ops.push(PsOp::Floor),
380            "round" => ops.push(PsOp::Round),
381            "truncate" => ops.push(PsOp::Truncate),
382            "sqrt" => ops.push(PsOp::Sqrt),
383            "exp" => ops.push(PsOp::Exp),
384            "ln" => ops.push(PsOp::Ln),
385            "log" => ops.push(PsOp::Log),
386            "sin" => ops.push(PsOp::Sin),
387            "cos" => ops.push(PsOp::Cos),
388            "atan" => ops.push(PsOp::Atan),
389            "eq" => ops.push(PsOp::Eq),
390            "ne" => ops.push(PsOp::Ne),
391            "gt" => ops.push(PsOp::Gt),
392            "ge" => ops.push(PsOp::Ge),
393            "lt" => ops.push(PsOp::Lt),
394            "le" => ops.push(PsOp::Le),
395            "and" => ops.push(PsOp::And),
396            "or" => ops.push(PsOp::Or),
397            "not" => ops.push(PsOp::Not),
398            "xor" => ops.push(PsOp::Xor),
399            "bitshift" => ops.push(PsOp::Bitshift),
400            "dup" => ops.push(PsOp::Dup),
401            "exch" => ops.push(PsOp::Exch),
402            "pop" => ops.push(PsOp::Pop),
403            "copy" => ops.push(PsOp::Copy),
404            "index" => ops.push(PsOp::Index),
405            "roll" => ops.push(PsOp::Roll),
406            "cvi" => ops.push(PsOp::Cvi),
407            "cvr" => ops.push(PsOp::Cvr),
408            "true" => ops.push(PsOp::True),
409            "false" => ops.push(PsOp::False),
410            "if" | "ifelse" => {} // handled above with blocks
411            _ => {
412                // Try to parse as number
413                if let Ok(n) = token.parse::<f64>() {
414                    ops.push(PsOp::Num(n));
415                }
416                // else: unknown token, skip
417            }
418        }
419    }
420
421    Some(ops)
422}
423
424fn tokenize_ps(code: &str) -> Vec<String> {
425    let mut tokens = Vec::new();
426    let mut current = String::new();
427
428    for ch in code.chars() {
429        match ch {
430            '{' | '}' => {
431                if !current.is_empty() {
432                    tokens.push(std::mem::take(&mut current));
433                }
434                tokens.push(ch.to_string());
435            }
436            ' ' | '\t' | '\n' | '\r' => {
437                if !current.is_empty() {
438                    tokens.push(std::mem::take(&mut current));
439                }
440            }
441            _ => current.push(ch),
442        }
443    }
444    if !current.is_empty() {
445        tokens.push(current);
446    }
447
448    tokens
449}
450
451/// Extract a brace-delimited block from tokens starting at position `start`.
452/// Returns the block content as a string and the position after the closing brace.
453fn extract_block(tokens: &[String], start: usize) -> Option<(String, usize)> {
454    let mut depth = 1;
455    let mut i = start;
456    let mut parts = Vec::new();
457
458    while i < tokens.len() && depth > 0 {
459        if tokens[i] == "{" {
460            depth += 1;
461            parts.push(tokens[i].clone());
462        } else if tokens[i] == "}" {
463            depth -= 1;
464            if depth > 0 {
465                parts.push(tokens[i].clone());
466            }
467        } else {
468            parts.push(tokens[i].clone());
469        }
470        i += 1;
471    }
472
473    Some((parts.join(" "), i))
474}
475
476// ---------------------------------------------------------------------------
477// PostScript calculator executor
478// ---------------------------------------------------------------------------
479
480fn execute_ps_ops(stack: &mut Vec<f64>, ops: &[PsOp]) {
481    for op in ops {
482        match op {
483            PsOp::Num(n) => stack.push(*n),
484            PsOp::True => stack.push(1.0),
485            PsOp::False => stack.push(0.0),
486
487            // Arithmetic
488            PsOp::Add => {
489                if let (Some(b), Some(a)) = (stack.pop(), stack.pop()) {
490                    stack.push(a + b);
491                }
492            }
493            PsOp::Sub => {
494                if let (Some(b), Some(a)) = (stack.pop(), stack.pop()) {
495                    stack.push(a - b);
496                }
497            }
498            PsOp::Mul => {
499                if let (Some(b), Some(a)) = (stack.pop(), stack.pop()) {
500                    stack.push(a * b);
501                }
502            }
503            PsOp::Div => {
504                if let (Some(b), Some(a)) = (stack.pop(), stack.pop()) {
505                    stack.push(if b.abs() > 1e-20 { a / b } else { 0.0 });
506                }
507            }
508            PsOp::Idiv => {
509                if let (Some(b), Some(a)) = (stack.pop(), stack.pop()) {
510                    let bi = b as i64;
511                    let ai = a as i64;
512                    stack.push(if bi != 0 { (ai / bi) as f64 } else { 0.0 });
513                }
514            }
515            PsOp::Mod => {
516                if let (Some(b), Some(a)) = (stack.pop(), stack.pop()) {
517                    let bi = b as i64;
518                    let ai = a as i64;
519                    stack.push(if bi != 0 { (ai % bi) as f64 } else { 0.0 });
520                }
521            }
522            PsOp::Neg => {
523                if let Some(a) = stack.pop() {
524                    stack.push(-a);
525                }
526            }
527            PsOp::Abs => {
528                if let Some(a) = stack.pop() {
529                    stack.push(a.abs());
530                }
531            }
532            PsOp::Ceiling => {
533                if let Some(a) = stack.pop() {
534                    stack.push(a.ceil());
535                }
536            }
537            PsOp::Floor => {
538                if let Some(a) = stack.pop() {
539                    stack.push(a.floor());
540                }
541            }
542            PsOp::Round => {
543                if let Some(a) = stack.pop() {
544                    stack.push(a.round());
545                }
546            }
547            PsOp::Truncate => {
548                if let Some(a) = stack.pop() {
549                    stack.push(a.trunc());
550                }
551            }
552            PsOp::Sqrt => {
553                if let Some(a) = stack.pop() {
554                    stack.push(if a >= 0.0 { a.sqrt() } else { 0.0 });
555                }
556            }
557            PsOp::Exp => {
558                if let (Some(e), Some(base)) = (stack.pop(), stack.pop()) {
559                    stack.push(base.powf(e));
560                }
561            }
562            PsOp::Ln => {
563                if let Some(a) = stack.pop() {
564                    stack.push(if a > 0.0 { a.ln() } else { 0.0 });
565                }
566            }
567            PsOp::Log => {
568                if let Some(a) = stack.pop() {
569                    stack.push(if a > 0.0 { a.log10() } else { 0.0 });
570                }
571            }
572            PsOp::Sin => {
573                if let Some(a) = stack.pop() {
574                    stack.push(a.to_radians().sin());
575                }
576            }
577            PsOp::Cos => {
578                if let Some(a) = stack.pop() {
579                    stack.push(a.to_radians().cos());
580                }
581            }
582            PsOp::Atan => {
583                if let (Some(x), Some(y)) = (stack.pop(), stack.pop()) {
584                    stack.push(y.atan2(x).to_degrees());
585                }
586            }
587
588            // Comparison (push 1.0 for true, 0.0 for false)
589            PsOp::Eq => {
590                if let (Some(b), Some(a)) = (stack.pop(), stack.pop()) {
591                    stack.push(if (a - b).abs() < 1e-10 { 1.0 } else { 0.0 });
592                }
593            }
594            PsOp::Ne => {
595                if let (Some(b), Some(a)) = (stack.pop(), stack.pop()) {
596                    stack.push(if (a - b).abs() >= 1e-10 { 1.0 } else { 0.0 });
597                }
598            }
599            PsOp::Gt => {
600                if let (Some(b), Some(a)) = (stack.pop(), stack.pop()) {
601                    stack.push(if a > b { 1.0 } else { 0.0 });
602                }
603            }
604            PsOp::Ge => {
605                if let (Some(b), Some(a)) = (stack.pop(), stack.pop()) {
606                    stack.push(if a >= b { 1.0 } else { 0.0 });
607                }
608            }
609            PsOp::Lt => {
610                if let (Some(b), Some(a)) = (stack.pop(), stack.pop()) {
611                    stack.push(if a < b { 1.0 } else { 0.0 });
612                }
613            }
614            PsOp::Le => {
615                if let (Some(b), Some(a)) = (stack.pop(), stack.pop()) {
616                    stack.push(if a <= b { 1.0 } else { 0.0 });
617                }
618            }
619
620            // Boolean / bitwise
621            PsOp::And => {
622                if let (Some(b), Some(a)) = (stack.pop(), stack.pop()) {
623                    stack.push(((a as i64) & (b as i64)) as f64);
624                }
625            }
626            PsOp::Or => {
627                if let (Some(b), Some(a)) = (stack.pop(), stack.pop()) {
628                    stack.push(((a as i64) | (b as i64)) as f64);
629                }
630            }
631            PsOp::Not => {
632                if let Some(a) = stack.pop() {
633                    stack.push(if a == 0.0 { 1.0 } else { 0.0 });
634                }
635            }
636            PsOp::Xor => {
637                if let (Some(b), Some(a)) = (stack.pop(), stack.pop()) {
638                    stack.push(((a as i64) ^ (b as i64)) as f64);
639                }
640            }
641            PsOp::Bitshift => {
642                if let (Some(shift), Some(val)) = (stack.pop(), stack.pop()) {
643                    let v = val as i64;
644                    let s = shift as i32;
645                    let result = if s > 0 { v << s } else { v >> (-s) };
646                    stack.push(result as f64);
647                }
648            }
649
650            // Conditional
651            PsOp::If(block) => {
652                if let Some(cond) = stack.pop() {
653                    if cond != 0.0 {
654                        execute_ps_ops(stack, block);
655                    }
656                }
657            }
658            PsOp::IfElse(true_block, false_block) => {
659                if let Some(cond) = stack.pop() {
660                    if cond != 0.0 {
661                        execute_ps_ops(stack, true_block);
662                    } else {
663                        execute_ps_ops(stack, false_block);
664                    }
665                }
666            }
667
668            // Stack manipulation
669            PsOp::Dup => {
670                if let Some(&top) = stack.last() {
671                    stack.push(top);
672                }
673            }
674            PsOp::Exch => {
675                let len = stack.len();
676                if len >= 2 {
677                    stack.swap(len - 1, len - 2);
678                }
679            }
680            PsOp::Pop => {
681                stack.pop();
682            }
683            PsOp::Copy => {
684                if let Some(n) = stack.pop() {
685                    let n = n as usize;
686                    let len = stack.len();
687                    if n <= len {
688                        let start = len - n;
689                        let copied: Vec<f64> = stack[start..].to_vec();
690                        stack.extend_from_slice(&copied);
691                    }
692                }
693            }
694            PsOp::Index => {
695                if let Some(n) = stack.pop() {
696                    let n = n as usize;
697                    let len = stack.len();
698                    if n < len {
699                        let val = stack[len - 1 - n];
700                        stack.push(val);
701                    }
702                }
703            }
704            PsOp::Roll => {
705                if let (Some(j), Some(n)) = (stack.pop(), stack.pop()) {
706                    let n = n as usize;
707                    let j = j as i64;
708                    let len = stack.len();
709                    if n > 0 && n <= len {
710                        let start = len - n;
711                        let j = ((j % n as i64) + n as i64) as usize % n;
712                        let split = n - j;
713                        let mut temp: Vec<f64> = stack[start..].to_vec();
714                        temp.rotate_left(split);
715                        stack[start..].copy_from_slice(&temp);
716                    }
717                }
718            }
719
720            // Conversion
721            PsOp::Cvi => {
722                if let Some(a) = stack.pop() {
723                    stack.push((a as i64) as f64);
724                }
725            }
726            PsOp::Cvr => {
727                // Already f64, no-op
728            }
729        }
730    }
731}
732
733#[cfg(test)]
734mod tests {
735    use super::*;
736
737    #[test]
738    fn test_ps_simple_add() {
739        let func = PdfFunction::PostScript {
740            domain: vec![(0.0, 1.0)],
741            range: vec![(0.0, 2.0)],
742            ops: vec![PsOp::Num(1.0), PsOp::Add],
743        };
744        let result = func.evaluate(&[0.5]);
745        assert!((result[0] - 1.5).abs() < 0.001);
746    }
747
748    #[test]
749    fn test_ps_mul_sub() {
750        let func = PdfFunction::PostScript {
751            domain: vec![(0.0, 1.0)],
752            range: vec![(0.0, 1.0)],
753            ops: vec![PsOp::Num(2.0), PsOp::Mul, PsOp::Num(0.5), PsOp::Sub],
754        };
755        // input 0.75 → 0.75*2 - 0.5 = 1.0
756        let result = func.evaluate(&[0.75]);
757        assert!((result[0] - 1.0).abs() < 0.001);
758    }
759
760    #[test]
761    fn test_ps_dup() {
762        let func = PdfFunction::PostScript {
763            domain: vec![(0.0, 1.0)],
764            range: vec![(0.0, 1.0), (0.0, 1.0)],
765            ops: vec![PsOp::Dup],
766        };
767        let result = func.evaluate(&[0.7]);
768        assert_eq!(result.len(), 2);
769        assert!((result[0] - 0.7).abs() < 0.001);
770        assert!((result[1] - 0.7).abs() < 0.001);
771    }
772
773    #[test]
774    fn test_ps_if() {
775        // { dup 0.5 gt { 1.0 } { 0.0 } ifelse }
776        let func = PdfFunction::PostScript {
777            domain: vec![(0.0, 1.0)],
778            range: vec![(0.0, 1.0)],
779            ops: vec![
780                PsOp::Dup,
781                PsOp::Num(0.5),
782                PsOp::Gt,
783                PsOp::IfElse(vec![PsOp::Pop, PsOp::Num(1.0)], vec![PsOp::Pop, PsOp::Num(0.0)]),
784            ],
785        };
786        assert!((func.evaluate(&[0.8])[0] - 1.0).abs() < 0.001);
787        assert!((func.evaluate(&[0.3])[0] - 0.0).abs() < 0.001);
788    }
789
790    #[test]
791    fn test_ps_neg_abs() {
792        let func = PdfFunction::PostScript {
793            domain: vec![(-1.0, 1.0)],
794            range: vec![(0.0, 1.0)],
795            ops: vec![PsOp::Neg, PsOp::Abs],
796        };
797        assert!((func.evaluate(&[-0.5])[0] - 0.5).abs() < 0.001);
798        assert!((func.evaluate(&[0.3])[0] - 0.3).abs() < 0.001);
799    }
800
801    #[test]
802    fn test_exponential() {
803        let func = PdfFunction::Exponential {
804            domain: vec![(0.0, 1.0)],
805            range: vec![(0.0, 1.0)],
806            c0: vec![0.0],
807            c1: vec![1.0],
808            n: 2.0,
809        };
810        // f(0.5) = 0 + 0.5^2 * (1 - 0) = 0.25
811        assert!((func.evaluate(&[0.5])[0] - 0.25).abs() < 0.001);
812    }
813
814    #[test]
815    fn test_parse_ps_code() {
816        let code = "{ 1 add 2 mul }";
817        let ops = parse_ps_code(code).unwrap();
818        assert_eq!(ops.len(), 4); // Num(1), Add, Num(2), Mul
819    }
820
821    #[test]
822    fn test_ps_trig() {
823        let func = PdfFunction::PostScript {
824            domain: vec![(0.0, 360.0)],
825            range: vec![(-1.0, 1.0)],
826            ops: vec![PsOp::Sin],
827        };
828        // sin(90°) = 1.0
829        assert!((func.evaluate(&[90.0])[0] - 1.0).abs() < 0.001);
830        // sin(0°) = 0.0
831        assert!((func.evaluate(&[0.0])[0] - 0.0).abs() < 0.001);
832    }
833
834    #[test]
835    fn test_ps_exch_roll() {
836        let func = PdfFunction::PostScript {
837            domain: vec![(0.0, 1.0), (0.0, 1.0)],
838            range: vec![(0.0, 1.0), (0.0, 1.0)],
839            ops: vec![PsOp::Exch],
840        };
841        let result = func.evaluate(&[0.2, 0.8]);
842        assert!((result[0] - 0.8).abs() < 0.001);
843        assert!((result[1] - 0.2).abs() < 0.001);
844    }
845}