font_catcher/
lib.rs

1use std::collections::HashMap;
2use std::fs::{self, create_dir_all, File};
3use std::io::{Result, Write};
4use std::path::PathBuf;
5use std::str;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use dirs::home_dir;
9
10#[cfg(unix)]
11use dirs::font_dir;
12
13//Windows workaround
14#[cfg(target_os = "windows")]
15fn windows_user_folder_fonts() -> Option<PathBuf> {
16    return std::env::var_os("userprofile").and_then(|u| {
17        if u.is_empty() {
18            None
19        } else {
20            let f = PathBuf::from(u).join("AppData\\Local\\Microsoft\\Windows\\Fonts");
21            if !f.is_dir() {
22                None
23            } else {
24                Some(f)
25            }
26        }
27    });
28}
29#[cfg(target_os = "windows")]
30use self::windows_user_folder_fonts as font_dir;
31
32use font_kit::handle::Handle;
33use font_kit::source::SystemSource;
34
35use chrono::offset::Utc;
36use chrono::{DateTime, NaiveDate};
37
38use curl::easy::Easy;
39
40use serde::{Deserialize, Serialize};
41use serde_json;
42use toml;
43
44#[derive(Serialize, Deserialize, Clone)]
45pub struct FontsList {
46    pub kind: String,
47    pub items: Vec<RepoFont>,
48}
49
50#[derive(Serialize, Deserialize)]
51pub struct Repository {
52    pub name: String,
53    pub url: String,
54    pub key: Option<String>,
55}
56
57#[derive(Serialize, Deserialize)]
58pub struct Repositories {
59    pub repo: Vec<Repository>,
60}
61
62#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
63pub struct RepoFont {
64    kind: Option<String>,
65    family: Option<String>,
66    variants: Vec<String>,
67    subsets: Option<Vec<String>>,
68    version: Option<String>,
69    lastModified: Option<String>,
70    files: HashMap<String, String>,
71    commentary: Option<String>,
72    creator: Option<String>,
73}
74
75#[derive(Clone, Debug, PartialEq)]
76pub struct LocalFont {
77    family: Option<String>,
78    variants: Option<Vec<String>>,
79    files: Option<HashMap<String, PathBuf>>,
80    lastModified: Option<SystemTime>,
81    installed: Option<bool>
82}
83
84#[derive(Eq, PartialEq, Hash, Debug, Clone)]
85pub enum Location {
86    User,
87    System,
88    Memory,
89}
90
91#[derive(Debug, Clone, PartialEq)]
92pub struct Font {
93    family: String,
94    repo_font: HashMap<String, RepoFont>,
95    local_font: HashMap<Location, LocalFont>,
96}
97
98fn download(url: &str) -> Vec<u8> {
99    let mut handle = Easy::new();
100    let mut file: Vec<u8> = Vec::new();
101
102    handle.url(url).unwrap();
103    let _location = handle.follow_location(true);
104
105    {
106        let mut transfer = handle.transfer();
107        transfer
108            .write_function(|data| {
109                file.extend_from_slice(data);
110                Ok(data.len())
111            })
112            .unwrap();
113        transfer.perform().unwrap();
114    }
115    file
116}
117
118fn download_file(output_file: &PathBuf, url: &str) -> Result<()> {
119    create_dir_all(output_file.parent().unwrap())?;
120    println!(
121        "Downloading to {} from {}...",
122        output_file.as_os_str().to_str().unwrap(),
123        url
124    );
125    let mut file = File::create(output_file)?;
126    file.write_all(download(url).as_slice())?;
127    Ok(())
128}
129
130pub fn get_default_repos() -> Vec<Repository> {
131    vec![
132        #[cfg(feature = "google_repo")]
133        Repository {
134            name: "Google Fonts".to_string(),
135            url: "https://www.googleapis.com/webfonts/v1/webfonts?key={API_KEY}".to_string(),
136            key: {
137                const PASSWORD: &'static str = env!("GOOGLE_FONTS_KEY");
138                Some(PASSWORD.to_string())
139            },
140        },
141        Repository {
142            name: "Open Font Repository".to_string(),
143            url: "https://raw.githubusercontent.com/GustavoPeredo/open-font-repository/main/fonts.json".to_string(),
144            key: None,
145        }
146    ]
147}
148
149pub fn generate_repos_from_str(repos_as_str: &str) -> Result<Vec<Repository>> {
150    let repositories: Repositories = match toml::from_str(&repos_as_str) {
151        Ok(r) => r,
152        Err(e) => {
153            eprintln!("error: {:#}", e);
154            println!("Skipping reading from local repositories");
155            Repositories {
156                repo: Vec::<Repository>::new(),
157            }
158        }
159    };
160    Ok(repositories.repo)
161}
162
163pub fn generate_repos_from_file(repos_path: &PathBuf) -> Result<Vec<Repository>> {
164    Ok(generate_repos_from_str(&fs::read_to_string(repos_path)?)?)
165}
166
167pub fn generate_repo_font_list_from_str(font_list_as_str: &str) -> Result<Vec<RepoFont>> {
168    Ok(serde_json::from_str::<FontsList>(font_list_as_str)?.items)
169}
170
171pub fn generate_repo_font_list_from_file(repo_path: &PathBuf) -> Result<Vec<RepoFont>> {
172    Ok(generate_repo_font_list_from_str(&fs::read_to_string(
173        repo_path,
174    )?)?)
175}
176
177pub fn generate_repo_font_list_from_url(
178    repo_url: &str,
179    key: Option<String>,
180) -> Result<Vec<RepoFont>> {
181    let repo_url = match key {
182        Some(key) => repo_url.replace("{API_KEY}", &key),
183        None => repo_url.to_string(),
184    };
185    Ok(generate_repo_font_list_from_str(
186        match str::from_utf8(download(&repo_url).as_slice()) {
187            Ok(s) => s,
188            Err(_) => "",
189        },
190    )?)
191}
192
193pub fn init() -> Result<HashMap<String, Font>> {
194    let local_fonts = generate_local_fonts(None)?;
195    let default_repos = get_default_repos();
196    let repo_fonts: HashMap<String, Vec<RepoFont>> = default_repos
197        .iter()
198        .map(|repo| {
199            (
200                repo.name.clone(),
201                generate_repo_font_list_from_url(&repo.url, repo.key.clone()).unwrap(),
202            )
203        })
204        .collect::<HashMap<String, Vec<RepoFont>>>();
205    Ok(generate_fonts_list(repo_fonts, local_fonts))
206}
207
208pub fn generate_local_fonts(location: Option<Location>) -> Result<Vec<LocalFont>> {
209    let fonts = SystemSource::new().all_families().unwrap();
210
211    let results = fonts.iter().map(|font_family| {
212        LocalFont {
213            family: Some(font_family.to_string()),
214            variants: None,
215            files: None,
216            lastModified: None,
217            installed: None
218        }
219    }).collect::<Vec<LocalFont>>();
220    Ok(results)
221}
222
223pub fn generate_fonts_list(
224    repos_font_lists: HashMap<String, Vec<RepoFont>>,
225    local_fonts: Vec<LocalFont>,
226) -> HashMap<String, Font> {
227    let mut result: HashMap<String, Font> = HashMap::new();
228
229    for (repo_name, repo_fonts) in repos_font_lists.iter() {
230        for repo_font in repo_fonts {
231            let current_font = result.entry(repo_font.family.clone().unwrap()).or_insert(
232                Font {
233                    family: repo_font.family.clone().unwrap(),
234                    repo_font: HashMap::new(),
235                    local_font: HashMap::new(),
236                }
237            );
238            current_font
239                .repo_font
240                .insert(repo_name.to_string(), repo_font.clone());
241        }
242    }
243
244    for local_font in local_fonts {
245        let local_font_format = Font {
246            family: local_font.family.clone().unwrap(),
247            repo_font: HashMap::new(),
248            local_font: HashMap::from([
249                (Location::System, local_font.clone()),
250                (Location::User, local_font.clone()),
251                (Location::Memory, local_font.clone())
252            ]),
253        };
254        let current_font = result.entry(local_font.family.clone().unwrap()).or_insert(
255            local_font_format
256        );
257        for location in [Location::User, Location::System, Location::Memory].iter() {
258            current_font.local_font.insert(
259                    location.clone(), local_font.clone(),
260            );
261        }
262    }
263    result
264}
265
266pub fn generate_local_font_from_handles(handles: &[Handle]) -> (Location, LocalFont) {
267    let mut family_name = "".to_string();
268    let mut variants: Vec<String> = Vec::new();
269    let mut files: HashMap<String, PathBuf> = HashMap::new();
270    let mut lastModified = None;
271
272    let mut location = Location::Memory;
273    
274    for handle in handles.iter() {
275        match handle.load() {
276            Ok(font_info) => {
277                family_name = font_info.family_name();
278
279                let variant = match font_info.postscript_name() {
280                    Some(postscript_name) => {
281                        let mut var = postscript_name.replace(&family_name, "")
282                            .replace(&family_name.replace(" ", ""), "")
283                            .replace("-", " ");
284                        if var.len() == 0 {
285                            var = "Regular".to_string();
286                        }
287                        while variants.contains(&var) {
288                            var = var + "-";
289                        }
290                        var
291                    },
292                    None => "Regular".to_string()
293                };
294
295                variants.push(variant.clone());
296
297                match handle {
298                    Handle::Path {ref path, font_index: _} => {
299                        lastModified = Some(
300                            match fs::metadata(&path) {
301                                Ok(metadata) => {
302                                    match metadata.modified() {
303                                        Ok(time) => time,
304                                        Err(_) => SystemTime::now()
305                                    }
306                                },
307                                Err(_) => SystemTime::now()
308                            }
309                        );
310                        location = if path.starts_with(home_dir().unwrap()) {
311                            Location::User
312                        } else {
313                            Location::System
314                        };
315
316                        files.insert(
317                            variant,
318                            path.to_path_buf()
319                        );
320                    },
321                    Memory => {
322                        lastModified = Some(SystemTime::now());
323                        location = Location::Memory;
324                    }
325                }
326            },
327            Err(_) => {}
328        }
329    }
330    (
331        location,
332        LocalFont {
333            family: Some(family_name),
334            variants: {
335                if !variants.is_empty() {
336                    Some(variants)
337                } else {
338                    None
339                }
340            },
341            files: {
342                if !files.is_empty() {
343                    Some(files)
344                } else {
345                    None
346                }
347            },
348            lastModified: lastModified,
349            installed: Some(true)
350        }
351    )
352}
353
354    macro_rules! create_fn {
355        (
356            $func_name:ident,
357            $variable:ident,
358            $default_return:expr,
359            $return_type:ty
360        ) => {
361            fn $func_name(&mut self, location: &Location) -> $return_type {
362                match self.local_font.get(location) {
363                    Some(font) => {
364                        match &font.$variable {
365                            Some(value) => value.clone(),
366                            None => {
367                                match SystemSource::new().select_family_by_name(&self.family) {
368                                    Ok(family_handle) => {
369                                        let new_local_font = generate_local_font_from_handles(
370                                            family_handle.fonts()
371                                        );
372                                        self.local_font.insert(
373                                            new_local_font.0.clone(), new_local_font.1
374                                        );
375                                        if &new_local_font.0 == location {
376                                            return self.$func_name(location);
377                                        }
378                                        $default_return
379                                    },
380                                    Err(_) => $default_return
381                                }
382                            }
383                        }
384                    },
385                    None => $default_return
386                }       
387            }
388        }
389    }
390
391impl Font {
392    create_fn!(is_font_x_installed, installed, false, bool);
393    create_fn!(get_local_x_variants, variants, Vec::new(), Vec<String>);
394    create_fn!(get_local_x_files, files, HashMap::new(), HashMap<String, PathBuf>);
395    create_fn!(get_local_x_last_modified, lastModified, SystemTime::now(), SystemTime);
396    create_fn!(get_local_x_font_family, family, "".to_string(), String);
397
398    pub fn is_font_system_installed(&mut self) -> bool {
399        self.is_font_x_installed(&Location::System)
400    }
401    pub fn is_font_user_installed(&mut self) -> bool {
402        self.is_font_x_installed(&Location::User)
403    }
404    pub fn is_font_memory_installed(&mut self) -> bool {
405        self.is_font_x_installed(&Location::Memory)
406    }
407
408    pub fn is_font_installed(&mut self) -> bool {
409        self.is_font_system_installed() || 
410        self.is_font_user_installed() ||
411        self.is_font_memory_installed()
412    }
413
414    pub fn get_local_system_variants(&mut self) -> Vec<String> {
415        self.get_local_x_variants(&Location::System)
416    }
417    pub fn get_local_user_variants(&mut self) -> Vec<String> {
418        self.get_local_x_variants(&Location::User)
419    }
420    pub fn get_local_memory_variants(&mut self) -> Vec<String> {
421        self.get_local_x_variants(&Location::Memory)
422    }
423
424    pub fn get_local_system_files(&mut self) -> HashMap<String, PathBuf> {
425        self.get_local_x_files(&Location::System)
426    }
427    pub fn get_local_user_files(&mut self) -> HashMap<String, PathBuf> {
428        self.get_local_x_files(&Location::User)
429    }
430    pub fn get_local_memory_files(&mut self) -> HashMap<String, PathBuf> {
431        self.get_local_x_files(&Location::Memory)
432    }
433
434    pub fn get_local_system_last_modified(&mut self) -> DateTime<Utc> {
435        DateTime::<Utc>::from(
436            self.get_local_x_last_modified(&Location::System)
437        )
438    }
439    pub fn get_local_user_last_modified(&mut self) -> DateTime<Utc> {
440        DateTime::<Utc>::from(
441            self.get_local_x_last_modified(&Location::User)
442        )
443    }
444    pub fn get_local_memory_last_modified(&mut self) -> DateTime<Utc> {
445        DateTime::<Utc>::from(
446            self.get_local_x_last_modified(&Location::Memory)
447        )
448    }
449
450    pub fn get_local_system_font_family(&mut self) -> String {
451        self.get_local_x_font_family(&Location::System).to_string()
452    }
453    pub fn get_local_user_font_family(&mut self) -> String {
454        self.get_local_x_font_family(&Location::User).to_string()
455    }
456    pub fn get_local_memory_font_family(&mut self) -> String {
457        self.get_local_x_font_family(&Location::Memory).to_string()
458    }
459
460    pub fn is_font_in_repo(&self, repo: &str) -> bool {
461        match &self.repo_font.get(repo) {
462            Some(_repo_font) => true,
463            None => false,
464        }
465    }
466
467    pub fn get_repos_availability(&self) -> Option<Vec<String>> {
468        if self.repo_font.len() > 0 {
469            Some(self.repo_font.keys().cloned().collect())
470        } else {
471            None
472        }
473    }
474
475    pub fn get_repo_variants(&self, repo: &str) -> Option<Vec<String>> {
476        match &self.repo_font.get(repo) {
477            Some(repo_font) => Some(repo_font.variants.clone()),
478            None => None,
479        }
480    }
481
482    pub fn get_repo_files(&self, repo: &str) -> Option<HashMap<String, String>> {
483        match &self.repo_font.get(repo) {
484            Some(repo_font) => Some(repo_font.files.clone()),
485            None => None,
486        }
487    }
488
489    pub fn get_repo_last_modified(&self, repo: &str) -> Option<DateTime<Utc>> {
490        match &self.repo_font.get(repo) {
491            Some(repo_font) => match &repo_font.lastModified {
492                Some(date) => {
493                    let naive_date = NaiveDate::parse_from_str(&date, "%Y-%m-%d");
494                    match naive_date {
495                        Ok(naive_date) => {
496                            Some(DateTime::from_utc(naive_date.and_hms(0, 0, 0), Utc))
497                        }
498                        Err(_) => {
499                            eprintln!("error: date not in %Y-%m-%d");
500                            None
501                        }
502                    }
503                }
504                None => None,
505            },
506            None => None,
507        }
508    }
509
510    pub fn get_repo_family(&self, repo: &str) -> Option<String> {
511        match &self.repo_font.get(repo) {
512            Some(repo_font) => Some(repo_font.family.clone().unwrap()),
513            None => None,
514        }
515    }
516
517    pub fn get_repo_subsets(&self, repo: &str) -> Option<Vec<String>> {
518        match &self.repo_font.get(repo) {
519            Some(repo_font) => match &repo_font.subsets {
520                Some(i) => Some(i.clone()),
521                None => None,
522            },
523            None => None,
524        }
525    }
526
527    pub fn get_repo_version(&self, repo: &str) -> Option<String> {
528        match &self.repo_font.get(repo) {
529            Some(repo_font) => match &repo_font.version {
530                Some(i) => Some(i.clone()),
531                None => None,
532            },
533            None => None,
534        }
535    }
536
537    pub fn get_repo_commentary(&self, repo: &str) -> Option<String> {
538        match &self.repo_font.get(repo) {
539            Some(repo_font) => match &repo_font.commentary {
540                Some(i) => Some(i.clone()),
541                None => None,
542            },
543            None => None,
544        }
545    }
546
547    pub fn get_repo_creator(&self, repo: &str) -> Option<String> {
548        match &self.repo_font.get(repo) {
549            Some(repo_font) => match &repo_font.creator {
550                Some(i) => Some(i.clone()),
551                None => None,
552            },
553            None => None,
554        }
555    }
556
557    pub fn get_all_repos_with_update_user(&mut self) -> Option<Vec<String>> {
558        let mut result: Vec<String> = Vec::new();
559        let local_last_modified = &self.get_local_user_last_modified();
560        match &self.get_repos_availability() {
561            Some(repos) => {
562                for repo in repos.iter() {
563                    match &self.get_repo_last_modified(repo) {
564                        Some(repo_last_modified) => {
565                            if repo_last_modified > local_last_modified {
566                                result.push(repo.to_string());
567                            }
568                        }
569                        None => {}
570                    }
571                }
572            }
573            None => {}
574        }
575        if result.len() > 0 {
576            Some(result)
577        } else {
578            None
579        }
580    }
581    pub fn get_all_repos_with_update_system(&mut self) -> Option<Vec<String>> {
582        let mut result: Vec<String> = Vec::new();
583        let local_last_modified = &self.get_local_system_last_modified();
584        match &self.get_repos_availability() {
585            Some(repos) => {
586                for repo in repos.iter() {
587                    match &self.get_repo_last_modified(repo) {
588                        Some(repo_last_modified) => {
589                            if repo_last_modified > local_last_modified {
590                                result.push(repo.to_string());
591                            }
592                        }
593                        None => {}
594                    }
595                }
596            }
597            None => {}
598        }
599        if result.len() > 0 {
600            Some(result)
601        } else {
602            None
603        }
604    }
605
606    pub fn is_update_available_user(&mut self) -> bool {
607        match self.get_all_repos_with_update_user() {
608            Some(_repos) => true,
609            None => false,
610        }
611    }
612    pub fn is_update_available_system(&mut self) -> bool {
613        match self.get_all_repos_with_update_system() {
614            Some(_repos) => true,
615            None => false,
616        }
617    }
618
619    pub fn get_first_available_repo(&self) -> Option<String> {
620        let repos = &self.get_repos_availability();
621        match repos {
622            Some(repos) => Some(repos.first().unwrap().to_string()),
623            None => None,
624        }
625    }
626
627    pub fn uninstall_from_user(&mut self, output: bool) -> Result<()> {
628        for (_name, file) in self.get_local_user_files(){
629            if output {
630                println!("Removing {}...", &file.display());
631            }
632            fs::remove_file(&file)?;
633        }
634        self.local_font.insert(
635            Location::User,
636            LocalFont {
637                family: None,
638                variants: None,
639                files: None,
640                lastModified: None,
641                installed: Some(false)
642            }
643        );
644        Ok(())
645    }
646
647    pub fn uninstall_from_system(&mut self, output: bool) -> Result<()> {
648        for (_name, file) in self.get_local_system_files() {
649            if output {
650                println!("Removing {}...", &file.display());
651            }
652            fs::remove_file(&file)?;
653        }
654        self.local_font.insert(
655            Location::System,
656            LocalFont {
657                family: None,
658                variants: None,
659                files: None,
660                lastModified: None,
661                installed: Some(false)
662            }
663        );
664        Ok(())
665    }
666
667    pub fn download(
668        &self,
669        repo: Option<&str>,
670        download_path: &PathBuf,
671        output: bool,
672    ) -> Result<()> {
673        let repos = self.get_first_available_repo();
674        let repo = match repo {
675            Some(repo) => repo,
676            None => match &repos {
677                Some(repo) => repo,
678                None => "",
679            },
680        };
681        match self.get_repo_files(repo) {
682            Some(files) => {
683                for (variant, file) in files {
684                    let extension: &str = file.split(".").collect::<Vec<&str>>().last().unwrap();
685
686                    if output {
687                        println!(
688                            "Downloading {} from {}",
689                            &format!(
690                                "{}-{}.{}",
691                                &self.get_repo_family(repo).unwrap(),
692                                &variant,
693                                &extension
694                            ),
695                            &file
696                        );
697                    }
698                    download_file(
699                        &download_path.join(&format!(
700                            "{}-{}.{}",
701                            &self.get_repo_family(repo).unwrap(),
702                            &variant,
703                            &extension
704                        )),
705                        &file,
706                    )?;
707                }
708            }
709            None => {}
710        }
711        Ok(())
712    }
713
714    pub fn output_paths(
715        &self,
716        repo: Option<&str>,
717        path: &PathBuf
718    ) -> Vec<PathBuf> {
719        let repos = self.get_first_available_repo();
720        let repo = match repo {
721            Some(repo) => repo,
722            None => match &repos {
723                Some(repo) => repo,
724                None => "",
725            },
726        };
727
728        let mut results: Vec<PathBuf> = Vec::new();
729
730        match self.get_repo_files(repo) {
731            Some(files) => {
732                for (variant, file) in files {
733                    let extension: &str = file.split(".").collect::<Vec<&str>>().last().unwrap();
734                    results.push(
735                        path.join(&format!(
736                            "{}-{}.{}",
737                            &self.get_repo_family(repo).unwrap(),
738                            &variant,
739                            &extension
740                        ))
741                    );
742                }
743            }
744            None => {}
745        }
746
747        results
748    }
749
750    pub fn install_to_user(&mut self, repo: Option<&str>, output: bool) -> Result<()> {
751        let install_dir = font_dir().unwrap();
752
753        self.download(repo.clone(), &install_dir, output)?;
754
755        let new_local_font = generate_local_font_from_handles(
756            &self.output_paths(repo, &install_dir).iter().map(
757                |path| {
758                    Handle::from_path(path.to_path_buf(), 0)
759                }).collect::<Vec<Handle>>()
760        );
761        self.local_font.insert(
762            new_local_font.0.clone(), new_local_font.1
763        );
764
765        Ok(())
766    }
767}