Skip to main content

stet_fonts/
charstring.rs

1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Type 1 charstring interpreter.
6//!
7//! Decrypts and executes Type 1 charstring opcodes to produce path segments
8//! and glyph width information.
9
10use crate::encoding::STANDARD_ENCODING;
11use crate::geometry::{PathSegment, PsPath};
12
13/// Result of executing a charstring: the glyph path and advance width.
14pub struct CharstringResult {
15    pub path: PsPath,
16    pub width_x: f64,
17    pub width_y: f64,
18    pub lsb_x: f64,
19    pub lsb_y: f64,
20    /// Deprecated seac (Standard Encoding Accented Character) from endchar with 4 args.
21    /// Contains (adx, ady, bchar, achar) — Standard Encoding codes for base and accent.
22    pub seac: Option<(f64, f64, u8, u8)>,
23}
24
25/// Decrypt a charstring using the Type 1 charstring cipher (R=4330).
26/// Skips the first `len_iv` random bytes.
27pub fn decrypt_charstring(data: &[u8], len_iv: usize) -> Vec<u8> {
28    // len_iv == usize::MAX is a sentinel for /lenIV -1 (no encryption).
29    // Return raw bytes without decryption or prefix stripping.
30    if len_iv == usize::MAX {
31        return data.to_vec();
32    }
33    let c1: u32 = 52845;
34    let c2: u32 = 22719;
35    let mut r: u32 = 4330;
36    let mut result = Vec::with_capacity(data.len().saturating_sub(len_iv));
37    for (i, &cipher) in data.iter().enumerate() {
38        let plain = (cipher as u32 ^ (r >> 8)) as u8;
39        if i >= len_iv {
40            result.push(plain);
41        }
42        r = ((cipher as u32 + r) * c1 + c2) & 0xFFFF;
43    }
44    result
45}
46
47/// Charstring lookup function for seac composite character support.
48/// Maps glyph name (bytes) to encrypted charstring bytes.
49pub type CharstringLookup<'a> = dyn Fn(&str) -> Option<Vec<u8>> + 'a;
50
51/// Execute a Type 1 charstring and produce path segments + width.
52///
53/// If `width_only` is true, path operations are skipped — only width is extracted.
54/// If `cs_lookup` is provided, seac (composite characters) can look up component charstrings.
55pub fn execute_charstring(
56    charstring: &[u8],
57    subrs: &[Vec<u8>],
58    len_iv: usize,
59    width_only: bool,
60) -> Result<CharstringResult, String> {
61    execute_charstring_ex(charstring, subrs, len_iv, width_only, None)
62}
63
64/// Execute a Type 1 charstring with optional charstring lookup for seac support.
65pub fn execute_charstring_ex(
66    charstring: &[u8],
67    subrs: &[Vec<u8>],
68    len_iv: usize,
69    width_only: bool,
70    cs_lookup: Option<&CharstringLookup<'_>>,
71) -> Result<CharstringResult, String> {
72    execute_charstring_mm(charstring, subrs, len_iv, width_only, cs_lookup, None)
73}
74
75/// Execute a Type 1 charstring with Multiple Master weight vector support.
76pub fn execute_charstring_mm(
77    charstring: &[u8],
78    subrs: &[Vec<u8>],
79    len_iv: usize,
80    width_only: bool,
81    cs_lookup: Option<&CharstringLookup<'_>>,
82    weight_vector: Option<&[f64]>,
83) -> Result<CharstringResult, String> {
84    let decrypted = decrypt_charstring(charstring, len_iv);
85    let mut interp = CharstringInterp::new(subrs, len_iv, width_only, cs_lookup);
86    interp.weight_vector = weight_vector.map(|wv| wv.to_vec());
87    interp.execute(&decrypted)?;
88    Ok(CharstringResult {
89        path: interp.path,
90        width_x: interp.width_x,
91        width_y: interp.width_y,
92        lsb_x: interp.lsb_x,
93        lsb_y: interp.lsb_y,
94        seac: None,
95    })
96}
97
98/// Execute a charstring for seac (accent composition), applying an offset.
99pub fn execute_charstring_with_offset(
100    charstring: &[u8],
101    subrs: &[Vec<u8>],
102    len_iv: usize,
103    offset_x: f64,
104    offset_y: f64,
105) -> Result<CharstringResult, String> {
106    execute_charstring_with_offset_mm(charstring, subrs, len_iv, offset_x, offset_y, None)
107}
108
109/// Execute a charstring for seac with MM weight vector support.
110pub fn execute_charstring_with_offset_mm(
111    charstring: &[u8],
112    subrs: &[Vec<u8>],
113    len_iv: usize,
114    offset_x: f64,
115    offset_y: f64,
116    weight_vector: Option<&[f64]>,
117) -> Result<CharstringResult, String> {
118    let decrypted = decrypt_charstring(charstring, len_iv);
119    let mut interp = CharstringInterp::new(subrs, len_iv, false, None);
120    interp.x = offset_x;
121    interp.y = offset_y;
122    interp.weight_vector = weight_vector.map(|wv| wv.to_vec());
123    interp.execute(&decrypted)?;
124    Ok(CharstringResult {
125        path: interp.path,
126        width_x: interp.width_x,
127        width_y: interp.width_y,
128        lsb_x: interp.lsb_x,
129        lsb_y: interp.lsb_y,
130        seac: None,
131    })
132}
133
134/// Internal charstring interpreter state.
135struct CharstringInterp<'a> {
136    stack: Vec<f64>,
137    path: PsPath,
138    x: f64,
139    y: f64,
140    width_x: f64,
141    width_y: f64,
142    lsb_x: f64,
143    lsb_y: f64,
144    subrs: &'a [Vec<u8>],
145    len_iv: usize,
146    width_only: bool,
147    done: bool,
148    // Flex support (OtherSubrs 0-3)
149    flex_active: bool,
150    flex_points: Vec<(f64, f64)>,
151    // OtherSubrs return stack (for pop operator)
152    ps_stack: Vec<f64>,
153    // Charstring lookup for seac composite character support
154    cs_lookup: Option<&'a CharstringLookup<'a>>,
155    // Multiple Master weight vector for blend OtherSubrs (14-17)
156    weight_vector: Option<Vec<f64>>,
157    // seac accent offset: when executing the accent component of a seac,
158    // hsbw/sbw adds this offset to the sidebearing instead of resetting
159    // the current point to zero.
160    seac_accent_offset: Option<(f64, f64)>,
161}
162
163impl<'a> CharstringInterp<'a> {
164    fn new(
165        subrs: &'a [Vec<u8>],
166        len_iv: usize,
167        width_only: bool,
168        cs_lookup: Option<&'a CharstringLookup<'a>>,
169    ) -> Self {
170        Self {
171            stack: Vec::with_capacity(48),
172            path: PsPath::new(),
173            x: 0.0,
174            y: 0.0,
175            width_x: 0.0,
176            width_y: 0.0,
177            lsb_x: 0.0,
178            lsb_y: 0.0,
179            subrs,
180            len_iv,
181            width_only,
182            done: false,
183            flex_active: false,
184            flex_points: Vec::new(),
185            ps_stack: Vec::new(),
186            cs_lookup,
187            weight_vector: None,
188            seac_accent_offset: None,
189        }
190    }
191
192    fn execute(&mut self, data: &[u8]) -> Result<(), String> {
193        self.execute_inner(data, 0)
194    }
195
196    fn execute_inner(&mut self, data: &[u8], depth: usize) -> Result<(), String> {
197        if depth > 10 {
198            return Err("Charstring subroutine depth exceeded".to_string());
199        }
200
201        let mut pos = 0;
202        while pos < data.len() && !self.done {
203            let b = data[pos];
204            pos += 1;
205
206            match b {
207                // Commands (0–31)
208                0 => {} // reserved, ignore
209                1 => {
210                    // hstem: y dy — ignore (hint), pop 2 args
211                    if self.stack.len() >= 2 {
212                        self.stack.pop();
213                        self.stack.pop();
214                    }
215                }
216                2 => {} // reserved
217                3 => {
218                    // vstem: x dx — ignore (hint), pop 2 args
219                    if self.stack.len() >= 2 {
220                        self.stack.pop();
221                        self.stack.pop();
222                    }
223                }
224                4 => {
225                    // vmoveto: dy
226                    if self.stack.is_empty() {
227                        return Err("vmoveto: stack underflow".to_string());
228                    }
229                    let dy = self.stack.pop().unwrap();
230                    self.y += dy;
231                    if !self.width_only && !self.flex_active {
232                        self.path.segments.push(PathSegment::MoveTo(self.x, self.y));
233                    }
234                    // During flex, moveto just updates current point — OtherSubrs 2 handles flex_points
235                }
236                5 => {
237                    // rlineto: dx dy
238                    if self.stack.len() < 2 {
239                        return Err("rlineto: stack underflow".to_string());
240                    }
241                    let dy = self.stack.pop().unwrap();
242                    let dx = self.stack.pop().unwrap();
243                    self.x += dx;
244                    self.y += dy;
245                    if !self.width_only {
246                        self.path.segments.push(PathSegment::LineTo(self.x, self.y));
247                    }
248                }
249                6 => {
250                    // hlineto: dx
251                    if self.stack.is_empty() {
252                        return Err("hlineto: stack underflow".to_string());
253                    }
254                    let dx = self.stack.pop().unwrap();
255                    self.x += dx;
256                    if !self.width_only {
257                        self.path.segments.push(PathSegment::LineTo(self.x, self.y));
258                    }
259                }
260                7 => {
261                    // vlineto: dy
262                    if self.stack.is_empty() {
263                        return Err("vlineto: stack underflow".to_string());
264                    }
265                    let dy = self.stack.pop().unwrap();
266                    self.y += dy;
267                    if !self.width_only {
268                        self.path.segments.push(PathSegment::LineTo(self.x, self.y));
269                    }
270                }
271                8 => {
272                    // rrcurveto: dx1 dy1 dx2 dy2 dx3 dy3
273                    if self.stack.len() < 6 {
274                        return Err("rrcurveto: stack underflow".to_string());
275                    }
276                    let dy3 = self.stack.pop().unwrap();
277                    let dx3 = self.stack.pop().unwrap();
278                    let dy2 = self.stack.pop().unwrap();
279                    let dx2 = self.stack.pop().unwrap();
280                    let dy1 = self.stack.pop().unwrap();
281                    let dx1 = self.stack.pop().unwrap();
282                    let x1 = self.x + dx1;
283                    let y1 = self.y + dy1;
284                    let x2 = x1 + dx2;
285                    let y2 = y1 + dy2;
286                    let x3 = x2 + dx3;
287                    let y3 = y2 + dy3;
288                    if !self.width_only {
289                        self.path.segments.push(PathSegment::CurveTo {
290                            x1,
291                            y1,
292                            x2,
293                            y2,
294                            x3,
295                            y3,
296                        });
297                    }
298                    self.x = x3;
299                    self.y = y3;
300                }
301                9 => {
302                    // closepath
303                    if !self.width_only {
304                        self.path.segments.push(PathSegment::ClosePath);
305                    }
306                }
307                10 => {
308                    // callsubr: index
309                    if self.stack.is_empty() {
310                        return Err("callsubr: stack underflow".to_string());
311                    }
312                    let idx = self.stack.pop().unwrap() as usize;
313                    if idx >= self.subrs.len() {
314                        return Err(format!("callsubr: index {} out of range", idx));
315                    }
316                    let subr_data = decrypt_charstring(&self.subrs[idx], self.len_iv);
317                    self.execute_inner(&subr_data, depth + 1)?;
318                }
319                11 => {
320                    // return — return from subroutine
321                    return Ok(());
322                }
323                12 => {
324                    // Two-byte escape
325                    if pos >= data.len() {
326                        break;
327                    }
328                    let b2 = data[pos];
329                    pos += 1;
330                    self.execute_escape(b2)?;
331                }
332                13 => {
333                    // hsbw: sbx wx
334                    // Sets sidebearing and width. Does NOT emit a MoveTo —
335                    // the first real moveto in the glyph body will do that.
336                    if self.stack.len() < 2 {
337                        return Err("hsbw: stack underflow".to_string());
338                    }
339                    let wx = self.stack.pop().unwrap();
340                    let sbx = self.stack.pop().unwrap();
341                    self.lsb_x = sbx;
342                    self.lsb_y = 0.0;
343                    self.width_x = wx;
344                    self.width_y = 0.0;
345                    if let Some((ox, oy)) = self.seac_accent_offset {
346                        // seac accent: offset from accent's sidebearing origin
347                        self.x = sbx + ox;
348                        self.y = oy;
349                    } else {
350                        self.x = sbx;
351                        self.y = 0.0;
352                    }
353                }
354                14 => {
355                    // endchar — signal completion
356                    if !self.width_only && !self.path.is_empty() {
357                        // Implicit closepath if path is open
358                    }
359                    self.done = true;
360                    return Ok(());
361                }
362                15..=20 => {} // reserved
363                21 => {
364                    // rmoveto: dx dy
365                    if self.stack.len() < 2 {
366                        return Err("rmoveto: stack underflow".to_string());
367                    }
368                    let dy = self.stack.pop().unwrap();
369                    let dx = self.stack.pop().unwrap();
370                    self.x += dx;
371                    self.y += dy;
372                    if !self.width_only && !self.flex_active {
373                        self.path.segments.push(PathSegment::MoveTo(self.x, self.y));
374                    }
375                    // During flex, moveto just updates current point — OtherSubrs 2 handles flex_points
376                }
377                22 => {
378                    // hmoveto: dx
379                    if self.stack.is_empty() {
380                        return Err("hmoveto: stack underflow".to_string());
381                    }
382                    let dx = self.stack.pop().unwrap();
383                    self.x += dx;
384                    if !self.width_only && !self.flex_active {
385                        self.path.segments.push(PathSegment::MoveTo(self.x, self.y));
386                    }
387                    // During flex, moveto just updates current point — OtherSubrs 2 handles flex_points
388                }
389                23..=29 => {} // reserved
390                30 => {
391                    // vhcurveto: dy1 dx2 dy2 dx3
392                    if self.stack.len() < 4 {
393                        return Err("vhcurveto: stack underflow".to_string());
394                    }
395                    let dx3 = self.stack.pop().unwrap();
396                    let dy2 = self.stack.pop().unwrap();
397                    let dx2 = self.stack.pop().unwrap();
398                    let dy1 = self.stack.pop().unwrap();
399                    let x1 = self.x;
400                    let y1 = self.y + dy1;
401                    let x2 = x1 + dx2;
402                    let y2 = y1 + dy2;
403                    let x3 = x2 + dx3;
404                    let y3 = y2;
405                    if !self.width_only {
406                        self.path.segments.push(PathSegment::CurveTo {
407                            x1,
408                            y1,
409                            x2,
410                            y2,
411                            x3,
412                            y3,
413                        });
414                    }
415                    self.x = x3;
416                    self.y = y3;
417                }
418                31 => {
419                    // hvcurveto: dx1 dx2 dy2 dy3
420                    if self.stack.len() < 4 {
421                        return Err("hvcurveto: stack underflow".to_string());
422                    }
423                    let dy3 = self.stack.pop().unwrap();
424                    let dy2 = self.stack.pop().unwrap();
425                    let dx2 = self.stack.pop().unwrap();
426                    let dx1 = self.stack.pop().unwrap();
427                    let x1 = self.x + dx1;
428                    let y1 = self.y;
429                    let x2 = x1 + dx2;
430                    let y2 = y1 + dy2;
431                    let x3 = x2;
432                    let y3 = y2 + dy3;
433                    if !self.width_only {
434                        self.path.segments.push(PathSegment::CurveTo {
435                            x1,
436                            y1,
437                            x2,
438                            y2,
439                            x3,
440                            y3,
441                        });
442                    }
443                    self.x = x3;
444                    self.y = y3;
445                }
446                // Number encoding
447                32..=246 => {
448                    // Single-byte integer: value = b - 139
449                    self.stack.push(b as f64 - 139.0);
450                }
451                247..=250 => {
452                    // Two-byte positive: ((b - 247) * 256 + next) + 108
453                    if pos >= data.len() {
454                        break;
455                    }
456                    let b2 = data[pos];
457                    pos += 1;
458                    let val = ((b as i32 - 247) * 256 + b2 as i32) + 108;
459                    self.stack.push(val as f64);
460                }
461                251..=254 => {
462                    // Two-byte negative: -((b - 251) * 256 + next) - 108
463                    if pos >= data.len() {
464                        break;
465                    }
466                    let b2 = data[pos];
467                    pos += 1;
468                    let val = -((b as i32 - 251) * 256 + b2 as i32) - 108;
469                    self.stack.push(val as f64);
470                }
471                255 => {
472                    // Five-byte signed 32-bit integer
473                    if pos + 4 > data.len() {
474                        break;
475                    }
476                    let val = i32::from_be_bytes([
477                        data[pos],
478                        data[pos + 1],
479                        data[pos + 2],
480                        data[pos + 3],
481                    ]);
482                    pos += 4;
483                    self.stack.push(val as f64);
484                }
485            }
486        }
487        Ok(())
488    }
489
490    fn execute_escape(&mut self, b2: u8) -> Result<(), String> {
491        match b2 {
492            0 => {
493                // dotsection — ignore (hint), no args
494            }
495            1 => {
496                // vstem3: x0 dx0 x1 dx1 x2 dx2 — ignore (hint), pop 6 args
497                for _ in 0..6.min(self.stack.len()) {
498                    self.stack.pop();
499                }
500            }
501            2 => {
502                // hstem3: y0 dy0 y1 dy1 y2 dy2 — ignore (hint), pop 6 args
503                for _ in 0..6.min(self.stack.len()) {
504                    self.stack.pop();
505                }
506            }
507            6 => {
508                // seac: asb adx ady bchar achar
509                // Builds a composite glyph from base + accent characters
510                if self.stack.len() < 5 {
511                    return Err("seac: stack underflow".to_string());
512                }
513                let achar = self.stack.pop().unwrap() as u8;
514                let bchar = self.stack.pop().unwrap() as u8;
515                let ady = self.stack.pop().unwrap();
516                let adx = self.stack.pop().unwrap();
517                let asb = self.stack.pop().unwrap();
518
519                // Look up base and accent glyph names in StandardEncoding
520                let bname = STANDARD_ENCODING[bchar as usize];
521                let aname = STANDARD_ENCODING[achar as usize];
522
523                // Extract charstring data from lookup before executing (borrow checker)
524                let bchar_data = self.cs_lookup.as_ref().and_then(|f| f(bname));
525                let achar_data = self.cs_lookup.as_ref().and_then(|f| f(aname));
526
527                if let Some(bchar_data) = bchar_data {
528                    let saved_width_x = self.width_x;
529                    let saved_width_y = self.width_y;
530                    let saved_x = self.x;
531                    let saved_y = self.y;
532
533                    // Execute base character charstring
534                    let decrypted = decrypt_charstring(&bchar_data, self.len_iv);
535                    self.x = 0.0;
536                    self.y = 0.0;
537                    self.done = false;
538                    self.execute(&decrypted)?;
539                    let base_lsb = self.lsb_x;
540                    self.done = false;
541
542                    // Execute accent character charstring with offset.
543                    // Per the Type 1 spec, the accent's origin (0,0) is placed
544                    // at (adx - asb + base_lsb, ady) in the composite's
545                    // coordinate system.  hsbw/sbw adds this translation to
546                    // the accent's sidebearing so all path elements shift.
547                    if let Some(achar_data) = achar_data {
548                        let decrypted = decrypt_charstring(&achar_data, self.len_iv);
549                        self.seac_accent_offset = Some((adx - asb + base_lsb, ady));
550                        self.execute(&decrypted)?;
551                        self.seac_accent_offset = None;
552                    }
553
554                    // Restore original width (from the composite's hsbw/sbw)
555                    self.width_x = saved_width_x;
556                    self.width_y = saved_width_y;
557                    self.x = saved_x;
558                    self.y = saved_y;
559                }
560                // If no lookup available, seac produces no path (graceful degradation)
561            }
562            7 => {
563                // sbw: sbx sby wx wy
564                // Sets sidebearing and width. Does NOT emit a MoveTo —
565                // the first real moveto in the glyph body will do that.
566                if self.stack.len() < 4 {
567                    return Err("sbw: stack underflow".to_string());
568                }
569                let wy = self.stack.pop().unwrap();
570                let wx = self.stack.pop().unwrap();
571                let sby = self.stack.pop().unwrap();
572                let sbx = self.stack.pop().unwrap();
573                self.lsb_x = sbx;
574                self.lsb_y = sby;
575                self.width_x = wx;
576                self.width_y = wy;
577                if let Some((ox, oy)) = self.seac_accent_offset {
578                    self.x = sbx + ox;
579                    self.y = sby + oy;
580                } else {
581                    self.x = sbx;
582                    self.y = sby;
583                }
584            }
585            12 => {
586                // div: num1 num2 → num1/num2
587                if self.stack.len() < 2 {
588                    return Err("div: stack underflow".to_string());
589                }
590                let b = self.stack.pop().unwrap();
591                let a = self.stack.pop().unwrap();
592                if b == 0.0 {
593                    self.stack.push(0.0);
594                } else {
595                    self.stack.push(a / b);
596                }
597            }
598            16 => {
599                // callothersubr: args... n subr#
600                if self.stack.len() < 2 {
601                    return Err("callothersubr: stack underflow".to_string());
602                }
603                let subr_num = self.stack.pop().unwrap() as i32;
604                let n_args = self.stack.pop().unwrap() as usize;
605
606                if self.stack.len() < n_args {
607                    return Err("callothersubr: not enough args".to_string());
608                }
609
610                // Pop arguments from charstring stack
611                let mut args: Vec<f64> = Vec::with_capacity(n_args);
612                for _ in 0..n_args {
613                    args.push(self.stack.pop().unwrap());
614                }
615                args.reverse(); // Args were popped in reverse order
616
617                match subr_num {
618                    0 => {
619                        // EndFlex: construct two bezier curves from flex points
620                        // args[0] = flex_depth (unused — we always draw curves)
621                        if self.flex_points.len() >= 7 {
622                            let _p0 = self.flex_points[0]; // reference point
623                            let p1 = self.flex_points[1];
624                            let p2 = self.flex_points[2];
625                            let p3 = self.flex_points[3];
626                            let p4 = self.flex_points[4];
627                            let p5 = self.flex_points[5];
628                            let p6 = self.flex_points[6];
629
630                            if !self.width_only {
631                                // First curve: from current (should be p0) to p3
632                                self.path.segments.push(PathSegment::CurveTo {
633                                    x1: p1.0,
634                                    y1: p1.1,
635                                    x2: p2.0,
636                                    y2: p2.1,
637                                    x3: p3.0,
638                                    y3: p3.1,
639                                });
640                                // Second curve: from p3 to p6
641                                self.path.segments.push(PathSegment::CurveTo {
642                                    x1: p4.0,
643                                    y1: p4.1,
644                                    x2: p5.0,
645                                    y2: p5.1,
646                                    x3: p6.0,
647                                    y3: p6.1,
648                                });
649                            }
650                            self.x = p6.0;
651                            self.y = p6.1;
652                        }
653
654                        self.flex_active = false;
655                        self.flex_points.clear();
656
657                        // Push y then x onto ps_stack so pop+pop+setcurrentpoint
658                        // gets the correct order (x on top, popped first into
659                        // charstring stack, then y).
660                        self.ps_stack.push(self.y);
661                        self.ps_stack.push(self.x);
662                    }
663                    1 => {
664                        // StartFlex: begin accumulating flex points
665                        // Do NOT pre-push current point — OtherSubrs 2 (AddFlex)
666                        // handles all point accumulation.
667                        self.flex_active = true;
668                        self.flex_points.clear();
669                    }
670                    2 => {
671                        // AddFlex: add current point to flex list
672                        self.flex_points.push((self.x, self.y));
673                        // Push y then x onto ps_stack for the subsequent pop+pop
674                        // in the standard flex subroutine.
675                        self.ps_stack.push(self.y);
676                        self.ps_stack.push(self.x);
677                    }
678                    3 => {
679                        // Hint replacement — push 3 onto ps_stack for pop
680                        self.ps_stack.push(3.0);
681                    }
682                    14..=18 => {
683                        // Multiple Master blend OtherSubrs:
684                        // OtherSubr 14 = blend 1 value, 15 = 2, 16 = 3, 17 = 4, 18 = 6
685                        let num_results = match subr_num {
686                            14 => 1,
687                            15 => 2,
688                            16 => 3,
689                            17 => 4,
690                            18 => 6,
691                            _ => unreachable!(),
692                        };
693                        if let Some(ref wv) = self.weight_vector {
694                            let nm = wv.len(); // number of masters
695                            let nd = nm - 1; // number of deltas per result
696                            // Layout after pop+reverse:
697                            //   [base0, base1, ..., baseN-1,
698                            //    d0_w1, d0_w2, ..., d0_wN-1,
699                            //    d1_w1, d1_w2, ..., d1_wN-1, ...]
700                            // result[r] = base[r] + w[1]*d[r][0] + w[2]*d[r][1] + ... + w[nm-1]*d[r][nd-1]
701                            //
702                            // Push results in REVERSE order so pop retrieves result0
703                            // first (matching the PS OtherSubr code's stack layout).
704                            let mut results = Vec::with_capacity(num_results);
705                            for r in 0..num_results {
706                                let base_val = if r < args.len() { args[r] } else { 0.0 };
707                                let mut blended = base_val;
708                                for j in 0..nd {
709                                    let delta_idx = num_results + r * nd + j;
710                                    let weight_idx = j + 1;
711                                    if delta_idx < args.len() && weight_idx < wv.len() {
712                                        blended += wv[weight_idx] * args[delta_idx];
713                                    }
714                                }
715                                results.push(blended);
716                            }
717                            for r in results.into_iter().rev() {
718                                self.ps_stack.push(r);
719                            }
720                        } else {
721                            // No weight vector — use base values only
722                            for r in 0..num_results {
723                                self.ps_stack
724                                    .push(if r < args.len() { args[r] } else { 0.0 });
725                            }
726                        }
727                    }
728                    _ => {
729                        // Unknown OtherSubr — push args onto ps_stack
730                        for &a in &args {
731                            self.ps_stack.push(a);
732                        }
733                    }
734                }
735            }
736            17 => {
737                // pop: move value from OtherSubrs stack to charstring stack
738                if let Some(val) = self.ps_stack.pop() {
739                    self.stack.push(val);
740                } else {
741                    self.stack.push(0.0);
742                }
743            }
744            33 => {
745                // setcurrentpoint: x y
746                if self.stack.len() < 2 {
747                    return Err("setcurrentpoint: stack underflow".to_string());
748                }
749                let y = self.stack.pop().unwrap();
750                let x = self.stack.pop().unwrap();
751                self.x = x;
752                self.y = y;
753            }
754            _ => {
755                // Unknown escape — ignore
756            }
757        }
758        Ok(())
759    }
760}
761
762// Fix: p0 is used in the flex code above but the compiler may not see it.
763// The flex code references p0 via flex_points[0] directly.
764
765#[cfg(test)]
766mod tests {
767    use super::*;
768
769    #[test]
770    fn test_decrypt_charstring_basic() {
771        // Encrypt some data with R=4330, then decrypt and verify
772        let plain = b"\x8b\x0e"; // push 0 (0x8b = 139-139=0), endchar (0x0e = 14)
773        let c1: u32 = 52845;
774        let c2: u32 = 22719;
775        let mut r: u32 = 4330;
776
777        // Prepend 4 random bytes (zeros)
778        let mut to_encrypt = vec![0u8; 4];
779        to_encrypt.extend_from_slice(plain);
780
781        let mut encrypted = Vec::new();
782        for &p in &to_encrypt {
783            let c = (p as u32 ^ (r >> 8)) as u8;
784            encrypted.push(c);
785            r = ((c as u32 + r) * c1 + c2) & 0xFFFF;
786        }
787
788        let decrypted = decrypt_charstring(&encrypted, 4);
789        assert_eq!(decrypted, plain);
790    }
791
792    #[test]
793    fn test_number_encoding_single_byte() {
794        // Test that single-byte numbers are decoded correctly
795        // byte 139 = 0, byte 140 = 1, byte 246 = 107, byte 32 = -107
796        // Use hsbw to consume 2 values, then endchar
797        let code = vec![
798            139, // push 0 (sbx)
799            140, // push 1 (wx)
800            13,  // hsbw
801            14,  // endchar
802        ];
803        let mut interp = CharstringInterp::new(&[], 4, true, None);
804        interp.execute_inner(&code, 0).unwrap();
805        assert!((interp.width_x - 1.0).abs() < 0.01);
806    }
807
808    #[test]
809    fn test_hsbw_sets_width() {
810        // hsbw: sbx=0 wx=600
811        // For 600: value = ((b-247)*256 + b2) + 108
812        // 600 - 108 = 492; 492 / 256 = 1 rem 236 → b=248, b2=236
813        let data = vec![
814            139, // push 0 (sbx)
815            248, 236, // push 600 (wx)
816            13,  // hsbw
817            14,  // endchar
818        ];
819        let mut interp = CharstringInterp::new(&[], 4, true, None);
820        interp.execute_inner(&data, 0).unwrap();
821        assert!((interp.width_x - 600.0).abs() < 0.01);
822        assert!((interp.lsb_x - 0.0).abs() < 0.01);
823    }
824
825    #[test]
826    fn test_rmoveto_rlineto() {
827        let data = vec![
828            139, // push 0 (sbx)
829            248,
830            236, // push 600 (wx)
831            13,  // hsbw
832            // rmoveto: dx=100, dy=200
833            139 + 100, // push 100
834            139 + 107, // push 107 (max single byte)
835            21,        // rmoveto
836            // rlineto: dx=50, dy=50
837            139 + 50, // push 50
838            139 + 50, // push 50
839            5,        // rlineto
840            9,        // closepath
841            14,       // endchar
842        ];
843        let mut interp = CharstringInterp::new(&[], 4, false, None);
844        interp.execute_inner(&data, 0).unwrap();
845
846        // hsbw(0, 600): sets x=0, y=0 but does NOT emit MoveTo
847        // rmoveto(100, 107): x=100, y=107, emits MoveTo(100,107)
848        // rlineto(50, 50): x=150, y=157, emits LineTo(150,157)
849        // closepath
850        assert_eq!(interp.path.segments.len(), 3); // moveto(rmoveto), lineto, closepath
851        match &interp.path.segments[1] {
852            PathSegment::LineTo(x, y) => {
853                assert!((x - 150.0).abs() < 0.01);
854                assert!((y - 157.0).abs() < 0.01);
855            }
856            _ => panic!("Expected LineTo"),
857        }
858    }
859
860    #[test]
861    fn test_execute_real_charstring() {
862        // Load a real font and execute the 'space' charstring
863        let font_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
864            .join("../../resources/Font/NimbusSans-Regular.t1");
865        if !font_path.exists() {
866            eprintln!("Skipping test — font file not found");
867            return;
868        }
869
870        let data = std::fs::read(&font_path).unwrap();
871        let font = crate::type1_parser::parse_type1(&data).unwrap();
872
873        // Execute 'space' charstring — should have a width but no path
874        let space_cs = font.charstrings.get("space").expect("'space' charstring");
875        let result = execute_charstring(space_cs, &font.subrs, font.len_iv, false).unwrap();
876        assert!(result.width_x > 0.0, "space should have positive width");
877
878        // Execute 'A' charstring — should have paths
879        let a_cs = font.charstrings.get("A").expect("'A' charstring");
880        let result = execute_charstring(a_cs, &font.subrs, font.len_iv, false).unwrap();
881        assert!(result.width_x > 0.0, "A should have positive width");
882        assert!(!result.path.is_empty(), "A should have path segments");
883
884        // Width-only mode should produce same width but empty path
885        let result_wo = execute_charstring(a_cs, &font.subrs, font.len_iv, true).unwrap();
886        assert!((result_wo.width_x - result.width_x).abs() < 0.01);
887        assert!(result_wo.path.is_empty());
888    }
889
890    #[test]
891    fn test_execute_multiple_glyphs() {
892        let font_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
893            .join("../../resources/Font/NimbusSans-Regular.t1");
894        if !font_path.exists() {
895            eprintln!("Skipping test — font file not found");
896            return;
897        }
898
899        let data = std::fs::read(&font_path).unwrap();
900        let font = crate::type1_parser::parse_type1(&data).unwrap();
901
902        // Execute several common glyphs
903        for glyph_name in &["A", "B", "a", "b", "zero", "one", "period", "comma"] {
904            if let Some(cs) = font.charstrings.get(*glyph_name) {
905                let result = execute_charstring(cs, &font.subrs, font.len_iv, false).unwrap();
906                assert!(
907                    result.width_x > 0.0,
908                    "'{}' should have positive width",
909                    glyph_name
910                );
911                assert!(
912                    !result.path.is_empty(),
913                    "'{}' should have path segments",
914                    glyph_name
915                );
916            }
917        }
918    }
919}