Skip to main content

rassa_shape/
lib.rs

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