#![forbid(unsafe_code)]
#[cfg(feature = "bundled-faces")]
pub mod bundled;
mod gvar;
pub mod outline;
#[must_use]
pub fn variable_triangle_fixture() -> Vec<u8> {
gvar::variable_triangle_fixture()
}
const MAX_LAYOUT_GLYPHS: usize = 65_536;
const MAX_COVERAGE_GLYPHS: usize = MAX_LAYOUT_GLYPHS;
pub const MISSING_GLYPH_REMAP: u16 = u16::MAX;
const MAX_VARIATION_AXES: usize = 64;
const MAX_NAMED_INSTANCES: usize = 256;
const MAX_AVAR_MAPS: usize = 64;
type AvarAxisMaps = Vec<Option<Vec<(f32, f32)>>>;
#[derive(Debug, Clone)]
struct Cmap4Segment {
start: u16,
end: u16,
id_delta: u16,
id_range_offset: u16,
id_range_offset_pos: usize,
}
#[derive(Debug, Clone)]
struct Cmap4Cache {
segments: Vec<Cmap4Segment>,
sorted_by_end: bool,
}
#[derive(Debug, Clone)]
pub struct Font {
data: Vec<u8>,
pub units_per_em: u16,
pub num_glyphs: u16,
pub ascent: i16,
pub descent: i16,
pub line_gap: i16,
num_h_metrics: u16,
hmtx_off: usize,
cmap_off: usize,
cmap_format: u16,
cmap4_cache: Option<Cmap4Cache>,
glyf: Option<(usize, usize)>,
loca_off: Option<usize>,
loca_long: bool,
kern0: Option<(usize, u16)>,
variation: Option<FontVariation>,
latin1_glyphs: [u16; 256],
latin1_advances_1000: [u32; 256],
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VariationAxis {
pub tag: [u8; 4],
pub min: f32,
pub default: f32,
pub max: f32,
pub flags: u16,
pub name_id: u16,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AxisBounds {
pub min: f32,
pub default: f32,
pub max: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct NamedInstance {
pub subfamily_name_id: u16,
pub flags: u16,
pub coordinates: Vec<f32>,
pub postscript_name_id: Option<u16>,
}
#[derive(Debug, Clone)]
struct FontVariation {
axes: Vec<VariationAxis>,
instances: Vec<NamedInstance>,
avar: AvarAxisMaps,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FontError {
BadMagic,
MissingTable(&'static str),
Truncated,
NoUnicodeCmap,
}
impl core::fmt::Display for FontError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::BadMagic => write!(f, "not a TrueType/OpenType font"),
Self::MissingTable(t) => write!(f, "missing required font table: {t}"),
Self::Truncated => write!(f, "font data is truncated"),
Self::NoUnicodeCmap => write!(f, "no usable Unicode cmap (format 4/12)"),
}
}
}
impl std::error::Error for FontError {}
pub(crate) fn be_u16(d: &[u8], o: usize) -> Option<u16> {
let bytes = d.get(o..o.checked_add(2)?)?;
Some(u16::from_be_bytes([bytes[0], bytes[1]]))
}
pub(crate) fn be_i16(d: &[u8], o: usize) -> Option<i16> {
be_u16(d, o).map(|v| v as i16)
}
pub(crate) fn be_u32(d: &[u8], o: usize) -> Option<u32> {
let bytes = d.get(o..o.checked_add(4)?)?;
Some(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
fn be_i32(d: &[u8], o: usize) -> Option<i32> {
be_u32(d, o).map(|v| v as i32)
}
fn fixed_16_16(v: i32) -> f32 {
(f64::from(v) / 65536.0) as f32
}
fn f2dot14(v: i16) -> f32 {
f32::from(v) / 16384.0
}
pub(crate) fn off(base: usize, delta: usize) -> Option<usize> {
base.checked_add(delta)
}
pub(crate) fn off_mul(base: usize, index: usize, stride: usize) -> Option<usize> {
base.checked_add(index.checked_mul(stride)?)
}
fn be_u16_at(d: &[u8], base: usize, delta: usize) -> Option<u16> {
be_u16(d, off(base, delta)?)
}
fn be_u32_at(d: &[u8], base: usize, delta: usize) -> Option<u32> {
be_u32(d, off(base, delta)?)
}
fn bytes_at(d: &[u8], base: usize, len: usize) -> Option<&[u8]> {
d.get(base..off(base, len)?)
}
fn write_u16(d: &mut [u8], off: usize, v: u16) -> Option<()> {
let b = v.to_be_bytes();
let dst = d.get_mut(off..off.checked_add(2)?)?;
dst.copy_from_slice(&b);
Some(())
}
pub(crate) fn write_u32(d: &mut [u8], off: usize, v: u32) -> Option<()> {
let b = v.to_be_bytes();
let dst = d.get_mut(off..off.checked_add(4)?)?;
dst.copy_from_slice(&b);
Some(())
}
pub(crate) fn table_checksum(d: &[u8]) -> u32 {
let mut sum: u32 = 0;
let mut chunks16 = d.chunks_exact(16);
for c in &mut chunks16 {
let w0 = u32::from_be_bytes([c[0], c[1], c[2], c[3]]);
let w1 = u32::from_be_bytes([c[4], c[5], c[6], c[7]]);
let w2 = u32::from_be_bytes([c[8], c[9], c[10], c[11]]);
let w3 = u32::from_be_bytes([c[12], c[13], c[14], c[15]]);
sum = sum
.wrapping_add(w0)
.wrapping_add(w1)
.wrapping_add(w2)
.wrapping_add(w3);
}
let mut chunks4 = chunks16.remainder().chunks_exact(4);
for c in &mut chunks4 {
sum = sum.wrapping_add(u32::from_be_bytes([c[0], c[1], c[2], c[3]]));
}
let rem = chunks4.remainder();
if !rem.is_empty() {
let mut buf = [0u8; 4];
buf[..rem.len()].copy_from_slice(rem);
sum = sum.wrapping_add(u32::from_be_bytes(buf));
}
sum
}
fn find_table(d: &[u8], tag: &[u8; 4]) -> Option<usize> {
find_table_full(d, tag).map(|(off, _)| off)
}
pub(crate) fn find_table_full(d: &[u8], tag: &[u8; 4]) -> Option<(usize, usize)> {
let num_tables = be_u16(d, 4)? as usize;
for i in 0..num_tables {
let rec = off_mul(12, i, 16)?;
if bytes_at(d, rec, 4)? == tag {
return Some((
be_u32_at(d, rec, 8)? as usize,
be_u32_at(d, rec, 12)? as usize,
));
}
}
None
}
fn find_kern0(d: &[u8]) -> Option<(usize, u16)> {
let (kern, kern_len) = find_table_full(d, b"kern")?;
let table_end = kern.checked_add(kern_len)?;
let version = be_u16(d, kern)?;
let n_tables = be_u16_at(d, kern, 2)? as usize;
if version != 0 {
return None;
}
let mut sub = off(kern, 4)?;
for _ in 0..n_tables {
if sub.checked_add(6)? > table_end {
return None;
}
let length = be_u16_at(d, sub, 2)? as usize;
let coverage = be_u16_at(d, sub, 4)?;
let format = coverage >> 8;
let horizontal = coverage & 0x0001 != 0;
let minimum = coverage & 0x0002 != 0;
let pairs = off(sub, 14)?;
if format == 0 && horizontal && !minimum && length >= 14 {
let sub_end = sub.checked_add(length)?;
if sub_end > table_end {
return None;
}
let n_pairs = be_u16_at(d, sub, 6)?;
let bytes_needed = (n_pairs as usize).checked_mul(6)?;
if pairs.checked_add(bytes_needed)? <= sub_end {
return Some((pairs, n_pairs));
}
return None;
}
if length == 0 {
return None;
}
sub = sub.checked_add(length)?;
}
None
}
fn parse_fvar(d: &[u8], table_off: usize, table_len: usize) -> Option<FontVariation> {
let table_end = table_off.checked_add(table_len)?;
if table_off.checked_add(16)? > table_end {
return None;
}
let major = be_u16(d, table_off)?;
if major != 1 {
return None;
}
let axes_array_offset = be_u16_at(d, table_off, 4)? as usize;
let axis_count = be_u16_at(d, table_off, 8)? as usize;
let axis_size = be_u16_at(d, table_off, 10)? as usize;
let instance_count = be_u16_at(d, table_off, 12)? as usize;
let instance_size = be_u16_at(d, table_off, 14)? as usize;
if axis_size < 20 {
return None;
}
let n_axes = axis_count.min(MAX_VARIATION_AXES);
let axes_off = off(table_off, axes_array_offset)?;
let axes_bytes = n_axes.checked_mul(axis_size)?;
if axes_off.checked_add(axes_bytes)? > table_end {
return None;
}
let mut axes = Vec::with_capacity(n_axes);
for i in 0..n_axes {
let rec = off_mul(axes_off, i, axis_size)?;
let tag_bytes = bytes_at(d, rec, 4)?;
let mut tag = [0u8; 4];
tag.copy_from_slice(tag_bytes);
let min = fixed_16_16(be_i32(d, off(rec, 4)?)?);
let default = fixed_16_16(be_i32(d, off(rec, 8)?)?);
let max = fixed_16_16(be_i32(d, off(rec, 12)?)?);
let flags = be_u16_at(d, rec, 16)?;
let name_id = be_u16_at(d, rec, 18)?;
axes.push(VariationAxis {
tag,
min,
default,
max,
flags,
name_id,
});
}
let n_inst = instance_count.min(MAX_NAMED_INSTANCES);
let coord_bytes = n_axes.checked_mul(4)?;
let min_inst_size = 4usize.checked_add(coord_bytes)?;
let mut instances = Vec::new();
if instance_size >= min_inst_size {
let inst_off = off(axes_off, axes_bytes)?;
let inst_bytes = n_inst.checked_mul(instance_size)?;
if inst_off.checked_add(inst_bytes)? <= table_end {
let has_ps_name = instance_size >= min_inst_size.saturating_add(2);
for i in 0..n_inst {
let rec = off_mul(inst_off, i, instance_size)?;
let subfamily_name_id = be_u16(d, rec)?;
let flags = be_u16_at(d, rec, 2)?;
let mut coordinates = Vec::with_capacity(n_axes);
let mut ok = true;
for a in 0..n_axes {
let Some(coord) = off(rec, 4)
.and_then(|base| off_mul(base, a, 4))
.and_then(|o| be_i32(d, o))
else {
ok = false;
break;
};
coordinates.push(fixed_16_16(coord));
}
if !ok {
continue;
}
let postscript_name_id = if has_ps_name {
be_u16_at(d, rec, min_inst_size)
} else {
None
};
instances.push(NamedInstance {
subfamily_name_id,
flags,
coordinates,
postscript_name_id,
});
}
}
}
let avar = vec![None; axes.len()];
Some(FontVariation {
axes,
instances,
avar,
})
}
fn parse_avar(d: &[u8], table_off: usize, table_len: usize, n_axes: usize) -> Option<AvarAxisMaps> {
let table_end = table_off.checked_add(table_len)?;
if table_off.checked_add(8)? > table_end {
return None;
}
let major = be_u16(d, table_off)?;
if major != 1 {
return None;
}
let axis_count = be_u16_at(d, table_off, 6)? as usize;
if axis_count != n_axes {
return None;
}
let mut maps = Vec::with_capacity(n_axes);
let mut cursor = off(table_off, 8)?;
for _ in 0..n_axes {
if cursor.checked_add(2)? > table_end {
return None;
}
let count = be_u16(d, cursor)? as usize;
cursor = off(cursor, 2)?;
let n = count.min(MAX_AVAR_MAPS);
let bytes = n.checked_mul(4)?;
if cursor.checked_add(bytes)? > table_end {
return None;
}
let mut segs = Vec::with_capacity(n);
for i in 0..n {
let rec = off_mul(cursor, i, 4)?;
let from = f2dot14(be_i16(d, rec)?);
let to = f2dot14(be_i16_at(d, rec, 2)?);
segs.push((from, to));
}
let claimed = count.checked_mul(4)?;
cursor = off(cursor, claimed)?;
if segs.len() < 2 {
maps.push(None);
continue;
}
let sorted = segs.windows(2).all(|w| w[0].0 <= w[1].0);
maps.push(if sorted { Some(segs) } else { None });
}
Some(maps)
}
fn be_i16_at(d: &[u8], base: usize, delta: usize) -> Option<i16> {
be_i16(d, off(base, delta)?)
}
fn avar_map(maps: &[(f32, f32)], x: f32) -> f32 {
let Some(first) = maps.first() else {
return x;
};
if x <= first.0 {
return first.1;
}
let Some(last) = maps.last() else {
return x;
};
if x >= last.0 {
return last.1;
}
for pair in maps.windows(2) {
let (from0, to0) = pair[0];
let (from1, to1) = pair[1];
if x <= from1 {
let span = from1 - from0;
if span == 0.0 {
return to0;
}
let t = (x - from0) / span;
return to0 + t * (to1 - to0);
}
}
last.1
}
fn normalize_user(user: f32, axis: &VariationAxis) -> f32 {
let lo = axis.min.min(axis.max);
let hi = axis.min.max(axis.max);
let user = if user < lo {
lo
} else if user > hi {
hi
} else {
user
};
let default = if axis.default < lo {
lo
} else if axis.default > hi {
hi
} else {
axis.default
};
if user < default {
let span = default - lo;
if span == 0.0 {
0.0
} else {
(user - default) / span
}
} else if user > default {
let span = hi - default;
if span == 0.0 {
0.0
} else {
(user - default) / span
}
} else {
0.0
}
}
impl Font {
pub fn parse(data: Vec<u8>) -> Result<Self, FontError> {
let d = data.as_slice();
let magic = be_u32(d, 0).ok_or(FontError::Truncated)?;
if magic != 0x0001_0000 && magic != 0x7472_7565 && magic != 0x4F54_544F {
return Err(FontError::BadMagic);
}
let head = find_table(d, b"head").ok_or(FontError::MissingTable("head"))?;
let maxp = find_table(d, b"maxp").ok_or(FontError::MissingTable("maxp"))?;
let hhea = find_table(d, b"hhea").ok_or(FontError::MissingTable("hhea"))?;
let hmtx = find_table(d, b"hmtx").ok_or(FontError::MissingTable("hmtx"))?;
let cmap = find_table(d, b"cmap").ok_or(FontError::MissingTable("cmap"))?;
let units_per_em =
be_u16(d, off(head, 18).ok_or(FontError::Truncated)?).ok_or(FontError::Truncated)?;
let num_glyphs =
be_u16(d, off(maxp, 4).ok_or(FontError::Truncated)?).ok_or(FontError::Truncated)?;
let ascent =
be_i16(d, off(hhea, 4).ok_or(FontError::Truncated)?).ok_or(FontError::Truncated)?;
let descent =
be_i16(d, off(hhea, 6).ok_or(FontError::Truncated)?).ok_or(FontError::Truncated)?;
let line_gap =
be_i16(d, off(hhea, 8).ok_or(FontError::Truncated)?).ok_or(FontError::Truncated)?;
let num_h_metrics =
be_u16(d, off(hhea, 34).ok_or(FontError::Truncated)?).ok_or(FontError::Truncated)?;
let (cmap_off, cmap_format) = select_cmap(d, cmap).ok_or(FontError::NoUnicodeCmap)?;
let cmap4_cache = if cmap_format == 4 {
parse_cmap4_cache(d, cmap_off)
} else {
None
};
let loca_long = off(head, 50)
.and_then(|offset| be_i16(d, offset))
.unwrap_or(0)
!= 0;
let loca_off = find_table(d, b"loca");
let glyf = find_table_full(d, b"glyf");
let kern0 = find_kern0(d);
let mut variation =
find_table_full(d, b"fvar").and_then(|(off, len)| parse_fvar(d, off, len));
if let Some(var) = variation.as_mut() {
if let Some(avar) = find_table_full(d, b"avar")
.and_then(|(off, len)| parse_avar(d, off, len, var.axes.len()))
{
var.avar = avar;
}
}
let mut font = Self {
data,
units_per_em,
num_glyphs,
ascent,
descent,
line_gap,
num_h_metrics,
hmtx_off: hmtx,
cmap_off,
cmap_format,
cmap4_cache,
glyf,
loca_off,
loca_long,
kern0,
variation,
latin1_glyphs: [0; 256],
latin1_advances_1000: [0; 256],
};
for b in 0..256 {
let gid = match font.cmap_format {
4 => font.cmap4_lookup(b as u32).unwrap_or(0),
12 => font.cmap12_lookup(b as u32).unwrap_or(0),
_ => 0,
};
font.latin1_glyphs[b] = gid;
}
if font.units_per_em > 0 {
for b in 0..256 {
let gid = font.latin1_glyphs[b];
let aw = font.advance_width(gid) as u32;
font.latin1_advances_1000[b] = aw * 1000 / font.units_per_em as u32;
}
}
Ok(font)
}
#[must_use]
pub fn has_glyf_outlines(&self) -> bool {
self.glyf.is_some() && self.loca_off.is_some()
}
#[must_use]
pub fn axes(&self) -> &[VariationAxis] {
self.variation
.as_ref()
.map(|v| v.axes.as_slice())
.unwrap_or(&[])
}
#[must_use]
pub fn named_instances(&self) -> &[NamedInstance] {
self.variation
.as_ref()
.map(|v| v.instances.as_slice())
.unwrap_or(&[])
}
#[must_use]
pub fn instance_bounds(&self, tag: [u8; 4]) -> Option<AxisBounds> {
self.axes()
.iter()
.find(|a| a.tag == tag)
.map(|a| AxisBounds {
min: a.min,
default: a.default,
max: a.max,
})
}
#[must_use]
pub fn normalized_axis(&self, tag: [u8; 4], user: f32) -> Option<f32> {
let var = self.variation.as_ref()?;
let (idx, axis) = var.axes.iter().enumerate().find(|(_, a)| a.tag == tag)?;
let mut n = normalize_user(user, axis);
if let Some(maps) = var.avar.get(idx).and_then(|m| m.as_ref()) {
n = avar_map(maps, n);
}
Some(n)
}
#[must_use]
pub fn instance(&self, weight: f32) -> Option<Font> {
crate::gvar::instance_font(self, weight)
}
#[must_use]
pub fn as_sfnt(&self) -> &[u8] {
&self.data
}
pub(crate) fn raw_bytes(&self) -> &[u8] {
self.as_sfnt()
}
fn glyph_range(&self, gid: u16) -> Option<(usize, usize)> {
let loca = self.loca_off?;
let (glyf_off, glyf_len) = self.glyf?;
let i = gid as usize;
let (start, end) = if self.loca_long {
(
be_u32(&self.data, off_mul(loca, i, 4)?)? as usize,
be_u32(&self.data, off_mul(loca, i.checked_add(1)?, 4)?)? as usize,
)
} else {
(
be_u16(&self.data, off_mul(loca, i, 2)?)? as usize * 2,
be_u16(&self.data, off_mul(loca, i.checked_add(1)?, 2)?)? as usize * 2,
)
};
if end < start || end > glyf_len {
return None;
}
Some((off(glyf_off, start)?, off(glyf_off, end)?))
}
#[must_use]
pub fn glyph_data(&self, gid: u16) -> Option<&[u8]> {
let (s, e) = self.glyph_range(gid)?;
self.data.get(s..e)
}
#[must_use]
pub fn glyph_bbox(&self, gid: u16) -> Option<[i16; 4]> {
let (s, e) = self.glyph_range(gid)?;
if e <= s {
return None; }
Some([
be_i16(&self.data, off(s, 2)?)?,
be_i16(&self.data, off(s, 4)?)?,
be_i16(&self.data, off(s, 6)?)?,
be_i16(&self.data, off(s, 8)?)?,
])
}
#[must_use]
pub fn is_composite(&self, gid: u16) -> bool {
match self.glyph_range(gid) {
Some((s, e)) if e > s => be_i16(&self.data, s).is_some_and(|n| n < 0),
_ => false,
}
}
#[must_use]
pub fn glyph_components(&self, gid: u16) -> Vec<u16> {
const ARG_WORDS: u16 = 0x0001;
const WE_HAVE_SCALE: u16 = 0x0008;
const MORE: u16 = 0x0020;
const X_Y_SCALE: u16 = 0x0040;
const TWO_BY_TWO: u16 = 0x0080;
let mut out = Vec::new();
let Some((s, e)) = self.glyph_range(gid) else {
return out;
};
if e <= s || be_i16(&self.data, s).is_none_or(|n| n >= 0) {
return out;
}
let Some(mut p) = off(s, 10) else {
return out;
};
while let Some(component_record_end) = off(p, 4) {
if component_record_end > e {
break;
}
let Some(flags) = be_u16(&self.data, p) else {
break;
};
let Some(comp) = off(p, 2).and_then(|offset| be_u16(&self.data, offset)) else {
break;
};
let mut step = 4usize + if flags & ARG_WORDS != 0 { 4 } else { 2 };
step += if flags & WE_HAVE_SCALE != 0 {
2
} else if flags & X_Y_SCALE != 0 {
4
} else if flags & TWO_BY_TWO != 0 {
8
} else {
0
};
let Some(next) = off(p, step) else {
break;
};
if next > e {
break;
}
out.push(comp);
p = next;
if flags & MORE == 0 || p >= e {
break;
}
}
out
}
#[must_use]
pub fn advance_width(&self, gid: u16) -> u16 {
let last = self.num_h_metrics.saturating_sub(1);
let idx = gid.min(last) as usize;
off_mul(self.hmtx_off, idx, 4)
.and_then(|offset| be_u16(&self.data, offset))
.unwrap_or(0)
}
#[must_use]
pub fn left_side_bearing(&self, gid: u16) -> i16 {
if self.num_h_metrics == 0 {
return 0;
}
let gid = gid as usize;
let num_h_metrics = self.num_h_metrics as usize;
let offset = if gid < num_h_metrics {
off_mul(self.hmtx_off, gid, 4).and_then(|base| off(base, 2))
} else {
off_mul(self.hmtx_off, num_h_metrics, 4)
.and_then(|base| off_mul(base, gid - num_h_metrics, 2))
};
offset
.and_then(|offset| be_i16(&self.data, offset))
.unwrap_or(0)
}
#[must_use]
#[inline(always)]
pub fn glyph_index(&self, ch: char) -> u16 {
let cp = ch as u32;
if cp < 256 {
return self.latin1_glyphs[cp as usize];
}
match self.cmap_format {
4 => self.cmap4_lookup(cp).unwrap_or(0),
12 => self.cmap12_lookup(cp).unwrap_or(0),
_ => 0,
}
}
#[must_use]
#[inline(always)]
pub fn advance_1000(&self, ch: char) -> u32 {
let cp = ch as u32;
if cp < 256 {
return self.latin1_advances_1000[cp as usize];
}
if self.units_per_em == 0 {
return 0;
}
let aw = self.advance_width(self.glyph_index(ch)) as u32;
aw * 1000 / self.units_per_em as u32
}
#[must_use]
pub fn kerning_between_glyphs(&self, left: u16, right: u16) -> i16 {
let Some((pairs, n_pairs)) = self.kern0 else {
return 0;
};
let target = ((left as u32) << 16) | right as u32;
let mut lo = 0usize;
let mut hi = n_pairs as usize;
while lo < hi {
let mid = lo + (hi - lo) / 2;
let Some(rec) = off_mul(pairs, mid, 6) else {
return 0;
};
let Some(l) = be_u16(&self.data, rec) else {
return 0;
};
let Some(r) = off(rec, 2).and_then(|offset| be_u16(&self.data, offset)) else {
return 0;
};
let key = ((l as u32) << 16) | r as u32;
if key == target {
return off(rec, 4)
.and_then(|offset| be_i16(&self.data, offset))
.unwrap_or(0);
}
if key < target {
lo = mid + 1;
} else {
hi = mid;
}
}
0
}
#[must_use]
pub fn kerning(&self, left: char, right: char) -> i16 {
self.kerning_between_glyphs(self.glyph_index(left), self.glyph_index(right))
}
#[must_use]
pub fn kerning_1000(&self, left: char, right: char) -> i32 {
if self.units_per_em == 0 {
return 0;
}
self.kerning(left, right) as i32 * 1000 / self.units_per_em as i32
}
fn cmap4_lookup(&self, cp: u32) -> Option<u16> {
if cp > 0xFFFF {
return Some(0);
}
let c = cp as u16;
if let Some(cache) = &self.cmap4_cache {
return self.cmap4_cached_lookup(c, cache);
}
self.cmap4_uncached_lookup(c)
}
fn cmap4_cached_lookup(&self, c: u16, cache: &Cmap4Cache) -> Option<u16> {
let segment = if cache.sorted_by_end {
let idx = cache.segments.partition_point(|seg| seg.end < c);
cache.segments.get(idx)
} else {
cache.segments.iter().find(|seg| c <= seg.end)
}?;
if c < segment.start {
return Some(0);
}
if segment.id_range_offset == 0 {
return Some(c.wrapping_add(segment.id_delta));
}
let gi_addr = off(
off(
segment.id_range_offset_pos,
segment.id_range_offset as usize,
)?,
2usize.checked_mul((c - segment.start) as usize)?,
)?;
let g = be_u16(&self.data, gi_addr)?;
Some(if g == 0 {
0
} else {
g.wrapping_add(segment.id_delta)
})
}
fn cmap4_uncached_lookup(&self, c: u16) -> Option<u16> {
let d = &self.data;
let base = self.cmap_off;
let seg_x2 = be_u16(d, off(base, 6)?)? as usize;
let seg_count = seg_x2 / 2;
let end_codes = off(base, 14)?;
let start_codes = off(off(end_codes, seg_x2)?, 2)?; let id_deltas = off(start_codes, seg_x2)?;
let id_range_offsets = off(id_deltas, seg_x2)?;
for i in 0..seg_count {
let end = be_u16(d, off_mul(end_codes, i, 2)?)?;
if c > end {
continue;
}
let start = be_u16(d, off_mul(start_codes, i, 2)?)?;
if c < start {
return Some(0);
}
let id_delta = be_u16(d, off_mul(id_deltas, i, 2)?)?;
let iro_pos = off_mul(id_range_offsets, i, 2)?;
let id_range_offset = be_u16(d, iro_pos)?;
if id_range_offset == 0 {
return Some(c.wrapping_add(id_delta));
}
let gi_addr = off(
off(iro_pos, id_range_offset as usize)?,
2usize.checked_mul((c - start) as usize)?,
)?;
let g = be_u16(d, gi_addr)?;
return Some(if g == 0 { 0 } else { g.wrapping_add(id_delta) });
}
Some(0)
}
#[must_use]
pub fn subset(&self, keep: &[char]) -> Option<Vec<u8>> {
let seed: Vec<u16> = keep.iter().map(|&c| self.glyph_index(c)).collect();
self.subset_core(&seed, keep, true).map(|(bytes, _)| bytes)
}
pub fn subset_glyphs(
&self,
glyphs: &[u16],
cmap_chars: &[char],
) -> Option<(Vec<u8>, std::collections::BTreeMap<u16, u16>)> {
let (bytes, lookup) = self.subset_glyphs_with_lookup(glyphs, cmap_chars)?;
let mut new_of = std::collections::BTreeMap::new();
for (old, new) in lookup.into_iter().enumerate() {
if new != MISSING_GLYPH_REMAP {
new_of.insert(u16::try_from(old).ok()?, new);
}
}
Some((bytes, new_of))
}
pub fn subset_glyphs_with_lookup(
&self,
glyphs: &[u16],
cmap_chars: &[char],
) -> Option<(Vec<u8>, Vec<u16>)> {
self.subset_core(glyphs, cmap_chars, false)
}
fn subset_core(
&self,
seed_glyphs: &[u16],
cmap_chars: &[char],
include_os2: bool,
) -> Option<(Vec<u8>, Vec<u16>)> {
if !self.has_glyf_outlines() {
return None;
}
let mut set: std::collections::BTreeSet<u16> = std::collections::BTreeSet::new();
set.insert(0);
for &gid in seed_glyphs {
if gid != 0 && gid < self.num_glyphs {
set.insert(gid);
}
}
let mut worklist: Vec<u16> = set.iter().copied().collect();
while let Some(gid) = worklist.pop() {
if self.is_composite(gid) {
for c in self.glyph_components(gid) {
if c < self.num_glyphs && set.insert(c) {
worklist.push(c);
}
}
}
}
let old_gids: Vec<u16> = set.into_iter().collect(); let mut new_of_lookup = vec![MISSING_GLYPH_REMAP; usize::from(self.num_glyphs).max(1)];
for (i, &g) in old_gids.iter().enumerate() {
let new_gid = u16::try_from(i).ok()?;
*new_of_lookup.get_mut(usize::from(g))? = new_gid;
}
let n = old_gids.len();
let n_u16 = u16::try_from(n).ok()?;
let mut glyf_bytes: Vec<u8> = Vec::with_capacity(n.saturating_mul(64));
let mut loca_bytes: Vec<u8> = Vec::with_capacity(n.checked_add(1)?.checked_mul(4)?);
for &old in &old_gids {
let offset = u32::try_from(glyf_bytes.len()).ok()?;
loca_bytes.extend_from_slice(&offset.to_be_bytes());
let gb = self.subset_glyph_bytes(old, &new_of_lookup)?;
glyf_bytes.extend_from_slice(&gb);
let rem = glyf_bytes.len() % 4;
if rem != 0 {
glyf_bytes.resize(glyf_bytes.len() + (4 - rem), 0);
}
}
let final_offset = u32::try_from(glyf_bytes.len()).ok()?;
loca_bytes.extend_from_slice(&final_offset.to_be_bytes());
let (maxp_off, maxp_len) = find_table_full(&self.data, b"maxp")?;
let mut maxp = self.data.get(maxp_off..off(maxp_off, maxp_len)?)?.to_vec();
write_u16(&mut maxp, 4, n_u16)?;
let (hhea_off, hhea_len) = find_table_full(&self.data, b"hhea")?;
let mut hhea = self.data.get(hhea_off..off(hhea_off, hhea_len)?)?.to_vec();
write_u16(&mut hhea, 34, n_u16)?;
let mut hmtx: Vec<u8> = Vec::with_capacity(n.checked_mul(4)?);
for &old in &old_gids {
let [a0, a1] = self.advance_width(old).to_be_bytes();
let [l0, l1] = self.left_side_bearing(old).to_be_bytes();
hmtx.extend_from_slice(&[a0, a1, l0, l1]);
}
let (head_off, head_len) = find_table_full(&self.data, b"head")?;
let mut head = self.data.get(head_off..off(head_off, head_len)?)?.to_vec();
write_u32(&mut head, 8, 0)?;
write_u16(&mut head, 50, 1)?;
let cmap = if self.subset_reaches_supplementary_plane(cmap_chars, &new_of_lookup) {
self.build_cmap12(cmap_chars, &new_of_lookup)?
} else {
self.build_cmap4(cmap_chars, &new_of_lookup)?
};
let mut name: Vec<u8> = Vec::with_capacity(6);
name.extend_from_slice(&0u16.to_be_bytes());
name.extend_from_slice(&0u16.to_be_bytes());
name.extend_from_slice(&6u16.to_be_bytes());
let mut post: Vec<u8> = Vec::with_capacity(32);
post.extend_from_slice(&0x0003_0000u32.to_be_bytes()); post.extend_from_slice(&0u32.to_be_bytes()); post.extend_from_slice(&0u16.to_be_bytes()); post.extend_from_slice(&0u16.to_be_bytes()); post.extend_from_slice(&0u32.to_be_bytes()); post.extend_from_slice(&0u32.to_be_bytes()); post.extend_from_slice(&0u32.to_be_bytes()); post.extend_from_slice(&0u32.to_be_bytes()); post.extend_from_slice(&0u32.to_be_bytes());
let os2: Option<Vec<u8>> = if include_os2 {
find_table_full(&self.data, b"OS/2")
.and_then(|(o, l)| Some(self.data.get(o..off(o, l)?)?.to_vec()))
} else {
None
};
let mut tables: Vec<(&[u8; 4], Vec<u8>)> = vec![
(b"head", head),
(b"hhea", hhea),
(b"maxp", maxp),
(b"hmtx", hmtx),
(b"loca", loca_bytes),
(b"glyf", glyf_bytes),
(b"cmap", cmap),
(b"name", name),
(b"post", post),
];
if let Some(os2) = os2 {
tables.push((b"OS/2", os2));
}
tables.sort_by(|a, b| a.0.cmp(b.0));
let num_tables = tables.len();
let mut pw: usize = 1;
let mut es: u16 = 0;
while pw * 2 <= num_tables {
pw *= 2;
es += 1;
}
let search_range = (pw as u16).wrapping_mul(16);
let entry_selector = es;
let range_shift = (num_tables as u16)
.wrapping_mul(16)
.wrapping_sub(search_range);
let dir_size = 12 + num_tables * 16;
let mut body: Vec<u8> = Vec::new();
let mut records: Vec<([u8; 4], u32, u32, u32)> = Vec::with_capacity(num_tables);
let mut head_offset: usize = 0;
for (tag, bytes) in &tables {
while (dir_size + body.len()) % 4 != 0 {
body.push(0);
}
let table_offset = dir_size + body.len();
if *tag == b"head" {
head_offset = table_offset;
}
let checksum = table_checksum(bytes);
records.push((
**tag,
checksum,
u32::try_from(table_offset).ok()?,
u32::try_from(bytes.len()).ok()?,
));
body.extend_from_slice(bytes);
}
while body.len() % 4 != 0 {
body.push(0);
}
let mut out: Vec<u8> = Vec::with_capacity(dir_size + body.len());
out.extend_from_slice(&0x0001_0000u32.to_be_bytes()); out.extend_from_slice(&(num_tables as u16).to_be_bytes());
out.extend_from_slice(&search_range.to_be_bytes());
out.extend_from_slice(&entry_selector.to_be_bytes());
out.extend_from_slice(&range_shift.to_be_bytes());
for (tag, checksum, toff, tlen) in &records {
out.extend_from_slice(tag);
out.extend_from_slice(&checksum.to_be_bytes());
out.extend_from_slice(&toff.to_be_bytes());
out.extend_from_slice(&tlen.to_be_bytes());
}
out.extend_from_slice(&body);
let file_checksum = table_checksum(&out);
let adj = 0xB1B0_AFBAu32.wrapping_sub(file_checksum);
write_u32(&mut out, off(head_offset, 8)?, adj)?;
Some((out, new_of_lookup))
}
fn subset_glyph_bytes(&self, old: u16, new_of: &[u16]) -> Option<Vec<u8>> {
const ARG_WORDS: u16 = 0x0001;
const WE_HAVE_SCALE: u16 = 0x0008;
const MORE: u16 = 0x0020;
const X_Y_SCALE: u16 = 0x0040;
const TWO_BY_TWO: u16 = 0x0080;
const WE_HAVE_INSTRUCTIONS: u16 = 0x0100;
let data = self.glyph_data(old).unwrap_or(&[]);
if data.is_empty() {
return Some(Vec::new());
}
let num_contours = be_i16(data, 0)?;
if num_contours >= 0 {
return strip_simple_glyph_instructions(data, num_contours as usize);
}
let mut out = data.to_vec();
let mut p = 10usize; let mut instruction_flags_positions = Vec::new();
let mut dangling_more = false;
loop {
let last_flags_pos = p;
let flags = be_u16(&out, p)?;
if flags & WE_HAVE_INSTRUCTIONS != 0 {
instruction_flags_positions.push(p);
}
let comp_old = be_u16_at(&out, p, 2)?;
let comp_new = remapped_gid(new_of, comp_old).unwrap_or(0);
let nb = comp_new.to_be_bytes();
*out.get_mut(off(p, 2)?)? = nb[0];
*out.get_mut(off(p, 3)?)? = nb[1];
p = off(p, 4)?;
p = off(p, if flags & ARG_WORDS != 0 { 4 } else { 2 })?;
if flags & WE_HAVE_SCALE != 0 {
p = off(p, 2)?;
} else if flags & X_Y_SCALE != 0 {
p = off(p, 4)?;
} else if flags & TWO_BY_TWO != 0 {
p = off(p, 8)?;
}
if flags & MORE == 0 {
break;
}
if off(p, 4).is_none_or(|end| end > out.len()) {
write_u16(
&mut out,
last_flags_pos,
flags & !(MORE | WE_HAVE_INSTRUCTIONS),
)?;
dangling_more = true;
break;
}
}
if dangling_more {
for flags_pos in &instruction_flags_positions {
let flags = be_u16(&out, *flags_pos)?;
write_u16(&mut out, *flags_pos, flags & !WE_HAVE_INSTRUCTIONS)?;
}
} else if !instruction_flags_positions.is_empty() {
for flags_pos in instruction_flags_positions {
let flags = be_u16(&out, flags_pos)?;
write_u16(&mut out, flags_pos, flags & !WE_HAVE_INSTRUCTIONS)?;
}
let instruction_len = be_u16(&out, p)? as usize;
let instruction_start = off(p, 2)?;
let instruction_end = off(instruction_start, instruction_len)?;
if instruction_end > out.len() {
return None;
}
out.drain(p..instruction_end);
}
Some(out)
}
fn build_cmap4(&self, keep: &[char], new_of: &[u16]) -> Option<Vec<u8>> {
let mut codes: std::collections::BTreeMap<u16, u16> = std::collections::BTreeMap::new();
for &ch in keep {
let cp = ch as u32;
if cp >= 0xFFFF {
continue;
}
let old = self.glyph_index(ch);
let Some(ng) = remapped_gid(new_of, old) else {
continue;
};
codes.insert(cp as u16, ng);
}
let entries: Vec<(u16, u16)> = codes.into_iter().collect();
let seg_count = entries.len().checked_add(1)?; let sub_len = 16usize.checked_add(seg_count.checked_mul(8)?)?;
let sub_len_u16 = u16::try_from(sub_len).ok()?;
let seg_count_x2 = u16::try_from(seg_count.checked_mul(2)?).ok()?;
let mut pw: usize = 1;
let mut es: u16 = 0;
while pw * 2 <= seg_count {
pw *= 2;
es += 1;
}
let search_range = u16::try_from(pw.checked_mul(2)?).ok()?;
let entry_selector = es;
let range_shift = seg_count_x2.checked_sub(search_range)?;
let mut sub: Vec<u8> = Vec::with_capacity(sub_len);
sub.extend_from_slice(&4u16.to_be_bytes()); sub.extend_from_slice(&sub_len_u16.to_be_bytes()); sub.extend_from_slice(&0u16.to_be_bytes()); sub.extend_from_slice(&seg_count_x2.to_be_bytes()); sub.extend_from_slice(&search_range.to_be_bytes());
sub.extend_from_slice(&entry_selector.to_be_bytes());
sub.extend_from_slice(&range_shift.to_be_bytes());
for &(code, _) in &entries {
sub.extend_from_slice(&code.to_be_bytes());
}
sub.extend_from_slice(&0xFFFFu16.to_be_bytes());
sub.extend_from_slice(&0u16.to_be_bytes());
for &(code, _) in &entries {
sub.extend_from_slice(&code.to_be_bytes());
}
sub.extend_from_slice(&0xFFFFu16.to_be_bytes());
for &(code, ng) in &entries {
sub.extend_from_slice(&ng.wrapping_sub(code).to_be_bytes());
}
sub.extend_from_slice(&1u16.to_be_bytes());
for _ in &entries {
sub.extend_from_slice(&0u16.to_be_bytes());
}
sub.extend_from_slice(&0u16.to_be_bytes());
let mut cmap: Vec<u8> = Vec::with_capacity(12 + sub.len());
cmap.extend_from_slice(&0u16.to_be_bytes()); cmap.extend_from_slice(&1u16.to_be_bytes()); cmap.extend_from_slice(&3u16.to_be_bytes()); cmap.extend_from_slice(&1u16.to_be_bytes()); cmap.extend_from_slice(&12u32.to_be_bytes()); cmap.extend_from_slice(&sub);
Some(cmap)
}
fn subset_reaches_supplementary_plane(&self, keep: &[char], new_of: &[u16]) -> bool {
keep.iter().any(|&ch| {
(ch as u32) >= 0x1_0000 && remapped_gid(new_of, self.glyph_index(ch)).is_some()
})
}
fn build_cmap12(&self, keep: &[char], new_of: &[u16]) -> Option<Vec<u8>> {
let mut codes: std::collections::BTreeMap<u32, u16> = std::collections::BTreeMap::new();
for &ch in keep {
let old = self.glyph_index(ch);
let Some(ng) = remapped_gid(new_of, old) else {
continue;
};
codes.insert(u32::from(ch), ng);
}
struct Group {
start_cp: u32,
end_cp: u32,
start_gid: u16,
}
let mut groups: Vec<Group> = Vec::with_capacity(codes.len());
for (&cp, &ng) in &codes {
match groups.last_mut() {
Some(g)
if g.end_cp.checked_add(1) == Some(cp)
&& u64::from(g.start_gid) + (g.end_cp - g.start_cp) as u64 + 1
== u64::from(ng) =>
{
g.end_cp = cp;
}
_ => groups.push(Group {
start_cp: cp,
end_cp: cp,
start_gid: ng,
}),
}
}
let sub_len = 16usize.checked_add(groups.len().checked_mul(12)?)?;
if sub_len > u32::MAX as usize {
return None;
}
let mut sub: Vec<u8> = Vec::with_capacity(sub_len);
sub.extend_from_slice(&12u16.to_be_bytes()); sub.extend_from_slice(&0u16.to_be_bytes()); sub.extend_from_slice(&(sub_len as u32).to_be_bytes()); sub.extend_from_slice(&0u32.to_be_bytes()); sub.extend_from_slice(&(groups.len() as u32).to_be_bytes()); for g in &groups {
sub.extend_from_slice(&g.start_cp.to_be_bytes());
sub.extend_from_slice(&g.end_cp.to_be_bytes());
sub.extend_from_slice(&u32::from(g.start_gid).to_be_bytes());
}
let mut cmap: Vec<u8> = Vec::with_capacity(12 + sub.len());
cmap.extend_from_slice(&0u16.to_be_bytes()); cmap.extend_from_slice(&1u16.to_be_bytes()); cmap.extend_from_slice(&3u16.to_be_bytes()); cmap.extend_from_slice(&10u16.to_be_bytes()); cmap.extend_from_slice(&12u32.to_be_bytes()); cmap.extend_from_slice(&sub);
Some(cmap)
}
fn cmap12_lookup(&self, cp: u32) -> Option<u16> {
let d = &self.data;
let base = self.cmap_off;
let num_groups = be_u32(d, off(base, 12)?)? as usize;
for i in 0..num_groups {
let g = off_mul(off(base, 16)?, i, 12)?;
let start = be_u32(d, g)?;
let end = be_u32(d, off(g, 4)?)?;
if cp >= start && cp <= end {
let start_gid = be_u32(d, off(g, 8)?)?;
let gid = start_gid.checked_add(cp - start)?;
return Some((gid & 0xFFFF) as u16);
}
}
Some(0)
}
}
fn remapped_gid(new_of: &[u16], old: u16) -> Option<u16> {
match new_of.get(usize::from(old)).copied()? {
MISSING_GLYPH_REMAP => None,
gid => Some(gid),
}
}
fn strip_simple_glyph_instructions(data: &[u8], contour_count: usize) -> Option<Vec<u8>> {
let instruction_len_offset = off(10, contour_count.checked_mul(2)?)?;
let instruction_len = be_u16(data, instruction_len_offset)? as usize;
let instruction_start = off(instruction_len_offset, 2)?;
let instruction_end = off(instruction_start, instruction_len)?;
if instruction_end > data.len() {
return None;
}
let mut out = Vec::with_capacity(data.len().saturating_sub(instruction_len));
out.extend_from_slice(data.get(..instruction_len_offset)?);
out.extend_from_slice(&0u16.to_be_bytes());
out.extend_from_slice(data.get(instruction_end..)?);
Some(out)
}
fn parse_cmap4_cache(d: &[u8], base: usize) -> Option<Cmap4Cache> {
let seg_x2 = be_u16(d, off(base, 6)?)? as usize;
let seg_count = seg_x2 / 2;
let end_codes = off(base, 14)?;
let start_codes = off(off(end_codes, seg_x2)?, 2)?;
let id_deltas = off(start_codes, seg_x2)?;
let id_range_offsets = off(id_deltas, seg_x2)?;
let mut segments = Vec::with_capacity(seg_count);
let mut sorted_by_end = true;
let mut prev_end: Option<u16> = None;
for i in 0..seg_count {
let end = be_u16(d, off_mul(end_codes, i, 2)?)?;
let start = be_u16(d, off_mul(start_codes, i, 2)?)?;
let id_delta = be_u16(d, off_mul(id_deltas, i, 2)?)?;
let id_range_offset_pos = off_mul(id_range_offsets, i, 2)?;
let id_range_offset = be_u16(d, id_range_offset_pos)?;
if prev_end.is_some_and(|prev| end < prev) {
sorted_by_end = false;
}
prev_end = Some(end);
segments.push(Cmap4Segment {
start,
end,
id_delta,
id_range_offset,
id_range_offset_pos,
});
}
Some(Cmap4Cache {
segments,
sorted_by_end,
})
}
fn select_cmap(d: &[u8], cmap: usize) -> Option<(usize, u16)> {
let num = be_u16(d, off(cmap, 2)?)? as usize;
let mut best: Option<(usize, u16, u8)> = None; for i in 0..num {
let rec = off_mul(off(cmap, 4)?, i, 8)?;
let platform = be_u16(d, rec)?;
let encoding = be_u16(d, off(rec, 2)?)?;
let sub = off(cmap, be_u32(d, off(rec, 4)?)? as usize)?;
let format = be_u16(d, sub)?;
let unicode = matches!((platform, encoding), (0, _) | (3, 1) | (3, 10));
if !unicode {
continue;
}
let rank = match format {
12 => 3,
4 => {
if (platform, encoding) == (3, 1) || platform == 0 {
2
} else {
1
}
}
_ => continue,
};
if best.is_none_or(|(_, _, r)| rank > r) {
best = Some((sub, format, rank));
}
}
best.map(|(off, fmt, _)| (off, fmt))
}
#[derive(Default)]
struct PairKeyHasher(u64);
impl std::hash::Hasher for PairKeyHasher {
fn write_u32(&mut self, v: u32) {
self.0 = u64::from(v).wrapping_mul(0x9E37_79B9_7F4A_7C15);
}
fn write(&mut self, _bytes: &[u8]) {
}
fn finish(&self) -> u64 {
let mut x = self.0;
x ^= x >> 29;
x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9);
x ^= x >> 32;
x
}
}
type PairMap = std::collections::HashMap<u32, i16, std::hash::BuildHasherDefault<PairKeyHasher>>;
fn pair_key(left: u16, right: u16) -> u32 {
(u32::from(left) << 16) | u32::from(right)
}
#[derive(Clone, Debug)]
enum ClassDef {
Format1 { start: u16, classes: Vec<u16> },
Format2 {
ranges: Vec<(u16, u16, u16)>,
dense: bool,
},
}
impl ClassDef {
fn class(&self, g: u16) -> u16 {
match self {
ClassDef::Format1 { start, classes } => {
if g >= *start {
let i = (g - *start) as usize;
if i < classes.len() {
return classes[i];
}
}
0
}
ClassDef::Format2 { ranges, dense } => {
if *dense {
let idx = ranges.partition_point(|&(s, _, _)| s <= g);
if idx > 0 {
let (_, e, c) = ranges[idx - 1];
if g <= e {
return c;
}
}
0
} else {
for &(s, e, c) in ranges {
if g >= s && g <= e {
return c;
}
}
0
}
}
}
}
}
#[derive(Clone, Debug)]
enum KernSubtable {
Format1 { pairs: PairMap },
Format2 {
coverage: Vec<u16>,
class1: ClassDef,
class2: ClassDef,
class1_count: u16,
class2_count: u16,
matrix: Vec<i16>,
},
}
impl KernSubtable {
fn lookup(&self, left: u16, right: u16) -> Option<i16> {
match self {
KernSubtable::Format1 { pairs } => pairs.get(&pair_key(left, right)).copied(),
KernSubtable::Format2 {
coverage,
class1,
class2,
class1_count,
class2_count,
matrix,
} => {
if coverage.binary_search(&left).is_err() {
return None;
}
let c1 = class1.class(left) as usize;
let c2 = class2.class(right) as usize;
let c1_count = *class1_count as usize;
let c2_count = *class2_count as usize;
if c1 >= c1_count || c2 >= c2_count {
return Some(0);
}
if matrix.is_empty() {
return Some(0);
}
let idx = c1.checked_mul(c2_count)?.checked_add(c2)?;
Some(matrix.get(idx).copied().unwrap_or(0))
}
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Kerning {
subtables: Vec<KernSubtable>,
}
impl Kerning {
#[must_use]
pub fn pair(&self, left: u16, right: u16) -> i16 {
for st in &self.subtables {
if let Some(v) = st.lookup(left, right) {
return v;
}
}
0
}
pub fn for_each_ascii_pair(
&self,
glyph_of: impl Fn(u8) -> u16,
mut emit: impl FnMut(u8, u8, i16),
) {
let glyphs: [u16; 128] = std::array::from_fn(|b| glyph_of(b as u8));
let mut by_glyph: Vec<(u16, u8)> = glyphs
.iter()
.enumerate()
.map(|(b, &g)| (g, b as u8))
.collect();
by_glyph.sort_unstable();
let mut defined = [[false; 128]; 128];
for st in &self.subtables {
match st {
KernSubtable::Format1 { pairs } => {
for (&key, &v) in pairs {
let left = (key >> 16) as u16;
let right = (key & 0xFFFF) as u16;
let ls = by_glyph.partition_point(|&(g, _)| g < left);
let le = by_glyph.partition_point(|&(g, _)| g <= left);
let rs = by_glyph.partition_point(|&(g, _)| g < right);
let re = by_glyph.partition_point(|&(g, _)| g <= right);
for &(_, l) in &by_glyph[ls..le] {
for &(_, r) in &by_glyph[rs..re] {
let row = &mut defined[usize::from(l)];
if row[usize::from(r)] {
continue;
}
row[usize::from(r)] = true;
if v != 0 {
emit(l, r, v);
}
}
}
}
}
KernSubtable::Format2 {
coverage,
class1,
class2,
class1_count,
class2_count,
matrix,
} => {
let c2_of: [u16; 128] = std::array::from_fn(|b| class2.class(glyphs[b]));
let c1_count = usize::from(*class1_count);
let c2_count = usize::from(*class2_count);
for l in 0..128u8 {
if coverage.binary_search(&glyphs[usize::from(l)]).is_err() {
continue;
}
let c1 = usize::from(class1.class(glyphs[usize::from(l)]));
for r in 0..128u8 {
let row = &mut defined[usize::from(l)];
if row[usize::from(r)] {
continue;
}
let value = if c1 >= c1_count
|| usize::from(c2_of[usize::from(r)]) >= c2_count
|| matrix.is_empty()
{
Some(0)
} else {
c1.checked_mul(c2_count)
.and_then(|m| m.checked_add(usize::from(c2_of[usize::from(r)])))
.map(|idx| matrix.get(idx).copied().unwrap_or(0))
};
if let Some(v) = value {
row[usize::from(r)] = true;
if v != 0 {
emit(l, r, v);
}
}
}
}
}
}
}
}
}
fn value_record_size(value_format: u16) -> usize {
value_format.count_ones() as usize * 2
}
fn value_record_x_advance(d: &[u8], off: usize, value_format: u16) -> Option<i16> {
const X_ADVANCE: u16 = 0x0004;
if value_format & X_ADVANCE == 0 {
return Some(0);
}
let skip = (value_format & 0x0003).count_ones() as usize * 2;
be_i16(d, off.checked_add(skip)?)
}
fn parse_coverage_glyphs(d: &[u8], cov: usize) -> Option<Vec<u16>> {
let format = be_u16(d, cov)?;
match format {
1 => {
let count = be_u16_at(d, cov, 2)? as usize;
let mut v = Vec::with_capacity(count.min(d.len() / 2 + 1));
for i in 0..count {
v.push(be_u16(d, off_mul(off(cov, 4)?, i, 2)?)?);
}
Some(v)
}
2 => {
let range_count = be_u16_at(d, cov, 2)? as usize;
let mut by_index: std::collections::BTreeMap<u32, u16> =
std::collections::BTreeMap::new();
let mut total: usize = 0;
for i in 0..range_count {
let rec = off_mul(off(cov, 4)?, i, 6)?;
let start = be_u16(d, rec)? as u32;
let end = be_u16_at(d, rec, 2)? as u32;
let start_idx = be_u16_at(d, rec, 4)? as u32;
if end < start {
continue;
}
total = total.checked_add((end - start + 1) as usize)?;
if total > MAX_COVERAGE_GLYPHS {
return None;
}
let mut g = start;
let mut idx = start_idx;
while g <= end {
by_index.insert(idx, g as u16);
g += 1;
idx += 1;
}
}
Some(by_index.into_values().collect())
}
_ => None,
}
}
fn parse_class_def(d: &[u8], cd: usize) -> Option<ClassDef> {
let format = be_u16(d, cd)?;
match format {
1 => {
let start = be_u16_at(d, cd, 2)?;
let count = be_u16_at(d, cd, 4)? as usize;
let mut classes = Vec::with_capacity(count.min(d.len() / 2 + 1));
for i in 0..count {
classes.push(be_u16(d, off_mul(off(cd, 6)?, i, 2)?)?);
}
Some(ClassDef::Format1 { start, classes })
}
2 => {
let range_count = be_u16_at(d, cd, 2)? as usize;
let mut ranges = Vec::with_capacity(range_count.min(d.len() / 6 + 1));
for i in 0..range_count {
let rec = off_mul(off(cd, 4)?, i, 6)?;
let s = be_u16(d, rec)?;
let e = be_u16_at(d, rec, 2)?;
let c = be_u16_at(d, rec, 4)?;
ranges.push((s, e, c));
}
ranges.sort_by_key(|&(s, _, _)| s);
let dense = ranges
.windows(2)
.all(|w| w[0].1 < w[1].0 || (w[0].1 == w[1].0 && w[0].2 == w[1].2));
Some(ClassDef::Format2 { ranges, dense })
}
_ => None,
}
}
fn parse_pair_subtable(d: &[u8], sub: usize) -> Option<KernSubtable> {
let pos_format = be_u16(d, sub)?;
match pos_format {
1 => parse_pair_format1(d, sub),
2 => parse_pair_format2(d, sub),
_ => None,
}
}
fn parse_pair_format1(d: &[u8], sub: usize) -> Option<KernSubtable> {
let cov_off = be_u16_at(d, sub, 2)? as usize;
let vf1 = be_u16_at(d, sub, 4)?;
let vf2 = be_u16_at(d, sub, 6)?;
let pair_set_count = be_u16_at(d, sub, 8)? as usize;
let rec1_size = value_record_size(vf1);
let rec2_size = value_record_size(vf2);
let pair_rec_size = off(2, off(rec1_size, rec2_size)?)?;
let coverage = parse_coverage_glyphs(d, off(sub, cov_off)?)?;
let mut pairs: PairMap = std::collections::HashMap::default();
let mut work: usize = 0;
for i in 0..pair_set_count {
work += 1;
if work > MAX_LAYOUT_GLYPHS {
break;
}
let Some(left_glyph) = coverage.get(i).copied() else {
continue;
};
let Some(ps_off) = off_mul(off(sub, 10)?, i, 2).and_then(|slot| be_u16(d, slot)) else {
continue;
};
let Some(ps) = off(sub, ps_off as usize) else {
continue;
};
let Some(pair_value_count) = be_u16(d, ps) else {
continue;
};
let Some(mut p) = off(ps, 2) else {
continue;
};
for _ in 0..pair_value_count {
work += 1;
if work > MAX_LAYOUT_GLYPHS {
break;
}
let Some(second) = be_u16(d, p) else {
break;
};
let x_adv = off(p, 2)
.and_then(|value_off| value_record_x_advance(d, value_off, vf1))
.unwrap_or(0);
pairs.entry(pair_key(left_glyph, second)).or_insert(x_adv);
let Some(np) = p.checked_add(pair_rec_size) else {
break;
};
p = np;
}
}
Some(KernSubtable::Format1 { pairs })
}
fn parse_pair_format2(d: &[u8], sub: usize) -> Option<KernSubtable> {
let cov_off = be_u16_at(d, sub, 2)? as usize;
let vf1 = be_u16_at(d, sub, 4)?;
let vf2 = be_u16_at(d, sub, 6)?;
let class_def1_off = be_u16_at(d, sub, 8)? as usize;
let class_def2_off = be_u16_at(d, sub, 10)? as usize;
let class1_count = be_u16_at(d, sub, 12)? as usize;
let class2_count = be_u16_at(d, sub, 14)? as usize;
let rec1_size = value_record_size(vf1);
let rec2_size = value_record_size(vf2);
let class_rec_size = off(rec1_size, rec2_size)?;
let matrix_base = off(sub, 16)?;
let cell_count = class1_count.checked_mul(class2_count)?;
let matrix: Vec<i16> = if class_rec_size == 0 {
Vec::new()
} else {
let needed = cell_count.checked_mul(class_rec_size)?;
let end = matrix_base.checked_add(needed)?;
if end > d.len() {
return None;
}
let mut m = Vec::with_capacity(cell_count);
for idx in 0..cell_count {
let cell = off_mul(matrix_base, idx, class_rec_size)?;
let x_adv = value_record_x_advance(d, cell, vf1).unwrap_or(0);
m.push(x_adv);
}
m
};
let mut coverage = parse_coverage_glyphs(d, off(sub, cov_off)?)?;
coverage.sort_unstable();
let class1 = parse_class_def(d, off(sub, class_def1_off)?)?;
let class2 = parse_class_def(d, off(sub, class_def2_off)?)?;
Some(KernSubtable::Format2 {
coverage,
class1,
class2,
class1_count: class1_count as u16,
class2_count: class2_count as u16,
matrix,
})
}
fn resolve_extension(d: &[u8], sub: usize) -> Option<(u16, usize)> {
let pos_format = be_u16(d, sub)?;
if pos_format != 1 {
return None;
}
let ext_type = be_u16_at(d, sub, 2)?;
let ext_off = be_u32_at(d, sub, 4)? as usize;
Some((ext_type, sub.checked_add(ext_off)?))
}
impl Font {
#[must_use]
pub fn gpos_kerning(&self) -> Kerning {
self.parse_gpos_kerning().unwrap_or_default()
}
fn parse_gpos_kerning(&self) -> Option<Kerning> {
let d = &self.data;
let (gpos, _gpos_len) = find_table_full(d, b"GPOS")?;
let feature_list_off = be_u16_at(d, gpos, 6)? as usize;
let lookup_list_off = be_u16_at(d, gpos, 8)? as usize;
let feature_list = off(gpos, feature_list_off)?;
let lookup_list = off(gpos, lookup_list_off)?;
let feature_count = be_u16(d, feature_list)? as usize;
let mut lookup_indices: Vec<u16> = Vec::new();
for i in 0..feature_count {
let rec = off_mul(off(feature_list, 2)?, i, 6)?;
let Some(tag) = bytes_at(d, rec, 4) else {
break;
};
if tag != b"kern" {
continue;
}
let Some(feat_off) = be_u16_at(d, rec, 4) else {
continue;
};
let Some(feat) = off(feature_list, feat_off as usize) else {
continue;
};
let Some(lookup_index_count) = be_u16_at(d, feat, 2) else {
continue;
};
for j in 0..lookup_index_count as usize {
if let Some(idx) = off_mul(off(feat, 4)?, j, 2).and_then(|slot| be_u16(d, slot)) {
if !lookup_indices.contains(&idx) {
lookup_indices.push(idx);
}
}
}
}
let lookup_count = be_u16(d, lookup_list)? as usize;
let mut subtables: Vec<KernSubtable> = Vec::new();
for &li in &lookup_indices {
let li = li as usize;
if li >= lookup_count {
continue;
}
let Some(lookup_off) =
off_mul(off(lookup_list, 2)?, li, 2).and_then(|slot| be_u16(d, slot))
else {
continue;
};
let Some(lookup) = off(lookup_list, lookup_off as usize) else {
continue;
};
let Some(lookup_type) = be_u16(d, lookup) else {
continue;
};
let Some(sub_count) = be_u16_at(d, lookup, 4) else {
continue;
};
for s in 0..sub_count as usize {
let Some(sub_off) = off_mul(off(lookup, 6)?, s, 2).and_then(|slot| be_u16(d, slot))
else {
continue;
};
let Some(sub) = off(lookup, sub_off as usize) else {
continue;
};
match lookup_type {
2 => {
if let Some(st) = parse_pair_subtable(d, sub) {
subtables.push(st);
}
}
9 => {
if let Some((ext_type, real_sub)) = resolve_extension(d, sub) {
if ext_type == 2 {
if let Some(st) = parse_pair_subtable(d, real_sub) {
subtables.push(st);
}
}
}
}
_ => {}
}
}
}
Some(Kerning { subtables })
}
}
#[derive(Clone, Debug)]
struct LigRule {
components: Vec<u16>,
ligature: u16,
}
#[derive(Clone, Debug, Default)]
pub struct Ligatures {
rules: std::collections::BTreeMap<u16, Vec<LigRule>>,
}
impl Ligatures {
#[must_use]
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
pub fn rule_start_glyphs(&self) -> impl Iterator<Item = &u16> {
self.rules.keys()
}
#[must_use]
pub fn max_rule_len(&self) -> usize {
self.rules
.values()
.flat_map(|rules| rules.iter().map(|r| r.components.len() + 1))
.max()
.unwrap_or(1)
}
#[must_use]
pub fn substitute(&self, gids: &[u16]) -> Vec<u16> {
self.substitute_with_spans(gids)
.into_iter()
.map(|(g, _)| g)
.collect()
}
#[must_use]
pub fn substitute_with_spans(&self, gids: &[u16]) -> Vec<(u16, usize)> {
let mut out = Vec::with_capacity(gids.len());
self.substitute_with_spans_into(gids, &mut out);
out
}
pub fn substitute_with_spans_into(&self, gids: &[u16], out: &mut Vec<(u16, usize)>) {
out.clear();
out.reserve(gids.len());
let mut i = 0;
while i < gids.len() {
let mut applied = false;
if let Some(rules) = self.rules.get(&gids[i]) {
for r in rules {
let n = r.components.len();
if i + 1 + n <= gids.len() && gids[i + 1..i + 1 + n] == r.components[..] {
out.push((r.ligature, n + 1));
i += n + 1;
applied = true;
break;
}
}
}
if !applied {
out.push((gids[i], 1));
i += 1;
}
}
}
}
impl Font {
#[must_use]
pub fn gsub_ligatures(&self) -> Ligatures {
self.parse_gsub_ligatures().unwrap_or_default()
}
fn parse_gsub_ligatures(&self) -> Option<Ligatures> {
let d = &self.data;
let (gsub, _) = find_table_full(d, b"GSUB")?;
let feature_list = off(gsub, be_u16_at(d, gsub, 6)? as usize)?;
let lookup_list = off(gsub, be_u16_at(d, gsub, 8)? as usize)?;
let feature_count = be_u16(d, feature_list)? as usize;
let mut lookup_indices: Vec<u16> = Vec::new();
for i in 0..feature_count {
let rec = off_mul(off(feature_list, 2)?, i, 6)?;
let Some(tag) = bytes_at(d, rec, 4) else {
break;
};
if tag != b"liga" {
continue;
}
let Some(feat_off) = be_u16_at(d, rec, 4) else {
continue;
};
let Some(feat) = off(feature_list, feat_off as usize) else {
continue;
};
let Some(n) = be_u16_at(d, feat, 2) else {
continue;
};
for j in 0..n as usize {
if let Some(idx) = off_mul(off(feat, 4)?, j, 2).and_then(|slot| be_u16(d, slot)) {
if !lookup_indices.contains(&idx) {
lookup_indices.push(idx);
}
}
}
}
let lookup_count = be_u16(d, lookup_list)? as usize;
let mut rules: std::collections::BTreeMap<u16, Vec<LigRule>> =
std::collections::BTreeMap::new();
for &li in &lookup_indices {
let li = li as usize;
if li >= lookup_count {
continue;
}
let Some(lookup_off) =
off_mul(off(lookup_list, 2)?, li, 2).and_then(|slot| be_u16(d, slot))
else {
continue;
};
let Some(lookup) = off(lookup_list, lookup_off as usize) else {
continue;
};
let Some(lookup_type) = be_u16(d, lookup) else {
continue;
};
let Some(sub_count) = be_u16_at(d, lookup, 4) else {
continue;
};
for s in 0..sub_count as usize {
let Some(sub_off) = off_mul(off(lookup, 6)?, s, 2).and_then(|slot| be_u16(d, slot))
else {
continue;
};
let Some(sub) = off(lookup, sub_off as usize) else {
continue;
};
match lookup_type {
4 => parse_ligature_subst(d, sub, &mut rules),
7 => {
if let Some((ext_type, real)) = resolve_extension(d, sub) {
if ext_type == 4 {
parse_ligature_subst(d, real, &mut rules);
}
}
}
_ => {}
}
}
}
for v in rules.values_mut() {
v.sort_by_key(|r| std::cmp::Reverse(r.components.len()));
}
Some(Ligatures { rules })
}
}
fn parse_ligature_subst(
d: &[u8],
sub: usize,
rules: &mut std::collections::BTreeMap<u16, Vec<LigRule>>,
) {
let Some(format) = be_u16(d, sub) else {
return;
};
if format != 1 {
return;
}
let Some(cov_off) = be_u16_at(d, sub, 2) else {
return;
};
let Some(set_count) = be_u16_at(d, sub, 4) else {
return;
};
let Some(coverage) = off(sub, cov_off as usize).and_then(|cov| parse_coverage_glyphs(d, cov))
else {
return;
};
let Some(set_offsets) = off(sub, 6) else {
return;
};
let mut work: usize = 0;
for i in 0..set_count as usize {
work += 1;
if work > MAX_LAYOUT_GLYPHS {
return;
}
let Some(first) = coverage.get(i).copied() else {
continue;
};
let Some(set_off) = off_mul(set_offsets, i, 2).and_then(|slot| be_u16(d, slot)) else {
continue;
};
let Some(lig_set) = off(sub, set_off as usize) else {
continue;
};
let Some(lig_count) = be_u16(d, lig_set) else {
continue;
};
let Some(lig_offsets) = off(lig_set, 2) else {
continue;
};
for j in 0..lig_count as usize {
work += 1;
if work > MAX_LAYOUT_GLYPHS {
return;
}
let Some(lig_off) = off_mul(lig_offsets, j, 2).and_then(|slot| be_u16(d, slot)) else {
continue;
};
let Some(lig) = off(lig_set, lig_off as usize) else {
continue;
};
let Some(lig_glyph) = be_u16(d, lig) else {
continue;
};
let Some(comp_count) = be_u16_at(d, lig, 2) else {
continue;
};
if comp_count == 0 {
continue;
}
let mut components = Vec::with_capacity(comp_count as usize - 1);
let mut ok = true;
let Some(component_base) = off(lig, 4) else {
continue;
};
for k in 0..(comp_count as usize - 1) {
match off_mul(component_base, k, 2).and_then(|slot| be_u16(d, slot)) {
Some(g) => components.push(g),
None => {
ok = false;
break;
}
}
}
if ok {
rules.entry(first).or_default().push(LigRule {
components,
ligature: lig_glyph,
});
}
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
#[allow(clippy::indexing_slicing, clippy::unwrap_used)]
mod dos_tests {
use super::{MAX_COVERAGE_GLYPHS, parse_coverage_glyphs};
fn be(v: u16) -> [u8; 2] {
v.to_be_bytes()
}
#[test]
fn coverage_format2_valid_range_expands() {
let mut d = Vec::new();
d.extend_from_slice(&be(2));
d.extend_from_slice(&be(1));
d.extend_from_slice(&be(10)); d.extend_from_slice(&be(20)); d.extend_from_slice(&be(0)); let got = parse_coverage_glyphs(&d, 0).unwrap();
assert_eq!(got, (10u16..=20).collect::<Vec<_>>());
}
#[test]
fn coverage_format2_overclaiming_table_is_rejected_not_expanded() {
let mut d = Vec::new();
d.extend_from_slice(&be(2));
d.extend_from_slice(&be(2)); for _ in 0..2 {
d.extend_from_slice(&be(0)); d.extend_from_slice(&be(0xFFFF)); d.extend_from_slice(&be(0)); }
assert!(parse_coverage_glyphs(&d, 0).is_none());
assert_eq!(MAX_COVERAGE_GLYPHS, 65_536);
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod subset_degradation_tests {
use super::{
Font, MISSING_GLYPH_REMAP, be_i16, be_u16, find_table_full, strip_simple_glyph_instructions,
};
fn cm_regular() -> Font {
let bytes = std::fs::read(concat!(
env!("CARGO_MANIFEST_DIR"),
"/fonts/computer-modern/cmunrm.ttf"
))
.expect("read bundled font");
Font::parse(bytes).expect("parse bundled font")
}
fn all_faces() -> Vec<Font> {
let base = env!("CARGO_MANIFEST_DIR");
[
"/fonts/computer-modern/cmunrm.ttf",
"/fonts/computer-modern/cmunbx.ttf",
"/fonts/computer-modern/cmunti.ttf",
"/fonts/computer-modern/cmunbi.ttf",
"/fonts/computer-modern/cmuntt.ttf",
"/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf",
"/fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf",
"/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf",
"/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.ttf",
]
.iter()
.filter_map(|p| Font::parse(std::fs::read(format!("{base}{p}")).ok()?).ok())
.collect()
}
fn test_remap(font: &Font, pairs: &[(u16, u16)]) -> Vec<u16> {
let mut new_of = vec![MISSING_GLYPH_REMAP; usize::from(font.num_glyphs).max(1)];
for &(old, new) in pairs {
if let Some(slot) = new_of.get_mut(usize::from(old)) {
*slot = new;
}
}
new_of
}
#[test]
fn subset_glyphs_with_lookup_matches_btreemap_remap() {
fn assert_agree(font: &Font, glyphs: &[u16], cmap_chars: &[char]) {
let (map_bytes, remap) = font
.subset_glyphs(glyphs, cmap_chars)
.expect("map-path subset");
let (dense_bytes, lookup) = font
.subset_glyphs_with_lookup(glyphs, cmap_chars)
.expect("dense-path subset");
assert_eq!(map_bytes, dense_bytes, "font bytes must be identical");
assert_eq!(
lookup.len(),
usize::from(font.num_glyphs).max(1),
"dense lookup covers every source glyph"
);
let mut mapped = 0usize;
for (old, new) in lookup.iter().enumerate() {
if *new == MISSING_GLYPH_REMAP {
assert!(
!remap.contains_key(&(old as u16)),
"dense sentinel at {old} must be absent from the map"
);
} else {
mapped += 1;
assert_eq!(
remap.get(&(old as u16)).copied(),
Some(*new),
"dense entry {old} -> {new} must match the map"
);
}
}
assert_eq!(
remap.len(),
mapped,
"map and dense table cover the same glyphs"
);
let dense_pairs: Vec<(u16, u16)> = lookup
.iter()
.enumerate()
.filter(|&(_, &v)| v != MISSING_GLYPH_REMAP)
.map(|(old, &v)| (old as u16, v))
.collect();
let map_pairs: Vec<(u16, u16)> = remap.iter().map(|(&k, &v)| (k, v)).collect();
assert_eq!(dense_pairs, map_pairs);
}
for font in all_faces() {
let a = font.glyph_index('A');
let b = font.glyph_index('B');
let q = font.glyph_index('Q');
let composite =
(0..font.num_glyphs).find(|&g| font.is_composite(g) && g != a && g != b && g != q);
assert_agree(&font, &[], &[]);
assert_agree(&font, &[], &['A', '\u{1D49C}']);
assert_agree(&font, &[0], &[]);
assert_agree(&font, &[a, b, q], &['A', 'B', 'Q']);
let over = font.num_glyphs.saturating_add(3);
assert_agree(
&font,
&[over, q, 0, u16::MAX, b, a, a, over],
&['A', 'B', 'Q'],
);
if let Some(comp) = composite {
assert_agree(&font, &[comp], &[]);
let (bytes, _) = font.subset_glyphs(&[comp], &[]).expect("composite subset");
let sub = Font::parse(bytes).expect("composite subset re-parses");
assert!(
sub.num_glyphs > 2,
"closure must have pulled components beyond .notdef + the composite"
);
}
}
}
fn simple_instruction_len(data: &[u8]) -> Option<usize> {
let contours = be_i16(data, 0)?;
if contours < 0 {
return None;
}
let instruction_len_offset = 10usize.checked_add((contours as usize).checked_mul(2)?)?;
be_u16(data, instruction_len_offset).map(usize::from)
}
#[test]
fn simple_glyph_instruction_stripper_zeroes_length_and_removes_bytes() {
let mut glyph = Vec::new();
glyph.extend_from_slice(&1i16.to_be_bytes()); glyph.extend_from_slice(&[0u8; 8]); glyph.extend_from_slice(&0u16.to_be_bytes()); glyph.extend_from_slice(&3u16.to_be_bytes()); glyph.extend_from_slice(&[0xAA, 0xBB, 0xCC]); glyph.extend_from_slice(&[0x11, 0x22, 0x33]);
let stripped = strip_simple_glyph_instructions(&glyph, 1).expect("valid simple glyph");
assert_eq!(simple_instruction_len(&stripped), Some(0));
assert_eq!(stripped.len(), glyph.len() - 3);
assert_eq!(&stripped[stripped.len() - 3..], &[0x11, 0x22, 0x33]);
}
#[test]
fn subset_glyph_bytes_strips_simple_instructions_when_present() {
let Some((font, gid, original_len)) = all_faces().into_iter().find_map(|font| {
(1..font.num_glyphs).find_map(|gid| {
let data = font.glyph_data(gid)?;
let len = simple_instruction_len(data)?;
(len > 0).then_some((font.clone(), gid, len))
})
}) else {
eprintln!("skipping: bundled fonts have no hinted simple glyphs");
return;
};
let new_of = test_remap(&font, &[(0u16, 0u16), (gid, 1u16)]);
let stripped = font
.subset_glyph_bytes(gid, &new_of)
.expect("hinted simple glyph should subset");
assert_eq!(simple_instruction_len(&stripped), Some(0));
assert_eq!(
stripped.len(),
font.glyph_data(gid).expect("original glyph").len() - original_len
);
}
#[test]
fn subset_hmtx_preserves_true_left_side_bearings() {
let (font, ch, old_gid, old_lsb) = all_faces()
.into_iter()
.find_map(|font| {
(33u8..=126).find_map(|byte| {
let ch = char::from(byte);
let gid = font.glyph_index(ch);
let lsb = font.left_side_bearing(gid);
(gid != 0 && lsb != 0).then_some((font.clone(), ch, gid, lsb))
})
})
.expect("at least one bundled printable glyph has a nonzero lsb");
let (bytes, remap) = font
.subset_glyphs(&[old_gid], &[ch])
.expect("subset with nonzero-lsb glyph");
let subset = Font::parse(bytes).expect("subset re-parses");
let new_gid = remap[&old_gid];
assert_eq!(subset.left_side_bearing(new_gid), old_lsb);
}
#[test]
fn html_subset_carries_verbatim_os2_while_pdf_subset_stays_lean() {
for font in all_faces() {
let (src_off, src_len) = find_table_full(&font.data, b"OS/2")
.expect("every bundled face carries an OS/2 table");
let src_os2 = font.data[src_off..src_off + src_len].to_vec();
let html_bytes = font.subset(&['A', 'b']).expect("html subset");
let html_font = Font::parse(html_bytes).expect("html subset re-parses");
let (o, l) = find_table_full(&html_font.data, b"OS/2")
.expect("html subset must keep OS/2 for browser sanitizers");
assert_eq!(
&html_font.data[o..o + l],
&src_os2[..],
"OS/2 must be copied verbatim"
);
let gid = font.glyph_index('A');
assert_ne!(gid, 0, "bundled faces must map 'A'");
let (pdf_bytes, _) = font.subset_glyphs(&[gid], &['A']).expect("pdf subset");
assert!(
find_table_full(&pdf_bytes, b"OS/2").is_none(),
"pdf subset must not grow an OS/2 table (golden bytes)"
);
assert!(Font::parse(pdf_bytes).is_ok());
}
}
#[test]
fn subset_skips_cmap_char_whose_glyph_is_absent_from_the_set() {
let font = cm_regular();
let g_b = font.glyph_index('B');
assert_ne!(g_b, 0, "test font must map 'B'");
let out = font.subset_glyphs(&[g_b], &['A', 'B']);
let (bytes, _) = out.expect("un-subsettable cmap char must be skipped, not abort");
assert!(Font::parse(bytes).is_ok());
}
#[test]
fn simple_instruction_len_rejects_composite_data() {
assert_eq!(simple_instruction_len(&(-1i16).to_be_bytes()), None);
}
#[test]
fn subset_glyph_bytes_substitutes_notdef_for_a_missing_component() {
let (font, comp) = all_faces()
.into_iter()
.find_map(|f| {
(1..f.num_glyphs)
.find(|&g| f.is_composite(g))
.map(|g| (f, g))
})
.expect("at least one bundled face has a composite glyph");
let new_of = test_remap(&font, &[(0u16, 0u16), (comp, 1u16)]);
let bytes = font
.subset_glyph_bytes(comp, &new_of)
.expect("missing component must be substituted, not abort");
assert!(!bytes.is_empty(), "a composite glyph is non-empty");
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
mod synthetic_font_tests {
use super::*;
fn push16(out: &mut Vec<u8>, v: u16) {
out.extend_from_slice(&v.to_be_bytes());
}
fn push_i16(out: &mut Vec<u8>, v: i16) {
out.extend_from_slice(&v.to_be_bytes());
}
fn push32(out: &mut Vec<u8>, v: u32) {
out.extend_from_slice(&v.to_be_bytes());
}
fn sfnt(magic: u32, tables: &[(&[u8; 4], Vec<u8>)]) -> Vec<u8> {
let mut out = Vec::new();
push32(&mut out, magic);
push16(&mut out, u16::try_from(tables.len()).unwrap());
out.extend_from_slice(&[0u8; 6]); let mut offset = 12 + tables.len() * 16;
let mut body = Vec::new();
for (tag, bytes) in tables {
out.extend_from_slice(&tag[..]);
push32(&mut out, 0); push32(&mut out, u32::try_from(offset).unwrap());
push32(&mut out, u32::try_from(bytes.len()).unwrap());
offset += bytes.len();
body.extend_from_slice(bytes);
}
out.extend_from_slice(&body);
out
}
fn head_table(upem: u16, loca_long: bool) -> Vec<u8> {
let mut t = vec![0u8; 54];
t[18..20].copy_from_slice(&upem.to_be_bytes());
t[50..52].copy_from_slice(&u16::from(loca_long).to_be_bytes());
t
}
fn maxp_table(num_glyphs: u16) -> Vec<u8> {
let mut t = vec![0u8; 6];
t[4..6].copy_from_slice(&num_glyphs.to_be_bytes());
t
}
fn hhea_table(num_h_metrics: u16) -> Vec<u8> {
let mut t = vec![0u8; 36];
t[4..6].copy_from_slice(&700i16.to_be_bytes());
t[6..8].copy_from_slice(&(-200i16).to_be_bytes());
t[8..10].copy_from_slice(&50i16.to_be_bytes());
t[34..36].copy_from_slice(&num_h_metrics.to_be_bytes());
t
}
fn hmtx_long(metrics: &[(u16, i16)]) -> Vec<u8> {
let mut t = Vec::new();
for &(aw, lsb) in metrics {
push16(&mut t, aw);
push_i16(&mut t, lsb);
}
t
}
fn cmap4_table(segs: &[(u16, u16, u16, u16)], glyph_id_array: &[u8]) -> Vec<u8> {
let seg_count = segs.len();
let mut t = Vec::new();
push16(&mut t, 0); push16(&mut t, 1); push16(&mut t, 3); push16(&mut t, 1); push32(&mut t, 12); push16(&mut t, 4); push16(
&mut t,
u16::try_from(16 + seg_count * 8 + glyph_id_array.len()).unwrap(),
);
push16(&mut t, 0); push16(&mut t, u16::try_from(seg_count * 2).unwrap()); push16(&mut t, 0); push16(&mut t, 0); push16(&mut t, 0); for &(end, _, _, _) in segs {
push16(&mut t, end);
}
push16(&mut t, 0); for &(_, start, _, _) in segs {
push16(&mut t, start);
}
for &(_, _, delta, _) in segs {
push16(&mut t, delta);
}
for &(_, _, _, iro) in segs {
push16(&mut t, iro);
}
t.extend_from_slice(glyph_id_array);
t
}
fn cmap4_simple(map: &[(u16, u16)]) -> Vec<u8> {
let mut segs: Vec<(u16, u16, u16, u16)> = map
.iter()
.map(|&(code, gid)| (code, code, gid.wrapping_sub(code), 0))
.collect();
segs.push((0xFFFF, 0xFFFF, 1, 0));
cmap4_table(&segs, &[])
}
fn cmap12_table(groups: &[(u32, u32, u32)]) -> Vec<u8> {
let mut t = Vec::new();
push16(&mut t, 0); push16(&mut t, 1); push16(&mut t, 3); push16(&mut t, 10); push32(&mut t, 12); push16(&mut t, 12); push16(&mut t, 0); push32(&mut t, u32::try_from(16 + groups.len() * 12).unwrap());
push32(&mut t, 0); push32(&mut t, u32::try_from(groups.len()).unwrap());
for &(start, end, gid) in groups {
push32(&mut t, start);
push32(&mut t, end);
push32(&mut t, gid);
}
t
}
fn base_tables(
num_glyphs: u16,
num_h_metrics: u16,
upem: u16,
hmtx: Vec<u8>,
cmap: Vec<u8>,
) -> Vec<(&'static [u8; 4], Vec<u8>)> {
vec![
(b"head", head_table(upem, false)),
(b"maxp", maxp_table(num_glyphs)),
(b"hhea", hhea_table(num_h_metrics)),
(b"hmtx", hmtx),
(b"cmap", cmap),
]
}
fn parse(tables: &[(&[u8; 4], Vec<u8>)]) -> Font {
Font::parse(sfnt(0x0001_0000, tables)).expect("synthetic font parses")
}
const ARGW: u16 = 0x0001; const WHS: u16 = 0x0008; const MORE: u16 = 0x0020; const XYS: u16 = 0x0040; const TWO: u16 = 0x0080; const INSTR: u16 = 0x0100;
fn composite_glyph(bbox: [i16; 4], records: &[(u16, u16, &[u8])], trailer: &[u8]) -> Vec<u8> {
let mut g = Vec::new();
push_i16(&mut g, -1);
for v in bbox {
push_i16(&mut g, v);
}
for &(flags, gid, payload) in records {
push16(&mut g, flags);
push16(&mut g, gid);
g.extend_from_slice(payload);
}
g.extend_from_slice(trailer);
g
}
fn simple_glyph16() -> Vec<u8> {
let mut g = Vec::new();
push_i16(&mut g, 1); g.extend_from_slice(&[0u8; 8]); push16(&mut g, 0); push16(&mut g, 0); g.extend_from_slice(&[0x01, 0x00]); g
}
fn zoo_font() -> Font {
let glyphs: Vec<Vec<u8>> = vec![
Vec::new(),
1i16.to_be_bytes().to_vec(),
composite_glyph([1, 2, 3, 4], &[(WHS, 5, &[0, 0, 0x40, 0])], &[]),
composite_glyph([0; 4], &[(XYS, 5, &[0, 0, 0x40, 0, 0x40, 0])], &[]),
composite_glyph(
[0; 4],
&[(TWO, 5, &[0, 0, 0x40, 0, 0, 0, 0, 0, 0x40, 0])],
&[],
),
simple_glyph16(),
composite_glyph(
[0; 4],
&[(ARGW | MORE, 2, &[0, 0, 0, 0]), (0, 3, &[0, 0])],
&[],
),
composite_glyph([0; 4], &[(MORE, 5, &[0, 0])], &[]),
composite_glyph([0; 4], &[(INSTR, 5, &[0, 0])], &[0xFF, 0xFF]),
composite_glyph([0; 4], &[(0, 900, &[0, 0])], &[]),
composite_glyph([0; 4], &[(MORE, 5, &[0, 0])], &[0, 0]),
composite_glyph([0; 4], &[(TWO, 5, &[0, 0, 0x40, 0])], &[]),
composite_glyph([0; 4], &[(INSTR, 5, &[0, 0])], &[0x00, 0x02, 0xAA, 0xBB]),
];
let mut glyf = Vec::new();
let mut loca = Vec::new();
push16(&mut loca, 0);
for g in &glyphs {
glyf.extend_from_slice(g);
push16(&mut loca, u16::try_from(glyf.len() / 2).unwrap());
}
let metrics: Vec<(u16, i16)> = (0..13u16).map(|g| (500 + g, g as i16)).collect();
let mut tables = base_tables(13, 13, 1000, hmtx_long(&metrics), cmap4_simple(&[]));
tables.push((b"loca", loca));
tables.push((b"glyf", glyf));
parse(&tables)
}
fn truncated_composite_font(keep: usize) -> Font {
let glyph = composite_glyph([0; 4], &[(0, 5, &[0, 0])], &[]);
assert_eq!(glyph.len(), 16);
let mut loca = Vec::new();
push16(&mut loca, 0);
push16(&mut loca, 8);
let mut tables = base_tables(1, 1, 1000, hmtx_long(&[(500, 0)]), cmap4_simple(&[]));
tables.push((b"loca", loca));
tables.push((b"glyf", glyph));
let mut bytes = sfnt(0x0001_0000, &tables);
bytes.truncate(bytes.len() - (16 - keep));
Font::parse(bytes).expect("glyf payload is lazily read")
}
fn kern0_table(pairs: &[(u16, u16, i16)]) -> Vec<u8> {
let mut t = Vec::new();
push16(&mut t, 0); push16(&mut t, 1); push16(&mut t, 0); push16(&mut t, u16::try_from(14 + pairs.len() * 6).unwrap()); push16(&mut t, 0x0001); push16(&mut t, u16::try_from(pairs.len()).unwrap()); t.extend_from_slice(&[0u8; 6]); for &(l, r, v) in pairs {
push16(&mut t, l);
push16(&mut t, r);
push_i16(&mut t, v);
}
t
}
fn gpos_table(ext_format: u16, ext_type: u16, lookup_index: u16, pos_format: u16) -> Vec<u8> {
let mut g = Vec::new();
push32(&mut g, 0x0001_0000); push16(&mut g, 0); push16(&mut g, 10); push16(&mut g, 24); push16(&mut g, 1); g.extend_from_slice(b"kern");
push16(&mut g, 8); push16(&mut g, 0); push16(&mut g, 1); push16(&mut g, lookup_index);
push16(&mut g, 1); push16(&mut g, 4); push16(&mut g, 9); push16(&mut g, 0); push16(&mut g, 1); push16(&mut g, 8); push16(&mut g, ext_format);
push16(&mut g, ext_type);
push32(&mut g, 8); push16(&mut g, pos_format);
push16(&mut g, 18); push16(&mut g, 0x0004); push16(&mut g, 0); push16(&mut g, 1); push16(&mut g, 12); push16(&mut g, 1); push16(&mut g, 6); push_i16(&mut g, -40); push16(&mut g, 1);
push16(&mut g, 1);
push16(&mut g, 5);
assert_eq!(g.len(), 68);
g
}
fn gpos_kerning_of(table: Vec<u8>) -> Kerning {
let mut tables = base_tables(
2,
2,
1000,
hmtx_long(&[(600, 0), (600, 0)]),
cmap4_simple(&[]),
);
tables.push((b"GPOS", table));
parse(&tables).gpos_kerning()
}
fn gpos_font(ext_format: u16, ext_type: u16, lookup_index: u16, pos_format: u16) -> Kerning {
gpos_kerning_of(gpos_table(ext_format, ext_type, lookup_index, pos_format))
}
fn gsub_table(ext_format: u16, ext_type: u16, lookup_index: u16) -> Vec<u8> {
let mut g = Vec::new();
push32(&mut g, 0x0001_0000);
push16(&mut g, 0); push16(&mut g, 10); push16(&mut g, 24); push16(&mut g, 1);
g.extend_from_slice(b"liga");
push16(&mut g, 8); push16(&mut g, 0);
push16(&mut g, 1);
push16(&mut g, lookup_index);
push16(&mut g, 1);
push16(&mut g, 4); push16(&mut g, 7);
push16(&mut g, 0);
push16(&mut g, 1);
push16(&mut g, 8); push16(&mut g, ext_format);
push16(&mut g, ext_type);
push32(&mut g, 8); push16(&mut g, 1); push16(&mut g, 28); push16(&mut g, 1); push16(&mut g, 8); push16(&mut g, 2); push16(&mut g, 6); push16(&mut g, 14); push16(&mut g, 99);
push16(&mut g, 3);
push16(&mut g, 11);
push16(&mut g, 12);
push16(&mut g, 77);
push16(&mut g, 2);
push16(&mut g, 11);
push16(&mut g, 1);
push16(&mut g, 1);
push16(&mut g, 10);
assert_eq!(g.len(), 78);
g
}
fn gsub_ligatures_of(table: Vec<u8>) -> Ligatures {
let mut tables = base_tables(
2,
2,
1000,
hmtx_long(&[(600, 0), (600, 0)]),
cmap4_simple(&[]),
);
tables.push((b"GSUB", table));
parse(&tables).gsub_ligatures()
}
fn gsub_font(ext_format: u16, ext_type: u16, lookup_index: u16) -> Ligatures {
gsub_ligatures_of(gsub_table(ext_format, ext_type, lookup_index))
}
#[test]
fn parse_error_variants_and_display_messages() {
assert_eq!(Font::parse(Vec::new()).err(), Some(FontError::Truncated));
assert_eq!(
Font::parse(vec![0x00, 0x02, 0x00, 0x00]).err(),
Some(FontError::BadMagic)
);
let mut tables: Vec<(&[u8; 4], Vec<u8>)> = Vec::new();
let steps: [(&'static [u8; 4], &'static str, Vec<u8>); 4] = [
(b"head", "head", head_table(1000, false)),
(b"maxp", "maxp", maxp_table(1)),
(b"hhea", "hhea", hhea_table(1)),
(b"hmtx", "hmtx", hmtx_long(&[(500, 0)])),
];
for (tag, name, table) in steps {
assert_eq!(
Font::parse(sfnt(0x0001_0000, &tables)).err(),
Some(FontError::MissingTable(name))
);
tables.push((tag, table));
}
assert_eq!(
Font::parse(sfnt(0x0001_0000, &tables)).err(),
Some(FontError::MissingTable("cmap"))
);
let mut bad_cmap = Vec::new();
push16(&mut bad_cmap, 0);
push16(&mut bad_cmap, 2);
push16(&mut bad_cmap, 1); push16(&mut bad_cmap, 0);
push32(&mut bad_cmap, 20);
push16(&mut bad_cmap, 3); push16(&mut bad_cmap, 1);
push32(&mut bad_cmap, 20);
push16(&mut bad_cmap, 6); tables.push((b"cmap", bad_cmap));
assert_eq!(
Font::parse(sfnt(0x0001_0000, &tables)).err(),
Some(FontError::NoUnicodeCmap)
);
let short_head: Vec<(&[u8; 4], Vec<u8>)> = vec![
(b"maxp", maxp_table(1)),
(b"hhea", hhea_table(1)),
(b"hmtx", hmtx_long(&[(500, 0)])),
(b"cmap", cmap4_simple(&[])),
(b"head", vec![0u8; 10]),
];
assert_eq!(
Font::parse(sfnt(0x0001_0000, &short_head)).err(),
Some(FontError::Truncated)
);
assert_eq!(
FontError::BadMagic.to_string(),
"not a TrueType/OpenType font"
);
assert_eq!(
FontError::MissingTable("hhea").to_string(),
"missing required font table: hhea"
);
assert_eq!(FontError::Truncated.to_string(), "font data is truncated");
assert_eq!(
FontError::NoUnicodeCmap.to_string(),
"no usable Unicode cmap (format 4/12)"
);
}
#[test]
fn parse_accepts_true_and_otto_magics() {
let tables = base_tables(
3,
3,
2048,
hmtx_long(&[(500, 1), (510, 2), (520, 3)]),
cmap4_simple(&[(0x41, 1)]),
);
let t = Font::parse(sfnt(0x7472_7565, &tables)).expect("'true' magic parses");
assert_eq!(t.units_per_em, 2048);
assert_eq!(t.num_glyphs, 3);
assert_eq!(t.ascent, 700);
assert_eq!(t.descent, -200);
assert_eq!(t.line_gap, 50);
assert_eq!(t.glyph_index('A'), 1);
let o = Font::parse(sfnt(0x4F54_544F, &tables)).expect("'OTTO' magic parses");
assert!(!o.has_glyf_outlines());
assert_eq!(o.subset(&['A']), None);
assert_eq!(o.glyph_bbox(1), None);
assert_eq!(o.glyph_data(1), None);
assert!(!o.is_composite(1));
assert!(o.glyph_components(1).is_empty());
}
#[test]
fn left_side_bearing_reads_trailing_run_and_zero_metrics() {
let mut hmtx = hmtx_long(&[(500, 50)]);
push_i16(&mut hmtx, -7);
push_i16(&mut hmtx, 33);
let font = parse(&base_tables(3, 1, 1000, hmtx, cmap4_simple(&[])));
assert_eq!(font.left_side_bearing(0), 50);
assert_eq!(font.left_side_bearing(1), -7);
assert_eq!(font.left_side_bearing(2), 33);
assert_eq!(font.advance_width(0), 500);
assert_eq!(font.advance_width(2), 500);
let font0 = parse(&base_tables(1, 0, 1000, Vec::new(), cmap4_simple(&[])));
assert_eq!(font0.left_side_bearing(0), 0);
}
#[test]
fn legacy_kern_pair_and_char_kerning() {
let pairs = [(1u16, 2u16, -30i16), (1, 3, 15), (4, 1, 7)];
let metrics: Vec<(u16, i16)> = (0..8u16).map(|g| (600 + g, 0)).collect();
let mut tables = base_tables(
8,
8,
1000,
hmtx_long(&metrics),
cmap4_simple(&[(0x41, 1), (0x56, 2)]),
);
tables.push((b"kern", kern0_table(&pairs)));
let font = parse(&tables);
assert_eq!(font.kerning_between_glyphs(1, 2), -30);
assert_eq!(font.kerning_between_glyphs(1, 3), 15);
assert_eq!(font.kerning_between_glyphs(4, 1), 7);
assert_eq!(font.kerning_between_glyphs(2, 1), 0);
assert_eq!(font.kerning_between_glyphs(1, 4), 0);
assert_eq!(font.kerning('A', 'V'), -30);
assert_eq!(font.kerning_1000('A', 'V'), -30); assert_eq!(font.advance_1000('A'), 601);
let mut zero = base_tables(
8,
8,
0,
hmtx_long(&metrics),
cmap4_simple(&[(0x41, 1), (0x56, 2)]),
);
zero.push((b"kern", kern0_table(&pairs)));
let z = parse(&zero);
assert_eq!(z.units_per_em, 0);
assert_eq!(z.advance_1000('A'), 0);
assert_eq!(z.kerning_1000('A', 'V'), 0);
}
#[test]
fn legacy_kern_skips_short_vertical_minimum_and_format2_subtables() {
let mut k = Vec::new();
push16(&mut k, 0); push16(&mut k, 5); push16(&mut k, 0);
push16(&mut k, 10);
push16(&mut k, 0x0001);
k.extend_from_slice(&[0u8; 4]);
push16(&mut k, 0);
push16(&mut k, 14);
push16(&mut k, 0x0000);
k.extend_from_slice(&[0u8; 8]);
push16(&mut k, 0);
push16(&mut k, 14);
push16(&mut k, 0x0003);
k.extend_from_slice(&[0u8; 8]);
push16(&mut k, 0);
push16(&mut k, 14);
push16(&mut k, 0x0201);
k.extend_from_slice(&[0u8; 8]);
push16(&mut k, 0);
push16(&mut k, 20);
push16(&mut k, 0x0001);
push16(&mut k, 1); k.extend_from_slice(&[0u8; 6]);
push16(&mut k, 3);
push16(&mut k, 4);
push_i16(&mut k, -11);
let mut tables = base_tables(8, 1, 1000, hmtx_long(&[(600, 0)]), cmap4_simple(&[]));
tables.push((b"kern", k));
let font = parse(&tables);
assert_eq!(font.kerning_between_glyphs(3, 4), -11);
assert_eq!(font.kerning_between_glyphs(3, 5), 0);
}
#[test]
fn legacy_kern_rejects_malformed_table_headers() {
fn kern_font(kern: Vec<u8>) -> Font {
let mut tables = base_tables(8, 1, 1000, hmtx_long(&[(600, 0)]), cmap4_simple(&[]));
tables.push((b"kern", kern));
parse(&tables)
}
let mut v1 = kern0_table(&[(1, 2, -30)]);
v1[0..2].copy_from_slice(&1u16.to_be_bytes());
assert_eq!(kern_font(v1).kerning_between_glyphs(1, 2), 0);
let mut zero_len = Vec::new();
push16(&mut zero_len, 0);
push16(&mut zero_len, 2);
push16(&mut zero_len, 0); push16(&mut zero_len, 0); push16(&mut zero_len, 0x0000); zero_len.extend_from_slice(&[0u8; 8]);
assert_eq!(kern_font(zero_len).kerning_between_glyphs(1, 2), 0);
let mut walk_off = Vec::new();
push16(&mut walk_off, 0);
push16(&mut walk_off, 2);
push16(&mut walk_off, 0);
push16(&mut walk_off, 14);
push16(&mut walk_off, 0x0000); walk_off.extend_from_slice(&[0u8; 8]);
assert_eq!(kern_font(walk_off).kerning_between_glyphs(1, 2), 0);
let mut overlong = Vec::new();
push16(&mut overlong, 0);
push16(&mut overlong, 1);
push16(&mut overlong, 0);
push16(&mut overlong, 200); push16(&mut overlong, 0x0001);
overlong.extend_from_slice(&[0u8; 8]);
assert_eq!(kern_font(overlong).kerning_between_glyphs(1, 2), 0);
let mut hungry = Vec::new();
push16(&mut hungry, 0);
push16(&mut hungry, 1);
push16(&mut hungry, 0);
push16(&mut hungry, 20); push16(&mut hungry, 0x0001);
push16(&mut hungry, 3); hungry.extend_from_slice(&[0u8; 12]);
assert_eq!(kern_font(hungry).kerning_between_glyphs(1, 2), 0);
let mut vertical_only = Vec::new();
push16(&mut vertical_only, 0);
push16(&mut vertical_only, 1);
push16(&mut vertical_only, 0);
push16(&mut vertical_only, 14);
push16(&mut vertical_only, 0x0000);
vertical_only.extend_from_slice(&[0u8; 8]);
assert_eq!(kern_font(vertical_only).kerning_between_glyphs(1, 2), 0);
}
#[test]
fn legacy_kern_truncated_pair_records_kern_to_zero() {
let pairs = [(1u16, 2u16, -30i16), (1, 3, 15), (4, 1, 7), (5, 5, 9)];
for chop in [10usize, 16] {
let mut tables = base_tables(8, 1, 1000, hmtx_long(&[(600, 0)]), cmap4_simple(&[]));
tables.push((b"kern", kern0_table(&pairs)));
let mut bytes = sfnt(0x0001_0000, &tables);
bytes.truncate(bytes.len() - chop);
let font = Font::parse(bytes).expect("kern pair payload is lazily read");
assert_eq!(font.kerning_between_glyphs(1, 2), 0, "chop={chop}");
}
}
#[test]
fn glyph_range_rejects_inverted_and_overlong_loca_entries() {
let mut loca = Vec::new();
push16(&mut loca, 2);
push16(&mut loca, 1);
push16(&mut loca, 3);
let mut tables = base_tables(
2,
2,
1000,
hmtx_long(&[(500, 0), (500, 0)]),
cmap4_simple(&[]),
);
tables.push((b"loca", loca));
tables.push((b"glyf", vec![0u8; 4]));
let font = parse(&tables);
assert!(font.has_glyf_outlines());
assert_eq!(font.glyph_data(0), None);
assert_eq!(font.glyph_data(1), None);
assert_eq!(font.glyph_bbox(0), None);
assert!(!font.is_composite(0));
}
#[test]
fn glyph_components_walk_all_transform_variants() {
let font = zoo_font();
assert!(font.glyph_components(0).is_empty()); assert!(font.glyph_components(1).is_empty()); assert_eq!(font.glyph_components(2), vec![5]); assert_eq!(font.glyph_components(3), vec![5]); assert_eq!(font.glyph_components(4), vec![5]); assert_eq!(font.glyph_components(6), vec![2, 3]); assert_eq!(font.glyph_components(7), vec![5]); assert_eq!(font.glyph_components(10), vec![5]); assert!(font.glyph_components(11).is_empty()); assert!(font.is_composite(2));
assert!(!font.is_composite(1));
assert!(!font.is_composite(0));
assert_eq!(font.glyph_bbox(2), Some([1, 2, 3, 4]));
assert_eq!(font.glyph_bbox(0), None);
assert_eq!(font.glyph_data(0), Some(&[][..]));
}
#[test]
fn glyph_components_stop_at_truncated_component_records() {
for keep in [1usize, 10, 12] {
let font = truncated_composite_font(keep);
assert!(font.glyph_components(0).is_empty(), "keep={keep}");
assert_eq!(font.glyph_data(0), None, "keep={keep}");
}
}
#[test]
fn subset_rewrites_component_ids_across_transform_variants() {
let font = zoo_font();
let (bytes, remap) = font.subset_glyphs(&[2, 3, 4], &[]).expect("subset");
let remap: Vec<(u16, u16)> = remap.into_iter().collect();
assert_eq!(remap, vec![(0, 0), (2, 1), (3, 2), (4, 3), (5, 4)]);
let sub = Font::parse(bytes).expect("subset re-parses");
assert_eq!(sub.num_glyphs, 5);
assert_eq!(sub.glyph_components(1), vec![4]);
assert_eq!(sub.glyph_components(2), vec![4]);
assert_eq!(sub.glyph_components(3), vec![4]);
assert_eq!(sub.glyph_bbox(1), Some([1, 2, 3, 4]));
assert_eq!(sub.advance_width(1), 502);
assert_eq!(sub.left_side_bearing(1), 2);
assert_eq!(sub.advance_width(4), 505);
assert_eq!(sub.left_side_bearing(4), 5);
}
#[test]
fn subset_closure_skips_component_ids_past_num_glyphs() {
let font = zoo_font();
let (bytes, remap) = font.subset_glyphs(&[9], &[]).expect("subset");
assert_eq!(remap.get(&9).copied(), Some(1));
assert_eq!(remap.len(), 2); let sub = Font::parse(bytes).expect("subset re-parses");
assert_eq!(sub.num_glyphs, 2);
assert_eq!(sub.glyph_components(1), vec![0]);
}
#[test]
fn subset_shares_a_component_between_two_composites() {
let font = zoo_font();
let (bytes, remap) = font.subset_glyphs(&[2, 4], &[]).expect("subset");
let remap: Vec<(u16, u16)> = remap.into_iter().collect();
assert_eq!(remap, vec![(0, 0), (2, 1), (4, 2), (5, 3)]);
let sub = Font::parse(bytes).expect("subset re-parses");
assert_eq!(sub.glyph_components(1), vec![3]);
assert_eq!(sub.glyph_components(2), vec![3]);
}
#[test]
fn subset_tolerates_composite_whose_last_record_dangles_more() {
let font = zoo_font();
assert_eq!(font.glyph_components(7), vec![5]);
let mut new_of = vec![MISSING_GLYPH_REMAP; usize::from(font.num_glyphs)];
new_of[0] = 0;
new_of[5] = 1;
new_of[7] = 2;
let out = font
.subset_glyph_bytes(7, &new_of)
.expect("dangling MORE bit is tolerated and stripped");
assert_eq!(be_u16(&out, 10), Some(0));
assert!(font.subset_glyphs(&[7], &[]).is_some());
}
#[test]
fn subset_tolerates_composite_whose_dangling_record_claims_instructions() {
let glyph = composite_glyph([0; 4], &[(0x0100 | 0x0020, 0, &[0, 0])], &[]);
let mut glyf = Vec::new();
let mut loca = Vec::new();
push16(&mut loca, 0); push16(&mut loca, 0);
glyf.extend_from_slice(&glyph);
push16(&mut loca, u16::try_from(glyf.len() / 2).unwrap());
let mut tables = base_tables(
2,
1,
1000,
hmtx_long(&[(500, 0), (500, 1)]),
cmap4_simple(&[]),
);
tables.push((b"loca", loca));
tables.push((b"glyf", glyf));
let font = parse(&tables);
let mut new_of = vec![MISSING_GLYPH_REMAP; usize::from(font.num_glyphs)];
new_of[0] = 0;
new_of[1] = 1;
let out = font
.subset_glyph_bytes(1, &new_of)
.expect("dangling INSTRUCTIONS|MORE record is tolerated");
let flags = be_u16(&out, 10).expect("record flags readable");
assert_eq!(
flags & (0x0100 | 0x0020),
0,
"MORE and INSTRUCTIONS cleared"
);
}
#[test]
fn subset_emits_format12_cmap_when_supplementary_plane_is_kept() {
let groups: &[(u32, u32, u32)] =
&[(u32::from('A'), u32::from('B'), 1), (0x1D49C, 0x1D49D, 3)];
let mut tables = base_tables(
5,
1,
1000,
hmtx_long(&[(500, 0), (505, 1), (510, 2), (515, 3)]),
cmap12_table(groups),
);
let mut glyf = Vec::new();
let mut loca = Vec::new();
push16(&mut loca, 0);
for _ in 0..4 {
let g = simple_glyph16();
glyf.extend_from_slice(&g);
push16(&mut loca, u16::try_from(glyf.len() / 2).unwrap());
}
while glyf.len() % 4 != 0 {
glyf.push(0);
}
tables.push((b"loca", loca));
tables.push((b"glyf", glyf));
let font = parse(&tables);
assert_ne!(font.glyph_index('A'), 0);
assert_ne!(font.glyph_index('\u{1D49C}'), 0);
let subset = font
.subset(&['A', 'B', '\u{1D49C}', '\u{1D49D}'])
.expect("subset");
let reparsed = Font::parse(subset).expect("subset re-parses");
assert_ne!(reparsed.glyph_index('A'), 0, "BMP letter survives");
assert_ne!(reparsed.glyph_index('B'), 0, "BMP letter survives");
assert_ne!(
reparsed.glyph_index('\u{1D49C}'),
0,
"script A must survive the subset"
);
assert_ne!(
reparsed.glyph_index('\u{1D49D}'),
0,
"script B must survive the subset"
);
}
#[test]
fn subset_strips_valid_composite_instructions_and_clears_the_flag() {
let font = zoo_font();
let mut new_of = vec![MISSING_GLYPH_REMAP; usize::from(font.num_glyphs)];
new_of[0] = 0;
new_of[5] = 1;
new_of[12] = 2;
let out = font
.subset_glyph_bytes(12, &new_of)
.expect("valid instructions strip");
assert_eq!(out.len(), 16); assert_eq!(be_u16(&out, 10), Some(0)); assert_eq!(be_u16(&out, 12), Some(1)); let (bytes, remap) = font.subset_glyphs(&[12], &[]).expect("subset");
assert_eq!(remap.get(&12).copied(), Some(2));
let sub = Font::parse(bytes).expect("subset re-parses");
assert_eq!(sub.glyph_components(2), vec![1]);
}
#[test]
fn subset_cmap_skips_supplementary_plane_chars() {
let font = zoo_font();
let (bytes, _) = font.subset_glyphs(&[2], &['😀']).expect("subset");
let sub = Font::parse(bytes).expect("subset re-parses");
assert_eq!(sub.glyph_index('😀'), 0); }
#[test]
fn subset_rejects_composite_with_overlong_instruction_claim() {
let font = zoo_font();
let mut new_of = vec![MISSING_GLYPH_REMAP; usize::from(font.num_glyphs)];
new_of[0] = 0;
new_of[5] = 1;
new_of[8] = 2;
assert_eq!(font.subset_glyph_bytes(8, &new_of), None);
assert!(font.subset_glyphs(&[8], &[]).is_none());
}
#[test]
fn strip_simple_glyph_instructions_rejects_overlong_length() {
let mut glyph = Vec::new();
push_i16(&mut glyph, 1);
glyph.extend_from_slice(&[0u8; 8]); push16(&mut glyph, 0); push16(&mut glyph, 255); assert_eq!(strip_simple_glyph_instructions(&glyph, 1), None);
}
#[test]
fn cmap4_truncated_segment_arrays_fall_back_to_uncached_lookup() {
let segs = [
(0x5Au16, 0x41u16, 1u16.wrapping_sub(0x41), 0u16), (0x61, 0x61, 1, 6), (0x62, 0x62, 0, 2), (0x63, 0x63, 0, 0), (0x64, 0x64, 0, 7), (0x00FF, 0x00F0, 0, 0), ];
let tables = base_tables(30, 1, 1000, hmtx_long(&[(500, 0)]), cmap4_table(&segs, &[]));
let mut bytes = sfnt(0x0001_0000, &tables);
bytes.truncate(bytes.len() - 2); let font = Font::parse(bytes).expect("cmap payload is lazily read");
assert!(font.cmap4_cache.is_none());
assert_eq!(font.glyph_index('A'), 1);
assert_eq!(font.glyph_index('Z'), 26);
assert_eq!(font.glyph_index('@'), 0); assert_eq!(font.glyph_index('a'), 8); assert_eq!(font.glyph_index('b'), 0); assert_eq!(font.glyph_index('c'), 99); assert_eq!(font.glyph_index('d'), 0); assert_eq!(font.glyph_index('õ'), 0); assert_eq!(font.glyph_index('Ā'), 0); assert_eq!(font.glyph_index('😀'), 0); }
#[test]
fn cmap4_cached_lookup_reads_glyph_id_array() {
let segs = [(0x42u16, 0x41u16, 3u16, 4u16), (0xFFFF, 0xFFFF, 1, 0)];
let mut array = Vec::new();
push16(&mut array, 7);
push16(&mut array, 0);
let font = parse(&base_tables(
20,
1,
1000,
hmtx_long(&[(500, 0)]),
cmap4_table(&segs, &array),
));
let cache = font.cmap4_cache.as_ref().expect("valid table caches");
assert!(cache.sorted_by_end);
assert_eq!(font.glyph_index('A'), 10); assert_eq!(font.glyph_index('B'), 0); assert_eq!(font.glyph_index('C'), 0); }
#[test]
fn cmap4_unsorted_segments_use_first_match_linear_scan() {
let segs = [
(0x61u16, 0x61u16, 2u16.wrapping_sub(0x61), 0u16),
(0x5A, 0x41, 1u16.wrapping_sub(0x41), 0),
(0xFFFF, 0xFFFF, 1, 0),
];
let font = parse(&base_tables(
30,
1,
1000,
hmtx_long(&[(500, 0)]),
cmap4_table(&segs, &[]),
));
let cache = font
.cmap4_cache
.as_ref()
.expect("caches even when unsorted");
assert!(!cache.sorted_by_end);
assert_eq!(font.glyph_index('a'), 2);
assert_eq!(font.glyph_index('A'), 0);
assert_eq!(font.glyph_index('p'), 0);
}
#[test]
fn cmap4_unsorted_lookup_misses_when_no_segment_covers_the_code() {
let segs = [
(0x61u16, 0x61u16, 2u16.wrapping_sub(0x61), 0u16),
(0x5A, 0x41, 1u16.wrapping_sub(0x41), 0),
];
let font = parse(&base_tables(
30,
1,
1000,
hmtx_long(&[(500, 0)]),
cmap4_table(&segs, &[]),
));
assert!(!font.cmap4_cache.as_ref().expect("caches").sorted_by_end);
assert_eq!(font.glyph_index('a'), 2);
assert_eq!(font.glyph_index('p'), 0); }
#[test]
fn select_cmap_accepts_format4_under_a_non_bmp_encoding_record() {
let mut cmap = cmap4_simple(&[(0x41, 1)]);
cmap[6..8].copy_from_slice(&10u16.to_be_bytes()); let font = parse(&base_tables(5, 1, 1000, hmtx_long(&[(500, 0)]), cmap));
assert_eq!(font.cmap_format, 4);
assert_eq!(font.glyph_index('A'), 1);
}
#[test]
fn cmap12_groups_map_across_planes_and_truncate_gids() {
let cmap = cmap12_table(&[
(0x41, 0x5A, 100),
(0x2000, 0x2000, 0x0001_2345),
(0x1F600, 0x1F601, 7),
]);
let font = parse(&base_tables(200, 1, 1000, hmtx_long(&[(500, 0)]), cmap));
assert_eq!(font.cmap_format, 12);
assert!(font.cmap4_cache.is_none());
assert_eq!(font.glyph_index('A'), 100);
assert_eq!(font.glyph_index('Z'), 125);
assert_eq!(font.glyph_index('\u{2000}'), 0x2345); assert_eq!(font.glyph_index('😀'), 7);
assert_eq!(font.glyph_index('😁'), 8);
assert_eq!(font.glyph_index('0'), 0); }
#[test]
fn gpos_extension_lookup_resolves_wrapped_pair_kerning() {
let kern = gpos_font(1, 2, 0, 1);
assert_eq!(kern.pair(5, 6), -40);
assert_eq!(kern.pair(5, 7), 0);
assert_eq!(kern.pair(6, 6), 0);
}
#[test]
fn gpos_skips_foreign_extensions_bad_formats_and_lookup_indices() {
assert_eq!(gpos_font(1, 5, 0, 1).pair(5, 6), 0);
assert_eq!(gpos_font(2, 2, 0, 1).pair(5, 6), 0);
assert_eq!(gpos_font(1, 2, 0, 3).pair(5, 6), 0);
assert_eq!(gpos_font(1, 2, 9, 1).pair(5, 6), 0);
}
#[test]
fn gpos_truncated_structures_yield_empty_kerning() {
for end in [16usize, 20, 22, 26, 28, 32, 34] {
let mut g = gpos_table(1, 2, 0, 1);
g.truncate(end);
assert_eq!(gpos_kerning_of(g).pair(5, 6), 0, "end={end}");
}
let mut over = gpos_table(1, 2, 0, 1);
over[10..12].copy_from_slice(&12u16.to_be_bytes());
assert_eq!(gpos_kerning_of(over).pair(5, 6), -40);
let mut direct = gpos_table(1, 2, 0, 1);
direct[28..30].copy_from_slice(&1u16.to_be_bytes());
assert_eq!(gpos_kerning_of(direct).pair(5, 6), 0);
}
#[test]
fn resolve_extension_requires_format_1() {
let mut d = Vec::new();
push16(&mut d, 2);
push16(&mut d, 2);
push32(&mut d, 8);
assert_eq!(resolve_extension(&d, 0), None);
d[0..2].copy_from_slice(&1u16.to_be_bytes());
assert_eq!(resolve_extension(&d, 0), Some((2, 8)));
assert!(parse_pair_subtable(&[0, 3], 0).is_none());
}
#[test]
fn value_record_x_advance_field_extraction() {
assert_eq!(value_record_x_advance(&[], 0, 0), Some(0));
let rec = [0, 0, 0, 0, 0x12, 0x34];
assert_eq!(value_record_x_advance(&rec, 0, 0x0007), Some(0x1234));
assert_eq!(value_record_x_advance(&[0], 0, 0x0004), None);
}
#[test]
fn coverage_and_class_def_malformed_and_boundary_variants() {
let mut cov = Vec::new();
push16(&mut cov, 2);
push16(&mut cov, 1);
push16(&mut cov, 20); push16(&mut cov, 10); push16(&mut cov, 0);
assert_eq!(parse_coverage_glyphs(&cov, 0), Some(Vec::new()));
assert_eq!(parse_coverage_glyphs(&[0, 3, 0, 0], 0), None);
assert!(parse_class_def(&[0, 3, 0, 0], 0).is_none());
let mut cd = Vec::new();
push16(&mut cd, 1);
push16(&mut cd, 5); push16(&mut cd, 2); push16(&mut cd, 7);
push16(&mut cd, 9);
let cd1 = parse_class_def(&cd, 0).expect("format 1 parses");
assert_eq!(cd1.class(5), 7);
assert_eq!(cd1.class(6), 9);
assert_eq!(cd1.class(7), 0); assert_eq!(cd1.class(4), 0);
let mut cd2b = Vec::new();
push16(&mut cd2b, 2);
push16(&mut cd2b, 1);
push16(&mut cd2b, 10);
push16(&mut cd2b, 20);
push16(&mut cd2b, 3);
let cd2 = parse_class_def(&cd2b, 0).expect("format 2 parses");
assert_eq!(cd2.class(15), 3);
assert_eq!(cd2.class(9), 0);
assert_eq!(cd2.class(21), 0);
}
#[test]
fn kern_subtable_format2_guards_class_ranges_and_empty_matrix() {
let st = KernSubtable::Format2 {
coverage: vec![5, 9],
class1: ClassDef::Format1 {
start: 5,
classes: vec![1, 0, 0, 0, 9],
},
class2: ClassDef::Format1 {
start: 6,
classes: vec![1, 7],
},
class1_count: 2,
class2_count: 2,
matrix: vec![0, 0, 0, -55],
};
assert_eq!(st.lookup(4, 6), None); assert_eq!(st.lookup(5, 6), Some(-55)); assert_eq!(st.lookup(9, 6), Some(0)); assert_eq!(st.lookup(5, 7), Some(0));
let empty = KernSubtable::Format2 {
coverage: vec![5],
class1: ClassDef::Format2 {
ranges: Vec::new(),
dense: true,
},
class2: ClassDef::Format2 {
ranges: Vec::new(),
dense: true,
},
class1_count: 1,
class2_count: 1,
matrix: Vec::new(),
};
assert_eq!(empty.lookup(5, 6), Some(0));
let kerning = Kerning {
subtables: vec![empty, st],
};
assert_eq!(kerning.pair(5, 6), 0);
assert_eq!(kerning.pair(4, 6), 0);
}
#[test]
fn for_each_ascii_pair_matches_brute_force_pair_on_bundled_faces() {
let base = env!("CARGO_MANIFEST_DIR");
let mut any_kerning_face = false;
for path in [
"/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf",
"/fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf",
"/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf",
"/fonts/computer-modern/cmunrm.ttf",
"/fonts/computer-modern/cmuntt.ttf",
"/fonts/noto-sans-math/NotoSansMathSymbols.ttf",
] {
let Ok(bytes) = std::fs::read(format!("{base}{path}")) else {
continue;
};
let Ok(font) = Font::parse(bytes) else {
continue;
};
let kern = font.gpos_kerning();
let glyphs: [u16; 128] = std::array::from_fn(|b| font.glyph_index(b as u8 as char));
let nonzero_pairs = kern
.subtables
.iter()
.map(|st| match st {
KernSubtable::Format1 { pairs } => pairs.len(),
KernSubtable::Format2 { coverage, .. } => coverage.len() * 128,
})
.sum::<usize>();
if nonzero_pairs == 0 {
continue;
}
any_kerning_face = true;
let mut enumerated = [0i16; 128 * 128];
let mut emitted = 0usize;
kern.for_each_ascii_pair(
|b| glyphs[usize::from(b)],
|l, r, v| {
enumerated[usize::from(l) * 128 + usize::from(r)] = v;
emitted += 1;
},
);
let mut brute = [0i16; 128 * 128];
for l in 0..128usize {
for r in 0..128usize {
brute[l * 128 + r] = kern.pair(glyphs[l], glyphs[r]);
}
}
assert_eq!(enumerated, brute, "enumeration != brute force for {path}");
assert_eq!(
emitted,
brute.iter().filter(|&&v| v != 0).count(),
"emission count != nonzero cells for {path}"
);
}
assert!(
any_kerning_face,
"test is vacuous: no bundled face has GPOS kerning"
);
}
#[test]
fn for_each_ascii_pair_first_match_duplicate_glyphs_and_zero_shadowing() {
let glyph_of = |b: u8| -> u16 {
match b {
10..=12 => u16::from(b) - 5,
13 | 14 => 9,
other => u16::from(other),
}
};
let covered_all_rights = KernSubtable::Format2 {
coverage: vec![5],
class1: ClassDef::Format1 {
start: 5,
classes: vec![1],
},
class2: ClassDef::Format1 {
start: 6,
classes: vec![1, 1],
},
class1_count: 2,
class2_count: 2,
matrix: vec![0, 0, 30, -25],
};
let specific = KernSubtable::Format1 {
pairs: PairMap::from_iter([(pair_key(5, 6), -99), (pair_key(9, 9), -12)]),
};
let out_of_range_class_zero = KernSubtable::Format2 {
coverage: vec![9],
class1: ClassDef::Format1 {
start: 9,
classes: vec![9], },
class2: ClassDef::Format2 {
ranges: Vec::new(),
dense: true,
},
class1_count: 2,
class2_count: 2,
matrix: vec![0; 4],
};
let defined_zero = KernSubtable::Format1 {
pairs: PairMap::from_iter([(pair_key(6, 6), 0)]),
};
let shadowed = KernSubtable::Format1 {
pairs: PairMap::from_iter([(pair_key(6, 6), -77)]),
};
let kerning = Kerning {
subtables: vec![
covered_all_rights,
specific,
out_of_range_class_zero,
defined_zero,
shadowed,
],
};
let mut enumerated = [0i16; 128 * 128];
kerning.for_each_ascii_pair(glyph_of, |l, r, v| {
enumerated[usize::from(l) * 128 + usize::from(r)] = v;
});
let mut brute = [0i16; 128 * 128];
for l in 0..128usize {
for r in 0..128usize {
brute[l * 128 + r] = kerning.pair(glyph_of(l as u8), glyph_of(r as u8));
}
}
assert_eq!(enumerated, brute);
let cell = |l: u8, r: u8| enumerated[usize::from(l) * 128 + usize::from(r)];
assert_eq!(cell(10, 11), -25); assert_eq!(cell(10, 12), -25); assert_eq!(cell(10, 13), 30); assert_eq!(cell(13, 13), -12); assert_eq!(cell(13, 14), -12);
assert_eq!(cell(14, 13), -12);
assert_eq!(cell(14, 14), -12);
assert_eq!(cell(11, 11), 0); assert_eq!(cell(13, 11), 0);
let mut calls = 0;
Kerning::default().for_each_ascii_pair(glyph_of, |_, _, _| calls += 1);
assert_eq!(calls, 0);
}
#[test]
fn pair_format1_skips_malformed_sets_and_truncated_records() {
let mut d = Vec::new();
push16(&mut d, 1); push16(&mut d, 20); push16(&mut d, 0x0004); push16(&mut d, 0); push16(&mut d, 2); push16(&mut d, 14); push16(&mut d, 14); push16(&mut d, 1); push16(&mut d, 6); push_i16(&mut d, -40);
push16(&mut d, 1); push16(&mut d, 1);
push16(&mut d, 5);
let st = parse_pair_subtable(&d, 0).expect("format 1 parses");
assert_eq!(st.lookup(5, 6), Some(-40));
assert_eq!(st.lookup(5, 7), None);
let mut d2 = Vec::new();
push16(&mut d2, 1);
push16(&mut d2, 12); push16(&mut d2, 0x0004);
push16(&mut d2, 0);
push16(&mut d2, 1);
push16(&mut d2, 0x4000); push16(&mut d2, 1);
push16(&mut d2, 1);
push16(&mut d2, 5);
let st2 = parse_pair_subtable(&d2, 0).expect("parses to an empty set");
assert_eq!(st2.lookup(5, 6), None);
let mut d3 = Vec::new();
push16(&mut d3, 1);
push16(&mut d3, 12); push16(&mut d3, 0x0004);
push16(&mut d3, 0);
push16(&mut d3, 1);
push16(&mut d3, 18); push16(&mut d3, 1); push16(&mut d3, 1);
push16(&mut d3, 5);
push16(&mut d3, 2); push16(&mut d3, 6);
push_i16(&mut d3, -40);
let st3 = parse_pair_subtable(&d3, 0).expect("parses the readable record");
assert_eq!(st3.lookup(5, 6), Some(-40));
assert_eq!(st3.lookup(5, 0), None);
}
#[test]
fn pair_format1_work_cap_stops_aliased_pair_set_expansion() {
for count in [65_535u16, 65_534] {
let mut d = Vec::new();
push16(&mut d, 1); push16(&mut d, 14); push16(&mut d, 0); push16(&mut d, 0); push16(&mut d, 2); push16(&mut d, 22); push16(&mut d, 22); push16(&mut d, 1); push16(&mut d, 2);
push16(&mut d, 5);
push16(&mut d, 6);
push16(&mut d, count); d.resize(d.len() + usize::from(count) * 2, 0); let st = parse_pair_subtable(&d, 0).expect("parses under the work cap");
assert_eq!(st.lookup(5, 0), Some(0), "count={count}");
assert_eq!(st.lookup(6, 0), None, "count={count}");
}
}
#[test]
fn pair_format2_empty_value_formats_and_oversized_matrix() {
let mut d = Vec::new();
push16(&mut d, 2); push16(&mut d, 16); push16(&mut d, 0); push16(&mut d, 0); push16(&mut d, 22); push16(&mut d, 22); push16(&mut d, 1); push16(&mut d, 1); push16(&mut d, 1); push16(&mut d, 1);
push16(&mut d, 3);
push16(&mut d, 1); push16(&mut d, 0);
push16(&mut d, 0);
let st = parse_pair_subtable(&d, 0).expect("empty-value format 2 parses");
assert!(matches!(
&st,
KernSubtable::Format2 { matrix, .. } if matrix.is_empty()
));
assert_eq!(st.lookup(3, 42), Some(0));
assert_eq!(st.lookup(4, 42), None);
let mut big = Vec::new();
push16(&mut big, 2);
push16(&mut big, 16);
push16(&mut big, 0x0004);
push16(&mut big, 0);
push16(&mut big, 22);
push16(&mut big, 22);
push16(&mut big, 0xFFFF);
push16(&mut big, 0xFFFF);
assert!(parse_pair_subtable(&big, 0).is_none());
}
#[test]
fn gsub_extension_lookup_parses_greedy_ligatures() {
let ligs = gsub_font(1, 4, 0);
assert!(!ligs.is_empty());
assert!(Ligatures::default().is_empty());
assert_eq!(ligs.substitute(&[10, 11, 12]), vec![99]);
assert_eq!(ligs.substitute(&[10, 11, 7]), vec![77, 7]);
assert_eq!(ligs.substitute(&[10, 7]), vec![10, 7]);
assert_eq!(
ligs.substitute_with_spans(&[10, 11, 12, 10, 11]),
vec![(99, 3), (77, 2)]
);
}
#[test]
fn substitute_with_spans_into_matches_allocating_variant() {
let ligs = gsub_font(1, 4, 0);
let corpora: [&[u16]; 7] = [
&[],
&[10, 11, 12, 10, 11],
&[10, 11, 7],
&[10, 7],
&[99, 99, 99],
&[10, 11, 12, 10, 11, 12, 10, 11],
&[7, 8, 9, 10],
];
let mut scratch = vec![(u16::MAX, usize::MAX); 4];
for gids in corpora {
let mut into = Vec::new();
ligs.substitute_with_spans_into(gids, &mut into);
assert_eq!(into, ligs.substitute_with_spans(gids));
ligs.substitute_with_spans_into(gids, &mut scratch);
assert_eq!(scratch, into);
}
}
#[test]
fn max_rule_len_reports_longest_rule() {
assert_eq!(gsub_font(1, 4, 0).max_rule_len(), 3);
assert_eq!(Ligatures::default().max_rule_len(), 1);
}
#[test]
fn gsub_skips_foreign_extensions_and_bad_lookup_indices() {
assert!(gsub_font(1, 2, 0).is_empty());
assert!(gsub_font(2, 4, 0).is_empty());
assert!(gsub_font(1, 4, 9).is_empty());
}
#[test]
fn gsub_truncated_structures_yield_no_ligatures() {
for end in [16usize, 20, 22, 26, 28, 32, 34] {
let mut g = gsub_table(1, 4, 0);
g.truncate(end);
assert!(gsub_ligatures_of(g).is_empty(), "end={end}");
}
let mut over = gsub_table(1, 4, 0);
over[10..12].copy_from_slice(&12u16.to_be_bytes());
assert_eq!(gsub_ligatures_of(over).substitute(&[10, 11]), vec![77]);
let mut direct = gsub_table(1, 4, 0);
direct[28..30].copy_from_slice(&1u16.to_be_bytes());
assert!(gsub_ligatures_of(direct).is_empty());
}
#[test]
fn ligature_subst_skips_malformed_entries() {
let mut rules: std::collections::BTreeMap<u16, Vec<LigRule>> =
std::collections::BTreeMap::new();
parse_ligature_subst(&[0, 2, 0, 0], 0, &mut rules);
assert!(rules.is_empty());
parse_ligature_subst(&[], 0, &mut rules); parse_ligature_subst(&[0, 1], 0, &mut rules); parse_ligature_subst(&[0, 1, 0, 8], 0, &mut rules); parse_ligature_subst(&[0, 1, 0, 6, 0, 1, 0, 3], 0, &mut rules); assert!(rules.is_empty());
let mut d = Vec::new();
push16(&mut d, 1); push16(&mut d, 8); push16(&mut d, 1); push16(&mut d, 0x4000); push16(&mut d, 1); push16(&mut d, 1);
push16(&mut d, 10);
parse_ligature_subst(&d, 0, &mut rules);
assert!(rules.is_empty());
let mut d2 = Vec::new();
push16(&mut d2, 1); push16(&mut d2, 20); push16(&mut d2, 2); push16(&mut d2, 10); push16(&mut d2, 10); push16(&mut d2, 1); push16(&mut d2, 4); push16(&mut d2, 77); push16(&mut d2, 2); push16(&mut d2, 11); push16(&mut d2, 1); push16(&mut d2, 1);
push16(&mut d2, 10);
parse_ligature_subst(&d2, 0, &mut rules);
assert_eq!(rules.len(), 1);
assert_eq!(rules[&10].len(), 1);
assert_eq!(rules[&10][0].components, vec![11]);
assert_eq!(rules[&10][0].ligature, 77);
rules.clear();
let mut d3 = Vec::new();
push16(&mut d3, 1); push16(&mut d3, 8); push16(&mut d3, 1); push16(&mut d3, 14); push16(&mut d3, 1); push16(&mut d3, 1);
push16(&mut d3, 10);
push16(&mut d3, 4); push16(&mut d3, 10); push16(&mut d3, 20); push16(&mut d3, 0x4000); push16(&mut d3, 14); push16(&mut d3, 33); push16(&mut d3, 0); push16(&mut d3, 88); push16(&mut d3, 5); push16(&mut d3, 11);
push16(&mut d3, 12);
assert_eq!(d3.len(), 36);
parse_ligature_subst(&d3, 0, &mut rules);
assert!(rules.is_empty());
}
#[test]
fn ligature_subst_work_cap_stops_aliased_sets() {
for lig_count in [65_535u16, 65_534] {
let mut d = Vec::new();
push16(&mut d, 1); push16(&mut d, 10); push16(&mut d, 2); push16(&mut d, 18); push16(&mut d, 18); push16(&mut d, 1); push16(&mut d, 2);
push16(&mut d, 10);
push16(&mut d, 11);
push16(&mut d, lig_count); let mut rules: std::collections::BTreeMap<u16, Vec<LigRule>> =
std::collections::BTreeMap::new();
parse_ligature_subst(&d, 0, &mut rules);
assert!(rules.is_empty(), "lig_count={lig_count}");
}
}
fn f32_to_fixed(v: f32) -> i32 {
(f64::from(v) * 65536.0).round() as i32
}
fn f32_to_f2dot14(v: f32) -> i16 {
(v * 16384.0).round() as i16
}
fn push_i32(out: &mut Vec<u8>, v: i32) {
out.extend_from_slice(&v.to_be_bytes());
}
fn fvar_table(
axes: &[(&[u8; 4], f32, f32, f32, u16)],
instances: &[(u16, &[f32])],
with_ps_name: bool,
) -> Vec<u8> {
let axis_count = u16::try_from(axes.len()).unwrap();
let instance_count = u16::try_from(instances.len()).unwrap();
let instance_size = 4 + 4 * axis_count + u16::from(with_ps_name) * 2;
let mut t = Vec::new();
push16(&mut t, 1); push16(&mut t, 0); push16(&mut t, 16); push16(&mut t, 0); push16(&mut t, axis_count);
push16(&mut t, 20); push16(&mut t, instance_count);
push16(&mut t, instance_size);
for &(tag, min, default, max, name_id) in axes {
t.extend_from_slice(&tag[..]);
push_i32(&mut t, f32_to_fixed(min));
push_i32(&mut t, f32_to_fixed(default));
push_i32(&mut t, f32_to_fixed(max));
push16(&mut t, 0); push16(&mut t, name_id);
}
for &(name_id, coords) in instances {
push16(&mut t, name_id);
push16(&mut t, 0); for &c in coords {
push_i32(&mut t, f32_to_fixed(c));
}
if with_ps_name {
push16(&mut t, name_id.saturating_add(1000));
}
}
t
}
fn avar_table(maps: &[&[(f32, f32)]]) -> Vec<u8> {
let mut t = Vec::new();
push16(&mut t, 1); push16(&mut t, 0); push16(&mut t, 0); push16(&mut t, u16::try_from(maps.len()).unwrap());
for axis in maps {
push16(&mut t, u16::try_from(axis.len()).unwrap());
for &(from, to) in *axis {
push_i16(&mut t, f32_to_f2dot14(from));
push_i16(&mut t, f32_to_f2dot14(to));
}
}
t
}
fn variation_font(fvar: Vec<u8>, avar: Option<Vec<u8>>) -> Font {
let mut tables = base_tables(1, 1, 1000, hmtx_long(&[(500, 0)]), cmap4_simple(&[]));
tables.push((b"fvar", fvar));
if let Some(avar) = avar {
tables.push((b"avar", avar));
}
parse(&tables)
}
fn fmd_test_vf_bytes() -> Vec<u8> {
let fvar = fvar_table(
&[(b"wght", 100.0, 400.0, 900.0, 256)],
&[(258, &[400.0]), (259, &[700.0])],
true,
);
let avar = avar_table(&[&[(-1.0, -1.0), (0.0, 0.0), (1.0, 1.0)]]);
let mut tables = base_tables(1, 1, 1000, hmtx_long(&[(500, 0)]), cmap4_simple(&[]));
tables.push((b"fvar", fvar));
tables.push((b"avar", avar));
sfnt(0x0001_0000, &tables)
}
fn log_check(id: &str, subject: &str, ok: bool) {
eprintln!(
"check id={id} subject={subject} outcome={}",
if ok { "PASS" } else { "FAIL" }
);
assert!(ok, "{id}: {subject}");
}
#[test]
fn fvar_axes_tags_and_named_instances() {
let font = variation_font(
fvar_table(
&[
(b"wght", 100.0, 400.0, 900.0, 256),
(b"wdth", 75.0, 100.0, 125.0, 257),
],
&[(258, &[400.0, 100.0]), (259, &[700.0, 100.0])],
true,
),
None,
);
let axes = font.axes();
log_check("gk3v.1.axes.count", "two axes", axes.len() == 2);
log_check(
"gk3v.1.axes.wght",
"first tag wght",
axes[0].tag == *b"wght",
);
log_check(
"gk3v.1.axes.wdth",
"second tag wdth",
axes[1].tag == *b"wdth",
);
let wght = font.instance_bounds(*b"wght").expect("wght present");
log_check(
"gk3v.1.bounds.wght",
"wght 100/400/900",
(wght.min - 100.0).abs() < 1e-4
&& (wght.default - 400.0).abs() < 1e-4
&& (wght.max - 900.0).abs() < 1e-4,
);
log_check(
"gk3v.1.bounds.missing",
"unknown tag is None",
font.instance_bounds(*b"opsz").is_none(),
);
let inst = font.named_instances();
log_check("gk3v.1.inst.count", "two named instances", inst.len() == 2);
log_check(
"gk3v.1.inst.regular",
"Regular at wght=400",
inst[0].subfamily_name_id == 258
&& (inst[0].coordinates[0] - 400.0).abs() < 1e-4
&& inst[0].postscript_name_id == Some(1258),
);
log_check(
"gk3v.1.inst.bold",
"Bold at wght=700",
inst[1].subfamily_name_id == 259 && (inst[1].coordinates[0] - 700.0).abs() < 1e-4,
);
log_check(
"gk3v.1.static",
"static face has no axes",
parse(&base_tables(
1,
1,
1000,
hmtx_long(&[(500, 0)]),
cmap4_simple(&[]),
))
.axes()
.is_empty(),
);
}
#[test]
fn avar_clamp_edges_map_to_endpoints() {
let font = variation_font(
fvar_table(&[(b"wght", 100.0, 400.0, 900.0, 256)], &[], false),
Some(avar_table(&[&[
(-1.0, -1.0),
(0.0, 0.0),
(0.5, 0.25),
(1.0, 1.0),
]])),
);
let below = font.normalized_axis(*b"wght", 0.0);
let at_min = font.normalized_axis(*b"wght", 100.0);
let at_def = font.normalized_axis(*b"wght", 400.0);
let at_max = font.normalized_axis(*b"wght", 900.0);
let above = font.normalized_axis(*b"wght", 2000.0);
log_check(
"gk3v.1.avar.below",
"below-min → -1",
below.is_some_and(|v| (v + 1.0).abs() < 1e-4),
);
log_check(
"gk3v.1.avar.min",
"min → -1",
at_min.is_some_and(|v| (v + 1.0).abs() < 1e-4),
);
log_check(
"gk3v.1.avar.default",
"default → 0",
at_def.is_some_and(|v| v.abs() < 1e-4),
);
log_check(
"gk3v.1.avar.max",
"max → +1",
at_max.is_some_and(|v| (v - 1.0).abs() < 1e-4),
);
log_check(
"gk3v.1.avar.above",
"above-max → +1",
above.is_some_and(|v| (v - 1.0).abs() < 1e-4),
);
let mid = font.normalized_axis(*b"wght", 650.0);
log_check(
"gk3v.1.avar.mid",
"650 → avar(0.5)=0.25",
mid.is_some_and(|v| (v - 0.25).abs() < 1e-3),
);
}
#[test]
fn fvar_avar_truncation_and_hostile_headers() {
let good = fvar_table(&[(b"wght", 100.0, 400.0, 900.0, 256)], &[], false);
let font = variation_font(good[..10].to_vec(), None);
log_check(
"gk3v.1.trunc.header",
"truncated fvar header → no axes",
font.axes().is_empty(),
);
let mut bad_ver = good.clone();
bad_ver[0..2].copy_from_slice(&2u16.to_be_bytes());
log_check(
"gk3v.1.trunc.version",
"fvar major!=1 → no axes",
variation_font(bad_ver, None).axes().is_empty(),
);
let mut tiny_axis = good.clone();
tiny_axis[10..12].copy_from_slice(&8u16.to_be_bytes());
log_check(
"gk3v.1.trunc.axisSize",
"axisSize < 20 → no axes",
variation_font(tiny_axis, None).axes().is_empty(),
);
let mismatched = variation_font(
fvar_table(&[(b"wght", 100.0, 400.0, 900.0, 256)], &[], false),
Some(avar_table(&[
&[(-1.0, -1.0), (1.0, 1.0)],
&[(-1.0, -1.0), (1.0, 1.0)],
])),
);
log_check(
"gk3v.1.avar.mismatch",
"avar axisCount mismatch → identity",
mismatched.axes().len() == 1
&& mismatched
.normalized_axis(*b"wght", 100.0)
.is_some_and(|v| (v + 1.0).abs() < 1e-4),
);
}
#[test]
fn fvar_avar_lcg_mutation_never_panics() {
let base = fmd_test_vf_bytes();
let mut state = 0xC0FF_EE00u64;
let mut lcg = move || {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
(state >> 33) as usize
};
for round in 0..128 {
let mut mutated = base.clone();
for _ in 0..8 {
let pos = lcg() % mutated.len();
let bit = 1u8 << (lcg() % 8);
mutated[pos] ^= bit;
}
let outcome = std::panic::catch_unwind(move || {
if let Ok(font) = Font::parse(mutated) {
let _ = font.axes();
let _ = font.named_instances();
let _ = font.instance_bounds(*b"wght");
let _ = font.normalized_axis(*b"wght", 0.0);
let _ = font.normalized_axis(*b"wght", 400.0);
let _ = font.normalized_axis(*b"wght", 9999.0);
}
});
log_check(
"gk3v.1.lcg",
&format!("round {round} no panic"),
outcome.is_ok(),
);
}
for cut in (0..base.len()).step_by(7) {
let truncated = base[..cut].to_vec();
let outcome = std::panic::catch_unwind(move || {
if let Ok(font) = Font::parse(truncated) {
let _ = font.axes();
let _ = font.normalized_axis(*b"wght", 100.0);
}
});
log_check(
"gk3v.1.trunc.sweep",
&format!("cut {cut} no panic"),
outcome.is_ok(),
);
}
}
#[test]
fn fmd_test_vf_fixture_round_trip() {
let bytes = fmd_test_vf_bytes();
let committed = include_bytes!("../fonts/test-variable/FmdTestVF.ttf");
log_check(
"gk3v.1.fixture.bytes",
"committed TTF matches generator",
bytes.as_slice() == committed,
);
let font = Font::parse(bytes).expect("test VF parses");
log_check(
"gk3v.1.fixture.axes",
"committed-shape VF has wght",
font.axes().len() == 1 && font.axes()[0].tag == *b"wght",
);
log_check(
"gk3v.1.fixture.inst",
"Regular + Bold instances",
font.named_instances().len() == 2,
);
if std::env::var("FMD_DUMP_TEST_VF").ok().as_deref() == Some("1") {
std::fs::write("/tmp/FmdTestVF.ttf", fmd_test_vf_bytes()).unwrap();
eprintln!("check id=gk3v.1.dump subject=/tmp/FmdTestVF.ttf outcome=PASS");
}
}
}