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, depth)?;
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    /// Handle a two-byte (escape) operator.
491    ///
492    /// `depth` is the caller's subroutine nesting level, threaded through so
493    /// the `seac` handler can keep counting rather than restarting at zero.
494    fn execute_escape(&mut self, b2: u8, depth: usize) -> Result<(), String> {
495        match b2 {
496            0 => {
497                // dotsection — ignore (hint), no args
498            }
499            1 => {
500                // vstem3: x0 dx0 x1 dx1 x2 dx2 — ignore (hint), pop 6 args
501                for _ in 0..6.min(self.stack.len()) {
502                    self.stack.pop();
503                }
504            }
505            2 => {
506                // hstem3: y0 dy0 y1 dy1 y2 dy2 — ignore (hint), pop 6 args
507                for _ in 0..6.min(self.stack.len()) {
508                    self.stack.pop();
509                }
510            }
511            6 => {
512                // seac: asb adx ady bchar achar
513                // Builds a composite glyph from base + accent characters
514                if self.stack.len() < 5 {
515                    return Err("seac: stack underflow".to_string());
516                }
517                let achar = self.stack.pop().unwrap() as u8;
518                let bchar = self.stack.pop().unwrap() as u8;
519                let ady = self.stack.pop().unwrap();
520                let adx = self.stack.pop().unwrap();
521                let asb = self.stack.pop().unwrap();
522
523                // Look up base and accent glyph names in StandardEncoding
524                let bname = STANDARD_ENCODING[bchar as usize];
525                let aname = STANDARD_ENCODING[achar as usize];
526
527                // Extract charstring data from lookup before executing (borrow checker)
528                let bchar_data = self.cs_lookup.as_ref().and_then(|f| f(bname));
529                let achar_data = self.cs_lookup.as_ref().and_then(|f| f(aname));
530
531                if let Some(bchar_data) = bchar_data {
532                    let saved_width_x = self.width_x;
533                    let saved_width_y = self.width_y;
534                    let saved_x = self.x;
535                    let saved_y = self.y;
536
537                    // Execute base character charstring.
538                    //
539                    // `execute_inner`, not `execute`: the latter restarts the
540                    // counter at 0, so the depth guard above never fires and a
541                    // seac naming its own glyph recurses until the native stack
542                    // is gone — an abort rather than a panic.
543                    let decrypted = decrypt_charstring(&bchar_data, self.len_iv);
544                    self.x = 0.0;
545                    self.y = 0.0;
546                    self.done = false;
547                    self.execute_inner(&decrypted, depth + 1)?;
548                    let base_lsb = self.lsb_x;
549                    self.done = false;
550
551                    // Execute accent character charstring with offset.
552                    // Per the Type 1 spec, the accent's origin (0,0) is placed
553                    // at (adx - asb + base_lsb, ady) in the composite's
554                    // coordinate system.  hsbw/sbw adds this translation to
555                    // the accent's sidebearing so all path elements shift.
556                    if let Some(achar_data) = achar_data {
557                        let decrypted = decrypt_charstring(&achar_data, self.len_iv);
558                        self.seac_accent_offset = Some((adx - asb + base_lsb, ady));
559                        // Threaded, for the same reason as the base above.
560                        self.execute_inner(&decrypted, depth + 1)?;
561                        self.seac_accent_offset = None;
562                    }
563
564                    // Restore original width (from the composite's hsbw/sbw)
565                    self.width_x = saved_width_x;
566                    self.width_y = saved_width_y;
567                    self.x = saved_x;
568                    self.y = saved_y;
569                }
570                // If no lookup available, seac produces no path (graceful degradation)
571            }
572            7 => {
573                // sbw: sbx sby wx wy
574                // Sets sidebearing and width. Does NOT emit a MoveTo —
575                // the first real moveto in the glyph body will do that.
576                if self.stack.len() < 4 {
577                    return Err("sbw: stack underflow".to_string());
578                }
579                let wy = self.stack.pop().unwrap();
580                let wx = self.stack.pop().unwrap();
581                let sby = self.stack.pop().unwrap();
582                let sbx = self.stack.pop().unwrap();
583                self.lsb_x = sbx;
584                self.lsb_y = sby;
585                self.width_x = wx;
586                self.width_y = wy;
587                if let Some((ox, oy)) = self.seac_accent_offset {
588                    self.x = sbx + ox;
589                    self.y = sby + oy;
590                } else {
591                    self.x = sbx;
592                    self.y = sby;
593                }
594            }
595            12 => {
596                // div: num1 num2 → num1/num2
597                if self.stack.len() < 2 {
598                    return Err("div: stack underflow".to_string());
599                }
600                let b = self.stack.pop().unwrap();
601                let a = self.stack.pop().unwrap();
602                if b == 0.0 {
603                    self.stack.push(0.0);
604                } else {
605                    self.stack.push(a / b);
606                }
607            }
608            16 => {
609                // callothersubr: args... n subr#
610                if self.stack.len() < 2 {
611                    return Err("callothersubr: stack underflow".to_string());
612                }
613                let subr_num = self.stack.pop().unwrap() as i32;
614                let n_args = self.stack.pop().unwrap() as usize;
615
616                if self.stack.len() < n_args {
617                    return Err("callothersubr: not enough args".to_string());
618                }
619
620                // Pop arguments from charstring stack
621                let mut args: Vec<f64> = Vec::with_capacity(n_args);
622                for _ in 0..n_args {
623                    args.push(self.stack.pop().unwrap());
624                }
625                args.reverse(); // Args were popped in reverse order
626
627                match subr_num {
628                    0 => {
629                        // EndFlex: construct two bezier curves from flex points
630                        // args[0] = flex_depth (unused — we always draw curves)
631                        if self.flex_points.len() >= 7 {
632                            let _p0 = self.flex_points[0]; // reference point
633                            let p1 = self.flex_points[1];
634                            let p2 = self.flex_points[2];
635                            let p3 = self.flex_points[3];
636                            let p4 = self.flex_points[4];
637                            let p5 = self.flex_points[5];
638                            let p6 = self.flex_points[6];
639
640                            if !self.width_only {
641                                // First curve: from current (should be p0) to p3
642                                self.path.segments.push(PathSegment::CurveTo {
643                                    x1: p1.0,
644                                    y1: p1.1,
645                                    x2: p2.0,
646                                    y2: p2.1,
647                                    x3: p3.0,
648                                    y3: p3.1,
649                                });
650                                // Second curve: from p3 to p6
651                                self.path.segments.push(PathSegment::CurveTo {
652                                    x1: p4.0,
653                                    y1: p4.1,
654                                    x2: p5.0,
655                                    y2: p5.1,
656                                    x3: p6.0,
657                                    y3: p6.1,
658                                });
659                            }
660                            self.x = p6.0;
661                            self.y = p6.1;
662                        }
663
664                        self.flex_active = false;
665                        self.flex_points.clear();
666
667                        // Push y then x onto ps_stack so pop+pop+setcurrentpoint
668                        // gets the correct order (x on top, popped first into
669                        // charstring stack, then y).
670                        self.ps_stack.push(self.y);
671                        self.ps_stack.push(self.x);
672                    }
673                    1 => {
674                        // StartFlex: begin accumulating flex points
675                        // Do NOT pre-push current point — OtherSubrs 2 (AddFlex)
676                        // handles all point accumulation.
677                        self.flex_active = true;
678                        self.flex_points.clear();
679                    }
680                    2 => {
681                        // AddFlex: add current point to flex list
682                        self.flex_points.push((self.x, self.y));
683                        // Push y then x onto ps_stack for the subsequent pop+pop
684                        // in the standard flex subroutine.
685                        self.ps_stack.push(self.y);
686                        self.ps_stack.push(self.x);
687                    }
688                    3 => {
689                        // Hint replacement — push 3 onto ps_stack for pop
690                        self.ps_stack.push(3.0);
691                    }
692                    14..=18 => {
693                        // Multiple Master blend OtherSubrs:
694                        // OtherSubr 14 = blend 1 value, 15 = 2, 16 = 3, 17 = 4, 18 = 6
695                        let num_results = match subr_num {
696                            14 => 1,
697                            15 => 2,
698                            16 => 3,
699                            17 => 4,
700                            18 => 6,
701                            _ => unreachable!(),
702                        };
703                        if let Some(ref wv) = self.weight_vector {
704                            let nm = wv.len(); // number of masters
705                            let nd = nm - 1; // number of deltas per result
706                            // Layout after pop+reverse:
707                            //   [base0, base1, ..., baseN-1,
708                            //    d0_w1, d0_w2, ..., d0_wN-1,
709                            //    d1_w1, d1_w2, ..., d1_wN-1, ...]
710                            // result[r] = base[r] + w[1]*d[r][0] + w[2]*d[r][1] + ... + w[nm-1]*d[r][nd-1]
711                            //
712                            // Push results in REVERSE order so pop retrieves result0
713                            // first (matching the PS OtherSubr code's stack layout).
714                            let mut results = Vec::with_capacity(num_results);
715                            for r in 0..num_results {
716                                let base_val = if r < args.len() { args[r] } else { 0.0 };
717                                let mut blended = base_val;
718                                for j in 0..nd {
719                                    let delta_idx = num_results + r * nd + j;
720                                    let weight_idx = j + 1;
721                                    if delta_idx < args.len() && weight_idx < wv.len() {
722                                        blended += wv[weight_idx] * args[delta_idx];
723                                    }
724                                }
725                                results.push(blended);
726                            }
727                            for r in results.into_iter().rev() {
728                                self.ps_stack.push(r);
729                            }
730                        } else {
731                            // No weight vector — use base values only
732                            for r in 0..num_results {
733                                self.ps_stack
734                                    .push(if r < args.len() { args[r] } else { 0.0 });
735                            }
736                        }
737                    }
738                    _ => {
739                        // Unknown OtherSubr — push args onto ps_stack
740                        for &a in &args {
741                            self.ps_stack.push(a);
742                        }
743                    }
744                }
745            }
746            17 => {
747                // pop: move value from OtherSubrs stack to charstring stack
748                if let Some(val) = self.ps_stack.pop() {
749                    self.stack.push(val);
750                } else {
751                    self.stack.push(0.0);
752                }
753            }
754            33 => {
755                // setcurrentpoint: x y
756                if self.stack.len() < 2 {
757                    return Err("setcurrentpoint: stack underflow".to_string());
758                }
759                let y = self.stack.pop().unwrap();
760                let x = self.stack.pop().unwrap();
761                self.x = x;
762                self.y = y;
763            }
764            _ => {
765                // Unknown escape — ignore
766            }
767        }
768        Ok(())
769    }
770}
771
772// Fix: p0 is used in the flex code above but the compiler may not see it.
773// The flex code references p0 via flex_points[0] directly.
774
775#[cfg(test)]
776mod tests {
777    use super::*;
778
779    #[test]
780    fn test_decrypt_charstring_basic() {
781        // Encrypt some data with R=4330, then decrypt and verify
782        let plain = b"\x8b\x0e"; // push 0 (0x8b = 139-139=0), endchar (0x0e = 14)
783        let c1: u32 = 52845;
784        let c2: u32 = 22719;
785        let mut r: u32 = 4330;
786
787        // Prepend 4 random bytes (zeros)
788        let mut to_encrypt = vec![0u8; 4];
789        to_encrypt.extend_from_slice(plain);
790
791        let mut encrypted = Vec::new();
792        for &p in &to_encrypt {
793            let c = (p as u32 ^ (r >> 8)) as u8;
794            encrypted.push(c);
795            r = ((c as u32 + r) * c1 + c2) & 0xFFFF;
796        }
797
798        let decrypted = decrypt_charstring(&encrypted, 4);
799        assert_eq!(decrypted, plain);
800    }
801
802    #[test]
803    fn test_number_encoding_single_byte() {
804        // Test that single-byte numbers are decoded correctly
805        // byte 139 = 0, byte 140 = 1, byte 246 = 107, byte 32 = -107
806        // Use hsbw to consume 2 values, then endchar
807        let code = vec![
808            139, // push 0 (sbx)
809            140, // push 1 (wx)
810            13,  // hsbw
811            14,  // endchar
812        ];
813        let mut interp = CharstringInterp::new(&[], 4, true, None);
814        interp.execute_inner(&code, 0).unwrap();
815        assert!((interp.width_x - 1.0).abs() < 0.01);
816    }
817
818    #[test]
819    fn test_hsbw_sets_width() {
820        // hsbw: sbx=0 wx=600
821        // For 600: value = ((b-247)*256 + b2) + 108
822        // 600 - 108 = 492; 492 / 256 = 1 rem 236 → b=248, b2=236
823        let data = vec![
824            139, // push 0 (sbx)
825            248, 236, // push 600 (wx)
826            13,  // hsbw
827            14,  // endchar
828        ];
829        let mut interp = CharstringInterp::new(&[], 4, true, None);
830        interp.execute_inner(&data, 0).unwrap();
831        assert!((interp.width_x - 600.0).abs() < 0.01);
832        assert!((interp.lsb_x - 0.0).abs() < 0.01);
833    }
834
835    #[test]
836    fn test_rmoveto_rlineto() {
837        let data = vec![
838            139, // push 0 (sbx)
839            248,
840            236, // push 600 (wx)
841            13,  // hsbw
842            // rmoveto: dx=100, dy=200
843            139 + 100, // push 100
844            139 + 107, // push 107 (max single byte)
845            21,        // rmoveto
846            // rlineto: dx=50, dy=50
847            139 + 50, // push 50
848            139 + 50, // push 50
849            5,        // rlineto
850            9,        // closepath
851            14,       // endchar
852        ];
853        let mut interp = CharstringInterp::new(&[], 4, false, None);
854        interp.execute_inner(&data, 0).unwrap();
855
856        // hsbw(0, 600): sets x=0, y=0 but does NOT emit MoveTo
857        // rmoveto(100, 107): x=100, y=107, emits MoveTo(100,107)
858        // rlineto(50, 50): x=150, y=157, emits LineTo(150,157)
859        // closepath
860        assert_eq!(interp.path.segments.len(), 3); // moveto(rmoveto), lineto, closepath
861        match &interp.path.segments[1] {
862            PathSegment::LineTo(x, y) => {
863                assert!((x - 150.0).abs() < 0.01);
864                assert!((y - 157.0).abs() < 0.01);
865            }
866            _ => panic!("Expected LineTo"),
867        }
868    }
869
870    #[test]
871    fn test_execute_real_charstring() {
872        // Load a real font and execute the 'space' charstring
873        let font_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
874            .join("../../resources/Font/NimbusSans-Regular.t1");
875        if !font_path.exists() {
876            eprintln!("Skipping test — font file not found");
877            return;
878        }
879
880        let data = std::fs::read(&font_path).unwrap();
881        let font = crate::type1_parser::parse_type1(&data).unwrap();
882
883        // Execute 'space' charstring — should have a width but no path
884        let space_cs = font.charstrings.get("space").expect("'space' charstring");
885        let result = execute_charstring(space_cs, &font.subrs, font.len_iv, false).unwrap();
886        assert!(result.width_x > 0.0, "space should have positive width");
887
888        // Execute 'A' charstring — should have paths
889        let a_cs = font.charstrings.get("A").expect("'A' charstring");
890        let result = execute_charstring(a_cs, &font.subrs, font.len_iv, false).unwrap();
891        assert!(result.width_x > 0.0, "A should have positive width");
892        assert!(!result.path.is_empty(), "A should have path segments");
893
894        // Width-only mode should produce same width but empty path
895        let result_wo = execute_charstring(a_cs, &font.subrs, font.len_iv, true).unwrap();
896        assert!((result_wo.width_x - result.width_x).abs() < 0.01);
897        assert!(result_wo.path.is_empty());
898    }
899
900    #[test]
901    fn test_execute_multiple_glyphs() {
902        let font_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
903            .join("../../resources/Font/NimbusSans-Regular.t1");
904        if !font_path.exists() {
905            eprintln!("Skipping test — font file not found");
906            return;
907        }
908
909        let data = std::fs::read(&font_path).unwrap();
910        let font = crate::type1_parser::parse_type1(&data).unwrap();
911
912        // Execute several common glyphs
913        for glyph_name in &["A", "B", "a", "b", "zero", "one", "period", "comma"] {
914            if let Some(cs) = font.charstrings.get(*glyph_name) {
915                let result = execute_charstring(cs, &font.subrs, font.len_iv, false).unwrap();
916                assert!(
917                    result.width_x > 0.0,
918                    "'{}' should have positive width",
919                    glyph_name
920                );
921                assert!(
922                    !result.path.is_empty(),
923                    "'{}' should have path segments",
924                    glyph_name
925                );
926            }
927        }
928    }
929}