1use std::borrow::Cow;
9
10use serde_json::{Map, Value, json};
11use ttf_parser::{Face, PlatformId};
12
13const NAME_KEYS: [&str; 23] = [
15 "copyright",
16 "fontFamily",
17 "fontSubfamily",
18 "uniqueID",
19 "fullName",
20 "version",
21 "postScriptName",
22 "trademark",
23 "manufacturer",
24 "designer",
25 "description",
26 "manufacturerURL",
27 "designerURL",
28 "license",
29 "licenseURL",
30 "reserved",
31 "preferredFamily",
32 "preferredSubfamily",
33 "compatibleFullName",
34 "sampleText",
35 "postScriptFindFontName",
36 "wwsFamily",
37 "wwsSubfamily",
38];
39
40const WEIGHT_WORDS: [(&str, &str); 16] = [
42 ("thin", "Thin"),
43 ("hairline", "Hairline"),
44 ("extralight", "ExtraLight"),
45 ("ultralight", "UltraLight"),
46 ("light", "Light"),
47 ("regular", "Regular"),
48 ("normal", "Regular"),
49 ("medium", "Medium"),
50 ("demibold", "DemiBold"),
51 ("semibold", "SemiBold"),
52 ("bold", "Bold"),
53 ("extrabold", "ExtraBold"),
54 ("ultrabold", "UltraBold"),
55 ("black", "Black"),
56 ("extrablack", "ExtraBlack"),
57 ("heavy", "Heavy"),
58];
59
60fn windows_language(id: u16) -> Option<&'static str> {
63 Some(match id {
64 0x0409 => "en",
65 0x0809 => "en-GB",
66 0x0C09 => "en-AU",
67 0x1009 => "en-CA",
68 0x0804 => "zh",
69 0x0404 => "zh-TW",
70 0x0C04 => "zh-HK",
71 0x1004 => "zh-SG",
72 0x1404 => "zh-MO",
73 0x0411 => "ja",
74 0x0412 => "ko",
75 0x040C => "fr",
76 0x0C0C => "fr-CA",
77 0x0407 => "de",
78 0x0410 => "it",
79 0x0C0A => "es",
80 0x040A => "es",
81 0x0416 => "pt",
82 0x0816 => "pt-PT",
83 0x0413 => "nl",
84 0x0419 => "ru",
85 0x041D => "sv",
86 0x0406 => "da",
87 0x0414 => "nb",
88 0x040B => "fi",
89 0x0415 => "pl",
90 0x0405 => "cs",
91 0x040E => "hu",
92 0x0408 => "el",
93 0x041F => "tr",
94 0x040D => "he",
95 0x0401 => "ar",
96 0x041E => "th",
97 0x042A => "vi",
98 0x0421 => "id",
99 0x0422 => "uk",
100 0x0418 => "ro",
101 0x041B => "sk",
102 0x0424 => "sl",
103 0x041A => "hr",
104 0x0402 => "bg",
105 0x0403 => "ca",
106 0x042D => "eu",
107 _ => return None,
108 })
109}
110
111fn mac_language(id: u16) -> Option<&'static str> {
113 Some(match id {
114 0 => "en",
115 1 => "fr",
116 2 => "de",
117 3 => "it",
118 4 => "nl",
119 5 => "sv",
120 6 => "es",
121 7 => "da",
122 8 => "pt",
123 9 => "no",
124 13 => "fi",
125 _ => return None,
126 })
127}
128
129const MAC_ROMAN_HIGH: &str = "ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø¿¡¬√ƒ≈∆«»…\u{a0}ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄€‹›fifl‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔ\u{f8ff}ÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ";
131
132fn mac_roman(bytes: &[u8]) -> String {
133 let high: Vec<char> = MAC_ROMAN_HIGH.chars().collect();
134 bytes.iter().map(|&b| if b < 0x80 { b as char } else { high.get(usize::from(b - 0x80)).copied().unwrap_or('\u{fffd}') }).collect()
135}
136
137fn utf16_be(bytes: &[u8]) -> String {
138 let units: Vec<u16> = bytes.as_chunks::<2>().0.iter().map(|c| u16::from_be_bytes(*c)).collect();
139 String::from_utf16_lossy(&units)
140}
141
142pub fn sfnt(data: &[u8]) -> Option<Cow<'_, [u8]>> {
147 match data.get(..4) {
148 Some(b"wOFF") => woff1_tables(data).map(|(flavor, t)| Cow::Owned(build_sfnt(flavor, &t))),
149 Some(b"wOF2") => woff2_tables(data).map(|(flavor, t)| Cow::Owned(build_sfnt(flavor, &t))),
150 _ => Some(Cow::Borrowed(data)),
151 }
152}
153
154fn be16(data: &[u8], o: usize) -> Option<u16> {
155 data.get(o..o + 2).map(|b| u16::from_be_bytes([b[0], b[1]]))
156}
157
158fn be32(data: &[u8], o: usize) -> Option<u32> {
159 data.get(o..o + 4).map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
160}
161
162type Table = (u32, u32, Vec<u8>);
164
165fn woff1_tables(data: &[u8]) -> Option<(u32, Vec<Table>)> {
166 let flavor = be32(data, 4)?;
167 let num = be16(data, 12)?;
168 let mut tables = Vec::with_capacity(num.into());
169 for i in 0..usize::from(num) {
170 let e = 44 + i * 20;
171 let tag = be32(data, e)?;
172 let (off, comp, orig, sum) = (be32(data, e + 4)? as usize, be32(data, e + 8)? as usize, be32(data, e + 12)? as usize, be32(data, e + 16)?);
173 let raw = data.get(off..off.checked_add(comp)?)?;
174 let bytes = if comp < orig { miniz_oxide::inflate::decompress_to_vec_zlib(raw).ok()? } else { raw.to_vec() };
175 if bytes.len() != orig {
176 return None;
177 }
178 tables.push((tag, sum, bytes));
179 }
180 Some((flavor, tables))
181}
182
183const WOFF2_TAGS: [&[u8; 4]; 63] = [
186 b"cmap", b"head", b"hhea", b"hmtx", b"maxp", b"name", b"OS/2", b"post", b"cvt ", b"fpgm", b"glyf", b"loca", b"prep", b"CFF ", b"VORG",
187 b"EBDT", b"EBLC", b"gasp", b"hdmx", b"kern", b"LTSH", b"PCLT", b"VDMX", b"vhea", b"vmtx", b"BASE", b"GDEF", b"GPOS", b"GSUB", b"EBSC",
188 b"JSTF", b"MATH", b"CBDT", b"CBLC", b"COLR", b"CPAL", b"SVG ", b"sbix", b"acnt", b"avar", b"bdat", b"bloc", b"bsln", b"cvar", b"fdsc",
189 b"feat", b"fmtx", b"fvar", b"gvar", b"hsty", b"just", b"lcar", b"mort", b"morx", b"opbd", b"prop", b"trak", b"Zapf", b"Silf", b"Glat",
190 b"Gloc", b"Feat", b"Sill",
191];
192
193fn base128(data: &[u8], at: &mut usize) -> Option<u32> {
196 let mut v: u32 = 0;
197 for i in 0..5 {
198 let b = *data.get(*at)?;
199 *at += 1;
200 if i == 0 && b == 0x80 {
201 return None;
202 }
203 if v & 0xFE00_0000 != 0 {
204 return None;
205 }
206 v = (v << 7) | u32::from(b & 0x7F);
207 if b & 0x80 == 0 {
208 return Some(v);
209 }
210 }
211 None
212}
213
214fn woff2_tables(data: &[u8]) -> Option<(u32, Vec<Table>)> {
217 use std::io::Read;
218 let flavor = be32(data, 4)?;
219 if &flavor.to_be_bytes() == b"ttcf" {
220 return None;
221 }
222 let num = usize::from(be16(data, 12)?);
223 let compressed_len = be32(data, 20)? as usize;
224 let mut at = 48;
225 let mut entries = Vec::with_capacity(num);
227 for _ in 0..num {
228 let flags = *data.get(at)?;
229 at += 1;
230 let tag = match usize::from(flags & 0x3F) {
231 63 => {
232 let t = be32(data, at)?;
233 at += 4;
234 t
235 }
236 i => u32::from_be_bytes(*WOFF2_TAGS[i]),
237 };
238 let version = flags >> 6;
239 let orig = base128(data, &mut at)?;
240 let glyf_or_loca = &tag.to_be_bytes() == b"glyf" || &tag.to_be_bytes() == b"loca";
241 let transformed = if glyf_or_loca { version != 3 } else { version != 0 };
244 let stored = if transformed { base128(data, &mut at)? } else { orig };
245 entries.push((tag, stored as usize, !transformed));
246 }
247 let total: usize = entries.iter().try_fold(0usize, |acc, e| acc.checked_add(e.1))?;
248 let stream = data.get(at..at.checked_add(compressed_len)?)?;
249 let mut raw = Vec::with_capacity(total.min(64 << 20));
250 brotli_decompressor::Decompressor::new(stream, 4096).take(total as u64).read_to_end(&mut raw).ok()?;
251 if raw.len() != total {
252 return None;
253 }
254 let mut tables = Vec::new();
255 let mut off = 0;
256 for (tag, len, keep) in entries {
257 let bytes = &raw[off..off + len];
258 off += len;
259 if keep {
260 tables.push((tag, checksum(bytes), bytes.to_vec()));
261 }
262 }
263 tables.sort_by_key(|t| t.0);
264 Some((flavor, tables))
265}
266
267fn checksum(bytes: &[u8]) -> u32 {
269 bytes.chunks(4).fold(0u32, |sum, c| {
270 let mut w = [0u8; 4];
271 w[..c.len()].copy_from_slice(c);
272 sum.wrapping_add(u32::from_be_bytes(w))
273 })
274}
275
276fn build_sfnt(flavor: u32, tables: &[Table]) -> Vec<u8> {
279 let num = tables.len() as u16;
280 let mut pow = 1u16;
281 let mut log = 0u16;
282 while pow * 2 <= num {
283 pow *= 2;
284 log += 1;
285 }
286 let mut out = Vec::new();
287 out.extend_from_slice(&flavor.to_be_bytes());
288 for v in [num, pow * 16, log, (num * 16).saturating_sub(pow * 16)] {
289 out.extend_from_slice(&v.to_be_bytes());
290 }
291 let mut offset = 12 + tables.len() * 16;
292 for (tag, sum, bytes) in tables {
293 out.extend_from_slice(&tag.to_be_bytes());
294 out.extend_from_slice(&sum.to_be_bytes());
295 out.extend_from_slice(&(offset as u32).to_be_bytes());
296 out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
297 offset += bytes.len().div_ceil(4) * 4;
298 }
299 for (_, _, bytes) in tables {
300 out.extend_from_slice(bytes);
301 out.resize(out.len().div_ceil(4) * 4, 0);
302 }
303 out
304}
305
306pub fn parse(data: &[u8]) -> Option<Face<'_>> {
309 Face::parse(data, 0).ok()
310}
311
312pub fn names(face: &Face) -> Map<String, Value> {
316 let mut out = Map::new();
317 for n in face.names() {
318 let (lang, text) = match n.platform_id {
319 PlatformId::Windows if matches!(n.encoding_id, 1 | 10) => (windows_language(n.language_id), utf16_be(n.name)),
320 PlatformId::Macintosh if n.encoding_id == 0 => (mac_language(n.language_id), mac_roman(n.name)),
321 PlatformId::Unicode if n.language_id == 0xFFFF => (Some("und"), utf16_be(n.name)),
322 _ => continue,
323 };
324 let Some(lang) = lang else { continue };
325 if text.is_empty() {
326 continue;
327 }
328 let key = NAME_KEYS.get(usize::from(n.name_id)).map_or_else(|| n.name_id.to_string(), |k| k.to_string());
329 let entry = out.entry(key).or_insert_with(|| Value::Object(Map::new()));
330 if let Value::Object(m) = entry {
331 m.insert(lang.to_string(), Value::String(text));
332 }
333 }
334 out
335}
336
337fn first_value<'a>(m: &'a Map<String, Value>, key: &str) -> Option<&'a str> {
338 m.get(key)?.as_object()?.values().next()?.as_str()
339}
340
341fn en_value<'a>(m: &'a Map<String, Value>, key: &str) -> Option<&'a str> {
342 m.get(key)?.get("en")?.as_str()
343}
344
345pub fn weight(metas: &Map<String, Value>) -> String {
348 let ps = first_value(metas, "postScriptName").unwrap_or("").to_lowercase();
349 if ps.is_empty() {
350 return "Regular".into();
351 }
352 let mut w = WEIGHT_WORDS.iter().find(|(word, _)| ps.contains(word)).map(|(_, w)| w.to_string());
353 if w.is_none() {
354 w = Some(en_value(metas, "preferredSubfamily").or_else(|| en_value(metas, "fontSubfamily")).unwrap_or("Regular").to_string());
355 }
356 let mut w = w.filter(|w| WEIGHT_WORDS.iter().any(|(_, x)| x == w)).unwrap_or_else(|| "Regular".into());
357 if !w.to_lowercase().contains("italic") && ps.contains("italic") {
358 w.push_str(" Italic");
359 }
360 w
361}
362
363#[derive(Debug, Clone, Default, PartialEq)]
367pub struct FontInfo {
368 pub postscript: String,
369 pub full_name: String,
370 pub family: String,
371 pub style: String,
373 pub version: String,
374 pub designer: String,
375 pub manufacturer: String,
376 pub description: String,
377 pub copyright: String,
379 pub trademark: String,
380 pub glyphs: Option<u64>,
381}
382
383impl FontInfo {
384 pub fn from_metas(metas: &Map<String, Value>) -> FontInfo {
385 let name = |k: &str| en_value(metas, k).or_else(|| first_value(metas, k)).unwrap_or("").trim().to_string();
386 let copyright = name("copyright");
387 FontInfo {
388 postscript: name("postScriptName"),
389 full_name: name("fullName"),
390 family: name("fontFamily"),
391 style: metas.get("weight").and_then(Value::as_str).unwrap_or("").to_string(),
392 version: name("version"),
393 designer: name("designer"),
394 manufacturer: name("manufacturer"),
395 description: name("description"),
396 copyright: if copyright.is_empty() { name("license") } else { copyright },
397 trademark: name("trademark"),
398 glyphs: metas.get("numGlyphs").and_then(Value::as_u64),
399 }
400 }
401}
402
403pub const SAMPLE_LANGUAGES: [(&str, &str); 5] =
406 [("en", "English"), ("jp", "日本語"), ("kr", "한국어"), ("zh_CN", "简体中文"), ("zh_TW", "繁體中文")];
407
408pub fn font_metas(data: &[u8], fallback_id: &str) -> Option<Map<String, Value>> {
411 let data = sfnt(data)?;
412 let face = parse(&data)?;
413 let mut metas = names(&face);
414 if metas.is_empty() {
415 metas.insert("postScriptName".into(), json!({ "en": fallback_id }));
416 metas.insert("fullName".into(), json!({ "en": fallback_id }));
417 }
418 if !metas.contains_key("postScriptName") {
419 let id = en_value(&metas, "uniqueID").unwrap_or(fallback_id).to_string();
420 metas.insert("postScriptName".into(), json!({ "en": id }));
421 }
422 metas.insert("numGlyphs".into(), json!(face.number_of_glyphs()));
423 let has = |s: &str| s.chars().all(|c| face.glyph_index(c).is_some_and(|g| g.0 > 0));
424 let mut support = Map::new();
425 if has("龙岁") {
426 support.insert("zh_CN".into(), json!(true));
427 }
428 if has("実匁図") {
429 support.insert("jp".into(), json!(true));
430 }
431 if has("陳鑿發") {
432 support.insert("zh_TW".into(), json!(true));
433 }
434 if has("별았") {
435 support.insert("kr".into(), json!(true));
436 }
437 metas.insert("support".into(), Value::Object(support));
438 let w = weight(&metas);
439 metas.insert("weight".into(), json!(w));
440 Some(metas)
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 fn metas(ps: &str, sub: Option<&str>) -> Map<String, Value> {
448 let mut m = Map::new();
449 m.insert("postScriptName".into(), json!({ "en": ps }));
450 if let Some(s) = sub {
451 m.insert("fontSubfamily".into(), json!({ "en": s }));
452 }
453 m
454 }
455
456 #[test]
457 fn weights() {
458 assert_eq!(weight(&metas("Inter-Thin", None)), "Thin");
459 assert_eq!(weight(&metas("Inter-SemiBoldItalic", None)), "SemiBold Italic");
460 assert_eq!(weight(&metas("Inter-ExtraBold", None)), "Bold");
462 assert_eq!(weight(&metas("Foo", Some("Medium"))), "Medium");
463 assert_eq!(weight(&metas("Foo", Some("Oblique"))), "Regular");
464 assert_eq!(weight(&metas("Foo-Italic", None)), "Regular Italic");
465 assert_eq!(weight(&Map::new()), "Regular");
466 }
467
468 #[test]
469 fn info_prefers_english_names() {
470 let m = json!({
471 "copyright": {"en": "(c) Someone"},
472 "fontFamily": {"zh": "字体", "en": "Ziti"},
473 "fullName": {"ja": "フル"},
474 "postScriptName": {"en": " Ziti-Bold "},
475 "license": {"en": "OFL"},
476 "numGlyphs": 1234,
477 "weight": "Bold",
478 });
479 let info = FontInfo::from_metas(m.as_object().unwrap());
480 assert_eq!(info.family, "Ziti");
481 assert_eq!(info.full_name, "フル", "the first language when there is no English");
482 assert_eq!(info.postscript, "Ziti-Bold", "trimmed");
483 assert_eq!(info.copyright, "(c) Someone");
484 assert_eq!((info.style.as_str(), info.glyphs), ("Bold", Some(1234)));
485 assert_eq!(info.designer, "");
486 let no_copyright = FontInfo::from_metas(json!({"license": {"en": "OFL"}}).as_object().unwrap());
487 assert_eq!(no_copyright.copyright, "OFL", "the licence stands in for a missing copyright");
488 assert_eq!(FontInfo::from_metas(&Map::new()), FontInfo::default());
489 }
490
491 #[test]
492 fn mac_roman_text() {
493 assert_eq!(MAC_ROMAN_HIGH.chars().count(), 128);
494 assert_eq!(mac_roman(b"Caf\x8e \xa9 \xff"), "Café © ˇ");
495 }
496
497 #[test]
498 fn system_font() {
499 let path = ["/System/Library/Fonts/Supplemental/Arial.ttf", "/System/Library/Fonts/Helvetica.ttc"]
500 .into_iter()
501 .find(|p| std::path::Path::new(p).is_file());
502 let Some(path) = path else { return };
503 let data = std::fs::read(path).unwrap();
504 let m = font_metas(&data, "X").unwrap();
505 assert!(first_value(&m, "fontFamily").is_some());
506 assert!(first_value(&m, "postScriptName").is_some());
507 let keys: Vec<&str> = m.keys().map(|k| k.as_str()).collect();
508 assert_eq!(&keys[keys.len() - 3..], ["numGlyphs", "support", "weight"]);
509 assert!(m["numGlyphs"].as_u64().unwrap() > 100);
510 }
511
512 #[test]
513 fn woff_round_trip() {
514 let Ok(data) = std::fs::read("/System/Library/Fonts/Supplemental/Arial.ttf") else { return };
515 let face = ttf_parser::RawFace::parse(&data, 0).unwrap();
517 let records: Vec<_> = face.table_records.into_iter().collect();
518 let mut dir = Vec::new();
519 let mut blobs = Vec::new();
520 let mut offset = 44 + records.len() * 20;
521 for r in &records {
522 let raw = &data[r.offset as usize..(r.offset + r.length) as usize];
523 let comp = miniz_oxide::deflate::compress_to_vec_zlib(raw, 6);
524 let body = if comp.len() < raw.len() { comp } else { raw.to_vec() };
525 dir.extend_from_slice(&r.tag.as_u32().to_be_bytes());
526 dir.extend_from_slice(&(offset as u32).to_be_bytes());
527 dir.extend_from_slice(&(body.len() as u32).to_be_bytes());
528 dir.extend_from_slice(&r.length.to_be_bytes());
529 dir.extend_from_slice(&r.check_sum.to_be_bytes());
530 offset += body.len().div_ceil(4) * 4;
531 let mut body = body;
532 body.resize(body.len().div_ceil(4) * 4, 0);
533 blobs.push(body);
534 }
535 let mut woff = b"wOFF".to_vec();
536 woff.extend_from_slice(&data[..4]);
537 woff.extend_from_slice(&(offset as u32).to_be_bytes());
538 woff.extend_from_slice(&(records.len() as u16).to_be_bytes());
539 woff.resize(44, 0);
540 woff.extend(dir);
541 for b in blobs {
542 woff.extend(b);
543 }
544 let a = font_metas(&data, "X").unwrap();
545 let b = font_metas(&woff, "X").unwrap();
546 assert_eq!(a, b);
547 }
548
549 fn brotli_stored(data: &[u8]) -> Vec<u8> {
554 let mut out = Vec::new();
555 let mut first = true;
556 for chunk in data.chunks(65536) {
557 let len = (chunk.len() - 1) as u32;
558 let (bits, n) = if first { ((len << 4) | (1 << 20), 21) } else { ((len << 3) | (1 << 19), 20) };
560 first = false;
561 for i in 0..(n as usize).div_ceil(8) {
562 out.push((bits >> (8 * i)) as u8);
563 }
564 out.extend_from_slice(chunk);
565 }
566 out.push(if first { 0b110 } else { 0b11 });
568 out
569 }
570
571 fn base128_bytes(mut v: u32) -> Vec<u8> {
572 let mut groups = vec![(v & 0x7F) as u8];
573 v >>= 7;
574 while v > 0 {
575 groups.push((v & 0x7F) as u8 | 0x80);
576 v >>= 7;
577 }
578 groups.reverse();
579 groups
580 }
581
582 fn woff2(data: &[u8], transform_glyf: bool) -> Vec<u8> {
586 let face = ttf_parser::RawFace::parse(data, 0).unwrap();
587 let mut dir = Vec::new();
588 let mut stream = Vec::new();
589 for r in face.table_records {
590 let raw = &data[r.offset as usize..(r.offset + r.length) as usize];
591 let tag = r.tag.to_bytes();
592 let index = [b"cmap", b"head", b"hhea", b"hmtx", b"maxp", b"name"].iter().position(|t| **t == tag);
593 let glyf_or_loca = &tag == b"glyf" || &tag == b"loca";
594 let version: u8 = if glyf_or_loca && !transform_glyf { 3 } else { 0 };
595 dir.push((version << 6) | index.map_or(63, |i| i as u8));
596 if index.is_none() {
597 dir.extend_from_slice(&tag);
598 }
599 dir.extend(base128_bytes(r.length));
600 if glyf_or_loca && transform_glyf {
601 let junk = if &tag == b"glyf" { vec![7u8; 100] } else { Vec::new() };
602 dir.extend(base128_bytes(junk.len() as u32));
603 stream.extend(junk);
604 } else {
605 stream.extend_from_slice(raw);
606 }
607 }
608 let comp = brotli_stored(&stream);
609 let mut out = b"wOF2".to_vec();
610 out.extend_from_slice(&data[..4]);
611 out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&face.table_records.len().to_be_bytes());
613 out.extend_from_slice(&[0, 0]);
614 out.extend_from_slice(&(data.len() as u32).to_be_bytes());
615 out.extend_from_slice(&(comp.len() as u32).to_be_bytes());
616 out.resize(48, 0);
617 out.extend(dir);
618 out.extend(comp);
619 let n = out.len() as u32;
620 out[8..12].copy_from_slice(&n.to_be_bytes());
621 out
622 }
623
624 #[test]
625 fn base128_reads_the_spec_examples() {
626 let read = |b: &[u8]| base128(b, &mut 0);
627 assert_eq!(read(&[0x3F]), Some(63));
628 assert_eq!(read(&[0x81, 0x00]), Some(128));
629 assert_eq!(read(&base128_bytes(1_234_567)), Some(1_234_567));
630 assert_eq!(read(&[0x80, 0x01]), None, "leading zero group");
631 assert_eq!(read(&[0xFF; 6]), None, "too long");
632 }
633
634 #[test]
635 fn woff2_names_match_the_plain_font() {
636 let Ok(data) = std::fs::read("/System/Library/Fonts/Supplemental/Arial.ttf") else { return };
637 let plain = font_metas(&data, "X").unwrap();
638 for transform in [false, true] {
639 let packed = woff2(&data, transform);
640 assert_eq!(font_metas(&packed, "X").as_ref(), Some(&plain), "transformed glyf: {transform}");
641 }
642 let mut bad = woff2(&data, false);
644 let len = bad.len();
645 bad.truncate(len - 1000);
646 assert!(font_metas(&bad, "X").is_none());
647 }
648
649 #[test]
650 fn woff2_imports_with_font_metas() {
651 let Ok(data) = std::fs::read("/System/Library/Fonts/Supplemental/Arial.ttf") else { return };
652 let tmp = tempfile::tempdir().unwrap();
653 let mut lib = crate::Library::create(tmp.path().join("F.library")).unwrap();
654 let src = tmp.path().join("Arial.woff2");
655 std::fs::write(&src, woff2(&data, false)).unwrap();
656 let id = lib.import_file(&src, &crate::import::ImportOptions::default()).unwrap();
657 let item = lib.item(&id).unwrap();
658 let metas = item.extra.get("fontMetas").and_then(Value::as_object).unwrap();
659 assert_eq!(first_value(metas, "fontFamily"), Some("Arial"));
660 assert!(item.tags.iter().any(|t| t == "Regular"), "{:?}", item.tags);
661 assert_eq!((item.width, item.height), (600, 600), "the specimen was drawn");
662 let packed = woff2(&data, true);
664 let m = font_metas(&packed, "X").unwrap();
665 assert!(crate::fontpreview::render(&packed, &m).is_none());
666 }
667}