1use std::{
2 collections::HashMap,
3 fs,
4 path::{Path, PathBuf},
5 str::FromStr,
6 sync::{Arc, Mutex, OnceLock},
7};
8
9static FONT_BYTES_CACHE: OnceLock<Mutex<HashMap<PathBuf, Arc<Vec<u8>>>>> = OnceLock::new();
10
11fn font_bytes_cache() -> &'static Mutex<HashMap<PathBuf, Arc<Vec<u8>>>> {
12 FONT_BYTES_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
13}
14
15pub fn register_virtual_font_bytes(path: impl Into<PathBuf>, bytes: impl Into<Vec<u8>>) {
22 font_bytes_cache()
23 .lock()
24 .expect("font bytes cache mutex poisoned")
25 .insert(path.into(), Arc::new(bytes.into()));
26}
27
28pub fn virtual_font_bytes(path: &Path) -> Option<Arc<Vec<u8>>> {
30 font_bytes_cache()
31 .lock()
32 .expect("font bytes cache mutex poisoned")
33 .get(path)
34 .cloned()
35}
36
37fn cached_font_bytes(path: &Path) -> Option<Arc<Vec<u8>>> {
38 if let Some(bytes) = virtual_font_bytes(path) {
39 return Some(bytes);
40 }
41
42 let bytes = Arc::new(fs::read(path).ok()?);
43 font_bytes_cache()
44 .lock()
45 .expect("font bytes cache mutex poisoned")
46 .insert(path.to_path_buf(), bytes.clone());
47 Some(bytes)
48}
49
50use harfrust::{Direction, FontRef, Language, ShaperData, UnicodeBuffer};
51use rassa_core::RassaResult;
52use rassa_fonts::{FontMatch, FontProvider, FontQuery};
53use rassa_unicode::{BidiDirection, TextSegment, UnicodeAnalysis, UnicodePipeline};
54
55#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
56pub enum ShapingMode {
57 #[default]
58 Simple,
59 Complex,
60}
61
62#[derive(Clone, Debug, PartialEq)]
63pub struct ShapeRequest {
64 pub text: String,
65 pub family: String,
66 pub style: Option<String>,
67 pub weight: Option<i32>,
68 pub language: Option<String>,
69 pub mode: ShapingMode,
70 pub font_size: Option<f32>,
71}
72
73impl ShapeRequest {
74 pub fn new(text: impl Into<String>, family: impl Into<String>) -> Self {
75 Self {
76 text: text.into(),
77 family: family.into(),
78 style: None,
79 weight: None,
80 language: None,
81 mode: ShapingMode::Simple,
82 font_size: None,
83 }
84 }
85
86 pub fn with_style(mut self, style: impl Into<String>) -> Self {
87 self.style = Some(style.into());
88 self
89 }
90
91 pub fn with_weight(mut self, weight: i32) -> Self {
92 self.weight = Some(weight);
93 self
94 }
95
96 pub fn with_optional_weight(mut self, weight: Option<i32>) -> Self {
97 self.weight = weight;
98 self
99 }
100
101 pub fn with_language(mut self, language: impl Into<String>) -> Self {
102 self.language = Some(language.into());
103 self
104 }
105
106 pub fn with_mode(mut self, mode: ShapingMode) -> Self {
107 self.mode = mode;
108 self
109 }
110
111 pub fn with_font_size(mut self, font_size: f32) -> Self {
112 self.font_size = font_size.is_finite().then_some(font_size.max(0.0));
113 self
114 }
115}
116
117#[derive(Clone, Debug, Default, PartialEq)]
118pub struct GlyphInfo {
119 pub glyph_id: u32,
120 pub cluster: usize,
121 pub x_advance: f32,
122 pub y_advance: f32,
123 pub x_offset: f32,
124 pub y_offset: f32,
125}
126
127#[derive(Clone, Debug, Default, PartialEq)]
128pub struct ShapedRun {
129 pub text: String,
130 pub char_range: std::ops::Range<usize>,
131 pub byte_range: std::ops::Range<usize>,
132 pub direction: BidiDirection,
133 pub font: FontMatch,
134 pub glyphs: Vec<GlyphInfo>,
135}
136
137#[derive(Clone, Debug, Default, PartialEq)]
138pub struct ShapedText {
139 pub analysis: UnicodeAnalysis,
140 pub font: FontMatch,
141 pub mode: ShapingMode,
142 pub runs: Vec<ShapedRun>,
143}
144
145pub trait Shaper {
146 fn shape_segment(
147 &self,
148 segment: &TextSegment,
149 font: &FontMatch,
150 direction: BidiDirection,
151 ) -> Vec<GlyphInfo>;
152}
153
154#[derive(Default)]
155pub struct SimpleShaper;
156
157impl Shaper for SimpleShaper {
158 fn shape_segment(
159 &self,
160 segment: &TextSegment,
161 _font: &FontMatch,
162 direction: BidiDirection,
163 ) -> Vec<GlyphInfo> {
164 let char_count = segment.text.chars().count();
165 let mut glyphs = Vec::with_capacity(char_count);
166 match direction {
167 BidiDirection::RightToLeft | BidiDirection::WeakRightToLeft => {
168 let characters = segment.text.chars().collect::<Vec<_>>();
169 for (cluster, character) in characters.into_iter().enumerate().rev() {
170 glyphs.push(GlyphInfo {
171 glyph_id: character as u32,
172 cluster,
173 x_advance: 1.0,
174 y_advance: 0.0,
175 x_offset: 0.0,
176 y_offset: 0.0,
177 });
178 }
179 }
180 _ => {
181 for (cluster, character) in segment.text.chars().enumerate() {
182 glyphs.push(GlyphInfo {
183 glyph_id: character as u32,
184 cluster,
185 x_advance: 1.0,
186 y_advance: 0.0,
187 x_offset: 0.0,
188 y_offset: 0.0,
189 });
190 }
191 }
192 }
193 glyphs
194 }
195}
196
197#[derive(Default)]
198pub struct ShapeEngine {
199 unicode: UnicodePipeline,
200 simple: SimpleShaper,
201}
202
203impl ShapeEngine {
204 pub fn new() -> Self {
205 Self::default()
206 }
207
208 pub fn shape_text<P: FontProvider>(
209 &self,
210 provider: &P,
211 request: &ShapeRequest,
212 ) -> RassaResult<ShapedText> {
213 let analysis = self
214 .unicode
215 .analyze_text(&request.text, request.language.as_deref())?;
216 let font = provider.resolve(&FontQuery {
217 family: request.family.clone(),
218 style: request.style.clone(),
219 weight: request.weight,
220 });
221 let direction = analysis.bidi_analysis.direction;
222
223 let runs = analysis
224 .segments
225 .iter()
226 .map(|segment| ShapedRun {
227 text: segment.text.clone(),
228 char_range: segment.char_range.clone(),
229 byte_range: segment.byte_range.clone(),
230 direction,
231 font: font.clone(),
232 glyphs: match request.mode {
233 ShapingMode::Simple => self.simple.shape_segment(segment, &font, direction),
234 ShapingMode::Complex => self
235 .shape_segment_complex(
236 segment,
237 &font,
238 direction,
239 request.language.as_deref(),
240 request.font_size,
241 )
242 .unwrap_or_else(|| self.simple.shape_segment(segment, &font, direction)),
243 },
244 })
245 .collect();
246
247 Ok(ShapedText {
248 analysis,
249 font,
250 mode: request.mode,
251 runs,
252 })
253 }
254
255 fn shape_segment_complex(
256 &self,
257 segment: &TextSegment,
258 font: &FontMatch,
259 direction: BidiDirection,
260 language: Option<&str>,
261 font_size: Option<f32>,
262 ) -> Option<Vec<GlyphInfo>> {
263 let font_path = font.path.as_ref()?;
264 let bytes = cached_font_bytes(font_path)?;
265 let font_ref = FontRef::from_index(bytes.as_slice(), font.face_index.unwrap_or(0)).ok()?;
266 let shaper_data = ShaperData::new(&font_ref);
267 let shaper = shaper_data.shaper(&font_ref).build();
268
269 let mut buffer = UnicodeBuffer::new();
270 buffer.push_str(&segment.text);
271 buffer.guess_segment_properties();
272 buffer.set_direction(convert_direction(direction));
273 if let Some(language) = language.and_then(|value| Language::from_str(value).ok()) {
274 buffer.set_language(language);
275 }
276
277 let glyph_buffer = shaper.shape(buffer, &[]);
278 let units_per_em = shaper.units_per_em().max(1) as f32;
279 let scale = font_size
280 .filter(|size| size.is_finite() && *size > 0.0)
281 .unwrap_or(1.0)
282 / units_per_em;
283 let glyph_infos = glyph_buffer.glyph_infos();
284 let glyph_positions = glyph_buffer.glyph_positions();
285 if glyph_infos.len() != glyph_positions.len() {
286 return None;
287 }
288
289 Some(
290 glyph_infos
291 .iter()
292 .zip(glyph_positions.iter())
293 .map(|(info, position)| GlyphInfo {
294 glyph_id: info.glyph_id,
295 cluster: info.cluster as usize,
296 x_advance: position.x_advance as f32 * scale,
297 y_advance: position.y_advance as f32 * scale,
298 x_offset: position.x_offset as f32 * scale,
299 y_offset: position.y_offset as f32 * scale,
300 })
301 .collect(),
302 )
303 }
304}
305
306fn convert_direction(direction: BidiDirection) -> Direction {
307 match direction {
308 BidiDirection::RightToLeft | BidiDirection::WeakRightToLeft => Direction::RightToLeft,
309 _ => Direction::LeftToRight,
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316 use rassa_fonts::{FontProviderKind, FontconfigProvider, NullFontProvider};
317
318 #[test]
319 fn shape_engine_produces_one_run_for_single_line_text() {
320 let engine = ShapeEngine::new();
321 let provider = NullFontProvider;
322 let shaped = engine
323 .shape_text(&provider, &ShapeRequest::new("hello", "Sans"))
324 .expect("shaping should succeed");
325
326 assert_eq!(shaped.runs.len(), 1);
327 assert_eq!(shaped.runs[0].glyphs.len(), 5);
328 assert_eq!(shaped.font.provider, FontProviderKind::Null);
329 }
330
331 #[test]
332 fn shape_engine_splits_runs_on_mandatory_breaks() {
333 let engine = ShapeEngine::new();
334 let provider = NullFontProvider;
335 let shaped = engine
336 .shape_text(&provider, &ShapeRequest::new("a\nb", "Sans"))
337 .expect("shaping should succeed");
338
339 assert_eq!(shaped.runs.len(), 2);
340 assert_eq!(shaped.runs[0].text, "a\n");
341 assert_eq!(shaped.runs[1].text, "b");
342 }
343
344 #[test]
345 fn complex_shaping_uses_resolved_font_path() {
346 let engine = ShapeEngine::new();
347 let provider = FontconfigProvider::new();
348 let shaped = engine
349 .shape_text(
350 &provider,
351 &ShapeRequest::new("office", "sans")
352 .with_language("en")
353 .with_mode(ShapingMode::Complex),
354 )
355 .expect("complex shaping should succeed");
356
357 assert_eq!(shaped.mode, ShapingMode::Complex);
358 assert!(!shaped.runs.is_empty());
359 assert!(!shaped.runs[0].glyphs.is_empty());
360 assert!(shaped.font.path.is_some());
361 }
362}