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 characters = segment.text.chars().collect::<Vec<_>>();
165 let indices: Vec<usize> = match direction {
166 BidiDirection::RightToLeft | BidiDirection::WeakRightToLeft => {
167 (0..characters.len()).rev().collect()
168 }
169 _ => (0..characters.len()).collect(),
170 };
171
172 indices
173 .into_iter()
174 .map(|cluster| GlyphInfo {
175 glyph_id: characters[cluster] as u32,
176 cluster,
177 x_advance: 1.0,
178 y_advance: 0.0,
179 x_offset: 0.0,
180 y_offset: 0.0,
181 })
182 .collect()
183 }
184}
185
186#[derive(Default)]
187pub struct ShapeEngine {
188 unicode: UnicodePipeline,
189 simple: SimpleShaper,
190}
191
192impl ShapeEngine {
193 pub fn new() -> Self {
194 Self::default()
195 }
196
197 pub fn shape_text<P: FontProvider>(
198 &self,
199 provider: &P,
200 request: &ShapeRequest,
201 ) -> RassaResult<ShapedText> {
202 let analysis = self
203 .unicode
204 .analyze_text(&request.text, request.language.as_deref())?;
205 let font = provider.resolve(&FontQuery {
206 family: request.family.clone(),
207 style: request.style.clone(),
208 weight: request.weight,
209 });
210 let direction = analysis.bidi_analysis.direction;
211
212 let runs = analysis
213 .segments
214 .iter()
215 .map(|segment| ShapedRun {
216 text: segment.text.clone(),
217 char_range: segment.char_range.clone(),
218 byte_range: segment.byte_range.clone(),
219 direction,
220 font: font.clone(),
221 glyphs: match request.mode {
222 ShapingMode::Simple => self.simple.shape_segment(segment, &font, direction),
223 ShapingMode::Complex => self
224 .shape_segment_complex(
225 segment,
226 &font,
227 direction,
228 request.language.as_deref(),
229 request.font_size,
230 )
231 .unwrap_or_else(|| self.simple.shape_segment(segment, &font, direction)),
232 },
233 })
234 .collect();
235
236 Ok(ShapedText {
237 analysis,
238 font,
239 mode: request.mode,
240 runs,
241 })
242 }
243
244 fn shape_segment_complex(
245 &self,
246 segment: &TextSegment,
247 font: &FontMatch,
248 direction: BidiDirection,
249 language: Option<&str>,
250 font_size: Option<f32>,
251 ) -> Option<Vec<GlyphInfo>> {
252 let font_path = font.path.as_ref()?;
253 let bytes = cached_font_bytes(font_path)?;
254 let font_ref = FontRef::from_index(bytes.as_slice(), font.face_index.unwrap_or(0)).ok()?;
255 let shaper_data = ShaperData::new(&font_ref);
256 let shaper = shaper_data.shaper(&font_ref).build();
257
258 let mut buffer = UnicodeBuffer::new();
259 buffer.push_str(&segment.text);
260 buffer.guess_segment_properties();
261 buffer.set_direction(convert_direction(direction));
262 if let Some(language) = language.and_then(|value| Language::from_str(value).ok()) {
263 buffer.set_language(language);
264 }
265
266 let glyph_buffer = shaper.shape(buffer, &[]);
267 let units_per_em = shaper.units_per_em().max(1) as f32;
268 let scale = font_size
269 .filter(|size| size.is_finite() && *size > 0.0)
270 .unwrap_or(1.0)
271 / units_per_em;
272 let glyph_infos = glyph_buffer.glyph_infos();
273 let glyph_positions = glyph_buffer.glyph_positions();
274 if glyph_infos.len() != glyph_positions.len() {
275 return None;
276 }
277
278 Some(
279 glyph_infos
280 .iter()
281 .zip(glyph_positions.iter())
282 .map(|(info, position)| GlyphInfo {
283 glyph_id: info.glyph_id,
284 cluster: info.cluster as usize,
285 x_advance: position.x_advance as f32 * scale,
286 y_advance: position.y_advance as f32 * scale,
287 x_offset: position.x_offset as f32 * scale,
288 y_offset: position.y_offset as f32 * scale,
289 })
290 .collect(),
291 )
292 }
293}
294
295fn convert_direction(direction: BidiDirection) -> Direction {
296 match direction {
297 BidiDirection::RightToLeft | BidiDirection::WeakRightToLeft => Direction::RightToLeft,
298 _ => Direction::LeftToRight,
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use rassa_fonts::{FontProviderKind, FontconfigProvider, NullFontProvider};
306
307 #[test]
308 fn shape_engine_produces_one_run_for_single_line_text() {
309 let engine = ShapeEngine::new();
310 let provider = NullFontProvider;
311 let shaped = engine
312 .shape_text(&provider, &ShapeRequest::new("hello", "Sans"))
313 .expect("shaping should succeed");
314
315 assert_eq!(shaped.runs.len(), 1);
316 assert_eq!(shaped.runs[0].glyphs.len(), 5);
317 assert_eq!(shaped.font.provider, FontProviderKind::Null);
318 }
319
320 #[test]
321 fn shape_engine_splits_runs_on_mandatory_breaks() {
322 let engine = ShapeEngine::new();
323 let provider = NullFontProvider;
324 let shaped = engine
325 .shape_text(&provider, &ShapeRequest::new("a\nb", "Sans"))
326 .expect("shaping should succeed");
327
328 assert_eq!(shaped.runs.len(), 2);
329 assert_eq!(shaped.runs[0].text, "a\n");
330 assert_eq!(shaped.runs[1].text, "b");
331 }
332
333 #[test]
334 fn complex_shaping_uses_resolved_font_path() {
335 let engine = ShapeEngine::new();
336 let provider = FontconfigProvider::new();
337 let shaped = engine
338 .shape_text(
339 &provider,
340 &ShapeRequest::new("office", "sans")
341 .with_language("en")
342 .with_mode(ShapingMode::Complex),
343 )
344 .expect("complex shaping should succeed");
345
346 assert_eq!(shaped.mode, ShapingMode::Complex);
347 assert!(!shaped.runs.is_empty());
348 assert!(!shaped.runs[0].glyphs.is_empty());
349 assert!(shaped.font.path.is_some());
350 }
351}