Coverage Report

Created: 2026-09-14 08:06

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/fontations/write-fonts/src/tables/gsub/builders.rs
Line
Count
Source
1
//! GSUB lookup builders
2
3
use std::{collections::BTreeMap, convert::TryFrom};
4
5
use types::{FixedSize, GlyphId16, Offset16};
6
7
use crate::{
8
    tables::{
9
        layout::{builders::Builder, CoverageTable},
10
        variations::ivs_builder::VariationStoreBuilder,
11
    },
12
    FontWrite,
13
};
14
15
/// Helper type for splitting layout subtables
16
// NOTE: currently just used for ligature substitution, but hopefully can be
17
// reused for other lookups as needed?
18
#[derive(Clone, Debug)]
19
struct TableSplitter<T: SplitTable> {
20
    finished: Vec<T>,
21
    current_coverage: Vec<GlyphId16>,
22
    current_items: Vec<T::Component>,
23
    current_size: usize,
24
}
25
26
/// A trait for splitting layout subtables.
27
trait SplitTable {
28
    /// The component item of this table.
29
    type Component;
30
31
    /// The (maximum) number of bytes required to store this item.
32
    ///
33
    /// This should include only the costs of the item, not the cost of adding
34
    /// to the coverage table.
35
    ///
36
    /// 'Maximum' because this does not account for possible size savings from
37
    /// deduplication of identical objects, for instance.
38
    fn size_for_item(item: &Self::Component) -> usize;
39
    /// The starting size of a new table that will contain this item.
40
    ///
41
    /// This does not include the size of the item itself! The item is provided
42
    /// because sometimes the table contains fields derived from its members.
43
    fn initial_size_for_item(item: &Self::Component) -> usize;
44
    fn instantiate(coverage: CoverageTable, items: Vec<Self::Component>) -> Self;
45
}
46
47
impl<T: SplitTable + FontWrite> TableSplitter<T> {
48
    const MAX_TABLE_SIZE: usize = u16::MAX as usize;
49
50
0
    fn new() -> Self {
51
0
        Self {
52
0
            finished: Vec::new(),
53
0
            current_coverage: Vec::new(),
54
0
            current_items: Vec::new(),
55
0
            current_size: 0,
56
0
        }
57
0
    }
58
59
0
    fn add(&mut self, gid: GlyphId16, item: T::Component) {
60
0
        let item_size = T::size_for_item(&item);
61
0
        if item_size + self.current_size > Self::MAX_TABLE_SIZE {
62
0
            let current_len = self.current_coverage.len();
63
0
            self.finish_current();
64
0
            let type_ = self.finished.last().unwrap().table_type();
65
0
            log::info!("adding split in {type_} at {current_len}");
66
0
        }
67
68
0
        if self.current_size == 0 {
69
0
            self.current_size = T::initial_size_for_item(&item);
70
0
        }
71
0
        self.current_coverage.push(gid);
72
0
        self.current_items.push(item);
73
        // item size + a glyph in the coverage table (worst case)
74
0
        self.current_size += item_size + GlyphId16::RAW_BYTE_LEN;
75
0
    }
76
77
0
    fn finish_current(&mut self) {
78
0
        if !self.current_coverage.is_empty() {
79
0
            let coverage = std::mem::take(&mut self.current_coverage).into();
80
0
            self.finished.push(T::instantiate(
81
0
                coverage,
82
0
                std::mem::take(&mut self.current_items),
83
0
            ));
84
0
            self.current_size = 0;
85
0
        }
86
0
    }
87
88
0
    fn finish(mut self) -> Vec<T> {
89
0
        self.finish_current();
90
0
        self.finished
91
0
    }
92
}
93
/// A builder for [`SingleSubst`](super::SingleSubst) subtables.
94
#[derive(Clone, Debug, Default, PartialEq, Eq)]
95
pub struct SingleSubBuilder {
96
    items: BTreeMap<GlyphId16, GlyphId16>,
97
}
98
99
impl SingleSubBuilder {
100
    /// Add this replacement to the builder.
101
    ///
102
    /// If there is an existing substitution for the provided target, it will
103
    /// be overwritten.
104
    pub fn insert(&mut self, target: GlyphId16, replacement: GlyphId16) {
105
        self.items.insert(target, replacement);
106
    }
107
108
    /// Returns `true` if all the pairs of items in the two iterators can be
109
    /// added to this lookup.
110
    ///
111
    /// The iterators are expected to be equal length.
112
    pub fn can_add(&self, target: GlyphId16, replacement: GlyphId16) -> bool {
113
        // only false if target exists with a different replacement
114
        !matches!(self.items.get(&target), Some(x) if *x != replacement)
115
    }
116
117
    /// Returns the number of rules in the builder.
118
    pub fn len(&self) -> usize {
119
        self.items.len()
120
    }
121
122
    /// Returns `true` if there are no rules in this builder.
123
    pub fn is_empty(&self) -> bool {
124
        self.items.is_empty()
125
    }
126
127
    /// Iterate all the substitution pairs in this builder.
128
    ///
129
    /// used when compiling the `aalt` feature.
130
    #[deprecated(since = "0.38.2", note = "use ::iter instead")]
131
    pub fn iter_pairs(&self) -> impl Iterator<Item = (GlyphId16, GlyphId16)> + '_ {
132
        self.iter()
133
    }
134
135
    /// Iterate all the substitution pairs in this builder.
136
    pub fn iter(&self) -> impl Iterator<Item = (GlyphId16, GlyphId16)> + '_ {
137
0
        self.items.iter().map(|(target, alt)| (*target, *alt))
138
    }
139
140
    /// Convert this `SingleSubBuilder` into a `MultipleSubBuilder`.
141
    ///
142
    /// This is used by the fea compiler in some cases to reduce the number
143
    /// of generated lookups.
144
    pub fn promote_to_multi_sub(self) -> MultipleSubBuilder {
145
        MultipleSubBuilder {
146
            items: self
147
                .items
148
                .into_iter()
149
0
                .map(|(key, gid)| (key, vec![gid]))
150
                .collect(),
151
        }
152
    }
153
154
    /// Convert this `SingleSubBuilder` into a `LigatureSubBuilder`.
155
    ///
156
    /// This is used by the fea compiler in some cases to reduce the number
157
    /// of generated lookups.
158
    pub fn promote_to_ligature_sub(self) -> LigatureSubBuilder {
159
        let mut items = BTreeMap::new();
160
        for (from, to) in self.items.into_iter() {
161
            items
162
                .entry(from)
163
                .or_insert(Vec::new())
164
                .push((Vec::new(), to));
165
        }
166
        LigatureSubBuilder { items }
167
    }
168
}
169
170
impl Builder for SingleSubBuilder {
171
    type Output = Vec<super::SingleSubst>;
172
173
    fn build(self, _: &mut VariationStoreBuilder) -> Self::Output {
174
        if self.items.is_empty() {
175
            return Default::default();
176
        }
177
        // if all pairs are equidistant and within the i16 range, find the
178
        // common delta
179
        let delta = self
180
            .items
181
            .iter()
182
0
            .map(|(k, v)| v.to_u16() as i32 - k.to_u16() as i32)
183
0
            .reduce(|acc, val| if acc == val { acc } else { i32::MAX })
184
0
            .and_then(|delta| i16::try_from(delta).ok());
185
186
        let coverage = self.items.keys().copied().collect();
187
        if let Some(delta) = delta {
188
            vec![super::SingleSubst::format_1(coverage, delta)]
189
        } else {
190
            let replacements = self.items.values().copied().collect();
191
            vec![super::SingleSubst::format_2(coverage, replacements)]
192
        }
193
    }
194
}
195
196
/// A builder for [`MultipleSubstFormat1`](super::MultipleSubstFormat1) subtables.
197
#[derive(Clone, Debug, Default, PartialEq, Eq)]
198
pub struct MultipleSubBuilder {
199
    items: BTreeMap<GlyphId16, Vec<GlyphId16>>,
200
}
201
202
impl Builder for MultipleSubBuilder {
203
    type Output = Vec<super::MultipleSubstFormat1>;
204
205
    fn build(self, _: &mut VariationStoreBuilder) -> Self::Output {
206
        let coverage = self.items.keys().copied().collect();
207
        let seq_tables = self.items.into_values().map(super::Sequence::new).collect();
208
        vec![super::MultipleSubstFormat1::new(coverage, seq_tables)]
209
    }
210
}
211
212
impl MultipleSubBuilder {
213
    /// Returns the number of rules in the builder.
214
    pub fn len(&self) -> usize {
215
        self.items.len()
216
    }
217
218
    /// Returns `true` if there are no rules in this builder.
219
    pub fn is_empty(&self) -> bool {
220
        self.items.is_empty()
221
    }
222
223
    /// Add a new substitution to this builder.
224
    ///
225
    /// If the target already exists with a different replacement, it will be
226
    /// overwritten.
227
    pub fn insert(&mut self, target: GlyphId16, replacement: Vec<GlyphId16>) {
228
        self.items.insert(target, replacement);
229
    }
230
231
    /// Returns `true` if no other replacement already exists for this target.
232
    pub fn can_add(&self, target: GlyphId16, replacement: &[GlyphId16]) -> bool {
233
        match self.items.get(&target) {
234
            None => true,
235
            Some(thing) => thing == replacement,
236
        }
237
    }
238
239
    /// Iterate over the rules in this builder.
240
    pub fn iter(&self) -> impl Iterator<Item = (&GlyphId16, &Vec<GlyphId16>)> {
241
        self.items.iter()
242
    }
243
}
244
245
/// A builder for [`AlternateSubstFormat1`](super::AlternateSubstFormat1) subtables
246
#[derive(Clone, Debug, Default, PartialEq, Eq)]
247
pub struct AlternateSubBuilder {
248
    items: BTreeMap<GlyphId16, Vec<GlyphId16>>,
249
}
250
251
impl AlternateSubBuilder {
252
    /// Returns the number of rules in the builder.
253
    pub fn len(&self) -> usize {
254
        self.items.len()
255
    }
256
257
    /// Returns `true` if there are no rules in this builder.
258
    pub fn is_empty(&self) -> bool {
259
        self.items.is_empty()
260
    }
261
262
    /// Add a new alternate sub rule to this lookup.
263
    pub fn insert(&mut self, target: GlyphId16, replacement: Vec<GlyphId16>) {
264
        self.items.insert(target, replacement);
265
    }
266
267
    /// Iterate over the rules in this builder.
268
    pub fn iter(&self) -> impl Iterator<Item = (&GlyphId16, &Vec<GlyphId16>)> {
269
        self.items.iter()
270
    }
271
272
    /// Iterate all alternates in this lookup.
273
    ///
274
    /// used when compiling aalt
275
    pub fn iter_pairs(&self) -> impl Iterator<Item = (GlyphId16, GlyphId16)> + '_ {
276
        self.items
277
            .iter()
278
0
            .flat_map(|(target, alt)| alt.iter().map(|alt| (*target, *alt)))
279
    }
280
}
281
282
impl Builder for AlternateSubBuilder {
283
    type Output = Vec<super::AlternateSubstFormat1>;
284
285
    fn build(self, _: &mut VariationStoreBuilder) -> Self::Output {
286
        let coverage = self.items.keys().copied().collect();
287
        let seq_tables = self
288
            .items
289
            .into_values()
290
            .map(super::AlternateSet::new)
291
            .collect();
292
        vec![super::AlternateSubstFormat1::new(coverage, seq_tables)]
293
    }
294
}
295
296
/// A builder for [`LigatureSubstFormat1`](super::LigatureSubstFormat1) subtables.
297
#[derive(Clone, Debug, Default, PartialEq, Eq)]
298
pub struct LigatureSubBuilder {
299
    items: BTreeMap<GlyphId16, Vec<(Vec<GlyphId16>, GlyphId16)>>,
300
}
301
302
impl LigatureSubBuilder {
303
    /// Returns the number of rules in the builder.
304
    pub fn len(&self) -> usize {
305
        self.items.len()
306
    }
307
308
    /// Returns `true` if there are no rules in this builder.
309
    pub fn is_empty(&self) -> bool {
310
        self.items.is_empty()
311
    }
312
313
    /// Add a new ligature substitution rule to the builder.
314
    pub fn insert(&mut self, target: Vec<GlyphId16>, replacement: GlyphId16) {
315
        let (first, rest) = target.split_first().unwrap();
316
        let entry = self.items.entry(*first).or_default();
317
        // skip duplicates
318
        if !entry
319
            .iter()
320
0
            .any(|existing| existing.0 == rest && existing.1 == replacement)
321
        {
322
            entry.push((rest.to_owned(), replacement))
323
        }
324
    }
325
326
    /// Check if this target sequence already has a replacement in this lookup.
327
    pub fn can_add(&self, target: &[GlyphId16], replacement: GlyphId16) -> bool {
328
        let Some((first, rest)) = target.split_first() else {
329
            return false;
330
        };
331
        match self.items.get(first) {
332
            Some(ligs) => !ligs
333
                .iter()
334
0
                .any(|(seq, target)| seq == rest && *target != replacement),
335
            None => true,
336
        }
337
    }
338
339
    /// Iterate over the current rules in the builder.
340
    ///
341
    /// The result is a tuple where the first item is the target glyph and the
342
    /// second item is a tuple of (components, replacement).
343
    pub fn iter(&self) -> impl Iterator<Item = (&GlyphId16, &Vec<(Vec<GlyphId16>, GlyphId16)>)> {
344
        self.items.iter()
345
    }
346
}
347
348
impl Builder for LigatureSubBuilder {
349
    type Output = Vec<super::LigatureSubstFormat1>;
350
351
    fn build(self, _: &mut VariationStoreBuilder) -> Self::Output {
352
        let mut splitter = TableSplitter::<super::LigatureSubstFormat1>::new();
353
        for (gid, mut ligs) in self.items.into_iter() {
354
            // we want to sort longer items first, but otherwise preserve
355
            // the order provided by the user.
356
0
            ligs.sort_by_key(|(lig, _)| std::cmp::Reverse(lig.len()));
357
            let lig_set = super::LigatureSet::new(
358
                ligs.into_iter()
359
0
                    .map(|(components, replacement)| super::Ligature::new(replacement, components))
360
                    .collect(),
361
            );
362
            splitter.add(gid, lig_set);
363
        }
364
        splitter.finish()
365
    }
366
}
367
368
impl SplitTable for super::LigatureSubstFormat1 {
369
    type Component = super::LigatureSet;
370
371
0
    fn size_for_item(item: &Self::Component) -> usize {
372
0
        item.compute_size()
373
0
    }
374
375
0
    fn initial_size_for_item(_item: &Self::Component) -> usize {
376
        // format, coverage offset, set count, sets offset
377
0
        u16::RAW_BYTE_LEN * 4
378
0
    }
379
380
0
    fn instantiate(coverage: CoverageTable, items: Vec<Self::Component>) -> Self {
381
0
        Self::new(coverage, items)
382
0
    }
383
}
384
385
impl super::LigatureSet {
386
0
    fn compute_size(&self) -> usize {
387
        // ligatureCount
388
0
        u16::RAW_BYTE_LEN
389
0
            // ligatureOffsets
390
0
            + Offset16::RAW_BYTE_LEN * self.ligatures.len()
391
            // size of each referenced ligature table
392
0
            + self
393
0
                .ligatures
394
0
                .iter()
395
0
                .map(|lig| lig.compute_size())
396
0
                .sum::<usize>()
397
0
    }
398
}
399
400
impl super::Ligature {
401
0
    fn compute_size(&self) -> usize {
402
        // ligatureGlyph
403
0
        u16::RAW_BYTE_LEN
404
0
            // componentCount
405
0
            + u16::RAW_BYTE_LEN
406
0
            // componentGlyphIDs
407
0
            + u16::RAW_BYTE_LEN * self.component_glyph_ids.len()
408
0
    }
409
}
410
411
#[cfg(test)]
412
mod tests {
413
    use crate::tables::gsub::LigatureSubstFormat1;
414
415
    use super::*;
416
417
    fn make_lig_table(n_bytes: u16, first: u16) -> super::super::Ligature {
418
        assert!(n_bytes >= 6, "minimum table size");
419
        assert!(n_bytes % 2 == 0, "can only generate even sizes: {n_bytes}");
420
        // 6 bytes per Ligature (header, including offset)
421
        let n_glyphs = (n_bytes - 6) / 2;
422
        let components = (first..=first + n_glyphs).map(GlyphId16::new).collect();
423
        super::super::Ligature::new(GlyphId16::new(first), components)
424
    }
425
    fn make_2048_bytes_of_ligature() -> super::super::LigatureSet {
426
        // 4 bytes for header
427
        // 2048 - 4 = 2044
428
        // 2044 / 2 = 1022
429
        let lig1 = make_lig_table(1022, 1);
430
        let lig2 = make_lig_table(1022, 3);
431
        super::super::LigatureSet::new(vec![lig1, lig2])
432
    }
433
434
    #[test]
435
    fn who_tests_the_testers1() {
436
        for size in [6, 12, 144, 2046, u16::MAX - 1] {
437
            let table = make_lig_table(size, 1);
438
            let bytes = crate::dump_table(&table).unwrap();
439
            assert_eq!(bytes.len(), size as usize);
440
        }
441
    }
442
443
    #[test]
444
    fn splitting_ligature_subs() {
445
        let mut splitter = TableSplitter::<LigatureSubstFormat1>::new();
446
        let ligset = make_2048_bytes_of_ligature();
447
        for gid in 0u16..31 {
448
            // in real packing these would be deduplicated but we can't now that here
449
            splitter.add(GlyphId16::new(gid), ligset.clone());
450
        }
451
452
        // 31 * 2048 < u16::MAX
453
        assert_eq!(splitter.clone().finish().len(), 1);
454
        splitter.add(GlyphId16::new(32), ligset);
455
        // 32 * 2048 < u16::MAX
456
        assert_eq!(splitter.finish().len(), 2)
457
    }
458
}