wow_sharedmedia/converter/
font.rs1use std::path::Path;
4
5use crate::Error;
6
7pub const LOCALE_NAMES: &[&str] = &["koKR", "ruRU", "zhCN", "zhTW", "western"];
9
10pub const DEFAULT_LOCALES: &[&str] = &["western"];
12
13pub fn validate_locale_names(names: &[&str]) -> Result<Vec<String>, Error> {
15 let valid: std::collections::HashSet<&str> = LOCALE_NAMES.iter().copied().collect();
16 let mut invalid = Vec::new();
17 for name in names {
18 if !valid.contains(name) {
19 invalid.push(name.to_string());
20 }
21 }
22 if invalid.is_empty() {
23 Ok(names.iter().map(|s| s.to_string()).collect())
24 } else {
25 Err(Error::InvalidLocale(format!(
26 "Invalid locale names: {}",
27 invalid.join(", ")
28 )))
29 }
30}
31
32pub fn extract_font_metadata(path: &Path) -> Result<FontMetadata, Error> {
34 let data = std::fs::read(path).map_err(|e| Error::Io {
35 source: e,
36 path: path.to_path_buf(),
37 })?;
38
39 if data.is_empty() {
40 return Err(Error::InvalidFont(format!("Font file is empty: {}", path.display())));
41 }
42
43 let face =
44 ttf_parser::Face::parse(&data, 0).map_err(|e| Error::InvalidFont(format!("Failed to parse font: {e}")))?;
45
46 let family_name = face
47 .names()
48 .into_iter()
49 .find(|n| n.name_id == ttf_parser::name_id::FAMILY && n.is_unicode())
50 .and_then(|n| n.to_string())
51 .unwrap_or_default();
52
53 let style_name = face
54 .names()
55 .into_iter()
56 .find(|n| n.name_id == ttf_parser::name_id::SUBFAMILY && n.is_unicode())
57 .and_then(|n| n.to_string())
58 .unwrap_or_default();
59
60 Ok(FontMetadata {
61 family_name,
62 style_name,
63 is_monospace: face.is_monospaced(),
64 num_glyphs: face.number_of_glyphs() as u32,
65 is_variable_font: face.is_variable(),
66 })
67}
68
69pub fn validate_font(path: &Path) -> Result<(), Error> {
73 let ext = path
74 .extension()
75 .and_then(|e| e.to_str())
76 .map(|e| e.to_lowercase())
77 .unwrap_or_default();
78
79 if ext != "ttf" && ext != "otf" {
80 return Err(Error::InvalidFont(format!(
81 "Unsupported font extension: .{} (expected .ttf or .otf)",
82 ext
83 )));
84 }
85
86 let data = std::fs::read(path).map_err(|e| Error::Io {
87 source: e,
88 path: path.to_path_buf(),
89 })?;
90
91 if data.is_empty() {
92 return Err(Error::InvalidFont("Font file is empty".to_string()));
93 }
94
95 ttf_parser::Face::parse(&data, 0).map_err(|e| Error::InvalidFont(format!("Failed to parse font: {e}")))?;
96
97 Ok(())
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct FontMetadata {
103 pub family_name: String,
105 pub style_name: String,
107 pub is_monospace: bool,
109 pub num_glyphs: u32,
111 pub is_variable_font: bool,
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use tempfile::TempDir;
119
120 #[test]
121 fn test_validate_locale_names_valid() {
122 assert!(validate_locale_names(&["western", "zhCN"]).is_ok());
123 }
124
125 #[test]
126 fn test_validate_locale_names_invalid() {
127 assert!(validate_locale_names(&["western", "invalid"]).is_err());
128 }
129
130 #[test]
131 fn test_validate_locale_names_empty() {
132 let result = validate_locale_names(&[]);
133 assert!(result.is_ok());
134 assert!(result.unwrap().is_empty());
135 }
136
137 #[test]
138 fn test_default_locales() {
139 let locales: Vec<String> = DEFAULT_LOCALES.iter().map(|s| s.to_string()).collect();
140 assert_eq!(locales, vec!["western"]);
141 }
142
143 #[test]
144 fn test_validate_font_rejects_wrong_extension() {
145 let dir = TempDir::new().unwrap();
146 let path = dir.path().join("fake.txt");
147 std::fs::write(&path, b"not a font").unwrap();
148
149 let result = validate_font(&path);
150 assert!(result.is_err());
151 match result.unwrap_err() {
152 Error::InvalidFont(msg) => assert!(msg.contains("Unsupported font extension")),
153 other => panic!("Expected InvalidFont, got: {other}"),
154 }
155 }
156
157 #[test]
158 fn test_validate_font_rejects_empty_file() {
159 let dir = TempDir::new().unwrap();
160 let path = dir.path().join("empty.ttf");
161 std::fs::write(&path, b"").unwrap();
162
163 let result = validate_font(&path);
164 assert!(result.is_err());
165 match result.unwrap_err() {
166 Error::InvalidFont(msg) => assert!(msg.contains("empty")),
167 other => panic!("Expected InvalidFont, got: {other}"),
168 }
169 }
170
171 #[cfg(target_os = "windows")]
172 #[test]
173 fn test_extract_font_metadata_from_system_font_smoke() {
174 let candidates = [
175 std::path::Path::new(r"C:\Windows\Fonts\arial.ttf"),
176 std::path::Path::new(r"C:\Windows\Fonts\calibri.ttf"),
177 std::path::Path::new(r"C:\Windows\Fonts\consola.ttf"),
178 ];
179
180 let font = candidates
181 .iter()
182 .find(|p| p.exists())
183 .expect("expected at least one standard Windows font to exist");
184
185 validate_font(font).unwrap();
186 let meta = extract_font_metadata(font).unwrap();
187
188 assert!(!meta.family_name.is_empty() || !meta.style_name.is_empty());
189 assert!(meta.num_glyphs > 0);
190 }
191}