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}