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 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}