Skip to main content

ttf_view/tables/
hmtx.rs

1use crate::{
2    tables::{TableDirectoryRepr, cmap::GlyphId},
3    types::{FWORD, Tag, UFWORD, tags},
4};
5
6#[repr(C)]
7#[non_exhaustive]
8pub struct HmtxTableRepr {
9    /// Note: It's a little bit faster to work with `&[FWORD]` than with separately typed slices.
10    /// See [`HmtxTableHandle::metric`] method for explanation.
11    raw_words: [FWORD; 0],
12    // : h_metrics: [LongHorMetricRepr; hhea().num_h_metrics],
13    // : left_side_bearings: [FWORD; maxp().num_glyphs - hhea().num_h_metrics],
14}
15#[repr(C)]
16pub struct LongHorMetricRepr {
17    pub advance_width: UFWORD,
18    pub lsb: FWORD,
19}
20
21impl super::Table for HmtxTableRepr {
22    const TAG: Tag = tags::hmtx;
23    type Handle<'a> = HmtxTableHandle<'a>;
24}
25impl<'a> super::TableHandle<'a> for HmtxTableHandle<'a> {
26    fn in_directory(dir: &'a TableDirectoryRepr) -> Option<Self> {
27        let raw_words = dir.table_raw::<HmtxTableRepr>()?.raw_words.as_ptr();
28        let num_h_metrics = dir.hhea()?.number_of_h_metrics.get() as usize;
29        let num_glyphs = dir.maxp()?.num_glyphs.get() as usize;
30
31        let total_word_count = num_h_metrics + num_glyphs;
32        let raw_words = unsafe { std::slice::from_raw_parts(raw_words, total_word_count) };
33
34        Some(Self { raw_words, num_h_metrics })
35    }
36}
37
38// Note: HmtxTableRepr can't provide anything on its own. We need data from two other tables:
39// `number_of_h_metrics` from 'hhea' and `num_glyphs` from 'maxp' to slice the data correctly.
40#[derive(Copy)]
41#[derive_const(Clone)]
42pub struct HmtxTableHandle<'a> {
43    raw_words: &'a [FWORD],
44    num_h_metrics: usize,
45}
46
47#[derive(Debug, Copy, Hash)]
48#[derive_const(Clone, Default, PartialEq, Eq)]
49pub struct LongHorMetric {
50    pub aw: u16,
51    pub lsb: i16,
52}
53
54impl LongHorMetric {
55    pub const fn new(aw: u16, lsb: i16) -> Self {
56        Self { aw, lsb }
57    }
58}
59const impl From<&LongHorMetricRepr> for LongHorMetric {
60    fn from(value: &LongHorMetricRepr) -> Self {
61        Self { aw: value.advance_width.get(), lsb: value.lsb.get() }
62    }
63}
64
65impl<'a> HmtxTableHandle<'a> {
66    pub const fn num_h_metrics(&self) -> u16 {
67        self.num_h_metrics as u16
68    }
69    pub const fn num_glyphs(&self) -> u16 {
70        (self.raw_words.len() - self.num_h_metrics) as u16
71    }
72
73    const fn h_metrics(&self) -> &'a [LongHorMetricRepr] {
74        unsafe { std::slice::from_raw_parts(self.raw_words.as_ptr().cast(), self.num_h_metrics) }
75    }
76
77    pub const fn last_advance_width(&self) -> Option<u16> {
78        Some(self.h_metrics().last()?.advance_width.get())
79    }
80
81    pub const fn metric(&self, glyph_id: GlyphId) -> Option<LongHorMetric> {
82        /// Normally, you'd check if you need to access h_metrics() or lsbs(), and then either:
83        /// a) get both values from h_metrics(), or b) get lsb from lsbs(), and also maybe get
84        /// the advance from h_metrics().last(), with both of these operations involving bounds
85        /// checks. That's a total of 3 branches!
86        ///
87        /// But there's a way to combine 2 of them, leaving only 2 bounds checks:
88        ///
89        /// ```rs
90        /// idx <= hcount-1 {
91        ///     let min = idx;
92        ///     // (idx*2, idx*2+1)
93        ///     // (idx*2, idx+idx+1)
94        ///     (min*2, min+idx+1)
95        /// }
96        /// idx > hcount-1 {
97        ///     let min = hcount-1;
98        ///     // ((hcount-1)*2, (hcount*2)+(idx-hcount))
99        ///     // ((hcount-1)*2, (hcount-1)+idx+1)
100        ///     (min*2, min+idx+1)
101        /// }
102        ///
103        /// // No branching! 😎 (compiles to asm 'cmp, cmovge')
104        /// let min = idx.min(hcount-1);
105        /// (min*2, min+idx+1)
106        /// ```
107        ///
108        /// Now we only have 2 bounds checks: one to ensure the glyph is in range of this cmap,
109        /// and another checking if `hcount` is 0 - the only scenario in which `min` would be -1,
110        /// out of range. That, of course, would mean than `min` in `min+idx+1` is `-1` too, but
111        /// that's okay, - `min+1` would wrap around to 0, and all that'd remain would be `idx`.
112        ///
113        struct _CodeExplanation;
114
115        let idx: usize = glyph_id.into();
116        // Do the comparison as `isize`, to ensure that `-1` from `hcount-1` goes through to `min`
117        let min = (idx as isize).min(self.num_h_metrics.wrapping_sub(1) as isize) as usize;
118
119        Some(LongHorMetric {
120            // Do a bounds check on min+idx+1 to check if this glyph is even represented here
121            lsb: self.raw_words.get(min.wrapping_add(idx).wrapping_add(1))?.get(),
122
123            aw: {
124                if self.num_h_metrics != 0 {
125                    // Unless hcount is 0, min*2 is always in valid range
126                    unsafe { self.raw_words.get_unchecked(min.wrapping_mul(2)) }.get() as u16
127                } else {
128                    // Otherwise, return 0 as advance_width
129                    0
130                }
131            },
132        })
133    }
134
135    pub const fn iter(&self) -> Iter<'_> {
136        Iter::new(*self)
137    }
138}
139
140const impl<'a> IntoIterator for HmtxTableHandle<'a> {
141    type Item = (GlyphId, LongHorMetric);
142    type IntoIter = Iter<'a>;
143    fn into_iter(self) -> Self::IntoIter {
144        Iter::new(self)
145    }
146}
147const impl<'a> IntoIterator for &HmtxTableHandle<'a> {
148    type Item = (GlyphId, LongHorMetric);
149    type IntoIter = Iter<'a>;
150    fn into_iter(self) -> Self::IntoIter {
151        Iter::new(*self)
152    }
153}
154
155// TODO: When std::slice::Iter's Clone is constified, make the derive const
156#[derive(Clone)]
157pub struct Iter<'a> {
158    glyph_id: u16,
159    num_h_metrics: u16,
160    default_aw: u16,
161    raw_words: std::slice::Iter<'a, FWORD>,
162}
163
164impl<'a> Iter<'a> {
165    pub const fn new(hmtx: HmtxTableHandle<'a>) -> Self {
166        Self {
167            glyph_id: 0,
168            num_h_metrics: hmtx.num_h_metrics(),
169            default_aw: hmtx.last_advance_width().unwrap_or(0),
170            raw_words: hmtx.raw_words.iter(),
171        }
172    }
173}
174
175impl Iterator for Iter<'_> {
176    type Item = (GlyphId, LongHorMetric);
177
178    fn next(&mut self) -> Option<Self::Item> {
179        let aw = if self.glyph_id < self.num_h_metrics {
180            self.raw_words.next()?.get() as u16
181        } else {
182            self.default_aw
183        };
184        let lsb = self.raw_words.next()?.get();
185
186        let id = GlyphId::new(self.glyph_id);
187        self.glyph_id += 1;
188        Some((id, LongHorMetric::new(aw, lsb)))
189    }
190
191    fn size_hint(&self) -> (usize, Option<usize>) {
192        let len = self.len();
193        (len, Some(len))
194    }
195}
196impl ExactSizeIterator for Iter<'_> {
197    fn len(&self) -> usize {
198        let h_metrics_left = self.num_h_metrics.saturating_sub(self.glyph_id);
199        self.raw_words.len() - h_metrics_left as usize
200    }
201}
202impl std::iter::FusedIterator for Iter<'_> {}