use rust_fontconfig::*;
#[test]
fn test_operating_system_font_expansion() {
let windows_os = OperatingSystem::Windows;
let no_ranges: &[UnicodeRange] = &[];
assert_eq!(windows_os.get_serif_fonts(no_ranges), vec!["Times New Roman".to_string()]);
assert_eq!(
windows_os.get_sans_serif_fonts(no_ranges),
vec!["Segoe UI", "Tahoma", "Microsoft Sans Serif", "MS Sans Serif", "Helv"]
.iter().map(|s| s.to_string()).collect::<Vec<_>>()
);
assert_eq!(
windows_os.get_monospace_fonts(no_ranges),
vec!["Segoe UI Mono", "Courier New", "Cascadia Code", "Cascadia Mono", "Consolas"]
.iter().map(|s| s.to_string()).collect::<Vec<_>>()
);
let macos_os = OperatingSystem::MacOS;
assert_eq!(
macos_os.get_serif_fonts(no_ranges),
vec!["Times New Roman", "Times", "New York", "Palatino"].iter().map(|s| s.to_string()).collect::<Vec<_>>()
);
assert_eq!(
macos_os.get_sans_serif_fonts(no_ranges),
vec!["San Francisco", ".AppleSystemUIFont", ".SFUIText", ".SFUI-Regular", "Helvetica Neue", "Helvetica", "Lucida Grande"]
.iter().map(|s| s.to_string()).collect::<Vec<_>>()
);
assert_eq!(
macos_os.get_monospace_fonts(no_ranges),
vec!["SF Mono", "Menlo", "Monaco", "Courier", "Oxygen Mono", "Source Code Pro", "Fira Mono"]
.iter().map(|s| s.to_string()).collect::<Vec<_>>()
);
let linux_os = OperatingSystem::Linux;
assert_eq!(
linux_os.get_serif_fonts(no_ranges).len(),
8,
"Linux should have 8 serif fonts"
);
assert_eq!(
linux_os.get_sans_serif_fonts(no_ranges),
vec!["Ubuntu", "Arial", "DejaVu Sans", "Noto Sans", "Liberation Sans"]
.iter().map(|s| s.to_string()).collect::<Vec<_>>()
);
let families = vec!["Arial".to_string(), "sans-serif".to_string()];
let expanded = expand_font_families(&families, OperatingSystem::MacOS, no_ranges);
assert_eq!(expanded[0], "Arial");
assert_eq!(expanded[1], "San Francisco");
assert_eq!(expanded[2], ".AppleSystemUIFont");
assert_eq!(expanded[3], ".SFUIText");
let specific = vec!["MyCustomFont".to_string()];
let expanded = expand_font_families(&specific, OperatingSystem::Windows, no_ranges);
assert_eq!(expanded, vec!["MyCustomFont".to_string()]);
}
#[test]
fn test_unicode_range_matching() {
let latin_font = FcFont {
bytes: vec![0, 1, 2, 3], font_index: 0,
id: "latin-font".to_string(),
};
let cyrillic_font = FcFont {
bytes: vec![4, 5, 6, 7], font_index: 0,
id: "cyrillic-font".to_string(),
};
let cjk_font = FcFont {
bytes: vec![8, 9, 10, 11], font_index: 0,
id: "cjk-font".to_string(),
};
let latin_pattern = FcPattern {
name: Some("Latin Font".to_string()),
family: Some("Latin Family".to_string()),
unicode_ranges: vec![
UnicodeRange {
start: 0x0000,
end: 0x007F,
}, UnicodeRange {
start: 0x0080,
end: 0x00FF,
}, ],
..Default::default()
};
let cyrillic_pattern = FcPattern {
name: Some("Cyrillic Font".to_string()),
family: Some("Cyrillic Family".to_string()),
unicode_ranges: vec![
UnicodeRange {
start: 0x0400,
end: 0x04FF,
}, ],
..Default::default()
};
let cjk_pattern = FcPattern {
name: Some("CJK Font".to_string()),
family: Some("CJK Family".to_string()),
unicode_ranges: vec![
UnicodeRange {
start: 0x4E00,
end: 0x9FFF,
}, ],
..Default::default()
};
let mut cache = FcFontCache::default();
cache.with_memory_fonts(vec![
(latin_pattern.clone(), latin_font),
(cyrillic_pattern.clone(), cyrillic_font),
(cjk_pattern.clone(), cjk_font),
]);
let font_list = cache.list();
let latin_id = font_list
.iter()
.find(|(pattern, _)| pattern.name == Some("Latin Font".to_string()))
.map(|(_, id)| *id)
.expect("Latin font not found");
let cyrillic_id = font_list
.iter()
.find(|(pattern, _)| pattern.name == Some("Cyrillic Font".to_string()))
.map(|(_, id)| *id)
.expect("Cyrillic font not found");
let mut trace: Vec<TraceMsg> = Vec::new();
let latin_query = FcPattern {
unicode_ranges: vec![UnicodeRange {
start: 0x0041,
end: 0x005A,
}], ..Default::default()
};
let matches: Vec<_> = cache.list().into_iter()
.filter(|(pattern, _)| {
if pattern.unicode_ranges.is_empty() { return false; }
pattern.unicode_ranges.iter().any(|r| {
latin_query.unicode_ranges.iter().any(|q| {
r.start <= q.end && q.start <= r.end
})
})
})
.collect();
assert_eq!(matches.len(), 1);
assert_eq!(cache.get_memory_font(&latin_id).is_some(), true);
trace.clear();
let cyrillic_query = FcPattern {
unicode_ranges: vec![UnicodeRange {
start: 0x0410,
end: 0x044F,
}], ..Default::default()
};
let matches: Vec<_> = cache.list().into_iter()
.filter(|(pattern, _)| {
if pattern.unicode_ranges.is_empty() { return false; }
pattern.unicode_ranges.iter().any(|r| {
cyrillic_query.unicode_ranges.iter().any(|q| {
r.start <= q.end && q.start <= r.end
})
})
})
.collect();
assert_eq!(matches.len(), 1);
assert_eq!(cache.get_memory_font(&cyrillic_id).is_some(), true);
#[cfg(feature = "std")]
{
let text = "Hello Привет ä½ å¥½";
let families: Vec<String> = cache.list().iter()
.filter_map(|(pattern, _)| pattern.family.clone())
.collect();
let chain = cache.resolve_font_chain(
&families,
FcWeight::Normal,
PatternMatch::DontCare,
PatternMatch::DontCare,
&mut trace,
);
let runs = chain.query_for_text(&cache, text);
let unique_fonts: std::collections::HashSet<_> = runs.iter()
.filter_map(|r| r.font_id)
.collect();
assert!(
unique_fonts.len() >= 2,
"Should use multiple fonts for multilingual text"
);
}
}
#[test]
fn test_weight_matching() {
let normal_font = FcFont {
bytes: vec![0, 1, 2, 3],
font_index: 0,
id: "normal-font".to_string(),
};
let bold_font = FcFont {
bytes: vec![4, 5, 6, 7],
font_index: 0,
id: "bold-font".to_string(),
};
let normal_pattern = FcPattern {
name: Some("Normal Font".to_string()),
family: Some("Test Family".to_string()),
weight: FcWeight::Normal,
..Default::default()
};
let bold_pattern = FcPattern {
name: Some("Bold Font".to_string()),
family: Some("Test Family".to_string()),
weight: FcWeight::Bold,
bold: PatternMatch::True,
..Default::default()
};
let mut cache = FcFontCache::default();
cache.with_memory_fonts(vec![
(normal_pattern.clone(), normal_font),
(bold_pattern.clone(), bold_font),
]);
let mut trace = Vec::new();
let normal_query = FcPattern {
family: Some("Test Family".to_string()),
weight: FcWeight::Normal,
..Default::default()
};
let matches = cache.query(&normal_query, &mut trace);
assert!(matches.is_some(), "Should match normal weight font");
let bold_query = FcPattern {
family: Some("Test Family".to_string()),
weight: FcWeight::Bold,
..Default::default()
};
let matches = cache.query(&bold_query, &mut trace);
assert!(matches.is_some(), "Should match bold weight font");
trace.clear();
let wrong_family_query = FcPattern {
family: Some("Wrong Family".to_string()),
weight: FcWeight::Normal,
..Default::default()
};
let matches = cache.query(&wrong_family_query, &mut trace);
assert!(matches.is_none(), "Should not match with wrong family");
let family_mismatch_traces = trace
.iter()
.filter(|msg| matches!(msg.reason, MatchReason::FamilyMismatch { .. }))
.count();
assert!(
family_mismatch_traces > 0,
"Expected family mismatch trace messages"
);
trace.clear();
let light_query = FcPattern {
family: Some("Test Family".to_string()),
weight: FcWeight::Light,
..Default::default()
};
let matches = cache.query(&light_query, &mut trace);
assert!(matches.is_none(), "Should not match with weight mismatch");
let weight_mismatch_traces = trace
.iter()
.filter(|msg| matches!(msg.reason, MatchReason::WeightMismatch { .. }))
.count();
assert!(
weight_mismatch_traces > 0,
"Expected weight mismatch trace messages"
);
let available_weights = [FcWeight::Light, FcWeight::Normal, FcWeight::Bold];
assert_eq!(
FcWeight::Normal.find_best_match(&available_weights),
Some(FcWeight::Normal),
"Should find exact match when available"
);
assert_eq!(
FcWeight::ExtraLight.find_best_match(&available_weights),
Some(FcWeight::Light),
"Should find closest lighter weight for weights < 400"
);
assert_eq!(
FcWeight::ExtraBold.find_best_match(&available_weights),
Some(FcWeight::Bold),
"Should find closest heavier weight for weights > 500"
);
let available = [FcWeight::Light, FcWeight::Bold];
assert_eq!(
FcWeight::Normal.find_best_match(&available),
Some(FcWeight::Light),
"For weight 400, should prefer lightest weight when 500 unavailable"
);
let available = [FcWeight::Light, FcWeight::SemiBold];
assert_eq!(
FcWeight::Medium.find_best_match(&available),
Some(FcWeight::Light),
"For weight 500, should prefer 400 first"
);
}
#[test]
fn test_trace_messages() {
let test_font = FcFont {
bytes: vec![0, 1, 2, 3],
font_index: 0,
id: "test-font".to_string(),
};
let test_pattern = FcPattern {
name: Some("Test Font".to_string()),
family: Some("Test Family".to_string()),
italic: PatternMatch::False,
monospace: PatternMatch::True,
weight: FcWeight::Normal,
stretch: FcStretch::Normal,
unicode_ranges: vec![UnicodeRange {
start: 0x0000,
end: 0x007F,
}],
..Default::default()
};
let mut cache = FcFontCache::default();
cache.with_memory_fonts(vec![(test_pattern.clone(), test_font)]);
let mut trace = Vec::new();
let name_query = FcPattern {
name: Some("Wrong Name".to_string()),
..Default::default()
};
let matches = cache.query(&name_query, &mut trace);
assert!(matches.is_none(), "Should not match with wrong name");
assert!(!trace.is_empty(), "Trace should not be empty");
let name_mismatch = trace.iter().any(|msg| {
if let MatchReason::NameMismatch { requested, found } = &msg.reason {
requested.as_ref() == Some(&"Wrong Name".to_string())
&& found.as_ref() == Some(&"Test Font".to_string())
} else {
false
}
});
assert!(name_mismatch, "Name mismatch trace message not found");
trace.clear();
let style_query = FcPattern {
name: Some("Test Font".to_string()),
italic: PatternMatch::True,
..Default::default()
};
let matches = cache.query(&style_query, &mut trace);
assert!(matches.is_none(), "Should not match with style mismatch");
let style_mismatch = trace.iter().any(|msg| {
if let MatchReason::StyleMismatch { property, .. } = &msg.reason {
property == &"italic"
} else {
false
}
});
assert!(style_mismatch, "Style mismatch trace message not found");
trace.clear();
let stretch_query = FcPattern {
name: Some("Test Font".to_string()),
stretch: FcStretch::Condensed,
..Default::default()
};
let matches = cache.query(&stretch_query, &mut trace);
assert!(matches.is_none(), "Should not match with stretch mismatch");
let stretch_mismatch = trace
.iter()
.any(|msg| matches!(msg.reason, MatchReason::StretchMismatch { .. }));
assert!(stretch_mismatch, "Stretch mismatch trace message not found");
trace.clear();
let range_query = FcPattern {
name: Some("Test Font".to_string()),
unicode_ranges: vec![UnicodeRange {
start: 0x0370,
end: 0x03FF,
}], ..Default::default()
};
let matches = cache.query(&range_query, &mut trace);
assert!(
matches.is_none(),
"Should not match with Unicode range mismatch"
);
let range_mismatch = trace
.iter()
.any(|msg| matches!(msg.reason, MatchReason::UnicodeRangeMismatch { .. }));
assert!(
range_mismatch,
"Unicode range mismatch trace message not found"
);
}
fn getfonts(
arial_id: FontId,
arial_bold_id: FontId,
courier_id: FontId,
fira_id: FontId,
noto_cjk_id: FontId,
) -> Vec<(FontId, FcPattern, FcFont)> {
return vec![
(
arial_id,
FcPattern {
name: Some("Arial".to_string()),
family: Some("Arial".to_string()),
weight: FcWeight::Normal,
bold: PatternMatch::False,
monospace: PatternMatch::False,
unicode_ranges: vec![UnicodeRange {
start: 0x0000,
end: 0x007F,
}],
..Default::default()
},
FcFont {
bytes: vec![1, 2, 3, 4],
font_index: 0,
id: "arial-regular".to_string(),
},
),
(
arial_bold_id,
FcPattern {
name: Some("Arial Bold".to_string()),
family: Some("Arial".to_string()),
weight: FcWeight::Bold,
bold: PatternMatch::True,
monospace: PatternMatch::False,
unicode_ranges: vec![UnicodeRange {
start: 0x0000,
end: 0x007F,
}],
..Default::default()
},
FcFont {
bytes: vec![5, 6, 7, 8],
font_index: 0,
id: "arial-bold".to_string(),
},
),
(
courier_id,
FcPattern {
name: Some("Courier New".to_string()),
family: Some("Courier New".to_string()),
weight: FcWeight::Normal,
monospace: PatternMatch::True,
unicode_ranges: vec![UnicodeRange {
start: 0x0000,
end: 0x007F,
}],
..Default::default()
},
FcFont {
bytes: vec![9, 10, 11, 12],
font_index: 0,
id: "courier-new".to_string(),
},
),
(
fira_id,
FcPattern {
name: Some("Fira Code".to_string()),
family: Some("Fira Code".to_string()),
weight: FcWeight::Normal,
monospace: PatternMatch::True,
unicode_ranges: vec![UnicodeRange {
start: 0x0000,
end: 0x007F,
}],
..Default::default()
},
FcFont {
bytes: vec![13, 14, 15, 16],
font_index: 0,
id: "fira-code".to_string(),
},
),
(
noto_cjk_id,
FcPattern {
name: Some("Noto Sans CJK".to_string()),
family: Some("Noto Sans CJK".to_string()),
weight: FcWeight::Normal,
monospace: PatternMatch::False,
unicode_ranges: vec![
UnicodeRange {
start: 0x0000,
end: 0x007F,
}, UnicodeRange {
start: 0x4E00,
end: 0x9FFF,
}, ],
..Default::default()
},
FcFont {
bytes: vec![17, 18, 19, 20],
font_index: 0,
id: "noto-sans-cjk".to_string(),
},
),
];
}
#[test]
fn test_font_search() {
let arial_id = FontId(1);
let arial_bold_id = FontId(2);
let courier_id = FontId(3);
let fira_id = FontId(4);
let noto_cjk_id = FontId(5);
let fonts = getfonts(arial_id, arial_bold_id, courier_id, fira_id, noto_cjk_id);
let mut cache = FcFontCache::default();
for (id, pattern, font) in fonts {
cache.with_memory_font_with_id(id, pattern, font);
}
let mut trace: Vec<TraceMsg> = Vec::new();
let results: Vec<_> = cache.list().into_iter()
.filter(|(pattern, _)| pattern.monospace == PatternMatch::True)
.collect();
assert_eq!(results.len(), 2, "Should find two monospace fonts");
let result_ids: Vec<FontId> = results.into_iter().map(|(_, id)| id).collect();
assert!(
result_ids.contains(&courier_id),
"Should include Courier New"
);
assert!(result_ids.contains(&fira_id), "Should include Fira Code");
#[cfg(feature = "std")]
{
let cjk_text = "ä½ å¥½";
let families: Vec<String> = cache.list().iter()
.filter_map(|(pattern, _)| pattern.family.clone())
.collect();
let chain = cache.resolve_font_chain(
&families,
FcWeight::Normal,
PatternMatch::DontCare,
PatternMatch::DontCare,
&mut trace,
);
let runs = chain.query_for_text(&cache, cjk_text);
assert!(!runs.is_empty(), "Should find fonts for CJK text");
let result_ids: Vec<FontId> = runs.iter()
.filter_map(|r| r.font_id)
.collect();
assert!(
result_ids.contains(¬o_cjk_id),
"Should include Noto Sans CJK"
);
trace.clear();
let mixed_text = "Hello ä½ å¥½";
let runs = chain.query_for_text(&cache, mixed_text);
let unique_fonts: std::collections::HashSet<_> = runs.iter()
.filter_map(|r| r.font_id)
.collect();
assert!(
unique_fonts.len() >= 1,
"Should find at least one font for mixed text"
);
let cjk_found = unique_fonts.contains(¬o_cjk_id);
assert!(cjk_found, "Should find a CJK-capable font");
}
}
#[test]
fn test_failing_isolated() {
let arial_id = FontId(1);
let arial_bold_id = FontId(2);
let courier_id = FontId(3);
let fira_id = FontId(4);
let noto_cjk_id = FontId(5);
let fonts = getfonts(arial_id, arial_bold_id, courier_id, fira_id, noto_cjk_id);
let mut cache = FcFontCache::default();
for (id, pattern, font) in fonts {
cache.with_memory_font_with_id(id, pattern, font);
}
let mut trace = Vec::new();
let arial_query = FcPattern {
name: Some("Arial".to_string()),
..Default::default()
};
let result = cache.query(&arial_query, &mut trace);
assert!(result.is_some(), "Should find Arial font");
assert_eq!(result.unwrap().id, arial_id, "Should match Arial font ID");
}
#[test]
fn test_failing_isolated_2() {
let arial_id = FontId(1);
let arial_bold_id = FontId(2);
let courier_id = FontId(3);
let fira_id = FontId(4);
let noto_cjk_id = FontId(5);
let fonts = getfonts(arial_id, arial_bold_id, courier_id, fira_id, noto_cjk_id);
let mut cache = FcFontCache::default();
for (id, pattern, font) in fonts {
cache.with_memory_font_with_id(id, pattern, font);
}
let mut trace = Vec::new();
let arial_bold_query = FcPattern {
family: Some("Arial".to_string()),
bold: PatternMatch::True,
..Default::default()
};
let result = cache.query(&arial_bold_query, &mut trace);
assert!(result.is_some(), "Should find Arial Bold font");
assert_eq!(
result.unwrap().id,
arial_bold_id,
"Should match Arial Bold font ID"
);
}
#[cfg(all(feature = "std", feature = "parsing"))]
#[test]
fn test_memory_font_generic_serif_resolves_char() {
let font_bytes = include_bytes!("fixtures/InstrumentSerif-Regular.ttf").to_vec();
let cache = FcFontCache::default();
let pattern = FcPattern {
name: Some("serif".to_string()),
family: Some("serif".to_string()),
unicode_ranges: Vec::new(),
..Default::default()
};
let font = FcFont {
bytes: font_bytes,
font_index: 0,
id: "bundled-serif".to_string(),
};
cache.with_memory_fonts(vec![(pattern, font)]);
let mut trace: Vec<TraceMsg> = Vec::new();
let chain = cache.resolve_font_chain_with_scripts(
&["serif".to_string()],
FcWeight::Normal,
PatternMatch::False,
PatternMatch::False,
None,
&mut trace,
);
let resolved = chain.resolve_char(&cache, 'A');
assert!(
resolved.is_some(),
"bundled in-memory 'serif' font must resolve ASCII 'A' on a headless cache; \
got None (chain = {:#?})",
chain
);
}
#[test]
fn query_with_fallback_is_total_like_fc_match() {
let installed = FcPattern {
name: Some("Only Font".to_string()),
family: Some("Only Family".to_string()),
weight: FcWeight::Normal,
unicode_ranges: vec![UnicodeRange { start: 0x0000, end: 0x007F }],
..Default::default()
};
let mut cache = FcFontCache::default();
cache.with_memory_fonts(vec![(
installed.clone(),
FcFont { bytes: vec![0, 1, 2, 3], font_index: 0, id: "only-font".to_string() },
)]);
let missing = FcPattern {
family: Some("Cantarell".to_string()),
..Default::default()
};
let mut trace = Vec::new();
assert!(
cache.query(&missing, &mut trace).is_none(),
"query must stay FALLIBLE — callers rely on it to report an unresolved family",
);
let mut trace = Vec::new();
let fallback = cache.query_with_fallback(&missing, &mut trace);
assert!(
fallback.is_some(),
"query_with_fallback must be TOTAL while any font exists (fc-match never fails)",
);
let mut trace = Vec::new();
let expected = cache.query(&installed, &mut trace).expect("the installed font matches itself");
assert_eq!(
fallback.unwrap().id,
expected.id,
"the fallback must resolve to the one font in the cache",
);
let missing_bold = FcPattern {
family: Some("Cantarell".to_string()),
weight: FcWeight::Bold,
..Default::default()
};
let mut trace = Vec::new();
assert!(
cache.query_with_fallback(&missing_bold, &mut trace).is_some(),
"a bold request for a missing family must still resolve",
);
let mut trace = Vec::new();
assert!(
FcFontCache::default().query_with_fallback(&missing, &mut trace).is_none(),
"an empty cache is the only legitimate None",
);
}
#[test]
fn normalize_unicode_ranges_coalesces_so_coverage_is_not_double_counted() {
let r = |start, end| UnicodeRange { start, end };
let raw = vec![
r(0x0100, 0x017F), r(0x0000, 0x007F), r(0x0040, 0x00FF), r(0x0000, 0x007F), ];
let merged = FcFontCache::normalize_unicode_ranges(raw.clone());
assert_eq!(merged, vec![r(0x0000, 0x017F)]);
assert_eq!(FcFontCache::calculate_unicode_coverage(&merged), 0x180);
assert_eq!(FcFontCache::calculate_unicode_coverage(&raw), 0x240);
let disjoint =
FcFontCache::normalize_unicode_ranges(vec![r(0x0200, 0x02FF), r(0x0000, 0x007F)]);
assert_eq!(disjoint, vec![r(0x0000, 0x007F), r(0x0200, 0x02FF)]);
let maxed = FcFontCache::normalize_unicode_ranges(vec![r(0x0000, u32::MAX), r(0x0010, 0x0020)]);
assert_eq!(maxed, vec![r(0x0000, u32::MAX)]);
}
#[cfg(all(feature = "std", feature = "parsing"))]
#[test]
fn parsed_font_coverage_is_a_normalized_set() {
let font_bytes = include_bytes!("fixtures/InstrumentSerif-Regular.ttf").to_vec();
let cache = FcFontCache::default();
let pattern = FcPattern {
name: Some("instrument".to_string()),
unicode_ranges: Vec::new(),
..Default::default()
};
cache.with_memory_fonts(vec![(
pattern.clone(),
FcFont { bytes: font_bytes, font_index: 0, id: "instrument".to_string() },
)]);
let mut trace = Vec::new();
let matched = cache
.query(&pattern, &mut trace)
.expect("the registered font matches itself");
let ranges = &matched.unicode_ranges;
assert!(!ranges.is_empty(), "a parsed Latin font must report some coverage");
for pair in ranges.windows(2) {
let (a, b) = (pair[0], pair[1]);
assert!(
a.end.saturating_add(1) < b.start,
"ranges must be sorted, disjoint and non-touching; {a:?} then {b:?} in {ranges:?}",
);
}
assert!(
ranges.iter().any(|r| r.start <= 'A' as u32 && 'A' as u32 <= r.end),
"a Latin font must cover 'A', got {ranges:?}",
);
}
#[cfg(all(feature = "std", feature = "parsing"))]
fn strip_table(font: &[u8], drop_tag: &[u8; 4]) -> Vec<u8> {
let num = u16::from_be_bytes([font[4], font[5]]) as usize;
let mut tables: Vec<([u8; 4], u32, Vec<u8>)> = Vec::new();
for i in 0..num {
let rec = 12 + i * 16;
let tag: [u8; 4] = font[rec..rec + 4].try_into().unwrap();
let checksum = u32::from_be_bytes(font[rec + 4..rec + 8].try_into().unwrap());
let offset = u32::from_be_bytes(font[rec + 8..rec + 12].try_into().unwrap()) as usize;
let len = u32::from_be_bytes(font[rec + 12..rec + 16].try_into().unwrap()) as usize;
if &tag != drop_tag {
tables.push((tag, checksum, font[offset..offset + len].to_vec()));
}
}
tables.sort_by_key(|(tag, _, _)| *tag);
let n = tables.len();
let entry_selector = (usize::BITS - 1 - n.leading_zeros()) as u16;
let search_range = (1u16 << entry_selector) * 16;
let mut out = Vec::new();
out.extend_from_slice(&font[0..4]); out.extend_from_slice(&(n 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(&((n as u16) * 16 - search_range).to_be_bytes());
let mut body = Vec::new();
let mut offset = 12 + n * 16;
for (tag, checksum, data) in &tables {
out.extend_from_slice(tag);
out.extend_from_slice(&checksum.to_be_bytes());
out.extend_from_slice(&(offset as u32).to_be_bytes());
out.extend_from_slice(&(data.len() as u32).to_be_bytes());
body.extend_from_slice(data);
let pad = (4 - data.len() % 4) % 4;
body.extend(std::iter::repeat(0).take(pad));
offset += data.len() + pad;
}
out.extend_from_slice(&body);
out
}
#[cfg(all(feature = "std", feature = "parsing"))]
fn set_head_bold(font: &mut [u8]) {
let num = u16::from_be_bytes([font[4], font[5]]) as usize;
for i in 0..num {
let rec = 12 + i * 16;
if &font[rec..rec + 4] == b"head" {
let offset = u32::from_be_bytes(font[rec + 8..rec + 12].try_into().unwrap()) as usize;
let mac_style = offset + 44; let cur = u16::from_be_bytes([font[mac_style], font[mac_style + 1]]);
font[mac_style..mac_style + 2].copy_from_slice(&(cur | 1).to_be_bytes());
return;
}
}
panic!("no head table");
}
#[test]
#[cfg(all(feature = "std", feature = "parsing"))]
fn parses_a_font_without_an_os2_table() {
let original = include_bytes!("fixtures/InstrumentSerif-Regular.ttf").to_vec();
let baseline = FcParseFontBytes(&original, "InstrumentSerif")
.expect("fixture itself must parse");
let (baseline_pattern, _) = &baseline[0];
let stripped = strip_table(&original, b"OS/2");
assert!(
stripped.len() < original.len(),
"fixture had no OS/2 table to strip, so this test proves nothing"
);
let parsed = FcParseFontBytes(&stripped, "InstrumentSerif")
.expect("a font without OS/2 must still parse");
let (pattern, _) = &parsed[0];
assert_eq!(pattern.family, baseline_pattern.family);
assert!(
!pattern.unicode_ranges.is_empty(),
"coverage comes from the cmap, so it must survive the loss of OS/2"
);
assert_eq!(pattern.weight, FcWeight::Normal);
let mut bolded = strip_table(&original, b"OS/2");
set_head_bold(&mut bolded);
let parsed_bold = FcParseFontBytes(&bolded, "InstrumentSerif")
.expect("a bold font without OS/2 must still parse");
assert_eq!(parsed_bold[0].0.weight, FcWeight::Bold);
}