1mod axes;
2mod designers;
3mod fonts_public;
4pub mod knowledge; use std::{
8 cell::OnceCell,
9 collections::HashMap,
10 fs::{self, File},
11 io::{BufRead, BufReader, Error, ErrorKind},
12 path::{Path, PathBuf},
13 str::FromStr,
14};
15
16pub use axes::{AxisProto, FallbackProto};
17pub use designers::{AvatarProto, DesignerInfoProto};
18pub use fonts_public::*;
19use google_fonts_languages::LANGUAGES;
20pub use google_fonts_languages::{
21 ExemplarCharsProto, LanguageProto, RegionProto, SampleTextProto, ScriptProto,
22};
23use protobuf::text_format::ParseError;
24use regex::Regex;
25use walkdir::WalkDir;
26
27pub fn read_family(s: &str) -> Result<FamilyProto, ParseError> {
31 if s.contains("position") {
32 let re = Regex::new(r"(?m)position\s+\{[^}]*\}").expect("Valid re");
33 let s = re.replace_all(s, "");
34 protobuf::text_format::parse_from_str(&s)
35 } else {
36 protobuf::text_format::parse_from_str(s)
37 }
38}
39
40fn exemplar_score(font: &FontProto, preferred_style: FontStyle, preferred_weight: i32) -> i32 {
41 let mut score = 0;
42 if font.style() == preferred_style.style() {
44 score += 16;
45 }
46
47 score -= (font.weight() - preferred_weight).abs() / 100;
49
50 if font.weight() > preferred_weight {
52 score += 1;
53 }
54
55 if font.filename().contains("].") {
57 score += 2;
58 }
59
60 score
61}
62
63pub fn exemplar(family: &FamilyProto) -> Option<&FontProto> {
69 fn score(font: &FontProto) -> i32 {
70 exemplar_score(font, FontStyle::Normal, 400)
71 }
72 family
73 .fonts
74 .iter()
75 .reduce(|acc, e| if score(acc) >= score(e) { acc } else { e })
76}
77
78#[derive(Copy, Clone, Debug, PartialEq)]
80pub enum FontStyle {
81 Normal,
82 Italic,
83}
84
85impl FontStyle {
86 fn style(&self) -> &str {
87 match self {
88 FontStyle::Normal => "normal",
89 FontStyle::Italic => "italic",
90 }
91 }
92}
93
94pub fn select_font(
96 family: &FamilyProto,
97 preferred_style: FontStyle,
98 preferred_weight: i32,
99) -> Option<&FontProto> {
100 let score =
101 |font: &FontProto| -> i32 { exemplar_score(font, preferred_style, preferred_weight) };
102 family
103 .fonts
104 .iter()
105 .reduce(|acc, e| if score(acc) >= score(e) { acc } else { e })
106}
107
108fn iter_families(
109 root: &Path,
110 filter: Option<&Regex>,
111) -> impl Iterator<Item = (PathBuf, Result<FamilyProto, ParseError>)> {
112 WalkDir::new(root)
113 .into_iter()
114 .filter_map(|d| d.ok())
115 .filter(|d| d.file_name() == "METADATA.pb")
116 .filter(move |d| {
117 filter
118 .map(|r| r.find(&d.path().to_string_lossy()).is_some())
119 .unwrap_or(true)
120 })
121 .map(|d| {
122 (
123 d.path().to_path_buf(),
124 read_family(&fs::read_to_string(d.path()).expect("To read files!")),
125 )
126 })
127}
128
129pub fn iter_languages(_root: &Path) -> impl Iterator<Item = Result<LanguageProto, ParseError>> {
131 LANGUAGES.values().map(|l| Ok(*l.clone()))
132}
133
134pub fn read_tags(root: &Path) -> Result<Vec<Tagging>, Error> {
136 let mut tag_dir = root.to_path_buf();
137 tag_dir.push("tags/all");
138 let mut tags = Vec::new();
139 for entry in fs::read_dir(&tag_dir).expect("To read tag dir") {
140 let entry = entry.expect("To access tag dir entries");
141 if entry
142 .path()
143 .extension()
144 .expect("To have extensions")
145 .to_str()
146 .expect("utf-8")
147 != "csv"
148 {
149 continue;
150 }
151 let fd = File::open(entry.path())?;
152 let rdr = BufReader::new(fd);
153 tags.extend(
154 rdr.lines()
155 .map(|s| s.expect("Valid tag lines"))
156 .map(|s| Tagging::from_str(&s).expect("Valid tag lines")),
157 );
158 }
159 Ok(tags)
160}
161
162pub fn read_tag_metadata(root: &Path) -> Result<Vec<TagMetadata>, Error> {
164 let mut tag_metadata_file = root.to_path_buf();
165 tag_metadata_file.push("tags/tags_metadata.csv");
166 let mut metadata = Vec::new();
167
168 let fd = File::open(&tag_metadata_file)?;
169 let rdr = BufReader::new(fd);
170 metadata.extend(
171 rdr.lines()
172 .map(|s| s.expect("Valid tag lines"))
173 .map(|s| TagMetadata::from_str(&s).expect("Valid tag metadata lines")),
174 );
175
176 Ok(metadata)
177}
178
179fn csv_values(s: &str) -> Vec<&str> {
180 let mut s = s;
181 let mut values = Vec::new();
182 while !s.is_empty() {
183 s = s.trim();
184 let mut end_idx = None;
185 if let Some(s) = s.strip_prefix('"') {
186 end_idx = Some(s.find('"').expect("Close quote"));
187 }
188 end_idx = s[end_idx.unwrap_or_default()..]
189 .find(',')
190 .map(|v| v + end_idx.unwrap_or_default());
191 if let Some(end_idx) = end_idx {
192 let (value, rest) = s.split_at(end_idx);
193 values.push(value.trim());
194 s = &rest[1..];
195 } else {
196 values.push(s);
197 s = "";
198 }
199 }
200 values
201}
202
203#[derive(Clone, Debug)]
208pub struct Tagging {
209 pub family: String,
211 pub loc: String,
216 pub tag: String,
218 pub value: f32,
220}
221
222impl FromStr for Tagging {
223 type Err = Error;
224
225 fn from_str(s: &str) -> Result<Self, Self::Err> {
226 let values = csv_values(s);
227 let (family, loc, tag, value) = match values[..] {
228 [family, tag, value] => (family, "", tag, value),
229 [family, loc, tag, value] => (family, loc, tag, value),
230 _ => return Err(Error::new(ErrorKind::InvalidData, "Unparseable tag")),
231 };
232 Ok(Tagging {
233 family: family.to_string(),
234 loc: loc.to_string(),
235 tag: tag.to_string(),
236 value: f32::from_str(value)
237 .map_err(|_| Error::new(ErrorKind::InvalidData, "Invalid tag value"))?,
238 })
239 }
240}
241
242#[derive(Clone, Debug)]
244pub struct TagMetadata {
245 pub tag: String,
247 pub min_value: f32,
249 pub max_value: f32,
251 pub prompt_name: String,
253}
254
255impl FromStr for TagMetadata {
256 type Err = Error;
257
258 fn from_str(s: &str) -> Result<Self, Self::Err> {
259 let values = csv_values(s);
260 let [tag, min, max, prompt_name] = values[..] else {
261 return Err(Error::new(
262 ErrorKind::InvalidData,
263 "Unparseable tag metadata, wrong number of values",
264 ));
265 };
266 Ok(TagMetadata {
267 tag: tag.into(),
268 min_value: f32::from_str(min)
269 .map_err(|_| Error::new(ErrorKind::InvalidData, "Invalid min value"))?,
270 max_value: f32::from_str(max)
271 .map_err(|_| Error::new(ErrorKind::InvalidData, "Invalid min value"))?,
272 prompt_name: prompt_name.into(),
273 })
274 }
275}
276
277pub struct GoogleFonts {
285 repo_dir: PathBuf,
286 family_filter: Option<Regex>,
287 families: OnceCell<Vec<(PathBuf, Result<FamilyProto, ParseError>)>>,
288 family_by_font_file: OnceCell<HashMap<String, usize>>,
289 tags: OnceCell<Result<Vec<Tagging>, Error>>,
290 tag_metadata: OnceCell<Result<Vec<TagMetadata>, Error>>,
291}
292
293impl GoogleFonts {
294 pub fn new(p: PathBuf, family_filter: Option<Regex>) -> Self {
305 Self {
306 repo_dir: p,
307 family_filter,
308 families: OnceCell::new(),
309 family_by_font_file: OnceCell::new(),
310 tags: OnceCell::new(),
311 tag_metadata: OnceCell::new(),
312 }
313 }
314 pub fn tags(&self) -> Result<&[Tagging], &Error> {
322 self.tags
323 .get_or_init(|| read_tags(&self.repo_dir))
324 .as_ref()
325 .map(|tags| tags.as_slice())
326 }
327 pub fn tag_metadata(&self) -> Result<&[TagMetadata], &Error> {
333 self.tag_metadata
334 .get_or_init(|| read_tag_metadata(&self.repo_dir))
335 .as_ref()
336 .map(|metadata| metadata.as_slice())
337 }
338 pub fn families(&self) -> &[(PathBuf, Result<FamilyProto, ParseError>)] {
350 self.families
351 .get_or_init(|| iter_families(&self.repo_dir, self.family_filter.as_ref()).collect())
352 .as_slice()
353 }
354 pub fn language(&self, lang_id: &str) -> Option<&LanguageProto> {
361 LANGUAGES.get(lang_id).map(|l| &**l)
362 }
363
364 fn family_by_font_file(&self) -> &HashMap<String, usize> {
365 self.family_by_font_file.get_or_init(|| {
366 self.families()
367 .iter()
368 .enumerate()
369 .filter(|(_, (_, f))| f.is_ok())
370 .flat_map(|(i, (_, f))| {
371 f.as_ref()
372 .unwrap()
373 .fonts
374 .iter()
375 .map(move |f| (f.filename().to_string(), i))
376 })
377 .collect()
378 })
379 }
380
381 pub fn family(&self, font: &FontProto) -> Option<(&Path, &FamilyProto)> {
388 self.family_by_font_file()
389 .get(font.filename())
390 .copied()
391 .map(|i| {
392 let (p, f) = &self.families()[i];
393 (p.as_path(), f.as_ref().unwrap())
394 })
395 }
396 pub fn find_font_binary(&self, font: &FontProto) -> Option<PathBuf> {
404 let (family_path, _) = self.family(font)?;
405 let mut font_file = family_path.parent().unwrap().to_path_buf();
406 font_file.push(font.filename());
407 if !font_file.exists() {
408 eprintln!("No such file as {font_file:?}");
409 }
410 font_file.exists().then_some(font_file)
411 }
412
413 pub fn primary_language(&self, family: &FamilyProto) -> &LanguageProto {
428 let mut primary_language: Option<&LanguageProto> = None;
430 if primary_language.is_none() && family.has_primary_language() {
431 if let Some(lang) = self.language(family.primary_language()) {
432 primary_language = Some(lang);
433 } else {
434 eprintln!(
435 "{} specifies invalid primary_language {}",
436 family.name(),
437 family.primary_language()
438 );
439 }
440 }
441 if primary_language.is_none() && family.has_primary_script() {
442 let lang = LANGUAGES
444 .values()
445 .filter(|l| l.script.is_some() && l.script() == family.primary_script())
446 .reduce(|acc, e| {
447 if acc.population() > e.population() {
448 acc
449 } else {
450 e
451 }
452 });
453 if let Some(lang) = lang {
454 primary_language = Some(lang);
455 } else {
456 eprintln!(
457 "{} specifies a primary_script that matches no languages {}",
458 family.name(),
459 family.primary_script()
460 );
461 }
462 }
463 if primary_language.is_none() {
464 primary_language = self.language("en_Latn");
465 }
466 primary_language
467 .unwrap_or_else(|| panic!("Not even our final fallback worked for {}", family.name()))
468 }
469}
470
471#[cfg(test)]
472mod tests {
473
474 use std::fs;
475
476 use super::*;
477
478 fn testdata_dir() -> std::path::PathBuf {
479 ["./resources/testdata", "../resources/testdata"]
484 .iter()
485 .map(std::path::PathBuf::from)
486 .find(|pb| pb.exists())
487 .unwrap()
488 }
489
490 fn testdata_file_content(relative_path: &str) -> String {
491 let mut p = testdata_dir();
492 p.push(relative_path);
493 fs::read_to_string(p).unwrap()
494 }
495
496 #[test]
497 fn roboto_exemplar() {
498 let roboto = read_family(&testdata_file_content("roboto-metadata.pb")).unwrap();
499 let exemplar = exemplar(&roboto).unwrap();
500 assert_eq!("Roboto[wdth,wght].ttf", exemplar.filename());
501 }
502
503 #[test]
504 fn wix_exemplar() {
505 let roboto = read_family(&testdata_file_content("wixmadefortext-metadata.pb")).unwrap();
506 let exemplar = exemplar(&roboto).unwrap();
507 assert_eq!("WixMadeforText[wght].ttf", exemplar.filename());
508 }
509
510 #[test]
511 fn parse_roboto_metadata() {
512 read_family(&testdata_file_content("roboto-metadata.pb")).unwrap();
513 }
514
515 #[test]
516 fn parse_wix_metadata() {
517 read_family(&testdata_file_content("wixmadefortext-metadata.pb")).unwrap();
519 }
520
521 #[test]
522 fn parse_primary_lang_script_metadata() {
523 let family = read_family(&testdata_file_content("kosugimaru-metadata.pb")).unwrap();
524 assert_eq!(
525 ("Jpan", "Invalid"),
526 (family.primary_script(), family.primary_language())
527 );
528 }
529
530 #[test]
531 fn parse_tag3() {
532 Tagging::from_str("Roboto Slab, /quant/stroke_width_min, 26.31").expect("To parse");
533 }
534
535 #[test]
536 fn parse_tag4() {
537 Tagging::from_str("Roboto Slab, wght@100, /quant/stroke_width_min, 26.31")
538 .expect("To parse");
539 }
540
541 #[test]
542 fn parse_tag_quoted() {
543 Tagging::from_str("Georama, \"ital,wght@1,100\", /quant/stroke_width_min, 16.97")
544 .expect("To parse");
545 }
546
547 #[test]
548 fn parse_tag_quoted2() {
549 Tagging::from_str("\"\",t,1").expect("To parse");
550 }
551}