Skip to main content

pfx_load/
font.rs

1#[derive(Clone, Debug, PartialEq, Eq)]
2pub struct Font {
3    pub bytes: Vec<u8>,
4    pub families: Vec<String>,
5}
6
7impl Font {
8    pub fn parse(bytes: &[u8]) -> Result<Self, String> {
9        if bytes.starts_with(b"ttcf") {
10            return Err("font collections are not supported".into());
11        }
12        let families = face_families(bytes, 0)?;
13        Ok(Font {
14            bytes: bytes.to_vec(),
15            families,
16        })
17    }
18}
19
20fn face_families(bytes: &[u8], directory: usize) -> Result<Vec<String>, String> {
21    if bytes.len() < directory + 12 {
22        return Err("font is too small".into());
23    }
24    let version = u32be(bytes, directory)?;
25    let truetype = version == 0x0001_0000;
26    let otf = version == u32::from_be_bytes(*b"OTTO");
27    let apple = version == u32::from_be_bytes(*b"true");
28    if !truetype && !otf && !apple {
29        return Err("not a font".into());
30    }
31    let tables = usize::from(u16be(bytes, directory + 4)?);
32    let mut name_at = None;
33    let mut name_len = None;
34    for index in 0..tables {
35        let record = directory + 12 + index * 16;
36        let tag = bytes.get(record..record + 4).ok_or("truncated font")?;
37        if tag == b"name" {
38            name_at = Some(u32be(bytes, record + 8)? as usize);
39            name_len = Some(u32be(bytes, record + 12)? as usize);
40        }
41    }
42    let (at, len) = name_at.zip(name_len).ok_or("font has no name table")?;
43    let table = bytes.get(at..at + len).ok_or("truncated name table")?;
44    families(table)
45}
46
47fn families(table: &[u8]) -> Result<Vec<String>, String> {
48    if table.len() < 6 {
49        return Err("truncated name table".into());
50    }
51    let count = usize::from(u16be(table, 2)?);
52    let strings = usize::from(u16be(table, 4)?);
53    let mut family = None;
54    let mut typographic = None;
55    for index in 0..count {
56        let record = 6 + index * 12;
57        if record + 12 > table.len() {
58            return Err("truncated name record".into());
59        }
60        let platform = u16be(table, record)?;
61        let encoding = u16be(table, record + 2)?;
62        let language = u16be(table, record + 4)?;
63        let name_id = u16be(table, record + 6)?;
64        let length = usize::from(u16be(table, record + 8)?);
65        let offset = usize::from(u16be(table, record + 10)?);
66        if name_id != 1 && name_id != 16 {
67            continue;
68        }
69        let Some(rank) = rank(platform, encoding, language) else {
70            continue;
71        };
72        let start = strings + offset;
73        let raw = table
74            .get(start..start + length)
75            .ok_or("truncated name string")?;
76        let Some(text) = decode_name(platform, encoding, raw) else {
77            continue;
78        };
79        if text.is_empty() {
80            continue;
81        }
82        let slot = if name_id == 16 {
83            &mut typographic
84        } else {
85            &mut family
86        };
87        let replace = slot.as_ref().is_none_or(|(best, _)| rank < *best);
88        if replace {
89            *slot = Some((rank, text));
90        }
91    }
92    let mut names = Vec::new();
93    if let Some((_, text)) = typographic {
94        names.push(text);
95    }
96    if let Some((_, text)) = family
97        && !names.iter().any(|name| name == &text)
98    {
99        names.push(text);
100    }
101    if names.is_empty() {
102        return Err("font has no family name".into());
103    }
104    Ok(names)
105}
106
107fn rank(platform: u16, encoding: u16, language: u16) -> Option<u8> {
108    match (platform, encoding) {
109        (3, 1 | 10) => Some(if language == 0x0409 { 0 } else { 2 }),
110        (0, _) => Some(if language == 0 || language == 0x0409 {
111            1
112        } else {
113            3
114        }),
115        (1, 0) => Some(if language == 0 { 4 } else { 5 }),
116        _ => None,
117    }
118}
119
120fn decode_name(platform: u16, encoding: u16, raw: &[u8]) -> Option<String> {
121    let text = match (platform, encoding) {
122        (0, _) | (3, 1 | 10) => {
123            if !raw.len().is_multiple_of(2) {
124                return None;
125            }
126            let units: Vec<u16> = raw
127                .chunks_exact(2)
128                .map(|pair| u16::from_be_bytes([pair[0], pair[1]]))
129                .collect();
130            String::from_utf16(&units).ok()?
131        }
132        (1, 0) => {
133            if raw.iter().any(|byte| *byte >= 128) {
134                return None;
135            }
136            String::from_utf8(raw.to_vec()).ok()?
137        }
138        _ => return None,
139    };
140    let text = text.trim_start_matches('\u{FEFF}').to_string();
141    (!text.is_empty()).then_some(text)
142}
143
144fn u16be(bytes: &[u8], at: usize) -> Result<u16, String> {
145    let pair = bytes.get(at..at + 2).ok_or("truncated font")?;
146    Ok(u16::from_be_bytes([pair[0], pair[1]]))
147}
148
149fn u32be(bytes: &[u8], at: usize) -> Result<u32, String> {
150    let word = bytes.get(at..at + 4).ok_or("truncated font")?;
151    Ok(u32::from_be_bytes([word[0], word[1], word[2], word[3]]))
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    fn push_u16(out: &mut Vec<u8>, value: u16) {
159        out.extend(value.to_be_bytes());
160    }
161
162    fn push_u32(out: &mut Vec<u8>, value: u32) {
163        out.extend(value.to_be_bytes());
164    }
165
166    fn font_with(platform: u16, encoding: u16, language: u16, name: &str) -> Vec<u8> {
167        let encoded: Vec<u8> = if platform == 1 {
168            name.bytes().collect()
169        } else {
170            name.encode_utf16()
171                .flat_map(|unit| unit.to_be_bytes())
172                .collect()
173        };
174        let mut name_table = Vec::new();
175        push_u16(&mut name_table, 0);
176        push_u16(&mut name_table, 1);
177        push_u16(&mut name_table, 18);
178        push_u16(&mut name_table, platform);
179        push_u16(&mut name_table, encoding);
180        push_u16(&mut name_table, language);
181        push_u16(&mut name_table, 1);
182        push_u16(&mut name_table, encoded.len() as u16);
183        push_u16(&mut name_table, 0);
184        name_table.extend(&encoded);
185        let mut font = Vec::new();
186        push_u32(&mut font, 0x0001_0000);
187        push_u16(&mut font, 1);
188        push_u16(&mut font, 16);
189        push_u16(&mut font, 0);
190        push_u16(&mut font, 0);
191        font.extend(b"name");
192        push_u32(&mut font, 0);
193        push_u32(&mut font, 28);
194        push_u32(&mut font, name_table.len() as u32);
195        font.extend(name_table);
196        font
197    }
198
199    #[test]
200    fn a_font_reports_its_typographic_family_first() {
201        let font = Font::parse(include_bytes!("../tests/data/family.ttf")).unwrap();
202        assert_eq!(
203            font.families,
204            vec!["Pito Text".to_string(), "Pito".to_string()]
205        );
206        assert_eq!(font.bytes, include_bytes!("../tests/data/family.ttf"));
207        assert!(Font::parse(b"nope").is_err());
208        let mac = Font::parse(&font_with(1, 0, 0, "Pito")).unwrap();
209        assert_eq!(mac.families, vec!["Pito".to_string()]);
210    }
211}