Skip to main content

pdfrum_type1/
charstring.rs

1//! The Type 1 charstring interpreter (Type 1 specification §6).
2//!
3//! A charstring is a stack machine whose program is a byte string: values
4//! 32–255 encode numbers, 0–31 encode operators, and `12 x` escapes to a
5//! second operator page. The machine draws exactly one glyph and reports its
6//! advance width.
7//!
8//! Three things make Type 1 charstrings more than a trivial decoder, and all
9//! three are here because the Foxit fallback faces use them:
10//!
11//! - **`callothersubr`** is a callback into PostScript procedures the font
12//!   ships. Numbers 0–3 are standardised (flex, hint replacement) and readers
13//!   *emulate* them rather than run the PostScript; 14–18 are the Multiple
14//!   Master blend, which folds `n` operand tuples into one using the weight
15//!   vector. Results come back through `pop`.
16//! - **`flex`** (othersubr 0/1/2) replaces seven `rmoveto`s that would
17//!   otherwise be a jagged near-straight line with two curves.
18//! - **`seac`** composes an accented glyph from two others named through
19//!   `StandardEncoding`, positioned by the difference of their side bearings.
20//!
21//! The interpreter is a pure function of `(charstring bytes, environment)` to
22//! a [`Glyph`]; it holds no state between glyphs.
23
24use crate::blend::Blend;
25use crate::encoding;
26use pdfrum_common::kurbo::{BezPath, Point};
27
28/// Maximum `callsubr` nesting. The specification says 10; broken fonts recurse
29/// deeper and a cap is what stops a hostile one from exhausting the stack.
30const MAX_DEPTH: u32 = 30;
31/// The interpreter's operand stack is 48 entries in the specification; the
32/// Multiple Master blend pushes up to `n_points * n_masters` at once, so this
33/// is generous rather than exact.
34const MAX_STACK: usize = 192;
35/// Ceiling on emitted path segments, so a charstring that loops through
36/// subroutines cannot grow a `BezPath` without bound.
37const MAX_SEGMENTS: usize = 65_536;
38
39/// Everything a charstring needs from the font around it.
40///
41/// A borrowed view rather than a reference to the font, because `seac` needs
42/// to interpret *other* charstrings while the outer one is mid-flight — which
43/// with a `&Type1Font` would be fine, but with the font's own outline cache in
44/// scope would not.
45#[derive(Clone, Copy)]
46pub(crate) struct Env<'a> {
47    /// The `/Subrs` array, already decrypted.
48    pub subrs: &'a [Vec<u8>],
49    /// Charstrings by glyph index, already decrypted.
50    pub charstrings: &'a [Vec<u8>],
51    /// Glyph index for a name — `seac` and nothing else.
52    pub name_lookup: &'a dyn Fn(&str) -> Option<usize>,
53    /// The active weight vector, empty for a non-Multiple-Master font.
54    pub weights: &'a [f32],
55    /// The font's Multiple-Master declaration, if any. Present so the
56    /// interpreter can tell "no blend requested" from "blend requested with
57    /// the wrong arity".
58    pub blend: Option<&'a Blend>,
59}
60
61/// One interpreted glyph.
62#[derive(Debug, Clone, PartialEq)]
63pub struct Glyph {
64    /// The outline in font units (before the `/FontMatrix`).
65    pub path: BezPath,
66    /// Advance width in font units, from `hsbw` or `sbw`.
67    pub advance: f32,
68    /// Left side bearing, which `seac` needs and metrics consumers want.
69    pub left_side_bearing: f32,
70}
71
72/// Why interpretation stopped early. The partial path is kept regardless; this
73/// only decides whether a diagnostic is recorded.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub(crate) enum Abort {
76    /// Ran off the end without `endchar`.
77    Truncated,
78    /// An operator wanted operands that were not there.
79    StackUnderflow,
80    /// `callsubr` named a subroutine the font does not have.
81    MissingSubr,
82    /// Nesting cap hit.
83    TooDeep,
84    /// A blend was requested but the operand count did not match the weight
85    /// vector.
86    BadBlend,
87    /// `seac` named a component the font does not have.
88    BadSeac,
89    /// The path grew past `MAX_SEGMENTS`.
90    TooBig,
91}
92
93/// Interpret one charstring.
94///
95/// Returns the glyph and, when interpretation could not finish, why. The glyph
96/// is always usable: a charstring that aborts halfway yields the outline built
97/// so far, which is what a rasterizer wants and what FreeType produces.
98pub(crate) fn interpret(code: &[u8], env: Env<'_>) -> (Glyph, Option<Abort>) {
99    let mut m = Machine::new(env);
100    let abort = m.run(code, 0).err();
101    m.finish();
102    (
103        Glyph {
104            path: m.path,
105            advance: m.advance,
106            left_side_bearing: m.lsb,
107        },
108        abort,
109    )
110}
111
112struct Machine<'a> {
113    env: Env<'a>,
114    stack: Vec<f64>,
115    /// The PostScript operand stack `callothersubr` results are `pop`ped from.
116    ps_stack: Vec<f64>,
117    path: BezPath,
118    open: bool,
119    current: Point,
120    /// Where the current subpath started, so `closepath` is a real close.
121    start: Point,
122    advance: f32,
123    lsb: f32,
124    segments: usize,
125    /// Non-`None` while a flex is being collected: the reference point plus
126    /// the seven points othersubr 1/2 accumulate.
127    flex: Option<Vec<Point>>,
128    /// `seac` may only run once and ends the charstring.
129    done: bool,
130}
131
132impl<'a> Machine<'a> {
133    fn new(env: Env<'a>) -> Self {
134        Self {
135            env,
136            stack: Vec::new(),
137            ps_stack: Vec::new(),
138            path: BezPath::new(),
139            open: false,
140            current: Point::ZERO,
141            start: Point::ZERO,
142            advance: 0.0,
143            lsb: 0.0,
144            segments: 0,
145            flex: None,
146            done: false,
147        }
148    }
149
150    fn finish(&mut self) {
151        if self.open {
152            self.path.close_path();
153            self.open = false;
154        }
155    }
156
157    fn push(&mut self, v: f64) -> Result<(), Abort> {
158        if self.stack.len() >= MAX_STACK {
159            return Err(Abort::StackUnderflow);
160        }
161        self.stack.push(v);
162        Ok(())
163    }
164
165    /// Take the last `n` operands, leaving the stack empty.
166    ///
167    /// Type 1 operators clear the stack, and — crucially — take their operands
168    /// from the *bottom* when more were pushed than they need, because a
169    /// hint-replacement subroutine leaves residue behind. FreeType reads from
170    /// the bottom for the path operators; doing otherwise misplaces glyphs in
171    /// exactly the fonts that use hint replacement.
172    fn take(&mut self, n: usize) -> Result<Vec<f64>, Abort> {
173        if self.stack.len() < n {
174            self.stack.clear();
175            return Err(Abort::StackUnderflow);
176        }
177        let args = self.stack.get(..n).unwrap_or_default().to_vec();
178        self.stack.clear();
179        Ok(args)
180    }
181
182    fn grow(&mut self) -> Result<(), Abort> {
183        self.segments = self.segments.saturating_add(1);
184        if self.segments > MAX_SEGMENTS {
185            return Err(Abort::TooBig);
186        }
187        Ok(())
188    }
189
190    fn move_to(&mut self, p: Point) -> Result<(), Abort> {
191        self.current = p;
192        // Inside a flex, the seven `rmoveto`s are data points, not moves.
193        if let Some(points) = self.flex.as_mut() {
194            points.push(p);
195            return Ok(());
196        }
197        self.grow()?;
198        if self.open {
199            self.path.close_path();
200        }
201        self.path.move_to(p);
202        self.start = p;
203        self.open = true;
204        Ok(())
205    }
206
207    fn line_to(&mut self, p: Point) -> Result<(), Abort> {
208        self.grow()?;
209        if !self.open {
210            self.path.move_to(self.current);
211            self.start = self.current;
212            self.open = true;
213        }
214        self.current = p;
215        self.path.line_to(p);
216        Ok(())
217    }
218
219    fn curve_to(&mut self, c1: Point, c2: Point, p: Point) -> Result<(), Abort> {
220        self.grow()?;
221        if !self.open {
222            self.path.move_to(self.current);
223            self.start = self.current;
224            self.open = true;
225        }
226        self.current = p;
227        self.path.curve_to(c1, c2, p);
228        Ok(())
229    }
230
231    fn close(&mut self) {
232        // Type 1's `closepath` closes the subpath but leaves the current point
233        // where it is — a following `rmoveto` is relative to that, not to the
234        // subpath start. Getting this wrong shifts every glyph with more than
235        // one contour.
236        if self.open {
237            self.path.close_path();
238            self.open = false;
239        }
240    }
241
242    fn run(&mut self, code: &[u8], depth: u32) -> Result<(), Abort> {
243        if depth > MAX_DEPTH {
244            return Err(Abort::TooDeep);
245        }
246        let mut i = 0usize;
247        while let Some(&b) = code.get(i) {
248            i = i.saturating_add(1);
249            match b {
250                // Number encodings (Type 1 specification §6.2).
251                32..=246 => self.push(f64::from(i16::from(b) - 139))?,
252                247..=250 => {
253                    let w = *code.get(i).ok_or(Abort::Truncated)?;
254                    i = i.saturating_add(1);
255                    self.push(f64::from((i16::from(b) - 247) * 256 + i16::from(w) + 108))?;
256                }
257                251..=254 => {
258                    let w = *code.get(i).ok_or(Abort::Truncated)?;
259                    i = i.saturating_add(1);
260                    self.push(f64::from(-(i16::from(b) - 251) * 256 - i16::from(w) - 108))?;
261                }
262                255 => {
263                    let bytes = code.get(i..i.saturating_add(4)).ok_or(Abort::Truncated)?;
264                    i = i.saturating_add(4);
265                    let mut v: i32 = 0;
266                    for &x in bytes {
267                        v = (v << 8) | i32::from(x);
268                    }
269                    self.push(f64::from(v))?;
270                }
271                12 => {
272                    let esc = *code.get(i).ok_or(Abort::Truncated)?;
273                    i = i.saturating_add(1);
274                    if self.escaped(esc, depth)? {
275                        return Ok(());
276                    }
277                }
278                _ => {
279                    if self.operator(b, code, &mut i, depth)? {
280                        return Ok(());
281                    }
282                }
283            }
284            if self.done {
285                return Ok(());
286            }
287        }
288        // Falling off the end without `endchar` or `return` is how a great
289        // many real subroutines finish; only a top-level charstring doing it
290        // is worth reporting.
291        if depth == 0 {
292            return Err(Abort::Truncated);
293        }
294        Ok(())
295    }
296
297    /// One single-byte operator. `Ok(true)` means "stop this charstring".
298    fn operator(&mut self, op: u8, code: &[u8], i: &mut usize, depth: u32) -> Result<bool, Abort> {
299        match op {
300            // hstem / vstem: hints, which we do not apply (we render filled
301            // unhinted outlines) but must consume so the stack is clean.
302            1 | 3 => {
303                self.stack.clear();
304            }
305            4 => {
306                // vmoveto: dy
307                let a = self.take(1)?;
308                let dy = a.first().copied().unwrap_or(0.0);
309                self.move_to(Point::new(self.current.x, self.current.y + dy))?;
310            }
311            5 => {
312                let a = self.take(2)?;
313                let (dx, dy) = (arg(&a, 0), arg(&a, 1));
314                self.line_to(Point::new(self.current.x + dx, self.current.y + dy))?;
315            }
316            6 => {
317                let a = self.take(1)?;
318                self.line_to(Point::new(self.current.x + arg(&a, 0), self.current.y))?;
319            }
320            7 => {
321                let a = self.take(1)?;
322                self.line_to(Point::new(self.current.x, self.current.y + arg(&a, 0)))?;
323            }
324            8 => {
325                let a = self.take(6)?;
326                self.relative_curve(
327                    arg(&a, 0),
328                    arg(&a, 1),
329                    arg(&a, 2),
330                    arg(&a, 3),
331                    arg(&a, 4),
332                    arg(&a, 5),
333                )?;
334            }
335            9 => {
336                self.stack.clear();
337                self.close();
338            }
339            10 => {
340                // callsubr: the index is the *last* operand, since a hint
341                // replacement leaves `subr# 4 callothersubr pop callsubr` with
342                // extra values below it.
343                let idx = self.stack.pop().ok_or(Abort::StackUnderflow)?;
344                let sub = usize::try_from(idx as i64)
345                    .ok()
346                    .and_then(|n| self.env.subrs.get(n))
347                    .ok_or(Abort::MissingSubr)?;
348                // Cloning the subroutine avoids borrowing `self.env` across
349                // the `&mut self` call. Subroutines are tens of bytes.
350                let sub = sub.clone();
351                self.run(&sub, depth.saturating_add(1))?;
352            }
353            11 => return Ok(true), // return
354            13 => {
355                // hsbw: sbx wx
356                let a = self.take(2)?;
357                self.lsb = arg(&a, 0) as f32;
358                self.advance = arg(&a, 1) as f32;
359                // The origin moves to the left side bearing; every subsequent
360                // relative move is from there.
361                self.current = Point::new(arg(&a, 0), 0.0);
362                self.start = self.current;
363            }
364            14 => {
365                self.done = true;
366                return Ok(true); // endchar
367            }
368            21 => {
369                let a = self.take(2)?;
370                self.move_to(Point::new(
371                    self.current.x + arg(&a, 0),
372                    self.current.y + arg(&a, 1),
373                ))?;
374            }
375            22 => {
376                let a = self.take(1)?;
377                self.move_to(Point::new(self.current.x + arg(&a, 0), self.current.y))?;
378            }
379            30 => {
380                // vhcurveto: dy1 dx2 dy2 dx3
381                let a = self.take(4)?;
382                self.relative_curve(0.0, arg(&a, 0), arg(&a, 1), arg(&a, 2), arg(&a, 3), 0.0)?;
383            }
384            31 => {
385                // hvcurveto: dx1 dx2 dy2 dy3
386                let a = self.take(4)?;
387                self.relative_curve(arg(&a, 0), 0.0, arg(&a, 1), arg(&a, 2), 0.0, arg(&a, 3))?;
388            }
389            // 0, 2, 15..=20, 23..=29 are reserved. A font using one is
390            // damaged; clearing the stack and carrying on recovers more than
391            // aborting does, and matches FreeType's "unknown operator" arm.
392            _ => {
393                let _ = (code, i);
394                self.stack.clear();
395            }
396        }
397        Ok(false)
398    }
399
400    /// A `12 x` two-byte operator.
401    fn escaped(&mut self, esc: u8, depth: u32) -> Result<bool, Abort> {
402        match esc {
403            6 => {
404                // seac: asb adx ady bchar achar
405                let a = self.take(5)?;
406                self.seac(
407                    arg(&a, 0),
408                    arg(&a, 1),
409                    arg(&a, 2),
410                    arg(&a, 3),
411                    arg(&a, 4),
412                    depth,
413                )?;
414                self.done = true;
415                return Ok(true);
416            }
417            7 => {
418                // sbw: sbx sby wx wy
419                let a = self.take(4)?;
420                self.lsb = arg(&a, 0) as f32;
421                self.advance = arg(&a, 2) as f32;
422                self.current = Point::new(arg(&a, 0), arg(&a, 1));
423                self.start = self.current;
424            }
425            12 => {
426                // div: divisor stays on the stack, so this does *not* clear.
427                let b = self.stack.pop().ok_or(Abort::StackUnderflow)?;
428                let a = self.stack.pop().ok_or(Abort::StackUnderflow)?;
429                self.push(if b == 0.0 { 0.0 } else { a / b })?;
430            }
431            16 => self.call_other_subr()?,
432            17 => {
433                // pop: take one result the othersubr left behind. A font that
434                // pops more than was produced gets zero, which is what
435                // FreeType hands back and keeps hint replacement working.
436                let v = self.ps_stack.pop().unwrap_or(0.0);
437                self.push(v)?;
438            }
439            33 => {
440                // setcurrentpoint: x y — absolute, and used to resynchronise
441                // after an othersubr moved the point out from under us.
442                let a = self.take(2)?;
443                self.current = Point::new(arg(&a, 0), arg(&a, 1));
444            }
445            // Everything else. `dotsection` (0), `vstem3` (1) and `hstem3`
446            // (2) are hinting directives that we deliberately ignore — we
447            // render filled unhinted outlines — and the rest are reserved.
448            // Both cases must still *consume* their operands, or the next
449            // path operator would read them as its own.
450            _ => self.stack.clear(),
451        }
452        Ok(false)
453    }
454
455    /// `callothersubr`: `arg1 … argn n othersubr# callothersubr`.
456    fn call_other_subr(&mut self) -> Result<(), Abort> {
457        let index = self.stack.pop().ok_or(Abort::StackUnderflow)? as i64;
458        let count = self.stack.pop().ok_or(Abort::StackUnderflow)?;
459        let count = usize::try_from(count as i64).unwrap_or(0);
460        if self.stack.len() < count {
461            self.stack.clear();
462            return Err(Abort::StackUnderflow);
463        }
464        let base = self.stack.len().saturating_sub(count);
465        let args: Vec<f64> = self.stack.get(base..).unwrap_or_default().to_vec();
466        self.stack.truncate(base);
467
468        match index {
469            0 => self.end_flex(&args),
470            1 => {
471                // Start collecting flex points; the reference point comes from
472                // the first of the seven `rmoveto`s that follow.
473                self.flex = Some(Vec::new());
474                Ok(())
475            }
476            2 => Ok(()), // collect: the rmoveto handler is doing the work
477            3 => {
478                // Hint replacement. The font expects `3` back so the following
479                // `pop callsubr` calls subr 3, which is a no-op by convention.
480                self.ps_stack.push(3.0);
481                Ok(())
482            }
483            14..=18 => self.blend(index, &args),
484            _ => {
485                // An othersubr we do not emulate. The convention is that its
486                // arguments come straight back through `pop`, which keeps a
487                // font using a private othersubr renderable.
488                self.ps_stack.extend(args.iter().rev().copied());
489                Ok(())
490            }
491        }
492    }
493
494    /// othersubr 0 — end of flex. Seven collected points become two curves.
495    ///
496    /// The published idiom is `flex_height end_x end_y 3 0 callothersubr`,
497    /// followed by `pop pop setcurrentpoint`. The three arguments are advisory
498    /// (the height is a hinting hint and the end point restates the seventh
499    /// collected point), so the outline is built entirely from the collected
500    /// points; the two values pushed back are what the following
501    /// `setcurrentpoint` consumes.
502    fn end_flex(&mut self, args: &[f64]) -> Result<(), Abort> {
503        let points = self.flex.take().unwrap_or_default();
504        // The first collected point is the flex *reference* point, which only
505        // exists to tell a hinter how far the curve strays from a line; the
506        // outline uses the six after it.
507        let p = |n: usize| points.get(n).copied();
508        let end = if let (Some(c1), Some(c2), Some(mid), Some(c3), Some(c4), Some(end)) =
509            (p(1), p(2), p(3), p(4), p(5), p(6))
510        {
511            self.curve_to(c1, c2, mid)?;
512            self.curve_to(c3, c4, end)?;
513            self.current = end;
514            end
515        } else {
516            // Too few points collected — a truncated flex. Fall back to a
517            // straight line to wherever the last point was, so the contour
518            // stays closed.
519            if let Some(last) = points.last().copied() {
520                self.line_to(last)?;
521            }
522            self.current
523        };
524        // Pushed y first so the font's `pop pop` reads x then y, which is the
525        // order `setcurrentpoint` wants.
526        self.ps_stack.push(end.y);
527        self.ps_stack.push(end.x);
528        let _ = args;
529        Ok(())
530    }
531
532    /// othersubr 14–18 — the Multiple Master blend.
533    ///
534    /// The operand block is `k` base values followed by `k * (m-1)` deltas,
535    /// where `k` is the point count this othersubr number implies and `m` the
536    /// number of masters. Each result is `base + Σ delta_j * weight_{j+1}` —
537    /// the first weight is not applied, because the base value *is* the first
538    /// master's value.
539    fn blend(&mut self, index: i64, args: &[f64]) -> Result<(), Abort> {
540        let masters = self.env.weights.len();
541        if masters < 2 || self.env.blend.is_none() {
542            // Not a Multiple Master font, or its declaration was rejected.
543            // Handing the arguments back is the only recovery that keeps a
544            // charstring's arithmetic consistent.
545            self.ps_stack.extend(args.iter().rev().copied());
546            return Ok(());
547        }
548        // 14→1 point, 15→2, 16→3, 17→4, 18→6. The jump at 18 is in the
549        // specification: it blends the six values of an `rrcurveto`.
550        let points = match index {
551            14 => 1usize,
552            15 => 2,
553            16 => 3,
554            17 => 4,
555            18 => 6,
556            _ => return Err(Abort::BadBlend),
557        };
558        if args.len() != points.saturating_mul(masters) {
559            return Err(Abort::BadBlend);
560        }
561        let mut blended = Vec::with_capacity(points);
562        for k in 0..points {
563            let mut v = args.get(k).copied().unwrap_or(0.0);
564            for (j, &w) in self.env.weights.iter().enumerate().skip(1) {
565                let delta = args
566                    .get(
567                        points
568                            .saturating_add(k.saturating_mul(masters.saturating_sub(1)))
569                            .saturating_add(j.saturating_sub(1)),
570                    )
571                    .copied()
572                    .unwrap_or(0.0);
573                v += delta * f64::from(w);
574            }
575            blended.push(v);
576        }
577        // Results come back through `pop`, last-pushed-first, so the font's
578        // run of `pop`s reads them left to right.
579        self.ps_stack.extend(blended.iter().rev().copied());
580        Ok(())
581    }
582
583    /// `seac` — compose from two `StandardEncoding`-named glyphs.
584    fn seac(
585        &mut self,
586        asb: f64,
587        adx: f64,
588        ady: f64,
589        bchar: f64,
590        achar: f64,
591        depth: u32,
592    ) -> Result<(), Abort> {
593        let code = |v: f64| u8::try_from(v as i64).ok();
594        let lookup = |v: f64| -> Option<Vec<u8>> {
595            let name = encoding::standard_encoding_name(code(v)?)?;
596            let gid = (self.env.name_lookup)(name)?;
597            self.env.charstrings.get(gid).cloned()
598        };
599        let (Some(base), Some(accent)) = (lookup(bchar), lookup(achar)) else {
600            return Err(Abort::BadSeac);
601        };
602
603        // The base is drawn at the origin, keeping its own side bearing and
604        // advance.
605        let outer_lsb = self.lsb;
606        self.reset_for_component();
607        self.run(&base, depth.saturating_add(1))?;
608        self.finish();
609        let base_advance = self.advance;
610
611        // The accent is drawn shifted. `asb` is the accent's side bearing as
612        // the *composing* glyph believed it to be, so the true offset corrects
613        // for any disagreement with the accent's own `hsbw`.
614        let accent_start = self.path.elements().len();
615        self.reset_for_component();
616        self.run(&accent, depth.saturating_add(1))?;
617        self.finish();
618        let shift = kurbo_translate(f64::from(outer_lsb) - asb + adx, ady);
619        translate_from(&mut self.path, accent_start, shift);
620
621        self.advance = base_advance;
622        self.lsb = outer_lsb;
623        Ok(())
624    }
625
626    /// Between `seac` components: keep the accumulated path, reset the pen.
627    fn reset_for_component(&mut self) {
628        self.stack.clear();
629        self.ps_stack.clear();
630        self.flex = None;
631        self.open = false;
632        self.current = Point::ZERO;
633        self.start = Point::ZERO;
634        self.done = false;
635    }
636
637    fn relative_curve(
638        &mut self,
639        dx1: f64,
640        dy1: f64,
641        dx2: f64,
642        dy2: f64,
643        dx3: f64,
644        dy3: f64,
645    ) -> Result<(), Abort> {
646        let c1 = Point::new(self.current.x + dx1, self.current.y + dy1);
647        let c2 = Point::new(c1.x + dx2, c1.y + dy2);
648        let end = Point::new(c2.x + dx3, c2.y + dy3);
649        self.curve_to(c1, c2, end)
650    }
651}
652
653fn arg(args: &[f64], i: usize) -> f64 {
654    args.get(i).copied().unwrap_or(0.0)
655}
656
657fn kurbo_translate(dx: f64, dy: f64) -> (f64, f64) {
658    (dx, dy)
659}
660
661/// Shift every path element from `from` onward. `BezPath` has no range
662/// transform, so this rebuilds the tail.
663fn translate_from(path: &mut BezPath, from: usize, (dx, dy): (f64, f64)) {
664    use pdfrum_common::kurbo::PathEl;
665    let shift = |p: Point| Point::new(p.x + dx, p.y + dy);
666    let tail: Vec<PathEl> = path
667        .elements()
668        .get(from..)
669        .unwrap_or_default()
670        .iter()
671        .map(|el| match *el {
672            PathEl::MoveTo(p) => PathEl::MoveTo(shift(p)),
673            PathEl::LineTo(p) => PathEl::LineTo(shift(p)),
674            PathEl::QuadTo(a, b) => PathEl::QuadTo(shift(a), shift(b)),
675            PathEl::CurveTo(a, b, c) => PathEl::CurveTo(shift(a), shift(b), shift(c)),
676            PathEl::ClosePath => PathEl::ClosePath,
677        })
678        .collect();
679    path.truncate(from);
680    for el in tail {
681        path.push(el);
682    }
683}
684
685#[cfg(test)]
686#[allow(
687    clippy::indexing_slicing,
688    clippy::float_cmp,
689    clippy::cast_possible_truncation,
690    clippy::cast_sign_loss,
691    clippy::similar_names
692)]
693mod tests {
694    use super::{Abort, Env, Glyph, interpret};
695    use crate::eexec;
696    use pdfrum_common::kurbo::Shape;
697
698    /// Assemble a charstring from a readable description.
699    ///
700    /// Numbers are encoded the way a real font would, so the tests exercise
701    /// the number decoder as well as the operators.
702    fn cs(items: &[Item]) -> Vec<u8> {
703        let mut out = Vec::new();
704        for it in items {
705            match *it {
706                Item::N(v) => encode_number(&mut out, v),
707                Item::Op(o) => out.push(o),
708                Item::Esc(o) => out.extend_from_slice(&[12, o]),
709            }
710        }
711        out
712    }
713
714    #[derive(Clone, Copy)]
715    enum Item {
716        N(i32),
717        Op(u8),
718        Esc(u8),
719    }
720    use Item::{Esc, N, Op};
721
722    fn encode_number(out: &mut Vec<u8>, v: i32) {
723        match v {
724            -107..=107 => out.push((v + 139) as u8),
725            108..=1131 => {
726                let v = v - 108;
727                out.push(((v >> 8) + 247) as u8);
728                out.push((v & 0xFF) as u8);
729            }
730            -1131..=-108 => {
731                let v = -v - 108;
732                out.push(((v >> 8) + 251) as u8);
733                out.push((v & 0xFF) as u8);
734            }
735            _ => {
736                out.push(255);
737                out.extend_from_slice(&v.to_be_bytes());
738            }
739        }
740    }
741
742    fn no_names(_: &str) -> Option<usize> {
743        None
744    }
745
746    fn run(code: &[u8]) -> (Glyph, Option<Abort>) {
747        run_with(code, &[], &[])
748    }
749
750    fn run_with(code: &[u8], subrs: &[Vec<u8>], charstrings: &[Vec<u8>]) -> (Glyph, Option<Abort>) {
751        interpret(
752            code,
753            Env {
754                subrs,
755                charstrings,
756                name_lookup: &no_names,
757                weights: &[],
758                blend: None,
759            },
760        )
761    }
762
763    #[test]
764    fn hsbw_sets_the_origin_and_advance() {
765        let (g, abort) = run(&cs(&[N(50), N(600), Op(13), Op(14)]));
766        assert_eq!(abort, None);
767        assert_eq!(g.advance, 600.0);
768        assert_eq!(g.left_side_bearing, 50.0);
769        // Nothing was drawn, but the pen sits at the side bearing: a following
770        // rmoveto is relative to (50, 0).
771        let (g, _) = run(&cs(&[
772            N(50),
773            N(600),
774            Op(13),
775            N(0),
776            N(0),
777            Op(21),
778            N(10),
779            Op(6),
780            Op(9),
781            Op(14),
782        ]));
783        assert_eq!(g.path.bounding_box().x0, 50.0);
784        assert_eq!(g.path.bounding_box().x1, 60.0);
785    }
786
787    #[test]
788    fn a_square_from_the_line_operators() {
789        // rlineto, hlineto, vlineto and closepath.
790        let (g, abort) = run(&cs(&[
791            N(0),
792            N(1000),
793            Op(13),
794            N(100),
795            N(100),
796            Op(21), // rmoveto
797            N(200),
798            Op(6), // hlineto
799            N(200),
800            Op(7), // vlineto
801            N(-200),
802            N(0),
803            Op(5), // rlineto
804            Op(9), // closepath
805            Op(14),
806        ]));
807        assert_eq!(abort, None);
808        let bb = g.path.bounding_box();
809        assert_eq!((bb.x0, bb.y0, bb.x1, bb.y1), (100.0, 100.0, 300.0, 300.0));
810        assert_eq!(g.path.elements().len(), 5); // move + 3 lines + close
811    }
812
813    #[test]
814    fn rrcurveto_vhcurveto_hvcurveto_place_control_points() {
815        use pdfrum_common::kurbo::{PathEl, Point};
816        let (g, _) = run(&cs(&[
817            N(0),
818            N(1000),
819            Op(13),
820            N(0),
821            N(0),
822            Op(21),
823            N(10),
824            N(20),
825            N(30),
826            N(40),
827            N(50),
828            N(60),
829            Op(8), // rrcurveto
830            N(10),
831            N(20),
832            N(30),
833            N(40),
834            Op(30), // vhcurveto
835            N(10),
836            N(20),
837            N(30),
838            N(40),
839            Op(31), // hvcurveto
840            Op(14),
841        ]));
842        let els = g.path.elements();
843        // rrcurveto from (0,0): c1=(10,20) c2=(40,60) end=(90,120)
844        assert_eq!(
845            els.get(1),
846            Some(&PathEl::CurveTo(
847                Point::new(10.0, 20.0),
848                Point::new(40.0, 60.0),
849                Point::new(90.0, 120.0)
850            ))
851        );
852        // vhcurveto dy1=10 dx2=20 dy2=30 dx3=40, from (90,120):
853        // c1=(90,130) c2=(110,160) end=(150,160)
854        assert_eq!(
855            els.get(2),
856            Some(&PathEl::CurveTo(
857                Point::new(90.0, 130.0),
858                Point::new(110.0, 160.0),
859                Point::new(150.0, 160.0)
860            ))
861        );
862        // hvcurveto dx1=10 dx2=20 dy2=30 dy3=40, from (150,160):
863        // c1=(160,160) c2=(180,190) end=(180,230)
864        assert_eq!(
865            els.get(3),
866            Some(&PathEl::CurveTo(
867                Point::new(160.0, 160.0),
868                Point::new(180.0, 190.0),
869                Point::new(180.0, 230.0)
870            ))
871        );
872    }
873
874    #[test]
875    fn callsubr_and_return() {
876        let subrs = vec![
877            Vec::new(),
878            Vec::new(),
879            Vec::new(),
880            Vec::new(),
881            cs(&[N(100), Op(6), Op(11)]), // subr 4: hlineto 100, return
882        ];
883        let (g, abort) = run_with(
884            &cs(&[
885                N(0),
886                N(1000),
887                Op(13),
888                N(0),
889                N(0),
890                Op(21),
891                N(4),
892                Op(10),
893                Op(14),
894            ]),
895            &subrs,
896            &[],
897        );
898        assert_eq!(abort, None);
899        assert_eq!(g.path.bounding_box().x1, 100.0);
900    }
901
902    #[test]
903    fn a_missing_subr_aborts_but_keeps_the_path() {
904        let (g, abort) = run(&cs(&[
905            N(0),
906            N(1000),
907            Op(13),
908            N(0),
909            N(0),
910            Op(21),
911            N(50),
912            Op(6),
913            N(99),
914            Op(10),
915            Op(14),
916        ]));
917        assert_eq!(abort, Some(Abort::MissingSubr));
918        assert_eq!(g.path.bounding_box().x1, 50.0);
919    }
920
921    #[test]
922    fn div_leaves_a_quotient_on_the_stack() {
923        let (g, _) = run(&cs(&[
924            N(0),
925            N(1000),
926            Op(13),
927            N(0),
928            N(0),
929            Op(21),
930            N(300),
931            N(3),
932            Esc(12),
933            Op(6),
934            Op(14),
935        ]));
936        assert_eq!(g.path.bounding_box().x1, 100.0);
937        // Division by zero yields zero rather than an infinity that would
938        // poison the outline.
939        let (g, _) = run(&cs(&[
940            N(0),
941            N(1000),
942            Op(13),
943            N(0),
944            N(0),
945            Op(21),
946            N(300),
947            N(0),
948            Esc(12),
949            Op(6),
950            Op(14),
951        ]));
952        assert!(g.path.bounding_box().x1.is_finite());
953    }
954
955    #[test]
956    fn setcurrentpoint_is_absolute() {
957        let (g, _) = run(&cs(&[
958            N(0),
959            N(1000),
960            Op(13),
961            N(0),
962            N(0),
963            Op(21),
964            N(500),
965            N(500),
966            Esc(33), // setcurrentpoint 500 500
967            N(10),
968            Op(6),
969            Op(14),
970        ]));
971        let bb = g.path.bounding_box();
972        assert_eq!((bb.x1, bb.y1), (510.0, 500.0));
973    }
974
975    #[test]
976    fn flex_becomes_two_curves() {
977        use pdfrum_common::kurbo::PathEl;
978        // The canonical shape: othersubr 1, seven rmovetos, othersubr 0.
979        let mut items = vec![N(0), N(1000), Op(13), N(0), N(0), Op(21)];
980        items.extend([N(0), N(1), Esc(16)]); // 0 1 callothersubr (start flex)
981        for (dx, dy) in [
982            (10, 10),
983            (10, 10),
984            (10, 10),
985            (10, 10),
986            (10, 10),
987            (10, 10),
988            (10, 10),
989        ] {
990            items.extend([N(0), N(2), Esc(16), N(dx), N(dy), Op(21)]);
991        }
992        // flex_height end_x end_y 3 0 callothersubr, then pop pop setcurrentpoint.
993        items.extend([N(50), N(70), N(70), N(3), N(0), Esc(16)]);
994        items.extend([Esc(17), Esc(17), Esc(33)]);
995        items.push(Op(14));
996        let (g, abort) = run(&cs(&items));
997        assert_eq!(abort, None);
998        // Two curves, no intervening moves: the seven rmovetos were absorbed.
999        let curves = g
1000            .path
1001            .elements()
1002            .iter()
1003            .filter(|e| matches!(e, PathEl::CurveTo(..)))
1004            .count();
1005        assert_eq!(curves, 2);
1006        assert_eq!(
1007            g.path
1008                .elements()
1009                .iter()
1010                .filter(|e| matches!(e, PathEl::MoveTo(_)))
1011                .count(),
1012            1
1013        );
1014    }
1015
1016    #[test]
1017    fn hint_replacement_returns_three_and_calls_the_subr() {
1018        // `subr# 1 3 callothersubr pop callsubr` is the published idiom; the
1019        // `3` that comes back through `pop` selects subr 3, conventionally a
1020        // no-op, and the outline must be unaffected.
1021        let subrs = vec![Vec::new(), Vec::new(), Vec::new(), cs(&[Op(11)])];
1022        let (g, abort) = run_with(
1023            &cs(&[
1024                N(0),
1025                N(1000),
1026                Op(13),
1027                N(0),
1028                N(0),
1029                Op(21),
1030                N(3),
1031                N(1),
1032                N(3),
1033                Esc(16), // 3 1 3 callothersubr
1034                Esc(17),
1035                Op(10), // pop callsubr
1036                N(70),
1037                Op(6),
1038                Op(14),
1039            ]),
1040            &subrs,
1041            &[],
1042        );
1043        assert_eq!(abort, None);
1044        assert_eq!(g.path.bounding_box().x1, 70.0);
1045    }
1046
1047    #[test]
1048    fn seac_composes_two_glyphs_with_the_side_bearing_correction() {
1049        // Glyph 1 is the base (a unit square at x=0), glyph 2 the accent
1050        // (a unit square at x=0 with its own side bearing of 20).
1051        let base = cs(&[
1052            N(0),
1053            N(500),
1054            Op(13),
1055            N(0),
1056            N(0),
1057            Op(21),
1058            N(100),
1059            Op(6),
1060            N(100),
1061            Op(7),
1062            Op(9),
1063            Op(14),
1064        ]);
1065        let accent = cs(&[
1066            N(20),
1067            N(300),
1068            Op(13),
1069            N(0),
1070            N(0),
1071            Op(21),
1072            N(50),
1073            Op(6),
1074            N(50),
1075            Op(7),
1076            Op(9),
1077            Op(14),
1078        ]);
1079        let charstrings = vec![Vec::new(), base, accent];
1080        // StandardEncoding: 'A' is 65, 'B' is 66.
1081        let lookup = |n: &str| match n {
1082            "A" => Some(1usize),
1083            "B" => Some(2usize),
1084            _ => None,
1085        };
1086        let (g, abort) = interpret(
1087            // 0 500 hsbw  20 300 100 65 66 seac
1088            &cs(&[
1089                N(0),
1090                N(500),
1091                Op(13),
1092                N(20),
1093                N(300),
1094                N(100),
1095                N(65),
1096                N(66),
1097                Esc(6),
1098            ]),
1099            Env {
1100                subrs: &[],
1101                charstrings: &charstrings,
1102                name_lookup: &lookup,
1103                weights: &[],
1104                blend: None,
1105            },
1106        );
1107        assert_eq!(abort, None);
1108        // The base keeps the composite's advance. The accent draws itself from
1109        // its own side bearing (20) and is then shifted by
1110        // `adx - asb + outer_lsb` = 300 - 20 + 0 = 280, so its 50-wide box
1111        // spans 300..350 in x and 100..150 in y.
1112        assert_eq!(g.advance, 500.0);
1113        let bb = g.path.bounding_box();
1114        assert_eq!((bb.x0, bb.y0), (0.0, 0.0));
1115        assert_eq!((bb.x1, bb.y1), (350.0, 150.0));
1116    }
1117
1118    #[test]
1119    fn seac_naming_a_missing_component_aborts_cleanly() {
1120        let (_, abort) = run(&cs(&[
1121            N(0),
1122            N(500),
1123            Op(13),
1124            N(0),
1125            N(0),
1126            N(0),
1127            N(65),
1128            N(66),
1129            Esc(6),
1130        ]));
1131        assert_eq!(abort, Some(Abort::BadSeac));
1132    }
1133
1134    #[test]
1135    fn multiple_master_blend_interpolates_the_operands() {
1136        use crate::blend::{AxisKind, Blend, DesignMap};
1137        use pdfrum_common::Diagnostics;
1138        let mut d = Diagnostics::default();
1139        let blend = Blend::new(
1140            vec![AxisKind::Weight],
1141            vec![vec![0.0], vec![1.0]],
1142            vec![DesignMap {
1143                knots: vec![(0.0, 0.0), (1.0, 1.0)],
1144            }],
1145            vec![0.5, 0.5],
1146            &mut d,
1147        )
1148        .expect("consistent");
1149
1150        // Two masters, one point: `base delta 1 14 callothersubr pop`.
1151        // At weight (0.25, 0.75) the result is base + delta * 0.75.
1152        let code = cs(&[
1153            N(0),
1154            N(1000),
1155            Op(13),
1156            N(0),
1157            N(0),
1158            Op(21),
1159            N(100),
1160            N(200),
1161            N(2),
1162            N(14),
1163            Esc(16), // 100 200 2 14 callothersubr
1164            Esc(17), // pop -> 100 + 200*0.75 = 250
1165            Op(6),   // hlineto
1166            Op(14),
1167        ]);
1168        let (g, abort) = interpret(
1169            &code,
1170            Env {
1171                subrs: &[],
1172                charstrings: &[],
1173                name_lookup: &no_names,
1174                weights: &[0.25, 0.75],
1175                blend: Some(&blend),
1176            },
1177        );
1178        assert_eq!(abort, None);
1179        assert!((g.path.bounding_box().x1 - 250.0).abs() < 1e-9);
1180
1181        // The same charstring at the other extreme moves the outline.
1182        let (g2, _) = interpret(
1183            &code,
1184            Env {
1185                subrs: &[],
1186                charstrings: &[],
1187                name_lookup: &no_names,
1188                weights: &[1.0, 0.0],
1189                blend: Some(&blend),
1190            },
1191        );
1192        assert!((g2.path.bounding_box().x1 - 100.0).abs() < 1e-9);
1193    }
1194
1195    #[test]
1196    fn a_blend_with_the_wrong_arity_aborts() {
1197        use crate::blend::{AxisKind, Blend, DesignMap};
1198        use pdfrum_common::Diagnostics;
1199        let mut d = Diagnostics::default();
1200        let blend = Blend::new(
1201            vec![AxisKind::Weight],
1202            vec![vec![0.0], vec![1.0]],
1203            vec![DesignMap {
1204                knots: vec![(0.0, 0.0), (1.0, 1.0)],
1205            }],
1206            vec![0.5, 0.5],
1207            &mut d,
1208        )
1209        .expect("consistent");
1210        // othersubr 14 wants 1 point x 2 masters = 2 operands; give it 3.
1211        let (_, abort) = interpret(
1212            &cs(&[
1213                N(0),
1214                N(1000),
1215                Op(13),
1216                N(1),
1217                N(2),
1218                N(3),
1219                N(3),
1220                N(14),
1221                Esc(16),
1222                Op(14),
1223            ]),
1224            Env {
1225                subrs: &[],
1226                charstrings: &[],
1227                name_lookup: &no_names,
1228                weights: &[0.5, 0.5],
1229                blend: Some(&blend),
1230            },
1231        );
1232        assert_eq!(abort, Some(Abort::BadBlend));
1233    }
1234
1235    #[test]
1236    fn closepath_leaves_the_current_point_alone() {
1237        // Two contours: the second `rmoveto` is relative to where the pen was
1238        // when `closepath` ran, not to the first contour's start.
1239        let (g, _) = run(&cs(&[
1240            N(0),
1241            N(1000),
1242            Op(13),
1243            N(100),
1244            N(0),
1245            Op(21),
1246            N(100),
1247            Op(6), // pen at (200, 0)
1248            Op(9), // closepath
1249            N(50),
1250            N(50),
1251            Op(21), // -> (250, 50), not (150, 50)
1252            N(10),
1253            Op(6),
1254            Op(14),
1255        ]));
1256        let bb = g.path.bounding_box();
1257        assert_eq!(bb.x1, 260.0);
1258    }
1259
1260    #[test]
1261    fn recursion_is_capped() {
1262        // Subr 0 calls itself forever.
1263        let subrs = vec![cs(&[N(0), Op(10), Op(11)])];
1264        let (_, abort) = run_with(
1265            &cs(&[N(0), N(1000), Op(13), N(0), Op(10), Op(14)]),
1266            &subrs,
1267            &[],
1268        );
1269        assert_eq!(abort, Some(Abort::TooDeep));
1270    }
1271
1272    #[test]
1273    fn unknown_operators_clear_the_stack_and_carry_on() {
1274        let (g, abort) = run(&cs(&[
1275            N(0),
1276            N(1000),
1277            Op(13),
1278            N(0),
1279            N(0),
1280            Op(21),
1281            N(1),
1282            N(2),
1283            Op(23),
1284            N(80),
1285            Op(6),
1286            Op(14),
1287        ]));
1288        assert_eq!(abort, None);
1289        assert_eq!(g.path.bounding_box().x1, 80.0);
1290    }
1291
1292    #[test]
1293    fn the_four_byte_number_encoding_round_trips() {
1294        let (g, _) = run(&cs(&[
1295            N(0),
1296            N(1000),
1297            Op(13),
1298            N(0),
1299            N(0),
1300            Op(21),
1301            N(70000),
1302            Op(6),
1303            Op(14),
1304        ]));
1305        assert_eq!(g.path.bounding_box().x1, 70000.0);
1306        let (g, _) = run(&cs(&[
1307            N(0),
1308            N(1000),
1309            Op(13),
1310            N(0),
1311            N(0),
1312            Op(21),
1313            N(-70000),
1314            Op(6),
1315            Op(14),
1316        ]));
1317        assert_eq!(g.path.bounding_box().x0, -70000.0);
1318    }
1319
1320    #[test]
1321    fn a_truncated_charstring_reports_it() {
1322        // 255 needs four more bytes; only two are there.
1323        let (_, abort) = run(&[255, 0, 1]);
1324        assert_eq!(abort, Some(Abort::Truncated));
1325        // And an entirely empty one.
1326        let (g, abort) = run(&[]);
1327        assert_eq!(abort, Some(Abort::Truncated));
1328        assert!(g.path.is_empty());
1329    }
1330
1331    #[test]
1332    fn encrypted_charstrings_decode_through_the_cipher() {
1333        // The integration the real font takes: charstrings arrive encrypted
1334        // with seed 4330 and four bytes of lead-in.
1335        let plain = {
1336            let mut v = vec![0u8; 4];
1337            v.extend(cs(&[
1338                N(0),
1339                N(1000),
1340                Op(13),
1341                N(0),
1342                N(0),
1343                Op(21),
1344                N(42),
1345                Op(6),
1346                Op(14),
1347            ]));
1348            v
1349        };
1350        let cipher = eexec::encrypt(&plain, eexec::CHARSTRING_SEED);
1351        let decoded = eexec::decrypt(&cipher, eexec::CHARSTRING_SEED, 4);
1352        let (g, abort) = run(&decoded);
1353        assert_eq!(abort, None);
1354        assert_eq!(g.path.bounding_box().x1, 42.0);
1355    }
1356}