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