use oxideav_ttf::Font;
pub fn shape_text_with_font(font: &Font<'_>, text: &str, features: &[[u8; 4]]) -> Vec<u16> {
shape_text_inner(font, text, features, None, &[])
}
pub fn shape_text_with_alternates_with_font(
font: &Font<'_>,
text: &str,
feature_alternates: &[([u8; 4], u16)],
) -> Vec<u16> {
let features: Vec<[u8; 4]> = feature_alternates.iter().map(|(tag, _)| *tag).collect();
shape_text_inner(font, text, &features, None, feature_alternates)
}
pub fn shape_text_with_script_with_font(
font: &Font<'_>,
text: &str,
script_tag: [u8; 4],
features: &[[u8; 4]],
) -> Vec<u16> {
shape_text_inner(font, text, features, Some(script_tag), &[])
}
pub fn shape_text_with_script_and_alternates_with_font(
font: &Font<'_>,
text: &str,
script_tag: [u8; 4],
feature_alternates: &[([u8; 4], u16)],
) -> Vec<u16> {
let features: Vec<[u8; 4]> = feature_alternates.iter().map(|(tag, _)| *tag).collect();
shape_text_inner(font, text, &features, Some(script_tag), feature_alternates)
}
fn shape_text_inner(
font: &Font<'_>,
text: &str,
features: &[[u8; 4]],
script_tag: Option<[u8; 4]>,
feature_alternates: &[([u8; 4], u16)],
) -> Vec<u16> {
if text.is_empty() {
return Vec::new();
}
let mut gids: Vec<u16> = text
.chars()
.map(|ch| font.glyph_index(ch).unwrap_or(0))
.collect();
if features.is_empty() {
return gids;
}
let lookup_list = font.gsub_lookup_list();
let lookup_type_of = |idx: u16| -> Option<u16> {
lookup_list
.iter()
.find(|(i, _, _)| *i == idx)
.map(|(_, ty, _)| *ty)
};
for feature_tag in features {
let lookups = match script_tag {
Some(tag) => resolve_feature_lookups_single_script(font, tag, feature_tag),
None => resolve_feature_lookups(font, feature_tag),
};
let alt_index: u16 = feature_alternates
.iter()
.find(|(t, _)| t == feature_tag)
.map(|(_, idx)| *idx)
.unwrap_or(0);
for lookup_idx in lookups {
match lookup_type_of(lookup_idx) {
Some(1) => {
for slot in gids.iter_mut() {
if let Some(rep) = font.gsub_apply_lookup_type_1(lookup_idx, *slot) {
*slot = rep;
}
}
}
Some(2) => {
let mut pos = 0usize;
while pos < gids.len() {
if let Some(seq) = font.gsub_apply_lookup_type_2(lookup_idx, gids[pos]) {
let new_len = seq.len();
gids.splice(pos..pos + 1, seq);
pos += new_len;
} else {
pos += 1;
}
}
}
Some(3) => {
for slot in gids.iter_mut() {
if let Some(rep) =
font.gsub_apply_lookup_type_3(lookup_idx, *slot, alt_index)
{
*slot = rep;
}
}
}
Some(4) => {
let mut pos = 0usize;
while pos < gids.len() {
if let Some((replacement, consumed)) =
font.gsub_apply_lookup_type_4(lookup_idx, &gids[pos..])
{
if consumed == 0 {
pos += 1;
continue;
}
gids.splice(pos..pos + consumed, std::iter::once(replacement));
pos += 1;
} else {
pos += 1;
}
}
}
_ => {
}
}
}
}
gids
}
fn resolve_feature_lookups(font: &Font<'_>, feature_tag: &[u8; 4]) -> Vec<u16> {
for &tag in script_tag_probe_list() {
let hits = resolve_feature_lookups_single_script(font, tag, feature_tag);
if !hits.is_empty() {
return hits;
}
}
Vec::new()
}
fn resolve_feature_lookups_single_script(
font: &Font<'_>,
script_tag: [u8; 4],
feature_tag: &[u8; 4],
) -> Vec<u16> {
let mut hits: Vec<u16> = Vec::new();
let features = font.gsub_features_for_script(script_tag, None);
for feat in features {
if &feat.tag == feature_tag {
hits.extend_from_slice(&feat.lookup_indices);
}
}
hits
}
const SCRIPT_TAG_PROBE_LIST: &[[u8; 4]] = &[
*b"latn", *b"cyrl", *b"grek", *b"DFLT", *b"arab", *b"hebr", *b"thai", *b"lao ",
*b"deva", *b"dev2", *b"beng", *b"bng2", *b"taml", *b"tml2", *b"gujr", *b"gjr2", *b"guru",
*b"gur2", *b"knda", *b"knd2", *b"mlym", *b"mlm2", *b"orya", *b"ory2", *b"telu", *b"tel2",
*b"sinh", *b"khmr", *b"mymr", *b"mym2", *b"hang", *b"hani", *b"kana",
];
fn script_tag_probe_list() -> &'static [[u8; 4]] {
SCRIPT_TAG_PROBE_LIST
}
#[cfg(test)]
mod tests {
use super::*;
const DEJAVU_BYTES: &[u8] = include_bytes!("../../tests/fixtures/DejaVuSans.ttf");
const INTER_BYTES: &[u8] = include_bytes!("../../tests/fixtures/InterVariable.ttf");
#[test]
fn empty_text_is_empty_vec() {
let bytes = DEJAVU_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("DejaVu parses");
face.with_font(|font| {
assert_eq!(shape_text_with_font(font, "", &[]).len(), 0);
assert_eq!(shape_text_with_font(font, "", &[*b"smcp"]).len(), 0);
})
.unwrap();
}
#[test]
fn empty_features_is_cmap_identity() {
let bytes = DEJAVU_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("DejaVu parses");
face.with_font(|font| {
let got = shape_text_with_font(font, "abc", &[]);
let expected: Vec<u16> = "abc"
.chars()
.map(|c| font.glyph_index(c).unwrap_or(0))
.collect();
assert_eq!(got, expected);
})
.unwrap();
}
#[test]
fn inter_smcp_substitutes_lowercase_ascii() {
let bytes = INTER_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("Inter parses");
face.with_font(|font| {
let cmap_only = shape_text_with_font(font, "abc", &[]);
let smcp_on = shape_text_with_font(font, "abc", &[*b"smcp"]);
assert_eq!(cmap_only.len(), 3);
assert_eq!(smcp_on.len(), 3);
assert_ne!(
cmap_only, smcp_on,
"smcp must reshape lowercase ASCII to small-cap glyphs"
);
let changed = cmap_only.iter().zip(smcp_on.iter()).any(|(a, b)| a != b);
assert!(
changed,
"smcp on Inter must remap at least one lowercase ASCII slot"
);
})
.unwrap();
}
#[test]
fn inter_sups_substitutes_digits() {
let bytes = INTER_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("Inter parses");
face.with_font(|font| {
let cmap_only = shape_text_with_font(font, "0123", &[]);
let sups_on = shape_text_with_font(font, "0123", &[*b"sups"]);
assert_eq!(cmap_only.len(), 4);
assert_eq!(sups_on.len(), 4);
assert_ne!(cmap_only, sups_on);
let changed = cmap_only.iter().zip(sups_on.iter()).any(|(a, b)| a != b);
assert!(changed, "sups must remap at least one digit slot");
})
.unwrap();
}
#[test]
fn inter_subs_is_distinct_from_sups() {
let bytes = INTER_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("Inter parses");
face.with_font(|font| {
let cmap_only = shape_text_with_font(font, "0123", &[]);
let sups_on = shape_text_with_font(font, "0123", &[*b"sups"]);
let subs_on = shape_text_with_font(font, "0123", &[*b"subs"]);
assert_ne!(cmap_only, subs_on);
assert_ne!(sups_on, subs_on, "sups and subs must reshape distinctly");
})
.unwrap();
}
#[test]
fn inter_smcp_then_case_on_lowercase_is_smcp_alone() {
let bytes = INTER_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("Inter parses");
face.with_font(|font| {
let smcp_alone = shape_text_with_font(font, "abc", &[*b"smcp"]);
let smcp_then_case = shape_text_with_font(font, "abc", &[*b"smcp", *b"case"]);
assert_eq!(
smcp_alone, smcp_then_case,
"case is a no-op on lowercase ASCII; result must match smcp alone"
);
})
.unwrap();
}
#[test]
fn unknown_feature_tag_is_identity() {
let bytes = INTER_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("Inter parses");
face.with_font(|font| {
let cmap_only = shape_text_with_font(font, "abc", &[]);
let unknown = shape_text_with_font(font, "abc", &[*b"zzzz"]);
assert_eq!(cmap_only, unknown);
})
.unwrap();
}
#[test]
fn dejavu_smcp_unsupported_is_cmap_identity() {
let bytes = DEJAVU_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("DejaVu parses");
face.with_font(|font| {
assert!(
!face.has_gsub_feature(*b"latn", *b"smcp"),
"DejaVu Sans is the no-smcp control fixture for this test"
);
let cmap_only = shape_text_with_font(font, "abc", &[]);
let smcp_on = shape_text_with_font(font, "abc", &[*b"smcp"]);
assert_eq!(cmap_only, smcp_on);
})
.unwrap();
}
#[test]
fn liga_collapses_fi_via_lookup_type_4() {
let bytes = DEJAVU_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("DejaVu parses");
face.with_font(|font| {
assert!(
face.has_gsub_feature(*b"latn", *b"liga"),
"DejaVu publishes `liga` — round-128 dispatches its lookup type 4"
);
let cmap_only = shape_text_with_font(font, "fi", &[]);
let liga_on = shape_text_with_font(font, "fi", &[*b"liga"]);
assert_eq!(cmap_only.len(), 2, "cmap maps 'f' and 'i' to two glyphs");
assert_eq!(
liga_on.len(),
1,
"round-128 collapses 'fi' into a single ligature glyph"
);
assert_ne!(
liga_on[0], cmap_only[0],
"the fi-ligature glyph differs from cmap('f')"
);
assert_ne!(
liga_on[0], cmap_only[1],
"the fi-ligature glyph differs from cmap('i')"
);
})
.unwrap();
}
#[test]
fn liga_leaves_uncovered_glyphs_alone() {
let bytes = DEJAVU_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("DejaVu parses");
face.with_font(|font| {
let cmap_only = shape_text_with_font(font, "abfi", &[]);
let liga_on = shape_text_with_font(font, "abfi", &[*b"liga"]);
assert_eq!(cmap_only.len(), 4);
assert_eq!(
liga_on.len(),
3,
"'a', 'b' pass through; 'fi' collapses to one glyph"
);
assert_eq!(liga_on[0], cmap_only[0]);
assert_eq!(liga_on[1], cmap_only[1]);
assert_ne!(liga_on[2], cmap_only[2]);
assert_ne!(liga_on[2], cmap_only[3]);
})
.unwrap();
}
#[test]
fn liga_is_identity_on_uncovered_run() {
let bytes = DEJAVU_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("DejaVu parses");
face.with_font(|font| {
let cmap_only = shape_text_with_font(font, "abc", &[]);
let liga_on = shape_text_with_font(font, "abc", &[*b"liga"]);
assert_eq!(
cmap_only, liga_on,
"no component prefix in 'abc' matches DejaVu's liga lookup"
);
})
.unwrap();
}
#[test]
fn aalt_dispatches_lookup_type_3_on_inter() {
let bytes = INTER_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("Inter parses");
face.with_font(|font| {
let cmap_only = shape_text_with_font(font, "a", &[]);
let aalt = shape_text_with_font(font, "a", &[*b"aalt"]);
assert_eq!(cmap_only.len(), 1);
assert_eq!(aalt.len(), 1, "Type 3 is length-preserving");
assert_ne!(
cmap_only[0], aalt[0],
"round-156 reshapes 'a' via aalt's Type-3 alternate-0"
);
})
.unwrap();
}
#[test]
fn aalt_is_idempotent_on_dejavu() {
let bytes = DEJAVU_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("DejaVu parses");
face.with_font(|font| {
let once = shape_text_with_font(font, "Iaaly", &[*b"aalt"]);
let twice = shape_text_with_font(font, "Iaaly", &[*b"aalt", *b"aalt"]);
assert_eq!(once, twice, "aalt's Type-3 component must be idempotent");
})
.unwrap();
}
#[test]
fn round175_probe_list_prefix_is_round15_priority() {
let list = script_tag_probe_list();
assert!(list.len() >= 4, "probe list keeps round-15 four-tag prefix");
assert_eq!(&list[..4], &[*b"latn", *b"cyrl", *b"grek", *b"DFLT"]);
}
#[test]
fn round175_probe_list_covers_broadened_scripts() {
let list = script_tag_probe_list();
for tag in [
*b"arab", *b"hebr", *b"thai", *b"lao ", *b"deva", *b"dev2", *b"beng", *b"bng2",
*b"taml", *b"tml2", *b"gujr", *b"gjr2", *b"guru", *b"gur2", *b"knda", *b"knd2",
*b"mlym", *b"mlm2", *b"orya", *b"ory2", *b"telu", *b"tel2", *b"sinh", *b"khmr",
*b"mymr", *b"mym2", *b"hang", *b"hani", *b"kana",
] {
assert!(
list.contains(&tag),
"round-175 probe list must contain script tag {:?}",
core::str::from_utf8(&tag).unwrap_or("???")
);
}
}
#[test]
fn round175_explicit_unknown_script_is_cmap_identity() {
let bytes = INTER_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("Inter parses");
face.with_font(|font| {
let cmap_only = shape_text_with_font(font, "abc", &[]);
let explicit_unknown =
shape_text_with_script_with_font(font, "abc", *b"zzzz", &[*b"smcp"]);
assert_eq!(
cmap_only, explicit_unknown,
"unknown script-tag must skip every feature"
);
})
.unwrap();
}
#[test]
fn round175_explicit_empty_features_is_cmap_identity() {
let bytes = INTER_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("Inter parses");
face.with_font(|font| {
let cmap_only = shape_text_with_font(font, "Hello", &[]);
let explicit_empty = shape_text_with_script_with_font(font, "Hello", *b"latn", &[]);
assert_eq!(cmap_only, explicit_empty);
})
.unwrap();
}
#[test]
fn round175_explicit_latn_matches_auto_probe_on_smcp() {
let bytes = INTER_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("Inter parses");
face.with_font(|font| {
let auto = shape_text_with_font(font, "abc", &[*b"smcp"]);
let explicit_latn =
shape_text_with_script_with_font(font, "abc", *b"latn", &[*b"smcp"]);
assert_eq!(
auto, explicit_latn,
"explicit `latn` resolution must agree with auto-probe on smcp"
);
})
.unwrap();
}
#[test]
fn round175_explicit_dflt_unknown_feature_is_cmap_identity() {
let bytes = INTER_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("Inter parses");
face.with_font(|font| {
let cmap_only = shape_text_with_font(font, "abc", &[]);
let auto = shape_text_with_font(font, "abc", &[*b"zzzz"]);
let explicit_dflt =
shape_text_with_script_with_font(font, "abc", *b"DFLT", &[*b"zzzz"]);
assert_eq!(cmap_only, auto);
assert_eq!(cmap_only, explicit_dflt);
})
.unwrap();
}
#[test]
fn round175_broadened_probe_preserves_latn_smcp_result() {
let bytes = INTER_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("Inter parses");
face.with_font(|font| {
let mut round15_hits: Vec<u16> = Vec::new();
for tag in [*b"latn", *b"cyrl", *b"grek", *b"DFLT"] {
let features = font.gsub_features_for_script(tag, None);
for feat in features {
if feat.tag == *b"smcp" {
round15_hits.extend_from_slice(&feat.lookup_indices);
}
}
if !round15_hits.is_empty() {
break;
}
}
let round175_hits = resolve_feature_lookups(font, b"smcp");
assert_eq!(
round15_hits, round175_hits,
"broadened probe must preserve the round-15 `smcp`/`latn` resolution"
);
})
.unwrap();
}
#[test]
fn round183_empty_alternates_is_cmap_identity() {
let bytes = INTER_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("Inter parses");
face.with_font(|font| {
let cmap_only = shape_text_with_font(font, "abc", &[]);
let round183 = shape_text_with_alternates_with_font(font, "abc", &[]);
assert_eq!(cmap_only, round183);
let round183_script =
shape_text_with_script_and_alternates_with_font(font, "abc", *b"latn", &[]);
assert_eq!(cmap_only, round183_script);
})
.unwrap();
}
#[test]
fn round183_index_zero_matches_round156_default() {
let bytes = INTER_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("Inter parses");
face.with_font(|font| {
let round156 = shape_text_with_font(font, "abcdefg", &[*b"aalt"]);
let round183 = shape_text_with_alternates_with_font(font, "abcdefg", &[(*b"aalt", 0)]);
assert_eq!(
round156, round183,
"round-183 with index 0 must reproduce the round-156 default"
);
})
.unwrap();
}
#[test]
fn round183_out_of_range_index_is_cmap_identity() {
let bytes = INTER_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("Inter parses");
face.with_font(|font| {
let cmap_only = shape_text_with_font(font, "abcdefg", &[]);
let out_of_range =
shape_text_with_alternates_with_font(font, "abcdefg", &[(*b"aalt", u16::MAX)]);
assert_eq!(
cmap_only, out_of_range,
"an out-of-range alternate index must fall back to cmap-identity per slot"
);
})
.unwrap();
}
#[test]
fn round183_explicit_unknown_script_is_cmap_identity() {
let bytes = INTER_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("Inter parses");
face.with_font(|font| {
let cmap_only = shape_text_with_font(font, "abc", &[]);
let unknown = shape_text_with_script_and_alternates_with_font(
font,
"abc",
*b"zzzz",
&[(*b"aalt", 0)],
);
assert_eq!(cmap_only, unknown);
})
.unwrap();
}
#[test]
fn round183_explicit_index_zero_matches_round175_default() {
let bytes = INTER_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("Inter parses");
face.with_font(|font| {
let round175 = shape_text_with_script_with_font(font, "abcdefg", *b"latn", &[*b"aalt"]);
let round183 = shape_text_with_script_and_alternates_with_font(
font,
"abcdefg",
*b"latn",
&[(*b"aalt", 0)],
);
assert_eq!(round175, round183);
})
.unwrap();
}
#[test]
fn round183_alternate_index_ignored_for_non_type_3_features() {
let bytes = DEJAVU_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("DejaVu parses");
face.with_font(|font| {
let round128 = shape_text_with_font(font, "fi", &[*b"liga"]);
let round183_with_bogus_index =
shape_text_with_alternates_with_font(font, "fi", &[(*b"liga", 5)]);
assert_eq!(
round128.len(),
1,
"DejaVu's `liga` Type-4 lookup collapses 'fi' to one glyph"
);
assert_eq!(
round128, round183_with_bogus_index,
"non-Type-3 features must ignore the alternate index"
);
})
.unwrap();
}
#[test]
fn round183_alternate_index_preserves_run_length() {
let bytes = INTER_BYTES.to_vec();
let face = crate::Face::from_ttf_bytes(bytes).expect("Inter parses");
face.with_font(|font| {
for sample in ["a", "ab", "abcdefg", "Hello, world!"] {
let cmap_only = shape_text_with_font(font, sample, &[]);
for idx in [0u16, 1, 2, 7, u16::MAX] {
let out =
shape_text_with_alternates_with_font(font, sample, &[(*b"aalt", idx)]);
assert_eq!(
cmap_only.len(),
out.len(),
"Type-3 length-preservation on {sample:?} for alt-index {idx} failed"
);
}
}
})
.unwrap();
}
}