use oxideav_ttf::Font;
const DEJAVU: &[u8] = include_bytes!("fixtures/DejaVuSans.ttf");
const ARABIC: &[u8] = include_bytes!("fixtures/NotoSansArabic-Regular.ttf");
#[test]
fn shape_without_features_is_nominal_cmap_plus_hmtx() {
let f = Font::from_bytes(DEJAVU).unwrap();
let text = "Hello";
let sh = f.shape(text, *b"latn", None, &[]);
assert_eq!(sh.len(), text.chars().count());
for (i, (byte_idx, ch)) in text.char_indices().enumerate() {
let gid = f.glyph_index(ch).unwrap();
assert_eq!(sh[i].glyph_id, gid, "glyph id at {i}");
assert_eq!(sh[i].cluster, byte_idx as u32, "cluster at {i}");
assert_eq!(sh[i].x_offset, 0);
assert_eq!(sh[i].y_offset, 0);
assert_eq!(
sh[i].x_advance,
f.glyph_advance(gid) as i32,
"nominal advance at {i}"
);
}
}
#[test]
fn shape_applies_latin_kerning() {
let f = Font::from_bytes(DEJAVU).unwrap();
let plain: i32 = f
.shape("AVATAR", *b"latn", None, &[])
.iter()
.map(|g| g.x_advance)
.sum();
let kerned: i32 = f
.shape("AVATAR", *b"latn", None, &[*b"kern"])
.iter()
.map(|g| g.x_advance)
.sum();
assert!(
kerned < plain,
"kerned advance {kerned} should be less than unkerned {plain}"
);
assert_eq!(f.shape("AVATAR", *b"latn", None, &[*b"kern"]).len(), 6);
}
#[test]
fn shape_applies_latin_ligature() {
let f = Font::from_bytes(DEJAVU).unwrap();
let plain = f.shape("office", *b"latn", None, &[]);
let ligated = f.shape("office", *b"latn", None, &[*b"liga"]);
assert_eq!(plain.len(), 6, "unligated 'office' is six glyphs");
assert!(
ligated.len() < plain.len(),
"liga must reduce glyph count: {} vs {}",
ligated.len(),
plain.len()
);
let mut prev = 0u32;
for g in &ligated {
assert!(g.cluster <= 5, "cluster {} out of range", g.cluster);
assert!(g.cluster >= prev, "clusters must be monotonic");
prev = g.cluster;
}
}
#[test]
fn shape_respects_requested_feature_set() {
let f = Font::from_bytes(DEJAVU).unwrap();
let no_liga = f.shape("fi", *b"latn", None, &[]);
let with_liga = f.shape("fi", *b"latn", None, &[*b"liga"]);
assert_eq!(no_liga.len(), 2, "'fi' stays two glyphs without liga");
assert!(with_liga.len() <= no_liga.len());
}
#[test]
fn shape_applies_arabic_joining_forms() {
let f = Font::from_bytes(ARABIC).unwrap();
let word = "\u{0628}\u{0631}\u{0643}\u{0629}"; let nominal: Vec<u16> = word.chars().map(|c| f.glyph_index(c).unwrap()).collect();
let sh = f.shape(
word,
*b"arab",
None,
&[*b"init", *b"medi", *b"fina", *b"isol"],
);
assert_eq!(sh.len(), 4, "joining is 1:1, four letters → four glyphs");
let shaped: Vec<u16> = sh.iter().map(|g| g.glyph_id).collect();
assert_ne!(
shaped, nominal,
"positional substitution must change at least one glyph"
);
let changed = shaped.iter().zip(&nominal).filter(|(a, b)| a != b).count();
assert!(
changed >= 2,
"expected >=2 positional substitutions, got {changed}"
);
assert_eq!(
sh.iter().map(|g| g.cluster).collect::<Vec<_>>(),
vec![0, 2, 4, 6]
);
}
#[test]
fn shape_attaches_arabic_mark_to_base() {
let f = Font::from_bytes(ARABIC).unwrap();
let word = "\u{0628}\u{064E}";
let sh = f.shape(word, *b"arab", None, &[*b"mark"]);
assert_eq!(sh.len(), 2);
assert_eq!(sh[0].x_offset, 0);
assert_eq!(sh[0].y_offset, 0);
assert!(
f.is_mark_glyph(sh[1].glyph_id),
"second glyph should be a combining mark"
);
assert!(
sh[1].x_offset != 0 || sh[1].y_offset != 0,
"mark must receive an attachment offset, got ({}, {})",
sh[1].x_offset,
sh[1].y_offset
);
}
#[test]
fn shape_leaves_mark_unattached_without_mark_feature() {
let f = Font::from_bytes(ARABIC).unwrap();
let word = "\u{0628}\u{064E}";
let sh = f.shape(word, *b"arab", None, &[]);
assert_eq!(sh.len(), 2);
assert_eq!((sh[1].x_offset, sh[1].y_offset), (0, 0));
}
#[test]
fn shape_unknown_script_falls_back_to_nominal() {
let f = Font::from_bytes(DEJAVU).unwrap();
let sh = f.shape("test", *b"zzzz", None, &[*b"kern", *b"liga"]);
assert_eq!(sh.len(), 4);
for (i, ch) in "test".chars().enumerate() {
let gid = f.glyph_index(ch).unwrap();
assert_eq!(sh[i].glyph_id, gid);
assert_eq!(sh[i].x_advance, f.glyph_advance(gid) as i32);
}
}
#[test]
fn shape_empty_text() {
let f = Font::from_bytes(DEJAVU).unwrap();
assert!(f.shape("", *b"latn", None, &[*b"kern"]).is_empty());
}
#[test]
fn shape_is_panic_free_across_mixed_input() {
let f = Font::from_bytes(ARABIC).unwrap();
let inputs = [
"\u{0628}\u{064E}\u{0631}\u{064F}\u{0643}\u{0650}\u{0629}",
"\u{0627}\u{0644}\u{0644}\u{0647}", "abc\u{FFFD}\u{10000}", ];
let feats: &[[u8; 4]] = &[
*b"init", *b"medi", *b"fina", *b"isol", *b"liga", *b"rlig", *b"calt", *b"mark", *b"mkmk",
*b"curs", *b"kern",
];
for w in inputs {
let sh = f.shape(w, *b"arab", None, feats);
for g in &sh {
assert!((g.cluster as usize) <= w.len());
}
}
}
#[test]
fn lookup_flags_accessor_reports_ignore_marks_bit() {
let f = Font::from_bytes(DEJAVU).unwrap();
let mut any_ignore_marks = false;
let mut any_plain = false;
for (idx, kind, _) in f.gsub_lookup_list() {
let fl = f.gsub_lookup_flags(idx);
if kind == 4 {
if (fl & 0x0008) != 0 {
any_ignore_marks = true;
} else {
any_plain = true;
}
}
}
assert!(
any_ignore_marks,
"DejaVu must ship at least one IGNORE_MARKS ligature lookup"
);
assert!(
any_plain,
"DejaVu must ship at least one non-IGNORE_MARKS ligature lookup"
);
assert_eq!(f.gsub_lookup_flags(u16::MAX), 0);
assert_eq!(f.gpos_lookup_flags(u16::MAX), 0);
}
#[test]
fn shape_mark_blocks_non_ignore_marks_ligature() {
let f = Font::from_bytes(DEJAVU).unwrap();
let ligated = f.shape("fi", *b"latn", None, &[*b"liga"]);
assert_eq!(ligated.len(), 1, "'fi' ligates to one glyph");
let blocked = f.shape("f\u{0301}i", *b"latn", None, &[*b"liga"]);
assert_eq!(
blocked.len(),
3,
"a mark between the components blocks a non-IGNORE_MARKS ligature"
);
}
#[test]
fn shape_pair_kerning_survives_interleaved_mark() {
let f = Font::from_bytes(DEJAVU).unwrap();
let av: i32 = f
.shape("AV", *b"latn", None, &[*b"kern"])
.iter()
.map(|g| g.x_advance)
.sum();
let av_unkerned: i32 = f
.shape("AV", *b"latn", None, &[])
.iter()
.map(|g| g.x_advance)
.sum();
assert!(
av < av_unkerned,
"AV must kern (kerned {av} < {av_unkerned})"
);
let sh = f.shape("A\u{0301}V", *b"latn", None, &[*b"kern"]);
assert_eq!(sh.len(), 3, "A + mark + V stays three glyphs");
let mark_gid = sh[1].glyph_id;
assert!(f.is_mark_glyph(mark_gid), "middle glyph is a GDEF mark");
assert_eq!(
sh[1].x_advance,
f.glyph_advance(mark_gid) as i32,
"the interleaved mark's advance must stay nominal"
);
}