read_fonts/tables/postscript/charstring.rs
1//! Parsing for PostScript charstrings.
2
3use super::{BlendState, Error, Index, Stack};
4use crate::{
5 types::{Fixed, Point},
6 Cursor,
7};
8
9/// Maximum nesting depth for subroutine calls.
10///
11/// See "Appendix B Type 2 Charstring Implementation Limits" at
12/// <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=33>
13pub const NESTING_DEPTH_LIMIT: u32 = 10;
14
15/// Trait for processing commands resulting from charstring evaluation.
16///
17/// During processing, the path construction operators (see "4.1 Path
18/// Construction Operators" at <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=15>)
19/// are simplified into the basic move, line, curve and close commands.
20///
21/// This also has optional callbacks for processing hint operators. See "4.3
22/// Hint Operators" at <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=21>
23/// for more detail.
24#[allow(unused_variables)]
25pub trait CommandSink {
26 // Path construction operators.
27 fn move_to(&mut self, x: Fixed, y: Fixed);
28 fn line_to(&mut self, x: Fixed, y: Fixed);
29 fn curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed);
30 fn close(&mut self);
31 // Hint operators.
32 /// Horizontal stem hint at `y` with height `dy`.
33 fn hstem(&mut self, y: Fixed, dy: Fixed) {}
34 /// Vertical stem hint at `x` with width `dx`.
35 fn vstem(&mut self, x: Fixed, dx: Fixed) {}
36 /// Bitmask defining the hints that should be made active for the
37 /// commands that follow.
38 fn hint_mask(&mut self, mask: &[u8]) {}
39 /// Bitmask defining the counter hints that should be made active for the
40 /// commands that follow.
41 fn counter_mask(&mut self, mask: &[u8]) {}
42}
43
44/// Evaluates the given charstring and emits the resulting commands to the
45/// specified sink.
46///
47/// If the Private DICT associated with this charstring contains local
48/// subroutines, then the `subrs` index must be provided, otherwise
49/// `Error::MissingSubroutines` will be returned if a callsubr operator
50/// is present.
51///
52/// If evaluating a CFF2 charstring and the top-level table contains an
53/// item variation store, then `blend_state` must be provided, otherwise
54/// `Error::MissingBlendState` will be returned if a blend operator is
55/// present.
56pub fn evaluate(
57 charstring_data: &[u8],
58 global_subrs: Index,
59 subrs: Option<Index>,
60 blend_state: Option<BlendState>,
61 sink: &mut impl CommandSink,
62) -> Result<(), Error> {
63 let mut evaluator = Evaluator::new(global_subrs, subrs, blend_state, sink);
64 evaluator.evaluate(charstring_data, 0)?;
65 Ok(())
66}
67
68/// Transient state for evaluating a charstring and handling recursive
69/// subroutine calls.
70struct Evaluator<'a, S> {
71 global_subrs: Index<'a>,
72 subrs: Option<Index<'a>>,
73 blend_state: Option<BlendState<'a>>,
74 sink: &'a mut S,
75 is_open: bool,
76 have_read_width: bool,
77 stem_count: usize,
78 x: Fixed,
79 y: Fixed,
80 stack: Stack,
81 stack_ix: usize,
82}
83
84impl<'a, S> Evaluator<'a, S>
85where
86 S: CommandSink,
87{
88 fn new(
89 global_subrs: Index<'a>,
90 subrs: Option<Index<'a>>,
91 blend_state: Option<BlendState<'a>>,
92 sink: &'a mut S,
93 ) -> Self {
94 Self {
95 global_subrs,
96 subrs,
97 blend_state,
98 sink,
99 is_open: false,
100 have_read_width: false,
101 stem_count: 0,
102 stack: Stack::new(),
103 x: Fixed::ZERO,
104 y: Fixed::ZERO,
105 stack_ix: 0,
106 }
107 }
108
109 fn evaluate(&mut self, charstring_data: &[u8], nesting_depth: u32) -> Result<(), Error> {
110 if nesting_depth > NESTING_DEPTH_LIMIT {
111 return Err(Error::CharstringNestingDepthLimitExceeded);
112 }
113 let mut cursor = crate::FontData::new(charstring_data).cursor();
114 while cursor.remaining_bytes() != 0 {
115 let b0 = cursor.read::<u8>()?;
116 match b0 {
117 // See "3.2 Charstring Number Encoding" <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=12>
118 //
119 // Push an integer to the stack
120 28 | 32..=254 => {
121 self.stack.push(super::dict::parse_int(&mut cursor, b0)?)?;
122 }
123 // Push a fixed point value to the stack
124 255 => {
125 let num = Fixed::from_bits(cursor.read::<i32>()?);
126 self.stack.push(num)?;
127 }
128 _ => {
129 let operator = Operator::read(&mut cursor, b0)?;
130 if !self.evaluate_operator(operator, &mut cursor, nesting_depth)? {
131 break;
132 }
133 }
134 }
135 }
136 Ok(())
137 }
138
139 /// Evaluates a single charstring operator.
140 ///
141 /// Returns `Ok(true)` if evaluation should continue.
142 fn evaluate_operator(
143 &mut self,
144 operator: Operator,
145 cursor: &mut Cursor,
146 nesting_depth: u32,
147 ) -> Result<bool, Error> {
148 use Operator::*;
149 use PointMode::*;
150 match operator {
151 // The following "flex" operators are intended to emit
152 // either two curves or a straight line depending on
153 // a "flex depth" parameter and the distance from the
154 // joining point to the chord connecting the two
155 // end points. In practice, we just emit the two curves,
156 // following FreeType:
157 // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L335>
158 //
159 // Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=18>
160 Flex => {
161 self.emit_curves([DxDy; 6])?;
162 self.reset_stack();
163 }
164 // Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=19>
165 HFlex => {
166 self.emit_curves([DxY, DxDy, DxY, DxY, DxInitialY, DxY])?;
167 self.reset_stack();
168 }
169 // Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=19>
170 HFlex1 => {
171 self.emit_curves([DxDy, DxDy, DxY, DxY, DxDy, DxInitialY])?;
172 self.reset_stack();
173 }
174 // Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=20>
175 Flex1 => {
176 self.emit_curves([DxDy, DxDy, DxDy, DxDy, DxDy, DLargerCoordDist])?;
177 self.reset_stack();
178 }
179 // Set the variation store index
180 // <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2charstr#syntax-for-font-variations-support-operators>
181 VariationStoreIndex => {
182 let blend_state = self.blend_state.as_mut().ok_or(Error::MissingBlendState)?;
183 let store_index = self.stack.pop_i32()? as u16;
184 blend_state.set_store_index(store_index)?;
185 }
186 // Apply blending to the current operand stack
187 // <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2charstr#syntax-for-font-variations-support-operators>
188 Blend => {
189 let blend_state = self.blend_state.as_ref().ok_or(Error::MissingBlendState)?;
190 self.stack.apply_blend(blend_state)?;
191 }
192 // Return from the current subroutine
193 // Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=29>
194 Return => {
195 return Ok(false);
196 }
197 // End the current charstring
198 // TODO: handle implied 'seac' operator
199 // Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=21>
200 // FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L2463>
201 EndChar => {
202 if !self.stack.is_empty() && !self.have_read_width {
203 self.have_read_width = true;
204 self.stack.clear();
205 }
206 if self.is_open {
207 self.is_open = false;
208 self.sink.close();
209 }
210 return Ok(false);
211 }
212 // Emits a sequence of stem hints
213 // Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=21>
214 // FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L777>
215 HStem | VStem | HStemHm | VStemHm => {
216 let mut i = 0;
217 let len = if self.stack.len_is_odd() && !self.have_read_width {
218 self.have_read_width = true;
219 i = 1;
220 self.stack.len() - 1
221 } else {
222 self.stack.len()
223 };
224 let is_horizontal = matches!(operator, HStem | HStemHm);
225 let mut u = Fixed::ZERO;
226 while i < self.stack.len() {
227 let args = self.stack.fixed_array::<2>(i)?;
228 u += args[0];
229 let w = args[1];
230 let v = u.wrapping_add(w);
231 if is_horizontal {
232 self.sink.hstem(u, v);
233 } else {
234 self.sink.vstem(u, v);
235 }
236 u = v;
237 i += 2;
238 }
239 self.stem_count += len / 2;
240 self.reset_stack();
241 }
242 // Applies a hint or counter mask.
243 // If there are arguments on the stack, this is also an
244 // implied series of VSTEMHM operators.
245 // Hint and counter masks are bitstrings that determine
246 // the currently active set of hints.
247 // Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=24>
248 // FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L2580>
249 HintMask | CntrMask => {
250 let mut i = 0;
251 let len = if self.stack.len_is_odd() && !self.have_read_width {
252 self.have_read_width = true;
253 i = 1;
254 self.stack.len() - 1
255 } else {
256 self.stack.len()
257 };
258 let mut u = Fixed::ZERO;
259 while i < self.stack.len() {
260 let args = self.stack.fixed_array::<2>(i)?;
261 u += args[0];
262 let w = args[1];
263 let v = u + w;
264 self.sink.vstem(u, v);
265 u = v;
266 i += 2;
267 }
268 self.stem_count += len / 2;
269 let count = (self.stem_count + 7) / 8;
270 let mask = cursor.read_array::<u8>(count)?;
271 if operator == HintMask {
272 self.sink.hint_mask(mask);
273 } else {
274 self.sink.counter_mask(mask);
275 }
276 self.reset_stack();
277 }
278 // Starts a new subpath
279 // Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=16>
280 // FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L2653>
281 RMoveTo => {
282 let mut i = 0;
283 if self.stack.len() == 3 && !self.have_read_width {
284 self.have_read_width = true;
285 i = 1;
286 }
287 if !self.is_open {
288 self.is_open = true;
289 } else {
290 self.sink.close();
291 }
292 let [dx, dy] = self.stack.fixed_array::<2>(i)?;
293 self.x += dx;
294 self.y += dy;
295 self.sink.move_to(self.x, self.y);
296 self.reset_stack();
297 }
298 // Starts a new subpath by moving the current point in the
299 // horizontal or vertical direction
300 // Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=16>
301 // FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L839>
302 HMoveTo | VMoveTo => {
303 let mut i = 0;
304 if self.stack.len() == 2 && !self.have_read_width {
305 self.have_read_width = true;
306 i = 1;
307 }
308 if !self.is_open {
309 self.is_open = true;
310 } else {
311 self.sink.close();
312 }
313 let delta = self.stack.get_fixed(i)?;
314 if operator == HMoveTo {
315 self.x += delta;
316 } else {
317 self.y += delta;
318 }
319 self.sink.move_to(self.x, self.y);
320 self.reset_stack();
321 }
322 // Emits a sequence of lines
323 // Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=16>
324 // FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L863>
325 RLineTo => {
326 let mut i = 0;
327 while i < self.stack.len() {
328 let [dx, dy] = self.stack.fixed_array::<2>(i)?;
329 self.x += dx;
330 self.y += dy;
331 self.sink.line_to(self.x, self.y);
332 i += 2;
333 }
334 self.reset_stack();
335 }
336 // Emits a sequence of alternating horizontal and vertical
337 // lines
338 // Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=16>
339 // FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L885>
340 HLineTo | VLineTo => {
341 let mut is_x = operator == HLineTo;
342 for i in 0..self.stack.len() {
343 let delta = self.stack.get_fixed(i)?;
344 if is_x {
345 self.x += delta;
346 } else {
347 self.y += delta;
348 }
349 is_x = !is_x;
350 self.sink.line_to(self.x, self.y);
351 }
352 self.reset_stack();
353 }
354 // Emits curves that start and end horizontal, unless
355 // the stack count is odd, in which case the first
356 // curve may start with a vertical tangent
357 // Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=17>
358 // FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L2789>
359 HhCurveTo => {
360 if self.stack.len_is_odd() {
361 self.y += self.stack.get_fixed(0)?;
362 self.stack_ix = 1;
363 }
364 // We need at least 4 coordinates to emit these curves
365 while self.coords_remaining() >= 4 {
366 self.emit_curves([DxY, DxDy, DxY])?;
367 }
368 self.reset_stack();
369 }
370 // Alternates between curves with horizontal and vertical
371 // tangents
372 // Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=17>
373 // FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L2834>
374 HvCurveTo | VhCurveTo => {
375 let count1 = self.stack.len();
376 let count = count1 & !2;
377 let mut is_horizontal = operator == HvCurveTo;
378 self.stack_ix = count1 - count;
379 while self.stack_ix < count {
380 let do_last_delta = count - self.stack_ix == 5;
381 if is_horizontal {
382 self.emit_curves([DxY, DxDy, MaybeDxDy(do_last_delta)])?;
383 } else {
384 self.emit_curves([XDy, DxDy, DxMaybeDy(do_last_delta)])?;
385 }
386 is_horizontal = !is_horizontal;
387 }
388 self.reset_stack();
389 }
390 // Emits a sequence of curves possibly followed by a line
391 // Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=17>
392 // FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L915>
393 RrCurveTo | RCurveLine => {
394 while self.coords_remaining() >= 6 {
395 self.emit_curves([DxDy; 3])?;
396 }
397 if operator == RCurveLine {
398 let [dx, dy] = self.stack.fixed_array::<2>(self.stack_ix)?;
399 self.x += dx;
400 self.y += dy;
401 self.sink.line_to(self.x, self.y);
402 }
403 self.reset_stack();
404 }
405 // Emits a sequence of lines followed by a curve
406 // Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=18>
407 // FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L2702>
408 RLineCurve => {
409 while self.coords_remaining() > 6 {
410 let [dx, dy] = self.stack.fixed_array::<2>(self.stack_ix)?;
411 self.x += dx;
412 self.y += dy;
413 self.sink.line_to(self.x, self.y);
414 self.stack_ix += 2;
415 }
416 self.emit_curves([DxDy; 3])?;
417 self.reset_stack();
418 }
419 // Emits curves that start and end vertical, unless
420 // the stack count is odd, in which case the first
421 // curve may start with a horizontal tangent
422 // Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=18>
423 // FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L2744>
424 VvCurveTo => {
425 if self.stack.len_is_odd() {
426 self.x += self.stack.get_fixed(0)?;
427 self.stack_ix = 1;
428 }
429 while self.coords_remaining() > 0 {
430 self.emit_curves([XDy, DxDy, XDy])?;
431 }
432 self.reset_stack();
433 }
434 // Call local or global subroutine
435 // Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=29>
436 // FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L972>
437 CallSubr | CallGsubr => {
438 let subrs_index = if operator == CallSubr {
439 self.subrs.as_ref().ok_or(Error::MissingSubroutines)?
440 } else {
441 &self.global_subrs
442 };
443 let biased_index = (self.stack.pop_i32()? + subrs_index.subr_bias()) as usize;
444 let subr_charstring_data = subrs_index.get(biased_index)?;
445 self.evaluate(subr_charstring_data, nesting_depth + 1)?;
446 }
447 }
448 Ok(true)
449 }
450
451 fn coords_remaining(&self) -> usize {
452 // This is overly defensive to avoid overflow but in the case of
453 // broken fonts, just return 0 when stack_ix > stack_len to prevent
454 // potential runaway while loops in the evaluator if this wraps
455 self.stack.len().saturating_sub(self.stack_ix)
456 }
457
458 fn emit_curves<const N: usize>(&mut self, modes: [PointMode; N]) -> Result<(), Error> {
459 use PointMode::*;
460 let initial_x = self.x;
461 let initial_y = self.y;
462 let mut count = 0;
463 let mut points = [Point::default(); 2];
464 for mode in modes {
465 let stack_used = match mode {
466 DxDy => {
467 self.x += self.stack.get_fixed(self.stack_ix)?;
468 self.y += self.stack.get_fixed(self.stack_ix + 1)?;
469 2
470 }
471 XDy => {
472 self.y += self.stack.get_fixed(self.stack_ix)?;
473 1
474 }
475 DxY => {
476 self.x += self.stack.get_fixed(self.stack_ix)?;
477 1
478 }
479 DxInitialY => {
480 self.x += self.stack.get_fixed(self.stack_ix)?;
481 self.y = initial_y;
482 1
483 }
484 // Emits a delta for the coordinate with the larger distance
485 // from the original value. Sets the other coordinate to the
486 // original value.
487 DLargerCoordDist => {
488 let delta = self.stack.get_fixed(self.stack_ix)?;
489 if (self.x - initial_x).abs() > (self.y - initial_y).abs() {
490 self.x += delta;
491 self.y = initial_y;
492 } else {
493 self.y += delta;
494 self.x = initial_x;
495 }
496 1
497 }
498 // Apply delta to y if `do_dy` is true.
499 DxMaybeDy(do_dy) => {
500 self.x += self.stack.get_fixed(self.stack_ix)?;
501 if do_dy {
502 self.y += self.stack.get_fixed(self.stack_ix + 1)?;
503 2
504 } else {
505 1
506 }
507 }
508 // Apply delta to x if `do_dx` is true.
509 MaybeDxDy(do_dx) => {
510 self.y += self.stack.get_fixed(self.stack_ix)?;
511 if do_dx {
512 self.x += self.stack.get_fixed(self.stack_ix + 1)?;
513 2
514 } else {
515 1
516 }
517 }
518 };
519 self.stack_ix += stack_used;
520 if count == 2 {
521 self.sink.curve_to(
522 points[0].x,
523 points[0].y,
524 points[1].x,
525 points[1].y,
526 self.x,
527 self.y,
528 );
529 count = 0;
530 } else {
531 points[count] = Point::new(self.x, self.y);
532 count += 1;
533 }
534 }
535 Ok(())
536 }
537
538 fn reset_stack(&mut self) {
539 self.stack.clear();
540 self.stack_ix = 0;
541 }
542}
543
544/// Specifies how point coordinates for a curve are computed.
545#[derive(Copy, Clone)]
546enum PointMode {
547 DxDy,
548 XDy,
549 DxY,
550 DxInitialY,
551 DLargerCoordDist,
552 DxMaybeDy(bool),
553 MaybeDxDy(bool),
554}
555
556/// PostScript charstring operator.
557///
558/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2charstr#appendix-a-cff2-charstring-command-codes>
559// TODO: This is currently missing legacy math and logical operators.
560// fonttools doesn't even implement these: <https://github.com/fonttools/fonttools/blob/65598197c8afd415781f6667a7fb647c2c987fff/Lib/fontTools/misc/psCharStrings.py#L409>
561#[derive(Copy, Clone, PartialEq, Eq, Debug)]
562enum Operator {
563 HStem,
564 VStem,
565 VMoveTo,
566 RLineTo,
567 HLineTo,
568 VLineTo,
569 RrCurveTo,
570 CallSubr,
571 Return,
572 EndChar,
573 VariationStoreIndex,
574 Blend,
575 HStemHm,
576 HintMask,
577 CntrMask,
578 RMoveTo,
579 HMoveTo,
580 VStemHm,
581 RCurveLine,
582 RLineCurve,
583 VvCurveTo,
584 HhCurveTo,
585 CallGsubr,
586 VhCurveTo,
587 HvCurveTo,
588 HFlex,
589 Flex,
590 HFlex1,
591 Flex1,
592}
593
594impl Operator {
595 fn read(cursor: &mut Cursor, b0: u8) -> Result<Self, Error> {
596 // Escape opcode for accessing two byte operators
597 const ESCAPE: u8 = 12;
598 let (opcode, operator) = if b0 == ESCAPE {
599 let b1 = cursor.read::<u8>()?;
600 (b1, Self::from_two_byte_opcode(b1))
601 } else {
602 (b0, Self::from_opcode(b0))
603 };
604 operator.ok_or(Error::InvalidCharstringOperator(opcode))
605 }
606
607 /// Creates an operator from the given opcode.
608 fn from_opcode(opcode: u8) -> Option<Self> {
609 use Operator::*;
610 Some(match opcode {
611 1 => HStem,
612 3 => VStem,
613 4 => VMoveTo,
614 5 => RLineTo,
615 6 => HLineTo,
616 7 => VLineTo,
617 8 => RrCurveTo,
618 10 => CallSubr,
619 11 => Return,
620 14 => EndChar,
621 15 => VariationStoreIndex,
622 16 => Blend,
623 18 => HStemHm,
624 19 => HintMask,
625 20 => CntrMask,
626 21 => RMoveTo,
627 22 => HMoveTo,
628 23 => VStemHm,
629 24 => RCurveLine,
630 25 => RLineCurve,
631 26 => VvCurveTo,
632 27 => HhCurveTo,
633 29 => CallGsubr,
634 30 => VhCurveTo,
635 31 => HvCurveTo,
636 _ => return None,
637 })
638 }
639
640 /// Creates an operator from the given extended opcode.
641 ///
642 /// These are preceded by a byte containing the escape value of 12.
643 pub fn from_two_byte_opcode(opcode: u8) -> Option<Self> {
644 use Operator::*;
645 Some(match opcode {
646 34 => HFlex,
647 35 => Flex,
648 36 => HFlex1,
649 37 => Flex1,
650 _ => return None,
651 })
652 }
653}
654
655#[cfg(test)]
656mod tests {
657 use super::*;
658 use crate::{tables::variations::ItemVariationStore, types::F2Dot14, FontData, FontRead};
659
660 #[derive(Copy, Clone, PartialEq, Debug)]
661 #[allow(clippy::enum_variant_names)]
662 enum Command {
663 MoveTo(Fixed, Fixed),
664 LineTo(Fixed, Fixed),
665 CurveTo(Fixed, Fixed, Fixed, Fixed, Fixed, Fixed),
666 }
667
668 #[derive(PartialEq, Default, Debug)]
669 struct CaptureCommandSink(Vec<Command>);
670
671 impl CommandSink for CaptureCommandSink {
672 fn move_to(&mut self, x: Fixed, y: Fixed) {
673 self.0.push(Command::MoveTo(x, y))
674 }
675
676 fn line_to(&mut self, x: Fixed, y: Fixed) {
677 self.0.push(Command::LineTo(x, y))
678 }
679
680 fn curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed) {
681 self.0.push(Command::CurveTo(cx0, cy0, cx1, cy1, x, y))
682 }
683
684 fn close(&mut self) {
685 // For testing purposes, replace the close command
686 // with a line to the most recent move or (0, 0)
687 // if none exists
688 let mut last_move = [Fixed::ZERO; 2];
689 for command in self.0.iter().rev() {
690 if let Command::MoveTo(x, y) = command {
691 last_move = [*x, *y];
692 break;
693 }
694 }
695 self.0.push(Command::LineTo(last_move[0], last_move[1]));
696 }
697 }
698
699 #[test]
700 fn cff2_example_subr() {
701 use Command::*;
702 let charstring = &font_test_data::cff2::EXAMPLE[0xc8..=0xe1];
703 let empty_index_bytes = [0u8; 8];
704 let store =
705 ItemVariationStore::read(FontData::new(&font_test_data::cff2::EXAMPLE[18..])).unwrap();
706 let global_subrs = Index::new(&empty_index_bytes, true).unwrap();
707 let coords = &[F2Dot14::from_f32(0.0)];
708 let blend_state = BlendState::new(store, coords, 0).unwrap();
709 let mut commands = CaptureCommandSink::default();
710 evaluate(
711 charstring,
712 global_subrs,
713 None,
714 Some(blend_state),
715 &mut commands,
716 )
717 .unwrap();
718 // 50 50 100 1 blend 0 rmoveto
719 // 500 -100 -200 1 blend hlineto
720 // 500 vlineto
721 // -500 100 200 1 blend hlineto
722 //
723 // applying blends at default location results in:
724 // 50 0 rmoveto
725 // 500 hlineto
726 // 500 vlineto
727 // -500 hlineto
728 //
729 // applying relative operators:
730 // 50 0 moveto
731 // 550 0 lineto
732 // 550 500 lineto
733 // 50 500 lineto
734 let expected = &[
735 MoveTo(Fixed::from_f64(50.0), Fixed::ZERO),
736 LineTo(Fixed::from_f64(550.0), Fixed::ZERO),
737 LineTo(Fixed::from_f64(550.0), Fixed::from_f64(500.0)),
738 LineTo(Fixed::from_f64(50.0), Fixed::from_f64(500.0)),
739 ];
740 assert_eq!(&commands.0, expected);
741 }
742
743 #[test]
744 fn all_path_ops() {
745 // This charstring was manually constructed in
746 // font-test-data/test_data/ttx/charstring_path_ops.ttx
747 //
748 // The encoded version was extracted from the font and inlined below
749 // for simplicity.
750 //
751 // The geometry is arbitrary but includes the full set of path
752 // construction operators:
753 // --------------------------------------------------------------------
754 // -137 -632 rmoveto
755 // 34 -5 20 -6 rlineto
756 // 1 2 3 hlineto
757 // -179 -10 3 vlineto
758 // -30 15 22 8 -50 26 -14 -42 -41 19 -15 25 rrcurveto
759 // -30 15 22 8 hhcurveto
760 // 8 -30 15 22 8 hhcurveto
761 // 24 20 15 41 42 -20 14 -24 -25 -19 -14 -42 -41 19 -15 25 hvcurveto
762 // 20 vmoveto
763 // -20 14 -24 -25 -19 -14 4 5 rcurveline
764 // -20 14 -24 -25 -19 -14 4 5 rlinecurve
765 // -55 -23 -22 -59 vhcurveto
766 // -30 15 22 8 vvcurveto
767 // 8 -30 15 22 8 vvcurveto
768 // 24 20 15 41 42 -20 14 -24 -25 -19 -14 -42 23 flex
769 // 24 20 15 41 42 -20 14 hflex
770 // 13 hmoveto
771 // 41 42 -20 14 -24 -25 -19 -14 -42 hflex1
772 // 15 41 42 -20 14 -24 -25 -19 -14 -42 8 flex1
773 // endchar
774 let charstring = &[
775 251, 29, 253, 12, 21, 173, 134, 159, 133, 5, 140, 141, 142, 6, 251, 71, 129, 142, 7,
776 109, 154, 161, 147, 89, 165, 125, 97, 98, 158, 124, 164, 8, 109, 154, 161, 147, 27,
777 147, 109, 154, 161, 147, 27, 163, 159, 154, 180, 181, 119, 153, 115, 114, 120, 125, 97,
778 98, 158, 124, 164, 31, 159, 4, 119, 153, 115, 114, 120, 125, 143, 144, 24, 119, 153,
779 115, 114, 120, 125, 143, 144, 25, 84, 116, 117, 80, 30, 109, 154, 161, 147, 26, 147,
780 109, 154, 161, 147, 26, 163, 159, 154, 180, 181, 119, 153, 115, 114, 120, 125, 97, 162,
781 12, 35, 163, 159, 154, 180, 181, 119, 153, 12, 34, 152, 22, 180, 181, 119, 153, 115,
782 114, 120, 125, 97, 12, 36, 154, 180, 181, 119, 153, 115, 114, 120, 125, 97, 147, 12,
783 37, 14,
784 ];
785 let empty_index_bytes = [0u8; 8];
786 let global_subrs = Index::new(&empty_index_bytes, false).unwrap();
787 use Command::*;
788 let mut commands = CaptureCommandSink::default();
789 evaluate(charstring, global_subrs, None, None, &mut commands).unwrap();
790 // Expected results from extracted glyph data in
791 // font-test-data/test_data/extracted/charstring_path_ops-glyphs.txt
792 // --------------------------------------------------------------------
793 // m -137,-632
794 // l -103,-637
795 // l -83,-643
796 // l -82,-643
797 // l -82,-641
798 // l -79,-641
799 // l -79,-820
800 // l -89,-820
801 // l -89,-817
802 // c -119,-802 -97,-794 -147,-768
803 // c -161,-810 -202,-791 -217,-766
804 // c -247,-766 -232,-744 -224,-744
805 // c -254,-736 -239,-714 -231,-714
806 // c -207,-714 -187,-699 -187,-658
807 // c -187,-616 -207,-602 -231,-602
808 // c -256,-602 -275,-616 -275,-658
809 // c -275,-699 -256,-714 -231,-714
810 // l -137,-632
811 // m -231,-694
812 // c -251,-680 -275,-705 -294,-719
813 // l -290,-714
814 // l -310,-700
815 // c -334,-725 -353,-739 -349,-734
816 // c -349,-789 -372,-811 -431,-811
817 // c -431,-841 -416,-819 -416,-811
818 // c -408,-841 -393,-819 -393,-811
819 // c -369,-791 -354,-750 -312,-770
820 // c -298,-794 -323,-813 -337,-855
821 // c -313,-855 -293,-840 -252,-840
822 // c -210,-840 -230,-855 -216,-855
823 // l -231,-694
824 // m -203,-855
825 // c -162,-813 -182,-799 -206,-799
826 // c -231,-799 -250,-813 -292,-855
827 // c -277,-814 -235,-834 -221,-858
828 // c -246,-877 -260,-919 -292,-911
829 // l -203,-855
830 let expected = &[
831 MoveTo(Fixed::from_i32(-137), Fixed::from_i32(-632)),
832 LineTo(Fixed::from_i32(-103), Fixed::from_i32(-637)),
833 LineTo(Fixed::from_i32(-83), Fixed::from_i32(-643)),
834 LineTo(Fixed::from_i32(-82), Fixed::from_i32(-643)),
835 LineTo(Fixed::from_i32(-82), Fixed::from_i32(-641)),
836 LineTo(Fixed::from_i32(-79), Fixed::from_i32(-641)),
837 LineTo(Fixed::from_i32(-79), Fixed::from_i32(-820)),
838 LineTo(Fixed::from_i32(-89), Fixed::from_i32(-820)),
839 LineTo(Fixed::from_i32(-89), Fixed::from_i32(-817)),
840 CurveTo(
841 Fixed::from_i32(-119),
842 Fixed::from_i32(-802),
843 Fixed::from_i32(-97),
844 Fixed::from_i32(-794),
845 Fixed::from_i32(-147),
846 Fixed::from_i32(-768),
847 ),
848 CurveTo(
849 Fixed::from_i32(-161),
850 Fixed::from_i32(-810),
851 Fixed::from_i32(-202),
852 Fixed::from_i32(-791),
853 Fixed::from_i32(-217),
854 Fixed::from_i32(-766),
855 ),
856 CurveTo(
857 Fixed::from_i32(-247),
858 Fixed::from_i32(-766),
859 Fixed::from_i32(-232),
860 Fixed::from_i32(-744),
861 Fixed::from_i32(-224),
862 Fixed::from_i32(-744),
863 ),
864 CurveTo(
865 Fixed::from_i32(-254),
866 Fixed::from_i32(-736),
867 Fixed::from_i32(-239),
868 Fixed::from_i32(-714),
869 Fixed::from_i32(-231),
870 Fixed::from_i32(-714),
871 ),
872 CurveTo(
873 Fixed::from_i32(-207),
874 Fixed::from_i32(-714),
875 Fixed::from_i32(-187),
876 Fixed::from_i32(-699),
877 Fixed::from_i32(-187),
878 Fixed::from_i32(-658),
879 ),
880 CurveTo(
881 Fixed::from_i32(-187),
882 Fixed::from_i32(-616),
883 Fixed::from_i32(-207),
884 Fixed::from_i32(-602),
885 Fixed::from_i32(-231),
886 Fixed::from_i32(-602),
887 ),
888 CurveTo(
889 Fixed::from_i32(-256),
890 Fixed::from_i32(-602),
891 Fixed::from_i32(-275),
892 Fixed::from_i32(-616),
893 Fixed::from_i32(-275),
894 Fixed::from_i32(-658),
895 ),
896 CurveTo(
897 Fixed::from_i32(-275),
898 Fixed::from_i32(-699),
899 Fixed::from_i32(-256),
900 Fixed::from_i32(-714),
901 Fixed::from_i32(-231),
902 Fixed::from_i32(-714),
903 ),
904 LineTo(Fixed::from_i32(-137), Fixed::from_i32(-632)),
905 MoveTo(Fixed::from_i32(-231), Fixed::from_i32(-694)),
906 CurveTo(
907 Fixed::from_i32(-251),
908 Fixed::from_i32(-680),
909 Fixed::from_i32(-275),
910 Fixed::from_i32(-705),
911 Fixed::from_i32(-294),
912 Fixed::from_i32(-719),
913 ),
914 LineTo(Fixed::from_i32(-290), Fixed::from_i32(-714)),
915 LineTo(Fixed::from_i32(-310), Fixed::from_i32(-700)),
916 CurveTo(
917 Fixed::from_i32(-334),
918 Fixed::from_i32(-725),
919 Fixed::from_i32(-353),
920 Fixed::from_i32(-739),
921 Fixed::from_i32(-349),
922 Fixed::from_i32(-734),
923 ),
924 CurveTo(
925 Fixed::from_i32(-349),
926 Fixed::from_i32(-789),
927 Fixed::from_i32(-372),
928 Fixed::from_i32(-811),
929 Fixed::from_i32(-431),
930 Fixed::from_i32(-811),
931 ),
932 CurveTo(
933 Fixed::from_i32(-431),
934 Fixed::from_i32(-841),
935 Fixed::from_i32(-416),
936 Fixed::from_i32(-819),
937 Fixed::from_i32(-416),
938 Fixed::from_i32(-811),
939 ),
940 CurveTo(
941 Fixed::from_i32(-408),
942 Fixed::from_i32(-841),
943 Fixed::from_i32(-393),
944 Fixed::from_i32(-819),
945 Fixed::from_i32(-393),
946 Fixed::from_i32(-811),
947 ),
948 CurveTo(
949 Fixed::from_i32(-369),
950 Fixed::from_i32(-791),
951 Fixed::from_i32(-354),
952 Fixed::from_i32(-750),
953 Fixed::from_i32(-312),
954 Fixed::from_i32(-770),
955 ),
956 CurveTo(
957 Fixed::from_i32(-298),
958 Fixed::from_i32(-794),
959 Fixed::from_i32(-323),
960 Fixed::from_i32(-813),
961 Fixed::from_i32(-337),
962 Fixed::from_i32(-855),
963 ),
964 CurveTo(
965 Fixed::from_i32(-313),
966 Fixed::from_i32(-855),
967 Fixed::from_i32(-293),
968 Fixed::from_i32(-840),
969 Fixed::from_i32(-252),
970 Fixed::from_i32(-840),
971 ),
972 CurveTo(
973 Fixed::from_i32(-210),
974 Fixed::from_i32(-840),
975 Fixed::from_i32(-230),
976 Fixed::from_i32(-855),
977 Fixed::from_i32(-216),
978 Fixed::from_i32(-855),
979 ),
980 LineTo(Fixed::from_i32(-231), Fixed::from_i32(-694)),
981 MoveTo(Fixed::from_i32(-203), Fixed::from_i32(-855)),
982 CurveTo(
983 Fixed::from_i32(-162),
984 Fixed::from_i32(-813),
985 Fixed::from_i32(-182),
986 Fixed::from_i32(-799),
987 Fixed::from_i32(-206),
988 Fixed::from_i32(-799),
989 ),
990 CurveTo(
991 Fixed::from_i32(-231),
992 Fixed::from_i32(-799),
993 Fixed::from_i32(-250),
994 Fixed::from_i32(-813),
995 Fixed::from_i32(-292),
996 Fixed::from_i32(-855),
997 ),
998 CurveTo(
999 Fixed::from_i32(-277),
1000 Fixed::from_i32(-814),
1001 Fixed::from_i32(-235),
1002 Fixed::from_i32(-834),
1003 Fixed::from_i32(-221),
1004 Fixed::from_i32(-858),
1005 ),
1006 CurveTo(
1007 Fixed::from_i32(-246),
1008 Fixed::from_i32(-877),
1009 Fixed::from_i32(-260),
1010 Fixed::from_i32(-919),
1011 Fixed::from_i32(-292),
1012 Fixed::from_i32(-911),
1013 ),
1014 LineTo(Fixed::from_i32(-203), Fixed::from_i32(-855)),
1015 ];
1016 assert_eq!(&commands.0, expected);
1017 }
1018
1019 /// Fuzzer caught subtract with overflow
1020 /// <https://g-issues.oss-fuzz.com/issues/383609770>
1021 #[test]
1022 fn coords_remaining_avoid_overflow() {
1023 // Test case:
1024 // Evaluate HHCURVETO operator with 2 elements on the stack
1025 let mut commands = CaptureCommandSink::default();
1026 let mut evaluator = Evaluator::new(Index::Empty, None, None, &mut commands);
1027 evaluator.stack.push(0).unwrap();
1028 evaluator.stack.push(0).unwrap();
1029 let mut cursor = FontData::new(&[]).cursor();
1030 // Just don't panic
1031 let _ = evaluator.evaluate_operator(Operator::HhCurveTo, &mut cursor, 0);
1032 }
1033}