read_fonts/tables/
hdmx.rs

1//! The [Horizontal Device Metrics](https://learn.microsoft.com/en-us/typography/opentype/spec/hdmx) table.
2
3include!("../../generated/generated_hdmx.rs");
4
5use std::cmp::Ordering;
6
7impl<'a> Hdmx<'a> {
8    /// Returns for the device record that exactly matches the given
9    /// size (as ppem).
10    pub fn record_for_size(&self, size: u8) -> Option<DeviceRecord<'a>> {
11        let records = self.records();
12        // Need a custom binary search because we're working with
13        // ComputedArray
14        let mut lo = 0;
15        let mut hi = records.len();
16        while lo < hi {
17            let mid = (lo + hi) / 2;
18            let record = records.get(mid).ok()?;
19            match record.pixel_size.cmp(&size) {
20                Ordering::Less => lo = mid + 1,
21                Ordering::Greater => hi = mid,
22                Ordering::Equal => return Some(record),
23            }
24        }
25        None
26    }
27}
28
29#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
30pub struct DeviceRecord<'a> {
31    /// Pixel size for following widths (as ppem).
32    pub pixel_size: u8,
33    /// Maximum width.
34    pub max_width: u8,
35    /// Array of glyphs (numGlyphs is from the 'maxp' table).
36    pub widths: &'a [u8],
37}
38
39impl<'a> DeviceRecord<'a> {
40    /// Pixel size for following widths (as ppem).
41    pub fn pixel_size(&self) -> u8 {
42        self.pixel_size
43    }
44
45    /// Maximum width.
46    pub fn max_width(&self) -> u8 {
47        self.max_width
48    }
49
50    /// Array of widths, indexed by glyph id.
51    pub fn widths(&self) -> &'a [u8] {
52        self.widths
53    }
54}
55
56impl ReadArgs for DeviceRecord<'_> {
57    type Args = (u16, u32);
58}
59
60impl ComputeSize for DeviceRecord<'_> {
61    fn compute_size(args: &(u16, u32)) -> Result<usize, ReadError> {
62        let (_num_glyphs, size_device_record) = *args;
63        // Record size is explicitly defined in the parent hdmx table
64        Ok(size_device_record as usize)
65    }
66}
67
68impl<'a> FontReadWithArgs<'a> for DeviceRecord<'a> {
69    fn read_with_args(data: FontData<'a>, args: &(u16, u32)) -> Result<Self, ReadError> {
70        let mut cursor = data.cursor();
71        let (num_glyphs, _size_device_record) = *args;
72        Ok(Self {
73            pixel_size: cursor.read()?,
74            max_width: cursor.read()?,
75            widths: cursor.read_array(num_glyphs as usize)?,
76        })
77    }
78}
79
80#[allow(clippy::needless_lifetimes)]
81impl<'a> DeviceRecord<'a> {
82    /// A constructor that requires additional arguments.
83    ///
84    /// This type requires some external state in order to be
85    /// parsed.
86    pub fn read(
87        data: FontData<'a>,
88        num_glyphs: u16,
89        size_device_record: u32,
90    ) -> Result<Self, ReadError> {
91        let args = (num_glyphs, size_device_record);
92        Self::read_with_args(data, &args)
93    }
94}
95
96#[cfg(feature = "experimental_traverse")]
97impl<'a> SomeRecord<'a> for DeviceRecord<'a> {
98    fn traverse(self, data: FontData<'a>) -> RecordResolver<'a> {
99        RecordResolver {
100            name: "DeviceRecord",
101            get_field: Box::new(move |idx, _data| match idx {
102                0usize => Some(Field::new("pixel_size", self.pixel_size())),
103                1usize => Some(Field::new("max_width", self.max_width())),
104                2usize => Some(Field::new("widths", self.widths())),
105                _ => None,
106            }),
107            data,
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use font_test_data::{be_buffer, bebuffer::BeBuffer};
116
117    #[test]
118    fn read_hdmx() {
119        let buf = make_hdmx();
120        let hdmx = Hdmx::read(buf.data().into(), 3).unwrap();
121        assert_eq!(hdmx.version(), 0);
122        assert_eq!(hdmx.num_records(), 3);
123        // Note: this table has sizes for 3 glyphs making each device
124        // record 5 bytes in actual length, but each entry in the array
125        // should be aligned to 32-bits so the size is bumped to 8 bytes
126        //
127        // See the HdmxHeader::sizeDeviceRecord field at
128        // <https://learn.microsoft.com/en-us/typography/opentype/spec/hdmx>
129        // "Size of a device record, 32-bit aligned."
130        assert_eq!(hdmx.size_device_record(), 8);
131        let records = hdmx
132            .records()
133            .iter()
134            .map(|rec| rec.unwrap())
135            .collect::<Vec<_>>();
136        assert_eq!(records.len(), 3);
137        let expected_records = [
138            DeviceRecord {
139                pixel_size: 8,
140                max_width: 13,
141                widths: &[10, 12, 13],
142            },
143            DeviceRecord {
144                pixel_size: 16,
145                max_width: 21,
146                widths: &[18, 20, 21],
147            },
148            DeviceRecord {
149                pixel_size: 32,
150                max_width: 52,
151                widths: &[38, 40, 52],
152            },
153        ];
154        assert_eq!(records, expected_records);
155    }
156
157    #[test]
158    fn find_by_size() {
159        let buf = make_hdmx();
160        let hdmx = Hdmx::read(buf.data().into(), 3).unwrap();
161        assert_eq!(hdmx.record_for_size(8).unwrap().pixel_size, 8);
162        assert_eq!(hdmx.record_for_size(16).unwrap().pixel_size, 16);
163        assert_eq!(hdmx.record_for_size(32).unwrap().pixel_size, 32);
164        assert!(hdmx.record_for_size(7).is_none());
165        assert!(hdmx.record_for_size(20).is_none());
166        assert!(hdmx.record_for_size(72).is_none());
167    }
168
169    fn make_hdmx() -> BeBuffer {
170        be_buffer! {
171            0u16,           // version
172            3u16,           // num_records
173            8u32,           // size_device_record
174            // 3 records [pixel_size, max_width, width0, width1, ..padding]
175            [8u8, 13, 10, 12, 13, 0, 0, 0],
176            [16u8, 21, 18, 20, 21, 0, 0, 0],
177            [32u8, 52, 38, 40, 52, 0, 0, 0]
178        }
179    }
180}