blob: 742470d8b50f2d4bf7e1a85945cab98776cf9409 [file] [log] [blame]
//! Scaling support for TrueType outlines.
mod deltas;
mod hint;
mod memory;
mod outline;
#[cfg(feature = "libm")]
#[allow(unused_imports)]
use core_maths::CoreFloat;
pub use hint::{HintError, HintInstance, HintOutline};
pub use outline::{Outline, ScaledOutline};
use raw::{FontRef, ReadError};
use super::{DrawError, GlyphHMetrics, Hinting};
use crate::GLYF_COMPOSITE_RECURSION_LIMIT;
use memory::{FreeTypeOutlineMemory, HarfBuzzOutlineMemory};
use read_fonts::{
tables::{
glyf::{
Anchor, CompositeGlyph, CompositeGlyphFlags, Glyf, Glyph, PointMarker, SimpleGlyph,
},
gvar::Gvar,
hdmx::Hdmx,
loca::Loca,
},
types::{F26Dot6, F2Dot14, Fixed, GlyphId, Point, Tag},
TableProvider,
};
/// Number of phantom points generated at the end of an outline.
pub const PHANTOM_POINT_COUNT: usize = 4;
/// Scaler state for TrueType outlines.
#[derive(Clone)]
pub struct Outlines<'a> {
pub(crate) font: FontRef<'a>,
pub(crate) glyph_metrics: GlyphHMetrics<'a>,
loca: Loca<'a>,
glyf: Glyf<'a>,
gvar: Option<Gvar<'a>>,
hdmx: Option<Hdmx<'a>>,
fpgm: &'a [u8],
prep: &'a [u8],
cvt_len: u32,
max_function_defs: u16,
max_instruction_defs: u16,
max_twilight_points: u16,
max_stack_elements: u16,
max_storage: u16,
glyph_count: u16,
units_per_em: u16,
os2_vmetrics: [i16; 2],
prefer_interpreter: bool,
}
impl<'a> Outlines<'a> {
pub fn new(font: &FontRef<'a>) -> Option<Self> {
let loca = font.loca(None).ok()?;
let glyf = font.glyf().ok()?;
let glyph_metrics = GlyphHMetrics::new(font)?;
let (
glyph_count,
max_function_defs,
max_instruction_defs,
max_twilight_points,
max_stack_elements,
max_storage,
max_instructions,
) = font
.maxp()
.map(|maxp| {
(
maxp.num_glyphs(),
maxp.max_function_defs().unwrap_or_default(),
maxp.max_instruction_defs().unwrap_or_default(),
// Add 4 for phantom points
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttobjs.c#L1188>
maxp.max_twilight_points()
.unwrap_or_default()
.saturating_add(4),
// Add 32 to match FreeType's heuristic for buggy fonts
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttinterp.c#L356>
maxp.max_stack_elements()
.unwrap_or_default()
.saturating_add(32),
maxp.max_storage().unwrap_or_default(),
maxp.max_size_of_instructions().unwrap_or_default(),
)
})
.unwrap_or_default();
let os2_vmetrics = font
.os2()
.map(|os2| [os2.s_typo_ascender(), os2.s_typo_descender()])
.unwrap_or_default();
let fpgm = font
.data_for_tag(Tag::new(b"fpgm"))
.unwrap_or_default()
.as_bytes();
let prep = font
.data_for_tag(Tag::new(b"prep"))
.unwrap_or_default()
.as_bytes();
// Copy FreeType's logic on whether to use the interpreter:
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/base/ftobjs.c#L1001>
let prefer_interpreter = !(max_instructions == 0 && fpgm.is_empty() && prep.is_empty());
let cvt_len = font.cvt().map(|cvt| cvt.len() as u32).unwrap_or_default();
Some(Self {
font: font.clone(),
glyph_metrics,
loca,
glyf,
gvar: font.gvar().ok(),
hdmx: font.hdmx().ok(),
fpgm,
prep,
cvt_len,
max_function_defs,
max_instruction_defs,
max_twilight_points,
max_stack_elements,
max_storage,
glyph_count,
units_per_em: font.head().ok()?.units_per_em(),
os2_vmetrics,
prefer_interpreter,
})
}
pub fn units_per_em(&self) -> u16 {
self.units_per_em
}
pub fn glyph_count(&self) -> usize {
self.glyph_count as usize
}
pub fn prefer_interpreter(&self) -> bool {
self.prefer_interpreter
}
pub fn outline(&self, glyph_id: GlyphId) -> Result<Outline<'a>, DrawError> {
let mut outline = Outline {
glyph_id,
has_variations: self.gvar.is_some(),
..Default::default()
};
let glyph = self.loca.get_glyf(glyph_id, &self.glyf)?;
if let Some(glyph) = glyph.as_ref() {
self.outline_rec(glyph, &mut outline, 0, 0)?;
}
outline.points += PHANTOM_POINT_COUNT;
outline.max_stack = self.max_stack_elements as usize;
outline.cvt_count = self.cvt_len as usize;
outline.storage_count = self.max_storage as usize;
outline.max_twilight_points = self.max_twilight_points as usize;
outline.glyph = glyph;
Ok(outline)
}
pub fn compute_scale(&self, ppem: Option<f32>) -> (bool, F26Dot6) {
if let Some(ppem) = ppem {
if self.units_per_em > 0 {
return (
true,
F26Dot6::from_bits((ppem * 64.) as i32)
/ F26Dot6::from_bits(self.units_per_em as i32),
);
}
}
(false, F26Dot6::from_bits(0x10000))
}
}
impl Outlines<'_> {
fn outline_rec(
&self,
glyph: &Glyph,
outline: &mut Outline,
component_depth: usize,
recurse_depth: usize,
) -> Result<(), DrawError> {
if recurse_depth > GLYF_COMPOSITE_RECURSION_LIMIT {
return Err(DrawError::RecursionLimitExceeded(outline.glyph_id));
}
match glyph {
Glyph::Simple(simple) => {
let num_points = simple.num_points();
let num_points_with_phantom = num_points + PHANTOM_POINT_COUNT;
outline.max_simple_points = outline.max_simple_points.max(num_points_with_phantom);
outline.points += num_points;
outline.contours += simple.end_pts_of_contours().len();
outline.has_hinting = outline.has_hinting || simple.instruction_length() != 0;
outline.max_other_points = outline.max_other_points.max(num_points_with_phantom);
outline.has_overlaps |= simple.has_overlapping_contours();
}
Glyph::Composite(composite) => {
let (mut count, instructions) = composite.count_and_instructions();
count += PHANTOM_POINT_COUNT;
let point_base = outline.points;
for (component, flags) in composite.component_glyphs_and_flags() {
outline.has_overlaps |= flags.contains(CompositeGlyphFlags::OVERLAP_COMPOUND);
let component_glyph = self.loca.get_glyf(component.into(), &self.glyf)?;
let Some(component_glyph) = component_glyph else {
continue;
};
self.outline_rec(
&component_glyph,
outline,
component_depth + count,
recurse_depth + 1,
)?;
}
let has_hinting = !instructions.unwrap_or_default().is_empty();
if has_hinting {
// We only need the "other points" buffers if the
// composite glyph has instructions.
let num_points_in_composite = outline.points - point_base + PHANTOM_POINT_COUNT;
outline.max_other_points =
outline.max_other_points.max(num_points_in_composite);
}
outline.max_component_delta_stack = outline
.max_component_delta_stack
.max(component_depth + count);
outline.has_hinting = outline.has_hinting || has_hinting;
}
}
Ok(())
}
fn hdmx_width(&self, ppem: f32, glyph_id: GlyphId) -> Option<u8> {
let hdmx = self.hdmx.as_ref()?;
let ppem_u8 = ppem as u8;
// Make sure our ppem is integral and fits into u8
if ppem_u8 as f32 == ppem {
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttgload.c#L1996>
hdmx.record_for_size(ppem_u8)?
.widths
.get(glyph_id.to_u32() as usize)
.copied()
} else {
None
}
}
}
trait Scaler {
fn outlines(&self) -> &Outlines;
fn setup_phantom_points(
&mut self,
bounds: [i16; 4],
lsb: i32,
advance: i32,
tsb: i32,
vadvance: i32,
);
fn load_empty(&mut self, glyph_id: GlyphId) -> Result<(), DrawError>;
fn load_simple(&mut self, glyph: &SimpleGlyph, glyph_id: GlyphId) -> Result<(), DrawError>;
fn load_composite(
&mut self,
glyph: &CompositeGlyph,
glyph_id: GlyphId,
recurse_depth: usize,
) -> Result<(), DrawError>;
fn load(
&mut self,
glyph: &Option<Glyph>,
glyph_id: GlyphId,
recurse_depth: usize,
) -> Result<(), DrawError> {
if recurse_depth > GLYF_COMPOSITE_RECURSION_LIMIT {
return Err(DrawError::RecursionLimitExceeded(glyph_id));
}
let bounds = match &glyph {
Some(glyph) => [glyph.x_min(), glyph.x_max(), glyph.y_min(), glyph.y_max()],
_ => [0; 4],
};
let outlines = self.outlines();
let lsb = outlines.glyph_metrics.lsb(glyph_id, &[]);
let advance = outlines.glyph_metrics.advance_width(glyph_id, &[]);
let [ascent, descent] = outlines.os2_vmetrics.map(|x| x as i32);
let tsb = ascent - bounds[3] as i32;
let vadvance = ascent - descent;
self.setup_phantom_points(bounds, lsb, advance, tsb, vadvance);
match glyph {
Some(Glyph::Simple(simple)) => self.load_simple(simple, glyph_id),
Some(Glyph::Composite(composite)) => {
self.load_composite(composite, glyph_id, recurse_depth)
}
None => self.load_empty(glyph_id),
}
}
}
/// f32 all the things. Hold your rounding. No hinting.
pub(crate) struct HarfBuzzScaler<'a> {
outlines: &'a Outlines<'a>,
memory: HarfBuzzOutlineMemory<'a>,
coords: &'a [F2Dot14],
point_count: usize,
contour_count: usize,
component_delta_count: usize,
ppem: f32,
scale: F26Dot6,
is_scaled: bool,
/// Phantom points. These are 4 extra points appended to the end of an
/// outline that allow the bytecode interpreter to produce hinted
/// metrics.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructing_glyphs#phantom-points>
phantom: [Point<f32>; PHANTOM_POINT_COUNT],
}
impl<'a> HarfBuzzScaler<'a> {
pub(crate) fn unhinted(
outlines: &'a Outlines<'a>,
outline: &'a Outline,
buf: &'a mut [u8],
ppem: Option<f32>,
coords: &'a [F2Dot14],
) -> Result<Self, DrawError> {
outline.ensure_point_count_limit()?;
let (is_scaled, scale) = outlines.compute_scale(ppem);
let memory =
HarfBuzzOutlineMemory::new(outline, buf).ok_or(DrawError::InsufficientMemory)?;
Ok(Self {
outlines,
memory,
coords,
point_count: 0,
contour_count: 0,
component_delta_count: 0,
ppem: ppem.unwrap_or_default(),
scale,
is_scaled,
phantom: Default::default(),
})
}
pub(crate) fn scale(
mut self,
glyph: &Option<Glyph>,
glyph_id: GlyphId,
) -> Result<ScaledOutline<'a, f32>, DrawError> {
self.load(glyph, glyph_id, 0)?;
Ok(ScaledOutline::new(
&mut self.memory.points[..self.point_count],
self.phantom,
&mut self.memory.flags[..self.point_count],
&mut self.memory.contours[..self.contour_count],
self.outlines.hdmx_width(self.ppem, glyph_id),
))
}
}
/// F26Dot6 coords, Fixed deltas, and a penchant for rounding
pub(crate) struct FreeTypeScaler<'a> {
outlines: &'a Outlines<'a>,
memory: FreeTypeOutlineMemory<'a>,
coords: &'a [F2Dot14],
point_count: usize,
contour_count: usize,
component_delta_count: usize,
ppem: f32,
scale: F26Dot6,
is_scaled: bool,
is_hinted: bool,
pedantic_hinting: bool,
/// Phantom points. These are 4 extra points appended to the end of an
/// outline that allow the bytecode interpreter to produce hinted
/// metrics.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructing_glyphs#phantom-points>
phantom: [Point<F26Dot6>; PHANTOM_POINT_COUNT],
hinter: Option<&'a HintInstance>,
}
impl<'a> FreeTypeScaler<'a> {
pub(crate) fn unhinted(
outlines: &'a Outlines<'a>,
outline: &'a Outline,
buf: &'a mut [u8],
ppem: Option<f32>,
coords: &'a [F2Dot14],
) -> Result<Self, DrawError> {
outline.ensure_point_count_limit()?;
let (is_scaled, scale) = outlines.compute_scale(ppem);
let memory = FreeTypeOutlineMemory::new(outline, buf, Hinting::None)
.ok_or(DrawError::InsufficientMemory)?;
Ok(Self {
outlines,
memory,
coords,
point_count: 0,
contour_count: 0,
component_delta_count: 0,
ppem: ppem.unwrap_or_default(),
scale,
is_scaled,
is_hinted: false,
pedantic_hinting: false,
phantom: Default::default(),
hinter: None,
})
}
pub(crate) fn hinted(
outlines: &'a Outlines<'a>,
outline: &'a Outline,
buf: &'a mut [u8],
ppem: Option<f32>,
coords: &'a [F2Dot14],
hinter: &'a HintInstance,
pedantic_hinting: bool,
) -> Result<Self, DrawError> {
outline.ensure_point_count_limit()?;
let (is_scaled, scale) = outlines.compute_scale(ppem);
let memory = FreeTypeOutlineMemory::new(outline, buf, Hinting::Embedded)
.ok_or(DrawError::InsufficientMemory)?;
Ok(Self {
outlines,
memory,
coords,
point_count: 0,
contour_count: 0,
component_delta_count: 0,
ppem: ppem.unwrap_or_default(),
scale,
is_scaled,
// We don't hint unscaled outlines
is_hinted: is_scaled,
pedantic_hinting,
phantom: Default::default(),
hinter: Some(hinter),
})
}
pub(crate) fn scale(
mut self,
glyph: &Option<Glyph>,
glyph_id: GlyphId,
) -> Result<ScaledOutline<'a, F26Dot6>, DrawError> {
self.load(glyph, glyph_id, 0)?;
// Use hdmx if hinting is requested and backward compatibility mode
// is not enabled.
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttgload.c#L2559>
let hdmx_width = if self.is_hinted
&& self
.hinter
.as_ref()
.map(|hinter| !hinter.backward_compatibility())
.unwrap_or(true)
{
self.outlines.hdmx_width(self.ppem, glyph_id)
} else {
None
};
Ok(ScaledOutline::new(
&mut self.memory.scaled[..self.point_count],
self.phantom,
&mut self.memory.flags[..self.point_count],
&mut self.memory.contours[..self.contour_count],
hdmx_width,
))
}
}
impl Scaler for FreeTypeScaler<'_> {
fn setup_phantom_points(
&mut self,
bounds: [i16; 4],
lsb: i32,
advance: i32,
tsb: i32,
vadvance: i32,
) {
// The four "phantom" points as computed by FreeType.
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttgload.c#L1365>
// horizontal:
self.phantom[0].x = F26Dot6::from_bits(bounds[0] as i32 - lsb);
self.phantom[0].y = F26Dot6::ZERO;
self.phantom[1].x = self.phantom[0].x + F26Dot6::from_bits(advance);
self.phantom[1].y = F26Dot6::ZERO;
// vertical:
self.phantom[2].x = F26Dot6::ZERO;
self.phantom[2].y = F26Dot6::from_bits(bounds[3] as i32 + tsb);
self.phantom[3].x = F26Dot6::ZERO;
self.phantom[3].y = self.phantom[2].y - F26Dot6::from_bits(vadvance);
}
fn outlines(&self) -> &Outlines {
self.outlines
}
fn load_empty(&mut self, glyph_id: GlyphId) -> Result<(), DrawError> {
// Roughly corresponds to the FreeType code at
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttgload.c#L1572>
let scale = self.scale;
let mut unscaled = self.phantom.map(|point| point.map(|x| x.to_bits()));
if self.outlines.gvar.is_some() && !self.coords.is_empty() {
if let Ok(Some(deltas)) = self.outlines.gvar.as_ref().unwrap().phantom_point_deltas(
&self.outlines.glyf,
&self.outlines.loca,
self.coords,
glyph_id,
) {
unscaled[0] += deltas[0].map(Fixed::to_i32);
unscaled[1] += deltas[1].map(Fixed::to_i32);
}
}
if self.is_scaled {
for (phantom, unscaled) in self.phantom.iter_mut().zip(&unscaled) {
*phantom = unscaled.map(F26Dot6::from_bits) * scale;
}
} else {
for (phantom, unscaled) in self.phantom.iter_mut().zip(&unscaled) {
*phantom = unscaled.map(F26Dot6::from_i32);
}
}
Ok(())
}
fn load_simple(&mut self, glyph: &SimpleGlyph, glyph_id: GlyphId) -> Result<(), DrawError> {
use DrawError::InsufficientMemory;
// Compute the ranges for our point/flag buffers and slice them.
let points_start = self.point_count;
let point_count = glyph.num_points();
let phantom_start = point_count;
let points_end = points_start + point_count + PHANTOM_POINT_COUNT;
let point_range = points_start..points_end;
let other_points_end = point_count + PHANTOM_POINT_COUNT;
// Scaled points and flags are accumulated as we load the outline.
let scaled = self
.memory
.scaled
.get_mut(point_range.clone())
.ok_or(InsufficientMemory)?;
let flags = self
.memory
.flags
.get_mut(point_range)
.ok_or(InsufficientMemory)?;
// Unscaled points are temporary and are allocated as needed. We only
// ever need one copy in memory for any simple or composite glyph so
// allocate from the base of the buffer.
let unscaled = self
.memory
.unscaled
.get_mut(..other_points_end)
.ok_or(InsufficientMemory)?;
// Read our unscaled points and flags (up to point_count which does not
// include phantom points).
glyph.read_points_fast(&mut unscaled[..point_count], &mut flags[..point_count])?;
// Compute the range for our contour end point buffer and slice it.
let contours_start = self.contour_count;
let contour_end_pts = glyph.end_pts_of_contours();
let contour_count = contour_end_pts.len();
let contours_end = contours_start + contour_count;
let contours = self
.memory
.contours
.get_mut(contours_start..contours_end)
.ok_or(InsufficientMemory)?;
// Read the contour end points, ensuring that they are properly
// ordered.
let mut last_end_pt = 0;
for (end_pt, contour) in contour_end_pts.iter().zip(contours.iter_mut()) {
let end_pt = end_pt.get();
if end_pt < last_end_pt {
return Err(ReadError::MalformedData(
"unordered contour end points in TrueType glyph",
)
.into());
}
last_end_pt = end_pt;
*contour = end_pt;
}
// Adjust the running point/contour total counts
self.point_count += point_count;
self.contour_count += contour_count;
// Append phantom points to the outline.
for (i, phantom) in self.phantom.iter().enumerate() {
unscaled[phantom_start + i] = phantom.map(|x| x.to_bits());
flags[phantom_start + i] = Default::default();
}
let mut have_deltas = false;
if self.outlines.gvar.is_some() && !self.coords.is_empty() {
let gvar = self.outlines.gvar.as_ref().unwrap();
let glyph = deltas::SimpleGlyph {
points: &mut unscaled[..],
flags: &mut flags[..],
contours,
};
let deltas = self
.memory
.deltas
.get_mut(..point_count + PHANTOM_POINT_COUNT)
.ok_or(InsufficientMemory)?;
let iup_buffer = self
.memory
.iup_buffer
.get_mut(..point_count + PHANTOM_POINT_COUNT)
.ok_or(InsufficientMemory)?;
if deltas::simple_glyph(gvar, glyph_id, self.coords, glyph, iup_buffer, deltas).is_ok()
{
have_deltas = true;
}
}
let ins = glyph.instructions();
let is_hinted = self.is_hinted;
if self.is_scaled {
let scale = self.scale;
if have_deltas {
for ((point, unscaled), delta) in scaled
.iter_mut()
.zip(unscaled.iter())
.zip(self.memory.deltas.iter())
{
let delta = delta.map(Fixed::to_f26dot6);
let scaled = (unscaled.map(F26Dot6::from_i32) + delta) * scale;
// The computed scale factor has an i32 -> 26.26 conversion built in. This undoes the
// extra shift.
*point = scaled.map(|v| F26Dot6::from_bits(v.to_i32()));
}
// FreeType applies different rounding to HVAR deltas. Since
// we're only using gvar, mimic that behavior for phantom point
// deltas when an HVAR table is present
if self.outlines.glyph_metrics.hvar.is_some() {
for ((point, unscaled), delta) in scaled[phantom_start..]
.iter_mut()
.zip(&unscaled[phantom_start..])
.zip(&self.memory.deltas[phantom_start..])
{
let delta = delta.map(Fixed::to_i32).map(F26Dot6::from_i32);
let scaled = (unscaled.map(F26Dot6::from_i32) + delta) * scale;
*point = scaled.map(|v| F26Dot6::from_bits(v.to_i32()));
}
}
if is_hinted {
// For hinting, we need to adjust the unscaled points as well.
// Round off deltas for unscaled outlines.
for (unscaled, delta) in unscaled.iter_mut().zip(self.memory.deltas.iter()) {
*unscaled += delta.map(Fixed::to_i32);
}
}
} else {
for (point, unscaled) in scaled.iter_mut().zip(unscaled.iter_mut()) {
*point = unscaled.map(|v| F26Dot6::from_bits(v) * scale);
}
}
} else {
if have_deltas {
// Round off deltas for unscaled outlines.
for (unscaled, delta) in unscaled.iter_mut().zip(self.memory.deltas.iter()) {
*unscaled += delta.map(Fixed::to_i32);
}
}
// Unlike FreeType, we also store unscaled outlines in 26.6.
for (point, unscaled) in scaled.iter_mut().zip(unscaled.iter()) {
*point = unscaled.map(F26Dot6::from_i32);
}
}
// Commit our potentially modified phantom points.
self.phantom.copy_from_slice(&scaled[phantom_start..]);
if let (Some(hinter), true) = (self.hinter.as_ref(), is_hinted) {
if !ins.is_empty() {
// Create a copy of our scaled points in original_scaled.
let original_scaled = self
.memory
.original_scaled
.get_mut(..other_points_end)
.ok_or(InsufficientMemory)?;
original_scaled.copy_from_slice(scaled);
// When hinting, round the phantom points.
for point in &mut scaled[phantom_start..] {
point.x = point.x.round();
point.y = point.y.round();
}
let mut input = HintOutline {
glyph_id,
unscaled,
scaled,
original_scaled,
flags,
contours,
bytecode: ins,
phantom: &mut self.phantom,
stack: self.memory.stack,
cvt: self.memory.cvt,
storage: self.memory.storage,
twilight_scaled: self.memory.twilight_scaled,
twilight_original_scaled: self.memory.twilight_original_scaled,
twilight_flags: self.memory.twilight_flags,
is_composite: false,
coords: self.coords,
};
let hint_res = hinter.hint(self.outlines, &mut input, self.pedantic_hinting);
if let (Err(e), true) = (hint_res, self.pedantic_hinting) {
return Err(e)?;
}
} else if !hinter.backward_compatibility() {
// Even when missing instructions, FreeType uses rounded
// phantom points when hinting is requested and backward
// compatibility mode is disabled.
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttgload.c#L823>
// Notably, FreeType never calls TT_Hint_Glyph for composite
// glyphs when instructions are missing so this only applies
// to simple glyphs.
for (scaled, phantom) in scaled[phantom_start..].iter().zip(&mut self.phantom) {
*phantom = scaled.map(|x| x.round());
}
}
}
if points_start != 0 {
// If we're not the first component, shift our contour end points.
for contour_end in contours.iter_mut() {
*contour_end += points_start as u16;
}
}
Ok(())
}
fn load_composite(
&mut self,
glyph: &CompositeGlyph,
glyph_id: GlyphId,
recurse_depth: usize,
) -> Result<(), DrawError> {
use DrawError::InsufficientMemory;
let scale = self.scale;
// The base indices of the points and contours for the current glyph.
let point_base = self.point_count;
let contour_base = self.contour_count;
// Compute the per component deltas. Since composites can be nested, we
// use a stack and keep track of the base.
let mut have_deltas = false;
let delta_base = self.component_delta_count;
if self.outlines.gvar.is_some() && !self.coords.is_empty() {
let gvar = self.outlines.gvar.as_ref().unwrap();
let count = glyph.components().count() + PHANTOM_POINT_COUNT;
let deltas = self
.memory
.composite_deltas
.get_mut(delta_base..delta_base + count)
.ok_or(InsufficientMemory)?;
if deltas::composite_glyph(gvar, glyph_id, self.coords, &mut deltas[..]).is_ok() {
// Apply deltas to phantom points.
for (phantom, delta) in self
.phantom
.iter_mut()
.zip(&deltas[deltas.len() - PHANTOM_POINT_COUNT..])
{
*phantom += delta.map(Fixed::to_i32).map(F26Dot6::from_bits);
}
have_deltas = true;
}
self.component_delta_count += count;
}
if self.is_scaled {
for point in self.phantom.iter_mut() {
*point *= scale;
}
} else {
for point in self.phantom.iter_mut() {
*point = point.map(|x| F26Dot6::from_i32(x.to_bits()));
}
}
for (i, component) in glyph.components().enumerate() {
// Loading a component glyph will override phantom points so save a copy. We'll
// restore them unless the USE_MY_METRICS flag is set.
let phantom = self.phantom;
// Load the component glyph and keep track of the points range.
let start_point = self.point_count;
let component_glyph = self
.outlines
.loca
.get_glyf(component.glyph.into(), &self.outlines.glyf)?;
self.load(&component_glyph, component.glyph.into(), recurse_depth + 1)?;
let end_point = self.point_count;
if !component
.flags
.contains(CompositeGlyphFlags::USE_MY_METRICS)
{
// If the USE_MY_METRICS flag is missing, we restore the phantom points we
// saved at the start of the loop.
self.phantom = phantom;
}
// Prepares the transform components for our conversion math below.
fn scale_component(x: F2Dot14) -> F26Dot6 {
F26Dot6::from_bits(x.to_bits() as i32 * 4)
}
let xform = &component.transform;
let xx = scale_component(xform.xx);
let yx = scale_component(xform.yx);
let xy = scale_component(xform.xy);
let yy = scale_component(xform.yy);
let have_xform = component.flags.intersects(
CompositeGlyphFlags::WE_HAVE_A_SCALE
| CompositeGlyphFlags::WE_HAVE_AN_X_AND_Y_SCALE
| CompositeGlyphFlags::WE_HAVE_A_TWO_BY_TWO,
);
if have_xform {
let scaled = &mut self.memory.scaled[start_point..end_point];
if self.is_scaled {
for point in scaled {
let x = point.x * xx + point.y * xy;
let y = point.x * yx + point.y * yy;
point.x = x;
point.y = y;
}
} else {
for point in scaled {
// This juggling is necessary because, unlike FreeType, we also
// return unscaled outlines in 26.6 format for a consistent interface.
let unscaled = point.map(|c| F26Dot6::from_bits(c.to_i32()));
let x = unscaled.x * xx + unscaled.y * xy;
let y = unscaled.x * yx + unscaled.y * yy;
*point = Point::new(x, y).map(|c| F26Dot6::from_i32(c.to_bits()));
}
}
}
let anchor_offset = match component.anchor {
Anchor::Offset { x, y } => {
let (mut x, mut y) = (x as i32, y as i32);
if have_xform
&& component.flags
& (CompositeGlyphFlags::SCALED_COMPONENT_OFFSET
| CompositeGlyphFlags::UNSCALED_COMPONENT_OFFSET)
== CompositeGlyphFlags::SCALED_COMPONENT_OFFSET
{
// According to FreeType, this algorithm is a "guess"
// and works better than the one documented by Apple.
// https://github.com/freetype/freetype/blob/b1c90733ee6a04882b133101d61b12e352eeb290/src/truetype/ttgload.c#L1259
fn hypot(a: F26Dot6, b: F26Dot6) -> Fixed {
let a = a.to_bits().abs();
let b = b.to_bits().abs();
Fixed::from_bits(if a > b {
a + ((3 * b) >> 3)
} else {
b + ((3 * a) >> 3)
})
}
// FreeType uses a fixed point multiplication here.
x = (Fixed::from_bits(x) * hypot(xx, xy)).to_bits();
y = (Fixed::from_bits(y) * hypot(yy, yx)).to_bits();
}
if have_deltas {
let delta = self
.memory
.composite_deltas
.get(delta_base + i)
.copied()
.unwrap_or_default();
// For composite glyphs, we copy FreeType and round off
// the fractional parts of deltas.
x += delta.x.to_i32();
y += delta.y.to_i32();
}
if self.is_scaled {
let mut offset = Point::new(x, y).map(F26Dot6::from_bits) * scale;
if self.is_hinted
&& component
.flags
.contains(CompositeGlyphFlags::ROUND_XY_TO_GRID)
{
// Only round the y-coordinate, per FreeType.
offset.y = offset.y.round();
}
offset
} else {
Point::new(x, y).map(F26Dot6::from_i32)
}
}
Anchor::Point { base, component } => {
let (base_offset, component_offset) = (base as usize, component as usize);
let base_point = self
.memory
.scaled
.get(point_base + base_offset)
.ok_or(DrawError::InvalidAnchorPoint(glyph_id, base))?;
let component_point = self
.memory
.scaled
.get(start_point + component_offset)
.ok_or(DrawError::InvalidAnchorPoint(glyph_id, component))?;
*base_point - *component_point
}
};
if anchor_offset.x != F26Dot6::ZERO || anchor_offset.y != F26Dot6::ZERO {
for point in &mut self.memory.scaled[start_point..end_point] {
*point += anchor_offset;
}
}
}
if have_deltas {
self.component_delta_count = delta_base;
}
if let (Some(hinter), true) = (self.hinter.as_ref(), self.is_hinted) {
let ins = glyph.instructions().unwrap_or_default();
if !ins.is_empty() {
// For composite glyphs, the unscaled and original points are
// simply copies of the current point set.
let start_point = point_base;
let end_point = self.point_count + PHANTOM_POINT_COUNT;
let point_range = start_point..end_point;
let phantom_start = point_range.len() - PHANTOM_POINT_COUNT;
let scaled = &mut self.memory.scaled[point_range.clone()];
let flags = self
.memory
.flags
.get_mut(point_range.clone())
.ok_or(InsufficientMemory)?;
// Append the current phantom points to the outline.
for (i, phantom) in self.phantom.iter().enumerate() {
scaled[phantom_start + i] = *phantom;
flags[phantom_start + i] = Default::default();
}
let other_points_end = point_range.len();
let unscaled = self
.memory
.unscaled
.get_mut(..other_points_end)
.ok_or(InsufficientMemory)?;
for (scaled, unscaled) in scaled.iter().zip(unscaled.iter_mut()) {
*unscaled = scaled.map(|x| x.to_bits());
}
let original_scaled = self
.memory
.original_scaled
.get_mut(..other_points_end)
.ok_or(InsufficientMemory)?;
original_scaled.copy_from_slice(scaled);
let contours = self
.memory
.contours
.get_mut(contour_base..self.contour_count)
.ok_or(InsufficientMemory)?;
// Round the phantom points.
for p in &mut scaled[phantom_start..] {
p.x = p.x.round();
p.y = p.y.round();
}
// Clear the "touched" flags that are used during IUP processing.
for flag in flags.iter_mut() {
flag.clear_marker(PointMarker::TOUCHED);
}
// Make sure our contour end points accurately reflect the
// outline slices.
if point_base != 0 {
let delta = point_base as u16;
for contour in contours.iter_mut() {
*contour -= delta;
}
}
let mut input = HintOutline {
glyph_id,
unscaled,
scaled,
original_scaled,
flags,
contours,
bytecode: ins,
phantom: &mut self.phantom,
stack: self.memory.stack,
cvt: self.memory.cvt,
storage: self.memory.storage,
twilight_scaled: self.memory.twilight_scaled,
twilight_original_scaled: self.memory.twilight_original_scaled,
twilight_flags: self.memory.twilight_flags,
is_composite: true,
coords: self.coords,
};
let hint_res = hinter.hint(self.outlines, &mut input, self.pedantic_hinting);
if let (Err(e), true) = (hint_res, self.pedantic_hinting) {
return Err(e)?;
}
// Undo the contour shifts if we applied them above.
if point_base != 0 {
let delta = point_base as u16;
for contour in contours.iter_mut() {
*contour += delta;
}
}
}
}
Ok(())
}
}
impl Scaler for HarfBuzzScaler<'_> {
fn setup_phantom_points(
&mut self,
bounds: [i16; 4],
lsb: i32,
advance: i32,
tsb: i32,
vadvance: i32,
) {
// Same pattern as FreeType, just f32
// horizontal:
self.phantom[0].x = bounds[0] as f32 - lsb as f32;
self.phantom[0].y = 0.0;
self.phantom[1].x = self.phantom[0].x + advance as f32;
self.phantom[1].y = 0.0;
// vertical:
self.phantom[2].x = 0.0;
self.phantom[2].y = bounds[3] as f32 + tsb as f32;
self.phantom[3].x = 0.0;
self.phantom[3].y = self.phantom[2].y - vadvance as f32;
}
fn outlines(&self) -> &Outlines {
self.outlines
}
fn load_empty(&mut self, glyph_id: GlyphId) -> Result<(), DrawError> {
// HB doesn't have an equivalent so this version just copies the
// FreeType version above but changed to use floating point
let scale = self.scale.to_f32();
let mut unscaled = self.phantom;
if self.outlines.glyph_metrics.hvar.is_none()
&& self.outlines.gvar.is_some()
&& !self.coords.is_empty()
{
if let Ok(Some(deltas)) = self.outlines.gvar.as_ref().unwrap().phantom_point_deltas(
&self.outlines.glyf,
&self.outlines.loca,
self.coords,
glyph_id,
) {
unscaled[0] += deltas[0].map(Fixed::to_f32);
unscaled[1] += deltas[1].map(Fixed::to_f32);
}
}
if self.is_scaled {
for (phantom, unscaled) in self.phantom.iter_mut().zip(&unscaled) {
*phantom = *unscaled * scale;
}
} else {
for (phantom, unscaled) in self.phantom.iter_mut().zip(&unscaled) {
*phantom = *unscaled;
}
}
Ok(())
}
fn load_simple(&mut self, glyph: &SimpleGlyph, glyph_id: GlyphId) -> Result<(), DrawError> {
use DrawError::InsufficientMemory;
// Compute the ranges for our point/flag buffers and slice them.
let points_start = self.point_count;
let point_count = glyph.num_points();
let phantom_start = point_count;
let points_end = points_start + point_count + PHANTOM_POINT_COUNT;
let point_range = points_start..points_end;
// Points and flags are accumulated as we load the outline.
let points = self
.memory
.points
.get_mut(point_range.clone())
.ok_or(InsufficientMemory)?;
let flags = self
.memory
.flags
.get_mut(point_range)
.ok_or(InsufficientMemory)?;
glyph.read_points_fast(&mut points[..point_count], &mut flags[..point_count])?;
// Compute the range for our contour end point buffer and slice it.
let contours_start = self.contour_count;
let contour_end_pts = glyph.end_pts_of_contours();
let contour_count = contour_end_pts.len();
let contours_end = contours_start + contour_count;
let contours = self
.memory
.contours
.get_mut(contours_start..contours_end)
.ok_or(InsufficientMemory)?;
// Read the contour end points.
for (end_pt, contour) in contour_end_pts.iter().zip(contours.iter_mut()) {
*contour = end_pt.get();
}
// Adjust the running point/contour total counts
self.point_count += point_count;
self.contour_count += contour_count;
// Append phantom points to the outline.
for (i, phantom) in self.phantom.iter().enumerate() {
points[phantom_start + i] = *phantom;
flags[phantom_start + i] = Default::default();
}
// Acquire deltas
if self.outlines.gvar.is_some() && !self.coords.is_empty() {
let gvar = self.outlines.gvar.as_ref().unwrap();
let glyph = deltas::SimpleGlyph {
points: &mut points[..],
flags: &mut flags[..],
contours,
};
let deltas = self
.memory
.deltas
.get_mut(..point_count + PHANTOM_POINT_COUNT)
.ok_or(InsufficientMemory)?;
let iup_buffer = self
.memory
.iup_buffer
.get_mut(..point_count + PHANTOM_POINT_COUNT)
.ok_or(InsufficientMemory)?;
if deltas::simple_glyph(gvar, glyph_id, self.coords, glyph, iup_buffer, deltas).is_ok()
{
for (point, delta) in points.iter_mut().zip(deltas) {
*point += *delta;
}
}
}
// Apply scaling
if self.is_scaled {
let scale = self.scale.to_f32();
for point in points.iter_mut() {
*point = point.map(|c| c * scale);
}
}
if points_start != 0 {
// If we're not the first component, shift our contour end points.
for contour_end in contours.iter_mut() {
*contour_end += points_start as u16;
}
}
Ok(())
}
fn load_composite(
&mut self,
glyph: &CompositeGlyph,
glyph_id: GlyphId,
recurse_depth: usize,
) -> Result<(), DrawError> {
use DrawError::InsufficientMemory;
let scale = self.scale.to_f32();
// The base indices of the points for the current glyph.
let point_base = self.point_count;
// Compute the per component deltas. Since composites can be nested, we
// use a stack and keep track of the base.
let mut have_deltas = false;
let delta_base = self.component_delta_count;
if self.outlines.gvar.is_some() && !self.coords.is_empty() {
let gvar = self.outlines.gvar.as_ref().unwrap();
let count = glyph.components().count() + PHANTOM_POINT_COUNT;
let deltas = self
.memory
.composite_deltas
.get_mut(delta_base..delta_base + count)
.ok_or(InsufficientMemory)?;
if deltas::composite_glyph(gvar, glyph_id, self.coords, &mut deltas[..]).is_ok() {
// Apply deltas to phantom points.
for (phantom, delta) in self
.phantom
.iter_mut()
.zip(&deltas[deltas.len() - PHANTOM_POINT_COUNT..])
{
*phantom += *delta;
}
have_deltas = true;
}
self.component_delta_count += count;
}
if self.is_scaled {
for point in self.phantom.iter_mut() {
*point *= scale;
}
}
for (i, component) in glyph.components().enumerate() {
// Loading a component glyph will override phantom points so save a copy. We'll
// restore them unless the USE_MY_METRICS flag is set.
let phantom = self.phantom;
// Load the component glyph and keep track of the points range.
let start_point = self.point_count;
let component_glyph = self
.outlines
.loca
.get_glyf(component.glyph.into(), &self.outlines.glyf)?;
self.load(&component_glyph, component.glyph.into(), recurse_depth + 1)?;
let end_point = self.point_count;
if !component
.flags
.contains(CompositeGlyphFlags::USE_MY_METRICS)
{
// If the USE_MY_METRICS flag is missing, we restore the phantom points we
// saved at the start of the loop.
self.phantom = phantom;
}
let have_xform = component.flags.intersects(
CompositeGlyphFlags::WE_HAVE_A_SCALE
| CompositeGlyphFlags::WE_HAVE_AN_X_AND_Y_SCALE
| CompositeGlyphFlags::WE_HAVE_A_TWO_BY_TWO,
);
let mut transform = if have_xform {
let xform = &component.transform;
[
xform.xx,
xform.yx,
xform.xy,
xform.yy,
F2Dot14::ZERO,
F2Dot14::ZERO,
]
.map(|x| x.to_f32())
} else {
[1.0, 0.0, 0.0, 1.0, 0.0, 0.0] // identity
};
let anchor_offset = match component.anchor {
Anchor::Offset { x, y } => {
let (mut x, mut y) = (x as f32, y as f32);
if have_xform
&& component.flags
& (CompositeGlyphFlags::SCALED_COMPONENT_OFFSET
| CompositeGlyphFlags::UNSCALED_COMPONENT_OFFSET)
== CompositeGlyphFlags::SCALED_COMPONENT_OFFSET
{
// Scale x by the magnitude of the x-basis, y by the y-basis
// FreeType implements hypot, we can just use the provided implementation
x *= hypot(transform[0], transform[2]);
y *= hypot(transform[1], transform[3]);
}
Point::new(x, y)
+ self
.memory
.composite_deltas
.get(delta_base + i)
.copied()
.unwrap_or_default()
}
Anchor::Point { base, component } => {
let (base_offset, component_offset) = (base as usize, component as usize);
let base_point = self
.memory
.points
.get(point_base + base_offset)
.ok_or(DrawError::InvalidAnchorPoint(glyph_id, base))?;
let component_point = self
.memory
.points
.get(start_point + component_offset)
.ok_or(DrawError::InvalidAnchorPoint(glyph_id, component))?;
*base_point - *component_point
}
};
transform[4] = anchor_offset.x;
transform[5] = anchor_offset.y;
let points = &mut self.memory.points[start_point..end_point];
for point in points.iter_mut() {
*point = map_point(transform, *point);
}
}
if have_deltas {
self.component_delta_count = delta_base;
}
Ok(())
}
}
/// Magnitude of the vector (x, y)
fn hypot(x: f32, y: f32) -> f32 {
x.hypot(y)
}
fn map_point(transform: [f32; 6], p: Point<f32>) -> Point<f32> {
Point {
x: transform[0] * p.x + transform[2] * p.y + transform[4],
y: transform[1] * p.x + transform[3] * p.y + transform[5],
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MetadataProvider;
use raw::{
tables::{
glyf::{CompositeGlyphFlags, Glyf, SimpleGlyphFlags},
loca::Loca,
},
FontRead, FontRef, TableProvider,
};
#[test]
fn overlap_flags() {
let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
let scaler = Outlines::new(&font).unwrap();
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| scaler.outline(GlyphId::from(*gid)).unwrap().has_overlaps)
.collect::<Vec<_>>()
);
}
#[test]
fn interpreter_preference() {
// no instructions in this font...
let font = FontRef::new(font_test_data::COLRV0V1).unwrap();
let outlines = Outlines::new(&font).unwrap();
// thus no preference for the interpreter
assert!(!outlines.prefer_interpreter());
// but this one has instructions...
let font = FontRef::new(font_test_data::TTHINT_SUBSET).unwrap();
let outlines = Outlines::new(&font).unwrap();
// so let's use it
assert!(outlines.prefer_interpreter());
}
#[test]
fn empty_glyph_advance() {
let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
let outlines = Outlines::new(&font).unwrap();
let coords = [F2Dot14::from_f32(0.5)];
let ppem = Some(24.0);
let gid = font.charmap().map(' ').unwrap();
let outline = outlines.outline(gid).unwrap();
// Make sure this is an empty outline since that's what we're testing
assert!(outline.glyph.is_none());
let mut buf = [0u8; 128];
let scaler =
FreeTypeScaler::unhinted(&outlines, &outline, &mut buf, ppem, &coords).unwrap();
let scaled = scaler.scale(&outline.glyph, gid).unwrap();
let advance = scaled.adjusted_advance_width();
assert!(advance != F26Dot6::ZERO);
}
#[test]
fn empty_glyphs_have_phantom_points_too() {
let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
let outlines = Outlines::new(&font).unwrap();
let gid = font.charmap().map(' ').unwrap();
let outline = outlines.outline(gid).unwrap();
assert!(outline.glyph.is_none());
assert_eq!(outline.points, PHANTOM_POINT_COUNT);
}
// fuzzer overflow for composite glyph with too many total points
// <https://issues.oss-fuzz.com/issues/391753684
#[test]
fn composite_with_too_many_points() {
let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
let mut outlines = Outlines::new(&font).unwrap();
// Hack glyf and loca to build a glyph that contains more than 64k
// total points
let mut glyf_buf = font_test_data::bebuffer::BeBuffer::new();
// Make a component glyph with 40k points so we overflow the
// total limit in a composite
let simple_glyph_point_count = 40000;
glyf_buf = glyf_buf.push(1u16); // number of contours
glyf_buf = glyf_buf.extend([0i16; 4]); // bbox
glyf_buf = glyf_buf.push((simple_glyph_point_count - 1) as u16); // contour ends
glyf_buf = glyf_buf.push(0u16); // instruction count
for _ in 0..simple_glyph_point_count {
glyf_buf =
glyf_buf.push(SimpleGlyphFlags::X_SHORT_VECTOR | SimpleGlyphFlags::Y_SHORT_VECTOR);
}
// x/y coords
for _ in 0..simple_glyph_point_count * 2 {
glyf_buf = glyf_buf.push(0u8);
}
let glyph0_end = glyf_buf.len();
// Now make a composite with two components
glyf_buf = glyf_buf.push(-1i16); // negative signifies composite
glyf_buf = glyf_buf.extend([0i16; 4]); // bbox
for i in 0..2 {
let flags = if i == 0 {
CompositeGlyphFlags::MORE_COMPONENTS | CompositeGlyphFlags::ARGS_ARE_XY_VALUES
} else {
CompositeGlyphFlags::ARGS_ARE_XY_VALUES
};
glyf_buf = glyf_buf.push(flags); // component flag
glyf_buf = glyf_buf.push(0u16); // component gid
glyf_buf = glyf_buf.extend([0u8; 2]); // x/y offset
}
let glyph1_end = glyf_buf.len();
outlines.glyf = Glyf::read(glyf_buf.data().into()).unwrap();
// Now create a loca table
let mut loca_buf = font_test_data::bebuffer::BeBuffer::new();
loca_buf = loca_buf.extend([0u32, glyph0_end as u32, glyph1_end as u32]);
outlines.loca = Loca::read(loca_buf.data().into(), true).unwrap();
let gid = GlyphId::new(1);
let outline = outlines.outline(gid).unwrap();
let mut mem_buf = vec![0u8; outline.required_buffer_size(Default::default())];
// This outline has more than 64k points...
assert!(outline.points > u16::MAX as usize);
let result = FreeTypeScaler::unhinted(&outlines, &outline, &mut mem_buf, None, &[]);
// And we get an error instead of an overflow panic
assert!(matches!(result, Err(DrawError::TooManyPoints(_))));
}
}