Skip to main content

provenant/utils/
font.rs

1// SPDX-FileCopyrightText: Provenant contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use 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}