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}