wordcloud/common/
font.rs

1use std::collections::HashSet;
2use std::hash::{Hash, Hasher};
3use std::io::Cursor;
4
5use std::sync::Arc;
6use swash::text::{Codepoint, Script};
7
8use swash::scale::ScaleContext;
9use swash::{FontRef, StringId, Tag};
10
11#[cfg(feature = "woff2")]
12use woff2::convert_woff2_to_ttf;
13
14#[derive(Clone)]
15#[allow(clippy::upper_case_acronyms)]
16pub(crate) enum FontType {
17    OTF,
18    TTF,
19    WOFF,
20    WOFF2,
21}
22
23impl FontType {
24    pub(crate) fn embed_tag(&self) -> &'static str {
25        match self {
26            FontType::OTF => "application/font-otf",
27            FontType::TTF => "application/font-ttf",
28            FontType::WOFF => "application/font-woff",
29            FontType::WOFF2 => "application/font-woff2",
30        }
31    }
32}
33
34/**
35    Error returned if font loading failed
36*/
37pub type FontLoadingError = String;
38
39/**
40    Result from [`FontLoadingError`]
41*/
42pub type FontLoadingResult<T> = Result<T, FontLoadingError>;
43
44/**
45    Represents a Font stored in memory. By default it supports `OTF` and `TTF` fonts, with
46    the create features `woff` and `woff2` it also supports loading `WOFF` fonts.
47*/
48pub struct Font<'a> {
49    name: String,
50    re: FontRef<'a>,
51    font_type: FontType,
52    supported_scripts: HashSet<CScript>,
53    packed_font_data: Option<Vec<u8>>,
54    approximate_pixel_width: f32,
55}
56
57impl<'a> Font<'a> {
58    fn identify_scripts_in_font(fr: &FontRef) -> HashSet<CScript> {
59        let mut scripts_in_specs = fr
60            .writing_systems()
61            .filter_map(|s| s.script())
62            .map(CScript::try_from)
63            .filter_map(|s| s.ok())
64            .collect::<HashSet<CScript>>();
65
66        if scripts_in_specs.is_empty() {
67            fr.charmap().enumerate(|i, _g| {
68                if let Ok(c) = <u32 as TryInto<char>>::try_into(i) {
69                    match CScript::try_from(c.script()) {
70                        Ok(cs) => {
71                            scripts_in_specs.insert(cs);
72                        }
73                        Err(_e) => {}
74                    };
75                }
76            });
77        }
78
79        #[allow(clippy::unwrap_used)]
80        scripts_in_specs.insert(CScript::try_from(Script::Common).unwrap());
81
82        scripts_in_specs
83    }
84
85    /**
86        Load a font from memory. The buffer, in which the font data is stored might be changed
87        after calling this function.
88    */
89    pub fn from_data(data: &'a mut Vec<u8>) -> FontLoadingResult<Self> {
90        assert!(data.len() >= 4);
91        let (font_type, re, packed_data) = if &data[0..4] == b"\x00\x01\x00\x00" {
92            (FontType::TTF, FontRef::from_index(data, 0), None)
93        } else if &data[0..4] == b"OTTO" {
94            (FontType::OTF, FontRef::from_index(data, 0), None)
95        } else if &data[0..4] == b"wOF2" {
96            #[cfg(feature = "woff2")]
97            {
98                let cv = match convert_woff2_to_ttf(&mut data.as_slice()) {
99                    Ok(c) => c,
100                    Err(e) => return Err(e.to_string()),
101                };
102                let pack = data.clone();
103
104                data.clear();
105                data.extend_from_slice(cv.as_slice());
106
107                (FontType::WOFF2, FontRef::from_index(data, 0), Some(pack))
108            }
109            #[cfg(not(feature = "woff2"))]
110            unimplemented!("activate the woff2 feature for this font")
111        } else if &data[0..4] == b"wOFF" {
112            let mut inp_cur = Cursor::new(&data);
113            let mut out_cur = Cursor::new(Vec::new());
114            rs_woff::woff2otf(&mut inp_cur, &mut out_cur)
115                .expect("font conversion from woff1 unsuccessful");
116
117            let pack = data.clone();
118
119            data.clear();
120            data.extend_from_slice(out_cur.get_ref().as_slice());
121
122            (FontType::WOFF, FontRef::from_index(data, 0), Some(pack))
123        } else {
124            unimplemented!("unrecognized font magic {:?}", &data[0..4]);
125        };
126
127        let re = match re {
128            None => return Err(FontLoadingError::from("loading font failed")),
129            Some(e) => e,
130        };
131
132        let font_name = match re
133            .localized_strings()
134            .find(|s| s.id() == StringId::PostScript)
135        {
136            None => "FontNameNotFound".to_string(),
137            Some(locale) => locale.to_string(),
138        };
139
140        let mut scale_context = ScaleContext::new();
141        let mut scaler = scale_context.builder(re).size(20_f32).build();
142        let glyph_id = re.charmap().map('a');
143        let outline = scaler.scale_outline(glyph_id).unwrap();
144
145        Ok(Font {
146            name: font_name,
147            re,
148            font_type,
149            supported_scripts: Font::identify_scripts_in_font(&re),
150            packed_font_data: packed_data,
151            approximate_pixel_width: outline.bounds().width() / 20.,
152        })
153    }
154
155    pub(crate) fn reference(&self) -> &FontRef<'a> {
156        &self.re
157    }
158
159    pub(crate) fn font_type(&self) -> &FontType {
160        &self.font_type
161    }
162
163    pub(crate) fn name(&self) -> &str {
164        &self.name
165    }
166
167    pub(crate) fn packed(&self) -> &Option<Vec<u8>> {
168        &self.packed_font_data
169    }
170
171    pub(crate) fn approximate_pixel_width(&self) -> f32 {
172        self.approximate_pixel_width
173    }
174    #[allow(dead_code)]
175    pub(crate) fn supported_features(&self) -> impl IntoIterator<Item = (Tag, u16)> + '_ {
176        self.reference().features().map(|f| (f.tag(), 1))
177    }
178}
179
180impl<'a> PartialEq<Self> for Font<'a> {
181    fn eq(&self, other: &Self) -> bool {
182        self.name.eq(&other.name)
183    }
184}
185
186impl<'a> Eq for Font<'a> {}
187
188impl<'a> Hash for Font<'a> {
189    fn hash<H: Hasher>(&self, state: &mut H) {
190        self.name.hash(state)
191    }
192}
193
194/**
195    Manages multiple [`Font`]s and selects the right one for each writing script used in the
196    input text. Has to be built via the [`FontSetBuilder`].
197*/
198#[derive(Clone)]
199pub struct FontSet<'a> {
200    inner: Arc<Vec<Font<'a>>>,
201}
202
203impl<'a> FontSet<'a> {
204    pub(crate) fn get_font_for_script(&self, script: &CScript) -> Option<&Font> {
205        self.inner
206            .iter()
207            .find(|f| f.supported_scripts.contains(script))
208    }
209}
210
211/**
212    Builds a [`FontSet`]
213
214    Example Use:
215    ```
216    use wordcloud::font::{FontSet, FontSetBuilder};
217
218    // let fonts = ...;
219    let font_set: FontSet = FontSetBuilder::new()
220        .extend(fonts)
221        .build();
222    ```
223*/
224#[derive(Default)]
225pub struct FontSetBuilder<'a> {
226    fonts: Vec<Font<'a>>,
227}
228
229impl<'a> FontSetBuilder<'a> {
230    /**
231        Construct a new [`FontSetBuilder`]
232    */
233    pub fn new() -> Self {
234        Self::default()
235    }
236
237    /**
238        Add a new [`Font`] to the [`FontSet`]
239    */
240    pub fn push(mut self, font: Font<'a>) -> Self {
241        if self.fonts.iter().any(|x| x.name == font.name) {
242            eprintln!(
243                "Skipped duplicate font / second font with duplicate name: {}",
244                font.name
245            )
246        } else {
247            self.fonts.push(font);
248        }
249        self
250    }
251
252    /**
253        Add a collection of [`Font`]s to the [`FontSet`]
254    */
255    pub fn extend(mut self, fonts: Vec<Font<'a>>) -> Self {
256        for font in fonts {
257            self = self.push(font);
258        }
259        self
260    }
261
262    /**
263        Build a [`FontSet`] from the fonts. Panics, if no font was provided.
264    */
265    pub fn build(self) -> FontSet<'a> {
266        if self.fonts.is_empty() {
267            panic!("At least one font needs to be provided.");
268        }
269        FontSet {
270            inner: Arc::new(self.fonts),
271        }
272    }
273}
274
275#[derive(Hash, PartialEq, Eq, Debug)]
276pub(crate) struct CScript {
277    u: unicode_script::Script,
278    s: swash::text::Script,
279}
280
281impl CScript {
282    #[allow(dead_code)]
283    pub fn u(&self) -> unicode_script::Script {
284        self.u
285    }
286
287    pub fn s(&self) -> swash::text::Script {
288        self.s
289    }
290}
291
292impl TryFrom<swash::text::Script> for CScript {
293    type Error = ();
294
295    fn try_from(value: swash::text::Script) -> Result<CScript, ()> {
296        match unicode_script::Script::from_full_name(value.name().replace(' ', "_").as_str()) {
297            None => Err(()),
298            Some(u) => Ok(CScript { u, s: value }),
299        }
300    }
301}
302
303impl Default for CScript {
304    fn default() -> Self {
305        CScript {
306            u: unicode_script::Script::Unknown,
307            s: swash::text::Script::Unknown,
308        }
309    }
310}
311
312pub(crate) trait GuessScript {
313    fn guess_script(&self) -> CScript;
314}
315
316impl GuessScript for String {
317    fn guess_script(&self) -> CScript {
318        match self.chars().next() {
319            None => CScript::default(),
320            Some(cr) => CScript {
321                u: unicode_script::UnicodeScript::script(&cr),
322                s: Codepoint::script(cr),
323            },
324        }
325    }
326}
327
328impl GuessScript for &str {
329    fn guess_script(&self) -> CScript {
330        match self.chars().next() {
331            None => CScript::default(),
332            Some(cr) => CScript {
333                u: unicode_script::UnicodeScript::script(&cr),
334                s: Codepoint::script(cr),
335            },
336        }
337    }
338}