mathtex_engine/
resource_font.rs1use 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#[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 #[must_use]
22 pub fn new(resources: R) -> Self {
23 Self {
24 resources,
25 loaded_fonts: RefCell::new(BTreeMap::new()),
26 }
27 }
28
29 #[must_use]
31 pub fn resources(&self) -> &R {
32 &self.resources
33 }
34
35 #[must_use]
37 pub fn into_resources(self) -> R {
38 self.resources
39 }
40
41 #[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}