Merge pull request #3376 from dotCooCoo/fix/tests-half-form-face-filter

Tests: the half-form test uses only a fallback face that forms Ka's half form (#2670)
This commit is contained in:
BFlat
2026-10-21 23:50:02 +07:00
committed by GitHub
2 changed files with 101 additions and 24 deletions
+46
View File
@@ -297,6 +297,52 @@ pub fn vertical_punctuation_font(alternates: bool) -> Option<Vec<u8>> {
Some(builder.build())
}
/// The family of [`half_form_font`].
pub const HALF_FORM_FAMILY: &str = "Halftest Sans";
/// A test-only cmap over bundled Source Sans 3: Devanagari Ka uses K, the halant uses a period, Ssa
/// uses S and the zero width joiner uses the space. The `GSUB` table lists the `dev2` script either
/// way. With `half`, its `half` feature substitutes k for K followed by the period (Ka's half form);
/// without it, the script has no features and Ka has no half form. `None` when it can't be built.
pub fn half_form_font(half: bool) -> Option<Vec<u8>> {
use write_fonts::tables::cmap::Cmap;
use write_fonts::tables::gsub::{Gsub, Ligature, LigatureSet, LigatureSubstFormat1, SubstitutionLookup, SubstitutionLookupList};
use write_fonts::tables::layout::{CoverageFormat1, Feature, FeatureList, FeatureRecord, LangSys, Lookup, Script, ScriptList, ScriptRecord};
use write_fonts::types::GlyphId16;
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
let font = skrifa::FontRef::new(BASE).ok()?;
let charmap = font.charmap();
let mapping = [('क', 'K'), ('\u{094D}', '.'), ('ष', 'S'), ('\u{200D}', ' ')];
let mappings = mapping.into_iter().map(|(c, source)| Some((c, write_fonts::types::GlyphId::new(charmap.map(source)?.to_u32()))));
let cmap = Cmap::from_mappings(mappings.collect::<Option<Vec<_>>>()?).ok()?;
let family = HALF_FORM_FAMILY;
let name = name_table(&[(1, family), (2, "Regular"), (4, "Halftest Sans Regular"), (6, "HalftestSans-Regular")])?;
let mut builder = FontBuilder::new();
builder.add_table(&cmap).ok()?;
builder.add_raw(Tag::new(b"name"), name);
let (features, lookups) = if half {
let gid = |c| Some(GlyphId16::new(u16::try_from(charmap.map(c)?.to_u32()).ok()?));
let coverage = CoverageFormat1::new(vec![gid('K')?]);
let subst = LigatureSubstFormat1::new(coverage.into(), vec![LigatureSet::new(vec![Ligature::new(gid('k')?, vec![gid('.')?])])]);
let features = FeatureList::new(vec![FeatureRecord::new(Tag::new(b"half"), Feature::new(None, vec![0]))]);
let lookups = SubstitutionLookupList::new(vec![SubstitutionLookup::Ligature(Lookup::new(Default::default(), vec![subst]))]);
(features, lookups)
} else {
(FeatureList::new(vec![]), SubstitutionLookupList::new(vec![]))
};
let langsys = LangSys::new(if half { vec![0] } else { vec![] });
let scripts = ScriptList::new(vec![ScriptRecord::new(Tag::new(b"dev2"), Script::new(Some(langsys), vec![]))]);
builder.add_table(&Gsub::new(scripts, features, lookups)).ok()?;
for r in font.table_directory.table_records() {
let tag = Tag::new(&r.tag().to_be_bytes());
if !builder.contains(tag) {
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
}
}
Some(builder.build())
}
/// The family of [`pair_kerning_font`].
pub const PAIR_KERNING_FAMILY: &str = "Pairtest Sans";
+55 -24
View File
@@ -5,7 +5,8 @@
//! specifications for Devanagari and Bengali include them in a syllable after a halant
//! (https://learn.microsoft.com/en-us/typography/script-development/devanagari,
//! https://learn.microsoft.com/en-us/typography/script-development/bengali).
//! No font is committed: the tests use installed faces and print SKIPPED when none is found.
//! No font is committed: the tests use installed faces and print SKIPPED when none is found, except
//! the check of the half-form test's face filter, which builds its faces in code.
use super::*;
use kurbo::Point;
use std::sync::Arc;
@@ -14,25 +15,16 @@ use vectorcraft_doc::CharStyle;
const ZWNJ: char = '\u{200C}';
const ZWJ: char = '\u{200D}';
/// A Latin face that maps both joiners but not `c`, and the face that the fallback gives for `c`.
/// With `tags`, the fallback face's GSUB script list must contain one of those script tags. Which
/// face the fallback gives depends on the faces loaded so far, so the tests compare with that face.
fn joiner_test_faces(test: &str, c: char, tags: &[&[u8; 4]]) -> Option<(Arc<FontFace>, Arc<FontFace>)> {
/// A Latin face that maps both joiners but not `c`, and the face that the fallback gives for `c`,
/// which must pass `suits`. Which face the fallback gives depends on the faces loaded so far, so the
/// tests compare with that face.
fn joiner_test_faces(test: &str, c: char, suits: impl Fn(&FontFace) -> bool) -> Option<(Arc<FontFace>, Arc<FontFace>)> {
let db = FontDb::global();
let opentype = |face: &FontFace| {
use skrifa::raw::TableProvider;
tags.is_empty()
|| face
.skrifa()
.and_then(|f| f.gsub().ok())
.and_then(|gsub| gsub.script_list().ok())
.is_some_and(|list| list.script_records().iter().any(|r| tags.contains(&&r.script_tag().to_be_bytes())))
};
let faces = ["Source Sans 3", "Helvetica", "Arial", "Times New Roman", "Liberation Sans", "DejaVu Sans"]
.into_iter()
.filter_map(|name| db.face(name, "Regular").filter(|face| face.family == name))
.filter(|primary| primary.covers(ZWNJ) && primary.covers(ZWJ) && !primary.covers(c))
.find_map(|primary| db.fallback_for(c, primary.id()).filter(|f| opentype(f)).map(|fallback| (primary, fallback)));
.find_map(|primary| db.fallback_for(c, primary.id()).filter(|f| suits(f)).map(|fallback| (primary, fallback)));
if let Some((primary, fallback)) = &faces {
let path = fallback.path().map_or_else(|| "bundled font".to_string(), |p| p.display().to_string());
eprintln!("ASSERTIONS RUN: {test}; {} {} with the fallback {} {} ({path})", primary.family, primary.style, fallback.family, fallback.style);
@@ -43,8 +35,38 @@ fn joiner_test_faces(test: &str, c: char, tags: &[&[u8; 4]]) -> Option<(Arc<Font
}
fn layout_in(face: &FontFace, text: &str, tracking: f64) -> TextLayout {
layout_in_db(FontDb::global(), face, text, tracking)
}
fn layout_in_db(db: &FontDb, face: &FontFace, text: &str, tracking: f64) -> TextLayout {
let st = CharStyle { font_family: face.family.clone(), font_style: face.style.clone(), size: 20.0, tracking, ..CharStyle::default() };
layout(FontDb::global(), &TextObject::point(Point::ZERO, text, st))
layout(db, &TextObject::point(Point::ZERO, text, st))
}
/// Ka + halant + ZWJ + Ssa: the half form of Ka before a full Ssa.
fn ka_half_ssa() -> String {
format!("क्{ZWJ}ष")
}
/// Whether `layout` of [`ka_half_ssa`] in `face` starts with a glyph other than Ka's and the
/// halant's (Ka's half form) and ends with Ssa's.
fn starts_with_a_half_form(face: &FontFace, layout: &TextLayout) -> bool {
let half = layout.glyphs.first().map(|g| g.gid);
half.is_some_and(|gid| gid != face.glyph_for('क') && gid != face.glyph_for('\u{094D}'))
&& layout.glyphs.last().map(|g| g.gid) == Some(face.glyph_for('ष'))
}
/// Whether `face` can serve the half-form test: its GSUB script list contains `dev2` or `deva`, and
/// [`ka_half_ssa`] set in `face` alone, in `db`, is drawn in `face` with Ka's half form.
fn forms_a_ka_half_form(db: &FontDb, face: &FontFace) -> bool {
use skrifa::raw::TableProvider;
let devanagari = face
.skrifa()
.and_then(|f| f.gsub().ok())
.and_then(|gsub| gsub.script_list().ok())
.is_some_and(|list| list.script_records().iter().any(|r| matches!(&r.script_tag().to_be_bytes(), b"dev2" | b"deva")));
let l = layout_in_db(db, face, &ka_half_ssa(), 0.0);
devanagari && l.glyphs.iter().all(|g| g.font_id == face.id()) && starts_with_a_half_form(face, &l)
}
/// `text` set in `primary` is drawn, spaced and stepped through exactly as `text` set in `fallback`.
@@ -65,7 +87,7 @@ fn assert_shaped_as_in_the_fallback(primary: &FontFace, fallback: &FontFace, tex
#[test]
fn a_zero_width_non_joiner_in_devanagari_is_shaped_with_the_letters_around_it() {
let test = "a_zero_width_non_joiner_in_devanagari_is_shaped_with_the_letters_around_it";
let Some((primary, fallback)) = joiner_test_faces(test, 'क', &[]) else { return };
let Some((primary, fallback)) = joiner_test_faces(test, 'क', |_| true) else { return };
// Ka + halant + ZWNJ + Ssa: Ka keeps its explicit halant instead of forming the KaSsa conjunct.
let s = format!("क्{ZWNJ}ष");
assert_shaped_as_in_the_fallback(&primary, &fallback, &s);
@@ -74,7 +96,7 @@ fn a_zero_width_non_joiner_in_devanagari_is_shaped_with_the_letters_around_it()
#[test]
fn a_zero_width_non_joiner_in_bangla_is_shaped_with_the_letters_around_it() {
let test = "a_zero_width_non_joiner_in_bangla_is_shaped_with_the_letters_around_it";
let Some((primary, fallback)) = joiner_test_faces(test, 'ক', &[]) else { return };
let Some((primary, fallback)) = joiner_test_faces(test, 'ক', |_| true) else { return };
let s = format!("ক্{ZWNJ}ষ");
assert_shaped_as_in_the_fallback(&primary, &fallback, &s);
}
@@ -82,12 +104,21 @@ fn a_zero_width_non_joiner_in_bangla_is_shaped_with_the_letters_around_it() {
#[test]
fn a_zero_width_joiner_in_devanagari_asks_for_a_half_form() {
let test = "a_zero_width_joiner_in_devanagari_asks_for_a_half_form";
let Some((primary, fallback)) = joiner_test_faces(test, 'क', &[b"dev2", b"deva"]) else { return };
// Ka + halant + ZWJ + Ssa: the half form of Ka before a full Ssa.
let s = format!("क्{ZWJ}ष");
let Some((primary, fallback)) = joiner_test_faces(test, 'क', |f| forms_a_ka_half_form(FontDb::global(), f)) else { return };
let s = ka_half_ssa();
assert_shaped_as_in_the_fallback(&primary, &fallback, &s);
let l = layout_in(&primary, &s, 0.0);
let half = l.glyphs.first().map(|g| g.gid);
assert!(half.is_some_and(|gid| gid != fallback.glyph_for('क') && gid != fallback.glyph_for('\u{094D}')), "no half form: {:?}", l.glyphs);
assert_eq!(l.glyphs.last().map(|g| g.gid), Some(fallback.glyph_for('ष')), "{:?}", l.glyphs);
assert!(starts_with_a_half_form(&fallback, &l), "no half form: {:?}", l.glyphs);
}
#[test]
fn a_devanagari_face_without_a_half_form_is_not_used_for_the_half_form_test() {
use crate::test_fonts::{HALF_FORM_FAMILY, half_form_font};
for half in [false, true] {
let db = FontDb::with_font_dirs(vec![]);
db.add_font(half_form_font(half).unwrap());
let face = db.face(HALF_FORM_FAMILY, "Regular").unwrap();
// Both faces list `dev2`; only the one with a `half` feature forms Ka's half form.
assert_eq!(forms_a_ka_half_form(&db, &face), half, "half feature {half}");
}
}