Skip to main content

mathtex_engine/
resource_font.rs

1use alloc::collections::BTreeMap;
2use alloc::string::{String, ToString};
3use core::cell::RefCell;
4
5use mathtex_font::{
6    FontData, FontError, FontLoader, FontQuery, FontSystem, ShapeRequest, ShapedText,
7};
8use mathtex_ir::FontId;
9
10use crate::resource::{ResourceError, ResourceProvider};
11
12/// Loads font bytes through a [`ResourceProvider`], shaping is unsupported.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct ResourceFontSystem<R> {
15    resources: R,
16    loaded_fonts: RefCell<BTreeMap<String, FontData>>,
17}
18
19impl<R> ResourceFontSystem<R> {
20    /// Creates a font system backed by the given resource provider.
21    #[must_use]
22    pub fn new(resources: R) -> Self {
23        Self {
24            resources,
25            loaded_fonts: RefCell::new(BTreeMap::new()),
26        }
27    }
28
29    /// Returns a reference to the underlying resource provider.
30    #[must_use]
31    pub fn resources(&self) -> &R {
32        &self.resources
33    }
34
35    /// Unwraps and returns the underlying resource provider.
36    #[must_use]
37    pub fn into_resources(self) -> R {
38        self.resources
39    }
40
41    /// Number of font resources cached by resolver facing name.
42    #[must_use]
43    pub fn cached_font_count(&self) -> usize {
44        self.loaded_fonts.borrow().len()
45    }
46}
47
48impl<R> FontSystem for ResourceFontSystem<R>
49where
50    R: ResourceProvider,
51{
52    fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
53        FontLoader::load_font(self, query)
54    }
55
56    fn shape_text(&self, _request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
57        Err(FontError::ShapingUnsupported {
58            message: "resource font system loads bytes but does not shape text".to_string(),
59        })
60    }
61}
62
63impl<R> FontLoader for ResourceFontSystem<R>
64where
65    R: ResourceProvider,
66{
67    fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
68        if let Some(font) = self
69            .loaded_fonts
70            .borrow()
71            .get(query.family.as_str())
72            .cloned()
73        {
74            return Ok(font);
75        }
76
77        let resource = self
78            .resources
79            .read_font(query.family.as_str())
80            .map_err(|error| resource_error_to_font_error(query, error))?;
81
82        let font = FontData::new(
83            font_id_for_name(&resource.canonical_name),
84            resource.canonical_name,
85            resource.bytes,
86        );
87        self.loaded_fonts
88            .borrow_mut()
89            .insert(query.family.clone(), font.clone());
90        Ok(font)
91    }
92}
93
94fn resource_error_to_font_error(query: &FontQuery, error: ResourceError) -> FontError {
95    match error {
96        ResourceError::NotFound { .. } => FontError::NotFound {
97            family: query.family.clone(),
98        },
99        ResourceError::Invalid { message, .. } | ResourceError::Denied { message, .. } => {
100            FontError::Invalid {
101                family: query.family.clone(),
102                message,
103            }
104        }
105    }
106}
107
108fn font_id_for_name(name: &str) -> FontId {
109    let mut hash = 2_166_136_261u32;
110    for byte in name.as_bytes() {
111        hash ^= u32::from(*byte);
112        hash = hash.wrapping_mul(16_777_619);
113    }
114    FontId(hash)
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::{InMemoryResourceProvider, ResourceKind};
121    use mathtex_ir::Length;
122
123    #[test]
124    fn resource_font_system_loads_fonts_through_resource_provider() {
125        let resources = InMemoryResourceProvider::new().with_resource(
126            "Latin Modern Math.otf",
127            ResourceKind::Font,
128            b"font-bytes",
129        );
130        let fonts = ResourceFontSystem::new(resources);
131
132        let font = FontSystem::load_font(
133            &fonts,
134            &FontQuery {
135                family: "Latin Modern Math.otf".to_string(),
136                size: Length::from_scaled_points(655_360),
137                math: true,
138            },
139        )
140        .expect("font should resolve through resource provider");
141
142        assert_eq!(font.canonical_name, "Latin Modern Math.otf");
143        assert_eq!(&**font.bytes().expect("library owned bytes"), b"font-bytes");
144        assert_eq!(font.id, font_id_for_name("Latin Modern Math.otf"));
145        assert_eq!(fonts.cached_font_count(), 1);
146
147        let cached = FontSystem::load_font(
148            &fonts,
149            &FontQuery {
150                family: "Latin Modern Math.otf".to_string(),
151                size: Length::from_scaled_points(327_680),
152                math: true,
153            },
154        )
155        .expect("cached font should resolve without another cache entry");
156
157        assert_eq!(cached, font);
158        assert_eq!(fonts.cached_font_count(), 1);
159    }
160
161    #[test]
162    fn resource_font_system_reports_missing_font_as_font_error() {
163        let fonts = ResourceFontSystem::new(InMemoryResourceProvider::new());
164
165        let error = FontSystem::load_font(
166            &fonts,
167            &FontQuery {
168                family: "missing.otf".to_string(),
169                size: Length::ZERO,
170                math: false,
171            },
172        )
173        .expect_err("missing resource should be a font error");
174
175        assert_eq!(
176            error,
177            FontError::NotFound {
178                family: "missing.otf".to_string(),
179            }
180        );
181    }
182
183    #[test]
184    fn resource_font_system_keeps_shaping_explicitly_separate() {
185        let fonts = ResourceFontSystem::new(InMemoryResourceProvider::new());
186
187        let error = fonts
188            .shape_text(&ShapeRequest {
189                font: FontId(0),
190                text: "x",
191                direction: mathtex_ir::Direction::LeftToRight,
192                source: None,
193                script: None,
194                features: Vec::new(),
195            })
196            .expect_err("resource adapter should not shape text");
197
198        match error {
199            FontError::ShapingUnsupported { message } => {
200                assert!(message.contains("does not shape text"));
201            }
202            other => panic!("unexpected error: {other:?}"),
203        }
204    }
205}