Skip to main content

provenant/utils/
font.rs

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}