use crate::model::Error;
use crate::{
model::glyph::outline::PathSink,
ps::{
cff::{
blend::BlendState, charset::Charset, dict, encoding::Encoding as RawEncoding,
fd_select::FdSelect, index::Index,
},
charstring::{self, CommandSink, NopFilterSink, TransformSink},
encoding::PredefinedEncoding,
hinting::HintingParams,
string::Sid,
transform::{ScaledFontMatrix, Transform},
},
tables::{cff, cff2, variations::ItemVariationStore},
FontData, FontRead,
};
use core::ops::Range;
use types::{BoundingBox, F2Dot14, Fixed, GlyphId};
use yoke::Yokeable;
#[derive(Clone, Yokeable)]
pub struct CffFontRef<'a> {
data: &'a [u8],
is_cff2: bool,
upem: i32,
global_subrs: Index<'a>,
top_dict: TopDict<'a>,
top_dict_index: u16,
}
impl<'a> CffFontRef<'a> {
#[cfg(test)]
pub(crate) fn sid_private_dict_range(&self) -> Option<Range<usize>> {
match &self.top_dict.kind {
CffFontKind::Sid { private_dict, .. } => {
Some(private_dict.start as usize..private_dict.end as usize)
}
_ => None,
}
}
pub fn new(data: &'a [u8], top_dict_index: u32, upem: Option<i32>) -> Result<Self, Error> {
let version: u8 = FontData::new(data).read_at(0)?;
match version {
1 => Self::new_cff(data, top_dict_index, upem),
2 => Self::new_cff2(data, upem),
_ => Err(Error::Malformed),
}
}
pub fn new_cff(data: &'a [u8], top_dict_index: u32, upem: Option<i32>) -> Result<Self, Error> {
let cff = cff::Cff::read(data.into())?;
let top_dict_data = cff
.top_dicts()
.get(top_dict_index as usize)
.ok_or(Error::Malformed)?;
let top_dict_index: u16 = top_dict_index.try_into().map_err(|_| Error::Malformed)?;
Self::new_impl(
data,
false,
upem,
top_dict_data,
top_dict_index,
cff.strings().into(),
cff.global_subrs().into(),
)
}
pub fn new_cff2(data: &'a [u8], upem: Option<i32>) -> Result<Self, Error> {
let cff = cff2::Cff2::read(data.into())?;
Self::new_impl(
data,
true,
upem,
cff.top_dict_data(),
0,
Index::Empty,
cff.global_subrs().into(),
)
}
fn new_impl(
data: &'a [u8],
is_cff2: bool,
upem: Option<i32>,
top_dict_data: &'a [u8],
top_dict_index: u16,
strings: Index<'a>,
global_subrs: Index<'a>,
) -> Result<Self, Error> {
let top_dict = TopDict::new(data, top_dict_data, strings, is_cff2)?;
let top_upem = top_dict.matrix.map(|mat| mat.scale).unwrap_or(1000);
Ok(Self {
data,
is_cff2,
upem: upem.unwrap_or(top_upem),
global_subrs,
top_dict,
top_dict_index,
})
}
pub fn data(&self) -> &'a [u8] {
self.data
}
pub fn version(&self) -> u16 {
if self.is_cff2 {
2
} else {
1
}
}
pub fn metadata(&self) -> Option<Metadata<'a>> {
Metadata::new(self.data, self.top_dict_index)
}
pub fn is_cid(&self) -> bool {
matches!(&self.top_dict.kind, CffFontKind::Cid { .. })
}
pub fn global_subrs(&self) -> &Index<'a> {
&self.global_subrs
}
pub fn num_glyphs(&self) -> u32 {
self.top_dict.charstrings.count()
}
pub fn charstrings(&self) -> &Index<'a> {
&self.top_dict.charstrings
}
pub fn strings(&self) -> Option<&Index<'a>> {
match &self.top_dict.kind {
CffFontKind::Sid { strings, .. } => Some(strings),
_ => None,
}
}
pub fn string(&self, sid: Sid) -> Option<&'a [u8]> {
match sid.resolve_standard() {
Ok(s) => Some(s),
Err(idx) => self.strings()?.get(idx),
}
}
pub fn charset(&self) -> Option<Charset<'a>> {
Charset::new(
self.data.into(),
self.top_dict.charset_offset.get()?,
self.top_dict.charstrings.count(),
)
}
pub fn encoding(&self) -> Option<Encoding<'a>> {
let charset = self.charset()?;
let encoding = RawEncoding::new(self.data, self.top_dict.encoding_offset.get()?)?;
Some(Encoding { encoding, charset })
}
pub fn matrix(&self) -> Option<&ScaledFontMatrix> {
self.top_dict.matrix.as_ref()
}
pub fn upem(&self) -> i32 {
self.upem
}
pub fn var_store(&self) -> Option<&ItemVariationStore<'a>> {
self.top_dict.var_store.as_ref()
}
pub fn num_subfonts(&self) -> u16 {
match &self.top_dict.kind {
CffFontKind::Sid { .. } => 1,
CffFontKind::Cid { fd_array, .. } | CffFontKind::Cff2 { fd_array, .. } => {
fd_array.count() as u16
}
}
}
pub fn subfont_index(&self, gid: GlyphId) -> Option<u16> {
match &self.top_dict.kind {
CffFontKind::Sid { .. } => Some(0),
CffFontKind::Cid { fd_select, .. } | CffFontKind::Cff2 { fd_select, .. } => fd_select
.as_ref()
.map_or(Some(0), |fds| fds.font_index(gid)),
}
}
pub fn subfont(&self, index: u16, coords: &[F2Dot14]) -> Result<Subfont, Error> {
let blend = self.blend_state(0, coords);
match &self.top_dict.kind {
CffFontKind::Sid { private_dict, .. } => Subfont::new(
self.data,
private_dict.start as usize..private_dict.end as usize,
blend,
None,
),
CffFontKind::Cid { fd_array, .. } | CffFontKind::Cff2 { fd_array, .. } => {
let font_dict =
FontDict::new(fd_array.get(index as usize).ok_or(Error::Malformed)?)?;
Subfont::new(
self.data,
font_dict.private_dict_range,
blend,
font_dict.matrix,
)
}
}
}
pub fn subfont_hinted(
&self,
index: u16,
coords: &[F2Dot14],
) -> Result<(Subfont, HintingParams), Error> {
let blend = self.blend_state(0, coords);
match &self.top_dict.kind {
CffFontKind::Sid { private_dict, .. } => Subfont::new_hinted(
self.data,
private_dict.start as usize..private_dict.end as usize,
blend,
None,
),
CffFontKind::Cid { fd_array, .. } | CffFontKind::Cff2 { fd_array, .. } => {
let font_dict =
FontDict::new(fd_array.get(index as usize).ok_or(Error::Malformed)?)?;
Subfont::new_hinted(
self.data,
font_dict.private_dict_range,
blend,
font_dict.matrix,
)
}
}
}
pub fn transform(&self, subfont: &Subfont, ppem: Option<f32>) -> Transform {
let mut scale = ppem.map(|ppem| Transform::compute_scale(ppem, self.upem));
let scaled_matrix = if let Some(top_matrix) = self.top_dict.matrix {
if let Some(sub_matrix) = subfont.matrix {
Some(top_matrix.combine(&sub_matrix))
} else {
Some(top_matrix)
}
} else {
subfont.matrix.map(|matrix| matrix.normalize())
};
if let Some(matrix) = scaled_matrix.as_ref() {
if matrix.scale != self.upem {
let original_scale = scale.unwrap_or(Fixed::from_i32(64));
scale = Some(
original_scale
.mul_div(Fixed::from_bits(self.upem), Fixed::from_bits(matrix.scale)),
);
}
}
Transform {
matrix: scaled_matrix
.map(|scaled_mat| scaled_mat.matrix)
.unwrap_or_default(),
scale,
}
}
pub fn evaluate_width(
&self,
subfont: &Subfont,
gid: GlyphId,
coords: &[F2Dot14],
) -> Result<Option<Fixed>, Error> {
let transform = self.transform(subfont, None);
Ok(self
.charstring_width(subfont, gid, coords)?
.map(|width| transform.transform_h_metric(width)))
}
fn charstring_width(
&self,
subfont: &Subfont,
gid: GlyphId,
coords: &[F2Dot14],
) -> Result<Option<Fixed>, Error> {
let charstrings = self.top_dict.charstrings.clone();
let blend = self.blend_state(subfont.vs_index, coords);
let subrs = if subfont.subrs_offset != 0 {
let data = self
.data
.get(subfont.subrs_offset as usize..)
.ok_or(Error::Malformed)?;
Index::new(data, self.is_cff2)?
} else {
Index::Empty
};
let charstring_data = charstrings
.get(gid.to_u32() as usize)
.ok_or(Error::Malformed)?;
let ctx = (self.data, &charstrings, &self.global_subrs, &subrs);
Ok(
match charstring::evaluate_width(&ctx, blend, charstring_data)? {
Some(width) => Some(width + subfont.nominal_width),
None => subfont.default_width,
},
)
}
pub fn evaluate_charstring(
&self,
subfont: &Subfont,
gid: GlyphId,
coords: &[F2Dot14],
sink: &mut impl CommandSink,
) -> Result<Option<Fixed>, Error> {
let charstrings = self.top_dict.charstrings.clone();
let blend = self.blend_state(subfont.vs_index, coords);
let subrs = if subfont.subrs_offset != 0 {
let data = self
.data
.get(subfont.subrs_offset as usize..)
.ok_or(Error::Malformed)?;
Index::new(data, self.is_cff2)?
} else {
Index::Empty
};
let charstring_data = charstrings
.get(gid.to_u32() as usize)
.ok_or(Error::Malformed)?;
let ctx = (self.data, &charstrings, &self.global_subrs, &subrs);
if let Some(width) = charstring::evaluate(&ctx, blend, charstring_data, sink)? {
Ok(Some(width + subfont.nominal_width))
} else {
Ok(subfont.default_width)
}
}
pub fn advance(
&self,
subfont: &Subfont,
gid: GlyphId,
coords: &[F2Dot14],
ppem: Option<f32>,
) -> Result<Option<f32>, Error> {
let transform = self.transform(subfont, ppem);
Ok(self
.charstring_width(subfont, gid, coords)?
.map(|width| transform.transform_h_metric(width).to_f32().max(0.0)))
}
pub fn evaluate_extents(
&self,
subfont: &Subfont,
gid: GlyphId,
coords: &[F2Dot14],
) -> Result<Option<BoundingBox<Fixed>>, Error> {
let mut bounds = charstring::ControlBoundsSink::new();
let mut nop_filter = charstring::NopFilterSink::new(&mut bounds);
let transform = self.transform(subfont, None);
let mut transformer = charstring::TransformSink::new(&mut nop_filter, transform);
self.evaluate_charstring(subfont, gid, coords, &mut transformer)?;
Ok(bounds.bounding_box())
}
pub fn extents(
&self,
subfont: &Subfont,
gid: GlyphId,
coords: &[F2Dot14],
ppem: Option<f32>,
) -> Result<Option<BoundingBox<f32>>, Error> {
let mut bounds = charstring::ControlBoundsSink::new();
let mut nop_filter = charstring::NopFilterSink::new(&mut bounds);
let transform = self.transform(subfont, ppem);
let mut transformer = charstring::TransformSink::new(&mut nop_filter, transform);
self.evaluate_charstring(subfont, gid, coords, &mut transformer)?;
Ok(bounds.bounding_box().map(|b| BoundingBox {
x_min: b.x_min.to_f32(),
y_min: b.y_min.to_f32(),
x_max: b.x_max.to_f32(),
y_max: b.y_max.to_f32(),
}))
}
pub fn draw(
&self,
subfont: &Subfont,
gid: GlyphId,
coords: &[F2Dot14],
ppem: Option<f32>,
sink: &mut impl PathSink,
) -> Result<Option<f32>, Error> {
let mut nop_filter = NopFilterSink::new(sink);
let transform = self.transform(subfont, ppem);
let mut transformer = TransformSink::new(&mut nop_filter, transform);
let width = self.evaluate_charstring(subfont, gid, coords, &mut transformer)?;
Ok(width.map(|w| transform.transform_h_metric(w).to_f32().max(0.0)))
}
fn blend_state(&self, vs_index: u16, coords: &'a [F2Dot14]) -> Option<BlendState<'a>> {
self.top_dict
.var_store
.as_ref()
.and_then(|store| BlendState::new(store.clone(), coords, vs_index).ok())
}
}
#[derive(Clone)]
pub struct Encoding<'a> {
encoding: RawEncoding<'a>,
charset: Charset<'a>,
}
impl<'a> Encoding<'a> {
pub fn predefined(&self) -> Option<PredefinedEncoding> {
if let RawEncoding::Predefined(pre) = &self.encoding {
Some(*pre)
} else {
None
}
}
pub fn charset(&self) -> &Charset<'a> {
&self.charset
}
pub fn map(&self, code: u8) -> Option<GlyphId> {
self.encoding.map(&self.charset, code)
}
}
#[derive(Clone)]
enum CffFontKind<'a> {
Sid {
strings: Index<'a>,
private_dict: Range<u32>,
},
Cid {
fd_select: Option<FdSelect<'a>>,
fd_array: Index<'a>,
},
Cff2 {
fd_select: Option<FdSelect<'a>>,
fd_array: Index<'a>,
},
}
#[derive(Copy, Clone, Default, Debug)]
pub struct Subfont {
subrs_offset: u32,
default_width: Option<Fixed>,
nominal_width: Fixed,
matrix: Option<ScaledFontMatrix>,
vs_index: u16,
}
impl Subfont {
fn new(
data: &[u8],
range: Range<usize>,
blend: Option<BlendState>,
matrix: Option<ScaledFontMatrix>,
) -> Result<Self, Error> {
let mut subfont = Self {
matrix,
..Default::default()
};
let data = data.get(range.clone()).ok_or(Error::Malformed)?;
for entry in dict::unhinted_private_entries(data, blend).filter_map(|e| e.ok()) {
match entry {
dict::Entry::SubrsOffset(offset) => {
subfont.subrs_offset =
range.start.checked_add(offset).ok_or(Error::Malformed)? as u32;
}
dict::Entry::VariationStoreIndex(index) => subfont.vs_index = index,
dict::Entry::DefaultWidthX(width) => subfont.default_width = Some(width.floor()),
dict::Entry::NominalWidthX(width) => subfont.nominal_width = width.floor(),
_ => {}
}
}
Ok(subfont)
}
fn new_hinted(
data: &[u8],
range: Range<usize>,
blend: Option<BlendState>,
matrix: Option<ScaledFontMatrix>,
) -> Result<(Self, HintingParams), Error> {
let mut subfont = Self {
matrix,
..Default::default()
};
let mut params = HintingParams::default();
let data = data.get(range.clone()).ok_or(Error::Malformed)?;
for entry in dict::entries(data, blend).filter_map(|e| e.ok()) {
match entry {
dict::Entry::SubrsOffset(offset) => {
subfont.subrs_offset =
range.start.checked_add(offset).ok_or(Error::Malformed)? as u32;
}
dict::Entry::VariationStoreIndex(index) => subfont.vs_index = index,
dict::Entry::DefaultWidthX(width) => subfont.default_width = Some(width.floor()),
dict::Entry::NominalWidthX(width) => subfont.nominal_width = width.floor(),
dict::Entry::BlueValues(values) => params.blues = values,
dict::Entry::FamilyBlues(values) => params.family_blues = values,
dict::Entry::OtherBlues(values) => params.other_blues = values,
dict::Entry::FamilyOtherBlues(values) => params.family_other_blues = values,
dict::Entry::BlueScale(value) => params.blue_scale = value,
dict::Entry::BlueShift(value) => params.blue_shift = value,
dict::Entry::BlueFuzz(value) => params.blue_fuzz = value,
dict::Entry::LanguageGroup(group) => params.language_group = group,
_ => {}
}
}
Ok((subfont, params))
}
pub fn subrs_offset(&self) -> u32 {
self.subrs_offset
}
pub fn default_width(&self) -> Option<Fixed> {
self.default_width
}
pub fn nominal_width(&self) -> Fixed {
self.nominal_width
}
pub fn matrix(&self) -> Option<&ScaledFontMatrix> {
self.matrix.as_ref()
}
pub fn vs_index(&self) -> u16 {
self.vs_index
}
}
#[derive(Copy, Clone)]
struct MaybeOffset(u32);
impl MaybeOffset {
fn get(self) -> Option<usize> {
(self.0 != u32::MAX).then_some(self.0 as usize)
}
}
impl Default for MaybeOffset {
fn default() -> Self {
Self(u32::MAX)
}
}
#[derive(Clone)]
struct TopDict<'a> {
charstrings: Index<'a>,
charset_offset: MaybeOffset,
encoding_offset: MaybeOffset,
matrix: Option<ScaledFontMatrix>,
var_store: Option<ItemVariationStore<'a>>,
kind: CffFontKind<'a>,
}
impl<'a> TopDict<'a> {
fn new(
cff_data: &'a [u8],
top_dict_data: &[u8],
strings: Index<'a>,
is_cff2: bool,
) -> Result<Self, Error> {
let mut has_ros = false;
let mut charstrings = None;
let [mut charset_offset, mut encoding_offset] = if is_cff2 {
[MaybeOffset::default(); 2]
} else {
[MaybeOffset(0); 2]
};
let mut fd_array = None;
let mut fd_select = None;
let mut private_dict_range = 0..0;
let mut matrix = None;
let mut var_store = None;
for entry in dict::entries(top_dict_data, None).filter_map(|e| e.ok()) {
match entry {
dict::Entry::Ros { .. } => has_ros = true,
dict::Entry::CharstringsOffset(offset) => {
charstrings = cff_data
.get(offset..)
.and_then(|data| Index::new(data, is_cff2).ok());
}
dict::Entry::Charset(offset) => charset_offset = MaybeOffset(offset as u32),
dict::Entry::Encoding(offset) => encoding_offset = MaybeOffset(offset as u32),
dict::Entry::FdArrayOffset(offset) => {
fd_array = cff_data
.get(offset..)
.and_then(|data| Index::new(data, is_cff2).ok());
}
dict::Entry::FdSelectOffset(offset) => {
fd_select = cff_data
.get(offset..)
.and_then(|data| FdSelect::read(data.into()).ok());
}
dict::Entry::PrivateDictRange(range) => {
let _ = cff_data.get(range.clone()).ok_or(Error::Malformed)?;
private_dict_range = range;
}
dict::Entry::FontMatrix(font_matrix) => {
matrix = Some(font_matrix.normalize());
}
dict::Entry::VariationStoreOffset(offset) if is_cff2 => {
let offset = offset.checked_add(2).ok_or(Error::Malformed)?;
var_store = Some(ItemVariationStore::read(
cff_data.get(offset..).unwrap_or_default().into(),
)?);
}
_ => {}
}
}
let charstrings = charstrings.ok_or(Error::MissingResource)?;
let kind = if let Some(fd_array) = fd_array {
if is_cff2 {
CffFontKind::Cff2 {
fd_array,
fd_select,
}
} else {
CffFontKind::Cid {
fd_array,
fd_select,
}
}
} else {
if has_ros || is_cff2 {
return Err(Error::MissingResource);
}
CffFontKind::Sid {
strings,
private_dict: private_dict_range.start as u32..private_dict_range.end as u32,
}
};
Ok(Self {
charset_offset,
encoding_offset,
charstrings,
matrix,
kind,
var_store,
})
}
}
#[derive(Default)]
struct FontDict {
private_dict_range: Range<usize>,
matrix: Option<ScaledFontMatrix>,
}
impl FontDict {
fn new(dict_data: &[u8]) -> Result<Self, Error> {
let mut range = None;
let mut matrix = None;
for entry in dict::entries(dict_data, None) {
match entry? {
dict::Entry::PrivateDictRange(dict_range) => {
range = Some(dict_range);
}
dict::Entry::FontMatrix(font_matrix) => {
matrix = Some(font_matrix);
}
_ => {}
}
}
Ok(Self {
private_dict_range: range.ok_or(Error::MissingResource)?,
matrix,
})
}
}
#[derive(Copy, Clone, Debug)]
pub struct Metadata<'a> {
name: Option<&'a str>,
version: Option<&'a str>,
notice: Option<&'a str>,
copyright: Option<&'a str>,
full_name: Option<&'a str>,
family_name: Option<&'a str>,
weight: Option<&'a str>,
bbox: BoundingBox<Fixed>,
italic_angle: Fixed,
is_fixed_pitch: bool,
underline_position: Fixed,
underline_thickness: Fixed,
}
impl<'a> Metadata<'a> {
fn new(data: &'a [u8], top_dict_index: u16) -> Option<Self> {
let cff = cff::Cff::read(FontData::new(data)).ok()?;
let strings = cff.strings();
let get_str = |sid: Sid| {
let bytes = match sid.resolve_standard() {
Ok(bytes) => bytes,
Err(idx) => strings.get(idx)?,
};
core::str::from_utf8(bytes).ok()
};
let top_dict_data = cff.top_dicts().get(top_dict_index as usize)?;
let name = cff
.name(top_dict_index as usize)
.and_then(|bytes| core::str::from_utf8(bytes).ok());
let mut meta = Metadata {
name,
..Default::default()
};
for entry in dict::entries(top_dict_data, None).filter_map(|e| e.ok()) {
match entry {
dict::Entry::Version(sid) => meta.version = get_str(sid),
dict::Entry::Notice(sid) => meta.notice = get_str(sid),
dict::Entry::Copyright(sid) => meta.copyright = get_str(sid),
dict::Entry::FullName(sid) => meta.full_name = get_str(sid),
dict::Entry::FamilyName(sid) => meta.family_name = get_str(sid),
dict::Entry::Weight(sid) => meta.weight = get_str(sid),
dict::Entry::FontBbox([x_min, y_min, x_max, y_max]) => {
meta.bbox = BoundingBox {
x_min,
x_max,
y_min,
y_max,
}
}
dict::Entry::ItalicAngle(angle) => meta.italic_angle = angle,
dict::Entry::IsFixedPitch(fixed_pitch) => meta.is_fixed_pitch = fixed_pitch,
dict::Entry::UnderlinePosition(pos) => meta.underline_position = pos,
dict::Entry::UnderlineThickness(size) => meta.underline_thickness = size,
_ => {}
}
}
Some(meta)
}
pub fn name(&self) -> Option<&'a str> {
self.name
}
pub fn version(&self) -> Option<&'a str> {
self.version
}
pub fn notice(&self) -> Option<&'a str> {
self.notice
}
pub fn copyright(&self) -> Option<&'a str> {
self.copyright
}
pub fn full_name(&self) -> Option<&'a str> {
self.full_name
}
pub fn family_name(&self) -> Option<&'a str> {
self.family_name
}
pub fn weight(&self) -> Option<&'a str> {
self.weight
}
pub fn italic_angle(&self) -> Fixed {
self.italic_angle
}
pub fn is_fixed_pitch(&self) -> bool {
self.is_fixed_pitch
}
pub fn underline_position(&self) -> Fixed {
self.underline_position
}
pub fn underline_thickness(&self) -> Fixed {
self.underline_thickness
}
pub fn bbox(&self) -> BoundingBox<Fixed> {
self.bbox
}
}
impl Default for Metadata<'_> {
fn default() -> Self {
Self {
name: None,
version: None,
notice: None,
copyright: None,
full_name: None,
family_name: None,
weight: None,
bbox: BoundingBox::default(),
italic_angle: Fixed::ZERO,
is_fixed_pitch: false,
underline_position: Fixed::from_i32(-100),
underline_thickness: Fixed::from_i32(50),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
ps::{hinting::Blues, transform::FontMatrix},
FontData, FontRef, TableProvider,
};
use charstring::test_helpers::*;
use font_test_data::bebuffer::BeBuffer;
#[test]
fn read_cff_static() {
let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
let cff =
CffFontRef::new_cff(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
assert_eq!(cff.version(), 1);
assert!(cff.var_store().is_none());
let CffFontKind::Sid { private_dict, .. } = &cff.top_dict.kind else {
panic!("this is an SID font");
};
assert!(!private_dict.is_empty());
assert_eq!(cff.num_glyphs(), 5);
assert_eq!(cff.num_subfonts(), 1);
assert_eq!(cff.subfont_index(GlyphId::new(1)), Some(0));
assert_eq!(cff.global_subrs.count(), 17);
}
#[test]
fn read_cff_metadata() {
let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
let cff = CffFontRef::new(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
let meta = cff.metadata().unwrap();
assert_eq!(meta.name(), Some("NotoSerifDisplay-Regular"));
assert_eq!(meta.full_name(), Some("Noto Serif Display Regular"));
assert_eq!(meta.family_name(), Some("Noto Serif Display"));
assert_eq!(meta.weight(), None);
assert_eq!(
meta.bbox(),
BoundingBox {
x_min: Fixed::from_i32(-693),
y_min: Fixed::from_i32(-470),
x_max: Fixed::from_i32(2797),
y_max: Fixed::from_i32(1048)
}
);
assert_eq!(meta.italic_angle(), Fixed::ZERO);
assert!(!meta.is_fixed_pitch());
assert_eq!(meta.underline_position(), Fixed::from_i32(-100));
assert_eq!(meta.underline_thickness(), Fixed::from_i32(50));
let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET).unwrap();
let cff = CffFontRef::new(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
let meta = cff.metadata().unwrap();
assert_eq!(meta.name(), Some("GoogleMaterialIcons-Regular"));
assert_eq!(meta.full_name(), Some("GoogleMaterialIcons-Regular"));
assert_eq!(meta.family_name(), None);
assert_eq!(meta.weight(), None);
assert_eq!(
meta.bbox(),
BoundingBox {
x_min: Fixed::from_i32(-1),
y_min: Fixed::ZERO,
x_max: Fixed::from_i32(513),
y_max: Fixed::from_i32(512)
}
);
assert_eq!(meta.italic_angle(), Fixed::ZERO);
assert!(!meta.is_fixed_pitch());
assert_eq!(meta.underline_position(), Fixed::from_i32(-100));
assert_eq!(meta.underline_thickness(), Fixed::from_i32(50));
}
#[test]
fn read_cff2_static() {
let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
let cff =
CffFontRef::new_cff2(font.cff2().unwrap().offset_data().as_bytes(), None).unwrap();
assert_eq!(cff.version(), 2);
assert!(cff.var_store().is_some());
let CffFontKind::Cff2 { fd_array, .. } = &cff.top_dict.kind else {
panic!("this is a CFF2 font");
};
assert_eq!(fd_array.count(), 1);
assert_eq!(cff.num_glyphs(), 6);
assert_eq!(cff.num_subfonts(), 1);
assert_eq!(cff.subfont_index(GlyphId::new(1)), Some(0));
assert_eq!(cff.global_subrs.count(), 0);
}
#[test]
fn read_example_cff2_table() {
let cff = CffFontRef::new_cff2(font_test_data::cff2::EXAMPLE, None).unwrap();
assert_eq!(cff.version(), 2);
assert!(cff.var_store().is_some());
let CffFontKind::Cff2 { fd_array, .. } = &cff.top_dict.kind else {
panic!("this is a CFF2 font");
};
assert_eq!(fd_array.count(), 1);
assert_eq!(cff.num_glyphs(), 2);
assert_eq!(cff.num_subfonts(), 1);
assert_eq!(cff.subfont_index(GlyphId::new(1)), Some(0));
assert_eq!(cff.global_subrs.count(), 0);
}
#[test]
fn charset() {
let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
let cff =
CffFontRef::new_cff(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
let charset = cff.charset().unwrap();
let glyph_names = charset
.iter()
.map(|(gid, sid)| {
(
gid.to_u32(),
std::str::from_utf8(sid.resolve_standard().unwrap()).unwrap(),
)
})
.collect::<Vec<_>>();
let expected = [(0, ".notdef"), (1, "i"), (2, "j"), (3, "k"), (4, "l")];
assert_eq!(glyph_names, expected)
}
#[test]
fn top_dict_ivs_offset_overflow() {
let top_dict = BeBuffer::new()
.push(29u8) .push(-1i32) .push(24u8) .to_vec();
assert!(TopDict::new(&[], &top_dict, Index::Empty, true).is_err());
}
#[test]
fn subrs_offset_overflow() {
let private_dict = BeBuffer::new()
.push(0u32) .push(29u8) .push(-1i32) .push(19u8) .to_vec();
assert!(Subfont::new(&private_dict, 4..private_dict.len(), None, None).is_err());
}
#[test]
fn empty_private_dict() {
let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET).unwrap();
let cff =
CffFontRef::new_cff(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
let CffFontKind::Sid { private_dict, .. } = &cff.top_dict.kind else {
panic!("this is an SID font");
};
assert!(private_dict.is_empty());
}
#[test]
fn capture_family_other_blues() {
let private_dict_data = &font_test_data::cff2::EXAMPLE[0x4f..=0xc0];
let store =
ItemVariationStore::read(FontData::new(&font_test_data::cff2::EXAMPLE[18..])).unwrap();
let coords = &[F2Dot14::from_f32(0.0)];
let blend_state = BlendState::new(store, coords, 0).unwrap();
let (_subfont, hint_params) = Subfont::new_hinted(
private_dict_data,
0..private_dict_data.len(),
Some(blend_state),
None,
)
.unwrap();
assert_eq!(
hint_params.family_other_blues,
Blues::new([-249.0, -239.0].map(Fixed::from_f64).into_iter())
)
}
#[test]
fn subfont_cff() {
let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
let cff =
CffFontRef::new_cff(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
let subfont = cff.subfont(0, &[]).unwrap();
assert_eq!(subfont.default_width, None);
assert_eq!(subfont.nominal_width, Fixed::from_i32(598));
assert_eq!(subfont.vs_index, 0);
assert_eq!(subfont.matrix, None);
}
fn make_blues<const N: usize>(values: [i32; N]) -> Blues {
Blues::new(values.map(Fixed::from_i32).into_iter())
}
#[test]
fn hinted_subfont_cff() {
let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
let cff =
CffFontRef::new_cff(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
let (subfont, hinting) = cff.subfont_hinted(0, &[]).unwrap();
assert_eq!(subfont.default_width, None);
assert_eq!(subfont.nominal_width, Fixed::from_i32(598));
assert_eq!(subfont.vs_index, 0);
assert_eq!(subfont.matrix, None);
let expected_hinting = HintingParams {
blues: make_blues([-15, 0, 536, 547, 571, 582, 714, 726, 760, 772]),
family_blues: Blues::default(),
other_blues: make_blues([-255, -240]),
family_other_blues: Blues::default(),
blue_scale: Fixed::from_f64(0.0500030517578125),
blue_shift: Fixed::from_f64(7.0),
blue_fuzz: Fixed::ZERO,
language_group: 0,
};
assert_eq!(hinting, expected_hinting);
}
#[test]
fn subfont_cff2() {
let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
let cff =
CffFontRef::new_cff2(font.cff2().unwrap().offset_data().as_bytes(), None).unwrap();
let subfont = cff.subfont(0, &[]).unwrap();
assert_eq!(subfont.default_width, None);
assert_eq!(subfont.nominal_width, Fixed::ZERO);
assert_eq!(subfont.vs_index, 0);
assert_eq!(subfont.matrix, None);
}
#[test]
fn hinted_subfont_cff2() {
let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
let cff =
CffFontRef::new_cff2(font.cff2().unwrap().offset_data().as_bytes(), None).unwrap();
let (subfont, hinting) = cff.subfont_hinted(0, &[]).unwrap();
assert_eq!(subfont.default_width, None);
assert_eq!(subfont.nominal_width, Fixed::ZERO);
assert_eq!(subfont.vs_index, 0);
assert_eq!(subfont.matrix, None);
let expected_hinting = HintingParams {
blues: make_blues([-10, 0, 482, 492, 694, 704, 739, 749]),
family_blues: Blues::default(),
other_blues: make_blues([-227, -217]),
family_other_blues: Blues::default(),
blue_scale: Fixed::from_f64(0.0625),
blue_shift: Fixed::from_f64(7.0),
blue_fuzz: Fixed::ONE,
language_group: 0,
};
assert_eq!(hinting, expected_hinting);
}
#[test]
fn subfont_matrix() {
let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET_MATRIX).unwrap();
let cff =
CffFontRef::new_cff(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
let subfont = cff.subfont(0, &[]).unwrap();
assert_eq!(subfont.default_width, None);
assert_eq!(subfont.nominal_width, Fixed::ZERO);
assert_eq!(subfont.vs_index, 0);
let expected_matrix = FontMatrix::from_elements([
Fixed::from_i32(5),
Fixed::ZERO,
Fixed::ZERO,
Fixed::from_i32(5),
Fixed::ZERO,
Fixed::ZERO,
]);
let expected_scale = 10;
assert_eq!(
subfont.matrix,
Some(ScaledFontMatrix {
matrix: expected_matrix,
scale: expected_scale
})
);
}
#[test]
fn eval_charstring_cff() {
let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
let cff =
CffFontRef::new_cff(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
let mut sink = CharstringCommandCounter::default();
let subfont = cff.subfont(0, &[]).unwrap();
cff.evaluate_charstring(&subfont, GlyphId::new(2), &[], &mut sink)
.unwrap();
assert_eq!(sink.0, 18);
}
#[test]
fn eval_charstring_cff2() {
let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
let cff =
CffFontRef::new_cff2(font.cff2().unwrap().offset_data().as_bytes(), None).unwrap();
let mut sink = CharstringCommandCounter::default();
let subfont = cff.subfont(0, &[]).unwrap();
cff.evaluate_charstring(&subfont, GlyphId::new(2), &[], &mut sink)
.unwrap();
assert_eq!(sink.0, 11);
}
#[test]
fn select_version() {
assert_eq!(
CffFontRef::new(
FontRef::new(font_test_data::CANTARELL_VF_TRIMMED)
.unwrap()
.cff2()
.unwrap()
.offset_data()
.as_bytes(),
0,
None
)
.unwrap()
.version(),
2
);
assert_eq!(
CffFontRef::new(
FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET_MATRIX)
.unwrap()
.cff()
.unwrap()
.offset_data()
.as_bytes(),
0,
None
)
.unwrap()
.version(),
1
);
assert!(CffFontRef::new(&[0, 1, 4, 5], 0, None).is_err());
assert!(CffFontRef::new(&[1, 1, 4, 5], 0, None).is_err());
assert!(CffFontRef::new(&[2, 1, 4, 5], 0, None).is_err());
}
#[test]
fn transform() {
let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET_MATRIX).unwrap();
let cff =
CffFontRef::new(font.cff().unwrap().offset_data().as_bytes(), 0, Some(512)).unwrap();
let subfont = cff.subfont(0, &[]).unwrap();
let expected_matrix = [65536, 0, 5604, 65536, 0, 0].map(Fixed::from_bits);
let transform = cff.transform(&subfont, None);
assert_eq!(transform.matrix.elements(), expected_matrix);
assert_eq!(transform.scale, Some(Fixed::from_bits(32 << 16)));
let transform = cff.transform(&subfont, Some(16.0));
assert_eq!(transform.matrix.elements(), expected_matrix);
assert_eq!(transform.scale, Some(Fixed::from_bits(1 << 16)));
}
#[test]
fn cff_encoding() {
let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
let cff = CffFontRef::new(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
let encoding = cff.encoding().unwrap();
assert_eq!(encoding.predefined(), Some(PredefinedEncoding::Standard));
}
#[test]
fn cff2_lacks_encoding() {
let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
let cff = CffFontRef::new(font.cff2().unwrap().offset_data().as_bytes(), 0, None).unwrap();
assert!(cff.encoding().is_none());
}
}
#[cfg(test)]
mod width_only_tests {
use super::*;
use crate::{model::glyph::outline::NullSink, FontRef, TableProvider};
const FONTS: [&[u8]; 3] = [
font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
font_test_data::ift::CFF_FONT,
font_test_data::MATERIAL_ICONS_SUBSET_MATRIX,
];
fn for_each_glyph(data: &[u8], mut check: impl FnMut(&CffFontRef, &Subfont, GlyphId)) {
let font = FontRef::new(data).unwrap();
let cff =
CffFontRef::new_cff(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
let num_glyphs = font.maxp().unwrap().num_glyphs() as u32;
assert!(num_glyphs > 1);
for gid in (0..num_glyphs).map(GlyphId::new) {
let index = cff.subfont_index(gid).unwrap_or(0);
let subfont = cff.subfont(index, &[]).unwrap();
check(&cff, &subfont, gid);
}
}
#[test]
fn the_quick_width_is_what_a_full_run_reports() {
let mut mapped = 0;
for data in FONTS {
let mut stated = 0;
for_each_glyph(data, |cff, subfont, gid| {
let transform = cff.transform(subfont, None);
let full = cff
.evaluate_charstring(subfont, gid, &[], &mut charstring::NullSink)
.unwrap();
let quick = cff.evaluate_width(subfont, gid, &[]).unwrap();
assert_eq!(
full.map(|width| transform.transform_h_metric(width)),
quick,
"glyph {gid}"
);
stated += full.is_some() as u32;
if let (Some(full), Some(quick)) = (full, quick) {
mapped += (full != quick) as u32;
}
});
assert!(stated > 0, "no glyph reported a width");
}
assert!(mapped > 0, "no width was moved by a matrix");
}
#[test]
fn the_quick_advance_is_what_drawing_reports() {
for data in FONTS {
for ppem in [None, Some(16.0), Some(72.0)] {
for_each_glyph(data, |cff, subfont, gid| {
let drawn = cff.draw(subfont, gid, &[], ppem, &mut NullSink).unwrap();
let quick = cff.advance(subfont, gid, &[], ppem).unwrap();
assert_eq!(drawn, quick, "glyph {gid} at {ppem:?}");
});
}
}
}
}
#[cfg(test)]
mod extents_tests {
use super::*;
use crate::{model::glyph::outline::PathSink, FontRef, TableProvider};
const SHEARED: &[u8] = font_test_data::MATERIAL_ICONS_SUBSET_MATRIX;
#[derive(Default)]
struct BoundsPen(Option<[f32; 4]>);
impl BoundsPen {
fn add(&mut self, x: f32, y: f32) {
self.0 = Some(match self.0 {
Some([a, b, c, d]) => [a.min(x), b.min(y), c.max(x), d.max(y)],
None => [x, y, x, y],
});
}
}
impl PathSink for BoundsPen {
fn move_to(&mut self, x: f32, y: f32) {
self.add(x, y);
}
fn line_to(&mut self, x: f32, y: f32) {
self.add(x, y);
}
fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
self.add(cx, cy);
self.add(x, y);
}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.add(cx0, cy0);
self.add(cx1, cy1);
self.add(x, y);
}
fn close(&mut self) {}
}
fn corners(b: BoundingBox<f32>) -> [f32; 4] {
[b.x_min, b.y_min, b.x_max, b.y_max]
}
fn cff_of(data: &[u8]) -> CffFontRef<'_> {
let font = FontRef::new(data).unwrap();
CffFontRef::new_cff(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap()
}
fn each_glyph(data: &[u8], mut f: impl FnMut(&CffFontRef, &Subfont, GlyphId)) {
let cff = cff_of(data);
for gid in (0..cff.num_glyphs()).map(GlyphId::new) {
let index = cff.subfont_index(gid).unwrap_or(0);
let subfont = cff.subfont(index, &[]).unwrap();
f(&cff, &subfont, gid);
}
}
#[test]
fn the_extents_are_the_box_drawing_stays_within() {
for data in [
font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
font_test_data::ift::CFF_FONT,
SHEARED,
] {
for ppem in [None, Some(16.0)] {
let mut drew = 0;
each_glyph(data, |cff, subfont, gid| {
let mut pen = BoundsPen::default();
cff.draw(subfont, gid, &[], ppem, &mut pen).unwrap();
let measured = cff.extents(subfont, gid, &[], ppem).unwrap();
assert_eq!(measured.map(corners), pen.0, "glyph {gid} at {ppem:?}");
drew += pen.0.is_some() as u32;
});
assert!(drew > 0, "nothing drew, so this proves nothing");
}
}
}
#[test]
fn a_sheared_matrix_measures_smaller_than_a_box_turned_afterwards() {
let mut tighter = 0;
each_glyph(SHEARED, |cff, subfont, gid| {
let mut before = charstring::ControlBoundsSink::new();
let mut filter = charstring::NopFilterSink::new(&mut before);
cff.evaluate_charstring(subfont, gid, &[], &mut filter)
.unwrap();
let Some(plain) = before.bounding_box() else {
return;
};
let mut turned = charstring::ControlBoundsSink::new();
let transform = cff.transform(subfont, None);
let mut sink = charstring::TransformSink::new(&mut turned, transform);
for (x, y) in [
(plain.x_min, plain.y_min),
(plain.x_min, plain.y_max),
(plain.x_max, plain.y_min),
(plain.x_max, plain.y_max),
] {
sink.move_to(x, y);
}
let turned = turned.bounding_box().unwrap();
let measured = cff.extents(subfont, gid, &[], None).unwrap().unwrap();
assert!(measured.x_min >= turned.x_min.to_f32());
assert!(measured.x_max <= turned.x_max.to_f32());
if measured.x_min > turned.x_min.to_f32() || measured.x_max < turned.x_max.to_f32() {
tighter += 1;
}
});
assert!(
tighter > 0,
"no glyph measured tighter, so the shear went unnoticed"
);
}
#[test]
fn a_location_moves_a_cff2_box_as_it_moves_the_drawing() {
let data = font_test_data::ift::CFF2_FONT;
let font = FontRef::new(data).unwrap();
let cff =
CffFontRef::new_cff2(font.cff2().unwrap().offset_data().as_bytes(), None).unwrap();
let axes = font.fvar().unwrap().axis_count() as usize;
let mut at_default: Vec<Option<[f32; 4]>> = Vec::new();
let mut moved = 0;
for coord in [0.0f32, -1.0, 1.0] {
let coords: Vec<_> = (0..axes).map(|_| F2Dot14::from_f32(coord)).collect();
for (i, gid) in (0..cff.num_glyphs()).map(GlyphId::new).enumerate() {
let index = cff.subfont_index(gid).unwrap_or(0);
let subfont = cff.subfont(index, &coords).unwrap();
let measured = cff
.extents(&subfont, gid, &coords, None)
.unwrap()
.map(corners);
let mut pen = BoundsPen::default();
cff.draw(&subfont, gid, &coords, None, &mut pen).unwrap();
assert_eq!(measured, pen.0, "glyph {gid} at {coord}");
if coord == 0.0 {
at_default.push(measured);
} else if at_default[i] != measured {
moved += 1;
}
}
}
assert!(moved > 0, "no box moved, so the location went unread");
}
}