1use std::str::FromStr;
11use std::sync::Arc;
12
13use harfrust::{
14 Direction as HrDirection, Feature, FontRef as HrFontRef, GlyphBuffer, Language, Script,
15 ShaperData, ShaperInstance, Tag, UnicodeBuffer, Variation,
16};
17
18use typf_core::{
19 error::Result,
20 traits::{FontRef, Shaper, Stage},
21 types::{Direction, PositionedGlyph, ShapingResult},
22 ShapingParams,
23};
24
25pub use typf_core::shaping_cache::{CacheStats, ShapingCache, ShapingCacheKey, SharedShapingCache};
27
28pub struct HarfrustShaper {
32 cache: Option<SharedShapingCache>,
34}
35
36impl HarfrustShaper {
37 pub fn new() -> Self {
39 Self { cache: None }
40 }
41
42 pub fn with_cache() -> Self {
46 Self {
47 cache: Some(Arc::new(std::sync::RwLock::new(ShapingCache::new()))),
48 }
49 }
50
51 pub fn with_shared_cache(cache: SharedShapingCache) -> Self {
55 Self { cache: Some(cache) }
56 }
57
58 pub fn cache_stats(&self) -> Option<CacheStats> {
60 self.cache
61 .as_ref()
62 .and_then(|c| c.read().ok())
63 .map(|c| c.stats())
64 }
65
66 pub fn cache_hit_rate(&self) -> Option<f64> {
68 self.cache
69 .as_ref()
70 .and_then(|c| c.read().ok())
71 .map(|c| c.hit_rate())
72 }
73
74 fn to_hr_direction(dir: Direction) -> HrDirection {
76 match dir {
77 Direction::LeftToRight => HrDirection::LeftToRight,
78 Direction::RightToLeft => HrDirection::RightToLeft,
79 Direction::TopToBottom => HrDirection::TopToBottom,
80 Direction::BottomToTop => HrDirection::BottomToTop,
81 }
82 }
83
84 fn parse_tag(tag_str: &str) -> Option<Tag> {
86 if tag_str.len() == 4 {
87 let bytes = tag_str.as_bytes();
88 Some(Tag::new(&[bytes[0], bytes[1], bytes[2], bytes[3]]))
89 } else {
90 None
91 }
92 }
93
94 fn fallback_shape(
96 &self,
97 text: &str,
98 font: Arc<dyn FontRef>,
99 params: &ShapingParams,
100 ) -> ShapingResult {
101 let mut glyphs = Vec::new();
102 let mut x_offset = 0.0;
103
104 for ch in text.chars() {
105 if let Some(glyph_id) = font.glyph_id(ch) {
106 let advance = font.advance_width(glyph_id);
107 glyphs.push(PositionedGlyph {
108 id: glyph_id,
109 x: x_offset,
110 y: 0.0,
111 advance,
112 cluster: 0,
113 });
114 x_offset += advance * params.size / font.units_per_em() as f32;
115 }
116 }
117
118 ShapingResult {
119 glyphs,
120 advance_width: x_offset,
121 advance_height: params.size,
122 direction: params.direction,
123 }
124 }
125
126 fn extract_glyphs(buffer: &GlyphBuffer, ppem: f32, upem: u16) -> (Vec<PositionedGlyph>, f32) {
128 let mut glyphs = Vec::new();
129 let mut x_offset = 0.0;
130 let scale = ppem / upem as f32;
131
132 let positions = buffer.glyph_positions();
133 let infos = buffer.glyph_infos();
134
135 for (info, pos) in infos.iter().zip(positions.iter()) {
136 glyphs.push(PositionedGlyph {
137 id: info.glyph_id,
138 x: x_offset + (pos.x_offset as f32 * scale),
139 y: pos.y_offset as f32 * scale,
140 advance: pos.x_advance as f32 * scale,
141 cluster: info.cluster,
142 });
143
144 x_offset += pos.x_advance as f32 * scale;
145 }
146
147 (glyphs, x_offset)
148 }
149}
150
151impl Default for HarfrustShaper {
152 fn default() -> Self {
153 Self::new()
154 }
155}
156
157impl Stage for HarfrustShaper {
158 fn name(&self) -> &'static str {
159 "Harfrust"
160 }
161
162 fn process(
163 &self,
164 ctx: typf_core::context::PipelineContext,
165 ) -> Result<typf_core::context::PipelineContext> {
166 Ok(ctx)
168 }
169}
170
171impl Shaper for HarfrustShaper {
172 fn name(&self) -> &'static str {
173 "Harfrust"
174 }
175
176 fn shape(
177 &self,
178 text: &str,
179 font: Arc<dyn FontRef>,
180 params: &ShapingParams,
181 ) -> Result<ShapingResult> {
182 if text.is_empty() {
183 return Ok(ShapingResult {
184 glyphs: Vec::new(),
185 advance_width: 0.0,
186 advance_height: params.size,
187 direction: params.direction,
188 });
189 }
190
191 let font_data = font.data();
193
194 let cache_key = if self.cache.is_some() {
196 let key = ShapingCacheKey::new(
197 text,
198 Shaper::name(self),
199 font_data,
200 params.size,
201 params.language.clone(),
202 params.script.clone(),
203 params.features.clone(),
204 params.variations.clone(),
205 );
206 if let Some(ref cache) = self.cache {
208 if let Ok(cache_guard) = cache.read() {
209 if let Some(result) = cache_guard.get(&key) {
210 return Ok(result);
211 }
212 }
213 }
214 Some(key)
215 } else {
216 None
217 };
218
219 if font_data.is_empty() {
220 let result = self.fallback_shape(text, font, params);
222
223 if let Some(key) = cache_key {
225 if let Some(ref cache) = self.cache {
226 if let Ok(cache_guard) = cache.write() {
227 cache_guard.insert(key, result.clone());
228 }
229 }
230 }
231
232 return Ok(result);
233 }
234
235 let hr_font = match HrFontRef::new(font_data) {
237 Ok(f) => f,
238 Err(_) => {
239 let result = self.fallback_shape(text, font.clone(), params);
241 if let Some(key) = cache_key {
242 if let Some(ref cache) = self.cache {
243 if let Ok(cache_guard) = cache.write() {
244 cache_guard.insert(key, result.clone());
245 }
246 }
247 }
248 return Ok(result);
249 },
250 };
251
252 let shaper_data = ShaperData::new(&hr_font);
254
255 let instance = if !params.variations.is_empty() {
257 let variations: Vec<Variation> = params
258 .variations
259 .iter()
260 .filter_map(|(tag_str, value)| {
261 Self::parse_tag(tag_str).map(|tag| Variation { tag, value: *value })
262 })
263 .collect();
264 Some(ShaperInstance::from_variations(&hr_font, variations))
265 } else {
266 None
267 };
268
269 let mut builder = shaper_data.shaper(&hr_font);
271 if let Some(ref inst) = instance {
272 builder = builder.instance(Some(inst));
273 }
274 builder = builder.point_size(Some(params.size));
275 let shaper = builder.build();
276
277 let mut buffer = UnicodeBuffer::new();
279 buffer.push_str(text);
280 buffer.set_direction(Self::to_hr_direction(params.direction));
281
282 if let Some(ref lang) = params.language {
284 if let Ok(language) = Language::from_str(lang) {
285 buffer.set_language(language);
286 }
287 }
288
289 if let Some(ref script_str) = params.script {
291 if let Some(tag) = Self::parse_tag(script_str) {
292 if let Some(script) = Script::from_iso15924_tag(tag) {
293 buffer.set_script(script);
294 }
295 }
296 }
297
298 let features: Vec<Feature> = params
300 .features
301 .iter()
302 .filter_map(|(name, value)| {
303 Self::parse_tag(name).map(|tag| Feature {
304 tag,
305 value: *value,
306 start: 0,
307 end: u32::MAX,
308 })
309 })
310 .collect();
311
312 let output = shaper.shape(buffer, &features);
314
315 let upem = font.units_per_em();
317 let (glyphs, advance_width) = Self::extract_glyphs(&output, params.size, upem);
318
319 let result = ShapingResult {
320 glyphs,
321 advance_width,
322 advance_height: params.size,
323 direction: params.direction,
324 };
325
326 if let Some(key) = cache_key {
328 if let Some(ref cache) = self.cache {
329 if let Ok(cache_guard) = cache.write() {
330 cache_guard.insert(key, result.clone());
331 }
332 }
333 }
334
335 Ok(result)
336 }
337
338 fn supports_script(&self, _script: &str) -> bool {
339 true
341 }
342
343 fn clear_cache(&self) {
344 if let Some(ref cache) = self.cache {
345 if let Ok(mut cache_guard) = cache.write() {
346 *cache_guard = ShapingCache::new();
347 }
348 }
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 struct TestFont {
357 data: Vec<u8>,
358 }
359
360 impl FontRef for TestFont {
361 fn data(&self) -> &[u8] {
362 &self.data
363 }
364
365 fn units_per_em(&self) -> u16 {
366 1000
367 }
368
369 fn glyph_id(&self, ch: char) -> Option<u32> {
370 Some(ch as u32)
371 }
372
373 fn advance_width(&self, _: u32) -> f32 {
374 500.0
375 }
376 }
377
378 #[test]
379 fn test_empty_text() {
380 let shaper = HarfrustShaper::new();
381 let font = Arc::new(TestFont { data: vec![] });
382 let params = ShapingParams::default();
383
384 let result = shaper.shape("", font, ¶ms).unwrap();
385 assert_eq!(result.glyphs.len(), 0);
386 assert_eq!(result.advance_width, 0.0);
387 }
388
389 #[test]
390 fn test_simple_text_no_font_data() {
391 let shaper = HarfrustShaper::new();
392 let font = Arc::new(TestFont { data: vec![] });
393 let params = ShapingParams::default();
394
395 let result = shaper.shape("Hi", font, ¶ms).unwrap();
396 assert_eq!(result.glyphs.len(), 2);
397 assert!(result.advance_width > 0.0);
398 }
399
400 #[test]
401 #[cfg(target_os = "macos")]
402 fn test_with_system_font() {
403 use std::fs;
404
405 let font_path = "/System/Library/Fonts/Helvetica.ttc";
407 if let Ok(font_data) = fs::read(font_path) {
408 let font = Arc::new(TestFont { data: font_data });
409 let shaper = HarfrustShaper::new();
410 let params = ShapingParams::default();
411
412 let result = shaper.shape("Hello, World!", font, ¶ms);
413 assert!(result.is_ok());
414
415 let shaped = result.unwrap();
416 assert!(shaped.glyphs.len() > 10);
418 assert!(shaped.advance_width > 0.0);
419
420 for glyph in &shaped.glyphs {
422 assert!(glyph.id > 0);
423 assert!(glyph.advance > 0.0);
424 }
425 }
426 }
427
428 #[test]
429 fn test_complex_text_shaping() {
430 let shaper = HarfrustShaper::new();
431 let font = Arc::new(TestFont { data: vec![] });
432
433 let ltr_params = ShapingParams {
435 direction: Direction::LeftToRight,
436 ..Default::default()
437 };
438
439 let rtl_params = ShapingParams {
440 direction: Direction::RightToLeft,
441 ..Default::default()
442 };
443
444 let ltr_result = shaper.shape("abc", font.clone(), <r_params).unwrap();
446 assert_eq!(ltr_result.direction, Direction::LeftToRight);
447 assert_eq!(ltr_result.glyphs.len(), 3);
448
449 let rtl_result = shaper.shape("abc", font, &rtl_params).unwrap();
451 assert_eq!(rtl_result.direction, Direction::RightToLeft);
452 assert_eq!(rtl_result.glyphs.len(), 3);
453 }
454
455 #[test]
456 fn test_font_size_variations() {
457 let shaper = HarfrustShaper::new();
458 let font = Arc::new(TestFont { data: vec![] });
459
460 let text = "M"; for size in [12.0, 24.0, 48.0] {
464 let params = ShapingParams {
465 size,
466 ..Default::default()
467 };
468
469 let result = shaper.shape(text, font.clone(), ¶ms).unwrap();
470 assert_eq!(result.glyphs.len(), 1);
471 assert_eq!(result.advance_height, size);
472 }
473 }
474
475 #[test]
476 fn test_opentype_features() {
477 let shaper = HarfrustShaper::new();
478 let font = Arc::new(TestFont { data: vec![] });
479
480 let params_liga = ShapingParams {
482 features: vec![("liga".to_string(), 1)],
483 ..Default::default()
484 };
485
486 let result = shaper.shape("fi", font.clone(), ¶ms_liga).unwrap();
487 assert_eq!(result.glyphs.len(), 2); let params_kern = ShapingParams {
491 features: vec![("kern".to_string(), 1)],
492 ..Default::default()
493 };
494
495 let result = shaper.shape("AV", font.clone(), ¶ms_kern).unwrap();
496 assert_eq!(result.glyphs.len(), 2);
497
498 let params_multi = ShapingParams {
500 features: vec![
501 ("liga".to_string(), 1),
502 ("kern".to_string(), 1),
503 ("smcp".to_string(), 1), ],
505 ..Default::default()
506 };
507
508 let result = shaper.shape("Test", font, ¶ms_multi).unwrap();
509 assert_eq!(result.glyphs.len(), 4);
510 }
511
512 #[test]
513 fn test_language_and_script() {
514 let shaper = HarfrustShaper::new();
515 let font = Arc::new(TestFont { data: vec![] });
516
517 let params_lang = ShapingParams {
519 language: Some("en".to_string()),
520 ..Default::default()
521 };
522
523 let result = shaper.shape("Hello", font.clone(), ¶ms_lang).unwrap();
524 assert_eq!(result.glyphs.len(), 5);
525
526 let params_script = ShapingParams {
528 script: Some("latn".to_string()),
529 ..Default::default()
530 };
531
532 let result = shaper.shape("Test", font.clone(), ¶ms_script).unwrap();
533 assert_eq!(result.glyphs.len(), 4);
534
535 let params_both = ShapingParams {
537 language: Some("ar".to_string()),
538 script: Some("arab".to_string()),
539 ..Default::default()
540 };
541
542 let result = shaper.shape("text", font, ¶ms_both).unwrap();
543 assert!(!result.glyphs.is_empty());
544 }
545
546 #[test]
549 fn test_shaper_with_cache() {
550 let _guard = typf_core::cache_config::scoped_caching_enabled(true);
551
552 let shaper = HarfrustShaper::with_cache();
553 let font = Arc::new(TestFont { data: vec![] });
554 let params = ShapingParams::default();
555
556 let result1 = shaper.shape("Hello", font.clone(), ¶ms).unwrap();
558 assert_eq!(result1.glyphs.len(), 5);
559
560 let result2 = shaper.shape("Hello", font.clone(), ¶ms).unwrap();
562 assert_eq!(result2.glyphs.len(), 5);
563
564 assert_eq!(result1.advance_width, result2.advance_width);
566 }
567
568 #[test]
569 fn test_shaper_without_cache() {
570 let shaper = HarfrustShaper::new();
571
572 assert!(shaper.cache_stats().is_none());
574 assert!(shaper.cache_hit_rate().is_none());
575 }
576
577 #[test]
578 fn test_clear_cache() {
579 let _guard = typf_core::cache_config::scoped_caching_enabled(true);
580
581 let shaper = HarfrustShaper::with_cache();
582 let font = Arc::new(TestFont { data: vec![] });
583 let params = ShapingParams::default();
584
585 shaper.shape("ClearTest", font.clone(), ¶ms).unwrap();
587 shaper.shape("ClearTest", font.clone(), ¶ms).unwrap(); shaper.clear_cache();
591
592 let stats_after = shaper.cache_stats().unwrap();
594 assert_eq!(stats_after.hits, 0);
595 assert_eq!(stats_after.misses, 0);
596 }
597}