Skip to main content

rassa_shape/
lib.rs

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
15/// Register a virtual font file in memory.
16///
17/// This is primarily used by wasm/browser hosts that do not have a real
18/// filesystem/fontconfig database. Callers can return the same virtual `path`
19/// from their `FontProvider`; shaping and rasterization will then load bytes
20/// from this cache instead of `std::fs`.
21pub 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
28/// Look up previously registered virtual font bytes.
29pub 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}