write_fonts/tables/
loca.rs

1//! The [loca (Index to Location)][loca] table
2//!
3//! [loca]: https://docs.microsoft.com/en-us/typography/opentype/spec/loca
4
5use read_fonts::TopLevelTable;
6use types::Tag;
7
8use crate::{
9    validate::{Validate, ValidationCtx},
10    FontWrite,
11};
12
13/// The [loca] table.
14///
15/// [loca]: https://docs.microsoft.com/en-us/typography/opentype/spec/loca
16#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub struct Loca {
19    // we just store u32, and then convert to u16 if needed in the `FontWrite` impl
20    pub(crate) offsets: Vec<u32>,
21    loca_format: LocaFormat,
22}
23
24/// Whether or not the 'loca' table uses short or long offsets.
25///
26/// This flag is stored in the 'head' table's [indexToLocFormat][locformat] field.
27/// See the ['loca' spec][spec] for more information.
28///
29/// [locformat]: super::head::Head::index_to_loc_format
30/// [spec]: https://learn.microsoft.com/en-us/typography/opentype/spec/loca
31#[repr(u8)]
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34pub enum LocaFormat {
35    #[default]
36    Short = 0,
37    Long = 1,
38}
39
40impl TopLevelTable for Loca {
41    const TAG: Tag = Tag::new(b"loca");
42}
43
44impl Loca {
45    /// Create a new loca table from 32-bit offsets.
46    ///
47    /// The loca format will be calculated based on the raw values.
48    ///
49    /// You generally do not construct this directly; it is constructed alongside
50    /// the corresponding 'glyf' table using the
51    /// [GlyfLocaBuilder](super::glyf::GlyfLocaBuilder).
52    pub fn new(offsets: Vec<u32>) -> Self {
53        let loca_format = LocaFormat::new(&offsets);
54
55        Loca {
56            offsets,
57            loca_format,
58        }
59    }
60
61    pub fn format(&self) -> LocaFormat {
62        self.loca_format
63    }
64}
65
66impl LocaFormat {
67    fn new(loca: &[u32]) -> LocaFormat {
68        // https://github.com/fonttools/fonttools/blob/1c283756a5e39d69459eea80ed12792adc4922dd/Lib/fontTools/ttLib/tables/_l_o_c_a.py#L37
69        const MAX_SHORT_LOCA_VALUE: u32 = 0x20000;
70        if loca.last().copied().unwrap_or_default() < MAX_SHORT_LOCA_VALUE
71            && loca.iter().all(|offset| offset % 2 == 0)
72        {
73            LocaFormat::Short
74        } else {
75            LocaFormat::Long
76        }
77    }
78}
79
80impl FontWrite for Loca {
81    fn write_into(&self, writer: &mut crate::TableWriter) {
82        match self.loca_format {
83            LocaFormat::Long => self.offsets.write_into(writer),
84            LocaFormat::Short => self
85                .offsets
86                .iter()
87                .for_each(|off| ((off >> 1) as u16).write_into(writer)),
88        }
89    }
90}
91
92impl Validate for Loca {
93    fn validate_impl(&self, _ctx: &mut ValidationCtx) {}
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn no_glyphs_is_short() {
102        assert_eq!(LocaFormat::Short, LocaFormat::new(&Vec::new()));
103    }
104
105    #[test]
106    fn some_glyphs_is_short() {
107        assert_eq!(LocaFormat::Short, LocaFormat::new(&[24, 48, 112]));
108    }
109
110    #[test]
111    fn unpadded_glyphs_is_long() {
112        assert_eq!(LocaFormat::Long, LocaFormat::new(&[24, 7, 112]));
113    }
114
115    #[test]
116    fn big_glyphs_is_long() {
117        assert_eq!(
118            LocaFormat::Long,
119            LocaFormat::new(&(0..=32).map(|i| i * 0x1000).collect::<Vec<_>>())
120        );
121    }
122}