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