Skip to main content

read_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 crate::{
6    read::{FontRead, ReadArgs},
7    table_provider::TopLevelTable,
8    FontData, ReadError,
9};
10use types::{BigEndian, GlyphId, Tag};
11
12/// The [loca] table.
13///
14/// [loca]: https://docs.microsoft.com/en-us/typography/opentype/spec/loca
15#[derive(Clone)]
16pub enum Loca<'a> {
17    Short(&'a [BigEndian<u16>]),
18    Long(&'a [BigEndian<u32>]),
19}
20
21impl TopLevelTable for Loca<'_> {
22    const TAG: Tag = Tag::new(b"loca");
23}
24
25impl<'a> Loca<'a> {
26    pub fn read(data: FontData<'a>, is_long: bool) -> Result<Self, ReadError> {
27        Self::read_with_args(data, is_long)
28    }
29
30    pub fn len(&self) -> usize {
31        match self {
32            Loca::Short(data) => data.len().saturating_sub(1),
33            Loca::Long(data) => data.len().saturating_sub(1),
34        }
35    }
36
37    pub fn is_empty(&self) -> bool {
38        self.len() == 0
39    }
40
41    pub fn all_offsets_are_ascending(&self) -> bool {
42        match self {
43            Loca::Short(data) => !data
44                .iter()
45                .zip(data.iter().skip(1))
46                .any(|(start, end)| start > end),
47            Loca::Long(data) => !data
48                .iter()
49                .zip(data.iter().skip(1))
50                .any(|(start, end)| start > end),
51        }
52    }
53
54    /// Attempt to return the offset for a given glyph id.
55    pub fn get_raw(&self, idx: usize) -> Option<u32> {
56        match self {
57            Loca::Short(data) => data.get(idx).map(|x| x.get() as u32 * 2),
58            Loca::Long(data) => data.get(idx).map(|x| x.get()),
59        }
60    }
61
62    /// What this table says about a glyph, or `None` if it says nothing
63    /// readable.
64    pub fn get(&self, gid: GlyphId, glyf: &super::glyf::Glyf<'a>) -> Option<LocaGlyph<'a>> {
65        let idx = gid.to_u32() as usize;
66        let start = self.get_raw(idx)?;
67        let end = self.get_raw(idx + 1)?;
68        if start == end {
69            return Some(LocaGlyph::Empty);
70        }
71        let data = glyf.offset_data().slice(start as usize..end as usize)?;
72        super::glyf::Glyph::read(data).ok().map(LocaGlyph::Glyph)
73    }
74
75    /// The outline for a glyph, `Ok(None)` where the glyph is empty, and an
76    /// error where this table says nothing readable about it.
77    ///
78    /// Prefer [`get`][Self::get], which names the empty glyph instead of
79    /// spelling it as an inner `None`. This reports every unreadable glyph as
80    /// [`ReadError::OutOfBounds`], whatever the reason.
81    #[doc(hidden)]
82    pub fn get_glyf(
83        &self,
84        gid: GlyphId,
85        glyf: &super::glyf::Glyf<'a>,
86    ) -> Result<Option<super::glyf::Glyph<'a>>, ReadError> {
87        self.get(gid, glyf)
88            .map(LocaGlyph::into_glyph)
89            .ok_or(ReadError::OutOfBounds)
90    }
91}
92
93/// What a glyph's entry in the `loca` table leads to.
94///
95/// A glyph with no contours — a space, say — is described by an empty range,
96/// and finding one is a success. It is separate from the glyph being
97/// unreadable, which [`Loca::get`] reports by answering `None`.
98#[derive(Clone)]
99pub enum LocaGlyph<'a> {
100    /// The glyph has no outline.
101    Empty,
102    /// The glyph's outline.
103    Glyph(super::glyf::Glyph<'a>),
104}
105
106impl<'a> LocaGlyph<'a> {
107    /// The outline, or `None` where the glyph is empty.
108    pub fn glyph(&self) -> Option<&super::glyf::Glyph<'a>> {
109        match self {
110            Self::Empty => None,
111            Self::Glyph(glyph) => Some(glyph),
112        }
113    }
114
115    /// The outline by value, or `None` where the glyph is empty.
116    pub fn into_glyph(self) -> Option<super::glyf::Glyph<'a>> {
117        match self {
118            Self::Empty => None,
119            Self::Glyph(glyph) => Some(glyph),
120        }
121    }
122}
123
124impl ReadArgs for Loca<'_> {
125    type Args = bool;
126}
127
128impl<'a> FontRead<'a> for Loca<'a> {
129    fn read_with_args(data: FontData<'a>, args: Self::Args) -> Result<Self, ReadError> {
130        let is_long = args;
131        if is_long {
132            data.read_array(0..data.len()).map(Loca::Long)
133        } else {
134            data.read_array(0..data.len()).map(Loca::Short)
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use font_test_data::bebuffer::BeBuffer;
142    use types::Scalar;
143
144    use super::Loca;
145
146    fn to_loca_bytes<T: Scalar + Copy>(values: &[T]) -> (BeBuffer, bool) {
147        let value_num_bytes = std::mem::size_of::<T>();
148        let is_long = if value_num_bytes == 2 {
149            false
150        } else if value_num_bytes == 4 {
151            true
152        } else {
153            panic!("invalid integer type must be u32 or u16")
154        };
155        let mut buffer = BeBuffer::default();
156
157        for v in values {
158            buffer = buffer.push(*v);
159        }
160
161        (buffer, is_long)
162    }
163
164    fn check_loca_sorting(values: &[u16], is_sorted: bool) {
165        let (bytes, is_long) = to_loca_bytes(values);
166        let loca = Loca::read(bytes.data().into(), is_long).unwrap();
167        assert_eq!(loca.all_offsets_are_ascending(), is_sorted);
168
169        let u32_values: Vec<u32> = values.iter().map(|v| *v as u32).collect();
170        let (bytes, is_long) = to_loca_bytes(&u32_values);
171        let loca = Loca::read(bytes.data().into(), is_long).unwrap();
172        assert_eq!(loca.all_offsets_are_ascending(), is_sorted);
173    }
174
175    #[test]
176    fn all_offsets_are_ascending() {
177        // Sorted
178        let empty: &[u16] = &[];
179        check_loca_sorting(empty, true);
180        check_loca_sorting(&[0], true);
181        check_loca_sorting(&[0, 0], true);
182        check_loca_sorting(&[0, 1], true);
183        check_loca_sorting(&[1, 2, 2, 3, 7], true);
184
185        // Unsorted
186        check_loca_sorting(&[1, 0], false);
187        check_loca_sorting(&[1, 3, 2], false);
188        check_loca_sorting(&[2, 1, 3], false);
189        check_loca_sorting(&[1, 2, 3, 2, 7], false);
190    }
191}