Skip to main content

harfrust/hb/ot/gsub/
ligature.rs

1use crate::hb::buffer::GlyphInfo;
2use crate::hb::ot::{coverage_index, coverage_index_cached, CoverageInfo};
3use crate::hb::ot_layout_gsubgpos::OT::hb_ot_apply_context_t;
4use crate::hb::ot_layout_gsubgpos::{
5    ligate_input, match_always, match_glyph, match_input, may_skip_t, skipping_iterator_t, Apply,
6    LigatureSubstFormat1Cache, LigatureSubstFormat1SmallCache, SubtableExternalCache,
7    SubtableExternalCacheMode, WouldApply, WouldApplyContext,
8};
9use crate::hb::set_digest::hb_set_digest_t;
10use alloc::boxed::Box;
11use read_fonts::tables::gsub::{Ligature, LigatureSet, LigatureSubstFormat1};
12use read_fonts::types::GlyphId;
13
14// HarfBuzz builds this cache from tables that have already passed sanitizer
15// traversal budgets. read-fonts is lazy, so bound this optional raw-table walk
16// locally and fall back to a full digest when the table is pathological.
17const MAX_LIGATURE_CACHE_WORK: usize = 16_384;
18
19impl WouldApply for Ligature<'_> {
20    fn would_apply(&self, ctx: &WouldApplyContext) -> bool {
21        let components = self.component_glyph_ids();
22        ctx.glyphs.len() == components.len() + 1
23            && components
24                .iter()
25                .map(|comp| GlyphId::from(comp.get()))
26                .enumerate()
27                .all(|(i, comp)| ctx.glyphs[i + 1] == comp)
28    }
29}
30
31impl Apply for Ligature<'_> {
32    fn apply(&self, ctx: &mut hb_ot_apply_context_t) -> Option<()> {
33        // Special-case to make it in-place and not consider this
34        // as a "ligated" substitution.
35        let components = self.component_glyph_ids();
36        if components.is_empty() {
37            ctx.replace_glyph(self.ligature_glyph().into());
38            Some(())
39        } else {
40            let f = |info: &mut GlyphInfo, index| {
41                let value = components.get(index as usize).unwrap().get().to_u32();
42                match_glyph(info, value)
43            };
44
45            let mut match_end = 0;
46            let mut total_component_count = 0;
47
48            if !match_input(
49                ctx,
50                components.len() as u16,
51                f,
52                &mut match_end,
53                Some(&mut total_component_count),
54            ) {
55                ctx.buffer
56                    .unsafe_to_concat(Some(ctx.buffer.idx), Some(match_end));
57                return None;
58            }
59            let count = components.len() + 1;
60            ligate_input(
61                ctx,
62                count,
63                match_end,
64                total_component_count,
65                self.ligature_glyph().into(),
66            );
67            Some(())
68        }
69    }
70}
71
72impl WouldApply for LigatureSet<'_> {
73    fn would_apply(&self, ctx: &WouldApplyContext) -> bool {
74        self.ligatures()
75            .iter()
76            .filter_map(Result::ok)
77            .any(|lig| lig.would_apply(ctx))
78    }
79}
80
81pub trait ApplyLigatureSet {
82    fn apply(&self, ctx: &mut hb_ot_apply_context_t, seconds: &hb_set_digest_t) -> Option<()>;
83}
84
85impl ApplyLigatureSet for LigatureSet<'_> {
86    fn apply(&self, ctx: &mut hb_ot_apply_context_t, seconds: &hb_set_digest_t) -> Option<()> {
87        let mut second = GlyphId::new(u32::MAX);
88        let mut unsafe_to = 0;
89        let ligatures = self.ligatures();
90        let slow_path = if ligatures.len() <= 1 {
91            true
92        } else {
93            let mut iter = skipping_iterator_t::with_match_fn(ctx, true, Some(match_always));
94            iter.reset(iter.buffer.idx);
95            let matched = iter.next(Some(&mut unsafe_to));
96            if !matched {
97                true
98            } else {
99                second = iter.buffer.info[iter.index()].glyph_id.into();
100                unsafe_to = iter.index() + 1;
101
102                // Can't use the fast path if eg. the next char is a default-ignorable
103                // or other skippable.
104                iter.may_skip(&iter.buffer.info[iter.index()]) != may_skip_t::SKIP_NO
105            }
106        };
107
108        if slow_path {
109            // Slow path
110            for lig in ligatures.iter().filter_map(Result::ok) {
111                if lig.apply(ctx).is_some() {
112                    return Some(());
113                }
114            }
115        } else {
116            // Fast path
117            if !seconds.may_have(second.into()) {
118                return None;
119            }
120            let mut unsafe_to_concat = false;
121            for lig in ligatures.iter().filter_map(|lig| lig.ok()) {
122                let components = lig.component_glyph_ids();
123                if components.is_empty() || components[0].get() == second {
124                    if lig.apply(ctx).is_some() {
125                        if unsafe_to_concat {
126                            ctx.buffer
127                                .unsafe_to_concat(Some(ctx.buffer.idx), Some(unsafe_to));
128                        }
129                        return Some(());
130                    }
131                } else if !components.is_empty() {
132                    unsafe_to_concat = true;
133                }
134            }
135            if unsafe_to_concat {
136                ctx.buffer
137                    .unsafe_to_concat(Some(ctx.buffer.idx), Some(unsafe_to));
138            }
139        }
140        None
141    }
142}
143
144impl WouldApply for LigatureSubstFormat1<'_> {
145    fn would_apply(&self, ctx: &WouldApplyContext) -> bool {
146        self.coverage()
147            .ok()
148            .and_then(|coverage| coverage.get(ctx.glyphs[0]))
149            .and_then(|index| self.ligature_sets().get(index as usize).ok())
150            .is_some_and(|set| set.would_apply(ctx))
151    }
152}
153
154impl Apply for LigatureSubstFormat1<'_> {
155    fn apply_with_external_cache(
156        &self,
157        ctx: &mut hb_ot_apply_context_t,
158        external_cache: &SubtableExternalCache,
159    ) -> Option<()> {
160        let glyph = ctx.buffer.cur(0).as_glyph();
161
162        let (index, seconds) = match external_cache {
163            SubtableExternalCache::LigatureSubstFormat1Cache(cache) => (
164                coverage_index_cached(
165                    |gid| self.coverage().ok()?.get(gid),
166                    glyph,
167                    &cache.coverage,
168                )?,
169                &cache.seconds,
170            ),
171            SubtableExternalCache::LigatureSubstFormat1SmallCache(cache) => (
172                cache.coverage.index(&self.offset_data(), glyph)?,
173                &cache.seconds,
174            ),
175            _ => (
176                coverage_index(self.coverage(), glyph)?,
177                &hb_set_digest_t::full(),
178            ),
179        };
180        self.ligature_sets()
181            .get(index as usize)
182            .ok()
183            .and_then(|set| set.apply(ctx, seconds))
184    }
185
186    fn external_cache_create(&self, mode: SubtableExternalCacheMode) -> SubtableExternalCache {
187        match mode {
188            SubtableExternalCacheMode::Full => SubtableExternalCache::LigatureSubstFormat1Cache(
189                Box::new(LigatureSubstFormat1Cache::new(collect_seconds(self))),
190            ),
191            SubtableExternalCacheMode::Small => {
192                if let Some(coverage) =
193                    CoverageInfo::new(&self.offset_data(), self.coverage_offset().to_u32() as u16)
194                {
195                    SubtableExternalCache::LigatureSubstFormat1SmallCache(
196                        LigatureSubstFormat1SmallCache {
197                            coverage,
198                            seconds: collect_seconds(self),
199                        },
200                    )
201                } else {
202                    SubtableExternalCache::None
203                }
204            }
205            SubtableExternalCacheMode::None => SubtableExternalCache::None,
206        }
207    }
208}
209
210fn collect_seconds(lig_subst: &LigatureSubstFormat1) -> hb_set_digest_t {
211    let mut seconds = hb_set_digest_t::new();
212    let mut remaining_work = MAX_LIGATURE_CACHE_WORK;
213    let ligature_sets = lig_subst.ligature_sets();
214    if ligature_sets.len() > remaining_work {
215        return hb_set_digest_t::full();
216    }
217    remaining_work -= ligature_sets.len();
218
219    for lig_set in ligature_sets.iter().filter_map(Result::ok) {
220        let ligatures = lig_set.ligatures();
221        if ligatures.len() > remaining_work {
222            return hb_set_digest_t::full();
223        }
224        remaining_work -= ligatures.len();
225
226        for lig in ligatures.iter().filter_map(Result::ok) {
227            if let Some(gid) = lig.component_glyph_ids().first() {
228                seconds.add(gid.get().into());
229            } else {
230                return hb_set_digest_t::full();
231            }
232        }
233    }
234    seconds
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use read_fonts::{FontData, FontRead};
241
242    fn assert_full_digest(digest: hb_set_digest_t) {
243        assert!(digest.may_have(0));
244        assert!(digest.may_have(12345));
245        assert!(digest.may_have(u16::MAX as u32));
246    }
247
248    #[test]
249    fn collect_seconds_bails_on_too_many_ligature_sets() {
250        let ligature_set_count = MAX_LIGATURE_CACHE_WORK + 1;
251        let mut data = vec![0; 6 + ligature_set_count * 2];
252        data[0..2].copy_from_slice(&1u16.to_be_bytes());
253        data[4..6].copy_from_slice(&(ligature_set_count as u16).to_be_bytes());
254
255        let lig_subst = LigatureSubstFormat1::read(FontData::new(&data)).unwrap();
256        assert_full_digest(collect_seconds(&lig_subst));
257    }
258
259    #[test]
260    fn collect_seconds_bails_on_too_many_ligatures() {
261        let ligature_count = MAX_LIGATURE_CACHE_WORK;
262        let mut data = vec![0; 10 + ligature_count * 2];
263        data[0..2].copy_from_slice(&1u16.to_be_bytes());
264        data[4..6].copy_from_slice(&1u16.to_be_bytes());
265        data[6..8].copy_from_slice(&8u16.to_be_bytes());
266        data[8..10].copy_from_slice(&(ligature_count as u16).to_be_bytes());
267
268        let lig_subst = LigatureSubstFormat1::read(FontData::new(&data)).unwrap();
269        assert_full_digest(collect_seconds(&lig_subst));
270    }
271}