1use ttf_parser::{Face, Tag};
20
21#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
28#[repr(u8)]
29pub enum WritingSystem {
30 Latin = 0,
31 Greek,
32 Cyrillic,
33 Armenian,
34 Hebrew,
35 Arabic,
36 Syriac,
37 Thaana,
38 Devanagari,
39 Bengali,
40 Gurmukhi,
41 Gujarati,
42 Oriya,
43 Tamil,
44 Telugu,
45 Kannada,
46 Malayalam,
47 Sinhala,
48 Thai,
49 Lao,
50 Tibetan,
51 Myanmar,
52 Georgian,
53 Khmer,
54 SimplifiedChinese,
55 TraditionalChinese,
56 Japanese,
57 Korean,
58 Vietnamese,
59 Symbol,
60 Ogham,
61 Runic,
62 Nko,
63}
64
65impl WritingSystem {
66 pub const ALL: [WritingSystem; 33] = [
68 WritingSystem::Latin,
69 WritingSystem::Greek,
70 WritingSystem::Cyrillic,
71 WritingSystem::Armenian,
72 WritingSystem::Hebrew,
73 WritingSystem::Arabic,
74 WritingSystem::Syriac,
75 WritingSystem::Thaana,
76 WritingSystem::Devanagari,
77 WritingSystem::Bengali,
78 WritingSystem::Gurmukhi,
79 WritingSystem::Gujarati,
80 WritingSystem::Oriya,
81 WritingSystem::Tamil,
82 WritingSystem::Telugu,
83 WritingSystem::Kannada,
84 WritingSystem::Malayalam,
85 WritingSystem::Sinhala,
86 WritingSystem::Thai,
87 WritingSystem::Lao,
88 WritingSystem::Tibetan,
89 WritingSystem::Myanmar,
90 WritingSystem::Georgian,
91 WritingSystem::Khmer,
92 WritingSystem::SimplifiedChinese,
93 WritingSystem::TraditionalChinese,
94 WritingSystem::Japanese,
95 WritingSystem::Korean,
96 WritingSystem::Vietnamese,
97 WritingSystem::Symbol,
98 WritingSystem::Ogham,
99 WritingSystem::Runic,
100 WritingSystem::Nko,
101 ];
102
103 pub const fn id(self) -> &'static str {
106 match self {
107 WritingSystem::Latin => "latin",
108 WritingSystem::Greek => "greek",
109 WritingSystem::Cyrillic => "cyrillic",
110 WritingSystem::Armenian => "armenian",
111 WritingSystem::Hebrew => "hebrew",
112 WritingSystem::Arabic => "arabic",
113 WritingSystem::Syriac => "syriac",
114 WritingSystem::Thaana => "thaana",
115 WritingSystem::Devanagari => "devanagari",
116 WritingSystem::Bengali => "bengali",
117 WritingSystem::Gurmukhi => "gurmukhi",
118 WritingSystem::Gujarati => "gujarati",
119 WritingSystem::Oriya => "oriya",
120 WritingSystem::Tamil => "tamil",
121 WritingSystem::Telugu => "telugu",
122 WritingSystem::Kannada => "kannada",
123 WritingSystem::Malayalam => "malayalam",
124 WritingSystem::Sinhala => "sinhala",
125 WritingSystem::Thai => "thai",
126 WritingSystem::Lao => "lao",
127 WritingSystem::Tibetan => "tibetan",
128 WritingSystem::Myanmar => "myanmar",
129 WritingSystem::Georgian => "georgian",
130 WritingSystem::Khmer => "khmer",
131 WritingSystem::SimplifiedChinese => "simplified-chinese",
132 WritingSystem::TraditionalChinese => "traditional-chinese",
133 WritingSystem::Japanese => "japanese",
134 WritingSystem::Korean => "korean",
135 WritingSystem::Vietnamese => "vietnamese",
136 WritingSystem::Symbol => "symbol",
137 WritingSystem::Ogham => "ogham",
138 WritingSystem::Runic => "runic",
139 WritingSystem::Nko => "nko",
140 }
141 }
142
143 pub const fn english_name(self) -> &'static str {
147 match self {
148 WritingSystem::Latin => "Latin",
149 WritingSystem::Greek => "Greek",
150 WritingSystem::Cyrillic => "Cyrillic",
151 WritingSystem::Armenian => "Armenian",
152 WritingSystem::Hebrew => "Hebrew",
153 WritingSystem::Arabic => "Arabic",
154 WritingSystem::Syriac => "Syriac",
155 WritingSystem::Thaana => "Thaana",
156 WritingSystem::Devanagari => "Devanagari",
157 WritingSystem::Bengali => "Bengali",
158 WritingSystem::Gurmukhi => "Gurmukhi",
159 WritingSystem::Gujarati => "Gujarati",
160 WritingSystem::Oriya => "Oriya",
161 WritingSystem::Tamil => "Tamil",
162 WritingSystem::Telugu => "Telugu",
163 WritingSystem::Kannada => "Kannada",
164 WritingSystem::Malayalam => "Malayalam",
165 WritingSystem::Sinhala => "Sinhala",
166 WritingSystem::Thai => "Thai",
167 WritingSystem::Lao => "Lao",
168 WritingSystem::Tibetan => "Tibetan",
169 WritingSystem::Myanmar => "Myanmar",
170 WritingSystem::Georgian => "Georgian",
171 WritingSystem::Khmer => "Khmer",
172 WritingSystem::SimplifiedChinese => "Simplified Chinese",
173 WritingSystem::TraditionalChinese => "Traditional Chinese",
174 WritingSystem::Japanese => "Japanese",
175 WritingSystem::Korean => "Korean",
176 WritingSystem::Vietnamese => "Vietnamese",
177 WritingSystem::Symbol => "Symbol",
178 WritingSystem::Ogham => "Ogham",
179 WritingSystem::Runic => "Runic",
180 WritingSystem::Nko => "N'Ko",
181 }
182 }
183
184 pub const fn sample_text(self) -> &'static str {
187 match self {
188 WritingSystem::Latin => "Aa Bb Yy Zz",
189 WritingSystem::Greek => "Αα Ββ Γγ",
190 WritingSystem::Cyrillic => "Аа Бб Вв",
191 WritingSystem::Armenian => "Աա Բբ Գգ",
192 WritingSystem::Hebrew => "אבּג",
193 WritingSystem::Arabic => "أبجد",
194 WritingSystem::Syriac => "ܐܒܓ",
195 WritingSystem::Thaana => "ހށނ",
196 WritingSystem::Devanagari => "अआइ",
197 WritingSystem::Bengali => "অআই",
198 WritingSystem::Gurmukhi => "ਅਆਇ",
199 WritingSystem::Gujarati => "અઆઇ",
200 WritingSystem::Oriya => "ଅଆଇ",
201 WritingSystem::Tamil => "அஆஇ",
202 WritingSystem::Telugu => "అఆఇ",
203 WritingSystem::Kannada => "ಅಆಇ",
204 WritingSystem::Malayalam => "അആഇ",
205 WritingSystem::Sinhala => "අආඇ",
206 WritingSystem::Thai => "กขค",
207 WritingSystem::Lao => "ກຂຄ",
208 WritingSystem::Tibetan => "ཀཁག",
209 WritingSystem::Myanmar => "ကခဂ",
210 WritingSystem::Georgian => "აბგ",
211 WritingSystem::Khmer => "កខគ",
212 WritingSystem::SimplifiedChinese => "汉字样本",
213 WritingSystem::TraditionalChinese => "漢字樣本",
214 WritingSystem::Japanese => "あア漢字",
215 WritingSystem::Korean => "한국어",
216 WritingSystem::Vietnamese => "Tiếng Việt",
217 WritingSystem::Symbol => "★ ♦ ♣ ♠",
218 WritingSystem::Ogham => "ᚁᚂᚃ",
219 WritingSystem::Runic => "ᚠᚢᚦ",
220 WritingSystem::Nko => "ߊߋߌ",
221 }
222 }
223}
224
225#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
229pub struct WritingSystemSet(u128);
230
231impl WritingSystemSet {
232 pub const fn new() -> Self {
234 Self(0)
235 }
236
237 pub fn insert(&mut self, ws: WritingSystem) {
239 self.0 |= 1u128 << (ws as u8);
240 }
241
242 pub const fn contains(self, ws: WritingSystem) -> bool {
244 self.0 & (1u128 << (ws as u8)) != 0
245 }
246
247 pub const fn is_empty(self) -> bool {
249 self.0 == 0
250 }
251
252 pub const fn len(self) -> u32 {
254 self.0.count_ones()
255 }
256
257 pub const fn union(self, other: Self) -> Self {
259 Self(self.0 | other.0)
260 }
261
262 pub fn iter(self) -> impl Iterator<Item = WritingSystem> {
264 WritingSystem::ALL
265 .into_iter()
266 .filter(move |&ws| self.contains(ws))
267 }
268}
269
270const UNICODE_RANGE_BITS: &[(u8, WritingSystem)] = &[
275 (0, WritingSystem::Latin), (7, WritingSystem::Greek), (9, WritingSystem::Cyrillic), (10, WritingSystem::Armenian), (11, WritingSystem::Hebrew), (13, WritingSystem::Arabic), (14, WritingSystem::Nko), (15, WritingSystem::Devanagari), (16, WritingSystem::Bengali), (17, WritingSystem::Gurmukhi), (18, WritingSystem::Gujarati), (19, WritingSystem::Oriya), (20, WritingSystem::Tamil), (21, WritingSystem::Telugu), (22, WritingSystem::Kannada), (23, WritingSystem::Malayalam), (24, WritingSystem::Thai), (25, WritingSystem::Lao), (26, WritingSystem::Georgian), (28, WritingSystem::Korean), (49, WritingSystem::Japanese), (50, WritingSystem::Japanese), (52, WritingSystem::Korean), (56, WritingSystem::Korean), (70, WritingSystem::Tibetan), (71, WritingSystem::Syriac), (72, WritingSystem::Thaana), (73, WritingSystem::Sinhala), (74, WritingSystem::Myanmar), (78, WritingSystem::Ogham), (79, WritingSystem::Runic), (80, WritingSystem::Khmer), ];
308
309const CODEPAGE_RANGE1_BITS: &[(u8, WritingSystem)] = &[
312 (8, WritingSystem::Vietnamese), (16, WritingSystem::Thai), (17, WritingSystem::Japanese), (18, WritingSystem::SimplifiedChinese), (19, WritingSystem::Korean), (20, WritingSystem::TraditionalChinese), (21, WritingSystem::Korean), ];
320
321const CMAP_PROBES: &[(WritingSystem, &[char])] = &[
327 (WritingSystem::Latin, &['A', 'z']),
328 (WritingSystem::Greek, &['Α', 'ω']),
329 (WritingSystem::Cyrillic, &['А', 'я']),
330 (WritingSystem::Armenian, &['Ա', 'ֆ']),
331 (WritingSystem::Hebrew, &['א', 'ת']),
332 (WritingSystem::Arabic, &['ا', 'ي']),
333 (WritingSystem::Syriac, &['ܐ']),
334 (WritingSystem::Thaana, &['ހ']),
335 (WritingSystem::Devanagari, &['अ', 'ह']),
336 (WritingSystem::Bengali, &['অ']),
337 (WritingSystem::Gurmukhi, &['ਅ']),
338 (WritingSystem::Gujarati, &['અ']),
339 (WritingSystem::Oriya, &['ଅ']),
340 (WritingSystem::Tamil, &['அ']),
341 (WritingSystem::Telugu, &['అ']),
342 (WritingSystem::Kannada, &['ಅ']),
343 (WritingSystem::Malayalam, &['അ']),
344 (WritingSystem::Sinhala, &['අ']),
345 (WritingSystem::Thai, &['ก', 'ฮ']),
346 (WritingSystem::Lao, &['ກ']),
347 (WritingSystem::Tibetan, &['ཀ']),
348 (WritingSystem::Myanmar, &['က']),
349 (WritingSystem::Georgian, &['ა', 'ჰ']),
350 (WritingSystem::Khmer, &['ក']),
351 (WritingSystem::Japanese, &['あ', 'ア']),
352 (WritingSystem::Korean, &['가', '한']),
353 (WritingSystem::Vietnamese, &['ế', 'ệ']),
354 (WritingSystem::Ogham, &['ᚁ']),
355 (WritingSystem::Runic, &['ᚠ']),
356 (WritingSystem::Nko, &['ߊ']),
357];
358
359const OS2_HAN_BIT: u8 = 59;
361
362pub fn writing_systems_for_face(bytes: &[u8], index: u32) -> WritingSystemSet {
369 let mut set = WritingSystemSet::new();
370 let Ok(face) = Face::parse(bytes, index) else {
371 return set;
372 };
373
374 let unicode_bits = face.tables().os2.map(|t| t.unicode_ranges().0).unwrap_or(0);
376 for &(bit, ws) in UNICODE_RANGE_BITS {
377 if unicode_bits & (1u128 << bit) != 0 {
378 set.insert(ws);
379 }
380 }
381 let has_han = unicode_bits & (1u128 << OS2_HAN_BIT) != 0;
382
383 if let Some(cp1) = raw_code_page_range1(&face) {
386 for &(bit, ws) in CODEPAGE_RANGE1_BITS {
387 if cp1 & (1u32 << bit) != 0 {
388 set.insert(ws);
389 }
390 }
391 }
392
393 if has_han && !has_any_cjk_language(set) {
397 set.insert(WritingSystem::SimplifiedChinese);
398 set.insert(WritingSystem::TraditionalChinese);
399 }
400
401 for &(ws, probes) in CMAP_PROBES {
403 if !set.contains(ws) && probes.iter().all(|&c| face.glyph_index(c).is_some()) {
404 set.insert(ws);
405 }
406 }
407 if !has_any_cjk_language(set) && ['中', '文'].iter().all(|&c| face.glyph_index(c).is_some()) {
409 set.insert(WritingSystem::SimplifiedChinese);
410 set.insert(WritingSystem::TraditionalChinese);
411 }
412
413 if set.is_empty() {
416 set.insert(WritingSystem::Symbol);
417 }
418
419 set
420}
421
422const fn has_any_cjk_language(set: WritingSystemSet) -> bool {
425 set.contains(WritingSystem::SimplifiedChinese)
426 || set.contains(WritingSystem::TraditionalChinese)
427 || set.contains(WritingSystem::Japanese)
428 || set.contains(WritingSystem::Korean)
429}
430
431fn raw_code_page_range1(face: &Face) -> Option<u32> {
438 let data = face.raw_face().table(Tag::from_bytes(b"OS/2"))?;
439 if data.len() < 82 {
441 return None;
442 }
443 let version = u16::from_be_bytes([data[0], data[1]]);
444 if version < 1 {
445 return None; }
447 Some(u32::from_be_bytes([data[78], data[79], data[80], data[81]]))
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453
454 #[test]
455 fn set_insert_contains_iter() {
456 let mut set = WritingSystemSet::new();
457 assert!(set.is_empty());
458 set.insert(WritingSystem::Latin);
459 set.insert(WritingSystem::Arabic);
460 assert!(set.contains(WritingSystem::Latin));
461 assert!(set.contains(WritingSystem::Arabic));
462 assert!(!set.contains(WritingSystem::Greek));
463 assert_eq!(set.len(), 2);
464 let collected: Vec<_> = set.iter().collect();
465 assert_eq!(collected, vec![WritingSystem::Latin, WritingSystem::Arabic]);
466 }
467
468 #[test]
469 fn all_ids_unique_and_stable() {
470 let mut ids: Vec<&str> = WritingSystem::ALL.iter().map(|w| w.id()).collect();
471 let n = ids.len();
472 ids.sort_unstable();
473 ids.dedup();
474 assert_eq!(ids.len(), n, "writing-system ids must be unique");
475 assert_eq!(n, 33);
476 }
477
478 #[test]
479 fn every_system_has_a_sample() {
480 for ws in WritingSystem::ALL {
481 assert!(!ws.sample_text().is_empty(), "{ws:?} has no sample text");
482 assert!(!ws.english_name().is_empty());
483 }
484 }
485}