//! Loading, scaling and hinting of glyph outlines.
//!
//! This module provides support for retrieving (optionally scaled and hinted)
//! glyph outlines in the form of vector paths.
//!
//! # Drawing a glyph
//!
//! Generating SVG [path commands](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#path_commands)
//! for a character (this assumes a local variable `font` of type [`FontRef`](crate::FontRef)):
//!
//! ```rust
//! use skrifa::{
//! instance::{LocationRef, Size},
//! outline::{DrawSettings, OutlinePen},
//! FontRef, MetadataProvider,
//! };
//!
//! # fn wrapper(font: FontRef) {
//! // First, grab the set of outline glyphs from the font.
//! let outlines = font.outline_glyphs();
//!
//! // Find the glyph identifier for our character.
//! let glyph_id = font.charmap().map('Q').unwrap();
//!
//! // Grab the outline glyph.
//! let glyph = outlines.get(glyph_id).unwrap();
//!
//! // Define how we want the glyph to be drawn. This creates
//! // settings for an instance without hinting at a size of
//! // 16px with no variations applied.
//! let settings = DrawSettings::unhinted(Size::new(16.0), LocationRef::default());
//!
//! // Alternatively, we can apply variations like so:
//! let var_location = font.axes().location(&[("wght", 650.0), ("wdth", 100.0)]);
//! let settings = DrawSettings::unhinted(Size::new(16.0), &var_location);
//!
//! // At this point, we need a "sink" to receive the resulting path. This
//! // is done by creating an implementation of the OutlinePen trait.
//!
//! // Let's make one that generates SVG path data.
//! #[derive(Default)]
//! struct SvgPath(String);
//!
//! // Implement the OutlinePen trait for this type. This emits the appropriate
//! // SVG path commands for each element type.
//! impl OutlinePen for SvgPath {
//! fn move_to(&mut self, x: f32, y: f32) {
//! self.0.push_str(&format!("M{x:.1},{y:.1} "));
//! }
//!
//! fn line_to(&mut self, x: f32, y: f32) {
//! self.0.push_str(&format!("L{x:.1},{y:.1} "));
//! }
//!
//! fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
//! self.0
//! .push_str(&format!("Q{cx0:.1},{cy0:.1} {x:.1},{y:.1} "));
//! }
//!
//! fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
//! self.0.push_str(&format!(
//! "C{cx0:.1},{cy0:.1} {cx1:.1},{cy1:.1} {x:.1},{y:.1} "
//! ));
//! }
//!
//! fn close(&mut self) {
//! self.0.push_str("Z ");
//! }
//! }
//! // Now, construct an instance of our pen.
//! let mut svg_path = SvgPath::default();
//!
//! // And draw the glyph!
//! glyph.draw(settings, &mut svg_path).unwrap();
//!
//! // See what we've drawn.
//! println!("{}", svg_path.0);
//! # }
//! ```
mod autohint;
mod cff;
mod glyf;
mod hint;
mod unscaled;
pub mod error;
use core::fmt::Debug;
use raw::tables::glyf::ToPathStyle;
use read_fonts::{types::GlyphId, TableProvider};
#[doc(inline)]
pub use error::DrawError;
pub use hint::{HintingInstance, HintingMode, LcdLayout};
pub use read_fonts::types::Pen as OutlinePen;
use self::glyf::{FreeTypeScaler, HarfBuzzScaler};
use super::{
instance::{LocationRef, NormalizedCoord, Size},
GLYF_COMPOSITE_RECURSION_LIMIT,
};
/// Source format for an outline glyph.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum OutlineGlyphFormat {
/// TrueType outlines sourced from the `glyf` table.
Glyf,
/// PostScript outlines sourced from the `CFF` table.
Cff,
/// PostScript outlines sourced from the `CFF2` table.
Cff2,
}
/// Specifies the hinting strategy for memory size calculations.
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
pub enum Hinting {
/// Hinting is disabled.
#[default]
None,
/// Application of hints that are embedded in the font.
///
/// For TrueType, these are bytecode instructions associated with each
/// glyph outline. For PostScript (CFF/CFF2), these are stem hints
/// encoded in the character string.
Embedded,
}
/// Information and adjusted metrics generated while drawing an outline glyph.
///
/// When applying hints to a TrueType glyph, the outline may be shifted in
/// the horizontal direction, affecting the left side bearing and advance width
/// of the glyph. This captures those metrics.
#[derive(Copy, Clone, Default, Debug)]
pub struct AdjustedMetrics {
/// True if the underlying glyph contains flags indicating the
/// presence of overlapping contours or components.
pub has_overlaps: bool,
/// If present, an adjusted left side bearing value generated by the
/// scaler.
///
/// This is equivalent to the `horiBearingX` value in
/// [`FT_Glyph_Metrics`](https://freetype.org/freetype2/docs/reference/ft2-glyph_retrieval.html#ft_glyph_metrics).
pub lsb: Option<f32>,
/// If present, an adjusted advance width value generated by the
/// scaler.
///
/// This is equivalent to the `advance.x` value in
/// [`FT_GlyphSlotRec`](https://freetype.org/freetype2/docs/reference/ft2-glyph_retrieval.html#ft_glyphslotrec).
pub advance_width: Option<f32>,
}
/// Options that define how a [glyph](OutlineGlyph) is drawn to a
/// [pen](OutlinePen).
pub struct DrawSettings<'a> {
instance: DrawInstance<'a>,
memory: Option<&'a mut [u8]>,
path_style: ToPathStyle,
}
impl<'a> DrawSettings<'a> {
/// Creates settings for an unhinted draw operation with the given size and
/// location in variation space.
pub fn unhinted(size: Size, location: impl Into<LocationRef<'a>>) -> Self {
Self {
instance: DrawInstance::Unhinted(size, location.into()),
memory: None,
path_style: ToPathStyle::default(),
}
}
/// Creates settings for a hinted draw operation using hinting data
/// contained in the font.
///
/// If `is_pedantic` is true then any error that occurs during hinting will
/// cause drawing to fail. This is equivalent to the `FT_LOAD_PEDANTIC` flag
/// in FreeType.
///
/// The font size, location in variation space and hinting mode are
/// defined by the current configuration of the given hinting instance.
pub fn hinted(instance: &'a HintingInstance, is_pedantic: bool) -> Self {
Self {
instance: DrawInstance::Hinted {
instance,
is_pedantic,
},
memory: None,
path_style: ToPathStyle::default(),
}
}
/// Builder method to associate a user memory buffer to be used for
/// temporary allocations during drawing.
///
/// The required size of this buffer can be computed using the
/// [`OutlineGlyph::draw_memory_size`] method.
///
/// If not provided, any necessary memory will be allocated internally.
pub fn with_memory(mut self, memory: Option<&'a mut [u8]>) -> Self {
self.memory = memory;
self
}
/// Builder method to control nuances of [`glyf`](https://learn.microsoft.com/en-us/typography/opentype/spec/glyf) pointstream interpretation.
///
/// Meant for use when trying to match legacy code behavior in Rust.
pub fn with_path_style(mut self, path_style: ToPathStyle) -> Self {
self.path_style = path_style;
self
}
}
enum DrawInstance<'a> {
Unhinted(Size, LocationRef<'a>),
Hinted {
instance: &'a HintingInstance,
is_pedantic: bool,
},
}
impl<'a, L> From<(Size, L)> for DrawSettings<'a>
where
L: Into<LocationRef<'a>>,
{
fn from(value: (Size, L)) -> Self {
DrawSettings::unhinted(value.0, value.1.into())
}
}
impl From<Size> for DrawSettings<'_> {
fn from(value: Size) -> Self {
DrawSettings::unhinted(value, LocationRef::default())
}
}
impl<'a> From<&'a HintingInstance> for DrawSettings<'a> {
fn from(value: &'a HintingInstance) -> Self {
DrawSettings::hinted(value, false)
}
}
/// A scalable glyph outline.
///
/// This can be sourced from the [`glyf`](https://learn.microsoft.com/en-us/typography/opentype/spec/glyf),
/// [`CFF`](https://learn.microsoft.com/en-us/typography/opentype/spec/cff) or
/// [`CFF2`](https://learn.microsoft.com/en-us/typography/opentype/spec/cff2)
/// tables. Use the [`format`](OutlineGlyph::format) method to determine which
/// was chosen for this glyph.
#[derive(Clone)]
pub struct OutlineGlyph<'a> {
kind: OutlineKind<'a>,
}
impl<'a> OutlineGlyph<'a> {
/// Returns the underlying source format for this outline.
pub fn format(&self) -> OutlineGlyphFormat {
match &self.kind {
OutlineKind::Glyf(..) => OutlineGlyphFormat::Glyf,
OutlineKind::Cff(cff, ..) => {
if cff.is_cff2() {
OutlineGlyphFormat::Cff2
} else {
OutlineGlyphFormat::Cff
}
}
}
}
/// Returns a value indicating if the outline may contain overlapping
/// contours or components.
///
/// For CFF outlines, returns `None` since this information is unavailable.
pub fn has_overlaps(&self) -> Option<bool> {
match &self.kind {
OutlineKind::Glyf(_, outline) => Some(outline.has_overlaps),
_ => None,
}
}
/// Returns a value indicating whether the outline has hinting
/// instructions.
///
/// For CFF outlines, returns `None` since this is unknown prior
/// to loading the outline.
pub fn has_hinting(&self) -> Option<bool> {
match &self.kind {
OutlineKind::Glyf(_, outline) => Some(outline.has_hinting),
_ => None,
}
}
/// Returns the size (in bytes) of the temporary memory required to draw
/// this outline.
///
/// This is used to compute the size of the memory buffer required for the
/// [`DrawSettings::with_memory`] method.
///
/// The `hinting` parameter determines which hinting method, if any, will
/// be used for drawing which has an effect on memory requirements.
///
/// The appropriate hinting types are as follows:
///
/// | For draw settings | Use hinting |
/// |------------------------------------|-----------------------|
/// | [`DrawSettings::unhinted`] | [`Hinting::None`] |
/// | [`DrawSettings::hinted`] | [`Hinting::Embedded`] |
pub fn draw_memory_size(&self, hinting: Hinting) -> usize {
match &self.kind {
OutlineKind::Glyf(_, outline) => outline.required_buffer_size(hinting),
_ => 0,
}
}
/// Draws the outline glyph with the given settings and emits the resulting
/// path commands to the specified pen.
pub fn draw<'s>(
&self,
settings: impl Into<DrawSettings<'a>>,
pen: &mut impl OutlinePen,
) -> Result<AdjustedMetrics, DrawError> {
let settings: DrawSettings<'a> = settings.into();
match (settings.instance, settings.path_style) {
(DrawInstance::Unhinted(size, location), ToPathStyle::FreeType) => {
self.draw_unhinted(size, location, settings.memory, settings.path_style, pen)
}
(DrawInstance::Unhinted(size, location), ToPathStyle::HarfBuzz) => {
self.draw_unhinted(size, location, settings.memory, settings.path_style, pen)
}
(
DrawInstance::Hinted {
instance: hinting_instance,
is_pedantic,
},
ToPathStyle::FreeType,
) => {
if hinting_instance.is_enabled() {
hinting_instance.draw(
self,
settings.memory,
settings.path_style,
pen,
is_pedantic,
)
} else {
self.draw_unhinted(
hinting_instance.size(),
hinting_instance.location(),
settings.memory,
settings.path_style,
pen,
)
}
}
(DrawInstance::Hinted { .. }, ToPathStyle::HarfBuzz) => {
Err(DrawError::HarfBuzzHintingUnsupported)
}
}
}
fn draw_unhinted(
&self,
size: Size,
location: impl Into<LocationRef<'a>>,
user_memory: Option<&mut [u8]>,
path_style: ToPathStyle,
pen: &mut impl OutlinePen,
) -> Result<AdjustedMetrics, DrawError> {
let ppem = size.ppem();
let coords = location.into().coords();
match &self.kind {
OutlineKind::Glyf(glyf, outline) => {
with_glyf_memory(outline, Hinting::None, user_memory, |buf| {
let (lsb, advance_width) = match path_style {
ToPathStyle::FreeType => {
let scaled_outline =
FreeTypeScaler::unhinted(glyf.clone(), outline, buf, ppem, coords)?
.scale(&outline.glyph, outline.glyph_id)?;
scaled_outline.to_path(path_style, pen)?;
(
scaled_outline.adjusted_lsb().to_f32(),
scaled_outline.adjusted_advance_width().to_f32(),
)
}
ToPathStyle::HarfBuzz => {
let scaled_outline =
HarfBuzzScaler::unhinted(glyf.clone(), outline, buf, ppem, coords)?
.scale(&outline.glyph, outline.glyph_id)?;
scaled_outline.to_path(path_style, pen)?;
(
scaled_outline.adjusted_lsb(),
scaled_outline.adjusted_advance_width(),
)
}
};
Ok(AdjustedMetrics {
has_overlaps: outline.has_overlaps,
lsb: Some(lsb),
advance_width: Some(advance_width),
})
})
}
OutlineKind::Cff(cff, glyph_id, subfont_ix) => {
let subfont = cff.subfont(*subfont_ix, ppem, coords)?;
cff.draw(&subfont, *glyph_id, coords, false, pen)?;
Ok(AdjustedMetrics::default())
}
}
}
/// Internal drawing API for autohinting that offers unified compact
/// storage for unscaled outlines.
#[allow(dead_code)]
fn draw_unscaled(
&self,
location: impl Into<LocationRef<'a>>,
user_memory: Option<&mut [u8]>,
sink: &mut impl unscaled::UnscaledOutlineSink,
) -> Result<(), DrawError> {
let coords = location.into().coords();
let ppem = None;
match &self.kind {
OutlineKind::Glyf(glyf, outline) => {
with_glyf_memory(outline, Hinting::None, user_memory, |buf| {
let outline =
FreeTypeScaler::unhinted(glyf.clone(), outline, buf, ppem, coords)?
.scale(&outline.glyph, outline.glyph_id)?;
sink.try_reserve(outline.points.len())?;
let mut contour_start = 0;
for contour_end in outline.contours.iter().map(|contour| *contour as usize) {
if contour_end > contour_start {
if let Some(points) = outline.points.get(contour_start..=contour_end) {
let flags = &outline.flags[contour_start..=contour_end];
sink.extend(points.iter().zip(flags).enumerate().map(
|(ix, (point, flags))| {
unscaled::UnscaledPoint::from_glyf_point(
*point,
*flags,
ix == 0,
)
},
))?;
}
}
contour_start = contour_end + 1;
}
Ok(())
})
}
OutlineKind::Cff(cff, glyph_id, subfont_ix) => {
let subfont = cff.subfont(*subfont_ix, ppem, coords)?;
let mut adapter = unscaled::UnscaledPenAdapter::new(sink);
cff.draw(&subfont, *glyph_id, coords, false, &mut adapter)?;
adapter.finish()?;
Ok(())
}
}
}
fn units_per_em(&self) -> u16 {
match &self.kind {
OutlineKind::Cff(cff, ..) => cff.units_per_em(),
OutlineKind::Glyf(glyf, ..) => glyf.units_per_em(),
}
}
}
#[derive(Clone)]
enum OutlineKind<'a> {
Glyf(glyf::Outlines<'a>, glyf::Outline<'a>),
// Third field is subfont index
Cff(cff::Outlines<'a>, GlyphId, u32),
}
impl Debug for OutlineKind<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Glyf(_, outline) => f.debug_tuple("Glyf").field(&outline.glyph_id).finish(),
Self::Cff(_, gid, subfont_index) => f
.debug_tuple("Cff")
.field(gid)
.field(subfont_index)
.finish(),
}
}
}
/// Collection of scalable glyph outlines.
#[derive(Debug, Clone)]
pub struct OutlineGlyphCollection<'a> {
kind: OutlineCollectionKind<'a>,
}
impl<'a> OutlineGlyphCollection<'a> {
/// Creates a new outline collection for the given font.
pub fn new(font: &impl TableProvider<'a>) -> Self {
let kind = if let Some(glyf) = glyf::Outlines::new(font) {
OutlineCollectionKind::Glyf(glyf)
} else if let Ok(cff) = cff::Outlines::new(font) {
OutlineCollectionKind::Cff(cff)
} else {
OutlineCollectionKind::None
};
Self { kind }
}
/// Creates a new outline collection for the given font and outline
/// format.
///
/// Returns `None` if the font does not contain outlines in the requested
/// format.
pub fn with_format(font: &impl TableProvider<'a>, format: OutlineGlyphFormat) -> Option<Self> {
let kind = match format {
OutlineGlyphFormat::Glyf => OutlineCollectionKind::Glyf(glyf::Outlines::new(font)?),
OutlineGlyphFormat::Cff => {
let upem = font.head().ok()?.units_per_em();
OutlineCollectionKind::Cff(cff::Outlines::from_cff(font.cff().ok()?, 0, upem).ok()?)
}
OutlineGlyphFormat::Cff2 => {
let upem = font.head().ok()?.units_per_em();
OutlineCollectionKind::Cff(cff::Outlines::from_cff2(font.cff2().ok()?, upem).ok()?)
}
};
Some(Self { kind })
}
/// Returns the underlying format of the source outline tables.
pub fn format(&self) -> Option<OutlineGlyphFormat> {
match &self.kind {
OutlineCollectionKind::Glyf(..) => Some(OutlineGlyphFormat::Glyf),
OutlineCollectionKind::Cff(cff) => cff
.is_cff2()
.then_some(OutlineGlyphFormat::Cff2)
.or(Some(OutlineGlyphFormat::Cff)),
_ => None,
}
}
/// Returns the outline for the given glyph identifier.
pub fn get(&self, glyph_id: GlyphId) -> Option<OutlineGlyph<'a>> {
match &self.kind {
OutlineCollectionKind::None => None,
OutlineCollectionKind::Glyf(glyf) => Some(OutlineGlyph {
kind: OutlineKind::Glyf(glyf.clone(), glyf.outline(glyph_id).ok()?),
}),
OutlineCollectionKind::Cff(cff) => Some(OutlineGlyph {
kind: OutlineKind::Cff(cff.clone(), glyph_id, cff.subfont_index(glyph_id)),
}),
}
}
/// Returns an iterator over all of the outline glyphs in the collection.
pub fn iter(&self) -> impl Iterator<Item = (GlyphId, OutlineGlyph<'a>)> + 'a + Clone {
let len = match &self.kind {
OutlineCollectionKind::Glyf(glyf) => glyf.glyph_count(),
OutlineCollectionKind::Cff(cff) => cff.glyph_count(),
_ => 0,
} as u16;
let copy = self.clone();
(0..len).filter_map(move |gid| {
let gid = GlyphId::from(gid);
let glyph = copy.get(gid)?;
Some((gid, glyph))
})
}
}
#[derive(Clone)]
enum OutlineCollectionKind<'a> {
None,
Glyf(glyf::Outlines<'a>),
Cff(cff::Outlines<'a>),
}
impl Debug for OutlineCollectionKind<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::None => write!(f, "None"),
Self::Glyf(..) => f.debug_tuple("Glyf").finish(),
Self::Cff(..) => f.debug_tuple("Cff").finish(),
}
}
}
/// Arbitrarily chosen smallish size for stack allocation to avoid the heap
/// when possible while drawing glyf outlines.
///
/// Upcoming work on TrueType hinting will likely adjust this to use bucketed
/// sizes based on actual data captured from fonts.
const GLYF_DRAW_STACK_BUFFER_SIZE: usize = 4096;
/// Invokes the callback with a memory buffer suitable for drawing
/// the given TrueType outline.
pub(super) fn with_glyf_memory<R>(
outline: &glyf::Outline,
hinting: Hinting,
memory: Option<&mut [u8]>,
mut f: impl FnMut(&mut [u8]) -> R,
) -> R {
// Wrap in a function and prevent inlining to avoid stack allocation
// and zeroing if we don't take this code path.
#[inline(never)]
fn stack_mem<R>(mut f: impl FnMut(&mut [u8]) -> R) -> R {
f(&mut [0u8; GLYF_DRAW_STACK_BUFFER_SIZE])
}
match memory {
Some(buf) => f(buf),
None => {
let buf_size = outline.required_buffer_size(hinting);
if buf_size <= GLYF_DRAW_STACK_BUFFER_SIZE {
stack_mem(f)
} else {
f(&mut vec![0u8; buf_size])
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{instance::Location, MetadataProvider};
use kurbo::{Affine, BezPath, PathEl, Point};
use read_fonts::{scaler_test, types::GlyphId, FontRef, TableProvider};
use pretty_assertions::assert_eq;
const PERIOD: u32 = 0x2E_u32;
const COMMA: u32 = 0x2C_u32;
#[test]
fn outline_glyph_formats() {
let font_format_pairs = [
(font_test_data::VAZIRMATN_VAR, OutlineGlyphFormat::Glyf),
(
font_test_data::CANTARELL_VF_TRIMMED,
OutlineGlyphFormat::Cff2,
),
(
font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
OutlineGlyphFormat::Cff,
),
(font_test_data::COLRV0V1_VARIABLE, OutlineGlyphFormat::Glyf),
];
for (font_data, format) in font_format_pairs {
assert_eq!(
FontRef::new(font_data).unwrap().outline_glyphs().format(),
Some(format)
);
}
}
#[test]
fn vazirmatin_var() {
compare_glyphs(
font_test_data::VAZIRMATN_VAR,
font_test_data::VAZIRMATN_VAR_GLYPHS,
);
}
#[test]
fn cantarell_vf() {
compare_glyphs(
font_test_data::CANTARELL_VF_TRIMMED,
font_test_data::CANTARELL_VF_TRIMMED_GLYPHS,
);
}
#[test]
fn noto_serif_display() {
compare_glyphs(
font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS,
);
}
#[test]
fn overlap_flags() {
let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
let outlines = font.outline_glyphs();
let glyph_count = font.maxp().unwrap().num_glyphs();
// GID 2 is a composite glyph with the overlap bit on a component
// GID 3 is a simple glyph with the overlap bit on the first flag
let expected_gids_with_overlap = vec![2, 3];
assert_eq!(
expected_gids_with_overlap,
(0..glyph_count)
.filter(
|gid| outlines.get(GlyphId::from(*gid)).unwrap().has_overlaps() == Some(true)
)
.collect::<Vec<_>>()
);
}
fn compare_glyphs(font_data: &[u8], expected_outlines: &str) {
let font = FontRef::new(font_data).unwrap();
let expected_outlines = scaler_test::parse_glyph_outlines(expected_outlines);
let mut path = scaler_test::Path::default();
for expected_outline in &expected_outlines {
if expected_outline.size == 0.0 && !expected_outline.coords.is_empty() {
continue;
}
let size = if expected_outline.size != 0.0 {
Size::new(expected_outline.size)
} else {
Size::unscaled()
};
path.elements.clear();
font.outline_glyphs()
.get(expected_outline.glyph_id)
.unwrap()
.draw(
DrawSettings::unhinted(size, expected_outline.coords.as_slice()),
&mut path,
)
.unwrap();
assert_eq!(path.elements, expected_outline.path, "mismatch in glyph path for id {} (size: {}, coords: {:?}): path: {:?} expected_path: {:?}",
expected_outline.glyph_id,
expected_outline.size,
expected_outline.coords,
&path.elements,
&expected_outline.path
);
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
enum GlyphPoint {
On { x: f32, y: f32 },
Off { x: f32, y: f32 },
}
impl GlyphPoint {
fn implied_oncurve(&self, other: Self) -> Self {
let (x1, y1) = self.xy();
let (x2, y2) = other.xy();
Self::On {
x: (x1 + x2) / 2.0,
y: (y1 + y2) / 2.0,
}
}
fn xy(&self) -> (f32, f32) {
match self {
GlyphPoint::On { x, y } | GlyphPoint::Off { x, y } => (*x, *y),
}
}
}
#[derive(Debug)]
struct PointPen {
points: Vec<GlyphPoint>,
}
impl PointPen {
fn new() -> Self {
Self { points: Vec::new() }
}
fn into_points(self) -> Vec<GlyphPoint> {
self.points
}
}
impl OutlinePen for PointPen {
fn move_to(&mut self, x: f32, y: f32) {
self.points.push(GlyphPoint::On { x, y });
}
fn line_to(&mut self, x: f32, y: f32) {
self.points.push(GlyphPoint::On { x, y });
}
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
self.points.push(GlyphPoint::Off { x: cx0, y: cy0 });
self.points.push(GlyphPoint::On { x, y });
}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.points.push(GlyphPoint::Off { x: cx0, y: cy0 });
self.points.push(GlyphPoint::Off { x: cx1, y: cy1 });
self.points.push(GlyphPoint::On { x, y });
}
fn close(&mut self) {
// We can't drop a 0-length closing line for fear of breaking interpolation compatibility
// - some other instance might have it not 0-length
// However, if the last command isn't a line and ends at the subpath start we can drop the endpoint
// - if any instance had it other than at the start there would be a closing line
// - and it wouldn't be interpolation compatible
// See <https://github.com/googlefonts/fontations/pull/818/files#r1521188624>
let np = self.points.len();
// We need at least 3 points to satisfy subsequent conditions
if np > 2
&& self.points[0] == self.points[np - 1]
&& matches!(
(self.points[0], self.points[np - 2]),
(GlyphPoint::On { .. }, GlyphPoint::Off { .. })
)
{
self.points.pop();
}
}
}
const STARTING_OFF_CURVE_POINTS: [GlyphPoint; 4] = [
GlyphPoint::Off { x: 278.0, y: 710.0 },
GlyphPoint::On { x: 278.0, y: 470.0 },
GlyphPoint::On { x: 998.0, y: 470.0 },
GlyphPoint::On { x: 998.0, y: 710.0 },
];
const MOSTLY_OFF_CURVE_POINTS: [GlyphPoint; 5] = [
GlyphPoint::Off { x: 278.0, y: 710.0 },
GlyphPoint::Off { x: 278.0, y: 470.0 },
GlyphPoint::On { x: 998.0, y: 470.0 },
GlyphPoint::Off { x: 998.0, y: 710.0 },
GlyphPoint::Off { x: 750.0, y: 500.0 },
];
/// Captures the svg drawing command sequence, e.g. MLLZ.
///
/// Intended use is to confirm the command sequence pushed to the pen is interpolation compatible.
#[derive(Default, Debug)]
struct CommandPen {
commands: String,
}
impl OutlinePen for CommandPen {
fn move_to(&mut self, _x: f32, _y: f32) {
self.commands.push('M');
}
fn line_to(&mut self, _x: f32, _y: f32) {
self.commands.push('L');
}
fn quad_to(&mut self, _cx0: f32, _cy0: f32, _x: f32, _y: f32) {
self.commands.push('Q');
}
fn curve_to(&mut self, _cx0: f32, _cy0: f32, _cx1: f32, _cy1: f32, _x: f32, _y: f32) {
self.commands.push('C');
}
fn close(&mut self) {
self.commands.push('Z');
}
}
fn draw_to_pen(font: &[u8], codepoint: u32, settings: DrawSettings, pen: &mut impl OutlinePen) {
let font = FontRef::new(font).unwrap();
let gid = font
.cmap()
.unwrap()
.map_codepoint(codepoint)
.unwrap_or_else(|| panic!("No gid for 0x{codepoint:04x}"));
let outlines = font.outline_glyphs();
let outline = outlines.get(gid).unwrap_or_else(|| {
panic!(
"No outline for {gid:?} in collection of {:?}",
outlines.format()
)
});
outline.draw(settings, pen).unwrap();
}
fn draw_commands(font: &[u8], codepoint: u32, settings: DrawSettings) -> String {
let mut pen = CommandPen::default();
draw_to_pen(font, codepoint, settings, &mut pen);
pen.commands
}
fn drawn_points(font: &[u8], codepoint: u32, settings: DrawSettings) -> Vec<GlyphPoint> {
let mut pen = PointPen::new();
draw_to_pen(font, codepoint, settings, &mut pen);
pen.into_points()
}
fn insert_implicit_oncurve(pointstream: &[GlyphPoint]) -> Vec<GlyphPoint> {
let mut expanded_points = Vec::new();
for i in 0..pointstream.len() - 1 {
expanded_points.push(pointstream[i]);
if matches!(
(pointstream[i], pointstream[i + 1]),
(GlyphPoint::Off { .. }, GlyphPoint::Off { .. })
) {
expanded_points.push(pointstream[i].implied_oncurve(pointstream[i + 1]));
}
}
expanded_points.push(*pointstream.last().unwrap());
expanded_points
}
fn as_on_off_sequence(points: &[GlyphPoint]) -> Vec<&'static str> {
points
.iter()
.map(|p| match p {
GlyphPoint::On { .. } => "On",
GlyphPoint::Off { .. } => "Off",
})
.collect()
}
#[test]
fn always_get_closing_lines() {
// <https://github.com/googlefonts/fontations/pull/818/files#r1521188624>
let period = draw_commands(
font_test_data::INTERPOLATE_THIS,
PERIOD,
Size::unscaled().into(),
);
let comma = draw_commands(
font_test_data::INTERPOLATE_THIS,
COMMA,
Size::unscaled().into(),
);
assert_eq!(
period, comma,
"Incompatible\nperiod\n{period:#?}\ncomma\n{comma:#?}\n"
);
assert_eq!(
"MLLLZ", period,
"We should get an explicit L for close even when it's a nop"
);
}
#[test]
fn triangle_and_square_retain_compatibility() {
// <https://github.com/googlefonts/fontations/pull/818/files#r1521188624>
let period = drawn_points(
font_test_data::INTERPOLATE_THIS,
PERIOD,
Size::unscaled().into(),
);
let comma = drawn_points(
font_test_data::INTERPOLATE_THIS,
COMMA,
Size::unscaled().into(),
);
assert_ne!(period, comma);
assert_eq!(
as_on_off_sequence(&period),
as_on_off_sequence(&comma),
"Incompatible\nperiod\n{period:#?}\ncomma\n{comma:#?}\n"
);
assert_eq!(
4,
period.len(),
"we should have the same # of points we started with"
);
}
fn assert_walked_backwards_like_freetype(pointstream: &[GlyphPoint], font: &[u8]) {
assert!(
matches!(pointstream[0], GlyphPoint::Off { .. }),
"Bad testdata, should start off curve"
);
// The default: look for an oncurve at the back, as freetype would do
let mut expected_points = pointstream.to_vec();
let last = *expected_points.last().unwrap();
let first_move = if matches!(last, GlyphPoint::Off { .. }) {
expected_points[0].implied_oncurve(last)
} else {
expected_points.pop().unwrap()
};
expected_points.insert(0, first_move);
expected_points = insert_implicit_oncurve(&expected_points);
let actual = drawn_points(font, PERIOD, Size::unscaled().into());
assert_eq!(
expected_points, actual,
"expected\n{expected_points:#?}\nactual\n{actual:#?}"
);
}
fn assert_walked_forwards_like_harfbuzz(pointstream: &[GlyphPoint], font: &[u8]) {
assert!(
matches!(pointstream[0], GlyphPoint::Off { .. }),
"Bad testdata, should start off curve"
);
// look for an oncurve at the front, as harfbuzz would do
let mut expected_points = pointstream.to_vec();
let first = expected_points.remove(0);
expected_points.push(first);
if matches!(expected_points[0], GlyphPoint::Off { .. }) {
expected_points.insert(0, first.implied_oncurve(expected_points[0]))
};
expected_points = insert_implicit_oncurve(&expected_points);
let settings: DrawSettings = Size::unscaled().into();
let settings = settings.with_path_style(ToPathStyle::HarfBuzz);
let actual = drawn_points(font, PERIOD, settings);
assert_eq!(
expected_points, actual,
"expected\n{expected_points:#?}\nactual\n{actual:#?}"
);
}
#[test]
fn starting_off_curve_walk_backwards_like_freetype() {
assert_walked_backwards_like_freetype(
&STARTING_OFF_CURVE_POINTS,
font_test_data::STARTING_OFF_CURVE,
);
}
#[test]
fn mostly_off_curve_walk_backwards_like_freetype() {
assert_walked_backwards_like_freetype(
&MOSTLY_OFF_CURVE_POINTS,
font_test_data::MOSTLY_OFF_CURVE,
);
}
#[test]
fn starting_off_curve_walk_forwards_like_hbdraw() {
assert_walked_forwards_like_harfbuzz(
&STARTING_OFF_CURVE_POINTS,
font_test_data::STARTING_OFF_CURVE,
);
}
#[test]
fn mostly_off_curve_walk_forwards_like_hbdraw() {
assert_walked_forwards_like_harfbuzz(
&MOSTLY_OFF_CURVE_POINTS,
font_test_data::MOSTLY_OFF_CURVE,
);
}
// A location noted for making FreeType and HarfBuzz results differ
// See https://github.com/googlefonts/sleipnir/pull/15
fn icon_loc_off_default(font: &FontRef) -> Location {
font.axes().location(&[
("wght", 700.0),
("opsz", 48.0),
("GRAD", 200.0),
("FILL", 1.0),
])
}
fn pt(x: f32, y: f32) -> Point {
(x as f64, y as f64).into()
}
// String command rounded to two decimal places, suitable for assert comparison
fn svg_commands(elements: &[PathEl]) -> Vec<String> {
elements
.iter()
.map(|e| match e {
PathEl::MoveTo(p) => format!("M{:.2},{:.2}", p.x, p.y),
PathEl::LineTo(p) => format!("L{:.2},{:.2}", p.x, p.y),
PathEl::QuadTo(c0, p) => format!("Q{:.2},{:.2} {:.2},{:.2}", c0.x, c0.y, p.x, p.y),
PathEl::CurveTo(c0, c1, p) => format!(
"C{:.2},{:.2} {:.2},{:.2} {:.2},{:.2}",
c0.x, c0.y, c1.x, c1.y, p.x, p.y
),
PathEl::ClosePath => "Z".to_string(),
})
.collect()
}
// Declared here to avoid a write-fonts dependency that is awkward for google3 at time of writing
#[derive(Default)]
struct BezPen {
path: BezPath,
}
impl OutlinePen for BezPen {
fn move_to(&mut self, x: f32, y: f32) {
self.path.move_to(pt(x, y));
}
fn line_to(&mut self, x: f32, y: f32) {
self.path.line_to(pt(x, y));
}
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
self.path.quad_to(pt(cx0, cy0), pt(x, y));
}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.path.curve_to(pt(cx0, cy0), pt(cx1, cy1), pt(x, y));
}
fn close(&mut self) {
self.path.close_path();
}
}
// We take glyph id here to bypass the need to resolve codepoint:gid and apply substitutions
fn assert_glyph_path_start_with(
font: &FontRef,
gid: GlyphId,
loc: Location,
path_style: ToPathStyle,
expected_path_start: &[PathEl],
) {
let glyph = font
.outline_glyphs()
.get(gid)
.unwrap_or_else(|| panic!("No glyph for {gid}"));
let mut pen = BezPen::default();
glyph
.draw(
DrawSettings::unhinted(Size::unscaled(), &loc).with_path_style(path_style),
&mut pen,
)
.unwrap_or_else(|e| panic!("Unable to draw {gid}: {e}"));
let bez = Affine::FLIP_Y * pen.path; // like an icon svg
let actual_path_start = &bez.elements()[..expected_path_start.len()];
// round2 can still leave very small differences from the typed 2-decimal value
// and the diff isn't pleasant so just compare as svg string fragments
assert_eq!(
svg_commands(expected_path_start),
svg_commands(actual_path_start)
);
}
const MATERIAL_SYMBOL_GID_MAIL_AT_DEFAULT: GlyphId = GlyphId::new(1);
const MATERIAL_SYMBOL_GID_MAIL_OFF_DEFAULT: GlyphId = GlyphId::new(2);
#[test]
fn draw_icon_freetype_style_at_default() {
let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
assert_glyph_path_start_with(
&font,
MATERIAL_SYMBOL_GID_MAIL_AT_DEFAULT,
Location::default(),
ToPathStyle::FreeType,
&[
PathEl::MoveTo((160.0, -160.0).into()),
PathEl::QuadTo((127.0, -160.0).into(), (103.5, -183.5).into()),
PathEl::QuadTo((80.0, -207.0).into(), (80.0, -240.0).into()),
],
);
}
#[test]
fn draw_icon_harfbuzz_style_at_default() {
let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
assert_glyph_path_start_with(
&font,
MATERIAL_SYMBOL_GID_MAIL_AT_DEFAULT,
Location::default(),
ToPathStyle::HarfBuzz,
&[
PathEl::MoveTo((160.0, -160.0).into()),
PathEl::QuadTo((127.0, -160.0).into(), (103.5, -183.5).into()),
PathEl::QuadTo((80.0, -207.0).into(), (80.0, -240.0).into()),
],
);
}
#[test]
fn draw_icon_freetype_style_off_default() {
let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
assert_glyph_path_start_with(
&font,
MATERIAL_SYMBOL_GID_MAIL_OFF_DEFAULT,
icon_loc_off_default(&font),
ToPathStyle::FreeType,
&[
PathEl::MoveTo((150.0, -138.0).into()),
PathEl::QuadTo((113.0, -138.0).into(), (86.0, -165.5).into()),
PathEl::QuadTo((59.0, -193.0).into(), (59.0, -229.0).into()),
],
);
}
#[test]
fn draw_icon_harfbuzz_style_off_default() {
let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
assert_glyph_path_start_with(
&font,
MATERIAL_SYMBOL_GID_MAIL_OFF_DEFAULT,
icon_loc_off_default(&font),
ToPathStyle::HarfBuzz,
&[
PathEl::MoveTo((150.0, -138.0).into()),
PathEl::QuadTo((113.22, -138.0).into(), (86.11, -165.61).into()),
PathEl::QuadTo((59.0, -193.22).into(), (59.0, -229.0).into()),
],
);
}
const GLYF_COMPONENT_GID_NON_UNIFORM_SCALE: GlyphId = GlyphId::new(3);
const GLYF_COMPONENT_GID_SCALED_COMPONENT_OFFSET: GlyphId = GlyphId::new(7);
const GLYF_COMPONENT_GID_NO_SCALED_COMPONENT_OFFSET: GlyphId = GlyphId::new(8);
#[test]
fn draw_nonuniform_scale_component_freetype() {
let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
assert_glyph_path_start_with(
&font,
GLYF_COMPONENT_GID_NON_UNIFORM_SCALE,
Location::default(),
ToPathStyle::FreeType,
&[
PathEl::MoveTo((-138.0, -185.0).into()),
PathEl::LineTo((-32.0, -259.0).into()),
PathEl::LineTo((26.0, -175.0).into()),
PathEl::LineTo((-80.0, -101.0).into()),
PathEl::ClosePath,
],
);
}
#[test]
fn draw_nonuniform_scale_component_harfbuzz() {
let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
assert_glyph_path_start_with(
&font,
GLYF_COMPONENT_GID_NON_UNIFORM_SCALE,
Location::default(),
ToPathStyle::HarfBuzz,
&[
PathEl::MoveTo((-137.8, -184.86).into()),
PathEl::LineTo((-32.15, -258.52).into()),
PathEl::LineTo((25.9, -175.24).into()),
PathEl::LineTo((-79.75, -101.58).into()),
PathEl::ClosePath,
],
);
}
#[test]
fn draw_scaled_component_offset_freetype() {
let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
assert_glyph_path_start_with(
&font,
GLYF_COMPONENT_GID_SCALED_COMPONENT_OFFSET,
Location::default(),
ToPathStyle::FreeType,
&[
// Adds (x-transform magnitude * x-offset, y-transform magnitude * y-offset) to x/y offset
PathEl::MoveTo((715.0, -360.0).into()),
],
);
}
#[test]
fn draw_no_scaled_component_offset_freetype() {
let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
assert_glyph_path_start_with(
&font,
GLYF_COMPONENT_GID_NO_SCALED_COMPONENT_OFFSET,
Location::default(),
ToPathStyle::FreeType,
&[PathEl::MoveTo((705.0, -340.0).into())],
);
}
#[test]
fn draw_scaled_component_offset_harfbuzz() {
let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
assert_glyph_path_start_with(
&font,
GLYF_COMPONENT_GID_SCALED_COMPONENT_OFFSET,
Location::default(),
ToPathStyle::HarfBuzz,
&[
// Adds (x-transform magnitude * x-offset, y-transform magnitude * y-offset) to x/y offset
PathEl::MoveTo((714.97, -360.0).into()),
],
);
}
#[test]
fn draw_no_scaled_component_offset_harfbuzz() {
let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
assert_glyph_path_start_with(
&font,
GLYF_COMPONENT_GID_NO_SCALED_COMPONENT_OFFSET,
Location::default(),
ToPathStyle::HarfBuzz,
&[PathEl::MoveTo((704.97, -340.0).into())],
);
}
const CUBIC_GLYPH: GlyphId = GlyphId::new(2);
#[test]
fn draw_cubic() {
let font = FontRef::new(font_test_data::CUBIC_GLYF).unwrap();
assert_glyph_path_start_with(
&font,
CUBIC_GLYPH,
Location::default(),
ToPathStyle::FreeType,
&[
PathEl::MoveTo((278.0, -710.0).into()),
PathEl::LineTo((278.0, -470.0).into()),
PathEl::CurveTo(
(300.0, -500.0).into(),
(800.0, -500.0).into(),
(998.0, -470.0).into(),
),
PathEl::LineTo((998.0, -710.0).into()),
],
);
}
/// Case where a font subset caused hinting to fail because execution
/// budget was derived from glyph count.
/// <https://github.com/googlefonts/fontations/issues/936>
#[test]
fn tthint_with_subset() {
let font = FontRef::new(font_test_data::TTHINT_SUBSET).unwrap();
let glyphs = font.outline_glyphs();
let hinting = HintingInstance::new(
&glyphs,
Size::new(16.0),
LocationRef::default(),
HintingMode::default(),
)
.unwrap();
let glyph = glyphs.get(GlyphId::new(1)).unwrap();
// Shouldn't fail in pedantic mode
glyph
.draw(DrawSettings::hinted(&hinting, true), &mut BezPen::default())
.unwrap();
}
}