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