//! Parsing for PostScript charstrings.
use super::{BlendState, Error, Index, Stack};
use crate::{
types::{Fixed, Pen, Point},
Cursor,
};
/// Maximum nesting depth for subroutine calls.
///
/// See "Appendix B Type 2 Charstring Implementation Limits" at
/// <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=33>
pub const NESTING_DEPTH_LIMIT: u32 = 10;
/// Trait for processing commands resulting from charstring evaluation.
///
/// During processing, the path construction operators (see "4.1 Path
/// Construction Operators" at <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=15>)
/// are simplified into the basic move, line, curve and close commands.
///
/// This also has optional callbacks for processing hint operators. See "4.3
/// Hint Operators" at <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=21>
/// for more detail.
#[allow(unused_variables)]
pub trait CommandSink {
// Path construction operators.
fn move_to(&mut self, x: Fixed, y: Fixed);
fn line_to(&mut self, x: Fixed, y: Fixed);
fn curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed);
fn close(&mut self);
// Hint operators.
/// Horizontal stem hint at `y` with height `dy`.
fn hstem(&mut self, y: Fixed, dy: Fixed) {}
/// Vertical stem hint at `x` with width `dx`.
fn vstem(&mut self, x: Fixed, dx: Fixed) {}
/// Bitmask defining the hints that should be made active for the
/// commands that follow.
fn hint_mask(&mut self, mask: &[u8]) {}
/// Bitmask defining the counter hints that should be made active for the
/// commands that follow.
fn counter_mask(&mut self, mask: &[u8]) {}
}
/// Command sink that sends the results of charstring evaluation to a [Pen].
pub struct PenSink<'a, P>(&'a mut P);
impl<'a, P> PenSink<'a, P> {
pub fn new(pen: &'a mut P) -> Self {
Self(pen)
}
}
impl<'a, P> CommandSink for PenSink<'a, P>
where
P: Pen,
{
fn move_to(&mut self, x: Fixed, y: Fixed) {
self.0.move_to(x.to_f32(), y.to_f32());
}
fn line_to(&mut self, x: Fixed, y: Fixed) {
self.0.line_to(x.to_f32(), y.to_f32());
}
fn curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed) {
self.0.curve_to(
cx0.to_f32(),
cy0.to_f32(),
cx1.to_f32(),
cy1.to_f32(),
x.to_f32(),
y.to_f32(),
);
}
fn close(&mut self) {
self.0.close();
}
}
/// Evaluates the given charstring and emits the resulting commands to the
/// specified sink.
///
/// If the Private DICT associated with this charstring contains local
/// subroutines, then the `subrs` index must be provided, otherwise
/// `Error::MissingSubroutines` will be returned if a callsubr operator
/// is present.
///
/// If evaluating a CFF2 charstring and the top-level table contains an
/// item variation store, then `blend_state` must be provided, otherwise
/// `Error::MissingBlendState` will be returned if a blend operator is
/// present.
pub fn evaluate(
charstring_data: &[u8],
global_subrs: Index,
subrs: Option<Index>,
blend_state: Option<BlendState>,
sink: &mut impl CommandSink,
) -> Result<(), Error> {
let mut evaluator = Evaluator::new(global_subrs, subrs, blend_state, sink);
evaluator.evaluate(charstring_data, 0)?;
Ok(())
}
/// Transient state for evaluating a charstring and handling recursive
/// subroutine calls.
struct Evaluator<'a, S> {
global_subrs: Index<'a>,
subrs: Option<Index<'a>>,
blend_state: Option<BlendState<'a>>,
sink: &'a mut S,
is_open: bool,
have_read_width: bool,
stem_count: usize,
x: Fixed,
y: Fixed,
stack: Stack,
stack_ix: usize,
}
impl<'a, S> Evaluator<'a, S>
where
S: CommandSink,
{
fn new(
global_subrs: Index<'a>,
subrs: Option<Index<'a>>,
blend_state: Option<BlendState<'a>>,
sink: &'a mut S,
) -> Self {
Self {
global_subrs,
subrs,
blend_state,
sink,
is_open: false,
have_read_width: false,
stem_count: 0,
stack: Stack::new(),
x: Fixed::ZERO,
y: Fixed::ZERO,
stack_ix: 0,
}
}
fn evaluate(&mut self, charstring_data: &[u8], nesting_depth: u32) -> Result<(), Error> {
if nesting_depth > NESTING_DEPTH_LIMIT {
return Err(Error::CharstringNestingDepthLimitExceeded);
}
let mut cursor = crate::FontData::new(charstring_data).cursor();
while cursor.remaining_bytes() != 0 {
let b0 = cursor.read::<u8>()?;
match b0 {
// See "3.2 Charstring Number Encoding" <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=12>
//
// Push an integer to the stack
28 | 32..=254 => {
self.stack.push(super::dict::parse_int(&mut cursor, b0)?)?;
}
// Push a fixed point value to the stack
255 => {
let num = Fixed::from_bits(cursor.read::<i32>()?);
self.stack.push(num)?;
}
_ => {
let operator = Operator::read(&mut cursor, b0)?;
if !self.evaluate_operator(operator, &mut cursor, nesting_depth)? {
break;
}
}
}
}
Ok(())
}
/// Evaluates a single charstring operator.
///
/// Returns `Ok(true)` if evaluation should continue.
fn evaluate_operator(
&mut self,
operator: Operator,
cursor: &mut Cursor,
nesting_depth: u32,
) -> Result<bool, Error> {
use Operator::*;
use PointMode::*;
match operator {
// The following "flex" operators are intended to emit
// either two curves or a straight line depending on
// a "flex depth" parameter and the distance from the
// joining point to the chord connecting the two
// end points. In practice, we just emit the two curves,
// following FreeType:
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L335>
//
// Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=18>
Flex => {
self.emit_curves([DxDy; 6])?;
self.reset_stack();
}
// Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=19>
HFlex => {
self.emit_curves([DxY, DxDy, DxY, DxY, DxInitialY, DxY])?;
self.reset_stack();
}
// Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=19>
HFlex1 => {
self.emit_curves([DxDy, DxDy, DxY, DxY, DxDy, DxInitialY])?;
self.reset_stack();
}
// Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=20>
Flex1 => {
self.emit_curves([DxDy, DxDy, DxDy, DxDy, DxDy, DLargerCoordDist])?;
self.reset_stack();
}
// Set the variation store index
// <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2charstr#syntax-for-font-variations-support-operators>
VariationStoreIndex => {
let blend_state = self.blend_state.as_mut().ok_or(Error::MissingBlendState)?;
let store_index = self.stack.pop_i32()? as u16;
blend_state.set_store_index(store_index)?;
}
// Apply blending to the current operand stack
// <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2charstr#syntax-for-font-variations-support-operators>
Blend => {
let blend_state = self.blend_state.as_ref().ok_or(Error::MissingBlendState)?;
self.stack.apply_blend(blend_state)?;
}
// Return from the current subroutine
// Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=29>
Return => {
return Ok(false);
}
// End the current charstring
// TODO: handle implied 'seac' operator
// Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=21>
// FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L2463>
EndChar => {
if !self.stack.is_empty() && !self.have_read_width {
self.have_read_width = true;
self.stack.clear();
}
if self.is_open {
self.is_open = false;
self.sink.close();
}
return Ok(false);
}
// Emits a sequence of stem hints
// Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=21>
// FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L777>
HStem | VStem | HStemHm | VStemHm => {
let mut i = 0;
let len = if self.stack.len_is_odd() && !self.have_read_width {
self.have_read_width = true;
i = 1;
self.stack.len() - 1
} else {
self.stack.len()
};
let is_horizontal = matches!(operator, HStem | HStemHm);
let mut u = Fixed::ZERO;
while i < self.stack.len() {
let args = self.stack.fixed_array::<2>(i)?;
u += args[0];
let w = args[1];
let v = u.wrapping_add(w);
if is_horizontal {
self.sink.hstem(u, v);
} else {
self.sink.vstem(u, v);
}
u = v;
i += 2;
}
self.stem_count += len / 2;
self.reset_stack();
}
// Applies a hint or counter mask.
// If there are arguments on the stack, this is also an
// implied series of VSTEMHM operators.
// Hint and counter masks are bitstrings that determine
// the currently active set of hints.
// Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=24>
// FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L2580>
HintMask | CntrMask => {
let mut i = 0;
let len = if self.stack.len_is_odd() && !self.have_read_width {
self.have_read_width = true;
i = 1;
self.stack.len() - 1
} else {
self.stack.len()
};
let mut u = Fixed::ZERO;
while i < self.stack.len() {
let args = self.stack.fixed_array::<2>(i)?;
u += args[0];
let w = args[1];
let v = u + w;
self.sink.vstem(u, v);
u = v;
i += 2;
}
self.stem_count += len / 2;
let count = (self.stem_count + 7) / 8;
let mask = cursor.read_array::<u8>(count)?;
if operator == HintMask {
self.sink.hint_mask(mask);
} else {
self.sink.counter_mask(mask);
}
self.reset_stack();
}
// Starts a new subpath
// Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=16>
// FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L2653>
RMoveTo => {
let mut i = 0;
if self.stack.len() == 3 && !self.have_read_width {
self.have_read_width = true;
i = 1;
}
if !self.is_open {
self.is_open = true;
} else {
self.sink.close();
}
let [dx, dy] = self.stack.fixed_array::<2>(i)?;
self.x += dx;
self.y += dy;
self.sink.move_to(self.x, self.y);
self.reset_stack();
}
// Starts a new subpath by moving the current point in the
// horizontal or vertical direction
// Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=16>
// FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L839>
HMoveTo | VMoveTo => {
let mut i = 0;
if self.stack.len() == 2 && !self.have_read_width {
self.have_read_width = true;
i = 1;
}
if !self.is_open {
self.is_open = true;
} else {
self.sink.close();
}
let delta = self.stack.get_fixed(i)?;
if operator == HMoveTo {
self.x += delta;
} else {
self.y += delta;
}
self.sink.move_to(self.x, self.y);
self.reset_stack();
}
// Emits a sequence of lines
// Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=16>
// FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L863>
RLineTo => {
let mut i = 0;
while i < self.stack.len() {
let [dx, dy] = self.stack.fixed_array::<2>(i)?;
self.x += dx;
self.y += dy;
self.sink.line_to(self.x, self.y);
i += 2;
}
self.reset_stack();
}
// Emits a sequence of alternating horizontal and vertical
// lines
// Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=16>
// FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L885>
HLineTo | VLineTo => {
let mut is_x = operator == HLineTo;
for i in 0..self.stack.len() {
let delta = self.stack.get_fixed(i)?;
if is_x {
self.x += delta;
} else {
self.y += delta;
}
is_x = !is_x;
self.sink.line_to(self.x, self.y);
}
self.reset_stack();
}
// Emits curves that start and end horizontal, unless
// the stack count is odd, in which case the first
// curve may start with a vertical tangent
// Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=17>
// FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L2789>
HhCurveTo => {
if self.stack.len_is_odd() {
self.y += self.stack.get_fixed(0)?;
self.stack_ix = 1;
}
while self.coords_remaining() > 0 {
self.emit_curves([DxY, DxDy, DxY])?;
}
self.reset_stack();
}
// Alternates between curves with horizontal and vertical
// tangents
// Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=17>
// FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L2834>
HvCurveTo | VhCurveTo => {
let count1 = self.stack.len();
let count = count1 & !2;
let mut is_horizontal = operator == HvCurveTo;
self.stack_ix = count1 - count;
while self.stack_ix < count {
let do_last_delta = count - self.stack_ix == 5;
if is_horizontal {
self.emit_curves([DxY, DxDy, MaybeDxDy(do_last_delta)])?;
} else {
self.emit_curves([XDy, DxDy, DxMaybeDy(do_last_delta)])?;
}
is_horizontal = !is_horizontal;
}
self.reset_stack();
}
// Emits a sequence of curves possibly followed by a line
// Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=17>
// FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L915>
RrCurveTo | RCurveLine => {
while self.coords_remaining() >= 6 {
self.emit_curves([DxDy; 3])?;
}
if operator == RCurveLine {
let [dx, dy] = self.stack.fixed_array::<2>(self.stack_ix)?;
self.x += dx;
self.y += dy;
self.sink.line_to(self.x, self.y);
}
self.reset_stack();
}
// Emits a sequence of lines followed by a curve
// Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=18>
// FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L2702>
RLineCurve => {
while self.coords_remaining() > 6 {
let [dx, dy] = self.stack.fixed_array::<2>(self.stack_ix)?;
self.x += dx;
self.y += dy;
self.sink.line_to(self.x, self.y);
self.stack_ix += 2;
}
self.emit_curves([DxDy; 3])?;
self.reset_stack();
}
// Emits curves that start and end vertical, unless
// the stack count is odd, in which case the first
// curve may start with a horizontal tangent
// Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=18>
// FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L2744>
VvCurveTo => {
if self.stack.len_is_odd() {
self.x += self.stack.get_fixed(0)?;
self.stack_ix = 1;
}
while self.coords_remaining() > 0 {
self.emit_curves([XDy, DxDy, XDy])?;
}
self.reset_stack();
}
// Call local or global subroutine
// Spec: <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=29>
// FT: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L972>
CallSubr | CallGsubr => {
let subrs_index = if operator == CallSubr {
self.subrs.as_ref().ok_or(Error::MissingSubroutines)?
} else {
&self.global_subrs
};
let biased_index = (self.stack.pop_i32()? + subrs_index.subr_bias()) as usize;
let subr_charstring_data = subrs_index.get(biased_index)?;
self.evaluate(subr_charstring_data, nesting_depth + 1)?;
}
}
Ok(true)
}
fn coords_remaining(&self) -> usize {
self.stack.len() - self.stack_ix
}
fn emit_curves<const N: usize>(&mut self, modes: [PointMode; N]) -> Result<(), Error> {
use PointMode::*;
let initial_x = self.x;
let initial_y = self.y;
let mut count = 0;
let mut points = [Point::default(); 2];
for mode in modes {
let stack_used = match mode {
DxDy => {
self.x += self.stack.get_fixed(self.stack_ix)?;
self.y += self.stack.get_fixed(self.stack_ix + 1)?;
2
}
XDy => {
self.y += self.stack.get_fixed(self.stack_ix)?;
1
}
DxY => {
self.x += self.stack.get_fixed(self.stack_ix)?;
1
}
DxInitialY => {
self.x += self.stack.get_fixed(self.stack_ix)?;
self.y = initial_y;
1
}
// Emits a delta for the coordinate with the larger distance
// from the original value. Sets the other coordinate to the
// original value.
DLargerCoordDist => {
let delta = self.stack.get_fixed(self.stack_ix)?;
if (self.x - initial_x).abs() > (self.y - initial_y).abs() {
self.x += delta;
self.y = initial_y;
} else {
self.y += delta;
self.x = initial_x;
}
1
}
// Apply delta to y if `do_dy` is true.
DxMaybeDy(do_dy) => {
self.x += self.stack.get_fixed(self.stack_ix)?;
if do_dy {
self.y += self.stack.get_fixed(self.stack_ix + 1)?;
2
} else {
1
}
}
// Apply delta to x if `do_dx` is true.
MaybeDxDy(do_dx) => {
self.y += self.stack.get_fixed(self.stack_ix)?;
if do_dx {
self.x += self.stack.get_fixed(self.stack_ix + 1)?;
2
} else {
1
}
}
};
self.stack_ix += stack_used;
if count == 2 {
self.sink.curve_to(
points[0].x,
points[0].y,
points[1].x,
points[1].y,
self.x,
self.y,
);
count = 0;
} else {
points[count] = Point::new(self.x, self.y);
count += 1;
}
}
Ok(())
}
fn reset_stack(&mut self) {
self.stack.clear();
self.stack_ix = 0;
}
}
/// Specifies how point coordinates for a curve are computed.
#[derive(Copy, Clone)]
enum PointMode {
DxDy,
XDy,
DxY,
DxInitialY,
DLargerCoordDist,
DxMaybeDy(bool),
MaybeDxDy(bool),
}
/// PostScript charstring operator.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2charstr#appendix-a-cff2-charstring-command-codes>
// TODO: This is currently missing legacy math and logical operators.
// fonttools doesn't even implement these: <https://github.com/fonttools/fonttools/blob/65598197c8afd415781f6667a7fb647c2c987fff/Lib/fontTools/misc/psCharStrings.py#L409>
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
enum Operator {
HStem,
VStem,
VMoveTo,
RLineTo,
HLineTo,
VLineTo,
RrCurveTo,
CallSubr,
Return,
EndChar,
VariationStoreIndex,
Blend,
HStemHm,
HintMask,
CntrMask,
RMoveTo,
HMoveTo,
VStemHm,
RCurveLine,
RLineCurve,
VvCurveTo,
HhCurveTo,
CallGsubr,
VhCurveTo,
HvCurveTo,
HFlex,
Flex,
HFlex1,
Flex1,
}
impl Operator {
fn read(cursor: &mut Cursor, b0: u8) -> Result<Self, Error> {
// Escape opcode for accessing two byte operators
const ESCAPE: u8 = 12;
let (opcode, operator) = if b0 == ESCAPE {
let b1 = cursor.read::<u8>()?;
(b1, Self::from_two_byte_opcode(b1))
} else {
(b0, Self::from_opcode(b0))
};
operator.ok_or(Error::InvalidCharstringOperator(opcode))
}
/// Creates an operator from the given opcode.
fn from_opcode(opcode: u8) -> Option<Self> {
use Operator::*;
Some(match opcode {
1 => HStem,
3 => VStem,
4 => VMoveTo,
5 => RLineTo,
6 => HLineTo,
7 => VLineTo,
8 => RrCurveTo,
10 => CallSubr,
11 => Return,
14 => EndChar,
15 => VariationStoreIndex,
16 => Blend,
18 => HStemHm,
19 => HintMask,
20 => CntrMask,
21 => RMoveTo,
22 => HMoveTo,
23 => VStemHm,
24 => RCurveLine,
25 => RLineCurve,
26 => VvCurveTo,
27 => HhCurveTo,
29 => CallGsubr,
30 => VhCurveTo,
31 => HvCurveTo,
_ => return None,
})
}
/// Creates an operator from the given extended opcode.
///
/// These are preceded by a byte containing the escape value of 12.
pub fn from_two_byte_opcode(opcode: u8) -> Option<Self> {
use Operator::*;
Some(match opcode {
34 => HFlex,
35 => Flex,
36 => HFlex1,
37 => Flex1,
_ => return None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{tables::variations::ItemVariationStore, types::F2Dot14, FontData, FontRead};
#[derive(Copy, Clone, PartialEq, Debug)]
#[allow(clippy::enum_variant_names)]
enum Command {
MoveTo(Fixed, Fixed),
LineTo(Fixed, Fixed),
CurveTo(Fixed, Fixed, Fixed, Fixed, Fixed, Fixed),
}
#[derive(PartialEq, Default, Debug)]
struct CaptureCommandSink(Vec<Command>);
impl CommandSink for CaptureCommandSink {
fn move_to(&mut self, x: Fixed, y: Fixed) {
self.0.push(Command::MoveTo(x, y))
}
fn line_to(&mut self, x: Fixed, y: Fixed) {
self.0.push(Command::LineTo(x, y))
}
fn curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed) {
self.0.push(Command::CurveTo(cx0, cy0, cx1, cy1, x, y))
}
fn close(&mut self) {
// For testing purposes, replace the close command
// with a line to the most recent move or (0, 0)
// if none exists
let mut last_move = [Fixed::ZERO; 2];
for command in self.0.iter().rev() {
if let Command::MoveTo(x, y) = command {
last_move = [*x, *y];
break;
}
}
self.0.push(Command::LineTo(last_move[0], last_move[1]));
}
}
#[test]
fn cff2_example_subr() {
use Command::*;
let charstring = &font_test_data::cff2::EXAMPLE[0xc8..=0xe1];
let empty_index_bytes = [0u8; 8];
let store =
ItemVariationStore::read(FontData::new(&font_test_data::cff2::EXAMPLE[18..])).unwrap();
let global_subrs = Index::new(&empty_index_bytes, true).unwrap();
let coords = &[F2Dot14::from_f32(0.0)];
let blend_state = BlendState::new(store, coords, 0).unwrap();
let mut commands = CaptureCommandSink::default();
evaluate(
charstring,
global_subrs,
None,
Some(blend_state),
&mut commands,
)
.unwrap();
// 50 50 100 1 blend 0 rmoveto
// 500 -100 -200 1 blend hlineto
// 500 vlineto
// -500 100 200 1 blend hlineto
//
// applying blends at default location results in:
// 50 0 rmoveto
// 500 hlineto
// 500 vlineto
// -500 hlineto
//
// applying relative operators:
// 50 0 moveto
// 550 0 lineto
// 550 500 lineto
// 50 500 lineto
let expected = &[
MoveTo(Fixed::from_f64(50.0), Fixed::ZERO),
LineTo(Fixed::from_f64(550.0), Fixed::ZERO),
LineTo(Fixed::from_f64(550.0), Fixed::from_f64(500.0)),
LineTo(Fixed::from_f64(50.0), Fixed::from_f64(500.0)),
];
assert_eq!(&commands.0, expected);
}
#[test]
fn all_path_ops() {
// This charstring was manually constructed in
// font-test-data/test_data/ttx/charstring_path_ops.ttx
//
// The encoded version was extracted from the font and inlined below
// for simplicity.
//
// The geometry is arbitrary but includes the full set of path
// construction operators:
// --------------------------------------------------------------------
// -137 -632 rmoveto
// 34 -5 20 -6 rlineto
// 1 2 3 hlineto
// -179 -10 3 vlineto
// -30 15 22 8 -50 26 -14 -42 -41 19 -15 25 rrcurveto
// -30 15 22 8 hhcurveto
// 8 -30 15 22 8 hhcurveto
// 24 20 15 41 42 -20 14 -24 -25 -19 -14 -42 -41 19 -15 25 hvcurveto
// 20 vmoveto
// -20 14 -24 -25 -19 -14 4 5 rcurveline
// -20 14 -24 -25 -19 -14 4 5 rlinecurve
// -55 -23 -22 -59 vhcurveto
// -30 15 22 8 vvcurveto
// 8 -30 15 22 8 vvcurveto
// 24 20 15 41 42 -20 14 -24 -25 -19 -14 -42 23 flex
// 24 20 15 41 42 -20 14 hflex
// 13 hmoveto
// 41 42 -20 14 -24 -25 -19 -14 -42 hflex1
// 15 41 42 -20 14 -24 -25 -19 -14 -42 8 flex1
// endchar
let charstring = &[
251, 29, 253, 12, 21, 173, 134, 159, 133, 5, 140, 141, 142, 6, 251, 71, 129, 142, 7,
109, 154, 161, 147, 89, 165, 125, 97, 98, 158, 124, 164, 8, 109, 154, 161, 147, 27,
147, 109, 154, 161, 147, 27, 163, 159, 154, 180, 181, 119, 153, 115, 114, 120, 125, 97,
98, 158, 124, 164, 31, 159, 4, 119, 153, 115, 114, 120, 125, 143, 144, 24, 119, 153,
115, 114, 120, 125, 143, 144, 25, 84, 116, 117, 80, 30, 109, 154, 161, 147, 26, 147,
109, 154, 161, 147, 26, 163, 159, 154, 180, 181, 119, 153, 115, 114, 120, 125, 97, 162,
12, 35, 163, 159, 154, 180, 181, 119, 153, 12, 34, 152, 22, 180, 181, 119, 153, 115,
114, 120, 125, 97, 12, 36, 154, 180, 181, 119, 153, 115, 114, 120, 125, 97, 147, 12,
37, 14,
];
let empty_index_bytes = [0u8; 8];
let global_subrs = Index::new(&empty_index_bytes, false).unwrap();
use Command::*;
let mut commands = CaptureCommandSink::default();
evaluate(charstring, global_subrs, None, None, &mut commands).unwrap();
// Expected results from extracted glyph data in
// font-test-data/test_data/extracted/charstring_path_ops-glyphs.txt
// --------------------------------------------------------------------
// m -137,-632
// l -103,-637
// l -83,-643
// l -82,-643
// l -82,-641
// l -79,-641
// l -79,-820
// l -89,-820
// l -89,-817
// c -119,-802 -97,-794 -147,-768
// c -161,-810 -202,-791 -217,-766
// c -247,-766 -232,-744 -224,-744
// c -254,-736 -239,-714 -231,-714
// c -207,-714 -187,-699 -187,-658
// c -187,-616 -207,-602 -231,-602
// c -256,-602 -275,-616 -275,-658
// c -275,-699 -256,-714 -231,-714
// l -137,-632
// m -231,-694
// c -251,-680 -275,-705 -294,-719
// l -290,-714
// l -310,-700
// c -334,-725 -353,-739 -349,-734
// c -349,-789 -372,-811 -431,-811
// c -431,-841 -416,-819 -416,-811
// c -408,-841 -393,-819 -393,-811
// c -369,-791 -354,-750 -312,-770
// c -298,-794 -323,-813 -337,-855
// c -313,-855 -293,-840 -252,-840
// c -210,-840 -230,-855 -216,-855
// l -231,-694
// m -203,-855
// c -162,-813 -182,-799 -206,-799
// c -231,-799 -250,-813 -292,-855
// c -277,-814 -235,-834 -221,-858
// c -246,-877 -260,-919 -292,-911
// l -203,-855
let expected = &[
MoveTo(Fixed::from_i32(-137), Fixed::from_i32(-632)),
LineTo(Fixed::from_i32(-103), Fixed::from_i32(-637)),
LineTo(Fixed::from_i32(-83), Fixed::from_i32(-643)),
LineTo(Fixed::from_i32(-82), Fixed::from_i32(-643)),
LineTo(Fixed::from_i32(-82), Fixed::from_i32(-641)),
LineTo(Fixed::from_i32(-79), Fixed::from_i32(-641)),
LineTo(Fixed::from_i32(-79), Fixed::from_i32(-820)),
LineTo(Fixed::from_i32(-89), Fixed::from_i32(-820)),
LineTo(Fixed::from_i32(-89), Fixed::from_i32(-817)),
CurveTo(
Fixed::from_i32(-119),
Fixed::from_i32(-802),
Fixed::from_i32(-97),
Fixed::from_i32(-794),
Fixed::from_i32(-147),
Fixed::from_i32(-768),
),
CurveTo(
Fixed::from_i32(-161),
Fixed::from_i32(-810),
Fixed::from_i32(-202),
Fixed::from_i32(-791),
Fixed::from_i32(-217),
Fixed::from_i32(-766),
),
CurveTo(
Fixed::from_i32(-247),
Fixed::from_i32(-766),
Fixed::from_i32(-232),
Fixed::from_i32(-744),
Fixed::from_i32(-224),
Fixed::from_i32(-744),
),
CurveTo(
Fixed::from_i32(-254),
Fixed::from_i32(-736),
Fixed::from_i32(-239),
Fixed::from_i32(-714),
Fixed::from_i32(-231),
Fixed::from_i32(-714),
),
CurveTo(
Fixed::from_i32(-207),
Fixed::from_i32(-714),
Fixed::from_i32(-187),
Fixed::from_i32(-699),
Fixed::from_i32(-187),
Fixed::from_i32(-658),
),
CurveTo(
Fixed::from_i32(-187),
Fixed::from_i32(-616),
Fixed::from_i32(-207),
Fixed::from_i32(-602),
Fixed::from_i32(-231),
Fixed::from_i32(-602),
),
CurveTo(
Fixed::from_i32(-256),
Fixed::from_i32(-602),
Fixed::from_i32(-275),
Fixed::from_i32(-616),
Fixed::from_i32(-275),
Fixed::from_i32(-658),
),
CurveTo(
Fixed::from_i32(-275),
Fixed::from_i32(-699),
Fixed::from_i32(-256),
Fixed::from_i32(-714),
Fixed::from_i32(-231),
Fixed::from_i32(-714),
),
LineTo(Fixed::from_i32(-137), Fixed::from_i32(-632)),
MoveTo(Fixed::from_i32(-231), Fixed::from_i32(-694)),
CurveTo(
Fixed::from_i32(-251),
Fixed::from_i32(-680),
Fixed::from_i32(-275),
Fixed::from_i32(-705),
Fixed::from_i32(-294),
Fixed::from_i32(-719),
),
LineTo(Fixed::from_i32(-290), Fixed::from_i32(-714)),
LineTo(Fixed::from_i32(-310), Fixed::from_i32(-700)),
CurveTo(
Fixed::from_i32(-334),
Fixed::from_i32(-725),
Fixed::from_i32(-353),
Fixed::from_i32(-739),
Fixed::from_i32(-349),
Fixed::from_i32(-734),
),
CurveTo(
Fixed::from_i32(-349),
Fixed::from_i32(-789),
Fixed::from_i32(-372),
Fixed::from_i32(-811),
Fixed::from_i32(-431),
Fixed::from_i32(-811),
),
CurveTo(
Fixed::from_i32(-431),
Fixed::from_i32(-841),
Fixed::from_i32(-416),
Fixed::from_i32(-819),
Fixed::from_i32(-416),
Fixed::from_i32(-811),
),
CurveTo(
Fixed::from_i32(-408),
Fixed::from_i32(-841),
Fixed::from_i32(-393),
Fixed::from_i32(-819),
Fixed::from_i32(-393),
Fixed::from_i32(-811),
),
CurveTo(
Fixed::from_i32(-369),
Fixed::from_i32(-791),
Fixed::from_i32(-354),
Fixed::from_i32(-750),
Fixed::from_i32(-312),
Fixed::from_i32(-770),
),
CurveTo(
Fixed::from_i32(-298),
Fixed::from_i32(-794),
Fixed::from_i32(-323),
Fixed::from_i32(-813),
Fixed::from_i32(-337),
Fixed::from_i32(-855),
),
CurveTo(
Fixed::from_i32(-313),
Fixed::from_i32(-855),
Fixed::from_i32(-293),
Fixed::from_i32(-840),
Fixed::from_i32(-252),
Fixed::from_i32(-840),
),
CurveTo(
Fixed::from_i32(-210),
Fixed::from_i32(-840),
Fixed::from_i32(-230),
Fixed::from_i32(-855),
Fixed::from_i32(-216),
Fixed::from_i32(-855),
),
LineTo(Fixed::from_i32(-231), Fixed::from_i32(-694)),
MoveTo(Fixed::from_i32(-203), Fixed::from_i32(-855)),
CurveTo(
Fixed::from_i32(-162),
Fixed::from_i32(-813),
Fixed::from_i32(-182),
Fixed::from_i32(-799),
Fixed::from_i32(-206),
Fixed::from_i32(-799),
),
CurveTo(
Fixed::from_i32(-231),
Fixed::from_i32(-799),
Fixed::from_i32(-250),
Fixed::from_i32(-813),
Fixed::from_i32(-292),
Fixed::from_i32(-855),
),
CurveTo(
Fixed::from_i32(-277),
Fixed::from_i32(-814),
Fixed::from_i32(-235),
Fixed::from_i32(-834),
Fixed::from_i32(-221),
Fixed::from_i32(-858),
),
CurveTo(
Fixed::from_i32(-246),
Fixed::from_i32(-877),
Fixed::from_i32(-260),
Fixed::from_i32(-919),
Fixed::from_i32(-292),
Fixed::from_i32(-911),
),
LineTo(Fixed::from_i32(-203), Fixed::from_i32(-855)),
];
assert_eq!(&commands.0, expected);
}
}