Skip to main content

extend_docx/doc/
fonts.rs

1//! Font table (SttbfFfn, [MS-DOC] 2.9.281): maps the ftc indices used by
2//! character sprms to font names.
3
4pub struct FontTable {
5    names: Vec<String>,
6}
7
8impl FontTable {
9    pub fn empty() -> FontTable {
10        FontTable { names: Vec::new() }
11    }
12
13    pub fn parse(sttbf_ffn: &[u8]) -> FontTable {
14        let mut names = Vec::new();
15        let mut pos;
16
17        let read_u16 = |bytes: &[u8], offset: usize| -> Option<u16> {
18            bytes
19                .get(offset..offset + 2)
20                .map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
21        };
22
23        let Some(first) = read_u16(sttbf_ffn, 0) else {
24            return FontTable { names };
25        };
26        let count;
27        if first == 0xFFFF {
28            count = read_u16(sttbf_ffn, 2).unwrap_or(0) as usize;
29            pos = 6; // marker + cData + cbExtra
30        } else {
31            count = first as usize;
32            pos = 4; // cData + cbExtra
33        }
34
35        for _ in 0..count {
36            let Some(&cb_ffn_m1) = sttbf_ffn.get(pos) else {
37                break;
38            };
39            let entry_end = pos + 1 + cb_ffn_m1 as usize;
40            let Some(entry) = sttbf_ffn.get(pos + 1..entry_end) else {
41                break;
42            };
43            // FFN fixed header after the size byte: detail flags (1), wWeight
44            // (2), chs (1), ixchSzAlt (1), panose (10), fs (24) = 39 bytes,
45            // then the UTF-16 null-terminated primary name.
46            const NAME_OFFSET: usize = 39;
47            let mut name = String::new();
48            let mut offset = NAME_OFFSET;
49            while offset + 2 <= entry.len() {
50                let unit = u16::from_le_bytes([entry[offset], entry[offset + 1]]);
51                if unit == 0 {
52                    break;
53                }
54                name.push_str(&String::from_utf16_lossy(&[unit]));
55                offset += 2;
56            }
57            names.push(name);
58            pos = entry_end;
59        }
60
61        FontTable { names }
62    }
63
64    pub fn name(&self, ftc: u16) -> Option<String> {
65        self.names
66            .get(ftc as usize)
67            .filter(|name| !name.is_empty())
68            .cloned()
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    fn ffn_entry(name: &str) -> Vec<u8> {
77        let mut body = vec![0u8; 39];
78        for unit in name.encode_utf16() {
79            body.extend_from_slice(&unit.to_le_bytes());
80        }
81        body.extend_from_slice(&0u16.to_le_bytes());
82        let mut entry = vec![body.len() as u8];
83        entry.extend_from_slice(&body);
84        entry
85    }
86
87    #[test]
88    fn parses_font_names() {
89        let mut sttbf = Vec::new();
90        sttbf.extend_from_slice(&0xFFFFu16.to_le_bytes());
91        sttbf.extend_from_slice(&2u16.to_le_bytes());
92        sttbf.extend_from_slice(&0u16.to_le_bytes());
93        sttbf.extend_from_slice(&ffn_entry("Times New Roman"));
94        sttbf.extend_from_slice(&ffn_entry("Arial"));
95
96        let table = FontTable::parse(&sttbf);
97        assert_eq!(table.name(0).as_deref(), Some("Times New Roman"));
98        assert_eq!(table.name(1).as_deref(), Some("Arial"));
99        assert_eq!(table.name(9), None);
100    }
101}