Skip to main content

read_fonts/tables/
base.rs

1//! The [BASE](https://learn.microsoft.com/en-us/typography/opentype/spec/base) table
2
3use super::{
4    layout::DeviceOrVariationIndex,
5    variations::{DeltaSetIndex, ItemVariationStore},
6};
7
8include!("../../generated/generated_base.rs");
9
10/// Which axis table a baseline is read from.
11///
12/// This is the direction the text runs in, not the direction of the baseline
13/// itself: horizontal text takes its baselines from the horizontal axis.
14#[derive(Clone, Copy, PartialEq, Eq, Debug)]
15pub enum BaseAxis {
16    /// Text that runs left to right or right to left.
17    Horizontal,
18    /// Text that runs top to bottom or bottom to top.
19    Vertical,
20}
21
22/// A baseline coordinate read at a location in variation space and a size.
23///
24/// A coordinate carries at most one adjustment, and the two kinds land in
25/// different units. A `VariationIndex` names a delta in design units, so it is
26/// already folded into `value`. A `Device` table adjusts by whole pixels, and
27/// scaling that into design units needs the units per em, which this table
28/// does not know; a caller working in font units wants
29/// `value + delta_px * upem / ppem`, and one working in pixels has the
30/// adjustment already.
31#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
32pub struct BaseValue {
33    /// The coordinate in design units, with any variation delta applied.
34    pub value: F48Dot16,
35    /// The adjustment the coordinate's device table makes at the requested
36    /// size, in pixels. Zero unless the coordinate names a `Device` table that
37    /// covers that size.
38    pub delta_px: i32,
39}
40
41/// The script a font falls back to when it describes no baselines for the one
42/// asked for.
43const DEFAULT_SCRIPT: Tag = Tag::new(b"DFLT");
44
45/// The baseline tags the OpenType spec registers.
46pub mod baseline_tags {
47    use super::Tag;
48
49    /// The baseline alphabetic scripts sit on, such as Latin, Cyrillic and
50    /// Greek.
51    pub const ROMAN: Tag = Tag::new(b"romn");
52    /// The baseline scripts hang from, such as Devanagari.
53    pub const HANGING: Tag = Tag::new(b"hang");
54    /// The bottom or left edge of an ideographic character face.
55    pub const IDEO_FACE_BOTTOM_OR_LEFT: Tag = Tag::new(b"icfb");
56    /// The top or right edge of an ideographic character face.
57    pub const IDEO_FACE_TOP_OR_RIGHT: Tag = Tag::new(b"icft");
58    /// The centre of an ideographic character face.
59    pub const IDEO_FACE_CENTRAL: Tag = Tag::new(b"Icfc");
60    /// The bottom or left edge of an ideographic em box.
61    pub const IDEO_EMBOX_BOTTOM_OR_LEFT: Tag = Tag::new(b"ideo");
62    /// The top or right edge of an ideographic em box.
63    pub const IDEO_EMBOX_TOP_OR_RIGHT: Tag = Tag::new(b"idtp");
64    /// The centre of an ideographic em box.
65    pub const IDEO_EMBOX_CENTRAL: Tag = Tag::new(b"Idce");
66    /// The baseline mathematical characters are centred on.
67    pub const MATH: Tag = Tag::new(b"math");
68}
69
70/// Scripts whose horizontal text hangs from a line above it.
71///
72/// Grouped by the Unicode version that added each, as HarfBuzz lists them.
73const HANGING_SCRIPTS: &[Tag] = &[
74    // Unicode 1.1
75    Tag::new(b"Beng"),
76    Tag::new(b"Deva"),
77    Tag::new(b"Gujr"),
78    Tag::new(b"Guru"),
79    // Unicode 2.0
80    Tag::new(b"Tibt"),
81    // Unicode 4.0
82    Tag::new(b"Limb"),
83    // Unicode 4.1
84    Tag::new(b"Sylo"),
85    // Unicode 5.0
86    Tag::new(b"Phag"),
87    // Unicode 5.2
88    Tag::new(b"Mtei"),
89    // Unicode 6.1
90    Tag::new(b"Shrd"),
91    Tag::new(b"Takr"),
92    // Unicode 7.0
93    Tag::new(b"Modi"),
94    Tag::new(b"Sidd"),
95    Tag::new(b"Tirh"),
96    // Unicode 9.0
97    Tag::new(b"Marc"),
98    Tag::new(b"Newa"),
99    // Unicode 10.0
100    Tag::new(b"Soyo"),
101    Tag::new(b"Zanb"),
102    // Unicode 11.0
103    Tag::new(b"Dogr"),
104    Tag::new(b"Gong"),
105    // Unicode 12.0
106    Tag::new(b"Nand"),
107];
108
109/// Scripts written as ideographs.
110const IDEOGRAPHIC_SCRIPTS: &[Tag] = &[
111    // Unicode 1.1
112    Tag::new(b"Hang"),
113    Tag::new(b"Hani"),
114    Tag::new(b"Hira"),
115    Tag::new(b"Kana"),
116    // Unicode 3.0
117    Tag::new(b"Bopo"),
118    // Unicode 9.0
119    Tag::new(b"Tang"),
120    // Unicode 10.0
121    Tag::new(b"Nshu"),
122    // Unicode 13.0
123    Tag::new(b"Kits"),
124];
125
126/// The baseline horizontal text in a script sits on.
127///
128/// Scripts that hang from a line above take [`HANGING`], ideographic scripts
129/// take [`IDEO_FACE_BOTTOM_OR_LEFT`], and everything else, known or not, takes
130/// [`ROMAN`].
131///
132/// `script` is a Unicode script code -- `Deva`, `Hani` -- and not an OpenType
133/// script tag. The two are spelled differently: the script `Hang` is Hangul,
134/// while the baseline tag `hang` is the hanging baseline.
135///
136/// Mirrors HarfBuzz's [`hb_ot_layout_get_horizontal_baseline_tag_for_script`].
137///
138/// [`hb_ot_layout_get_horizontal_baseline_tag_for_script`]: https://github.com/harfbuzz/harfbuzz/blob/92e67ef19f2d595b0fe81f05a80783a321bb918f/src/hb-ot-layout.cc#L2235
139///
140/// [`HANGING`]: baseline_tags::HANGING
141/// [`IDEO_FACE_BOTTOM_OR_LEFT`]: baseline_tags::IDEO_FACE_BOTTOM_OR_LEFT
142/// [`ROMAN`]: baseline_tags::ROMAN
143pub fn horizontal_baseline_tag_for_script(script: Tag) -> Tag {
144    if HANGING_SCRIPTS.contains(&script) {
145        baseline_tags::HANGING
146    } else if IDEOGRAPHIC_SCRIPTS.contains(&script) {
147        baseline_tags::IDEO_FACE_BOTTOM_OR_LEFT
148    } else {
149        baseline_tags::ROMAN
150    }
151}
152
153impl<'a> Base<'a> {
154    /// The axis table for a writing direction, or `None` where the font
155    /// describes no baselines for that direction.
156    pub fn axis(&self, axis: BaseAxis) -> Option<Axis<'a>> {
157        match axis {
158            BaseAxis::Horizontal => self.horiz_axis(),
159            BaseAxis::Vertical => self.vert_axis(),
160        }?
161        .ok()
162    }
163}
164
165impl<'a> Axis<'a> {
166    /// The baselines a script uses, falling back to the default script, or
167    /// `None` where neither is described.
168    pub fn base_script(&self, script_tag: Tag) -> Option<BaseScript<'a>> {
169        let list = self.base_script_list().ok()?;
170        let records = list.base_script_records();
171        let record =
172            find_script(records, script_tag).or_else(|| find_script(records, DEFAULT_SCRIPT))?;
173        record.base_script(list.offset_data()).ok()
174    }
175
176    /// The coordinate of one baseline for a script, or `None` where the font
177    /// does not place that baseline.
178    ///
179    /// The script falls back to the default script, as in
180    /// [`base_script`][Self::base_script]. A language does not come into it:
181    /// languages vary the minimum and maximum extents, not the baselines.
182    pub fn baseline_coord(&self, baseline_tag: Tag, script_tag: Tag) -> Option<BaseCoord<'a>> {
183        let values = self.base_script(script_tag)?.base_values()?.ok()?;
184        let tags = self.base_tag_list()?.ok()?;
185        // The spec has these in alphabetical order, and a font that does not
186        // keep to it hides the baselines that are out of place.
187        let index = tags
188            .baseline_tags()
189            .binary_search_by_key(&baseline_tag, |tag| tag.get())
190            .ok()?;
191        values.base_coords().get(index).ok()
192    }
193}
194
195/// The record for a script, by tag.
196fn find_script(records: &[BaseScriptRecord], tag: Tag) -> Option<&BaseScriptRecord> {
197    let index = records
198        .binary_search_by_key(&tag, |record| record.base_script_tag())
199        .ok()?;
200    records.get(index)
201}
202
203impl<'a> BaseCoord<'a> {
204    /// The device or variation table that adjusts this coordinate.
205    ///
206    /// Only a format 3 coordinate has one.
207    pub fn device(&self) -> Option<DeviceOrVariationIndex<'a>> {
208        match self {
209            BaseCoord::Format3(coord) => coord.device()?.ok(),
210            _ => None,
211        }
212    }
213}
214
215/// A `BASE` table paired with a location in variation space.
216///
217/// The table carries its own item variation store, so an instance needs only
218/// the coordinates to read at; without them it reads the values as the font
219/// stores them.
220#[derive(Clone)]
221pub struct BaseInstance<'a> {
222    base: Base<'a>,
223    var_store: Option<ItemVariationStore<'a>>,
224    coords: &'a [F2Dot14],
225}
226
227impl<'a> BaseInstance<'a> {
228    /// Creates an instance that reads coordinates as the font stores them,
229    /// with no deltas applied.
230    pub fn new(base: Base<'a>) -> Self {
231        Self::with_coords(base, &[])
232    }
233
234    /// Creates an instance that reads coordinates at `coords`.
235    ///
236    /// Deltas are applied only when the table has an item variation store and
237    /// `coords` is not empty; without either this behaves as
238    /// [`new`][Self::new].
239    pub fn with_coords(base: Base<'a>, coords: &'a [F2Dot14]) -> Self {
240        let var_store = base.item_var_store().and_then(|store| store.ok());
241        Self {
242            base,
243            var_store,
244            coords,
245        }
246    }
247
248    /// The position of one baseline, in design units, or `None` where the font
249    /// does not place it.
250    ///
251    /// A baseline is a position on the axis across the writing direction: a y
252    /// coordinate for horizontal text, an x coordinate for vertical text.
253    pub fn baseline(&self, baseline_tag: Tag, axis: BaseAxis, script_tag: Tag) -> Option<F48Dot16> {
254        Some(
255            self.baseline_for_ppem(baseline_tag, axis, script_tag, 0)?
256                .value,
257        )
258    }
259
260    /// The position of one baseline as read at a size, or `None` where the
261    /// font does not place it.
262    ///
263    /// `ppem` is the size along the same axis the coordinate lies on, so it is
264    /// the vertical size for horizontal text and the horizontal size for
265    /// vertical text.
266    pub fn baseline_for_ppem(
267        &self,
268        baseline_tag: Tag,
269        axis: BaseAxis,
270        script_tag: Tag,
271        ppem: u16,
272    ) -> Option<BaseValue> {
273        let coord = self
274            .base
275            .axis(axis)?
276            .baseline_coord(baseline_tag, script_tag)?;
277        Some(self.coord_for_ppem(&coord, ppem))
278    }
279
280    /// A coordinate in design units, with any variation delta applied.
281    pub fn coord(&self, coord: &BaseCoord) -> F48Dot16 {
282        self.coord_for_ppem(coord, 0).value
283    }
284
285    /// A coordinate as read at a size.
286    pub fn coord_for_ppem(&self, coord: &BaseCoord, ppem: u16) -> BaseValue {
287        // A format 2 coordinate names a glyph and a contour point to take the
288        // position from. Nothing reads them: HarfBuzz answers with the plain
289        // coordinate, so the two formats behave alike.
290        let mut value = F48Dot16::from_i32(coord.coordinate() as i32);
291        let mut delta_px = 0;
292        match coord.device() {
293            Some(DeviceOrVariationIndex::Device(device)) => {
294                delta_px = device.delta_for_ppem(ppem);
295            }
296            Some(DeviceOrVariationIndex::VariationIndex(index)) => {
297                if let Some(delta) = self.delta(index.into()) {
298                    value += delta;
299                }
300            }
301            None => {}
302        }
303        BaseValue { value, delta_px }
304    }
305
306    fn delta(&self, index: DeltaSetIndex) -> Option<F48Dot16> {
307        if self.coords.is_empty() {
308            return None;
309        }
310        self.var_store.as_ref()?.compute_delta(index, self.coords)
311    }
312}
313
314impl<'a> core::ops::Deref for BaseInstance<'a> {
315    type Target = Base<'a>;
316
317    fn deref(&self) -> &Self::Target {
318        &self.base
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use font_test_data::bebuffer::BeBuffer;
325    use font_types::MajorMinor;
326
327    use super::*;
328
329    #[test]
330    /// https://learn.microsoft.com/en-us/typography/opentype/spec/base#base-table-examples
331    fn example_1() {
332        let data = BeBuffer::new()
333            .push(MajorMinor::VERSION_1_0)
334            .push(8u16) // horizaxis offset
335            .push(0x10c_u16) // verticalaxis
336            // axis table
337            .push(4u16) //basetaglist
338            .push(0x12_u16) // basescript list
339            // base tag list
340            .push(3u16) // count
341            .push(Tag::new(b"hang"))
342            .push(Tag::new(b"ideo"))
343            .push(Tag::new(b"romn"))
344            // basescriptlist
345            .push(4u16) // basescript count
346            .push(Tag::new(b"cyrl"))
347            .push(0x1a_u16)
348            .push(Tag::new(b"devn"))
349            .push(0x60_u16)
350            .push(Tag::new(b"hani"))
351            .push(0x8a_u16)
352            .push(Tag::new(b"latn"))
353            .push(0xb4_u16);
354
355        let base = Base::read(data.data().into()).unwrap();
356        assert_eq!(base.version(), MajorMinor::VERSION_1_0);
357        let horiz = base.horiz_axis().unwrap().unwrap();
358        let base_tag = horiz.base_tag_list().unwrap().unwrap();
359        assert_eq!(
360            base_tag.baseline_tags(),
361            &[Tag::new(b"hang"), Tag::new(b"ideo"), Tag::new(b"romn")]
362        );
363        assert_eq!(base_tag.min_byte_range().end, 14);
364        let base_script = horiz.base_script_list().unwrap();
365        assert_eq!(
366            base_script.base_script_records()[3].base_script_tag(),
367            Tag::new(b"latn")
368        );
369    }
370
371    const HANG: Tag = Tag::new(b"hang");
372    const IDEO: Tag = Tag::new(b"ideo");
373    const ROMN: Tag = Tag::new(b"romn");
374    const LATN: Tag = Tag::new(b"latn");
375    const HANI: Tag = Tag::new(b"hani");
376    const ARAB: Tag = Tag::new(b"arab");
377
378    /// The delta the test store holds for delta set (0, 0) at the far end of
379    /// its one axis.
380    const ROMN_DELTA: i32 = -40;
381
382    /// A BASE table with a horizontal axis, three baselines, and two scripts
383    /// plus a default.
384    ///
385    /// `latn` places its roman baseline with a `VariationIndex` and its
386    /// hanging baseline with a `Device` table, so the two kinds of adjustment
387    /// can be told apart.
388    fn base_table() -> BeBuffer {
389        let mut buf = BeBuffer::new()
390            .push(MajorMinor::VERSION_1_1)
391            .push_with_tag(0u16, "horiz_axis_offset")
392            .push(0u16) // vertical axis: null
393            .push_with_tag(0u32, "var_store_offset");
394
395        // -- HorizAxis --
396        let axis = buf.len();
397        buf = buf
398            .push_with_tag(0u16, "base_tag_list_offset")
399            .push_with_tag(0u16, "base_script_list_offset");
400
401        // BaseTagList, in the alphabetical order the spec calls for.
402        let tag_list = buf.len();
403        buf = buf.push(3u16).push(HANG).push(IDEO).push(ROMN);
404
405        // BaseScriptList, also sorted by tag.
406        let script_list = buf.len();
407        buf = buf
408            .push(3u16) // count
409            .push(Tag::new(b"DFLT"))
410            .push_with_tag(0u16, "dflt_script_offset")
411            .push(HANI)
412            .push_with_tag(0u16, "hani_script_offset")
413            .push(LATN)
414            .push_with_tag(0u16, "latn_script_offset");
415
416        // BaseScript for latn.
417        let latn_script = buf.len();
418        buf = buf
419            .push_with_tag(0u16, "latn_values_offset")
420            .push(0u16) // default min max: null
421            .push(0u16); // base lang sys count
422        let latn_values = buf.len();
423        buf = buf
424            .push(2u16) // default baseline index: romn
425            .push(3u16) // count, one per baseline tag
426            .push_with_tag(0u16, "latn_hang_offset")
427            .push_with_tag(0u16, "latn_ideo_offset")
428            .push_with_tag(0u16, "latn_romn_offset");
429
430        // BaseScript for hani, with no BaseValues at all.
431        let hani_script = buf.len();
432        buf = buf.push(0u16).push(0u16).push(0u16);
433
434        // BaseScript for DFLT.
435        let dflt_script = buf.len();
436        buf = buf
437            .push_with_tag(0u16, "dflt_values_offset")
438            .push(0u16)
439            .push(0u16);
440        let dflt_values = buf.len();
441        buf = buf
442            .push(2u16)
443            .push(3u16)
444            .push_with_tag(0u16, "dflt_hang_offset")
445            .push_with_tag(0u16, "dflt_ideo_offset")
446            .push_with_tag(0u16, "dflt_romn_offset");
447
448        // latn coordinates: hang has a device, romn has a variation index.
449        let latn_hang = buf.len();
450        buf = buf
451            .push(3u16) // format 3
452            .push(700i16)
453            .push_with_tag(0u16, "latn_hang_device");
454        let latn_ideo = buf.len();
455        buf = buf.push(1u16).push(-120i16);
456        let latn_romn = buf.len();
457        buf = buf
458            .push(3u16)
459            .push(0i16)
460            .push_with_tag(0u16, "latn_romn_varidx");
461
462        // DFLT coordinates, all plain. Format 2 is here to show it reads the
463        // same as format 1.
464        let dflt_hang = buf.len();
465        buf = buf.push(1u16).push(600i16);
466        let dflt_ideo = buf.len();
467        buf = buf.push(2u16).push(-100i16).push(9u16).push(3u16);
468        let dflt_romn = buf.len();
469        buf = buf.push(1u16).push(5i16);
470
471        let latn_hang_device = buf.len();
472        buf = buf
473            .push(10u16) // start size
474            .push(11u16) // end size
475            .push(3u16) // delta format: 8 bit
476            .push(0x02FEu16); // ppem 10 -> 2, ppem 11 -> -2
477        let latn_romn_varidx = buf.len();
478        buf = buf.push(0u16).push(0u16).push(0x8000u16);
479
480        // -- ItemVariationStore --
481        let var_store = buf.len();
482        buf = buf
483            .push(1u16) // format
484            .push_with_tag(0u32, "region_list_offset")
485            .push(1u16) // item variation data count
486            .push_with_tag(0u32, "var_data_offset");
487        let region_list = buf.len();
488        buf = buf
489            .push(1u16) // axis count
490            .push(1u16) // region count
491            .push(F2Dot14::from_f32(0.0))
492            .push(F2Dot14::from_f32(1.0))
493            .push(F2Dot14::from_f32(1.0));
494        let var_data = buf.len();
495        buf = buf
496            .push(1u16) // item count
497            .push(1u16) // word delta count
498            .push(1u16) // region index count
499            .push(0u16) // region indexes[0]
500            .push(ROMN_DELTA as i16);
501
502        buf.write_at("horiz_axis_offset", axis as u16);
503        buf.write_at("var_store_offset", var_store as u32);
504        buf.write_at("base_tag_list_offset", (tag_list - axis) as u16);
505        buf.write_at("base_script_list_offset", (script_list - axis) as u16);
506        buf.write_at("dflt_script_offset", (dflt_script - script_list) as u16);
507        buf.write_at("hani_script_offset", (hani_script - script_list) as u16);
508        buf.write_at("latn_script_offset", (latn_script - script_list) as u16);
509        buf.write_at("latn_values_offset", (latn_values - latn_script) as u16);
510        buf.write_at("dflt_values_offset", (dflt_values - dflt_script) as u16);
511        buf.write_at("latn_hang_offset", (latn_hang - latn_values) as u16);
512        buf.write_at("latn_ideo_offset", (latn_ideo - latn_values) as u16);
513        buf.write_at("latn_romn_offset", (latn_romn - latn_values) as u16);
514        buf.write_at("dflt_hang_offset", (dflt_hang - dflt_values) as u16);
515        buf.write_at("dflt_ideo_offset", (dflt_ideo - dflt_values) as u16);
516        buf.write_at("dflt_romn_offset", (dflt_romn - dflt_values) as u16);
517        buf.write_at("latn_hang_device", (latn_hang_device - latn_hang) as u16);
518        buf.write_at("latn_romn_varidx", (latn_romn_varidx - latn_romn) as u16);
519        buf.write_at("region_list_offset", (region_list - var_store) as u32);
520        buf.write_at("var_data_offset", (var_data - var_store) as u32);
521        buf
522    }
523
524    fn base(buf: &BeBuffer) -> Base<'_> {
525        Base::read(buf.data().into()).unwrap()
526    }
527
528    fn value(v: i32) -> F48Dot16 {
529        F48Dot16::from_i32(v)
530    }
531
532    #[test]
533    fn scripts_map_to_their_baseline() {
534        use baseline_tags::{HANGING, IDEO_FACE_BOTTOM_OR_LEFT, ROMAN};
535
536        assert_eq!(
537            horizontal_baseline_tag_for_script(Tag::new(b"Deva")),
538            HANGING
539        );
540        assert_eq!(
541            horizontal_baseline_tag_for_script(Tag::new(b"Nand")),
542            HANGING
543        );
544        assert_eq!(
545            horizontal_baseline_tag_for_script(Tag::new(b"Hani")),
546            IDEO_FACE_BOTTOM_OR_LEFT
547        );
548        assert_eq!(
549            horizontal_baseline_tag_for_script(Tag::new(b"Kits")),
550            IDEO_FACE_BOTTOM_OR_LEFT
551        );
552        assert_eq!(horizontal_baseline_tag_for_script(Tag::new(b"Latn")), ROMAN);
553        assert_eq!(horizontal_baseline_tag_for_script(Tag::new(b"Arab")), ROMAN);
554        // An unknown script is roman like any other.
555        assert_eq!(horizontal_baseline_tag_for_script(Tag::new(b"Zzzz")), ROMAN);
556
557        // The script Hang is Hangul, and takes the ideographic baseline. The
558        // baseline tag hang is a different thing spelled the same way.
559        assert_eq!(
560            horizontal_baseline_tag_for_script(Tag::new(b"Hang")),
561            IDEO_FACE_BOTTOM_OR_LEFT
562        );
563        assert_ne!(Tag::new(b"Hang"), HANGING);
564
565        // The two lists are as long as HarfBuzz's, and do not overlap.
566        assert_eq!(HANGING_SCRIPTS.len(), 21);
567        assert_eq!(IDEOGRAPHIC_SCRIPTS.len(), 8);
568        for script in HANGING_SCRIPTS {
569            assert!(!IDEOGRAPHIC_SCRIPTS.contains(script), "{script} is in both");
570        }
571    }
572
573    #[test]
574    fn baselines_are_found_by_tag() {
575        let buf = base_table();
576        let instance = BaseInstance::new(base(&buf));
577
578        assert_eq!(
579            instance.baseline(HANG, BaseAxis::Horizontal, LATN),
580            Some(value(700))
581        );
582        assert_eq!(
583            instance.baseline(IDEO, BaseAxis::Horizontal, LATN),
584            Some(value(-120))
585        );
586        assert_eq!(
587            instance.baseline(ROMN, BaseAxis::Horizontal, LATN),
588            Some(value(0))
589        );
590
591        // A tag the font does not place.
592        assert_eq!(
593            instance.baseline(Tag::new(b"math"), BaseAxis::Horizontal, LATN),
594            None
595        );
596        // The font describes no vertical axis.
597        assert_eq!(instance.baseline(ROMN, BaseAxis::Vertical, LATN), None);
598    }
599
600    #[test]
601    fn an_unlisted_script_falls_back_to_the_default() {
602        let buf = base_table();
603        let instance = BaseInstance::new(base(&buf));
604
605        // arab is not in the list, so it reads the DFLT baselines.
606        assert_eq!(
607            instance.baseline(HANG, BaseAxis::Horizontal, ARAB),
608            Some(value(600))
609        );
610        // A format 2 coordinate reads as its plain coordinate.
611        assert_eq!(
612            instance.baseline(IDEO, BaseAxis::Horizontal, ARAB),
613            Some(value(-100))
614        );
615
616        // hani is listed but places no baselines at all, and the fallback does
617        // not apply once a script has been found.
618        assert_eq!(instance.baseline(HANG, BaseAxis::Horizontal, HANI), None);
619    }
620
621    #[test]
622    fn variation_deltas_come_from_the_tables_own_store() {
623        let buf = base_table();
624        let coords = [F2Dot14::from_f32(1.0)];
625        let instance = BaseInstance::with_coords(base(&buf), &coords);
626
627        // latn's roman baseline is stored at 0 with a VariationIndex.
628        assert_eq!(
629            instance.baseline(ROMN, BaseAxis::Horizontal, LATN),
630            Some(value(ROMN_DELTA))
631        );
632        // Its neighbours have no variation index and are untouched.
633        assert_eq!(
634            instance.baseline(IDEO, BaseAxis::Horizontal, LATN),
635            Some(value(-120))
636        );
637
638        // At the default location the store contributes nothing.
639        let default = BaseInstance::new(base(&buf));
640        assert_eq!(
641            default.baseline(ROMN, BaseAxis::Horizontal, LATN),
642            Some(value(0))
643        );
644    }
645
646    #[test]
647    fn device_tables_adjust_by_ppem() {
648        let buf = base_table();
649        let instance = BaseInstance::new(base(&buf));
650
651        // latn's hanging baseline is stored at 700 with a device covering
652        // 10 to 11.
653        let at = |ppem| instance.baseline_for_ppem(HANG, BaseAxis::Horizontal, LATN, ppem);
654        assert_eq!(
655            at(10),
656            Some(BaseValue {
657                value: value(700),
658                delta_px: 2
659            })
660        );
661        assert_eq!(
662            at(11),
663            Some(BaseValue {
664                value: value(700),
665                delta_px: -2
666            })
667        );
668        // Outside the sizes it covers.
669        assert_eq!(
670            at(12),
671            Some(BaseValue {
672                value: value(700),
673                delta_px: 0
674            })
675        );
676
677        // A variation index is not a device table, so it adjusts no pixels.
678        let coords = [F2Dot14::from_f32(1.0)];
679        let varying = BaseInstance::with_coords(base(&buf), &coords);
680        assert_eq!(
681            varying.baseline_for_ppem(ROMN, BaseAxis::Horizontal, LATN, 10),
682            Some(BaseValue {
683                value: value(ROMN_DELTA),
684                delta_px: 0
685            })
686        );
687    }
688}