Skip to main content

write_fonts/tables/
gvar.rs

1//! The gvar table
2
3include!("../../generated/generated_gvar.rs");
4
5use std::collections::HashMap;
6
7use indexmap::IndexMap;
8
9use crate::{
10    collections::HasLen,
11    tables::variations::{Deltas, TupleVariationStoreInputError},
12    OffsetMarker,
13};
14
15use super::variations::{
16    compute_shared_points, compute_tuple_variation_count, compute_tuple_variation_data_offset,
17    PackedDeltas, PackedPointNumbers, Tuple, TupleVariationCount, TupleVariationHeader,
18};
19
20pub use super::variations::Tent;
21
22pub mod iup;
23
24/// Variation data for a single glyph, before it is compiled
25#[derive(Clone, Debug)]
26pub struct GlyphVariations {
27    gid: GlyphId,
28    variations: Vec<GlyphDeltas>,
29}
30
31/// Glyph deltas for one point in the design space.
32pub type GlyphDeltas = Deltas<GlyphDelta>;
33
34/// A delta for a single value in a glyph.
35///
36/// This includes a flag indicating whether or not this delta is required (i.e
37/// it cannot be interpolated from neighbouring deltas and coordinates).
38/// This is only relevant for simple glyphs; interpolatable points may be omitted
39/// in the final binary when doing so saves space.
40/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/gvar#inferred-deltas-for-un-referenced-point-numbers>
41/// for more information.
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44pub struct GlyphDelta {
45    pub x: i16,
46    pub y: i16,
47    /// This delta must be included, i.e. cannot be interpolated
48    pub required: bool,
49}
50
51impl Gvar {
52    /// Construct a gvar table from a vector of per-glyph variations and the axis count.
53    ///
54    /// Variations must be present for each glyph, but may be empty.
55    /// For non-empty variations, the axis count must be equal to the provided
56    /// axis count, as specified by the 'fvar' table.
57    pub fn new(
58        mut variations: Vec<GlyphVariations>,
59        axis_count: u16,
60    ) -> Result<Self, TupleVariationStoreInputError<GlyphId>> {
61        fn compute_shared_peak_tuples(glyphs: &[GlyphVariations]) -> Vec<Tuple> {
62            const MAX_SHARED_TUPLES: usize = 4095;
63            let mut peak_tuple_counts = IndexMap::new();
64            for glyph in glyphs {
65                glyph.count_peak_tuples(&mut peak_tuple_counts);
66            }
67            let mut to_share = peak_tuple_counts
68                .into_iter()
69                .filter(|(_, n)| *n > 1)
70                .collect::<Vec<_>>();
71            // prefer IndexMap::sort_by_key over HashMap::sort_unstable_by_key so the
72            // order of the shared tuples with equal count doesn't change randomly
73            // but is kept stable to ensure builds are deterministic.
74            to_share.sort_by_key(|(_, n)| std::cmp::Reverse(*n));
75            to_share.truncate(MAX_SHARED_TUPLES);
76            to_share.into_iter().map(|(t, _)| t.to_owned()).collect()
77        }
78
79        for var in &variations {
80            var.validate()?;
81        }
82
83        if let Some(bad_var) = variations
84            .iter()
85            .find(|var| var.axis_count().is_some() && var.axis_count().unwrap() != axis_count)
86        {
87            return Err(TupleVariationStoreInputError::UnexpectedAxisCount {
88                index: bad_var.gid,
89                expected: axis_count,
90                actual: bad_var.axis_count().unwrap(),
91            });
92        }
93
94        let shared = compute_shared_peak_tuples(&variations);
95        let shared_idx_map = shared
96            .iter()
97            .enumerate()
98            .map(|(i, x)| (x, i as u16))
99            .collect();
100        variations.sort_unstable_by_key(|g| g.gid);
101        let glyphs = variations
102            .into_iter()
103            .map(|raw_g| raw_g.build(&shared_idx_map))
104            .collect();
105
106        Ok(Gvar {
107            axis_count,
108            shared_tuples: SharedTuples::new(shared).into(),
109            glyph_variation_data_offsets: glyphs,
110        })
111    }
112
113    fn compute_flags(&self) -> GvarFlags {
114        let max_offset = self
115            .glyph_variation_data_offsets
116            .iter()
117            .fold(0, |acc, val| acc + val.length + val.length % 2);
118
119        if max_offset / 2 <= (u16::MAX as u32) {
120            GvarFlags::default()
121        } else {
122            GvarFlags::LONG_OFFSETS
123        }
124    }
125
126    fn compute_glyph_count(&self) -> u16 {
127        self.glyph_variation_data_offsets.len().try_into().unwrap()
128    }
129
130    fn compute_shared_tuples_offset(&self) -> u32 {
131        const BASE_OFFSET: usize = MajorMinor::RAW_BYTE_LEN
132            + u16::RAW_BYTE_LEN // axis count
133            + u16::RAW_BYTE_LEN // shared tuples count
134            + Offset32::RAW_BYTE_LEN
135            + u16::RAW_BYTE_LEN + u16::RAW_BYTE_LEN // glyph count, flags
136            + u32::RAW_BYTE_LEN; // glyph_variation_data_array_offset
137
138        let bytes_per_offset = if self.compute_flags() == GvarFlags::LONG_OFFSETS {
139            u32::RAW_BYTE_LEN
140        } else {
141            u16::RAW_BYTE_LEN
142        };
143
144        let offsets_len = (self.glyph_variation_data_offsets.len() + 1) * bytes_per_offset;
145
146        (BASE_OFFSET + offsets_len).try_into().unwrap()
147    }
148
149    fn compute_data_array_offset(&self) -> u32 {
150        let shared_tuples_len: u32 =
151            (array_len(&self.shared_tuples) * self.axis_count as usize * 2)
152                .try_into()
153                .unwrap();
154        self.compute_shared_tuples_offset() + shared_tuples_len
155    }
156
157    fn compile_variation_data(&self) -> GlyphDataWriter<'_> {
158        GlyphDataWriter {
159            long_offsets: self.compute_flags() == GvarFlags::LONG_OFFSETS,
160            shared_tuples: &self.shared_tuples,
161            data: &self.glyph_variation_data_offsets,
162        }
163    }
164}
165
166impl GlyphVariations {
167    /// Construct a new set of variation deltas for a glyph.
168    pub fn new(gid: GlyphId, variations: Vec<GlyphDeltas>) -> Self {
169        Self { gid, variations }
170    }
171
172    /// called when we build gvar, so we only return errors in one place
173    fn validate(&self) -> Result<(), TupleVariationStoreInputError<GlyphId>> {
174        let (axis_count, delta_len) = self
175            .variations
176            .first()
177            .map(|var| (var.peak_tuple.len(), var.deltas.len()))
178            .unwrap_or_default();
179        for var in &self.variations {
180            if var.peak_tuple.len() != axis_count {
181                return Err(TupleVariationStoreInputError::InconsistentAxisCount(
182                    self.gid,
183                ));
184            }
185            if let Some((start, end)) = var.intermediate_region.as_ref() {
186                if start.len() != axis_count || end.len() != axis_count {
187                    return Err(TupleVariationStoreInputError::InconsistentTupleLengths(
188                        self.gid,
189                    ));
190                }
191            }
192            if var.deltas.len() != delta_len {
193                return Err(TupleVariationStoreInputError::InconsistentDeltaLength(
194                    self.gid,
195                ));
196            }
197        }
198        Ok(())
199    }
200
201    /// Will be `None` if there are no variations for this glyph
202    pub fn axis_count(&self) -> Option<u16> {
203        self.variations.first().map(|var| var.peak_tuple.len())
204    }
205
206    fn count_peak_tuples<'a>(&'a self, counter: &mut IndexMap<&'a Tuple, usize>) {
207        for tuple in &self.variations {
208            *counter.entry(&tuple.peak_tuple).or_default() += 1;
209        }
210    }
211
212    fn build(self, shared_tuple_map: &HashMap<&Tuple, u16>) -> GlyphVariationData {
213        let shared_points = compute_shared_points(&self.variations);
214
215        let (tuple_headers, tuple_data): (Vec<_>, Vec<_>) = self
216            .variations
217            .into_iter()
218            .map(|tup| tup.build(shared_tuple_map, shared_points.as_ref()))
219            .unzip();
220
221        let mut temp = GlyphVariationData {
222            tuple_variation_headers: tuple_headers,
223            shared_point_numbers: shared_points,
224            per_tuple_data: tuple_data,
225            length: 0,
226        };
227
228        temp.length = temp.compute_size();
229        temp
230    }
231}
232
233impl GlyphDelta {
234    /// Create a new delta value.
235    pub fn new(x: i16, y: i16, required: bool) -> Self {
236        Self { x, y, required }
237    }
238
239    /// Create a new delta value that must be encoded (cannot be interpolated)
240    pub fn required(x: i16, y: i16) -> Self {
241        Self::new(x, y, true)
242    }
243
244    /// Create a new delta value that may be omitted (can be interpolated)
245    pub fn optional(x: i16, y: i16) -> Self {
246        Self::new(x, y, false)
247    }
248}
249
250impl GlyphDeltas {
251    /// Create a new set of deltas.
252    ///
253    /// A None delta means do not explicitly encode, typically because IUP suggests
254    /// it isn't required.
255    pub fn new(tents: Vec<Tent>, deltas: Vec<GlyphDelta>) -> Self {
256        let peak_tuple = Tuple::new(tents.iter().map(Tent::peak).collect());
257
258        // File size optimisation: if all the intermediates can be derived from
259        // the relevant peak values, don't serialize them.
260        // https://github.com/fonttools/fonttools/blob/b467579c/Lib/fontTools/ttLib/tables/TupleVariation.py#L184-L193
261        let intermediate_region = if tents.iter().any(Tent::requires_intermediate) {
262            Some(tents.iter().map(Tent::bounds).unzip())
263        } else {
264            None
265        };
266
267        // at construction time we build both iup optimized & not versions
268        // of ourselves, to determine what representation is most efficient;
269        // the caller will look at the generated packed points to decide which
270        // set should be shared.
271        let best_point_packing = Self::pick_best_point_number_repr(&deltas);
272        GlyphDeltas {
273            peak_tuple,
274            intermediate_region,
275            deltas,
276            best_point_packing,
277        }
278    }
279
280    // this is a type method just to expose it for testing, we call it before
281    // we finish instantiating self.
282    //
283    // we do a lot of duplicate work here with creating & throwing away
284    // buffers, and that can be improved at the cost of a bit more complexity
285    // <https://github.com/googlefonts/fontations/issues/635>
286    fn pick_best_point_number_repr(deltas: &[GlyphDelta]) -> PackedPointNumbers {
287        if deltas.iter().all(|d| d.required) {
288            return PackedPointNumbers::All;
289        }
290
291        let dense = Self::build_non_sparse_data(deltas);
292        let sparse = Self::build_sparse_data(deltas);
293        let dense_size = dense.compute_size();
294        let sparse_size = sparse.compute_size();
295        log::trace!("dense {dense_size}, sparse {sparse_size}");
296        if sparse_size < dense_size {
297            sparse.private_point_numbers.unwrap()
298        } else {
299            PackedPointNumbers::All
300        }
301    }
302
303    fn build_non_sparse_data(deltas: &[GlyphDelta]) -> GlyphTupleVariationData {
304        let (x_deltas, y_deltas) = deltas
305            .iter()
306            .map(|delta| (delta.x as i32, delta.y as i32))
307            .unzip();
308        GlyphTupleVariationData {
309            private_point_numbers: Some(PackedPointNumbers::All),
310            x_deltas: PackedDeltas::new(x_deltas),
311            y_deltas: PackedDeltas::new(y_deltas),
312        }
313    }
314
315    fn build_sparse_data(deltas: &[GlyphDelta]) -> GlyphTupleVariationData {
316        let (x_deltas, y_deltas) = deltas
317            .iter()
318            .filter_map(|delta| delta.required.then_some((delta.x as i32, delta.y as i32)))
319            .unzip();
320        let point_numbers = deltas
321            .iter()
322            .enumerate()
323            .filter_map(|(i, delta)| delta.required.then_some(i as u16))
324            .collect();
325        GlyphTupleVariationData {
326            private_point_numbers: Some(PackedPointNumbers::Some(point_numbers)),
327            x_deltas: PackedDeltas::new(x_deltas),
328            y_deltas: PackedDeltas::new(y_deltas),
329        }
330    }
331
332    // shared points is just "whatever points, if any, are shared." We are
333    // responsible for seeing if these are actually our points, in which case
334    // we are using shared points.
335    fn build(
336        self,
337        shared_tuple_map: &HashMap<&Tuple, u16>,
338        shared_points: Option<&PackedPointNumbers>,
339    ) -> (TupleVariationHeader, GlyphTupleVariationData) {
340        let GlyphDeltas {
341            peak_tuple,
342            intermediate_region,
343            deltas,
344            best_point_packing: point_numbers,
345        } = self;
346
347        let (idx, peak_tuple) = match shared_tuple_map.get(&peak_tuple) {
348            Some(idx) => (Some(*idx), None),
349            None => (None, Some(peak_tuple)),
350        };
351
352        let has_private_points = Some(&point_numbers) != shared_points;
353        let (x_deltas, y_deltas) = match &point_numbers {
354            PackedPointNumbers::All => deltas.iter().map(|d| (d.x as i32, d.y as i32)).unzip(),
355            PackedPointNumbers::Some(pts) => pts
356                .iter()
357                .map(|idx| {
358                    let delta = deltas[*idx as usize];
359                    (delta.x as i32, delta.y as i32)
360                })
361                .unzip(),
362        };
363
364        let data = GlyphTupleVariationData {
365            private_point_numbers: has_private_points.then_some(point_numbers),
366            x_deltas: PackedDeltas::new(x_deltas),
367            y_deltas: PackedDeltas::new(y_deltas),
368        };
369        let data_size = data.compute_size();
370
371        let header = TupleVariationHeader::new(
372            data_size,
373            idx,
374            peak_tuple,
375            intermediate_region,
376            has_private_points,
377        );
378
379        (header, data)
380    }
381}
382
383/// The serializable representation of a glyph's variation data
384#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
385#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
386pub struct GlyphVariationData {
387    tuple_variation_headers: Vec<TupleVariationHeader>,
388    // optional; present if multiple variations have the same point numbers
389    shared_point_numbers: Option<PackedPointNumbers>,
390    per_tuple_data: Vec<GlyphTupleVariationData>,
391    /// calculated length required to store this data
392    ///
393    /// we compute this once up front because we need to know it in a bunch
394    /// of different places (u32 because offsets are max u32)
395    length: u32,
396}
397
398/// The serializable representation of a single glyph tuple variation data
399#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
400#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
401struct GlyphTupleVariationData {
402    // this is possibly shared, if multiple are identical for a given glyph
403    private_point_numbers: Option<PackedPointNumbers>,
404    x_deltas: PackedDeltas,
405    y_deltas: PackedDeltas,
406}
407
408impl GlyphTupleVariationData {
409    fn compute_size(&self) -> u16 {
410        self.private_point_numbers
411            .as_ref()
412            .map(PackedPointNumbers::compute_size)
413            .unwrap_or_default()
414            .checked_add(self.x_deltas.compute_size())
415            .unwrap()
416            .checked_add(self.y_deltas.compute_size())
417            .unwrap()
418    }
419}
420
421impl FontWrite for GlyphTupleVariationData {
422    fn write_into(&self, writer: &mut TableWriter) {
423        self.private_point_numbers.write_into(writer);
424        self.x_deltas.write_into(writer);
425        self.y_deltas.write_into(writer);
426    }
427}
428
429struct GlyphDataWriter<'a> {
430    long_offsets: bool,
431    shared_tuples: &'a SharedTuples,
432    data: &'a [GlyphVariationData],
433}
434
435impl FontWrite for GlyphDataWriter<'_> {
436    fn write_into(&self, writer: &mut TableWriter) {
437        if self.long_offsets {
438            let mut last = 0u32;
439            last.write_into(writer);
440
441            // write all the offsets
442            for glyph in self.data {
443                last += glyph.compute_size();
444                last.write_into(writer);
445            }
446        } else {
447            // for short offsets we divide the real offset by two; this means
448            // we will have to add padding if necessary
449            let mut last = 0u16;
450            last.write_into(writer);
451
452            // write all the offsets
453            for glyph in self.data {
454                let size = glyph.compute_size();
455                // ensure we're always rounding up to the next 2
456                let short_size = (size / 2) + size % 2;
457                last += short_size as u16;
458                last.write_into(writer);
459            }
460        }
461        // then write the shared tuples (should come here according to the spec)
462        // https://learn.microsoft.com/en-us/typography/opentype/spec/gvar#shared-tuples-array
463        // > The shared tuples array follows the GlyphVariationData offsets array at the end of the 'gvar' header.
464        self.shared_tuples.write_into(writer);
465        // then write the actual data
466        for glyph in self.data {
467            if !glyph.is_empty() {
468                glyph.write_into(writer);
469                if !self.long_offsets {
470                    writer.pad_to_2byte_aligned();
471                }
472            }
473        }
474    }
475}
476
477impl GlyphVariationData {
478    fn compute_tuple_variation_count(&self) -> TupleVariationCount {
479        compute_tuple_variation_count(
480            self.tuple_variation_headers.len(),
481            self.shared_point_numbers.is_some(),
482        )
483    }
484
485    fn is_empty(&self) -> bool {
486        self.tuple_variation_headers.is_empty()
487    }
488
489    fn compute_data_offset(&self) -> u16 {
490        compute_tuple_variation_data_offset(
491            &self.tuple_variation_headers,
492            TupleVariationCount::RAW_BYTE_LEN + u16::RAW_BYTE_LEN,
493        )
494    }
495
496    fn compute_size(&self) -> u32 {
497        if self.is_empty() {
498            return 0;
499        }
500
501        let data_start = self.compute_data_offset() as u32;
502        let shared_point_len = self
503            .shared_point_numbers
504            .as_ref()
505            .map(|pts| pts.compute_size())
506            .unwrap_or_default() as u32;
507        let tuple_data_len = self
508            .per_tuple_data
509            .iter()
510            .fold(0u32, |acc, tup| acc + tup.compute_size() as u32);
511        data_start + shared_point_len + tuple_data_len
512    }
513}
514
515impl Extend<F2Dot14> for Tuple {
516    fn extend<T: IntoIterator<Item = F2Dot14>>(&mut self, iter: T) {
517        self.values.extend(iter);
518    }
519}
520
521impl Validate for GlyphVariationData {
522    fn validate_impl(&self, ctx: &mut ValidationCtx) {
523        const MAX_TUPLE_VARIATIONS: usize = 4095;
524        if !(0..=MAX_TUPLE_VARIATIONS).contains(&self.tuple_variation_headers.len()) {
525            ctx.in_field("tuple_variation_headers", |ctx| {
526                ctx.report("expected 0-4095 tuple variation tables")
527            })
528        }
529    }
530}
531
532impl FontWrite for GlyphVariationData {
533    fn write_into(&self, writer: &mut TableWriter) {
534        self.compute_tuple_variation_count().write_into(writer);
535        self.compute_data_offset().write_into(writer);
536        self.tuple_variation_headers.write_into(writer);
537        self.shared_point_numbers.write_into(writer);
538        self.per_tuple_data.write_into(writer);
539    }
540}
541
542impl HasLen for SharedTuples {
543    fn len(&self) -> usize {
544        self.tuples.len()
545    }
546}
547
548impl FontWrite for TupleVariationCount {
549    fn write_into(&self, writer: &mut TableWriter) {
550        self.bits().write_into(writer)
551    }
552}
553#[cfg(test)]
554mod tests {
555    use super::*;
556
557    /// Helper function to concisely state test cases without intermediates.
558    fn peaks(peaks: Vec<F2Dot14>) -> Vec<Tent> {
559        peaks
560            .into_iter()
561            .map(|peak| Tent::new(peak, None))
562            .collect()
563    }
564
565    #[test]
566    fn gvar_smoke_test() {
567        let _ = env_logger::builder().is_test(true).try_init();
568        let table = Gvar::new(
569            vec![
570                GlyphVariations::new(GlyphId::new(0), vec![]),
571                GlyphVariations::new(
572                    GlyphId::new(1),
573                    vec![GlyphDeltas::new(
574                        peaks(vec![F2Dot14::from_f32(1.0), F2Dot14::from_f32(1.0)]),
575                        vec![
576                            GlyphDelta::required(30, 31),
577                            GlyphDelta::required(40, 41),
578                            GlyphDelta::required(-50, -49),
579                            GlyphDelta::required(101, 102),
580                            GlyphDelta::required(10, 11),
581                        ],
582                    )],
583                ),
584                GlyphVariations::new(
585                    GlyphId::new(2),
586                    vec![
587                        GlyphDeltas::new(
588                            peaks(vec![F2Dot14::from_f32(1.0), F2Dot14::from_f32(1.0)]),
589                            vec![
590                                GlyphDelta::required(11, -20),
591                                GlyphDelta::required(69, -41),
592                                GlyphDelta::required(-69, 49),
593                                GlyphDelta::required(168, 101),
594                                GlyphDelta::required(1, 2),
595                            ],
596                        ),
597                        GlyphDeltas::new(
598                            peaks(vec![F2Dot14::from_f32(0.8), F2Dot14::from_f32(1.0)]),
599                            vec![
600                                GlyphDelta::required(3, -200),
601                                GlyphDelta::required(4, -500),
602                                GlyphDelta::required(5, -800),
603                                GlyphDelta::required(6, -1200),
604                                GlyphDelta::required(7, -1500),
605                            ],
606                        ),
607                    ],
608                ),
609            ],
610            2,
611        )
612        .unwrap();
613
614        let g2 = &table.glyph_variation_data_offsets[1];
615        let computed = g2.compute_size();
616        let actual = crate::dump_table(g2).unwrap().len();
617        assert_eq!(computed as usize, actual);
618
619        let bytes = crate::dump_table(&table).unwrap();
620        let gvar = read_fonts::tables::gvar::Gvar::read(FontData::new(&bytes)).unwrap();
621        assert_eq!(gvar.version(), MajorMinor::VERSION_1_0);
622        assert_eq!(gvar.shared_tuple_count(), 1);
623        assert_eq!(gvar.glyph_count(), 3);
624        // Check offsets, the shared_tuples_offset should point to just after the table's header
625        assert_eq!(gvar.shared_tuples_offset(), Offset32::new(28));
626        assert_eq!(gvar.glyph_variation_data_array_offset(), 32);
627
628        let g1 = gvar.glyph_variation_data(GlyphId::new(1)).unwrap().unwrap();
629        let g1tup = g1.tuples().collect::<Vec<_>>();
630        assert_eq!(g1tup.len(), 1);
631
632        let (x, y): (Vec<_>, Vec<_>) = g1tup[0].deltas().map(|d| (d.x_delta, d.y_delta)).unzip();
633        assert_eq!(x, vec![30, 40, -50, 101, 10]);
634        assert_eq!(y, vec![31, 41, -49, 102, 11]);
635
636        let g2 = gvar.glyph_variation_data(GlyphId::new(2)).unwrap().unwrap();
637        let g2tup = g2.tuples().collect::<Vec<_>>();
638        assert_eq!(g2tup.len(), 2);
639
640        let (x, y): (Vec<_>, Vec<_>) = g2tup[0].deltas().map(|d| (d.x_delta, d.y_delta)).unzip();
641        assert_eq!(x, vec![11, 69, -69, 168, 1]);
642        assert_eq!(y, vec![-20, -41, 49, 101, 2]);
643
644        let (x, y): (Vec<_>, Vec<_>) = g2tup[1].deltas().map(|d| (d.x_delta, d.y_delta)).unzip();
645
646        assert_eq!(x, vec![3, 4, 5, 6, 7]);
647        assert_eq!(y, vec![-200, -500, -800, -1200, -1500]);
648    }
649
650    #[test]
651    fn use_iup_when_appropriate() {
652        // IFF iup provides space savings, we should prefer it.
653        let _ = env_logger::builder().is_test(true).try_init();
654        let gid = GlyphId::new(0);
655        let table = Gvar::new(
656            vec![GlyphVariations::new(
657                gid,
658                vec![GlyphDeltas::new(
659                    peaks(vec![F2Dot14::from_f32(1.0), F2Dot14::from_f32(1.0)]),
660                    vec![
661                        GlyphDelta::required(30, 31),
662                        GlyphDelta::optional(30, 31),
663                        GlyphDelta::optional(30, 31),
664                        GlyphDelta::required(101, 102),
665                        GlyphDelta::required(10, 11),
666                        GlyphDelta::optional(10, 11),
667                    ],
668                )],
669            )],
670            2,
671        )
672        .unwrap();
673
674        let bytes = crate::dump_table(&table).unwrap();
675        let gvar = read_fonts::tables::gvar::Gvar::read(FontData::new(&bytes)).unwrap();
676        assert_eq!(gvar.version(), MajorMinor::VERSION_1_0);
677        assert_eq!(gvar.shared_tuple_count(), 0);
678        assert_eq!(gvar.glyph_count(), 1);
679
680        let g1 = gvar.glyph_variation_data(gid).unwrap().unwrap();
681        let g1tup = g1.tuples().collect::<Vec<_>>();
682        assert_eq!(g1tup.len(), 1);
683        let tuple_variation = &g1tup[0];
684
685        assert!(!tuple_variation.has_deltas_for_all_points());
686        assert_eq!(
687            vec![0, 3, 4],
688            tuple_variation.point_numbers().collect::<Vec<_>>()
689        );
690
691        let points: Vec<_> = tuple_variation
692            .deltas()
693            .map(|d| (d.x_delta, d.y_delta))
694            .collect();
695        assert_eq!(points, vec![(30, 31), (101, 102), (10, 11)]);
696    }
697
698    #[test]
699    fn disregard_iup_when_appropriate() {
700        // if the cost of encoding the list of points is greater than the savings
701        // from omitting some deltas, we should just encode explicit zeros
702        let points = vec![
703            GlyphDelta::required(1, 2),
704            GlyphDelta::required(3, 4),
705            GlyphDelta::required(5, 6),
706            GlyphDelta::optional(5, 6),
707            GlyphDelta::required(7, 8),
708        ];
709        let gid = GlyphId::new(0);
710        let table = Gvar::new(
711            vec![GlyphVariations::new(
712                gid,
713                vec![GlyphDeltas::new(
714                    peaks(vec![F2Dot14::from_f32(1.0), F2Dot14::from_f32(1.0)]),
715                    points,
716                )],
717            )],
718            2,
719        )
720        .unwrap();
721        let bytes = crate::dump_table(&table).unwrap();
722        let gvar = read_fonts::tables::gvar::Gvar::read(FontData::new(&bytes)).unwrap();
723        assert_eq!(gvar.version(), MajorMinor::VERSION_1_0);
724        assert_eq!(gvar.shared_tuple_count(), 0);
725        assert_eq!(gvar.glyph_count(), 1);
726
727        let g1 = gvar.glyph_variation_data(gid).unwrap().unwrap();
728        let g1tup = g1.tuples().collect::<Vec<_>>();
729        assert_eq!(g1tup.len(), 1);
730        let tuple_variation = &g1tup[0];
731
732        assert!(tuple_variation.has_deltas_for_all_points());
733        let points: Vec<_> = tuple_variation
734            .deltas()
735            .map(|d| (d.x_delta, d.y_delta))
736            .collect();
737        assert_eq!(points, vec![(1, 2), (3, 4), (5, 6), (5, 6), (7, 8)]);
738    }
739
740    #[test]
741    fn share_points() {
742        let _ = env_logger::builder().is_test(true).try_init();
743        let variations = GlyphVariations::new(
744            GlyphId::new(0),
745            vec![
746                GlyphDeltas::new(
747                    peaks(vec![F2Dot14::from_f32(1.0), F2Dot14::from_f32(1.0)]),
748                    vec![
749                        GlyphDelta::required(1, 2),
750                        GlyphDelta::optional(3, 4),
751                        GlyphDelta::required(5, 6),
752                        GlyphDelta::optional(5, 6),
753                        GlyphDelta::required(7, 8),
754                        GlyphDelta::optional(7, 8),
755                    ],
756                ),
757                GlyphDeltas::new(
758                    peaks(vec![F2Dot14::from_f32(-1.0), F2Dot14::from_f32(-1.0)]),
759                    vec![
760                        GlyphDelta::required(10, 20),
761                        GlyphDelta::optional(30, 40),
762                        GlyphDelta::required(50, 60),
763                        GlyphDelta::optional(50, 60),
764                        GlyphDelta::required(70, 80),
765                        GlyphDelta::optional(70, 80),
766                    ],
767                ),
768            ],
769        );
770
771        assert_eq!(
772            compute_shared_points(&variations.variations),
773            Some(PackedPointNumbers::Some(vec![0, 2, 4]))
774        )
775    }
776
777    #[test]
778    fn share_all_points() {
779        let variations = GlyphVariations::new(
780            GlyphId::new(0),
781            vec![
782                GlyphDeltas::new(
783                    peaks(vec![F2Dot14::from_f32(1.0), F2Dot14::from_f32(1.0)]),
784                    vec![
785                        GlyphDelta::required(1, 2),
786                        GlyphDelta::required(3, 4),
787                        GlyphDelta::required(5, 6),
788                    ],
789                ),
790                GlyphDeltas::new(
791                    peaks(vec![F2Dot14::from_f32(-1.0), F2Dot14::from_f32(-1.0)]),
792                    vec![
793                        GlyphDelta::required(2, 4),
794                        GlyphDelta::required(6, 8),
795                        GlyphDelta::required(7, 9),
796                    ],
797                ),
798            ],
799        );
800
801        let shared_tups = HashMap::new();
802        let built = variations.build(&shared_tups);
803        assert_eq!(built.shared_point_numbers, Some(PackedPointNumbers::All))
804    }
805
806    // three tuples with three different packedpoint representations means
807    // that we should have no shared points
808    #[test]
809    fn dont_share_unique_points() {
810        let variations = GlyphVariations::new(
811            GlyphId::new(0),
812            vec![
813                GlyphDeltas::new(
814                    peaks(vec![F2Dot14::from_f32(1.0), F2Dot14::from_f32(1.0)]),
815                    vec![
816                        GlyphDelta::required(1, 2),
817                        GlyphDelta::optional(3, 4),
818                        GlyphDelta::required(5, 6),
819                        GlyphDelta::optional(5, 6),
820                        GlyphDelta::required(7, 8),
821                        GlyphDelta::optional(7, 8),
822                    ],
823                ),
824                GlyphDeltas::new(
825                    peaks(vec![F2Dot14::from_f32(-1.0), F2Dot14::from_f32(-1.0)]),
826                    vec![
827                        GlyphDelta::required(10, 20),
828                        GlyphDelta::required(35, 40),
829                        GlyphDelta::required(50, 60),
830                        GlyphDelta::optional(50, 60),
831                        GlyphDelta::required(70, 80),
832                        GlyphDelta::optional(70, 80),
833                    ],
834                ),
835                GlyphDeltas::new(
836                    peaks(vec![F2Dot14::from_f32(0.5), F2Dot14::from_f32(1.0)]),
837                    vec![
838                        GlyphDelta::required(1, 2),
839                        GlyphDelta::optional(3, 4),
840                        GlyphDelta::required(5, 6),
841                        GlyphDelta::optional(5, 6),
842                        GlyphDelta::optional(7, 8),
843                        GlyphDelta::optional(7, 8),
844                    ],
845                ),
846            ],
847        );
848
849        let shared_tups = HashMap::new();
850        let built = variations.build(&shared_tups);
851        assert!(built.shared_point_numbers.is_none());
852    }
853
854    // comparing our behaviour against what we know fonttools does.
855    #[test]
856    #[allow(non_snake_case)]
857    fn oswald_Lcaron() {
858        let _ = env_logger::builder().is_test(true).try_init();
859        // in this glyph, it is more efficient to encode all points for the first
860        // tuple, but sparse points for the second (the single y delta in the
861        // second tuple means you can't encode the y-deltas as 'all zero')
862        let variations = GlyphVariations::new(
863            GlyphId::new(0),
864            vec![
865                GlyphDeltas::new(
866                    peaks(vec![F2Dot14::from_f32(-1.0), F2Dot14::from_f32(-1.0)]),
867                    vec![
868                        GlyphDelta::optional(0, 0),
869                        GlyphDelta::required(35, 0),
870                        GlyphDelta::optional(0, 0),
871                        GlyphDelta::required(-24, 0),
872                        GlyphDelta::optional(0, 0),
873                        GlyphDelta::optional(0, 0),
874                    ],
875                ),
876                GlyphDeltas::new(
877                    peaks(vec![F2Dot14::from_f32(1.0), F2Dot14::from_f32(1.0)]),
878                    vec![
879                        GlyphDelta::optional(0, 0),
880                        GlyphDelta::required(26, 15),
881                        GlyphDelta::optional(0, 0),
882                        GlyphDelta::required(46, 0),
883                        GlyphDelta::optional(0, 0),
884                        GlyphDelta::optional(0, 0),
885                    ],
886                ),
887            ],
888        );
889        assert!(compute_shared_points(&variations.variations).is_none());
890        let tups = HashMap::new();
891        let built = variations.build(&tups);
892        assert_eq!(
893            built.per_tuple_data[0].private_point_numbers,
894            Some(PackedPointNumbers::All)
895        );
896        assert_eq!(
897            built.per_tuple_data[1].private_point_numbers,
898            Some(PackedPointNumbers::Some(vec![1, 3]))
899        );
900    }
901
902    #[test]
903    fn compute_shared_points_is_deterministic() {
904        // The deltas for glyph "etatonos.sc.ss06" in GoogleSans-VF are such that the
905        // TupleVariationStore's shared set of point numbers could potentionally be
906        // computed as either PackedPointNumbers::All or PackedPointNumbers::Some([1, 3])
907        // without affecting the size (or correctness) of the serialized data.
908        // However we want to ensure that the result is deterministic, and doesn't
909        // depend on e.g. HashMap random iteration order.
910        // https://github.com/googlefonts/fontc/issues/647
911        let _ = env_logger::builder().is_test(true).try_init();
912        let variations = GlyphVariations::new(
913            GlyphId::NOTDEF,
914            vec![
915                GlyphDeltas::new(
916                    peaks(vec![
917                        F2Dot14::from_f32(-1.0),
918                        F2Dot14::from_f32(0.0),
919                        F2Dot14::from_f32(0.0),
920                    ]),
921                    vec![
922                        GlyphDelta::optional(0, 0),
923                        GlyphDelta::required(-17, -4),
924                        GlyphDelta::optional(0, 0),
925                        GlyphDelta::required(-28, 0),
926                        GlyphDelta::optional(0, 0),
927                        GlyphDelta::optional(0, 0),
928                    ],
929                ),
930                GlyphDeltas::new(
931                    peaks(vec![
932                        F2Dot14::from_f32(0.0),
933                        F2Dot14::from_f32(1.0),
934                        F2Dot14::from_f32(0.0),
935                    ]),
936                    vec![
937                        GlyphDelta::optional(0, 0),
938                        GlyphDelta::required(0, -10),
939                        GlyphDelta::optional(0, 0),
940                        GlyphDelta::required(34, 0),
941                        GlyphDelta::optional(0, 0),
942                        GlyphDelta::optional(0, 0),
943                    ],
944                ),
945                GlyphDeltas::new(
946                    peaks(vec![
947                        F2Dot14::from_f32(0.0),
948                        F2Dot14::from_f32(0.0),
949                        F2Dot14::from_f32(-1.0),
950                    ]),
951                    vec![
952                        GlyphDelta::required(0, 0),
953                        GlyphDelta::optional(0, 0),
954                        GlyphDelta::optional(0, 0),
955                        GlyphDelta::optional(0, 0),
956                        GlyphDelta::optional(0, 0),
957                        GlyphDelta::optional(0, 0),
958                    ],
959                ),
960                GlyphDeltas::new(
961                    peaks(vec![
962                        F2Dot14::from_f32(0.0),
963                        F2Dot14::from_f32(0.0),
964                        F2Dot14::from_f32(1.0),
965                    ]),
966                    vec![
967                        GlyphDelta::required(0, 0),
968                        GlyphDelta::optional(0, 0),
969                        GlyphDelta::optional(0, 0),
970                        GlyphDelta::optional(0, 0),
971                        GlyphDelta::optional(0, 0),
972                        GlyphDelta::optional(0, 0),
973                    ],
974                ),
975                GlyphDeltas::new(
976                    peaks(vec![
977                        F2Dot14::from_f32(-1.0),
978                        F2Dot14::from_f32(1.0),
979                        F2Dot14::from_f32(0.0),
980                    ]),
981                    vec![
982                        GlyphDelta::optional(0, 0),
983                        GlyphDelta::required(-1, 10),
984                        GlyphDelta::optional(0, 0),
985                        GlyphDelta::required(-9, 0),
986                        GlyphDelta::optional(0, 0),
987                        GlyphDelta::optional(0, 0),
988                    ],
989                ),
990                GlyphDeltas::new(
991                    peaks(vec![
992                        F2Dot14::from_f32(-1.0),
993                        F2Dot14::from_f32(0.0),
994                        F2Dot14::from_f32(-1.0),
995                    ]),
996                    vec![
997                        GlyphDelta::required(0, 0),
998                        GlyphDelta::optional(0, 0),
999                        GlyphDelta::optional(0, 0),
1000                        GlyphDelta::optional(0, 0),
1001                        GlyphDelta::optional(0, 0),
1002                        GlyphDelta::optional(0, 0),
1003                    ],
1004                ),
1005                GlyphDeltas::new(
1006                    peaks(vec![
1007                        F2Dot14::from_f32(-1.0),
1008                        F2Dot14::from_f32(0.0),
1009                        F2Dot14::from_f32(1.0),
1010                    ]),
1011                    vec![
1012                        GlyphDelta::required(0, 0),
1013                        GlyphDelta::optional(0, 0),
1014                        GlyphDelta::optional(0, 0),
1015                        GlyphDelta::optional(0, 0),
1016                        GlyphDelta::optional(0, 0),
1017                        GlyphDelta::optional(0, 0),
1018                    ],
1019                ),
1020            ],
1021        );
1022
1023        assert_eq!(
1024            compute_shared_points(&variations.variations),
1025            // Also PackedPointNumbers::All would work, but Some([1, 3]) happens
1026            // to be the first one that fits the bill when iterating over the
1027            // tuple variations in the order they are listed for this glyph.
1028            Some(PackedPointNumbers::Some(vec![1, 3]))
1029        );
1030    }
1031
1032    // when using short offsets we store (real offset / 2), so all offsets must
1033    // be even, which means when we have an odd number of bytes we have to pad.
1034    fn make_31_bytes_of_variation_data() -> Vec<GlyphDeltas> {
1035        vec![
1036            GlyphDeltas::new(
1037                peaks(vec![F2Dot14::from_f32(-1.0), F2Dot14::from_f32(-1.0)]),
1038                vec![
1039                    GlyphDelta::optional(0, 0),
1040                    GlyphDelta::required(35, 0),
1041                    GlyphDelta::optional(0, 0),
1042                    GlyphDelta::required(-24, 0),
1043                    GlyphDelta::optional(0, 0),
1044                    GlyphDelta::optional(0, 0),
1045                ],
1046            ),
1047            GlyphDeltas::new(
1048                peaks(vec![F2Dot14::from_f32(1.0), F2Dot14::from_f32(1.0)]),
1049                vec![
1050                    GlyphDelta::optional(0, 0),
1051                    GlyphDelta::required(26, 15),
1052                    GlyphDelta::optional(0, 0),
1053                    GlyphDelta::required(46, 0),
1054                    GlyphDelta::optional(0, 0),
1055                    GlyphDelta::required(1, 0),
1056                ],
1057            ),
1058        ]
1059    }
1060
1061    // sanity checking my input data for two subsequent tests
1062    #[test]
1063    fn who_tests_the_testers() {
1064        let variations = GlyphVariations::new(GlyphId::NOTDEF, make_31_bytes_of_variation_data());
1065        let mut tupl_map = HashMap::new();
1066
1067        // without shared tuples this would be 39 bytes
1068        assert_eq!(variations.clone().build(&tupl_map).length, 39);
1069
1070        // to get our real size we need to mock up the shared tuples:
1071        tupl_map.insert(&variations.variations[0].peak_tuple, 1);
1072        tupl_map.insert(&variations.variations[1].peak_tuple, 2);
1073
1074        let built = variations.clone().build(&tupl_map);
1075        // we need an odd number to test impact of padding
1076        assert_eq!(built.length, 31);
1077    }
1078
1079    fn assert_test_offset_packing(n_glyphs: u16, should_be_short: bool) {
1080        let (offset_len, data_len, expected_flags) = if should_be_short {
1081            // when using short offset we need to pad data to ensure offset is even
1082            (u16::RAW_BYTE_LEN, 32, GvarFlags::empty())
1083        } else {
1084            (u32::RAW_BYTE_LEN, 31, GvarFlags::LONG_OFFSETS)
1085        };
1086
1087        let test_data = make_31_bytes_of_variation_data();
1088        let a_small_number_of_variations = (0..n_glyphs)
1089            .map(|i| GlyphVariations::new(GlyphId::from(i), test_data.clone()))
1090            .collect();
1091
1092        let gvar = Gvar::new(a_small_number_of_variations, 2).unwrap();
1093        assert_eq!(gvar.compute_flags(), expected_flags);
1094
1095        let writer = gvar.compile_variation_data();
1096        let mut sink = TableWriter::default();
1097        writer.write_into(&mut sink);
1098
1099        let bytes = sink.into_data().bytes;
1100        let expected_len = (n_glyphs + 1) as usize * offset_len // offsets
1101                             + 8 // shared tuples TODO remove magic number
1102                             + data_len * n_glyphs as usize; // rounded size of each glyph
1103        assert_eq!(bytes.len(), expected_len);
1104
1105        let dumped = crate::dump_table(&gvar).unwrap();
1106        let loaded = read_fonts::tables::gvar::Gvar::read(FontData::new(&dumped)).unwrap();
1107
1108        assert_eq!(loaded.glyph_count(), n_glyphs);
1109        assert_eq!(loaded.flags(), expected_flags);
1110        assert!(loaded
1111            .glyph_variation_data_offsets()
1112            .iter()
1113            .map(|off| off.unwrap().get())
1114            .enumerate()
1115            .all(|(i, off)| off as usize == i * data_len));
1116    }
1117
1118    #[test]
1119    fn prefer_short_offsets() {
1120        let _ = env_logger::builder().is_test(true).try_init();
1121        assert_test_offset_packing(5, true);
1122    }
1123
1124    #[test]
1125    fn use_long_offsets_when_necessary() {
1126        // 2**16 * 2 / (31 + 1 padding) (bytes per tuple) = 4096 should be the first
1127        // overflow
1128        let _ = env_logger::builder().is_test(true).try_init();
1129        assert_test_offset_packing(4095, true);
1130        assert_test_offset_packing(4096, false);
1131        assert_test_offset_packing(4097, false);
1132    }
1133
1134    #[test]
1135    fn shared_tuples_stable_order() {
1136        // Test that shared tuples are sorted stably and builds reproducible
1137        // https://github.com/googlefonts/fontc/issues/647
1138        let mut variations = Vec::new();
1139        for i in 0..2 {
1140            variations.push(GlyphVariations::new(
1141                GlyphId::new(i),
1142                vec![
1143                    GlyphDeltas::new(
1144                        peaks(vec![F2Dot14::from_f32(1.0)]),
1145                        vec![GlyphDelta::required(10, 20)],
1146                    ),
1147                    GlyphDeltas::new(
1148                        peaks(vec![F2Dot14::from_f32(-1.0)]),
1149                        vec![GlyphDelta::required(-10, -20)],
1150                    ),
1151                ],
1152            ))
1153        }
1154        for _ in 0..10 {
1155            let table = Gvar::new(variations.clone(), 1).unwrap();
1156            let bytes = crate::dump_table(&table).unwrap();
1157            let gvar = read_fonts::tables::gvar::Gvar::read(FontData::new(&bytes)).unwrap();
1158
1159            assert_eq!(gvar.shared_tuple_count(), 2);
1160            assert_eq!(
1161                gvar.shared_tuples()
1162                    .unwrap()
1163                    .tuples()
1164                    .iter()
1165                    .map(|t| t.unwrap().values.to_vec())
1166                    .collect::<Vec<_>>(),
1167                vec![vec![F2Dot14::from_f32(1.0)], vec![F2Dot14::from_f32(-1.0)]]
1168            );
1169        }
1170    }
1171
1172    #[test]
1173    fn unexpected_axis_count() {
1174        let variations = GlyphVariations::new(
1175            GlyphId::NOTDEF,
1176            vec![
1177                GlyphDeltas::new(
1178                    peaks(vec![F2Dot14::from_f32(1.0)]),
1179                    vec![GlyphDelta::required(1, 2)],
1180                ),
1181                GlyphDeltas::new(
1182                    peaks(vec![F2Dot14::from_f32(1.0)]),
1183                    vec![GlyphDelta::required(1, 2)],
1184                ),
1185            ],
1186        );
1187        let gvar = Gvar::new(vec![variations], 2);
1188        assert!(matches!(
1189            gvar,
1190            Err(TupleVariationStoreInputError::UnexpectedAxisCount {
1191                index: GlyphId::NOTDEF,
1192                expected: 2,
1193                actual: 1
1194            })
1195        ));
1196    }
1197
1198    #[test]
1199    fn empty_gvar_has_expected_axis_count() {
1200        let variations = GlyphVariations::new(GlyphId::NOTDEF, vec![]);
1201        let gvar = Gvar::new(vec![variations], 2).unwrap();
1202        assert_eq!(gvar.axis_count, 2);
1203    }
1204
1205    #[test]
1206    /// Test the logic for determining whether individual intermediates need to
1207    /// be serialised in the context of their peak coordinates.
1208    fn intermediates_only_when_explicit_needed() {
1209        let any_points = vec![]; // could be anything
1210
1211        // If an intermediate is not provided, one SHOULD NOT be serialised.
1212        let deltas = GlyphDeltas::new(
1213            vec![Tent::new(F2Dot14::from_f32(0.5), None)],
1214            any_points.clone(),
1215        );
1216        assert_eq!(deltas.intermediate_region, None);
1217
1218        // If an intermediate is provided but is equal to the implicit
1219        // intermediate from the peak, it SHOULD NOT be serialised.
1220        let deltas = GlyphDeltas::new(
1221            vec![Tent::new(
1222                F2Dot14::from_f32(0.5),
1223                Some(Tent::implied_intermediates_for_peak(F2Dot14::from_f32(0.5))),
1224            )],
1225            any_points.clone(),
1226        );
1227        assert_eq!(deltas.intermediate_region, None);
1228
1229        // If an intermediate is provided and it is not equal to the implicit
1230        // intermediate from the peak, it SHOULD be serialised.
1231        let deltas = GlyphDeltas::new(
1232            vec![Tent::new(
1233                F2Dot14::from_f32(0.5),
1234                Some((F2Dot14::from_f32(-0.3), F2Dot14::from_f32(0.4))),
1235            )],
1236            any_points.clone(),
1237        );
1238        assert_eq!(
1239            deltas.intermediate_region,
1240            Some((
1241                Tuple::new(vec![F2Dot14::from_f32(-0.3)]),
1242                Tuple::new(vec![F2Dot14::from_f32(0.4)]),
1243            ))
1244        );
1245    }
1246
1247    #[test]
1248    /// Test the logic for determining whether multiple intermediates need to be
1249    /// serialised in the context of their peak coordinates and each other.
1250    fn intermediates_only_when_at_least_one_needed() {
1251        let any_points = vec![]; // could be anything
1252
1253        // If every intermediate can be implied, none should be serialised.
1254        let deltas = GlyphDeltas::new(
1255            vec![
1256                Tent::new(F2Dot14::from_f32(0.5), None),
1257                Tent::new(F2Dot14::from_f32(0.5), None),
1258            ],
1259            any_points.clone(),
1260        );
1261        assert_eq!(deltas.intermediate_region, None);
1262
1263        // If even one intermediate cannot be implied, all should be serialised.
1264        let deltas = GlyphDeltas::new(
1265            vec![
1266                Tent::new(F2Dot14::from_f32(0.5), None),
1267                Tent::new(F2Dot14::from_f32(0.5), None),
1268                Tent::new(
1269                    F2Dot14::from_f32(0.5),
1270                    Some((F2Dot14::from_f32(-0.3), F2Dot14::from_f32(0.4))),
1271                ),
1272            ],
1273            any_points,
1274        );
1275        assert_eq!(
1276            deltas.intermediate_region,
1277            Some((
1278                Tuple::new(vec![
1279                    F2Dot14::from_f32(0.0),
1280                    F2Dot14::from_f32(0.0),
1281                    F2Dot14::from_f32(-0.3)
1282                ]),
1283                Tuple::new(vec![
1284                    F2Dot14::from_f32(0.5),
1285                    F2Dot14::from_f32(0.5),
1286                    F2Dot14::from_f32(0.4)
1287                ]),
1288            ))
1289        );
1290    }
1291}