use std::path::PathBuf;
use dxpdf::render::fonts::catalog::Scope;
use dxpdf::render::fonts::face::NameKind;
use dxpdf::render::fonts::opentype::{carve_collection_face, FontFormat, NameId};
use dxpdf::render::fonts::resolve::{resolve, FaceResolution, FallbackReason, ResolutionStep};
use dxpdf::render::fonts::{FaceCatalog, FaceRequest, Toggle};
use skia_safe::{FontMgr, Typeface};
fn fixture_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-files/fonts")
}
fn open_fixture_face(
font_mgr: &FontMgr,
bytes: &[u8],
index: u32,
face_count: u32,
) -> Option<Typeface> {
if face_count <= 1 {
return font_mgr.new_from_data(bytes, 0);
}
let carved = carve_collection_face(bytes, index, face_count)
.unwrap_or_else(|e| panic!("carving face {index} of {face_count}: {e}"));
font_mgr.new_from_data(&carved.bytes, 0)
}
fn load(font_mgr: &FontMgr, filename: &str) -> Vec<(String, Typeface)> {
let bytes = std::fs::read(fixture_dir().join(filename)).unwrap_or_else(|e| {
panic!("fixture '{filename}' is missing ({e}) — run scripts/make_font_fixtures.py")
});
let face_count = match FontFormat::detect(&bytes) {
Ok(FontFormat::Ttc { face_count }) => face_count,
Ok(_) => 1,
Err(e) => panic!("fixture '{filename}' is not a font: {e}"),
};
(0..face_count)
.filter_map(|index| {
let typeface = open_fixture_face(font_mgr, &bytes, index, face_count)?;
Some((typeface.family_name(), typeface))
})
.collect()
}
fn catalog() -> FaceCatalog {
let font_mgr = FontMgr::new();
let mut faces = Vec::new();
for filename in [
"DxSans-Regular.ttf",
"DxSans-Bold.ttf",
"DxSans-SemiBold.ttf",
"DxMedium-Regular.ttf",
"Dx-Regular.ttf",
"DxOblique.ttf",
"DxCollection.ttc",
"DxVariable.ttf",
] {
faces.extend(load(&font_mgr, filename));
}
assert!(
faces.len() >= 9,
"expected at least nine fixture faces, got {}",
faces.len()
);
FaceCatalog::from_faces(font_mgr, &faces)
}
#[track_caller]
fn select(name: &str, bold: Toggle, italic: Toggle) -> (String, ResolutionStep, Option<String>) {
let catalog = catalog();
let request = FaceRequest::new(name, bold, italic);
match resolve(&request, &catalog) {
FaceResolution::Selected(selection) => (
selection.record.canonical_family.clone(),
selection.step.clone(),
selection
.record
.instance
.as_ref()
.map(|i| i.subfamily.clone()),
),
FaceResolution::Default { reason } => {
panic!("'{name}' did not resolve: {reason:?}")
}
}
}
#[track_caller]
fn weight_of(name: &str, bold: Toggle, italic: Toggle) -> i32 {
let catalog = catalog();
match resolve(&FaceRequest::new(name, bold, italic), &catalog) {
FaceResolution::Selected(selection) => selection.record.intrinsic.weight,
FaceResolution::Default { reason } => panic!("'{name}' did not resolve: {reason:?}"),
}
}
#[test]
fn the_fixtures_carry_what_the_tests_assume() {
let font_mgr = FontMgr::new();
let semibold = load(&font_mgr, "DxSans-SemiBold.ttf");
assert_eq!(semibold.len(), 1);
let catalog = FaceCatalog::from_faces(font_mgr.clone(), &semibold);
let names = &catalog.embedded_faces();
assert!(
names.is_empty(),
"fixtures are host faces, not embedded ones"
);
let ttc = std::fs::read(fixture_dir().join("DxCollection.ttc")).unwrap();
assert!(
matches!(
FontFormat::detect(&ttc),
Ok(FontFormat::Ttc { face_count: 2 })
),
"DxCollection.ttc must declare two faces"
);
let openable = load(&font_mgr, "DxCollection.ttc");
assert_eq!(
openable.len(),
2,
"both collection faces must open on every platform"
);
let variable = load(&font_mgr, "DxVariable.ttf");
assert_eq!(variable.len(), 1);
let variable_bytes = std::fs::read(fixture_dir().join("DxVariable.ttf")).unwrap();
assert!(
has_table(&variable_bytes, b"gvar"),
"DxVariable.ttf must carry gvar deltas, or no instance's outline can \
differ from the default location's"
);
let oblique_bytes = std::fs::read(fixture_dir().join("DxOblique.ttf")).unwrap();
let fs_selection = os2_fs_selection(&oblique_bytes);
const FS_SELECTION_OBLIQUE: u16 = 1 << 9; assert_ne!(
fs_selection & FS_SELECTION_OBLIQUE,
0,
"DxOblique.ttf must declare OS/2 OBLIQUE (fsSelection bit 9)"
);
}
fn has_table(sfnt: &[u8], tag: &[u8; 4]) -> bool {
let num_tables = u16::from_be_bytes([sfnt[4], sfnt[5]]) as usize;
(0..num_tables).any(|i| {
let record = &sfnt[12 + i * 16..12 + i * 16 + 4];
record == tag
})
}
fn os2_fs_selection(sfnt: &[u8]) -> u16 {
let num_tables = u16::from_be_bytes([sfnt[4], sfnt[5]]) as usize;
let record = (0..num_tables)
.map(|i| &sfnt[12 + i * 16..12 + i * 16 + 16])
.find(|record| &record[0..4] == b"OS/2")
.expect("fixture must carry an OS/2 table");
let offset = u32::from_be_bytes(record[8..12].try_into().unwrap()) as usize;
u16::from_be_bytes([sfnt[offset + 62], sfnt[offset + 63]])
}
#[test]
fn a_family_ending_in_a_style_word_resolves_to_itself() {
let (family, step, _) = select("Dx Medium", Toggle::Absent, Toggle::Absent);
assert_eq!(family, "Dx Medium");
assert!(
matches!(
step,
ResolutionStep::SystemFamily
| ResolutionStep::SystemFaceName
| ResolutionStep::SystemMetadataAlias
),
"it must be found by reading the font, not parsed into one ({step:?})"
);
}
#[test]
fn the_base_family_the_parse_would_have_found_exists() {
let (family, step, _) = select("Dx", Toggle::Absent, Toggle::Absent);
assert_eq!(family, "Dx");
assert_eq!(step, ResolutionStep::SystemFamily);
}
#[test]
fn every_alias_kind_reaches_the_semibold_face() {
for name in ["Dx Sans SemiBold", "DxSans-SemiBold"] {
let (_, step, _) = select(name, Toggle::Absent, Toggle::Absent);
assert!(
matches!(
step,
ResolutionStep::SystemFamily
| ResolutionStep::SystemFaceName
| ResolutionStep::SystemMetadataAlias
),
"'{name}' resolved by guesswork ({step:?}), not by reading the font"
);
assert_eq!(
weight_of(name, Toggle::Absent, Toggle::Absent),
600,
"'{name}' must select the SemiBold face"
);
}
}
#[test]
fn the_typographic_family_groups_every_weight() {
assert_eq!(weight_of("Dx Sans", Toggle::Absent, Toggle::Absent), 400);
assert_eq!(weight_of("Dx Sans", Toggle::On, Toggle::Absent), 700);
}
#[test]
fn localized_names_resolve_to_the_same_face() {
for name in ["Дх Санс СемиБолд", "Dx サンズ セミボールド"] {
assert_eq!(
weight_of(name, Toggle::Absent, Toggle::Absent),
600,
"the localized name '{name}' must reach the SemiBold face"
);
}
}
#[test]
fn matching_folds_case_and_whitespace_but_not_punctuation() {
assert_eq!(
weight_of(" dx sans SEMIBOLD ", Toggle::Absent, Toggle::Absent),
600
);
let catalog = catalog();
let hyphenless = resolve(&FaceRequest::plain("DxSans SemiBold"), &catalog);
assert!(
matches!(hyphenless, FaceResolution::Default { .. }),
"a PostScript name with its hyphen removed is a different name"
);
}
#[test]
fn an_unrequested_weight_keeps_the_named_face_s_own() {
for toggle in [Toggle::Absent, Toggle::Off] {
assert_eq!(
weight_of("DxSans-SemiBold", toggle, Toggle::Absent),
600,
"{toggle:?} must not flatten the SemiBold face to Regular"
);
}
}
#[test]
fn an_explicit_bold_moves_to_the_family_s_bold() {
assert_eq!(
weight_of("DxSans-SemiBold", Toggle::On, Toggle::Absent),
700
);
}
#[test]
fn a_bare_family_still_resolves_to_regular() {
for toggle in [Toggle::Absent, Toggle::Off] {
assert_eq!(weight_of("Dx Sans", toggle, Toggle::Absent), 400);
}
}
#[test]
fn an_ambiguous_name_declines_rather_than_guessing() {
let font_mgr = FontMgr::new();
let mut faces = load(&font_mgr, "DxSans-Regular.ttf");
for (_, typeface) in load(&font_mgr, "DxSans-Bold.ttf") {
faces.push(("Dx Sans".to_string(), typeface));
}
let catalog = FaceCatalog::from_faces(font_mgr, &faces);
assert!(matches!(
resolve(&FaceRequest::plain("Dx Sans Regular"), &catalog),
FaceResolution::Selected(_)
));
let clash = FaceCatalog::from_faces(FontMgr::new(), &{
let font_mgr = FontMgr::new();
let mut faces = Vec::new();
for (_, typeface) in load(&font_mgr, "DxSans-Regular.ttf") {
faces.push(("Clash".to_string(), typeface));
}
for (_, typeface) in load(&font_mgr, "DxSans-Bold.ttf") {
faces.push(("Clash".to_string(), typeface));
}
faces
});
assert!(matches!(
resolve(&FaceRequest::plain("Clash"), &clash),
FaceResolution::Selected(_)
));
}
#[test]
fn a_face_name_shared_by_two_faces_is_reported_as_ambiguous() {
let font_mgr = FontMgr::new();
let catalog = catalog();
for name in ["DxSans-SemiBold", "Dx Sans Regular", "Dx Sans Bold"] {
assert!(
!matches!(
catalog.lookup(Scope::Deep, name, NameKind::is_primary_identity),
dxpdf::render::fonts::FaceLookup::Ambiguous { .. }
),
"'{name}' should not be ambiguous among the fixtures"
);
}
let _ = font_mgr;
}
#[test]
fn each_collection_face_resolves_to_itself() {
let font_mgr = FontMgr::new();
let faces = load(&font_mgr, "DxCollection.ttc");
assert_eq!(
faces.len(),
2,
"both collection faces must be reachable on every platform"
);
let expected: &[(&str, i32)] = &[("Dx Collection One", 400), ("Dx Collection Two", 700)];
for (index, _) in faces.iter().enumerate() {
let (family, weight) = expected[index];
let (resolved, step, _) = select(family, Toggle::Absent, Toggle::Absent);
assert_eq!(resolved, family);
assert!(
matches!(
step,
ResolutionStep::SystemFamily
| ResolutionStep::SystemFaceName
| ResolutionStep::SystemMetadataAlias
),
"collection face {index} resolved by guesswork ({step:?})"
);
assert_eq!(weight_of(family, Toggle::Absent, Toggle::Absent), weight);
}
}
#[test]
fn a_named_instance_resolves_with_its_coordinates() {
let catalog = catalog();
let selection = match resolve(&FaceRequest::plain("Dx Variable SemiBold"), &catalog) {
FaceResolution::Selected(selection) => selection,
FaceResolution::Default { reason } => panic!("did not resolve: {reason:?}"),
};
let instance = selection
.record
.instance
.as_ref()
.expect("'Dx Variable SemiBold' names an fvar instance");
assert_eq!(instance.subfamily, "SemiBold");
assert_eq!(
instance
.coords
.iter()
.map(|c| (c.axis.to_string(), c.value))
.collect::<Vec<_>>(),
vec![("wght".to_string(), 600.0), ("wdth".to_string(), 100.0)]
);
assert_eq!(
selection.record.intrinsic.weight, 600,
"the instance's coordinates become its intrinsic style"
);
}
#[test]
fn the_default_location_is_not_a_separate_instance() {
let catalog = catalog();
match resolve(&FaceRequest::plain("Dx Variable"), &catalog) {
FaceResolution::Selected(selection) => {
assert!(selection.record.instance.is_none());
assert_eq!(selection.record.intrinsic.weight, 400);
}
FaceResolution::Default { reason } => panic!("did not resolve: {reason:?}"),
}
}
#[test]
fn a_stat_composed_style_resolves() {
let catalog = catalog();
match resolve(
&FaceRequest::plain("Dx Variable Condensed SemiBold"),
&catalog,
) {
FaceResolution::Selected(selection) => {
assert_eq!(selection.record.intrinsic.weight, 600);
}
FaceResolution::Default { reason } => {
panic!("'Dx Variable Condensed SemiBold' did not resolve: {reason:?}")
}
}
}
#[test]
fn an_unknown_name_falls_back_with_a_reason() {
let catalog = catalog();
match resolve(&FaceRequest::plain("No Such Family 8f3a1c"), &catalog) {
FaceResolution::Default { reason } => {
assert_eq!(reason, FallbackReason::NoCandidate);
}
FaceResolution::Selected(selection) => {
panic!(
"an unknown name resolved to {}",
selection.record.canonical_family
)
}
}
}
#[test]
fn a_face_with_no_readable_names_still_resolves_by_family() {
let font_mgr = FontMgr::new();
let faces = load(&font_mgr, "DxSans-Regular.ttf");
let catalog = FaceCatalog::from_faces(font_mgr, &faces);
match resolve(&FaceRequest::plain("Dx Sans"), &catalog) {
FaceResolution::Selected(selection) => {
assert_eq!(selection.step, ResolutionStep::SystemFamily);
}
FaceResolution::Default { reason } => panic!("did not resolve: {reason:?}"),
}
}
#[test]
fn the_semibold_fixture_declares_the_names_and_weight_it_should() {
let font_mgr = FontMgr::new();
let faces = load(&font_mgr, "DxSans-SemiBold.ttf");
let catalog = FaceCatalog::from_faces(font_mgr, &faces);
let selection = match resolve(&FaceRequest::plain("DxSans-SemiBold"), &catalog) {
FaceResolution::Selected(selection) => selection,
FaceResolution::Default { reason } => panic!("did not resolve: {reason:?}"),
};
let record = &selection.record;
assert_eq!(record.intrinsic.weight, 600, "OS/2 usWeightClass");
assert_eq!(record.canonical_family, "Dx Sans SemiBold", "name ID 1");
assert_eq!(
record.typographic_family.as_deref(),
Some("Dx Sans"),
"name ID 16"
);
let asserted: Vec<(NameId, &str)> = record
.names
.iter()
.filter_map(|n| match n.kind {
NameKind::Table(id) => Some((id, n.text.as_str())),
_ => None,
})
.collect();
for expected in [
(NameId::Family, "Dx Sans SemiBold"),
(NameId::Full, "Dx Sans SemiBold"),
(NameId::PostScript, "DxSans-SemiBold"),
(NameId::TypographicFamily, "Dx Sans"),
(NameId::CompatibleFull, "Dx Sans SemiBold"),
(NameId::WwsFamily, "Dx Sans"),
] {
assert!(
asserted.contains(&expected),
"the font must assert {expected:?}; it has {asserted:?}"
);
}
assert!(
record
.names
.iter()
.any(|n| n.localized && n.text == "Дх Санс СемиБолд"),
"the Russian record must be kept and flagged as localized"
);
}