charts_rs/charts/
font.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5//     http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12
13use super::util::*;
14use fontdue::layout::{CoordinateSystem, Layout, TextStyle};
15use fontdue::Font;
16use once_cell::sync::OnceCell;
17use snafu::Snafu;
18use std::collections::HashMap;
19
20#[derive(Debug, Snafu)]
21pub enum Error {
22    #[snafu(display("Error font: {name} not found"))]
23    FontNotFound { name: String },
24    #[snafu(display("Error parse font: {message}"))]
25    ParseFont { message: String },
26}
27
28impl From<&str> for Error {
29    fn from(value: &str) -> Self {
30        Error::ParseFont {
31            message: value.to_string(),
32        }
33    }
34}
35
36pub type Result<T, E = Error> = std::result::Result<T, E>;
37
38pub static DEFAULT_FONT_FAMILY: &str = "Roboto";
39pub static DEFAULT_FONT_DATA: &[u8] = include_bytes!("../Roboto.ttf");
40
41fn get_family_from_font(font: &fontdue::Font) -> String {
42    if let Ok(re) = regex::Regex::new(r#"name:( ?)Some\("(?P<family>[\S ]+)"\)"#) {
43        let desc = format!("{:?}", font);
44        if let Some(caps) = re.captures(&desc) {
45            let mut family = caps["family"].to_string();
46            // https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight
47            // replace some font weight
48            if let Ok(weight) = regex::Regex::new(r#"Thin|Light|Regular|Medium|Bold|Black$"#) {
49                family = weight.replace_all(&family, "").to_string();
50            }
51            return family.trim().to_string();
52        }
53    }
54    "".to_string()
55}
56
57pub fn get_or_try_init_fonts(fonts: Option<Vec<&[u8]>>) -> Result<&'static HashMap<String, Font>> {
58    static GLOBAL_FONTS: OnceCell<HashMap<String, Font>> = OnceCell::new();
59    GLOBAL_FONTS.get_or_try_init(|| {
60        let mut m = HashMap::new();
61        // init fonts, will returns an error if load font fails.
62        let font = fontdue::Font::from_bytes(DEFAULT_FONT_DATA, fontdue::FontSettings::default())?;
63        m.insert(DEFAULT_FONT_FAMILY.to_string(), font);
64        let mut font_datas = vec![DEFAULT_FONT_DATA];
65        if let Some(value) = fonts {
66            for data in value.iter() {
67                let font = fontdue::Font::from_bytes(*data, fontdue::FontSettings::default())?;
68                let family = get_family_from_font(&font);
69                if !family.is_empty() {
70                    m.insert(family, font);
71                    font_datas.push(*data);
72                }
73            }
74        }
75        #[cfg(feature = "image-encoder")]
76        crate::get_or_init_fontdb(Some(font_datas));
77        Ok(m)
78    })
79}
80/// Gets font by font family.
81pub fn get_font(name: &str) -> Result<&Font> {
82    let fonts = get_or_try_init_fonts(None)?;
83    if let Some(font) = fonts.get(name).or_else(|| fonts.get(DEFAULT_FONT_FAMILY)) {
84        Ok(font)
85    } else {
86        FontNotFoundSnafu {
87            name: name.to_string(),
88        }
89        .fail()
90    }
91}
92/// Gets all supported font family
93pub fn get_font_families() -> Result<Vec<String>> {
94    let fonts = get_or_try_init_fonts(None)?;
95    let mut families = vec![];
96    for (name, _) in fonts.iter() {
97        families.push(name.to_string());
98    }
99    Ok(families)
100}
101
102/// Measures the display area of text of a specified font size.
103pub fn measure_text(font: &Font, font_size: f32, text: &str) -> Box {
104    let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
105    layout.append(&[font], &TextStyle::new(text, font_size, 0));
106
107    let mut right = 0.0_f32;
108    let mut bottom = 0.0_f32;
109    for g in layout.glyphs().iter() {
110        let x = g.x + g.width as f32;
111        let y = g.y + g.height as f32;
112        if x > right {
113            right = x;
114        }
115        if y > bottom {
116            bottom = y;
117        }
118    }
119    Box {
120        right,
121        bottom,
122        ..Default::default()
123    }
124}
125
126/// Measures the display area of text of a specified font size and font family.
127pub fn measure_text_width_family(font_family: &str, font_size: f32, text: &str) -> Result<Box> {
128    let font = get_font(font_family)?;
129    Ok(measure_text(font, font_size, text))
130}
131
132/// Gets the max width of multi text.
133pub fn measure_max_text_width_family(
134    font_family: &str,
135    font_size: f32,
136    texts: Vec<&str>,
137) -> Result<Box> {
138    let font = get_font(font_family)?;
139    let mut result = Box::default();
140    for item in texts.iter() {
141        let b = measure_text(font, font_size, item);
142        if b.width() > result.width() {
143            result = b;
144        }
145    }
146    Ok(result)
147}
148
149/// Cuts the text wrap fix size to muli text list.
150pub fn text_wrap_fit(
151    font_family: &str,
152    font_size: f32,
153    text: &str,
154    width: f32,
155) -> Result<Vec<String>> {
156    let font = get_font(font_family)?;
157    let b = measure_text(font, font_size, text);
158    if b.width() <= width {
159        return Ok(vec![text.to_string()]);
160    }
161
162    let mut current = "".to_string();
163    let mut result = vec![];
164    for item in text.chars() {
165        let new_str = current.clone() + &item.to_string();
166        let b = measure_text(font, font_size, &new_str);
167        if b.width() > width {
168            result.push(current);
169            current = item.to_string();
170            continue;
171        }
172        current = new_str;
173    }
174    if !current.is_empty() {
175        result.push(current);
176    }
177    Ok(result)
178}
179
180#[cfg(test)]
181mod tests {
182    use super::{get_font, get_font_families, measure_text_width_family, text_wrap_fit};
183    use pretty_assertions::assert_eq;
184    #[test]
185    fn measure_text() {
186        let name = "Roboto";
187        get_font(name).unwrap();
188
189        let str = "Hello World!";
190        let b = measure_text_width_family(name, 14.0, str).unwrap();
191
192        assert_eq!(79.0, b.width().ceil());
193        assert_eq!(14.0, b.height());
194
195        assert_eq!("Roboto", get_font_families().unwrap().join(","));
196    }
197    #[test]
198    fn wrap_fit() {
199        let name = "Roboto";
200        let result = text_wrap_fit(name, 14.0, "An event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications", 100.0).unwrap();
201        assert_eq!(
202            vec![
203                "An event-drive",
204                "n, non-blocking ",
205                "I/O platform fo",
206                "r writing async",
207                "hronous I/O ba",
208                "cked applicati",
209                "ons",
210            ],
211            result
212        );
213    }
214}