Skip to main content

write_fonts/tables/
gpos.rs

1//! the [GPOS] table
2//!
3//! [GPOS]: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos
4
5include!("../../generated/generated_gpos.rs");
6
7use std::collections::HashSet;
8
9//use super::layout::value_record::ValueRecord;
10use super::{
11    layout::{
12        ChainedSequenceContext, ClassDef, CoverageTable, DeviceOrVariationIndex, FeatureList,
13        FeatureVariations, Lookup, LookupList, LookupSubtable, LookupType, ScriptList,
14        SequenceContext, VariationIndex,
15    },
16    variations::{common_builder::RemapVarStore, ivs_builder::VariationIndexRemapping},
17};
18
19#[cfg(test)]
20mod spec_tests;
21
22pub mod builders;
23mod value_record;
24pub use value_record::ValueRecord;
25
26/// A GPOS lookup list table.
27pub type PositionLookupList = LookupList<PositionLookup>;
28
29super::layout::table_newtype!(
30    PositionSequenceContext,
31    SequenceContext,
32    read_fonts::tables::layout::SequenceContext<'a>
33);
34
35super::layout::table_newtype!(
36    PositionChainContext,
37    ChainedSequenceContext,
38    read_fonts::tables::layout::ChainedSequenceContext<'a>
39);
40
41impl Gpos {
42    fn compute_version(&self) -> MajorMinor {
43        if self.feature_variations.is_none() {
44            MajorMinor::VERSION_1_0
45        } else {
46            MajorMinor::VERSION_1_1
47        }
48    }
49}
50
51super::layout::lookup_type!(gpos, SinglePos, 1);
52super::layout::lookup_type!(gpos, PairPos, 2);
53super::layout::lookup_type!(gpos, CursivePosFormat1, 3);
54super::layout::lookup_type!(gpos, MarkBasePosFormat1, 4);
55super::layout::lookup_type!(gpos, MarkLigPosFormat1, 5);
56super::layout::lookup_type!(gpos, MarkMarkPosFormat1, 6);
57super::layout::lookup_type!(gpos, PositionSequenceContext, 7);
58super::layout::lookup_type!(gpos, PositionChainContext, 8);
59super::layout::lookup_type!(gpos, ExtensionSubtable, 9);
60
61impl<T: LookupSubtable + FontWrite> FontWrite for ExtensionPosFormat1<T> {
62    fn write_into(&self, writer: &mut TableWriter) {
63        1u16.write_into(writer);
64        T::TYPE.write_into(writer);
65        self.extension.write_into(writer);
66    }
67}
68
69// these can't have auto impls because the traits don't support generics
70impl ReadArgs for PositionLookup {
71    type Args = ();
72}
73
74impl<'a> FontRead<'a> for PositionLookup {
75    fn read_with_args(data: FontData<'a>, _: ()) -> Result<Self, ReadError> {
76        read_fonts::tables::gpos::PositionLookup::read(data).map(|x| x.to_owned_table())
77    }
78}
79
80impl ReadArgs for PositionLookupList {
81    type Args = ();
82}
83
84impl<'a> FontRead<'a> for PositionLookupList {
85    fn read_with_args(data: FontData<'a>, _: ()) -> Result<Self, ReadError> {
86        read_fonts::tables::gpos::PositionLookupList::read(data).map(|x| x.to_owned_table())
87    }
88}
89
90impl SinglePosFormat1 {
91    fn compute_value_format(&self) -> ValueFormat {
92        self.value_record.format()
93    }
94}
95
96impl SinglePosFormat2 {
97    fn compute_value_format(&self) -> ValueFormat {
98        self.value_records
99            .first()
100            .map(ValueRecord::format)
101            .unwrap_or(ValueFormat::empty())
102    }
103}
104
105impl PairPosFormat1 {
106    fn compute_value_format1(&self) -> ValueFormat {
107        self.pair_sets
108            .first()
109            .and_then(|pairset| pairset.pair_value_records.first())
110            .map(|rec| rec.value_record1.format())
111            .unwrap_or(ValueFormat::empty())
112    }
113
114    fn compute_value_format2(&self) -> ValueFormat {
115        self.pair_sets
116            .first()
117            .and_then(|pairset| pairset.pair_value_records.first())
118            .map(|rec| rec.value_record2.format())
119            .unwrap_or(ValueFormat::empty())
120    }
121
122    fn check_format_consistency(&self, ctx: &mut ValidationCtx) {
123        let vf1 = self.compute_value_format1();
124        let vf2 = self.compute_value_format2();
125        ctx.with_array_items(self.pair_sets.iter(), |ctx, item| {
126            ctx.in_field("pair_value_records", |ctx| {
127                if item.pair_value_records.iter().any(|pairset| {
128                    pairset.value_record1.format() != vf1 || pairset.value_record2.format() != vf2
129                }) {
130                    ctx.report("all ValueRecords must have same format")
131                }
132            })
133        })
134    }
135}
136
137impl PairPosFormat2 {
138    fn compute_value_format1(&self) -> ValueFormat {
139        self.class1_records
140            .first()
141            .and_then(|rec| rec.class2_records.first())
142            .map(|rec| rec.value_record1.format())
143            .unwrap_or(ValueFormat::empty())
144    }
145
146    fn compute_value_format2(&self) -> ValueFormat {
147        self.class1_records
148            .first()
149            .and_then(|rec| rec.class2_records.first())
150            .map(|rec| rec.value_record2.format())
151            .unwrap_or(ValueFormat::empty())
152    }
153
154    fn compute_class1_count(&self) -> u16 {
155        self.class_def1.class_count()
156    }
157
158    fn compute_class2_count(&self) -> u16 {
159        self.class_def2.class_count()
160    }
161
162    fn check_length_and_format_conformance(&self, ctx: &mut ValidationCtx) {
163        let n_class_1s = self.class_def1.class_count();
164        let n_class_2s = self.class_def2.class_count();
165        let format_1 = self.compute_value_format1();
166        let format_2 = self.compute_value_format2();
167        if self.class1_records.len() != n_class_1s as usize {
168            ctx.report("class1_records length must match number of class1 classes");
169        }
170        ctx.in_field("class1_records", |ctx| {
171            ctx.with_array_items(self.class1_records.iter(), |ctx, c1rec| {
172                if c1rec.class2_records.len() != n_class_2s as usize {
173                    ctx.report("class2_records length must match number of class2 classes ");
174                }
175                if c1rec.class2_records.iter().any(|rec| {
176                    rec.value_record1.format() != format_1 || rec.value_record2.format() != format_2
177                }) {
178                    ctx.report("all value records should report the same format");
179                }
180            })
181        });
182    }
183}
184
185impl MarkBasePosFormat1 {
186    fn compute_mark_class_count(&self) -> u16 {
187        self.mark_array.class_count()
188    }
189}
190
191impl MarkMarkPosFormat1 {
192    fn compute_mark_class_count(&self) -> u16 {
193        self.mark1_array.class_count()
194    }
195}
196
197impl MarkLigPosFormat1 {
198    fn compute_mark_class_count(&self) -> u16 {
199        self.mark_array.class_count()
200    }
201}
202
203impl MarkArray {
204    fn class_count(&self) -> u16 {
205        self.mark_records
206            .iter()
207            .map(|rec| rec.mark_class)
208            .collect::<HashSet<_>>()
209            .len() as u16
210    }
211}
212
213impl RemapVarStore<VariationIndex> for ValueRecord {
214    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
215        for table in [
216            self.x_placement_device.as_mut(),
217            self.y_placement_device.as_mut(),
218            self.x_advance_device.as_mut(),
219            self.y_advance_device.as_mut(),
220        ]
221        .into_iter()
222        .flatten()
223        {
224            table.remap_variation_indices(key_map)
225        }
226    }
227}
228
229impl RemapVarStore<VariationIndex> for DeviceOrVariationIndex {
230    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
231        if let DeviceOrVariationIndex::PendingVariationIndex(table) = self {
232            *self = key_map.get(table.delta_set_id).unwrap().into();
233        }
234    }
235}
236
237impl RemapVarStore<VariationIndex> for AnchorTable {
238    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
239        if let AnchorTable::Format3(table) = self {
240            table
241                .x_device
242                .as_mut()
243                .into_iter()
244                .chain(table.y_device.as_mut())
245                .for_each(|x| x.remap_variation_indices(key_map))
246        }
247    }
248}
249
250impl RemapVarStore<VariationIndex> for Gpos {
251    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
252        self.lookup_list.as_mut().remap_variation_indices(key_map)
253    }
254}
255
256impl RemapVarStore<VariationIndex> for PositionLookupList {
257    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
258        for lookup in &mut self.lookups {
259            lookup.remap_variation_indices(key_map)
260        }
261    }
262}
263
264impl RemapVarStore<VariationIndex> for PositionLookup {
265    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
266        match self {
267            PositionLookup::Single(lookup) => lookup.remap_variation_indices(key_map),
268            PositionLookup::Pair(lookup) => lookup.remap_variation_indices(key_map),
269            PositionLookup::Cursive(lookup) => lookup.remap_variation_indices(key_map),
270            PositionLookup::MarkToBase(lookup) => lookup.remap_variation_indices(key_map),
271            PositionLookup::MarkToLig(lookup) => lookup.remap_variation_indices(key_map),
272            PositionLookup::MarkToMark(lookup) => lookup.remap_variation_indices(key_map),
273            PositionLookup::Extension(lookup) => lookup.remap_variation_indices(key_map),
274
275            // don't contain any metrics directly
276            PositionLookup::Contextual(_) | PositionLookup::ChainContextual(_) => (),
277        }
278    }
279}
280
281impl RemapVarStore<VariationIndex> for ExtensionSubtable {
282    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
283        match self {
284            ExtensionSubtable::Single(table) => table.remap_variation_indices(key_map),
285            ExtensionSubtable::Pair(table) => table.remap_variation_indices(key_map),
286            ExtensionSubtable::Cursive(table) => table.remap_variation_indices(key_map),
287            ExtensionSubtable::MarkToBase(table) => table.remap_variation_indices(key_map),
288            ExtensionSubtable::MarkToLig(table) => table.remap_variation_indices(key_map),
289            ExtensionSubtable::MarkToMark(table) => table.remap_variation_indices(key_map),
290
291            // don't contain any metrics directly
292            ExtensionSubtable::Contextual(_) | ExtensionSubtable::ChainContextual(_) => (),
293        }
294    }
295}
296
297impl<T: RemapVarStore<VariationIndex>> RemapVarStore<VariationIndex> for ExtensionPosFormat1<T> {
298    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
299        self.extension.as_mut().remap_variation_indices(key_map)
300    }
301}
302
303impl<T: RemapVarStore<VariationIndex>> RemapVarStore<VariationIndex> for Lookup<T> {
304    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
305        for subtable in &mut self.subtables {
306            subtable.remap_variation_indices(key_map)
307        }
308    }
309}
310
311impl RemapVarStore<VariationIndex> for SinglePos {
312    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
313        match self {
314            SinglePos::Format1(table) => table.remap_variation_indices(key_map),
315            SinglePos::Format2(table) => table.remap_variation_indices(key_map),
316        }
317    }
318}
319
320impl RemapVarStore<VariationIndex> for SinglePosFormat1 {
321    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
322        self.value_record.remap_variation_indices(key_map);
323    }
324}
325
326impl RemapVarStore<VariationIndex> for SinglePosFormat2 {
327    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
328        for rec in &mut self.value_records {
329            rec.remap_variation_indices(key_map);
330        }
331    }
332}
333
334impl RemapVarStore<VariationIndex> for PairPosFormat1 {
335    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
336        for pairset in &mut self.pair_sets {
337            for pairrec in &mut pairset.pair_value_records {
338                pairrec.value_record1.remap_variation_indices(key_map);
339                pairrec.value_record2.remap_variation_indices(key_map);
340            }
341        }
342    }
343}
344
345impl RemapVarStore<VariationIndex> for PairPosFormat2 {
346    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
347        for class1rec in &mut self.class1_records {
348            for class2rec in &mut class1rec.class2_records {
349                class2rec.value_record1.remap_variation_indices(key_map);
350                class2rec.value_record2.remap_variation_indices(key_map);
351            }
352        }
353    }
354}
355
356impl RemapVarStore<VariationIndex> for PairPos {
357    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
358        match self {
359            PairPos::Format1(table) => table.remap_variation_indices(key_map),
360            PairPos::Format2(table) => table.remap_variation_indices(key_map),
361        }
362    }
363}
364
365impl RemapVarStore<VariationIndex> for MarkBasePosFormat1 {
366    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
367        self.mark_array.as_mut().remap_variation_indices(key_map);
368        for rec in &mut self.base_array.as_mut().base_records {
369            for anchor in &mut rec.base_anchors {
370                if let Some(anchor) = anchor.as_mut() {
371                    anchor.remap_variation_indices(key_map);
372                }
373            }
374        }
375    }
376}
377
378impl RemapVarStore<VariationIndex> for MarkMarkPosFormat1 {
379    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
380        self.mark1_array.as_mut().remap_variation_indices(key_map);
381        for rec in &mut self.mark2_array.as_mut().mark2_records {
382            for anchor in &mut rec.mark2_anchors {
383                if let Some(anchor) = anchor.as_mut() {
384                    anchor.remap_variation_indices(key_map);
385                }
386            }
387        }
388    }
389}
390
391impl RemapVarStore<VariationIndex> for MarkLigPosFormat1 {
392    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
393        self.mark_array.as_mut().remap_variation_indices(key_map);
394        for lig in &mut self.ligature_array.as_mut().ligature_attaches {
395            for rec in &mut lig.component_records {
396                for anchor in &mut rec.ligature_anchors {
397                    if let Some(anchor) = anchor.as_mut() {
398                        anchor.remap_variation_indices(key_map);
399                    }
400                }
401            }
402        }
403    }
404}
405
406impl RemapVarStore<VariationIndex> for CursivePosFormat1 {
407    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
408        for rec in &mut self.entry_exit_record {
409            for anchor in [rec.entry_anchor.as_mut(), rec.exit_anchor.as_mut()]
410                .into_iter()
411                .flatten()
412            {
413                anchor.remap_variation_indices(key_map);
414            }
415        }
416    }
417}
418
419impl RemapVarStore<VariationIndex> for MarkArray {
420    fn remap_variation_indices(&mut self, key_map: &VariationIndexRemapping) {
421        for rec in &mut self.mark_records {
422            rec.mark_anchor.remap_variation_indices(key_map);
423        }
424    }
425}
426
427#[cfg(test)]
428mod tests {
429
430    use read_fonts::tables::{gpos as read_gpos, layout::LookupFlag};
431
432    use crate::tables::layout::VariationIndex;
433
434    use super::*;
435
436    #[test]
437    fn remap_variation_indices_through_extension() {
438        use crate::tables::variations::{
439            ivs_builder::VariationStoreBuilder, RegionAxisCoordinates, VariationRegion,
440        };
441        use font_types::F2Dot14;
442
443        let region = VariationRegion::new(vec![RegionAxisCoordinates {
444            start_coord: F2Dot14::from_f32(0.0),
445            peak_coord: F2Dot14::from_f32(1.0),
446            end_coord: F2Dot14::from_f32(1.0),
447        }]);
448        let mut var_store = VariationStoreBuilder::new(1);
449        let temp_id = var_store.add_deltas(vec![(region, -20)]);
450        let (_, key_map) = var_store.build();
451
452        let value_record = ValueRecord::new()
453            .with_x_advance(-20)
454            .with_x_advance_device(DeviceOrVariationIndex::pending_variation_index(temp_id));
455        let pair_set = PairSet::new(vec![PairValueRecord::new(
456            GlyphId16::new(2),
457            value_record,
458            ValueRecord::default(),
459        )]);
460        let pair_pos = PairPos::format_1(
461            CoverageTable::format_1(vec![GlyphId16::new(1)]),
462            vec![pair_set],
463        );
464
465        // the same subtable, in a plain lookup and wrapped in an extension lookup
466        let mut plain =
467            PositionLookup::Pair(Lookup::new(LookupFlag::default(), vec![pair_pos.clone()]));
468        let mut extension = PositionLookup::Extension(Lookup::new(
469            LookupFlag::default(),
470            vec![ExtensionSubtable::Pair(ExtensionPosFormat1::new(
471                <PairPos as LookupSubtable>::TYPE.to_raw(),
472                pair_pos,
473            ))],
474        ));
475
476        plain.remap_variation_indices(&key_map);
477        extension.remap_variation_indices(&key_map);
478
479        // writing a PendingVariationIndex panics, so dump_table succeeding is the assertion
480        let plain_bytes = crate::dump_table(&plain).unwrap();
481        let ext_bytes = crate::dump_table(&extension).unwrap();
482
483        let parsed = read_gpos::PositionLookup::read(FontData::new(&ext_bytes)).unwrap();
484        let read_gpos::PositionSubtables::Pair(subtables) = parsed.subtables().unwrap() else {
485            panic!("expected a pair lookup");
486        };
487        let read_gpos::PairPos::Format1(subtable) = subtables.get(0).unwrap() else {
488            panic!("expected format 1");
489        };
490        let pair_set = subtable.pair_sets().get(0).unwrap();
491        let record = &pair_set.pair_value_records().get(0).unwrap();
492        let read_fonts::tables::layout::DeviceOrVariationIndex::VariationIndex(var_idx) =
493            record.value_record1.x_advance_device().unwrap().unwrap()
494        else {
495            panic!("expected a VariationIndex");
496        };
497        let expected = key_map.get(temp_id).unwrap();
498        assert_eq!(
499            (
500                var_idx.delta_set_outer_index(),
501                var_idx.delta_set_inner_index()
502            ),
503            (
504                expected.delta_set_outer_index,
505                expected.delta_set_inner_index
506            )
507        );
508        // the extension wrapper is the only difference, so the inner bytes must match
509        assert_eq!(plain_bytes.len() + 8, ext_bytes.len());
510    }
511
512    // adapted from/motivated by https://github.com/fonttools/fonttools/issues/471
513    #[test]
514    fn gpos_1_zero() {
515        let cov_one = CoverageTable::format_1(vec![GlyphId16::new(2)]);
516        let cov_two = CoverageTable::format_1(vec![GlyphId16::new(4)]);
517        let sub1 = SinglePos::format_1(cov_one, ValueRecord::default());
518        let sub2 = SinglePos::format_1(cov_two, ValueRecord::default().with_x_advance(500));
519        let lookup = Lookup::new(LookupFlag::default(), vec![sub1, sub2]);
520        let bytes = crate::dump_table(&lookup).unwrap();
521
522        let parsed = read_gpos::PositionLookup::read(FontData::new(&bytes)).unwrap();
523        let read_gpos::PositionLookup::Single(table) = parsed else {
524            panic!("something has gone seriously wrong");
525        };
526
527        assert_eq!(table.lookup_flag(), LookupFlag::empty());
528        assert_eq!(table.sub_table_count(), 2);
529        let read_gpos::SinglePos::Format1(sub1) = table.subtables().get(0).unwrap() else {
530            panic!("wrong table type");
531        };
532        let read_gpos::SinglePos::Format1(sub2) = table.subtables().get(1).unwrap() else {
533            panic!("wrong table type");
534        };
535
536        assert_eq!(sub1.value_format(), ValueFormat::empty());
537        assert!(sub1.value_record().is_empty());
538
539        assert_eq!(sub2.value_format(), ValueFormat::X_ADVANCE);
540        assert_eq!(sub2.value_record().x_advance(), Some(500));
541        assert_eq!(sub2.value_record().y_advance(), None);
542    }
543
544    // shared between a pair of tests below
545    fn make_rec(i: u16) -> ValueRecord {
546        // '0' here is shorthand for 'no device table'
547        if i == 0 {
548            return ValueRecord::new().with_explicit_value_format(ValueFormat::X_ADVANCE_DEVICE);
549        }
550        ValueRecord::new().with_x_advance_device(VariationIndex::new(0xff, i))
551    }
552
553    #[test]
554    fn compile_devices_pairpos2() {
555        let class1 = ClassDef::from_iter([(GlyphId16::new(5), 0), (GlyphId16::new(6), 1)]);
556        // class 0 is 'all the rest', here, always implicitly present
557        let class2 = ClassDef::from_iter([(GlyphId16::new(8), 1)]);
558
559        // two c1recs, each with two c2recs
560        let class1recs = vec![
561            Class1Record::new(vec![
562                Class2Record::new(make_rec(0), make_rec(0)),
563                Class2Record::new(make_rec(1), make_rec(2)),
564            ]),
565            Class1Record::new(vec![
566                Class2Record::new(make_rec(0), make_rec(0)),
567                Class2Record::new(make_rec(2), make_rec(3)),
568            ]),
569        ];
570        let coverage = class1.iter().map(|(gid, _)| gid).collect();
571        let a_table = PairPos::format_2(coverage, class1, class2, class1recs);
572
573        let bytes = crate::dump_table(&a_table).unwrap();
574        let read_back = PairPosFormat2::read(bytes.as_slice().into()).unwrap();
575
576        assert!(read_back.class1_records[0].class2_records[0]
577            .value_record1
578            .x_advance_device
579            .is_none());
580        assert!(read_back.class1_records[1].class2_records[1]
581            .value_record1
582            .x_advance_device
583            .is_some());
584
585        let DeviceOrVariationIndex::VariationIndex(dev2) = read_back.class1_records[0]
586            .class2_records[1]
587            .value_record2
588            .x_advance_device
589            .as_ref()
590            .unwrap()
591        else {
592            panic!("not a variation index")
593        };
594        assert_eq!(dev2.delta_set_inner_index, 2);
595    }
596
597    #[should_panic(expected = "all value records should report the same format")]
598    #[test]
599    fn validate_bad_pairpos2() {
600        let class1 = ClassDef::from_iter([(GlyphId16::new(5), 0), (GlyphId16::new(6), 1)]);
601        // class 0 is 'all the rest', here, always implicitly present
602        let class2 = ClassDef::from_iter([(GlyphId16::new(8), 1)]);
603        let coverage = class1.iter().map(|(gid, _)| gid).collect();
604
605        // two c1recs, each with two c2recs
606        let class1recs = vec![
607            Class1Record::new(vec![
608                Class2Record::new(make_rec(0), make_rec(0)),
609                Class2Record::new(make_rec(1), make_rec(2)),
610            ]),
611            Class1Record::new(vec![
612                Class2Record::new(make_rec(0), make_rec(0)),
613                // this is now the wrong type
614                Class2Record::new(make_rec(2), make_rec(3).with_x_advance(0x514)),
615            ]),
616        ];
617        let ppf2 = PairPos::format_2(coverage, class1, class2, class1recs);
618        crate::dump_table(&ppf2).unwrap();
619    }
620
621    #[test]
622    fn validate_pairpos1() {
623        let coverage: CoverageTable = [1, 2].into_iter().map(GlyphId16::new).collect();
624        let good_table = PairPosFormat1::new(
625            coverage.clone(),
626            vec![
627                PairSet::new(vec![PairValueRecord::new(
628                    GlyphId16::new(5),
629                    ValueRecord::new().with_x_advance(5),
630                    ValueRecord::new(),
631                )]),
632                PairSet::new(vec![PairValueRecord::new(
633                    GlyphId16::new(1),
634                    ValueRecord::new().with_x_advance(42),
635                    ValueRecord::new(),
636                )]),
637            ],
638        );
639
640        let bad_table = PairPosFormat1::new(
641            coverage,
642            vec![
643                PairSet::new(vec![PairValueRecord::new(
644                    GlyphId16::new(5),
645                    ValueRecord::new().with_x_advance(5),
646                    ValueRecord::new(),
647                )]),
648                PairSet::new(vec![PairValueRecord::new(
649                    GlyphId16::new(1),
650                    //this is a different format, which is not okay
651                    ValueRecord::new().with_x_placement(42),
652                    ValueRecord::new(),
653                )]),
654            ],
655        );
656
657        assert!(crate::dump_table(&good_table).is_ok());
658        assert!(matches!(
659            crate::dump_table(&bad_table),
660            Err(crate::error::Error::ValidationFailed(_))
661        ));
662    }
663}