1use std::collections::BTreeSet;
5use std::path::Path;
6
7use ttf_parser::{Face, Permissions, name_id};
8
9const SUPPORTED_FONT_EXTENSIONS: &[&str] = &["ttf", "otf"];
10
11pub(crate) fn extract_font_metadata_text(path: &Path, bytes: &[u8]) -> Option<String> {
12 let extension = path.extension().and_then(|ext| ext.to_str())?;
13 if !SUPPORTED_FONT_EXTENSIONS
14 .iter()
15 .any(|supported| extension.eq_ignore_ascii_case(supported))
16 {
17 return None;
18 }
19
20 let face = Face::parse(bytes, 0).ok()?;
21 let mut lines = Vec::new();
22 let mut seen = BTreeSet::new();
23
24 for record in face.names() {
25 let Some(label) = font_name_label(record.name_id) else {
26 continue;
27 };
28 if !record.is_unicode() {
29 continue;
30 }
31 let Some(value) = record.to_string().map(normalize_font_value) else {
32 continue;
33 };
34 if value.is_empty() {
35 continue;
36 }
37
38 let line = format!("{label}: {value}");
39 if seen.insert(line.clone()) {
40 lines.push(line);
41 }
42 }
43
44 if let Some(permissions) = face.permissions() {
45 let line = format!(
46 "Embedding permissions: {}",
47 font_permission_label(permissions)
48 );
49 if seen.insert(line.clone()) {
50 lines.push(line);
51 }
52 }
53
54 (!lines.is_empty()).then(|| lines.join("\n"))
55}
56
57fn font_name_label(name_id_value: u16) -> Option<&'static str> {
58 match name_id_value {
59 name_id::COPYRIGHT_NOTICE => Some("Copyright Notice"),
60 name_id::TRADEMARK => Some("Trademark"),
61 name_id::MANUFACTURER => Some("Manufacturer"),
62 name_id::DESCRIPTION => Some("Description"),
63 name_id::VENDOR_URL => Some("Vendor URL"),
64 name_id::DESIGNER_URL => Some("Designer URL"),
65 name_id::LICENSE => Some("License Description"),
66 name_id::LICENSE_URL => Some("License Info URL"),
67 _ => None,
68 }
69}
70
71fn normalize_font_value(value: String) -> String {
72 value
73 .split_whitespace()
74 .collect::<Vec<_>>()
75 .join(" ")
76 .trim()
77 .to_string()
78}
79
80fn font_permission_label(permission: Permissions) -> &'static str {
81 match permission {
82 Permissions::Installable => "Installable",
83 Permissions::Restricted => "Restricted",
84 Permissions::PreviewAndPrint => "Preview and Print",
85 Permissions::Editable => "Editable",
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use std::fs;
92 use std::path::Path;
93
94 use super::extract_font_metadata_text;
95
96 #[test]
97 fn extracts_ofl_metadata_from_lato_font_fixture() {
98 let bytes =
99 fs::read("testdata/font-fixtures/Lato-Bold.ttf").expect("read lato font fixture");
100
101 let text = extract_font_metadata_text(Path::new("Lato-Bold.ttf"), &bytes)
102 .expect("font metadata text");
103
104 assert!(text.contains("License Description:"), "{text}");
105 assert!(
106 text.contains("Open Font License") || text.contains("OFL"),
107 "{text}"
108 );
109 }
110
111 #[test]
112 fn extracts_apache_metadata_from_underline_test_font_fixture() {
113 let bytes = fs::read("testdata/font-fixtures/UnderlineTest-Close.ttf")
114 .expect("read apache font fixture");
115
116 let text = extract_font_metadata_text(Path::new("UnderlineTest-Close.ttf"), &bytes)
117 .expect("font metadata text");
118
119 assert!(
120 text.contains("License Description:") || text.contains("Copyright Notice:"),
121 "{text}"
122 );
123 assert!(
124 text.contains("Apache") || text.contains("http://www.apache.org/licenses"),
125 "{text}"
126 );
127 }
128}