harfrust/hb/ot/gsub/
ligature.rs1use 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
14const 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 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 iter.may_skip(&iter.buffer.info[iter.index()]) != may_skip_t::SKIP_NO
105 }
106 };
107
108 if slow_path {
109 for lig in ligatures.iter().filter_map(Result::ok) {
111 if lig.apply(ctx).is_some() {
112 return Some(());
113 }
114 }
115 } else {
116 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}