Coverage Report

Created: 2026-04-01 07:11

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/fontations/write-fonts/src/tables/loca.rs
Line
Count
Source
1
//! The [loca (Index to Location)][loca] table
2
//!
3
//! [loca]: https://docs.microsoft.com/en-us/typography/opentype/spec/loca
4
5
use read_fonts::TopLevelTable;
6
use types::Tag;
7
8
use 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))]
18
pub 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))]
34
pub enum LocaFormat {
35
    #[default]
36
    Short = 0,
37
    Long = 1,
38
}
39
40
impl TopLevelTable for Loca {
41
    const TAG: Tag = Tag::new(b"loca");
42
}
43
44
impl 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
0
    pub fn new(offsets: Vec<u32>) -> Self {
53
0
        let loca_format = LocaFormat::new(&offsets);
54
55
0
        Loca {
56
0
            offsets,
57
0
            loca_format,
58
0
        }
59
0
    }
60
61
0
    pub fn format(&self) -> LocaFormat {
62
0
        self.loca_format
63
0
    }
64
}
65
66
impl LocaFormat {
67
0
    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
0
        if loca.last().copied().unwrap_or_default() < MAX_SHORT_LOCA_VALUE
71
0
            && loca.iter().all(|offset| offset % 2 == 0)
72
        {
73
0
            LocaFormat::Short
74
        } else {
75
0
            LocaFormat::Long
76
        }
77
0
    }
78
}
79
80
impl FontWrite for Loca {
81
0
    fn write_into(&self, writer: &mut crate::TableWriter) {
82
0
        match self.loca_format {
83
0
            LocaFormat::Long => self.offsets.write_into(writer),
84
0
            LocaFormat::Short => self
85
0
                .offsets
86
0
                .iter()
87
0
                .for_each(|off| ((off >> 1) as u16).write_into(writer)),
88
        }
89
0
    }
90
}
91
92
impl Validate for Loca {
93
0
    fn validate_impl(&self, _ctx: &mut ValidationCtx) {}
94
}
95
96
#[cfg(test)]
97
mod 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
}