Skip to content

Commit c4f98f5

Browse files
authored
[aat] Apply language-specific morph features (#453)
1 parent 168bc7b commit c4f98f5

9 files changed

Lines changed: 92 additions & 33 deletions

File tree

HARFBUZZ.md

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -48,40 +48,23 @@ locks the environment variables.
4848
$ HB_SHAPER_LIST=harfrust meson test -C build
4949
```
5050

51-
If all goes well, you should see lots of output, ending in:
51+
The exact totals change as HarfBuzz adds tests. As of August 24, 2026,
52+
the relevant shaping suites report:
5253

5354
```
54-
Ok: 217
55-
Expected Fail: 0
56-
Fail: 2
57-
Unexpected Pass: 0
58-
Skipped: 0
59-
Timeout: 0
55+
shape+aots: 800/800 passed
56+
shape+text-rendering-tests: 437/437 passed
57+
shape+in-house: 5012/5021 passed
6058
```
6159

62-
If you scroll up, you'd see that the two failing tests are:
63-
```
64-
214/219 harfbuzz:shape+text-rendering-tests / text-rendering-tests FAIL 0.12s 432/435 subtests passed
65-
```
66-
and
67-
```
68-
218/219 harfbuzz:shape+in-house / in-house FAIL 0.36s 4779/4800 subtests passed
69-
```
70-
71-
This is pretty good. In total, there are 24 shaping tests failing.
72-
Those are mostly due to HarfRust not supporting some esoteric
73-
shaping features of HarfBuzz.
74-
7560
To see specific failures, you can run inspect the test log:
7661
```sh
7762
$ less build/meson-logs/testlog.txt
7863
```
7964

80-
Currently the following tests fail:
81-
- `SHBALI-3.tests`: Rounding differences with unusual UPEM.
82-
- `arabic-fallback-positioning.tests`: Not implemented.
83-
- `collections.tests`: `DFONT` format is not supported.
84-
- `vertical.tests`: Fallback based on glyph extents not supported.
65+
The nine remaining shaping failures are all in
66+
`arabic-fallback-shaping.tests`; HarfRust does not yet synthesize Arabic
67+
fallback lookups for fonts without the required OpenType substitutions.
8568

8669

8770
## Running HarfBuzz's Benchmark Tests

harfrust/src/hb/aat/layout.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ pub fn substitute(
508508
) {
509509
let mut aat_map = map::AatMap::default();
510510
if !features.is_empty() {
511-
let mut builder = map::AatMapBuilder::default();
511+
let mut builder = map::AatMapBuilder::new(plan.language.as_ref());
512512
for feature in features {
513513
builder.add_feature(face, feature);
514514
}

harfrust/src/hb/aat/layout_morx_table.rs

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use crate::hb::aat::layout_common::{
55
START_OF_TEXT,
66
};
77
use crate::hb::ot_layout::MAX_CONTEXT_LENGTH;
8-
use crate::hb::{hb_font_t, GlyphInfo};
8+
use crate::hb::tag::lang_matches;
9+
use crate::hb::{hb_font_t, GlyphInfo, Language};
910
use crate::U32Set;
1011
use alloc::{vec, vec::Vec};
1112
use read_fonts::tables::aat::{self, ExtendedStateTable, NoPayload, StateEntry, StateTable};
@@ -66,10 +67,32 @@ pub fn compile_flags(face: &hb_font_t, builder: &AatMapBuilder, map: &mut AatMap
6667
})
6768
.is_ok()
6869
};
70+
let language_matches = |setting: u16| {
71+
let Some(index) = setting.checked_sub(1) else {
72+
return false;
73+
};
74+
let Some(requested) = builder.language.as_ref() else {
75+
return false;
76+
};
77+
let Some(ltag) = face.aat_tables.ltag.as_ref() else {
78+
return false;
79+
};
80+
let Some(tag) = ltag
81+
.tag_indices()
82+
.find_map(|(tag_index, tag)| (tag_index == u32::from(index)).then_some(tag))
83+
else {
84+
return false;
85+
};
86+
let Some(language) = Language::new(tag) else {
87+
return false;
88+
};
89+
lang_matches(requested.as_bytes(), language.as_bytes())
90+
};
6991

7092
fn compile_chain(
7193
chain: &impl MorphChain,
7294
has_feature: &impl Fn(u16, u16) -> bool,
95+
language_matches: &impl Fn(u16) -> bool,
7396
chain_flags: &mut Vec<RangeFlags>,
7497
builder: &AatMapBuilder,
7598
) {
@@ -93,8 +116,12 @@ pub fn compile_flags(face: &hb_font_t, builder: &AatMapBuilder, map: &mut AatMap
93116
flags &= disable_flags;
94117
flags |= enable_flags;
95118
}
119+
} else if feature_type == FEATURE_TYPE_LANGUAGE_TAG_TYPE as u16
120+
&& language_matches(feature_setting)
121+
{
122+
flags &= disable_flags;
123+
flags |= enable_flags;
96124
}
97-
// TODO: Port the following commit: https://github.com/harfbuzz/harfbuzz/commit/2124ad890
98125
},
99126
);
100127

@@ -110,15 +137,27 @@ pub fn compile_flags(face: &hb_font_t, builder: &AatMapBuilder, map: &mut AatMap
110137
map.chain_flags.resize(chains.iter().count(), vec![]);
111138
for (chain, chain_flags) in chains.iter().zip(map.chain_flags.iter_mut()) {
112139
if let Ok(chain) = chain {
113-
compile_chain(&chain, &has_feature, chain_flags, builder);
140+
compile_chain(
141+
&chain,
142+
&has_feature,
143+
&language_matches,
144+
chain_flags,
145+
builder,
146+
);
114147
}
115148
}
116149
} else {
117150
let chains = face.aat_tables.mort.as_ref()?.0.chains();
118151
map.chain_flags.resize(chains.iter().count(), vec![]);
119152
for (chain, chain_flags) in chains.iter().zip(map.chain_flags.iter_mut()) {
120153
if let Ok(chain) = chain {
121-
compile_chain(&chain, &has_feature, chain_flags, builder);
154+
compile_chain(
155+
&chain,
156+
&has_feature,
157+
&language_matches,
158+
chain_flags,
159+
builder,
160+
);
122161
}
123162
}
124163
}

harfrust/src/hb/aat/map.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use alloc::vec::Vec;
55
use core::cmp::Ordering;
66

77
use super::layout::*;
8-
use crate::hb::{hb_font_t, hb_mask_t, hb_tag_t};
8+
use crate::hb::{hb_font_t, hb_mask_t, hb_tag_t, Language};
99

1010
/// HB: hb_aat_map_t
1111
///
@@ -31,6 +31,7 @@ pub struct RangeFlags {
3131
/// See <https://github.com/harfbuzz/harfbuzz/blob/2c22a65f0cb99544c36580b9703a43b5dc97a9e1/src/hb-aat-map.hh#L49>
3232
#[doc(alias = "hb_aat_map_builder_t")]
3333
pub struct AatMapBuilder {
34+
pub language: Option<Language>,
3435
pub current_features: Vec<FeatureInfo>,
3536
pub features: Vec<FeatureRange>,
3637
pub range_first: usize,
@@ -40,6 +41,7 @@ pub struct AatMapBuilder {
4041
impl Default for AatMapBuilder {
4142
fn default() -> Self {
4243
Self {
44+
language: None,
4345
range_first: HB_FEATURE_GLOBAL_START as usize,
4446
range_last: HB_FEATURE_GLOBAL_END as usize,
4547
current_features: Vec::default(),
@@ -49,6 +51,13 @@ impl Default for AatMapBuilder {
4951
}
5052

5153
impl AatMapBuilder {
54+
pub fn new(language: Option<&Language>) -> Self {
55+
Self {
56+
language: language.cloned(),
57+
..Self::default()
58+
}
59+
}
60+
5261
pub fn add_feature(&mut self, face: &hb_font_t, feature: &Feature) -> Option<()> {
5362
let feat = face.aat_tables.feat.as_ref()?;
5463

harfrust/src/hb/aat/mod.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ use crate::hb::ot::OtCache;
1313
use crate::hb::tables::TableRanges;
1414
use alloc::vec::Vec;
1515
use read_fonts::{
16-
tables::{ankr::Ankr, feat::Feat, kern::Kern, kerx::Kerx, mort::Mort, morx::Morx, trak::Trak},
16+
tables::{
17+
ankr::Ankr, feat::Feat, kern::Kern, kerx::Kerx, ltag::Ltag, mort::Mort, morx::Morx,
18+
trak::Trak,
19+
},
1720
FontRef, TableProvider,
1821
};
1922

@@ -35,6 +38,7 @@ pub struct AatCache {
3538
has_kerx: bool,
3639
pub(crate) has_trak: bool,
3740
has_feat: bool,
41+
has_ltag: bool,
3842
}
3943

4044
impl AatCache {
@@ -68,6 +72,7 @@ impl AatCache {
6872
cache.has_kerx = kerx.is_some();
6973
cache.has_trak = font.trak().is_ok();
7074
cache.has_feat = font.feat().is_ok();
75+
cache.has_ltag = font.ltag().is_ok();
7176

7277
if let Some(morx) = morx.filter(|_| cache.has_morx || cache.has_morx_from_tables) {
7378
let morx_base = morx.offset_data().as_bytes().as_ptr() as usize;
@@ -161,6 +166,7 @@ pub struct AatTables<'a> {
161166
pub kerx: Option<(Kerx<'a>, &'a [KerxSubtableCache])>,
162167
pub trak: Option<Trak<'a>>,
163168
pub feat: Option<Feat<'a>>,
169+
pub ltag: Option<Ltag<'a>>,
164170
}
165171

166172
use crate::algs::HB_CODEPOINT_ENCODE3 as encode3;
@@ -222,6 +228,10 @@ impl<'a> AatTables<'a> {
222228
.has_feat
223229
.then(|| table_ranges.feat.resolve_table(font))
224230
.flatten();
231+
let ltag = cache
232+
.has_ltag
233+
.then(|| table_ranges.ltag.resolve_table(font))
234+
.flatten();
225235
Self {
226236
safe_to_break: Some(&cache.safe_to_break),
227237
morx,
@@ -231,6 +241,7 @@ impl<'a> AatTables<'a> {
231241
kerx,
232242
trak,
233243
feat,
244+
ltag,
234245
}
235246
}
236247

@@ -270,6 +281,7 @@ impl<'a> AatTables<'a> {
270281
.map(|table| (table, cache.kerx.as_slice()));
271282
let trak = cache.has_trak.then(|| font.trak().ok()).flatten();
272283
let feat = cache.has_feat.then(|| font.feat().ok()).flatten();
284+
let ltag = cache.has_ltag.then(|| font.ltag().ok()).flatten();
273285
Self {
274286
safe_to_break: Some(&cache.safe_to_break),
275287
morx,
@@ -279,6 +291,7 @@ impl<'a> AatTables<'a> {
279291
kerx,
280292
trak,
281293
feat,
294+
ltag,
282295
}
283296
}
284297
}

harfrust/src/hb/face.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,7 @@ mod tests {
711711
assert!(aat_tables.kerx.is_none());
712712
assert!(aat_tables.trak.is_none());
713713
assert!(aat_tables.feat.is_none());
714+
assert!(aat_tables.ltag.is_none());
714715
assert_eq!(provider.loads.get(), 3);
715716
}
716717

harfrust/src/hb/ot_shape.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ impl<'a> hb_ot_shape_planner_t<'a> {
3636
language: Option<&Language>,
3737
) -> Self {
3838
let ot_map = hb_ot_map_builder_t::new(face, script, language);
39-
let aat_map = AatMapBuilder::default();
39+
let aat_map = AatMapBuilder::new(language);
4040

4141
let mut shaper = match script {
4242
Some(script) => hb_ot_shape_complex_categorize(

harfrust/src/hb/tables.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use read_fonts::{
1515
kern::Kern,
1616
kerx::Kerx,
1717
loca::Loca,
18+
ltag::Ltag,
1819
mort::Mort,
1920
morx::Morx,
2021
mvar::Mvar,
@@ -68,6 +69,7 @@ pub struct TableRanges {
6869
pub gpos: TableRange,
6970
pub mort: TableRange,
7071
pub morx: TableRange,
72+
pub ltag: TableRange,
7173
pub kerx: TableRange,
7274
pub ankr: TableRange,
7375
pub kern: TableRange,
@@ -143,6 +145,7 @@ impl TableRanges {
143145
let gpos = offset(Gpos::TAG);
144146
let mort = offset(Mort::TAG);
145147
let morx = offset(Morx::TAG);
148+
let ltag = offset(Ltag::TAG);
146149
let kerx = offset(Kerx::TAG);
147150
let ankr = offset(Ankr::TAG);
148151
let kern = offset(Kern::TAG);
@@ -173,6 +176,7 @@ impl TableRanges {
173176
gpos,
174177
mort,
175178
morx,
179+
ltag,
176180
kerx,
177181
ankr,
178182
kern,
@@ -230,6 +234,7 @@ impl TableRanges {
230234
gpos: TableRange::default(),
231235
mort: TableRange::default(),
232236
morx: TableRange::default(),
237+
ltag: TableRange::default(),
233238
kerx: TableRange::default(),
234239
ankr: TableRange::default(),
235240
kern: TableRange::default(),

harfrust/src/hb/tag.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,15 @@ mod tests {
311311
#![allow(non_snake_case)]
312312

313313
use super::*;
314+
315+
#[test]
316+
fn language_matches() {
317+
assert!(lang_matches(b"pl", b"pl"));
318+
assert!(lang_matches(b"pl-pl", b"pl"));
319+
assert!(!lang_matches(b"pl", b"pl-pl"));
320+
assert!(!lang_matches(b"plx", b"pl"));
321+
assert!(!lang_matches(b"en", b"pl"));
322+
}
314323
use alloc::vec::Vec;
315324

316325
fn new_tag_to_script(tag: hb_tag_t) -> Option<Script> {

0 commit comments

Comments
 (0)