mod helpers;
use std::collections::HashSet;
use helpers::{NOTO_ARABIC, NOTO_DEVANAGARI, NOTO_HEBREW, NOTO_SANS, Typesetter};
use text_typeset::font::resolve::resolve_font;
use text_typeset::shaping::shaper::{
TextDirection, shape_text, shape_text_directed, shape_text_with_fallback,
};
fn typesetter_with(font: &[u8]) -> Typesetter {
let mut ts = Typesetter::new();
let face = ts.register_font(font);
ts.set_default_font(face, 16.0);
ts.set_viewport(800.0, 600.0);
ts
}
fn isolated_glyphs(ts: &Typesetter, text: &str) -> Vec<u16> {
let resolved =
resolve_font(ts.font_registry(), None, None, None, None, None, 1.0, 1.0).unwrap();
let run = shape_text(ts.font_registry(), &resolved, text, 0).unwrap();
run.glyphs.iter().map(|g| g.glyph_id).collect()
}
const KAF: &str = "\u{0643}";
const TEH: &str = "\u{062A}";
const BEH: &str = "\u{0628}";
const KATABA: &str = "\u{0643}\u{062A}\u{0628}";
fn isolated_repertoire(ts: &Typesetter) -> HashSet<u16> {
[KAF, TEH, BEH]
.into_iter()
.flat_map(|l| isolated_glyphs(ts, l))
.collect()
}
#[test]
fn arabic_letters_join_into_contextual_forms() {
let ts = typesetter_with(NOTO_ARABIC);
let resolved =
resolve_font(ts.font_registry(), None, None, None, None, None, 1.0, 1.0).unwrap();
let isolated = isolated_repertoire(&ts);
let word = shape_text_directed(
ts.font_registry(),
&resolved,
KATABA,
0,
TextDirection::RightToLeft,
&[],
)
.unwrap();
assert!(
word.glyphs.iter().all(|g| g.glyph_id != 0),
"Arabic word should have no .notdef glyphs (wrong font?)"
);
let joined: Vec<u16> = word.glyphs.iter().map(|g| g.glyph_id).collect();
let contextual: Vec<u16> = joined
.iter()
.copied()
.filter(|g| !isolated.contains(g))
.collect();
assert!(
!contextual.is_empty(),
"Arabic joining should produce contextual glyph forms outside the \
isolated repertoire; got {joined:?}, all of which are isolated \
forms drawn from {isolated:?} — the letters did not join"
);
}
#[test]
fn arabic_joining_survives_an_explicit_direction() {
let ts = typesetter_with(NOTO_ARABIC);
let resolved =
resolve_font(ts.font_registry(), None, None, None, None, None, 1.0, 1.0).unwrap();
let shape = |dir| {
let run = shape_text_directed(ts.font_registry(), &resolved, KATABA, 0, dir, &[]).unwrap();
run.glyphs.iter().map(|g| g.glyph_id).collect::<Vec<u16>>()
};
assert_eq!(
shape(TextDirection::RightToLeft),
shape(TextDirection::Auto),
"explicitly naming the direction that auto-detection would have \
picked must produce identical glyphs"
);
let isolated = isolated_repertoire(&ts);
assert!(
shape(TextDirection::RightToLeft)
.iter()
.any(|g| !isolated.contains(g)),
"the explicit-direction path must produce joined forms"
);
}
#[test]
fn devanagari_forms_a_conjunct() {
let ts = typesetter_with(NOTO_DEVANAGARI);
let resolved =
resolve_font(ts.font_registry(), None, None, None, None, None, 1.0, 1.0).unwrap();
let text = "\u{0915}\u{094D}\u{0937}";
assert_eq!(text.chars().count(), 3);
let run = shape_text(ts.font_registry(), &resolved, text, 0).unwrap();
assert!(
run.glyphs.iter().all(|g| g.glyph_id != 0),
"Devanagari conjunct should have no .notdef glyphs (wrong font?)"
);
assert!(
run.glyphs.len() < text.chars().count(),
"Devanagari conjunct क्ष should shape to fewer glyphs than its \
{} codepoints (GSUB conjunct formation); got {} glyphs",
text.chars().count(),
run.glyphs.len()
);
}
#[test]
fn hebrew_rtl_glyphs_are_in_visual_order() {
let ts = typesetter_with(NOTO_HEBREW);
let resolved =
resolve_font(ts.font_registry(), None, None, None, None, None, 1.0, 1.0).unwrap();
let run = shape_text_directed(
ts.font_registry(),
&resolved,
"\u{05E9}\u{05DC}\u{05D5}\u{05DD}",
0,
TextDirection::RightToLeft,
&[],
)
.unwrap();
assert!(!run.glyphs.is_empty());
assert!(
run.glyphs.iter().all(|g| g.glyph_id != 0),
"Hebrew text should have no .notdef glyphs (wrong font?)"
);
let clusters: Vec<u32> = run.glyphs.iter().map(|g| g.cluster).collect();
assert!(
clusters.windows(2).all(|w| w[0] >= w[1]),
"RTL shaping should return glyphs in visual order (non-increasing \
clusters); got {clusters:?}"
);
assert!(
clusters.first() > clusters.last(),
"expected descending clusters across the RTL run; got {clusters:?}"
);
}
#[test]
fn arabic_joins_when_it_arrives_through_glyph_fallback() {
let mut fallback = Typesetter::new();
let latin = fallback.register_font(NOTO_SANS);
fallback.register_font(NOTO_ARABIC);
fallback.set_default_font(latin, 16.0);
let mut native = Typesetter::new();
let arabic = native.register_font(NOTO_ARABIC);
native.set_default_font(arabic, 16.0);
let glyphs = |ts: &Typesetter| -> Vec<u16> {
let resolved =
resolve_font(ts.font_registry(), None, None, None, None, None, 1.0, 1.0).unwrap();
shape_text_with_fallback(
ts.font_registry(),
&resolved,
KATABA,
0,
TextDirection::RightToLeft,
&[],
)
.unwrap()
.glyphs
.iter()
.map(|g| g.glyph_id)
.collect()
};
let via_fallback = glyphs(&fallback);
let direct = glyphs(&native);
assert!(
via_fallback.iter().all(|&g| g != 0),
"fallback should have resolved every glyph; got {via_fallback:?}"
);
assert_eq!(
via_fallback,
direct,
"Arabic reached through glyph fallback must shape the same as Arabic \
shaped natively — {} glyphs vs {}; per-character fallback yields the \
isolated bases with the dots stripped",
via_fallback.len(),
direct.len()
);
}