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}