mod autohint;
mod cff;
mod common;
mod glyf;
mod hint;
mod path;
mod unscaled;
#[cfg(test)]
mod testing;
pub mod error;
pub mod pen;
use common::OutlinesCommon;
pub use autohint::GlyphStyles;
pub use hint::{
Engine, HintingInstance, HintingMode, HintingOptions, LcdLayout, SmoothMode, Target,
};
use raw::FontRef;
#[doc(inline)]
pub use {error::DrawError, pen::OutlinePen};
use self::glyf::{FreeTypeScaler, HarfBuzzScaler};
use super::{
instance::{LocationRef, NormalizedCoord, Size},
GLYF_COMPOSITE_RECURSION_LIMIT,
};
use core::fmt::Debug;
use pen::PathStyle;
use read_fonts::{types::GlyphId, TableProvider};
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum OutlineGlyphFormat {
Glyf,
Cff,
Cff2,
}
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
pub enum Hinting {
#[default]
None,
Embedded,
}
#[derive(Copy, Clone, Default, Debug)]
pub struct AdjustedMetrics {
pub has_overlaps: bool,
pub lsb: Option<f32>,
pub advance_width: Option<f32>,
}
pub struct DrawSettings<'a> {
instance: DrawInstance<'a>,
memory: Option<&'a mut [u8]>,
path_style: PathStyle,
}
impl<'a> DrawSettings<'a> {
pub fn unhinted(size: Size, location: impl Into<LocationRef<'a>>) -> Self {
Self {
instance: DrawInstance::Unhinted(size, location.into()),
memory: None,
path_style: PathStyle::default(),
}
}
pub fn hinted(instance: &'a HintingInstance, is_pedantic: bool) -> Self {
Self {
instance: DrawInstance::Hinted {
instance,
is_pedantic,
},
memory: None,
path_style: PathStyle::default(),
}
}
pub fn with_memory(mut self, memory: Option<&'a mut [u8]>) -> Self {
self.memory = memory;
self
}
pub fn with_path_style(mut self, path_style: PathStyle) -> 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)
}
}
#[derive(Clone)]
pub struct OutlineGlyph<'a> {
kind: OutlineKind<'a>,
}
impl<'a> OutlineGlyph<'a> {
pub fn format(&self) -> OutlineGlyphFormat {
match &self.kind {
OutlineKind::Glyf(..) => OutlineGlyphFormat::Glyf,
OutlineKind::Cff(cff, ..) => {
if cff.is_cff2() {
OutlineGlyphFormat::Cff2
} else {
OutlineGlyphFormat::Cff
}
}
}
}
pub fn glyph_id(&self) -> GlyphId {
match &self.kind {
OutlineKind::Glyf(_, glyph) => glyph.glyph_id,
OutlineKind::Cff(_, gid, _) => *gid,
}
}
pub fn has_overlaps(&self) -> Option<bool> {
match &self.kind {
OutlineKind::Glyf(_, outline) => Some(outline.has_overlaps),
_ => None,
}
}
pub fn has_hinting(&self) -> Option<bool> {
match &self.kind {
OutlineKind::Glyf(_, outline) => Some(outline.has_hinting),
_ => None,
}
}
pub fn draw_memory_size(&self, hinting: Hinting) -> usize {
match &self.kind {
OutlineKind::Glyf(_, outline) => outline.required_buffer_size(hinting),
_ => 0,
}
}
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), PathStyle::FreeType) => {
self.draw_unhinted(size, location, settings.memory, settings.path_style, pen)
}
(DrawInstance::Unhinted(size, location), PathStyle::HarfBuzz) => {
self.draw_unhinted(size, location, settings.memory, settings.path_style, pen)
}
(
DrawInstance::Hinted {
instance: hinting_instance,
is_pedantic,
},
PathStyle::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 { .. }, PathStyle::HarfBuzz) => {
Err(DrawError::HarfBuzzHintingUnsupported)
}
}
}
fn draw_unhinted(
&self,
size: Size,
location: impl Into<LocationRef<'a>>,
user_memory: Option<&mut [u8]>,
path_style: PathStyle,
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 {
PathStyle::FreeType => {
let scaled_outline =
FreeTypeScaler::unhinted(glyf, 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(),
)
}
PathStyle::HarfBuzz => {
let scaled_outline =
HarfBuzzScaler::unhinted(glyf, 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())
}
}
}
#[allow(dead_code)]
fn draw_unscaled(
&self,
location: impl Into<LocationRef<'a>>,
user_memory: Option<&mut [u8]>,
sink: &mut impl unscaled::UnscaledOutlineSink,
) -> Result<i32, 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, 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(outline.adjusted_advance_width().to_bits() >> 6)
})
}
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()?;
let advance = cff.common.advance_width(*glyph_id, coords);
Ok(advance)
}
}
}
#[allow(dead_code)]
pub(crate) fn outlines_common(&self) -> &OutlinesCommon<'a> {
match &self.kind {
OutlineKind::Glyf(glyf, ..) => &glyf.common,
OutlineKind::Cff(cff, ..) => &cff.common,
}
}
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>),
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(),
}
}
}
#[derive(Debug, Clone)]
pub struct OutlineGlyphCollection<'a> {
kind: OutlineCollectionKind<'a>,
}
impl<'a> OutlineGlyphCollection<'a> {
pub fn new(font: &FontRef<'a>) -> Self {
let kind = if let Some(common) = OutlinesCommon::new(font) {
if let Some(glyf) = glyf::Outlines::new(&common) {
OutlineCollectionKind::Glyf(glyf)
} else if let Some(cff) = cff::Outlines::new(&common) {
OutlineCollectionKind::Cff(cff)
} else {
OutlineCollectionKind::None
}
} else {
OutlineCollectionKind::None
};
Self { kind }
}
pub fn with_format(font: &FontRef<'a>, format: OutlineGlyphFormat) -> Option<Self> {
let common = OutlinesCommon::new(font)?;
let kind = match format {
OutlineGlyphFormat::Glyf => OutlineCollectionKind::Glyf(glyf::Outlines::new(&common)?),
OutlineGlyphFormat::Cff => {
let upem = font.head().ok()?.units_per_em();
OutlineCollectionKind::Cff(cff::Outlines::from_cff(&common, upem)?)
}
OutlineGlyphFormat::Cff2 => {
let upem = font.head().ok()?.units_per_em();
OutlineCollectionKind::Cff(cff::Outlines::from_cff2(&common, upem)?)
}
};
Some(Self { kind })
}
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,
}
}
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)),
}),
}
}
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))
})
}
pub fn prefer_interpreter(&self) -> bool {
match &self.kind {
OutlineCollectionKind::Glyf(glyf) => glyf.prefer_interpreter(),
_ => true,
}
}
pub(crate) fn common(&self) -> Option<&OutlinesCommon<'a>> {
match &self.kind {
OutlineCollectionKind::Glyf(glyf) => Some(&glyf.common),
OutlineCollectionKind::Cff(cff) => Some(&cff.common),
_ => None,
}
}
}
#[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(),
}
}
}
pub(super) fn with_glyf_memory<R>(
outline: &glyf::Outline,
hinting: Hinting,
memory: Option<&mut [u8]>,
mut f: impl FnMut(&mut [u8]) -> R,
) -> R {
#[inline(never)]
fn stack_mem<const STACK_SIZE: usize, R>(mut f: impl FnMut(&mut [u8]) -> R) -> R {
f(&mut [0u8; STACK_SIZE])
}
match memory {
Some(buf) => f(buf),
None => {
let buf_size = outline.required_buffer_size(hinting);
if buf_size <= 512 {
stack_mem::<512, _>(f)
} else if buf_size <= 1024 {
stack_mem::<1024, _>(f)
} else if buf_size <= 2048 {
stack_mem::<2048, _>(f)
} else if buf_size <= 4096 {
stack_mem::<4096, _>(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::{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();
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 = testing::parse_glyph_outlines(expected_outlines);
let mut path = testing::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) {
let np = self.points.len();
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 },
];
#[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() {
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() {
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"
);
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"
);
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(PathStyle::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,
);
}
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()
}
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()
}
#[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();
}
}
fn assert_glyph_path_start_with(
font: &FontRef,
gid: GlyphId,
loc: Location,
path_style: PathStyle,
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; let actual_path_start = &bez.elements()[..expected_path_start.len()];
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(),
PathStyle::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(),
PathStyle::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),
PathStyle::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),
PathStyle::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(),
PathStyle::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(),
PathStyle::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(),
PathStyle::FreeType,
&[
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(),
PathStyle::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(),
PathStyle::HarfBuzz,
&[
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(),
PathStyle::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(),
PathStyle::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()),
],
);
}
#[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(),
HintingOptions::default(),
)
.unwrap();
let glyph = glyphs.get(GlyphId::new(1)).unwrap();
glyph
.draw(DrawSettings::hinted(&hinting, true), &mut BezPen::default())
.unwrap();
}
#[test]
fn empty_glyph_advance_unhinted() {
let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
let outlines = font.outline_glyphs();
let coords = [NormalizedCoord::from_f32(0.5)];
let gid = font.charmap().map(' ').unwrap();
let outline = outlines.get(gid).unwrap();
let advance = outline
.draw(
(Size::new(24.0), LocationRef::new(&coords)),
&mut super::pen::NullPen,
)
.unwrap()
.advance_width
.unwrap();
assert_eq!(advance, 10.796875);
}
#[test]
fn empty_glyph_advance_hinted() {
let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
let outlines = font.outline_glyphs();
let coords = [NormalizedCoord::from_f32(0.5)];
let hinter = HintingInstance::new(
&outlines,
Size::new(24.0),
LocationRef::new(&coords),
HintingOptions::default(),
)
.unwrap();
let gid = font.charmap().map(' ').unwrap();
let outline = outlines.get(gid).unwrap();
let advance = outline
.draw(&hinter, &mut super::pen::NullPen)
.unwrap()
.advance_width
.unwrap();
assert_eq!(advance, 11.0);
}
}