1use crate::{
9 tables::{name::Name, os2::SelectionFlags},
10 types::{BigEndian, NameId},
11 TableProvider,
12};
13
14pub fn is_hint_reliant<'a>(font: &impl TableProvider<'a>) -> bool {
24 if font.glyf().is_err() && font.glyf_extended().is_err() {
25 return false;
26 }
27 is_hint_reliant_name(font) || matches_hint_reliant_id_list(FontId::from_font(font))
28}
29
30fn is_hint_reliant_name<'a>(font: &impl TableProvider<'a>) -> bool {
33 let Ok(name) = font.name() else {
34 return false;
35 };
36 let wws = font
41 .os2()
42 .is_ok_and(|os2| os2.fs_selection().contains(SelectionFlags::WWS));
43 let ids: &[NameId] = if wws {
44 &[NameId::TYPOGRAPHIC_FAMILY_NAME, NameId::FAMILY_NAME]
45 } else {
46 &[
47 NameId::WWS_FAMILY_NAME,
48 NameId::TYPOGRAPHIC_FAMILY_NAME,
49 NameId::FAMILY_NAME,
50 ]
51 };
52 ids.iter()
53 .find_map(|id| AsciiName::read(&name, *id))
54 .is_some_and(|family| matches_hint_reliant_name_list(family.bytes()))
55}
56
57struct AsciiName<'a> {
61 data: &'a [u8],
62 utf16: bool,
63}
64
65impl<'a> AsciiName<'a> {
66 fn read(name: &Name<'a>, id: NameId) -> Option<Self> {
72 let (mut apple_english, mut apple_roman, mut windows, mut unicode) =
73 (None, None, None, None);
74 let mut windows_is_english = false;
75 let records = name.name_record().iter();
76 for record in records.filter(|record| record.name_id() == id && record.length() > 0) {
77 match record.platform_id() {
78 0 | 2 => unicode = Some(record),
79 1 if record.language_id() == 0 => apple_english = Some(record),
80 1 if record.encoding_id() == 0 => apple_roman = Some(record),
81 3 => {
82 let english = record.language_id() & 0x3ff == 0x009;
83 if (windows.is_none() || english) && matches!(record.encoding_id(), 0 | 1 | 10)
84 {
85 windows = Some(record);
86 windows_is_english = english;
87 }
88 }
89 _ => {}
90 }
91 }
92 let apple = apple_english.or(apple_roman);
93 let (record, utf16) = match (windows, apple) {
94 (Some(windows), _) if apple.is_none() || windows_is_english => (windows, true),
95 (_, Some(apple)) => (apple, false),
96 _ => (unicode?, true),
97 };
98 let start = record.string_offset().to_u32() as usize;
99 let data = name
100 .string_data()
101 .as_bytes()
102 .get(start..start + record.length() as usize)?;
103 Some(Self { data, utf16 })
104 }
105
106 fn bytes(&self) -> impl Iterator<Item = u8> + '_ {
109 let step = if self.utf16 { 2 } else { 1 };
110 self.data
111 .chunks_exact(step)
112 .map(|unit| {
113 unit.iter()
114 .fold(0u32, |code, byte| code << 8 | *byte as u32)
115 })
116 .take_while(|code| *code != 0)
117 .map(|code| {
118 if (32..=127).contains(&code) {
119 code as u8
120 } else {
121 b'?'
122 }
123 })
124 }
125}
126
127fn matches_hint_reliant_name_list(mut name: impl Iterator<Item = u8>) -> bool {
135 let mut head = [0u8; 8];
136 let mut head_len = 0;
137 for byte in name.by_ref().take(head.len()) {
138 head[head_len] = byte;
139 head_len += 1;
140 }
141 let tagged =
143 head_len == head.len() && head[6] == b'+' && head[..6].iter().all(u8::is_ascii_uppercase);
144 let skip = if tagged { 7 } else { 0 };
145 let mut window = [0u8; MAX_HINT_RELIANT_NAME_LEN];
147 let mut seen = 0;
148 head[skip..head_len]
149 .iter()
150 .copied()
151 .chain(name)
152 .any(|byte| {
153 window.copy_within(1.., 0);
154 window[MAX_HINT_RELIANT_NAME_LEN - 1] = byte;
155 seen += 1;
156 HINT_RELIANT_NAMES
157 .iter()
158 .any(|tricky| tricky.len() <= seen && window.ends_with(tricky.as_bytes()))
159 })
160}
161
162#[rustfmt::skip]
164const HINT_RELIANT_NAMES: &[&str] = &[
165 "cpop", "DFGirl-W6-WIN-BF", "DFGothic-EB", "DFGyoSho-Lt", "DFHei", "DFHSGothic-W5", "DFHSMincho-W3", "DFHSMincho-W7", "DFKaiSho-SB", "DFKaiShu", "DFKai-SB", "DFMing", "DLC", "HuaTianKaiTi?", "HuaTianSongTi?", "Ming(for ISO10646)", "MingLiU", "MingMedium", "PMingLiU", "MingLi43", ];
201
202const MAX_HINT_RELIANT_NAME_LEN: usize = 18;
203
204#[derive(Copy, Clone, PartialEq, Default, Debug)]
205struct TableId {
206 checksum: u32,
207 len: u32,
208}
209
210impl TableId {
211 fn from_data(data: Option<&[u8]>) -> Self {
212 let Some(data) = data else {
213 return Self::default();
214 };
215 Self {
216 checksum: crate::tables::compute_checksum(data),
219 len: data.len() as u32,
220 }
221 }
222
223 fn from_cvt(cvt: Option<&[BigEndian<i16>]>) -> Self {
229 let Some(cvt) = cvt else {
230 return Self::default();
231 };
232 let mut checksum = 0u32;
233 for pair in cvt.chunks(2) {
234 let high = pair[0].get() as u16 as u32;
235 let low = pair.get(1).map_or(0, |value| value.get() as u16 as u32);
236 checksum = checksum.wrapping_add((high << 16) | low);
237 }
238 Self {
239 checksum,
240 len: (cvt.len() * 2) as u32,
241 }
242 }
243}
244
245#[derive(Copy, Clone, PartialEq, Default, Debug)]
246struct FontId {
247 cvt: TableId,
248 fpgm: TableId,
249 prep: TableId,
250}
251
252impl FontId {
253 fn from_font<'a>(font: &impl TableProvider<'a>) -> Self {
254 Self {
255 cvt: TableId::from_cvt(font.cvt().ok()),
256 fpgm: TableId::from_data(font.fpgm().ok()),
257 prep: TableId::from_data(font.prep().ok()),
258 }
259 }
260}
261
262fn matches_hint_reliant_id_list(font_id: FontId) -> bool {
267 HINT_RELIANT_IDS.contains(&font_id)
268}
269
270#[rustfmt::skip]
272const HINT_RELIANT_IDS: &[FontId] = &[
273 FontId {
275 cvt: TableId { checksum: 0x05BCF058, len: 0x000002E4 },
276 fpgm: TableId { checksum: 0x28233BF1, len: 0x000087C4 },
277 prep: TableId { checksum: 0xA344A1EA, len: 0x000001E1 },
278 },
279 FontId {
281 cvt: TableId { checksum: 0x05BCF058, len: 0x000002E4 },
282 fpgm: TableId { checksum: 0x28233BF1, len: 0x000087C4 },
283 prep: TableId { checksum: 0xA344A1EB, len: 0x000001E1 },
284 },
285 FontId {
287 cvt: TableId { checksum: 0x12C3EBB2, len: 0x00000350 },
288 fpgm: TableId { checksum: 0xB680EE64, len: 0x000087A7 },
289 prep: TableId { checksum: 0xCE939563, len: 0x00000758 },
290 },
291 FontId {
293 cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000350 },
294 fpgm: TableId { checksum: 0xCE5956E9, len: 0x0000BC85 },
295 prep: TableId { checksum: 0x8272F416, len: 0x00000045 },
296 },
297 FontId {
299 cvt: TableId { checksum: 0x1257EB46, len: 0x00000350 },
300 fpgm: TableId { checksum: 0xF699D160, len: 0x0000715F },
301 prep: TableId { checksum: 0xD222F568, len: 0x000003BC },
302 },
303 FontId {
305 cvt: TableId { checksum: 0x1262EB4E, len: 0x00000350 },
306 fpgm: TableId { checksum: 0xE86A5D64, len: 0x00007940 },
307 prep: TableId { checksum: 0x7850F729, len: 0x000005FF },
308 },
309 FontId {
311 cvt: TableId { checksum: 0x122DEB0A, len: 0x00000350 },
312 fpgm: TableId { checksum: 0x3D16328A, len: 0x0000859B },
313 prep: TableId { checksum: 0xA93FC33B, len: 0x000002CB },
314 },
315 FontId {
317 cvt: TableId { checksum: 0x125FEB26, len: 0x00000350 },
318 fpgm: TableId { checksum: 0xA5ACC982, len: 0x00007EE1 },
319 prep: TableId { checksum: 0x90999196, len: 0x0000041F },
320 },
321 FontId {
323 cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000350 },
324 fpgm: TableId { checksum: 0x5A30CA3B, len: 0x00009063 },
325 prep: TableId { checksum: 0x13A42602, len: 0x0000007E },
326 },
327 FontId {
329 cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000350 },
330 fpgm: TableId { checksum: 0xA6E78C01, len: 0x00008998 },
331 prep: TableId { checksum: 0x13A42602, len: 0x0000007E },
332 },
333 FontId {
335 cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000360 },
336 fpgm: TableId { checksum: 0x9DB282B2, len: 0x0000C06E },
337 prep: TableId { checksum: 0x53E6D7CA, len: 0x00000082 },
338 },
339 FontId {
341 cvt: TableId { checksum: 0x1243EB18, len: 0x00000350 },
342 fpgm: TableId { checksum: 0xBA0A8C30, len: 0x000074AD },
343 prep: TableId { checksum: 0xF3D83409, len: 0x0000037B },
344 },
345 FontId {
347 cvt: TableId { checksum: 0x07DCF546, len: 0x00000308 },
348 fpgm: TableId { checksum: 0x40FE7C90, len: 0x00008E2A },
349 prep: TableId { checksum: 0x608174B5, len: 0x0000007A },
350 },
351 FontId {
353 cvt: TableId { checksum: 0xEB891238, len: 0x00000308 },
354 fpgm: TableId { checksum: 0xD2E4DCD4, len: 0x0000676F },
355 prep: TableId { checksum: 0x8EA5F293, len: 0x000003B8 },
356 },
357 FontId {
359 cvt: TableId { checksum: 0xFFFBFFFC, len: 0x00000008 },
360 fpgm: TableId { checksum: 0x9C9E48B8, len: 0x0000BEA2 },
361 prep: TableId { checksum: 0x70020112, len: 0x00000008 },
362 },
363 FontId {
365 cvt: TableId { checksum: 0xFFFBFFFC, len: 0x00000008 },
366 fpgm: TableId { checksum: 0x0A5A0483, len: 0x00017C39 },
367 prep: TableId { checksum: 0x70020112, len: 0x00000008 },
368 },
369 FontId {
371 cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
372 fpgm: TableId { checksum: 0x40C92555, len: 0x000000E5 },
373 prep: TableId { checksum: 0xA39B58E3, len: 0x0000117C },
374 },
375 FontId {
377 cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
378 fpgm: TableId { checksum: 0x33C41652, len: 0x000000E5 },
379 prep: TableId { checksum: 0x26D6C52A, len: 0x00000F6A },
380 },
381 FontId {
383 cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
384 fpgm: TableId { checksum: 0x6DB1651D, len: 0x0000019D },
385 prep: TableId { checksum: 0x6C6E4B03, len: 0x00002492 },
386 },
387 FontId {
389 cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
390 fpgm: TableId { checksum: 0x40C92555, len: 0x000000E5 },
391 prep: TableId { checksum: 0xDE51FAD0, len: 0x0000117C },
392 },
393 FontId {
395 cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
396 fpgm: TableId { checksum: 0x85E47664, len: 0x000000E5 },
397 prep: TableId { checksum: 0xA6C62831, len: 0x00001CAA },
398 },
399 FontId {
401 cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
402 fpgm: TableId { checksum: 0x2D891CFD, len: 0x0000019D },
403 prep: TableId { checksum: 0xA0604633, len: 0x00001DE8 },
404 },
405 FontId {
407 cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
408 fpgm: TableId { checksum: 0x40AA774C, len: 0x000001CB },
409 prep: TableId { checksum: 0x9B5CAA96, len: 0x00001F9A },
410 },
411 FontId {
413 cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
414 fpgm: TableId { checksum: 0x0D3DE9CB, len: 0x00000141 },
415 prep: TableId { checksum: 0xD4127766, len: 0x00002280 },
416 },
417 FontId {
419 cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
420 fpgm: TableId { checksum: 0x4A692698, len: 0x000001F0 },
421 prep: TableId { checksum: 0x340D4346, len: 0x00001FCA },
422 },
423 FontId {
425 cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
426 fpgm: TableId { checksum: 0xCD34C604, len: 0x00000166 },
427 prep: TableId { checksum: 0x6CF31046, len: 0x000022B0 },
428 },
429 FontId {
431 cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
432 fpgm: TableId { checksum: 0x5DA75315, len: 0x0000019D },
433 prep: TableId { checksum: 0x40745A5F, len: 0x000022E0 },
434 },
435 FontId {
437 cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
438 fpgm: TableId { checksum: 0xF055FC48, len: 0x000001C2 },
439 prep: TableId { checksum: 0x3900DED3, len: 0x00001E18 },
440 },
441 FontId {
443 cvt: TableId { checksum: 0x00170003, len: 0x00000060 },
444 fpgm: TableId { checksum: 0xDBB4306E, len: 0x000058AA },
445 prep: TableId { checksum: 0xD643482A, len: 0x00000035 },
446 },
447 FontId {
449 cvt: TableId { checksum: 0x1269EB58, len: 0x00000350 },
450 fpgm: TableId { checksum: 0x5CD5957A, len: 0x00006A4E },
451 prep: TableId { checksum: 0xF758323A, len: 0x00000380 },
452 },
453 FontId {
455 cvt: TableId { checksum: 0x122FEB0B, len: 0x00000350 },
456 fpgm: TableId { checksum: 0x7F10919A, len: 0x000070A9 },
457 prep: TableId { checksum: 0x7CD7E7B7, len: 0x0000025C },
458 },
459];
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use crate::{FontRef, TableProvider};
465
466 #[test]
469 fn cvt_identity_matches_the_byte_checksum() {
470 for data in [font_test_data::TINOS_SUBSET, font_test_data::CVAR] {
471 let font = FontRef::new(data).unwrap();
472 let cvt = font.cvt().unwrap();
473 let bytes = font.data_for_tag(crate::types::Tag::new(b"cvt ")).unwrap();
474 assert_eq!(
475 TableId::from_cvt(Some(cvt)),
476 TableId::from_data(Some(bytes.as_bytes()))
477 );
478 }
479 let cvt = [0x0102i16, 0x0304, 0x0506].map(BigEndian::from);
481 assert_eq!(
482 TableId::from_cvt(Some(&cvt)),
483 TableId::from_data(Some(&[1, 2, 3, 4, 5, 6]))
484 );
485 }
486
487 #[test]
489 fn font_model_agrees_with_font_ref() {
490 for data in [font_test_data::TINOS_SUBSET, font_test_data::VAZIRMATN_VAR] {
491 let font = FontRef::new(data).unwrap();
492 let model = crate::model::Font::new(data.to_vec(), 0).unwrap();
493 assert_eq!(model.is_hint_reliant(), is_hint_reliant(&font));
494 assert_eq!(is_hint_reliant(&model.tables()), is_hint_reliant(&font));
495 }
496 }
497
498 #[test]
500 fn uppercase_glyf_fonts_qualify() {
501 let data = uppercase_glyf(font_test_data::TINOS_SUBSET);
502 assert!(FontRef::new(&data).unwrap().glyf().is_err());
503 assert!(with_names(&data, &[(3, 0x409, 1, "MingLiU")]));
504 }
505
506 fn uppercase_glyf(data: &[u8]) -> Vec<u8> {
508 let count = u16::from_be_bytes([data[4], data[5]]) as usize;
509 let mut records: Vec<[u8; 16]> = data[12..12 + 16 * count]
510 .chunks_exact(16)
511 .map(|record| record.try_into().unwrap())
512 .collect();
513 for record in &mut records {
514 match &record[..4] {
515 b"glyf" => record[..4].copy_from_slice(b"GLYF"),
516 b"loca" => record[..4].copy_from_slice(b"LOCA"),
517 _ => {}
518 }
519 }
520 records.sort();
522 let mut font = data.to_vec();
523 font[12..12 + 16 * count].copy_from_slice(&records.concat());
524 font
525 }
526
527 fn hint_reliant_with_names(records: &[(u16, u16, u16, &str)]) -> bool {
532 with_names(font_test_data::TINOS_SUBSET, records)
533 }
534
535 fn with_names(data: &[u8], records: &[(u16, u16, u16, &str)]) -> bool {
537 let strings: Vec<Vec<u8>> = records
538 .iter()
539 .map(|(platform, _, _, value)| match platform {
540 1 => value.bytes().collect(),
541 _ => value.encode_utf16().flat_map(u16::to_be_bytes).collect(),
542 })
543 .collect();
544 let count = records.len() as u16;
545 let mut name = alloc::vec![0, 0];
546 name.extend_from_slice(&count.to_be_bytes());
547 name.extend_from_slice(&(6 + 12 * count).to_be_bytes());
548 let mut offset = 0u16;
549 for ((platform, language, id, _), string) in records.iter().zip(&strings) {
550 let encoding = if *platform == 1 { 0u16 } else { 1 };
551 for field in [
552 *platform,
553 encoding,
554 *language,
555 *id,
556 string.len() as u16,
557 offset,
558 ] {
559 name.extend_from_slice(&field.to_be_bytes());
560 }
561 offset += string.len() as u16;
562 }
563 strings
564 .iter()
565 .for_each(|string| name.extend_from_slice(string));
566 is_hint_reliant(&WithName(FontRef::new(data).unwrap(), &name))
567 }
568
569 struct WithName<'a>(FontRef<'a>, &'a [u8]);
571
572 impl<'a> TableProvider<'a> for WithName<'a> {
573 fn data_for_tag(&self, tag: crate::types::Tag) -> Option<crate::FontData<'a>> {
574 if tag == crate::types::Tag::new(b"name") {
575 return Some(crate::FontData::new(self.1));
576 }
577 self.0.data_for_tag(tag)
578 }
579 }
580
581 #[test]
584 fn family_names_are_read_as_freetype_reads_them() {
585 const FAMILY: u16 = 1;
586 const TYPOGRAPHIC: u16 = 16;
587 assert!(!hint_reliant_with_names(&[(3, 0x409, FAMILY, "Arial")]));
588 assert!(hint_reliant_with_names(&[(3, 0x409, FAMILY, "MingLiU")]));
589 assert!(hint_reliant_with_names(&[(
591 3,
592 0x409,
593 FAMILY,
594 "ABCDEF+DFKaiShu-SB-Estd-BF"
595 )]));
596 assert!(hint_reliant_with_names(&[(
599 3,
600 0x409,
601 FAMILY,
602 "HuaTianKaiTi体"
603 )]));
604 assert!(!hint_reliant_with_names(&[(
605 3,
606 0x409,
607 FAMILY,
608 "HuaTianKaiTi"
609 )]));
610 assert!(!hint_reliant_with_names(&[
612 (3, 0x409, FAMILY, "MingLiU"),
613 (3, 0x409, TYPOGRAPHIC, "Arial"),
614 ]));
615 assert!(!hint_reliant_with_names(&[
617 (3, 0x404, FAMILY, "MingLiU"),
618 (1, 0, FAMILY, "Arial"),
619 ]));
620 assert!(hint_reliant_with_names(&[
622 (3, 0x409, FAMILY, "MingLiU"),
623 (1, 0, FAMILY, "Arial"),
624 ]));
625 }
626
627 #[test]
629 fn only_truetype_fonts_are_hint_reliant() {
630 const FAMILY: u16 = 1;
631 let mingliu = [(3, 0x409, FAMILY, "MingLiU")];
632 assert!(with_names(font_test_data::TINOS_SUBSET, &mingliu));
633 assert!(!with_names(
634 font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
635 &mingliu
636 ));
637 }
638
639 #[test]
640 fn ensure_max_name_len() {
641 let max_len = HINT_RELIANT_NAMES
642 .iter()
643 .fold(0, |acc, name| acc.max(name.len()));
644 assert_eq!(max_len, MAX_HINT_RELIANT_NAME_LEN);
645 }
646
647 #[test]
648 fn skip_pdf_tags() {
649 let matches = |name: &str| matches_hint_reliant_name_list(name.bytes());
650 assert!(!matches("DLCABC+Arial"));
652 assert!(matches("ABCDEF+MingLiU"));
653 assert!(matches("DLCabc+Arial"));
655 assert!(matches("DLC12A+Arial"));
656 assert!(matches("DLCABCD+Arial"));
657 assert!(matches("DLCAB+X"));
659 assert!(matches("ABCDEF+DFKaiShu-SB-Estd-BF"));
661 }
662
663 #[test]
664 fn all_hint_reliant_names() {
665 for name in HINT_RELIANT_NAMES {
666 assert!(matches_hint_reliant_name_list(name.bytes()));
667 }
668 }
669
670 #[test]
671 fn non_hint_reliant_names() {
672 for not_tricky in ["Roboto", "Arial", "Helvetica", "Blah", ""] {
673 assert!(!matches_hint_reliant_name_list(not_tricky.bytes()));
674 }
675 }
676}