word_cloud/words/
fonts_provider.rs

1use crate::{
2    visualization::Image,
3    words::{Font, Word},
4};
5use image::Rgb;
6use rand::seq::SliceRandom;
7use rusttype::Scale;
8use serde::{Deserialize, Serialize};
9use serde_json::from_str;
10use std::{collections::HashMap, error::Error, fs::read_to_string};
11
12/// The font provider is responsible to provide fonts to the library.
13///
14/// Before a font can be used it must be registered.
15///
16/// ## Manual font registration
17///
18/// Fonts can be registered manually.
19///
20/// Provide the name alias ("droid" below) and the font file.
21///
22/// ```rust
23/// let mut font_provider = FontProvider::new();
24/// font_provider
25///     .register("droid", "assets/fonts/DroidSans.ttf")
26///     .unwrap();
27/// ```
28///
29/// ## Automatic font registration
30///
31/// Given there is a file describing the font registrations, the font provider can be created this way:
32///
33/// ```rust
34///    let font_provider = FontProvider::from_file("assets/fonts.json");
35///  ```
36///
37/// ## Format of the font registrations file
38///
39/// It's a JSON file containing an array of font registrations.
40///
41/// ```json
42/// [
43///     {
44///         "name": "droid",
45///         "file": "fonts/DroidSans.ttf"
46///     }
47/// ]
48/// ```
49pub struct FontProvider<'font> {
50    fonts: HashMap<String, Font<'font>>,
51}
52
53impl<'font> Default for FontProvider<'font> {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59impl<'font> FontProvider<'font> {
60    pub fn new() -> FontProvider<'font> {
61        FontProvider {
62            fonts: HashMap::new(),
63        }
64    }
65
66    pub fn from_file(filename: &str) -> FontProvider<'font> {
67        #[derive(Serialize, Deserialize)]
68        struct FontFile {
69            name: String,
70            file: String,
71        }
72
73        let mut fp = FontProvider::new();
74        let Ok(data) = read_to_string(filename) else {
75            panic!("Unable to read file '{}'", filename);
76        };
77
78        let Ok(fonts): Result<Vec<FontFile>, _> = from_str(&data) else {
79            panic!("Unable to parse file '{}'", filename);
80        };
81
82        for font in fonts {
83            let file = font.file.clone();
84            fp.register(&font.name, &file).unwrap();
85        }
86
87        fp
88    }
89
90    pub fn register(&mut self, name: &str, font_file: &str) -> Result<(), Box<dyn Error>> {
91        let font = Font::new(font_file.to_string())?;
92        self.fonts.insert(name.to_string(), font);
93
94        Ok(())
95    }
96
97    pub fn get_font(&self, name: &str) -> Result<&Font, String> {
98        match self.fonts.get(name) {
99            Some(font) => Ok(font),
100            None => Err(format!("Font not registered: {}", name)),
101        }
102    }
103
104    pub fn random_font_name(&self) -> String {
105        let keys: Vec<_> = self.fonts.keys().collect();
106        keys.choose(&mut rand::thread_rng()).unwrap().to_string()
107    }
108
109    pub fn debug_font(&self, font_name: &str, filename: &str) {
110        if let Some(font) = self.fonts.get(font_name) {
111            let word = Word::new(
112                font_name,
113                crate::geometry::Orientation::Horizontal,
114                Rgb([0, 0, 0]),
115                "",
116                64,
117            );
118            let bb = font.get_bounding_box(&word, Scale::uniform(64.));
119            let mut img = Image::new(bb.size().0 + 40, bb.size().1, Some(Rgb([255, 255, 255])));
120            img.draw_text(
121                (15, 0),
122                font_name,
123                font,
124                Scale::uniform(64.),
125                Rgb([0, 0, 0]),
126                &crate::geometry::Orientation::Horizontal,
127            );
128            img.save(filename).unwrap();
129        }
130    }
131}
132
133#[cfg(test)]
134mod test {
135    use super::*;
136
137    #[test]
138    fn register_and_read() {
139        let mut fp = FontProvider::new();
140        fp.register("dejavu", "assets/fonts/DroidSans.ttf").unwrap();
141        let _ = fp.get_font("dejavu").unwrap();
142        // The test should not panic.
143    }
144
145    #[test]
146    #[should_panic(expected = "Font file not found: unexisting.ttf")]
147    fn register_file_not_found() {
148        let mut fp = FontProvider::new();
149        fp.register("dejavu", "unexisting.ttf").unwrap();
150    }
151
152    #[test]
153    #[should_panic(expected = "Font not registered: dejavu")]
154    fn get_unexisting() {
155        let fp = FontProvider::new();
156        let _ = fp.get_font("dejavu").unwrap();
157    }
158}