Skip to main content

pdfrum_page/function/
mod.rs

1//! PDF functions, types 0, 2, 3 and 4 (ISO 32000-1 §7.10).
2//!
3//! **Type 1 does not exist here**, because it does not exist in PDFium
4//! either: the type dispatcher accepts exactly 0, 2, 3 and 4 and rejects
5//! everything else, so a `/FunctionType 1` sampled-spline function loads as
6//! nothing at all.
7//!
8//! # `/Domain` is the one universally required key
9//!
10//! Every type needs it, and `inputs = len(Domain) / 2` — integer division, so
11//! an odd-length array truncates and a one-element array yields **zero**
12//! inputs, which fails the load. `/Range` is required only for types 0 and 4.
13//!
14//! # Nothing is capped in the C++
15//!
16//! There is no limit on input count, output count, or type-3 nesting depth;
17//! recursion is bounded only by a cycle set and, in practice, by the native
18//! stack. A `Vec` sized from untrusted data is a denial-of-service vector
19//! Rust must not accept, so [`FunctionCache::load`] enforces
20//! [`MAX_DEPTH`] and [`MAX_OUTPUTS`]. Exceeding either fails the load, which
21//! is the same observable outcome as the C++'s stack overflow but survivable.
22
23mod exponential;
24mod postscript;
25mod sampled;
26mod stitching;
27
28pub(crate) use sampled::BitReader;
29
30pub(crate) use exponential::Exponential;
31pub use postscript::{PostScript, parse_program};
32pub(crate) use sampled::Sampled;
33pub(crate) use stitching::Stitching;
34
35use crate::error::Error;
36use crate::names;
37use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
38use pdfrum_object::{Dict, ObjRef, Object, Resolve};
39use std::collections::{HashMap, HashSet};
40use std::sync::Arc;
41
42/// How deep type-3 stitching may nest before the load fails.
43pub const MAX_DEPTH: u32 = 32;
44
45/// The largest output count a function may declare.
46pub const MAX_OUTPUTS: usize = 1024;
47
48/// A loaded PDF function.
49#[derive(Debug, Clone, PartialEq)]
50#[non_exhaustive]
51pub enum Function {
52    /// Type 0: samples on a regular grid, interpolated.
53    Sampled(Sampled),
54    /// Type 2: `C0 + x^N * (C1 - C0)`.
55    Exponential(Exponential),
56    /// Type 3: sub-functions stitched over sub-intervals of the domain.
57    Stitching(Stitching),
58    /// Type 4: a small PostScript calculator program.
59    PostScript(PostScript),
60}
61
62impl Function {
63    /// How many inputs the function takes.
64    #[must_use]
65    pub fn input_count(&self) -> usize {
66        self.domain().len() / 2
67    }
68
69    /// How many outputs the function produces.
70    #[must_use]
71    pub fn output_count(&self) -> usize {
72        match self {
73            Self::Sampled(f) => f.outputs,
74            Self::Exponential(f) => f.outputs,
75            Self::Stitching(f) => f.outputs,
76            Self::PostScript(f) => f.outputs,
77        }
78    }
79
80    /// The input intervals, `[lo0, hi0, lo1, hi1, …]`.
81    #[must_use]
82    pub fn domain(&self) -> &[f32] {
83        match self {
84            Self::Sampled(f) => &f.domain,
85            Self::Exponential(f) => &f.domain,
86            Self::Stitching(f) => &f.domain,
87            Self::PostScript(f) => &f.domain,
88        }
89    }
90
91    /// The output intervals, empty when `/Range` was absent.
92    ///
93    /// An empty range **skips output clamping entirely**, which is how a
94    /// type 2 function with a negative exponent can return an infinity.
95    #[must_use]
96    pub fn range(&self) -> &[f32] {
97        match self {
98            Self::Sampled(f) => &f.range,
99            Self::Exponential(f) => &f.range,
100            Self::Stitching(f) => &f.range,
101            Self::PostScript(f) => &f.range,
102        }
103    }
104
105    /// Evaluate the function.
106    ///
107    /// # Errors
108    ///
109    /// [`Error::FunctionArity`] when `input.len()` is not the declared input
110    /// count or `out` is shorter than the declared output count, and
111    /// [`Error::FunctionInterval`] when a `/Domain` or `/Range` interval has
112    /// its bounds the wrong way round — both of which PDFium reports by
113    /// returning "no result" and painting nothing.
114    ///
115    /// ```
116    /// # use pdfrum_common::{Diagnostics, Limits};
117    /// # use pdfrum_object::{Array, Dict, Name, NoResolve, Object};
118    /// # use pdfrum_page::FunctionCache;
119    /// let dict = Dict::from_pairs([
120    ///     (Name::from("FunctionType"), Object::Int(2)),
121    ///     (Name::from("Domain"), Object::Array(Array::of([Object::Int(0), Object::Int(1)]))),
122    ///     (Name::from("N"), Object::Int(1)),
123    /// ]);
124    /// let mut cache = FunctionCache::new();
125    /// let mut diags = Diagnostics::default();
126    /// let f = cache
127    ///     .load(&Object::Dict(dict), &NoResolve, &Limits::default(), &mut diags)
128    ///     .expect("a type 2 function");
129    ///
130    /// let mut out = [0.0f32];
131    /// f.eval(&[0.25], &mut out).expect("evaluates");
132    /// assert!((out[0] - 0.25).abs() < 1e-6);
133    /// ```
134    pub fn eval(&self, input: &[f32], out: &mut [f32]) -> Result<(), Error> {
135        let inputs = self.input_count();
136        let outputs = self.output_count();
137        if input.len() != inputs || out.len() < outputs {
138            return Err(Error::FunctionArity {
139                expected: inputs,
140                got: input.len(),
141                outputs,
142                got_outputs: out.len(),
143            });
144        }
145        // Clamp each input into its domain interval, refusing an inverted one.
146        let mut clamped = Vec::with_capacity(inputs);
147        let domain = self.domain();
148        for (i, v) in input.iter().enumerate() {
149            let lo = domain.get(i * 2).copied().unwrap_or(0.0);
150            let hi = domain.get(i * 2 + 1).copied().unwrap_or(0.0);
151            if lo > hi {
152                return Err(Error::FunctionInterval);
153            }
154            clamped.push(v.clamp(lo, hi));
155        }
156
157        if !self.eval_raw(&clamped, out) {
158            return Err(Error::FunctionInterval);
159        }
160
161        // An empty `/Range` skips clamping entirely.
162        let range = self.range();
163        if range.is_empty() {
164            return Ok(());
165        }
166        for i in 0..outputs {
167            let lo = range.get(i * 2).copied().unwrap_or(0.0);
168            let hi = range.get(i * 2 + 1).copied().unwrap_or(0.0);
169            if lo > hi {
170                return Err(Error::FunctionInterval);
171            }
172            if let Some(slot) = out.get_mut(i) {
173                *slot = slot.clamp(lo, hi);
174            }
175        }
176        Ok(())
177    }
178
179    /// Evaluate, returning the number of outputs written and **0** on any
180    /// failure.
181    ///
182    /// This is the shape every colorspace and shading call site wants: the
183    /// C++ returns an optional count, and zero means "paint nothing".
184    #[must_use]
185    pub fn eval_into(&self, input: &[f32], out: &mut [f32]) -> usize {
186        match self.eval(input, out) {
187            Ok(()) => self.output_count(),
188            Err(_) => 0,
189        }
190    }
191
192    /// The per-type evaluation, after domain clamping. `false` means the
193    /// function refused.
194    fn eval_raw(&self, input: &[f32], out: &mut [f32]) -> bool {
195        match self {
196            Self::Sampled(f) => f.eval(input, out),
197            Self::Exponential(f) => f.eval(input, out),
198            Self::Stitching(f) => f.eval(input, out),
199            Self::PostScript(f) => f.eval(input, out),
200        }
201    }
202}
203
204/// Linear interpolation with PDFium's degenerate-interval rule: a zero-width
205/// input interval yields `ymin` rather than a division by zero.
206#[must_use]
207pub fn interpolate(x: f32, xmin: f32, xmax: f32, ymin: f32, ymax: f32) -> f32 {
208    let divisor = xmax - xmin;
209    if divisor == 0.0 {
210        return ymin;
211    }
212    ymin + (x - xmin) * (ymax - ymin) / divisor
213}
214
215/// Session-scoped function memoization, keyed on the reference that named the
216/// function.
217#[derive(Debug, Default)]
218pub struct FunctionCache {
219    entries: HashMap<ObjRef, Option<Arc<Function>>>,
220    /// References on the current load path, which is the cycle guard.
221    in_flight: HashSet<ObjRef>,
222}
223
224impl FunctionCache {
225    /// An empty cache.
226    #[must_use]
227    pub fn new() -> Self {
228        Self::default()
229    }
230
231    /// How many functions have been loaded through this cache.
232    #[must_use]
233    pub fn len(&self) -> usize {
234        self.entries.len()
235    }
236
237    /// Whether nothing has been loaded yet.
238    #[must_use]
239    pub fn is_empty(&self) -> bool {
240        self.entries.is_empty()
241    }
242
243    /// Load the function `obj` names, or `None` for every failure.
244    #[must_use]
245    pub fn load<R: Resolve>(
246        &mut self,
247        obj: &Object,
248        r: &R,
249        limits: &Limits,
250        diags: &mut Diagnostics,
251    ) -> Option<Arc<Function>> {
252        let loaded = self.load_at(obj, r, limits, diags, 0);
253        if loaded.is_none() {
254            diags.record(Severity::Suspicious, DiagKind::FunctionUnsupported, None);
255        }
256        loaded
257    }
258
259    fn load_at<R: Resolve>(
260        &mut self,
261        obj: &Object,
262        r: &R,
263        limits: &Limits,
264        diags: &mut Diagnostics,
265        depth: u32,
266    ) -> Option<Arc<Function>> {
267        if depth > MAX_DEPTH {
268            return None;
269        }
270        let reference = obj.as_ref_id();
271        if let Some(id) = reference {
272            if let Some(hit) = self.entries.get(&id) {
273                return hit.clone();
274            }
275            // A cycle: the same function reachable through disjoint branches
276            // is fine, but a true loop is not.
277            if !self.in_flight.insert(id) {
278                return None;
279            }
280        }
281        let resolved = obj.resolve(r).ok();
282        let built = resolved
283            .as_deref()
284            .and_then(|direct| self.build(direct, r, limits, diags, depth))
285            .map(Arc::new);
286        if let Some(id) = reference {
287            self.in_flight.remove(&id);
288            self.entries.insert(id, built.clone());
289        }
290        built
291    }
292
293    fn build<R: Resolve>(
294        &mut self,
295        obj: &Object,
296        r: &R,
297        limits: &Limits,
298        diags: &mut Diagnostics,
299        depth: u32,
300    ) -> Option<Function> {
301        let dict = match obj {
302            Object::Dict(d) => d,
303            Object::Stream(s) => &s.dict,
304            _ => return None,
305        };
306        let kind = dict.int(names::FUNCTION_TYPE, r)?;
307        let common = Common::load(dict, r)?;
308        match kind {
309            0 => obj
310                .as_stream()
311                .and_then(|s| Sampled::load(s, &common, r, limits, diags))
312                .map(Function::Sampled),
313            2 => Exponential::load(dict, &common, r).map(Function::Exponential),
314            3 => Stitching::load(dict, &common, r, self, limits, diags, depth)
315                .map(Function::Stitching),
316            4 => obj
317                .as_stream()
318                .and_then(|s| PostScript::load(s, &common, r, limits, diags))
319                .map(Function::PostScript),
320            // 1 is not a typo: PDFium implements no type 1.
321            _ => None,
322        }
323    }
324}
325
326/// The `/Domain` and `/Range` every type shares.
327pub(crate) struct Common {
328    pub(crate) domain: Box<[f32]>,
329    pub(crate) range: Box<[f32]>,
330}
331
332impl Common {
333    /// `/Domain` is required and must describe at least one input;
334    /// `/Range` is optional here and checked per type.
335    fn load(dict: &Dict, r: &impl Resolve) -> Option<Self> {
336        let domain_array = dict.array(names::DOMAIN, r)?;
337        let inputs = domain_array.len() / 2;
338        if inputs == 0 {
339            return None;
340        }
341        let domain = (0..inputs * 2)
342            .map(|i| domain_array.number_at_or_zero(i))
343            .collect();
344        let range = match dict.array(names::RANGE, r) {
345            Some(a) => {
346                let outputs = a.len() / 2;
347                if outputs > MAX_OUTPUTS {
348                    return None;
349                }
350                (0..outputs * 2).map(|i| a.number_at_or_zero(i)).collect()
351            }
352            None => Box::default(),
353        };
354        Some(Self { domain, range })
355    }
356
357    pub(crate) fn inputs(&self) -> usize {
358        self.domain.len() / 2
359    }
360
361    pub(crate) fn outputs(&self) -> usize {
362        self.range.len() / 2
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    // Test fixtures quote the oracle's own vectors, compare floats exactly
369    // where the behaviour being pinned is exact, and index arrays whose
370    // length the fixture itself fixes.
371    #![allow(
372        clippy::unreadable_literal,
373        clippy::float_cmp,
374        clippy::indexing_slicing,
375        clippy::cast_precision_loss,
376        clippy::cast_possible_truncation,
377        reason = "test fixtures quote oracle vectors verbatim and compare exactly"
378    )]
379
380    use super::{Function, FunctionCache, interpolate};
381    use pdfrum_common::{Diagnostics, Limits};
382    use pdfrum_object::{Array, Dict, Name, NoResolve, Object};
383
384    pub(super) fn nums(values: &[f32]) -> Object {
385        Object::Array(Array::of(values.iter().copied().map(Object::Real)))
386    }
387
388    fn load(dict: Dict) -> Option<Function> {
389        let mut cache = FunctionCache::new();
390        let mut diags = Diagnostics::default();
391        cache
392            .load(
393                &Object::Dict(dict),
394                &NoResolve,
395                &Limits::default(),
396                &mut diags,
397            )
398            .map(|f| (*f).clone())
399    }
400
401    #[test]
402    fn interpolation_survives_a_degenerate_interval() {
403        assert!((interpolate(5.0, 0.0, 10.0, 0.0, 1.0) - 0.5).abs() < 1e-6);
404        // A zero-width input interval yields ymin, not NaN.
405        assert!((interpolate(5.0, 3.0, 3.0, 7.0, 9.0) - 7.0).abs() < 1e-6);
406    }
407
408    #[test]
409    fn only_types_zero_two_three_and_four_load() {
410        for kind in [-2i64, 1, 5, 100] {
411            let dict = Dict::from_pairs([
412                (Name::from("FunctionType"), Object::Int(kind)),
413                (Name::from("Domain"), nums(&[0.0, 1.0])),
414                (Name::from("N"), Object::Int(1)),
415            ]);
416            assert!(load(dict).is_none(), "type {kind} should not load");
417        }
418    }
419
420    #[test]
421    fn domain_is_required_and_must_describe_an_input() {
422        // Missing entirely.
423        let dict = Dict::from_pairs([
424            (Name::from("FunctionType"), Object::Int(2)),
425            (Name::from("N"), Object::Int(1)),
426        ]);
427        assert!(load(dict).is_none());
428
429        // Empty, so zero inputs.
430        let dict = Dict::from_pairs([
431            (Name::from("FunctionType"), Object::Int(2)),
432            (Name::from("Domain"), nums(&[])),
433            (Name::from("N"), Object::Int(1)),
434        ]);
435        assert!(load(dict).is_none());
436
437        // One element also truncates to zero inputs.
438        let dict = Dict::from_pairs([
439            (Name::from("FunctionType"), Object::Int(2)),
440            (Name::from("Domain"), nums(&[0.0])),
441            (Name::from("N"), Object::Int(1)),
442        ]);
443        assert!(load(dict).is_none());
444    }
445
446    #[test]
447    fn arity_mismatches_are_errors_not_panics() {
448        let dict = Dict::from_pairs([
449            (Name::from("FunctionType"), Object::Int(2)),
450            (Name::from("Domain"), nums(&[0.0, 1.0])),
451            (Name::from("N"), Object::Int(1)),
452        ]);
453        let f = load(dict).expect("a type 2 function");
454        let mut out = [0.0f32; 4];
455        assert!(f.eval(&[0.5, 0.5], &mut out).is_err());
456        assert_eq!(f.eval_into(&[0.5, 0.5], &mut out), 0);
457        assert!(f.eval(&[0.5], &mut []).is_err());
458    }
459}