1mod bands;
7mod cff;
8mod encoding;
9mod full;
10mod subset;
11pub(crate) use encoding::{CharacterMap, Characters, GlyphCharacters};
12pub(crate) use subset::{FontProgram, SubsetOutput};
13
14use crate::fallible::reserve_exact;
15use crate::{Cancellation, Error, Limits, RangedSource, Result, read_exact_at};
16use sha2::Digest;
17use std::rc::Rc;
18use xberg_ttf_parser::{Face, RawFaceTables};
19
20const MAX_TABLES: usize = 128;
21pub const MAX_FONT_METADATA_BYTES: u64 = 1024 * 1024;
23const METADATA_TAGS: [[u8; 4]; 8] = [
24 *b"head", *b"hhea", *b"maxp", *b"cmap", *b"hmtx", *b"OS/2", *b"post", *b"name",
25];
26pub(crate) const CHANGED: &str = "font source changed after its metadata was read";
28
29pub(crate) fn has_code(bitmap: &[u8], code: usize) -> bool {
31 bitmap[code / 8] & (1 << (code % 8)) != 0
32}
33
34pub(crate) fn mark_code(bitmap: &mut [u8], code: usize) {
36 bitmap[code / 8] |= 1 << (code % 8);
37}
38
39const OUTLINE_TAGS: [[u8; 4]; 6] = [*b"glyf", *b"loca", *b"cvt ", *b"fpgm", *b"prep", *b"CFF "];
43const CFF: usize = 5;
44
45#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
47pub enum FontEmbedding {
48 #[default]
50 Subset,
51 Full,
54}
55
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub struct FontGlyph {
59 pub id: u16,
60 pub advance: u16,
61}
62
63fn permits_subset_embedding(version: u16, fs_type: u16) -> bool {
71 let licensing = fs_type & 0xf;
72 (licensing == 0 || licensing & 0xc != 0) && (version < 2 || fs_type & 0x300 == 0)
73}
74
75pub struct OpenTypeFont<'a, S> {
83 pub(super) source: &'a mut S,
84 face: u32,
85 header: [u8; 12],
87 directory: [[u8; 16]; MAX_TABLES],
88 table_count: usize,
89 face_base: u64,
90 collection: bool,
91 tables: [Vec<u8>; 8],
92 outlines: [Option<(u64, u64)>; 6],
94 cff: Option<Rc<cff::Cff>>,
96 subset_bytes_read: u64,
98}
99
100impl<'a, S: RangedSource> OpenTypeFont<'a, S> {
101 pub fn read<C: Cancellation>(
105 source: &'a mut S,
106 face: u32,
107 limits: &Limits,
108 cancellation: &C,
109 ) -> Result<Self> {
110 limits.validate()?;
111 limits.check_input_size(source.size())?;
112 let mut header = [0; 12];
113 read(source, 0, &mut header, limits, cancellation)?;
114 let (base, collection_end) = if let Some((faces, dsig)) = collection(&header)? {
116 if face >= faces {
117 return Err(invalid("TrueType collection face index is out of range"));
118 }
119 let mut offset = [0; 4];
120 read(
121 source,
122 12 + 4 * u64::from(face),
123 &mut offset,
124 limits,
125 cancellation,
126 )?;
127 let base = u64::from(u32::from_be_bytes(offset));
128 read(source, base, &mut header, limits, cancellation)?;
129 (base, 12 + 4 * u64::from(faces) + dsig)
130 } else if face != 0 {
131 return Err(invalid("a standalone font has only face 0"));
132 } else {
133 (0, 0)
134 };
135 if !is_font_tag(&header) {
136 return Err(invalid("font must be an OpenType font or collection face"));
137 }
138 let count = usize::from(u16::from_be_bytes([header[4], header[5]]));
139 if count == 0 || count > MAX_TABLES {
140 return Err(invalid("unsupported TrueType table count"));
141 }
142 let directory_end = base + 12 + 16 * count as u64;
143 let mut directory = [[0_u8; 16]; MAX_TABLES];
144 let mut metadata_bytes = 0_u64;
145 let mut outlines = [None; 6];
146 for i in 0..count {
147 read(
148 source,
149 base + 12 + 16 * i as u64,
150 &mut directory[i],
151 limits,
152 cancellation,
153 )?;
154 let entry = directory[i];
155 let tag = &entry[..4];
156 if i != 0 && directory[i - 1][..4] >= *tag {
157 return Err(invalid("TrueType table tags must be unique and sorted"));
158 }
159 if tag == b"fvar" || tag == b"CFF2" {
160 return Err(invalid("variable fonts are not supported"));
161 }
162 let (offset, length) = span(&entry);
163 if (offset < directory_end && base < offset + length.max(1))
166 || offset < collection_end
167 || offset + length > source.size()
168 {
169 return Err(invalid("TrueType table range is outside the source"));
170 }
171 for previous in &directory[..i] {
172 let (start, len) = span(previous);
173 if length != 0 && len != 0 && offset < start + len && start < offset + length {
174 return Err(invalid("TrueType table ranges overlap"));
175 }
176 }
177 if let Some(slot) = OUTLINE_TAGS.iter().position(|wanted| wanted == tag) {
178 outlines[slot] = Some((offset, length));
179 }
180 if METADATA_TAGS.iter().any(|wanted| wanted == tag) {
181 metadata_bytes += length;
182 }
183 }
184 let truetype = outlines[0].is_some() && outlines[1].is_some();
185 if truetype == outlines[CFF].is_some() {
186 return Err(invalid("font needs either glyf and loca or CFF outlines"));
187 }
188 if metadata_bytes > MAX_FONT_METADATA_BYTES {
189 return Err(Error::limit(
190 "font metadata bytes",
191 MAX_FONT_METADATA_BYTES,
192 metadata_bytes,
193 ));
194 }
195 limits.check_allocation(metadata_bytes)?;
196 let mut tables: [Vec<u8>; 8] = Default::default();
197 for (tag, table) in METADATA_TAGS.iter().zip(&mut tables) {
198 let entry = directory[..count]
199 .iter()
200 .find(|entry| entry[..4] == *tag)
201 .ok_or(invalid("required TrueType metadata table is missing"))?;
202 let (offset, length) = span(entry);
203 let refused = limits.allocation_refused("font metadata bytes", length);
204 reserve_exact(table, length as usize, refused)?;
205 table.resize(length as usize, 0);
206 read(source, offset, table, limits, cancellation)?;
207 }
208 let mut font = Self {
209 source,
210 face,
211 header,
212 directory,
213 table_count: count,
214 face_base: base,
215 collection: collection_end != 0,
216 tables,
217 outlines,
218 cff: None,
219 subset_bytes_read: 0,
220 };
221 let face = font.face()?;
222 if face.tables().cmap.is_none()
223 || face.tables().hmtx.is_none()
224 || face.tables().os2.is_none()
225 || face.tables().post.is_none()
226 {
227 return Err(invalid("invalid required TrueType metadata table"));
228 }
229 let maps = face
232 .tables()
233 .cmap
234 .as_ref()
235 .map_or(0, |cmap| cmap.subtables.len());
236 if maps > 16 {
237 return Err(Error::limit("font character maps", 16, u64::from(maps)));
238 }
239 let field = |at: usize| {
241 font.tables[5]
242 .get(at..at + 2)
243 .map(|bytes| u16::from_be_bytes([bytes[0], bytes[1]]))
244 };
245 let permitted = field(0)
246 .zip(field(8))
247 .is_some_and(|(version, fs_type)| permits_subset_embedding(version, fs_type));
248 if !permitted {
249 return Err(invalid("font metadata does not permit subset embedding"));
250 }
251 if let Some(span) = font.outlines[CFF] {
252 let glyphs = face.number_of_glyphs();
253 font.cff = Some(Rc::new(cff::Cff::read(
254 font.source,
255 span,
256 glyphs,
257 limits,
258 cancellation,
259 )?));
260 }
261 Ok(font)
262 }
263
264 pub fn face_count<C: Cancellation>(
268 source: &mut S,
269 limits: &Limits,
270 cancellation: &C,
271 ) -> Result<u32> {
272 limits.validate()?;
273 limits.check_input_size(source.size())?;
274 let mut header = [0; 12];
275 read(source, 0, &mut header, limits, cancellation)?;
276 match collection(&header)? {
277 Some((faces, _)) => Ok(faces),
278 None if is_font_tag(&header) => Ok(1),
279 None => Err(invalid("font must be an OpenType font or collection face")),
280 }
281 }
282
283 pub fn units_per_em(&self) -> Result<u16> {
284 Ok(self.face()?.units_per_em())
285 }
286
287 pub fn glyph(&self, character: char) -> Result<FontGlyph> {
289 let face = self.face()?;
290 let id = face
291 .glyph_index(character)
292 .filter(|id| id.0 != 0 && id.0 < face.number_of_glyphs())
293 .ok_or(invalid("font has no glyph for the requested character"))?;
294 let advance = face
295 .glyph_hor_advance(id)
296 .ok_or(invalid("font glyph has no horizontal advance"))?;
297 Ok(FontGlyph { id: id.0, advance })
298 }
299
300 pub(super) fn face(&self) -> Result<Face<'_>> {
301 Face::from_raw_tables(RawFaceTables {
302 head: &self.tables[0],
303 hhea: &self.tables[1],
304 maxp: &self.tables[2],
305 cmap: Some(&self.tables[3]),
306 hmtx: Some(&self.tables[4]),
307 os2: Some(&self.tables[5]),
308 post: Some(&self.tables[6]),
309 name: Some(&self.tables[7]),
310 ..Default::default()
311 })
312 .map_err(|_| invalid("invalid required TrueType face metadata"))
313 }
314
315 pub fn checksum_adjustment(&self) -> Result<u32> {
317 self.tables[0]
318 .get(8..12)
319 .map(|bytes| u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
320 .ok_or(invalid("invalid required TrueType face metadata"))
321 }
322
323 pub(crate) fn is_cff(&self) -> bool {
325 self.cff.is_some()
326 }
327
328 pub(crate) fn fingerprint(&self) -> [u8; 32] {
331 let mut hash = sha2::Sha256::new().chain_update(self.face.to_be_bytes());
332 hash.update(self.header);
333 for entry in &self.directory[..self.table_count] {
334 hash.update(entry);
335 }
336 for table in &self.tables {
337 hash.update((table.len() as u64).to_be_bytes());
338 hash.update(table);
339 }
340 for (offset, length) in self.outlines.iter().flatten() {
341 hash.update(offset.to_be_bytes());
342 hash.update(length.to_be_bytes());
343 }
344 hash.finalize().into()
345 }
346
347 pub(super) fn used_glyphs(
351 &self,
352 used: Characters<'_>,
353 limits: &Limits,
354 ) -> Result<Vec<(u16, u16)>> {
355 let face = self.face()?;
356 let count = face.number_of_glyphs();
357 let length = used.codes().count();
358 let bytes = length as u64 * size_of::<(u16, u16)>() as u64;
359 limits.check_allocation(bytes)?;
360 let mut glyphs = Vec::new();
361 reserve_exact(
362 &mut glyphs,
363 length,
364 limits.allocation_refused("font used glyphs", bytes),
365 )?;
366 for code in used.codes() {
367 let glyph = used
368 .glyph(usize::from(code))
369 .and_then(|character| face.glyph_index(character))
370 .filter(|id| id.0 != 0 && id.0 < count)
371 .map(|id| (id.0, code))
372 .ok_or(invalid(CHANGED))?;
373 glyphs.push(glyph);
374 }
375 Ok(glyphs)
376 }
377
378 pub fn postscript_name(&self) -> Result<String> {
381 let face = self.face()?;
382 let name = face
383 .names()
384 .into_iter()
385 .find(|name| name.name_id == 6 && name.is_unicode())
386 .ok_or(invalid("font requires a Unicode PostScript name"))?;
387 if name.name.is_empty() || name.name.len() > 126 || name.name.len() % 2 != 0 {
388 return Err(invalid(
389 "font PostScript name must have 1 to 63 ASCII characters",
390 ));
391 }
392 let name = name
393 .to_string()
394 .ok_or(invalid("font PostScript name is invalid UTF-16"))?;
395 if !is_valid_postscript_name(&name) {
396 return Err(invalid("font PostScript name contains invalid characters"));
397 }
398 Ok(name)
399 }
400}
401
402pub(crate) fn is_valid_postscript_name(name: &str) -> bool {
404 (1..=63).contains(&name.len())
405 && name
406 .bytes()
407 .all(|byte| (33..=126).contains(&byte) && !b"[](){}<>/%".contains(&byte))
408}
409
410fn collection(header: &[u8; 12]) -> Result<Option<(u32, u64)>> {
413 if header[..4] != *b"ttcf" {
414 return Ok(None);
415 }
416 let dsig = match u16::from_be_bytes([header[4], header[5]]) {
418 1 => 0,
419 2 => 12,
420 _ => return Err(invalid("unsupported TrueType collection version")),
421 };
422 Ok(Some((
423 u32::from_be_bytes(header[8..12].try_into().unwrap()),
424 dsig,
425 )))
426}
427
428fn is_font_tag(header: &[u8; 12]) -> bool {
430 header[..4] == [0, 1, 0, 0] || header[..4] == *b"true" || header[..4] == *b"OTTO"
431}
432
433fn span(entry: &[u8; 16]) -> (u64, u64) {
434 (
435 u64::from(u32::from_be_bytes(entry[8..12].try_into().unwrap())),
436 u64::from(u32::from_be_bytes(entry[12..16].try_into().unwrap())),
437 )
438}
439
440fn invalid(reason: &'static str) -> Error {
441 Error::invalid(reason)
442}
443
444fn read<S: RangedSource, C: Cancellation>(
445 source: &mut S,
446 offset: u64,
447 bytes: &mut [u8],
448 limits: &Limits,
449 cancellation: &C,
450) -> Result<()> {
451 for (index, chunk) in bytes.chunks_mut(limits.io_chunk_bytes).enumerate() {
452 read_exact_at(
453 source,
454 offset + (index * limits.io_chunk_bytes) as u64,
455 chunk,
456 limits,
457 cancellation,
458 )?;
459 }
460 Ok(())
461}
462
463#[cfg(test)]
464pub(crate) mod tests;