Skip to main content

read_fonts/tables/glyf/
hint_reliant.rs

1//! Detection of fonts that need their TrueType instructions to draw correct
2//! contours.
3//!
4//! These are often called "tricky" fonts. They are recognized by family
5//! name or by the checksums of the `cvt`, `fpgm` and `prep` tables. The
6//! lists of names and checksums come from FreeType.
7
8use crate::{
9    tables::{name::Name, os2::SelectionFlags},
10    types::{BigEndian, NameId},
11    TableProvider,
12};
13
14/// Returns `true` if the font needs its instructions to draw correct
15/// contours.
16///
17/// Only fonts with TrueType outlines qualify, so a font with neither a
18/// `glyf` nor a `GLYF` table returns `false`.
19///
20/// This reads the family name and the hinting tables. A caller that checks
21/// more than once should cache the result, as
22/// [`Font::is_hint_reliant`](crate::model::Font::is_hint_reliant) does.
23pub 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
30/// Returns `true` if the family name that FreeType reports for the font is
31/// on the list.
32fn is_hint_reliant_name<'a>(font: &impl TableProvider<'a>) -> bool {
33    let Ok(name) = font.name() else {
34        return false;
35    };
36    // With the OS/2 WWS bit set the typographic family comes first, and
37    // without it the WWS family does.
38    //
39    // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/sfnt/sfobjs.c#L1045>
40    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
57/// A name decoded to ASCII the same way as FreeType.
58///
59/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/sfnt/sfobjs.c#L136>
60struct AsciiName<'a> {
61    data: &'a [u8],
62    utf16: bool,
63}
64
65impl<'a> AsciiName<'a> {
66    /// Reads the record that FreeType chooses for `id`.
67    ///
68    /// A Windows record is used if it is English or if there is no Macintosh
69    /// record. Otherwise, a Macintosh English or Roman record is used. A
70    /// Unicode record is used if there is neither.
71    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    /// Returns the name's characters up to the first NUL, with `?` in place
107    /// of each character outside the range 32 to 127.
108    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
127/// Returns `true` if `name` contains a family name that FreeType knows to
128/// rely on hinting.
129///
130/// The random tag of a font embedded in a PDF is skipped. Matching follows
131/// FreeType's `strstr` search.
132///
133/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L153>
134fn 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    // The tag is six capitals and a plus, in a name of at least eight.
142    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    // The trailing bytes, enough to hold the longest name on the list.
146    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/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L180>
163#[rustfmt::skip]
164const HINT_RELIANT_NAMES: &[&str] = &[
165    "cpop",               /* dftt-p7.ttf; version 1.00, 1992 [DLJGyShoMedium] */
166    "DFGirl-W6-WIN-BF",   /* dftt-h6.ttf; version 1.00, 1993 */
167    "DFGothic-EB",        /* DynaLab Inc. 1992-1995 */
168    "DFGyoSho-Lt",        /* DynaLab Inc. 1992-1995 */
169    "DFHei",              /* DynaLab Inc. 1992-1995 [DFHei-Bd-WIN-HK-BF] */
170                          /* covers "DFHei-Md-HK-BF", maybe DynaLab Inc. */
171
172    "DFHSGothic-W5",      /* DynaLab Inc. 1992-1995 */
173    "DFHSMincho-W3",      /* DynaLab Inc. 1992-1995 */
174    "DFHSMincho-W7",      /* DynaLab Inc. 1992-1995 */
175    "DFKaiSho-SB",        /* dfkaisb.ttf */
176    "DFKaiShu",           /* covers "DFKaiShu-Md-HK-BF", maybe DynaLab Inc. */
177    "DFKai-SB",           /* kaiu.ttf; version 3.00, 1998 [DFKaiShu-SB-Estd-BF] */
178
179    "DFMing",             /* DynaLab Inc. 1992-1995 [DFMing-Md-WIN-HK-BF] */
180                          /* covers "DFMing-Bd-HK-BF", maybe DynaLab Inc. */
181
182    "DLC",                /* dftt-m7.ttf; version 1.00, 1993 [DLCMingBold] */
183                          /* dftt-f5.ttf; version 1.00, 1993 [DLCFongSung] */
184                          /* covers following */
185                          /* "DLCHayMedium", dftt-b5.ttf; version 1.00, 1993 */
186                          /* "DLCHayBold",   dftt-b7.ttf; version 1.00, 1993 */
187                          /* "DLCKaiMedium", dftt-k5.ttf; version 1.00, 1992 */
188                          /* "DLCLiShu",     dftt-l5.ttf; version 1.00, 1992 */
189                          /* "DLCRoundBold", dftt-r7.ttf; version 1.00, 1993 */
190
191    "HuaTianKaiTi?",      /* htkt2.ttf */
192    "HuaTianSongTi?",     /* htst3.ttf */
193    "Ming(for ISO10646)", /* hkscsiic.ttf; version 0.12, 2007 [Ming] */
194                          /* iicore.ttf; version 0.07, 2007 [Ming] */
195    "MingLiU",            /* mingliu.ttf */
196                          /* mingliu.ttc; version 3.21, 2001 */
197    "MingMedium",         /* dftt-m5.ttf; version 1.00, 1993 [DLCMingMedium] */
198    "PMingLiU",           /* mingliu.ttc; version 3.21, 2001 */
199    "MingLi43",           /* mingli.ttf; version 1.00, 1992 */
200];
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            // Note: FreeType always just computes the checksum
217            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L281>
218            checksum: crate::tables::compute_checksum(data),
219            len: data.len() as u32,
220        }
221    }
222
223    /// Creates the identity of a control value table.
224    ///
225    /// The checksum sums the entries the same way that
226    /// [`compute_checksum`](crate::tables::compute_checksum) sums bytes. Two
227    /// big-endian entries form a word, and a trailing entry is padded.
228    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
262/// Checks for fonts that require hinting based on the length and checksum of
263/// the cvt, fpgm and prep tables.
264///
265/// Roughly equivalent to <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L309>
266fn matches_hint_reliant_id_list(font_id: FontId) -> bool {
267    HINT_RELIANT_IDS.contains(&font_id)
268}
269
270/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L314>
271#[rustfmt::skip]
272const HINT_RELIANT_IDS: &[FontId] = &[
273    // MingLiU 1995
274    FontId {
275        cvt: TableId { checksum: 0x05BCF058, len: 0x000002E4 },
276        fpgm: TableId { checksum: 0x28233BF1, len: 0x000087C4 },
277        prep: TableId { checksum: 0xA344A1EA, len: 0x000001E1 },
278    },
279    // MingLiU 1996-
280    FontId {
281        cvt: TableId { checksum: 0x05BCF058, len: 0x000002E4 },
282        fpgm: TableId { checksum: 0x28233BF1, len: 0x000087C4 },
283        prep: TableId { checksum: 0xA344A1EB, len: 0x000001E1 },
284    },
285    // DFGothic-EB
286    FontId {
287        cvt: TableId { checksum: 0x12C3EBB2, len: 0x00000350 },
288        fpgm: TableId { checksum: 0xB680EE64, len: 0x000087A7 },
289        prep: TableId { checksum: 0xCE939563, len: 0x00000758 },
290    },
291    // DFGyoSho-Lt
292    FontId {
293        cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000350 },
294        fpgm: TableId { checksum: 0xCE5956E9, len: 0x0000BC85 },
295        prep: TableId { checksum: 0x8272F416, len: 0x00000045 },
296    },
297    // DFHei-Md-HK-BF
298    FontId {
299        cvt: TableId { checksum: 0x1257EB46, len: 0x00000350 },
300        fpgm: TableId { checksum: 0xF699D160, len: 0x0000715F },
301        prep: TableId { checksum: 0xD222F568, len: 0x000003BC },
302    },
303    // DFHSGothic-W5
304    FontId {
305        cvt: TableId { checksum: 0x1262EB4E, len: 0x00000350 },
306        fpgm: TableId { checksum: 0xE86A5D64, len: 0x00007940 },
307        prep: TableId { checksum: 0x7850F729, len: 0x000005FF },
308    },
309    // DFHSMincho-W3
310    FontId {
311        cvt: TableId { checksum: 0x122DEB0A, len: 0x00000350 },
312        fpgm: TableId { checksum: 0x3D16328A, len: 0x0000859B },
313        prep: TableId { checksum: 0xA93FC33B, len: 0x000002CB },
314    },
315    // DFHSMincho-W7
316    FontId {
317        cvt: TableId { checksum: 0x125FEB26, len: 0x00000350 },
318        fpgm: TableId { checksum: 0xA5ACC982, len: 0x00007EE1 },
319        prep: TableId { checksum: 0x90999196, len: 0x0000041F },
320    },
321    // DFKaiShu
322    FontId {
323        cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000350 },
324        fpgm: TableId { checksum: 0x5A30CA3B, len: 0x00009063 },
325        prep: TableId { checksum: 0x13A42602, len: 0x0000007E },
326    },
327    // DFKaiShu, variant
328    FontId {
329        cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000350 },
330        fpgm: TableId { checksum: 0xA6E78C01, len: 0x00008998 },
331        prep: TableId { checksum: 0x13A42602, len: 0x0000007E },
332    },
333    // DFKaiShu-Md-HK-BF
334    FontId {
335        cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000360 },
336        fpgm: TableId { checksum: 0x9DB282B2, len: 0x0000C06E },
337        prep: TableId { checksum: 0x53E6D7CA, len: 0x00000082 },
338    },
339    // DFMing-Bd-HK-BF
340    FontId {
341        cvt: TableId { checksum: 0x1243EB18, len: 0x00000350 },
342        fpgm: TableId { checksum: 0xBA0A8C30, len: 0x000074AD },
343        prep: TableId { checksum: 0xF3D83409, len: 0x0000037B },
344    },
345    // DLCLiShu
346    FontId {
347        cvt: TableId { checksum: 0x07DCF546, len: 0x00000308 },
348        fpgm: TableId { checksum: 0x40FE7C90, len: 0x00008E2A },
349        prep: TableId { checksum: 0x608174B5, len: 0x0000007A },
350    },
351    // DLCHayBold
352    FontId {
353        cvt: TableId { checksum: 0xEB891238, len: 0x00000308 },
354        fpgm: TableId { checksum: 0xD2E4DCD4, len: 0x0000676F },
355        prep: TableId { checksum: 0x8EA5F293, len: 0x000003B8 },
356    },
357    // HuaTianKaiTi
358    FontId {
359        cvt: TableId { checksum: 0xFFFBFFFC, len: 0x00000008 },
360        fpgm: TableId { checksum: 0x9C9E48B8, len: 0x0000BEA2 },
361        prep: TableId { checksum: 0x70020112, len: 0x00000008 },
362    },
363    // HuaTianSongTi
364    FontId {
365        cvt: TableId { checksum: 0xFFFBFFFC, len: 0x00000008 },
366        fpgm: TableId { checksum: 0x0A5A0483, len: 0x00017C39 },
367        prep: TableId { checksum: 0x70020112, len: 0x00000008 },
368    },
369    // NEC fadpop7.ttf
370    FontId {
371        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
372        fpgm: TableId { checksum: 0x40C92555, len: 0x000000E5 },
373        prep: TableId { checksum: 0xA39B58E3, len: 0x0000117C },
374    },
375    // NEC fadrei5.ttf
376    FontId {
377        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
378        fpgm: TableId { checksum: 0x33C41652, len: 0x000000E5 },
379        prep: TableId { checksum: 0x26D6C52A, len: 0x00000F6A },
380    },
381    // NEC fangot7.ttf
382    FontId {
383        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
384        fpgm: TableId { checksum: 0x6DB1651D, len: 0x0000019D },
385        prep: TableId { checksum: 0x6C6E4B03, len: 0x00002492 },
386    },
387    // NEC fangyo5.ttf
388    FontId {
389        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
390        fpgm: TableId { checksum: 0x40C92555, len: 0x000000E5 },
391        prep: TableId { checksum: 0xDE51FAD0, len: 0x0000117C },
392    },
393    // NEC fankyo5.ttf
394    FontId {
395        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
396        fpgm: TableId { checksum: 0x85E47664, len: 0x000000E5 },
397        prep: TableId { checksum: 0xA6C62831, len: 0x00001CAA },
398    },
399    // NEC fanrgo5.ttf
400    FontId {
401        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
402        fpgm: TableId { checksum: 0x2D891CFD, len: 0x0000019D },
403        prep: TableId { checksum: 0xA0604633, len: 0x00001DE8 },
404    },
405    // NEC fangot5.ttc
406    FontId {
407        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
408        fpgm: TableId { checksum: 0x40AA774C, len: 0x000001CB },
409        prep: TableId { checksum: 0x9B5CAA96, len: 0x00001F9A },
410    },
411    // NEC fanmin3.ttc
412    FontId {
413        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
414        fpgm: TableId { checksum: 0x0D3DE9CB, len: 0x00000141 },
415        prep: TableId { checksum: 0xD4127766, len: 0x00002280 },
416    },
417    // NEC FA-Gothic, 1996
418    FontId {
419        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
420        fpgm: TableId { checksum: 0x4A692698, len: 0x000001F0 },
421        prep: TableId { checksum: 0x340D4346, len: 0x00001FCA },
422    },
423    // NEC FA-Minchou, 1996
424    FontId {
425        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
426        fpgm: TableId { checksum: 0xCD34C604, len: 0x00000166 },
427        prep: TableId { checksum: 0x6CF31046, len: 0x000022B0 },
428    },
429    // NEC FA-RoundGothicB, 1996
430    FontId {
431        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
432        fpgm: TableId { checksum: 0x5DA75315, len: 0x0000019D },
433        prep: TableId { checksum: 0x40745A5F, len: 0x000022E0 },
434    },
435    // NEC FA-RoundGothicM, 1996
436    FontId {
437        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
438        fpgm: TableId { checksum: 0xF055FC48, len: 0x000001C2 },
439        prep: TableId { checksum: 0x3900DED3, len: 0x00001E18 },
440    },
441    // MINGLI.TTF, 1992
442    FontId {
443        cvt: TableId { checksum: 0x00170003, len: 0x00000060 },
444        fpgm: TableId { checksum: 0xDBB4306E, len: 0x000058AA },
445        prep: TableId { checksum: 0xD643482A, len: 0x00000035 },
446    },
447    // DFHei-Bd-WIN-HK-BF, issue #1087
448    FontId {
449        cvt: TableId { checksum: 0x1269EB58, len: 0x00000350 },
450        fpgm: TableId { checksum: 0x5CD5957A, len: 0x00006A4E },
451        prep: TableId { checksum: 0xF758323A, len: 0x00000380 },
452    },
453    // DFMing-Md-WIN-HK-BF, issue #1087
454    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    // The identity of a cvt is summed from its entries, which has to agree
467    // with the byte checksum FreeType takes.
468    #[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        // An odd entry count pads the last word.
480        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    // The font model answers the same as a FontRef, through its own tables.
488    #[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    // A font with its outlines in GLYF and LOCA qualifies.
499    #[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    /// `data` with its `glyf` and `loca` tables renamed `GLYF` and `LOCA`.
507    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        // The directory stays sorted by tag.
521        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    /// Returns whether a font with Tinos's tables and a `name` table
528    /// containing `records` is hint reliant.
529    ///
530    /// Each record is a platform, language, name id and string.
531    fn hint_reliant_with_names(records: &[(u16, u16, u16, &str)]) -> bool {
532        with_names(font_test_data::TINOS_SUBSET, records)
533    }
534
535    /// Whether `data`, with a name table holding `records`, is hint reliant.
536    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    /// A font's tables with a replacement `name` table.
570    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    // FreeType's family name: the whole string, its non-ASCII characters
582    // read as `?`, from the record and name id FreeType picks.
583    #[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        // Longer than any name on the list.
590        assert!(hint_reliant_with_names(&[(
591            3,
592            0x409,
593            FAMILY,
594            "ABCDEF+DFKaiShu-SB-Estd-BF"
595        )]));
596        // The list's "HuaTianKaiTi?" ends in a character FreeType could not
597        // read as ASCII.
598        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        // The typographic family wins over the family.
611        assert!(!hint_reliant_with_names(&[
612            (3, 0x409, FAMILY, "MingLiU"),
613            (3, 0x409, TYPOGRAPHIC, "Arial"),
614        ]));
615        // A Macintosh English record wins over a non-English Windows one,
616        assert!(!hint_reliant_with_names(&[
617            (3, 0x404, FAMILY, "MingLiU"),
618            (1, 0, FAMILY, "Arial"),
619        ]));
620        // and loses to an English one.
621        assert!(hint_reliant_with_names(&[
622            (3, 0x409, FAMILY, "MingLiU"),
623            (1, 0, FAMILY, "Arial"),
624        ]));
625    }
626
627    // FreeType checks only fonts with TrueType outlines.
628    #[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        // A tag is skipped, so a listed name inside it does not count.
651        assert!(!matches("DLCABC+Arial"));
652        assert!(matches("ABCDEF+MingLiU"));
653        // Not a tag: lowercase, digits, a misplaced plus.
654        assert!(matches("DLCabc+Arial"));
655        assert!(matches("DLC12A+Arial"));
656        assert!(matches("DLCABCD+Arial"));
657        // A name shorter than eight is never tagged.
658        assert!(matches("DLCAB+X"));
659        // The name after a tag can be longer than any on the list.
660        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}