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}