skrifa/outline/
hint_reliant.rs

1//! Name detection for fonts that require hinting to be run for correct
2//! contours (FreeType calls these "tricky" fonts).
3
4use crate::{string::StringId, FontRef, MetadataProvider, Tag};
5
6pub(super) fn require_interpreter(font: &FontRef) -> bool {
7    is_hint_reliant_by_name(font) || matches_hint_reliant_id_list(FontId::from_font(font))
8}
9
10fn is_hint_reliant_by_name(font: &FontRef) -> bool {
11    font.localized_strings(StringId::FAMILY_NAME)
12        .english_or_first()
13        .map(|name| {
14            let mut buf = [0u8; MAX_HINT_RELIANT_NAME_LEN];
15            let mut len = 0;
16            let mut chars = name.chars();
17            for ch in chars.by_ref().take(MAX_HINT_RELIANT_NAME_LEN) {
18                buf[len] = ch as u8;
19                len += 1;
20            }
21            if chars.next().is_some() {
22                return false;
23            }
24            matches_hint_reliant_name_list(core::str::from_utf8(&buf[..len]).unwrap_or_default())
25        })
26        .unwrap_or_default()
27}
28
29/// Is this name on the list of fonts that require hinting?
30///
31/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L174>
32fn matches_hint_reliant_name_list(name: &str) -> bool {
33    let name = skip_pdf_random_tag(name);
34    HINT_RELIANT_NAMES
35        .iter()
36        // FreeType uses strstr(name, tricky_name) so we use contains() to
37        // match behavior.
38        .any(|tricky_name| name.contains(*tricky_name))
39}
40
41/// Fonts embedded in PDFs add random prefixes. Strip these
42/// for tricky font comparison purposes.
43///
44/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L153>
45fn skip_pdf_random_tag(name: &str) -> &str {
46    let bytes = name.as_bytes();
47    // Random tag is 6 uppercase letters followed by a +
48    if bytes.len() < 8 || bytes[6] != b'+' || !bytes.iter().take(6).all(|b| b.is_ascii_uppercase())
49    {
50        return name;
51    }
52    core::str::from_utf8(&bytes[7..]).unwrap_or(name)
53}
54
55/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L180>
56#[rustfmt::skip]
57const HINT_RELIANT_NAMES: &[&str] = &[
58    "cpop",               /* dftt-p7.ttf; version 1.00, 1992 [DLJGyShoMedium] */
59    "DFGirl-W6-WIN-BF",   /* dftt-h6.ttf; version 1.00, 1993 */
60    "DFGothic-EB",        /* DynaLab Inc. 1992-1995 */
61    "DFGyoSho-Lt",        /* DynaLab Inc. 1992-1995 */
62    "DFHei",              /* DynaLab Inc. 1992-1995 [DFHei-Bd-WIN-HK-BF] */
63                          /* covers "DFHei-Md-HK-BF", maybe DynaLab Inc. */
64
65    "DFHSGothic-W5",      /* DynaLab Inc. 1992-1995 */
66    "DFHSMincho-W3",      /* DynaLab Inc. 1992-1995 */
67    "DFHSMincho-W7",      /* DynaLab Inc. 1992-1995 */
68    "DFKaiSho-SB",        /* dfkaisb.ttf */
69    "DFKaiShu",           /* covers "DFKaiShu-Md-HK-BF", maybe DynaLab Inc. */
70    "DFKai-SB",           /* kaiu.ttf; version 3.00, 1998 [DFKaiShu-SB-Estd-BF] */
71
72    "DFMing",             /* DynaLab Inc. 1992-1995 [DFMing-Md-WIN-HK-BF] */
73                          /* covers "DFMing-Bd-HK-BF", maybe DynaLab Inc. */
74
75    "DLC",                /* dftt-m7.ttf; version 1.00, 1993 [DLCMingBold] */
76                          /* dftt-f5.ttf; version 1.00, 1993 [DLCFongSung] */
77                          /* covers following */
78                          /* "DLCHayMedium", dftt-b5.ttf; version 1.00, 1993 */
79                          /* "DLCHayBold",   dftt-b7.ttf; version 1.00, 1993 */
80                          /* "DLCKaiMedium", dftt-k5.ttf; version 1.00, 1992 */
81                          /* "DLCLiShu",     dftt-l5.ttf; version 1.00, 1992 */
82                          /* "DLCRoundBold", dftt-r7.ttf; version 1.00, 1993 */
83
84    "HuaTianKaiTi?",      /* htkt2.ttf */
85    "HuaTianSongTi?",     /* htst3.ttf */
86    "Ming(for ISO10646)", /* hkscsiic.ttf; version 0.12, 2007 [Ming] */
87                          /* iicore.ttf; version 0.07, 2007 [Ming] */
88    "MingLiU",            /* mingliu.ttf */
89                          /* mingliu.ttc; version 3.21, 2001 */
90    "MingMedium",         /* dftt-m5.ttf; version 1.00, 1993 [DLCMingMedium] */
91    "PMingLiU",           /* mingliu.ttc; version 3.21, 2001 */
92    "MingLi43",           /* mingli.ttf; version 1.00, 1992 */
93];
94
95const MAX_HINT_RELIANT_NAME_LEN: usize = 18;
96
97#[derive(Copy, Clone, PartialEq, Default, Debug)]
98struct TableId {
99    checksum: u32,
100    len: u32,
101}
102
103impl TableId {
104    fn from_font_and_tag(font: &FontRef, tag: Tag) -> Option<Self> {
105        let data = font.table_data(tag)?;
106        Some(Self {
107            // Note: FreeType always just computes the checksum
108            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L281>
109            checksum: raw::tables::compute_checksum(data.as_bytes()),
110            len: data.len() as u32,
111        })
112    }
113}
114
115#[derive(Copy, Clone, PartialEq, Default, Debug)]
116struct FontId {
117    cvt: TableId,
118    fpgm: TableId,
119    prep: TableId,
120}
121
122impl FontId {
123    fn from_font(font: &FontRef) -> Self {
124        Self {
125            cvt: TableId::from_font_and_tag(font, Tag::new(b"cvt ")).unwrap_or_default(),
126            fpgm: TableId::from_font_and_tag(font, Tag::new(b"fpgm")).unwrap_or_default(),
127            prep: TableId::from_font_and_tag(font, Tag::new(b"prep")).unwrap_or_default(),
128        }
129    }
130}
131
132/// Checks for fonts that require hinting based on the length and checksum of
133/// the cvt, fpgm and prep tables.
134///
135/// Roughly equivalent to <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L309>
136fn matches_hint_reliant_id_list(font_id: FontId) -> bool {
137    HINT_RELIANT_IDS.contains(&font_id)
138}
139
140/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L314>
141#[rustfmt::skip]
142const HINT_RELIANT_IDS: &[FontId] = &[
143    // MingLiU 1995
144    FontId {
145        cvt: TableId { checksum: 0x05BCF058, len: 0x000002E4 },
146        fpgm: TableId { checksum: 0x28233BF1, len: 0x000087C4 },
147        prep: TableId { checksum: 0xA344A1EA, len: 0x000001E1 },
148    },
149    // MingLiU 1996-
150    FontId {
151        cvt: TableId { checksum: 0x05BCF058, len: 0x000002E4 },
152        fpgm: TableId { checksum: 0x28233BF1, len: 0x000087C4 },
153        prep: TableId { checksum: 0xA344A1EB, len: 0x000001E1 },
154    },
155    // DFGothic-EB
156    FontId {
157        cvt: TableId { checksum: 0x12C3EBB2, len: 0x00000350 },
158        fpgm: TableId { checksum: 0xB680EE64, len: 0x000087A7 },
159        prep: TableId { checksum: 0xCE939563, len: 0x00000758 },
160    },
161    // DFGyoSho-Lt
162    FontId {
163        cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000350 },
164        fpgm: TableId { checksum: 0xCE5956E9, len: 0x0000BC85 },
165        prep: TableId { checksum: 0x8272F416, len: 0x00000045 },
166    },
167    // DFHei-Md-HK-BF
168    FontId {
169        cvt: TableId { checksum: 0x1257EB46, len: 0x00000350 },
170        fpgm: TableId { checksum: 0xF699D160, len: 0x0000715F },
171        prep: TableId { checksum: 0xD222F568, len: 0x000003BC },
172    },
173    // DFHSGothic-W5
174    FontId {
175        cvt: TableId { checksum: 0x1262EB4E, len: 0x00000350 },
176        fpgm: TableId { checksum: 0xE86A5D64, len: 0x00007940 },
177        prep: TableId { checksum: 0x7850F729, len: 0x000005FF },
178    },
179    // DFHSMincho-W3
180    FontId {
181        cvt: TableId { checksum: 0x122DEB0A, len: 0x00000350 },
182        fpgm: TableId { checksum: 0x3D16328A, len: 0x0000859B },
183        prep: TableId { checksum: 0xA93FC33B, len: 0x000002CB },
184    },
185    // DFHSMincho-W7
186    FontId {
187        cvt: TableId { checksum: 0x125FEB26, len: 0x00000350 },
188        fpgm: TableId { checksum: 0xA5ACC982, len: 0x00007EE1 },
189        prep: TableId { checksum: 0x90999196, len: 0x0000041F },
190    },
191    // DFKaiShu
192    FontId {
193        cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000350 },
194        fpgm: TableId { checksum: 0x5A30CA3B, len: 0x00009063 },
195        prep: TableId { checksum: 0x13A42602, len: 0x0000007E },
196    },
197    // DFKaiShu, variant
198    FontId {
199        cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000350 },
200        fpgm: TableId { checksum: 0xA6E78C01, len: 0x00008998 },
201        prep: TableId { checksum: 0x13A42602, len: 0x0000007E },
202    },
203    // DFKaiShu-Md-HK-BF
204    FontId {
205        cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000360 },
206        fpgm: TableId { checksum: 0x9DB282B2, len: 0x0000C06E },
207        prep: TableId { checksum: 0x53E6D7CA, len: 0x00000082 },
208    },
209    // DFMing-Bd-HK-BF
210    FontId {
211        cvt: TableId { checksum: 0x1243EB18, len: 0x00000350 },
212        fpgm: TableId { checksum: 0xBA0A8C30, len: 0x000074AD },
213        prep: TableId { checksum: 0xF3D83409, len: 0x0000037B },
214    },
215    // DLCLiShu
216    FontId {
217        cvt: TableId { checksum: 0x07DCF546, len: 0x00000308 },
218        fpgm: TableId { checksum: 0x40FE7C90, len: 0x00008E2A },
219        prep: TableId { checksum: 0x608174B5, len: 0x0000007A },
220    },
221    // DLCHayBold
222    FontId {
223        cvt: TableId { checksum: 0xEB891238, len: 0x00000308 },
224        fpgm: TableId { checksum: 0xD2E4DCD4, len: 0x0000676F },
225        prep: TableId { checksum: 0x8EA5F293, len: 0x000003B8 },
226    },
227    // HuaTianKaiTi
228    FontId {
229        cvt: TableId { checksum: 0xFFFBFFFC, len: 0x00000008 },
230        fpgm: TableId { checksum: 0x9C9E48B8, len: 0x0000BEA2 },
231        prep: TableId { checksum: 0x70020112, len: 0x00000008 },
232    },
233    // HuaTianSongTi
234    FontId {
235        cvt: TableId { checksum: 0xFFFBFFFC, len: 0x00000008 },
236        fpgm: TableId { checksum: 0x0A5A0483, len: 0x00017C39 },
237        prep: TableId { checksum: 0x70020112, len: 0x00000008 },
238    },
239    // NEC fadpop7.ttf
240    FontId {
241        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
242        fpgm: TableId { checksum: 0x40C92555, len: 0x000000E5 },
243        prep: TableId { checksum: 0xA39B58E3, len: 0x0000117C },
244    },
245    // NEC fadrei5.ttf
246    FontId {
247        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
248        fpgm: TableId { checksum: 0x33C41652, len: 0x000000E5 },
249        prep: TableId { checksum: 0x26D6C52A, len: 0x00000F6A },
250    },
251    // NEC fangot7.ttf
252    FontId {
253        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
254        fpgm: TableId { checksum: 0x6DB1651D, len: 0x0000019D },
255        prep: TableId { checksum: 0x6C6E4B03, len: 0x00002492 },
256    },
257    // NEC fangyo5.ttf
258    FontId {
259        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
260        fpgm: TableId { checksum: 0x40C92555, len: 0x000000E5 },
261        prep: TableId { checksum: 0xDE51FAD0, len: 0x0000117C },
262    },
263    // NEC fankyo5.ttf
264    FontId {
265        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
266        fpgm: TableId { checksum: 0x85E47664, len: 0x000000E5 },
267        prep: TableId { checksum: 0xA6C62831, len: 0x00001CAA },
268    },
269    // NEC fanrgo5.ttf
270    FontId {
271        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
272        fpgm: TableId { checksum: 0x2D891CFD, len: 0x0000019D },
273        prep: TableId { checksum: 0xA0604633, len: 0x00001DE8 },
274    },
275    // NEC fangot5.ttc
276    FontId {
277        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
278        fpgm: TableId { checksum: 0x40AA774C, len: 0x000001CB },
279        prep: TableId { checksum: 0x9B5CAA96, len: 0x00001F9A },
280    },
281    // NEC fanmin3.ttc
282    FontId {
283        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
284        fpgm: TableId { checksum: 0x0D3DE9CB, len: 0x00000141 },
285        prep: TableId { checksum: 0xD4127766, len: 0x00002280 },
286    },
287    // NEC FA-Gothic, 1996
288    FontId {
289        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
290        fpgm: TableId { checksum: 0x4A692698, len: 0x000001F0 },
291        prep: TableId { checksum: 0x340D4346, len: 0x00001FCA },
292    },
293    // NEC FA-Minchou, 1996
294    FontId {
295        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
296        fpgm: TableId { checksum: 0xCD34C604, len: 0x00000166 },
297        prep: TableId { checksum: 0x6CF31046, len: 0x000022B0 },
298    },
299    // NEC FA-RoundGothicB, 1996
300    FontId {
301        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
302        fpgm: TableId { checksum: 0x5DA75315, len: 0x0000019D },
303        prep: TableId { checksum: 0x40745A5F, len: 0x000022E0 },
304    },
305    // NEC FA-RoundGothicM, 1996
306    FontId {
307        cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
308        fpgm: TableId { checksum: 0xF055FC48, len: 0x000001C2 },
309        prep: TableId { checksum: 0x3900DED3, len: 0x00001E18 },
310    },
311    // MINGLI.TTF, 1992
312    FontId {
313        cvt: TableId { checksum: 0x00170003, len: 0x00000060 },
314        fpgm: TableId { checksum: 0xDBB4306E, len: 0x000058AA },
315        prep: TableId { checksum: 0xD643482A, len: 0x00000035 },
316    },
317    // DFHei-Bd-WIN-HK-BF, issue #1087
318    FontId {
319        cvt: TableId { checksum: 0x1269EB58, len: 0x00000350 },
320        fpgm: TableId { checksum: 0x5CD5957A, len: 0x00006A4E },
321        prep: TableId { checksum: 0xF758323A, len: 0x00000380 },
322    },
323    // DFMing-Md-WIN-HK-BF, issue #1087
324    FontId {
325        cvt: TableId { checksum: 0x122FEB0B, len: 0x00000350 },
326        fpgm: TableId { checksum: 0x7F10919A, len: 0x000070A9 },
327        prep: TableId { checksum: 0x7CD7E7B7, len: 0x0000025C },
328    },
329];
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    #[test]
336    fn ensure_max_name_len() {
337        let max_len = HINT_RELIANT_NAMES
338            .iter()
339            .fold(0, |acc, name| acc.max(name.len()));
340        assert_eq!(max_len, MAX_HINT_RELIANT_NAME_LEN);
341    }
342
343    #[test]
344    fn skip_pdf_tags() {
345        // length must be at least 8
346        assert_eq!(skip_pdf_random_tag("ABCDEF+"), "ABCDEF+");
347        // first six chars must be ascii uppercase
348        assert_eq!(skip_pdf_random_tag("AbCdEF+Arial"), "AbCdEF+Arial");
349        // no numbers
350        assert_eq!(skip_pdf_random_tag("Ab12EF+Arial"), "Ab12EF+Arial");
351        // missing +
352        assert_eq!(skip_pdf_random_tag("ABCDEFArial"), "ABCDEFArial");
353        // too long
354        assert_eq!(skip_pdf_random_tag("ABCDEFG+Arial"), "ABCDEFG+Arial");
355        // too short
356        assert_eq!(skip_pdf_random_tag("ABCDE+Arial"), "ABCDE+Arial");
357        // just right
358        assert_eq!(skip_pdf_random_tag("ABCDEF+Arial"), "Arial");
359    }
360
361    #[test]
362    fn all_hint_reliant_names() {
363        for name in HINT_RELIANT_NAMES {
364            assert!(matches_hint_reliant_name_list(name));
365        }
366    }
367
368    #[test]
369    fn non_hint_reliant_names() {
370        for not_tricky in ["Roboto", "Arial", "Helvetica", "Blah", ""] {
371            assert!(!matches_hint_reliant_name_list(not_tricky));
372        }
373    }
374}