use crate::{
tables::{name::Name, os2::SelectionFlags},
types::{BigEndian, NameId},
TableProvider,
};
pub fn is_hint_reliant<'a>(font: &impl TableProvider<'a>) -> bool {
if font.glyf().is_err() && font.glyf_extended().is_err() {
return false;
}
is_hint_reliant_name(font) || matches_hint_reliant_id_list(FontId::from_font(font))
}
fn is_hint_reliant_name<'a>(font: &impl TableProvider<'a>) -> bool {
let Ok(name) = font.name() else {
return false;
};
let wws = font
.os2()
.is_ok_and(|os2| os2.fs_selection().contains(SelectionFlags::WWS));
let ids: &[NameId] = if wws {
&[NameId::TYPOGRAPHIC_FAMILY_NAME, NameId::FAMILY_NAME]
} else {
&[
NameId::WWS_FAMILY_NAME,
NameId::TYPOGRAPHIC_FAMILY_NAME,
NameId::FAMILY_NAME,
]
};
ids.iter()
.find_map(|id| AsciiName::read(&name, *id))
.is_some_and(|family| matches_hint_reliant_name_list(family.bytes()))
}
struct AsciiName<'a> {
data: &'a [u8],
utf16: bool,
}
impl<'a> AsciiName<'a> {
fn read(name: &Name<'a>, id: NameId) -> Option<Self> {
let (mut apple_english, mut apple_roman, mut windows, mut unicode) =
(None, None, None, None);
let mut windows_is_english = false;
let records = name.name_record().iter();
for record in records.filter(|record| record.name_id() == id && record.length() > 0) {
match record.platform_id() {
0 | 2 => unicode = Some(record),
1 if record.language_id() == 0 => apple_english = Some(record),
1 if record.encoding_id() == 0 => apple_roman = Some(record),
3 => {
let english = record.language_id() & 0x3ff == 0x009;
if (windows.is_none() || english) && matches!(record.encoding_id(), 0 | 1 | 10)
{
windows = Some(record);
windows_is_english = english;
}
}
_ => {}
}
}
let apple = apple_english.or(apple_roman);
let (record, utf16) = match (windows, apple) {
(Some(windows), _) if apple.is_none() || windows_is_english => (windows, true),
(_, Some(apple)) => (apple, false),
_ => (unicode?, true),
};
let start = record.string_offset().to_u32() as usize;
let data = name
.string_data()
.as_bytes()
.get(start..start + record.length() as usize)?;
Some(Self { data, utf16 })
}
fn bytes(&self) -> impl Iterator<Item = u8> + '_ {
let step = if self.utf16 { 2 } else { 1 };
self.data
.chunks_exact(step)
.map(|unit| {
unit.iter()
.fold(0u32, |code, byte| code << 8 | *byte as u32)
})
.take_while(|code| *code != 0)
.map(|code| {
if (32..=127).contains(&code) {
code as u8
} else {
b'?'
}
})
}
}
fn matches_hint_reliant_name_list(mut name: impl Iterator<Item = u8>) -> bool {
let mut head = [0u8; 8];
let mut head_len = 0;
for byte in name.by_ref().take(head.len()) {
head[head_len] = byte;
head_len += 1;
}
let tagged =
head_len == head.len() && head[6] == b'+' && head[..6].iter().all(u8::is_ascii_uppercase);
let skip = if tagged { 7 } else { 0 };
let mut window = [0u8; MAX_HINT_RELIANT_NAME_LEN];
let mut seen = 0;
head[skip..head_len]
.iter()
.copied()
.chain(name)
.any(|byte| {
window.copy_within(1.., 0);
window[MAX_HINT_RELIANT_NAME_LEN - 1] = byte;
seen += 1;
HINT_RELIANT_NAMES
.iter()
.any(|tricky| tricky.len() <= seen && window.ends_with(tricky.as_bytes()))
})
}
#[rustfmt::skip]
const HINT_RELIANT_NAMES: &[&str] = &[
"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",
];
const MAX_HINT_RELIANT_NAME_LEN: usize = 18;
#[derive(Copy, Clone, PartialEq, Default, Debug)]
struct TableId {
checksum: u32,
len: u32,
}
impl TableId {
fn from_data(data: Option<&[u8]>) -> Self {
let Some(data) = data else {
return Self::default();
};
Self {
checksum: crate::tables::compute_checksum(data),
len: data.len() as u32,
}
}
fn from_cvt(cvt: Option<&[BigEndian<i16>]>) -> Self {
let Some(cvt) = cvt else {
return Self::default();
};
let mut checksum = 0u32;
for pair in cvt.chunks(2) {
let high = pair[0].get() as u16 as u32;
let low = pair.get(1).map_or(0, |value| value.get() as u16 as u32);
checksum = checksum.wrapping_add((high << 16) | low);
}
Self {
checksum,
len: (cvt.len() * 2) as u32,
}
}
}
#[derive(Copy, Clone, PartialEq, Default, Debug)]
struct FontId {
cvt: TableId,
fpgm: TableId,
prep: TableId,
}
impl FontId {
fn from_font<'a>(font: &impl TableProvider<'a>) -> Self {
Self {
cvt: TableId::from_cvt(font.cvt().ok()),
fpgm: TableId::from_data(font.fpgm().ok()),
prep: TableId::from_data(font.prep().ok()),
}
}
}
fn matches_hint_reliant_id_list(font_id: FontId) -> bool {
HINT_RELIANT_IDS.contains(&font_id)
}
#[rustfmt::skip]
const HINT_RELIANT_IDS: &[FontId] = &[
FontId {
cvt: TableId { checksum: 0x05BCF058, len: 0x000002E4 },
fpgm: TableId { checksum: 0x28233BF1, len: 0x000087C4 },
prep: TableId { checksum: 0xA344A1EA, len: 0x000001E1 },
},
FontId {
cvt: TableId { checksum: 0x05BCF058, len: 0x000002E4 },
fpgm: TableId { checksum: 0x28233BF1, len: 0x000087C4 },
prep: TableId { checksum: 0xA344A1EB, len: 0x000001E1 },
},
FontId {
cvt: TableId { checksum: 0x12C3EBB2, len: 0x00000350 },
fpgm: TableId { checksum: 0xB680EE64, len: 0x000087A7 },
prep: TableId { checksum: 0xCE939563, len: 0x00000758 },
},
FontId {
cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000350 },
fpgm: TableId { checksum: 0xCE5956E9, len: 0x0000BC85 },
prep: TableId { checksum: 0x8272F416, len: 0x00000045 },
},
FontId {
cvt: TableId { checksum: 0x1257EB46, len: 0x00000350 },
fpgm: TableId { checksum: 0xF699D160, len: 0x0000715F },
prep: TableId { checksum: 0xD222F568, len: 0x000003BC },
},
FontId {
cvt: TableId { checksum: 0x1262EB4E, len: 0x00000350 },
fpgm: TableId { checksum: 0xE86A5D64, len: 0x00007940 },
prep: TableId { checksum: 0x7850F729, len: 0x000005FF },
},
FontId {
cvt: TableId { checksum: 0x122DEB0A, len: 0x00000350 },
fpgm: TableId { checksum: 0x3D16328A, len: 0x0000859B },
prep: TableId { checksum: 0xA93FC33B, len: 0x000002CB },
},
FontId {
cvt: TableId { checksum: 0x125FEB26, len: 0x00000350 },
fpgm: TableId { checksum: 0xA5ACC982, len: 0x00007EE1 },
prep: TableId { checksum: 0x90999196, len: 0x0000041F },
},
FontId {
cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000350 },
fpgm: TableId { checksum: 0x5A30CA3B, len: 0x00009063 },
prep: TableId { checksum: 0x13A42602, len: 0x0000007E },
},
FontId {
cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000350 },
fpgm: TableId { checksum: 0xA6E78C01, len: 0x00008998 },
prep: TableId { checksum: 0x13A42602, len: 0x0000007E },
},
FontId {
cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000360 },
fpgm: TableId { checksum: 0x9DB282B2, len: 0x0000C06E },
prep: TableId { checksum: 0x53E6D7CA, len: 0x00000082 },
},
FontId {
cvt: TableId { checksum: 0x1243EB18, len: 0x00000350 },
fpgm: TableId { checksum: 0xBA0A8C30, len: 0x000074AD },
prep: TableId { checksum: 0xF3D83409, len: 0x0000037B },
},
FontId {
cvt: TableId { checksum: 0x07DCF546, len: 0x00000308 },
fpgm: TableId { checksum: 0x40FE7C90, len: 0x00008E2A },
prep: TableId { checksum: 0x608174B5, len: 0x0000007A },
},
FontId {
cvt: TableId { checksum: 0xEB891238, len: 0x00000308 },
fpgm: TableId { checksum: 0xD2E4DCD4, len: 0x0000676F },
prep: TableId { checksum: 0x8EA5F293, len: 0x000003B8 },
},
FontId {
cvt: TableId { checksum: 0xFFFBFFFC, len: 0x00000008 },
fpgm: TableId { checksum: 0x9C9E48B8, len: 0x0000BEA2 },
prep: TableId { checksum: 0x70020112, len: 0x00000008 },
},
FontId {
cvt: TableId { checksum: 0xFFFBFFFC, len: 0x00000008 },
fpgm: TableId { checksum: 0x0A5A0483, len: 0x00017C39 },
prep: TableId { checksum: 0x70020112, len: 0x00000008 },
},
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x40C92555, len: 0x000000E5 },
prep: TableId { checksum: 0xA39B58E3, len: 0x0000117C },
},
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x33C41652, len: 0x000000E5 },
prep: TableId { checksum: 0x26D6C52A, len: 0x00000F6A },
},
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x6DB1651D, len: 0x0000019D },
prep: TableId { checksum: 0x6C6E4B03, len: 0x00002492 },
},
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x40C92555, len: 0x000000E5 },
prep: TableId { checksum: 0xDE51FAD0, len: 0x0000117C },
},
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x85E47664, len: 0x000000E5 },
prep: TableId { checksum: 0xA6C62831, len: 0x00001CAA },
},
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x2D891CFD, len: 0x0000019D },
prep: TableId { checksum: 0xA0604633, len: 0x00001DE8 },
},
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x40AA774C, len: 0x000001CB },
prep: TableId { checksum: 0x9B5CAA96, len: 0x00001F9A },
},
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x0D3DE9CB, len: 0x00000141 },
prep: TableId { checksum: 0xD4127766, len: 0x00002280 },
},
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x4A692698, len: 0x000001F0 },
prep: TableId { checksum: 0x340D4346, len: 0x00001FCA },
},
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0xCD34C604, len: 0x00000166 },
prep: TableId { checksum: 0x6CF31046, len: 0x000022B0 },
},
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x5DA75315, len: 0x0000019D },
prep: TableId { checksum: 0x40745A5F, len: 0x000022E0 },
},
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0xF055FC48, len: 0x000001C2 },
prep: TableId { checksum: 0x3900DED3, len: 0x00001E18 },
},
FontId {
cvt: TableId { checksum: 0x00170003, len: 0x00000060 },
fpgm: TableId { checksum: 0xDBB4306E, len: 0x000058AA },
prep: TableId { checksum: 0xD643482A, len: 0x00000035 },
},
FontId {
cvt: TableId { checksum: 0x1269EB58, len: 0x00000350 },
fpgm: TableId { checksum: 0x5CD5957A, len: 0x00006A4E },
prep: TableId { checksum: 0xF758323A, len: 0x00000380 },
},
FontId {
cvt: TableId { checksum: 0x122FEB0B, len: 0x00000350 },
fpgm: TableId { checksum: 0x7F10919A, len: 0x000070A9 },
prep: TableId { checksum: 0x7CD7E7B7, len: 0x0000025C },
},
];
#[cfg(test)]
mod tests {
use super::*;
use crate::{FontRef, TableProvider};
#[test]
fn cvt_identity_matches_the_byte_checksum() {
for data in [font_test_data::TINOS_SUBSET, font_test_data::CVAR] {
let font = FontRef::new(data).unwrap();
let cvt = font.cvt().unwrap();
let bytes = font.data_for_tag(crate::types::Tag::new(b"cvt ")).unwrap();
assert_eq!(
TableId::from_cvt(Some(cvt)),
TableId::from_data(Some(bytes.as_bytes()))
);
}
let cvt = [0x0102i16, 0x0304, 0x0506].map(BigEndian::from);
assert_eq!(
TableId::from_cvt(Some(&cvt)),
TableId::from_data(Some(&[1, 2, 3, 4, 5, 6]))
);
}
#[test]
fn font_model_agrees_with_font_ref() {
for data in [font_test_data::TINOS_SUBSET, font_test_data::VAZIRMATN_VAR] {
let font = FontRef::new(data).unwrap();
let model = crate::model::Font::new(data.to_vec(), 0).unwrap();
assert_eq!(model.is_hint_reliant(), is_hint_reliant(&font));
assert_eq!(is_hint_reliant(&model.tables()), is_hint_reliant(&font));
}
}
#[test]
fn uppercase_glyf_fonts_qualify() {
let data = uppercase_glyf(font_test_data::TINOS_SUBSET);
assert!(FontRef::new(&data).unwrap().glyf().is_err());
assert!(with_names(&data, &[(3, 0x409, 1, "MingLiU")]));
}
fn uppercase_glyf(data: &[u8]) -> Vec<u8> {
let count = u16::from_be_bytes([data[4], data[5]]) as usize;
let mut records: Vec<[u8; 16]> = data[12..12 + 16 * count]
.chunks_exact(16)
.map(|record| record.try_into().unwrap())
.collect();
for record in &mut records {
match &record[..4] {
b"glyf" => record[..4].copy_from_slice(b"GLYF"),
b"loca" => record[..4].copy_from_slice(b"LOCA"),
_ => {}
}
}
records.sort();
let mut font = data.to_vec();
font[12..12 + 16 * count].copy_from_slice(&records.concat());
font
}
fn hint_reliant_with_names(records: &[(u16, u16, u16, &str)]) -> bool {
with_names(font_test_data::TINOS_SUBSET, records)
}
fn with_names(data: &[u8], records: &[(u16, u16, u16, &str)]) -> bool {
let strings: Vec<Vec<u8>> = records
.iter()
.map(|(platform, _, _, value)| match platform {
1 => value.bytes().collect(),
_ => value.encode_utf16().flat_map(u16::to_be_bytes).collect(),
})
.collect();
let count = records.len() as u16;
let mut name = alloc::vec![0, 0];
name.extend_from_slice(&count.to_be_bytes());
name.extend_from_slice(&(6 + 12 * count).to_be_bytes());
let mut offset = 0u16;
for ((platform, language, id, _), string) in records.iter().zip(&strings) {
let encoding = if *platform == 1 { 0u16 } else { 1 };
for field in [
*platform,
encoding,
*language,
*id,
string.len() as u16,
offset,
] {
name.extend_from_slice(&field.to_be_bytes());
}
offset += string.len() as u16;
}
strings
.iter()
.for_each(|string| name.extend_from_slice(string));
is_hint_reliant(&WithName(FontRef::new(data).unwrap(), &name))
}
struct WithName<'a>(FontRef<'a>, &'a [u8]);
impl<'a> TableProvider<'a> for WithName<'a> {
fn data_for_tag(&self, tag: crate::types::Tag) -> Option<crate::FontData<'a>> {
if tag == crate::types::Tag::new(b"name") {
return Some(crate::FontData::new(self.1));
}
self.0.data_for_tag(tag)
}
}
#[test]
fn family_names_are_read_as_freetype_reads_them() {
const FAMILY: u16 = 1;
const TYPOGRAPHIC: u16 = 16;
assert!(!hint_reliant_with_names(&[(3, 0x409, FAMILY, "Arial")]));
assert!(hint_reliant_with_names(&[(3, 0x409, FAMILY, "MingLiU")]));
assert!(hint_reliant_with_names(&[(
3,
0x409,
FAMILY,
"ABCDEF+DFKaiShu-SB-Estd-BF"
)]));
assert!(hint_reliant_with_names(&[(
3,
0x409,
FAMILY,
"HuaTianKaiTi体"
)]));
assert!(!hint_reliant_with_names(&[(
3,
0x409,
FAMILY,
"HuaTianKaiTi"
)]));
assert!(!hint_reliant_with_names(&[
(3, 0x409, FAMILY, "MingLiU"),
(3, 0x409, TYPOGRAPHIC, "Arial"),
]));
assert!(!hint_reliant_with_names(&[
(3, 0x404, FAMILY, "MingLiU"),
(1, 0, FAMILY, "Arial"),
]));
assert!(hint_reliant_with_names(&[
(3, 0x409, FAMILY, "MingLiU"),
(1, 0, FAMILY, "Arial"),
]));
}
#[test]
fn only_truetype_fonts_are_hint_reliant() {
const FAMILY: u16 = 1;
let mingliu = [(3, 0x409, FAMILY, "MingLiU")];
assert!(with_names(font_test_data::TINOS_SUBSET, &mingliu));
assert!(!with_names(
font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
&mingliu
));
}
#[test]
fn ensure_max_name_len() {
let max_len = HINT_RELIANT_NAMES
.iter()
.fold(0, |acc, name| acc.max(name.len()));
assert_eq!(max_len, MAX_HINT_RELIANT_NAME_LEN);
}
#[test]
fn skip_pdf_tags() {
let matches = |name: &str| matches_hint_reliant_name_list(name.bytes());
assert!(!matches("DLCABC+Arial"));
assert!(matches("ABCDEF+MingLiU"));
assert!(matches("DLCabc+Arial"));
assert!(matches("DLC12A+Arial"));
assert!(matches("DLCABCD+Arial"));
assert!(matches("DLCAB+X"));
assert!(matches("ABCDEF+DFKaiShu-SB-Estd-BF"));
}
#[test]
fn all_hint_reliant_names() {
for name in HINT_RELIANT_NAMES {
assert!(matches_hint_reliant_name_list(name.bytes()));
}
}
#[test]
fn non_hint_reliant_names() {
for not_tricky in ["Roboto", "Arial", "Helvetica", "Blah", ""] {
assert!(!matches_hint_reliant_name_list(not_tricky.bytes()));
}
}
}