1use std::str::FromStr;
10use std::sync::Arc;
11
12use harfbuzz_rs::{Direction as HbDirection, Face, Feature, Font as HbFont, Tag, UnicodeBuffer};
13
14use typf_core::{
15 error::Result,
16 traits::{FontRef, Shaper, Stage},
17 types::{Direction, PositionedGlyph, ShapingResult},
18 ShapingParams,
19};
20
21pub use typf_core::shaping_cache::{CacheStats, ShapingCache, ShapingCacheKey, SharedShapingCache};
22
23pub struct HarfBuzzShaper {
28 cache: Option<SharedShapingCache>,
29}
30
31impl HarfBuzzShaper {
32 pub fn new() -> Self {
34 Self { cache: None }
35 }
36
37 pub fn with_cache() -> Self {
39 Self {
40 cache: Some(Arc::new(std::sync::RwLock::new(ShapingCache::new()))),
41 }
42 }
43
44 pub fn with_shared_cache(cache: SharedShapingCache) -> Self {
46 Self { cache: Some(cache) }
47 }
48
49 pub fn cache_stats(&self) -> Option<CacheStats> {
50 self.cache
51 .as_ref()
52 .and_then(|c| c.read().ok())
53 .map(|c| c.stats())
54 }
55
56 pub fn cache_hit_rate(&self) -> Option<f64> {
57 self.cache
58 .as_ref()
59 .and_then(|c| c.read().ok())
60 .map(|c| c.hit_rate())
61 }
62
63 fn to_hb_direction(dir: Direction) -> HbDirection {
65 match dir {
66 Direction::LeftToRight => HbDirection::Ltr,
67 Direction::RightToLeft => HbDirection::Rtl,
68 Direction::TopToBottom => HbDirection::Ttb,
69 Direction::BottomToTop => HbDirection::Btt,
70 }
71 }
72}
73
74impl Default for HarfBuzzShaper {
75 fn default() -> Self {
76 Self::new()
77 }
78}
79
80impl Stage for HarfBuzzShaper {
81 fn name(&self) -> &'static str {
82 "HarfBuzz"
83 }
84
85 fn process(
86 &self,
87 ctx: typf_core::context::PipelineContext,
88 ) -> Result<typf_core::context::PipelineContext> {
89 Ok(ctx)
90 }
91}
92
93impl Shaper for HarfBuzzShaper {
94 fn name(&self) -> &'static str {
95 "HarfBuzz"
96 }
97
98 fn shape(
99 &self,
100 text: &str,
101 font: Arc<dyn FontRef>,
102 params: &ShapingParams,
103 ) -> Result<ShapingResult> {
104 if text.is_empty() {
105 return Ok(ShapingResult {
106 glyphs: Vec::new(),
107 advance_width: 0.0,
108 advance_height: params.size,
109 direction: params.direction,
110 });
111 }
112
113 let font_data = font.data();
114
115 let cache_key = if self.cache.is_some() {
116 let key = ShapingCacheKey::new(
117 text,
118 Shaper::name(self),
119 font_data,
120 params.size,
121 params.language.clone(),
122 params.script.clone(),
123 params.features.clone(),
124 params.variations.clone(),
125 );
126 if let Some(ref cache) = self.cache {
127 if let Ok(cache_guard) = cache.read() {
128 if let Some(result) = cache_guard.get(&key) {
129 return Ok(result);
130 }
131 }
132 }
133 Some(key)
134 } else {
135 None
136 };
137 if font_data.is_empty() {
138 let mut glyphs = Vec::new();
139 let mut x_offset = 0.0;
140
141 for ch in text.chars() {
142 if let Some(glyph_id) = font.glyph_id(ch) {
143 let advance = font.advance_width(glyph_id);
144 glyphs.push(PositionedGlyph {
145 id: glyph_id,
146 x: x_offset,
147 y: 0.0,
148 advance,
149 cluster: 0,
150 });
151 x_offset += advance * params.size / font.units_per_em() as f32;
152 }
153 }
154
155 let result = ShapingResult {
156 glyphs,
157 advance_width: x_offset,
158 advance_height: params.size,
159 direction: params.direction,
160 };
161
162 if let Some(key) = cache_key {
163 if let Some(ref cache) = self.cache {
164 if let Ok(cache_guard) = cache.write() {
165 cache_guard.insert(key, result.clone());
166 }
167 }
168 }
169
170 return Ok(result);
171 }
172
173 let face = Face::from_bytes(font_data, 0);
174 let mut hb_font = HbFont::new(face);
175
176 let scale = (params.size * 64.0) as i32;
177 hb_font.set_scale(scale, scale);
178
179 if !params.variations.is_empty() {
180 let variations: Vec<harfbuzz_rs::Variation> = params
181 .variations
182 .iter()
183 .filter_map(|(tag_str, value)| {
184 if tag_str.len() == 4 {
185 let bytes = tag_str.as_bytes();
186 let tag = Tag::new(
187 bytes[0] as char,
188 bytes[1] as char,
189 bytes[2] as char,
190 bytes[3] as char,
191 );
192 Some(harfbuzz_rs::Variation::new(tag, *value))
193 } else {
194 None
195 }
196 })
197 .collect();
198 hb_font.set_variations(&variations);
199 }
200
201 let mut buffer = UnicodeBuffer::new()
202 .add_str(text)
203 .set_direction(Self::to_hb_direction(params.direction));
204
205 if let Some(ref lang) = params.language {
206 if let Ok(language) = harfbuzz_rs::Language::from_str(lang) {
207 buffer = buffer.set_language(language);
208 }
209 }
210
211 if let Some(ref script_str) = params.script {
212 if script_str.len() == 4 {
213 let bytes = script_str.as_bytes();
214 let tag = Tag::new(
215 bytes[0] as char,
216 bytes[1] as char,
217 bytes[2] as char,
218 bytes[3] as char,
219 );
220 buffer = buffer.set_script(tag);
221 }
222 }
223
224 let hb_features: Vec<Feature> = params
225 .features
226 .iter()
227 .filter_map(|(name, value)| {
228 if name.len() == 4 {
229 let bytes = name.as_bytes();
230 let tag = Tag::new(
231 bytes[0] as char,
232 bytes[1] as char,
233 bytes[2] as char,
234 bytes[3] as char,
235 );
236 Some(Feature::new(tag, *value, 0..text.len()))
237 } else {
238 None
239 }
240 })
241 .collect();
242
243 let output = harfbuzz_rs::shape(&hb_font, buffer, &hb_features);
244
245 let mut glyphs = Vec::new();
246 let mut x_offset = 0.0;
247
248 let positions = output.get_glyph_positions();
249 let infos = output.get_glyph_infos();
250
251 for (info, pos) in infos.iter().zip(positions.iter()) {
252 glyphs.push(PositionedGlyph {
253 id: info.codepoint,
254 x: x_offset + (pos.x_offset as f32 / 64.0),
255 y: pos.y_offset as f32 / 64.0,
256 advance: pos.x_advance as f32 / 64.0,
257 cluster: info.cluster,
258 });
259
260 x_offset += pos.x_advance as f32 / 64.0;
261 }
262
263 let advance_width = x_offset;
264 let advance_height = params.size;
265
266 let result = ShapingResult {
267 glyphs,
268 advance_width,
269 advance_height,
270 direction: params.direction,
271 };
272
273 if let Some(key) = cache_key {
274 if let Some(ref cache) = self.cache {
275 if let Ok(cache_guard) = cache.write() {
276 cache_guard.insert(key, result.clone());
277 }
278 }
279 }
280
281 Ok(result)
282 }
283
284 fn supports_script(&self, _script: &str) -> bool {
285 true
286 }
287
288 fn clear_cache(&self) {
289 if let Some(ref cache) = self.cache {
290 if let Ok(mut cache_guard) = cache.write() {
291 *cache_guard = ShapingCache::new();
292 }
293 }
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 struct TestFont {
302 data: Vec<u8>,
303 }
304
305 impl FontRef for TestFont {
306 fn data(&self) -> &[u8] {
307 &self.data
308 }
309
310 fn units_per_em(&self) -> u16 {
311 1000
312 }
313
314 fn glyph_id(&self, ch: char) -> Option<u32> {
315 Some(ch as u32)
316 }
317
318 fn advance_width(&self, _: u32) -> f32 {
319 500.0
320 }
321 }
322
323 #[test]
324 fn test_empty_text() {
325 let shaper = HarfBuzzShaper::new();
326 let font = Arc::new(TestFont { data: vec![] });
327 let params = ShapingParams::default();
328
329 let result = shaper.shape("", font, ¶ms).unwrap();
330 assert_eq!(result.glyphs.len(), 0);
331 assert_eq!(result.advance_width, 0.0);
332 }
333
334 #[test]
335 fn test_simple_text_no_font_data() {
336 let shaper = HarfBuzzShaper::new();
337 let font = Arc::new(TestFont { data: vec![] });
338 let params = ShapingParams::default();
339
340 let result = shaper.shape("Hi", font, ¶ms).unwrap();
341 assert_eq!(result.glyphs.len(), 2);
342 assert!(result.advance_width > 0.0);
343 }
344
345 #[test]
346 #[cfg(target_os = "macos")]
347 fn test_with_system_font() {
348 use std::fs;
349
350 let font_path = "/System/Library/Fonts/Helvetica.ttc";
352 if let Ok(font_data) = fs::read(font_path) {
353 let font = Arc::new(TestFont { data: font_data });
354 let shaper = HarfBuzzShaper::new();
355 let params = ShapingParams::default();
356
357 let result = shaper.shape("Hello, World!", font, ¶ms);
358 assert!(result.is_ok());
359
360 let shaped = result.unwrap();
361 assert!(shaped.glyphs.len() > 10);
363 assert!(shaped.advance_width > 0.0);
364
365 for glyph in &shaped.glyphs {
367 assert!(glyph.id > 0);
368 assert!(glyph.advance > 0.0);
369 }
370 }
371 }
372
373 #[test]
374 #[cfg(target_os = "linux")]
375 fn test_with_system_font_linux() {
376 use std::fs;
377
378 let font_paths = vec![
380 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
381 "/usr/share/fonts/liberation/LiberationSans-Regular.ttf",
382 ];
383
384 for font_path in font_paths {
385 if let Ok(font_data) = fs::read(font_path) {
386 let font = Arc::new(TestFont { data: font_data });
387 let shaper = HarfBuzzShaper::new();
388 let params = ShapingParams::default();
389
390 let result = shaper.shape("Test", font, ¶ms);
391 assert!(result.is_ok());
392
393 let shaped = result.unwrap();
394 assert_eq!(shaped.glyphs.len(), 4); assert!(shaped.advance_width > 0.0);
396 return; }
398 }
399 }
400
401 #[test]
402 fn test_complex_text_shaping() {
403 let shaper = HarfBuzzShaper::new();
404 let font = Arc::new(TestFont { data: vec![] });
405
406 let ltr_params = ShapingParams {
408 direction: Direction::LeftToRight,
409 ..Default::default()
410 };
411
412 let rtl_params = ShapingParams {
413 direction: Direction::RightToLeft,
414 ..Default::default()
415 };
416
417 let ltr_result = shaper.shape("abc", font.clone(), <r_params).unwrap();
419 assert_eq!(ltr_result.direction, Direction::LeftToRight);
420 assert_eq!(ltr_result.glyphs.len(), 3);
421
422 let rtl_result = shaper.shape("abc", font, &rtl_params).unwrap();
424 assert_eq!(rtl_result.direction, Direction::RightToLeft);
425 assert_eq!(rtl_result.glyphs.len(), 3);
426 }
427
428 #[test]
429 fn test_font_size_variations() {
430 let shaper = HarfBuzzShaper::new();
431 let font = Arc::new(TestFont { data: vec![] });
432
433 let text = "M"; for size in [12.0, 24.0, 48.0] {
437 let params = ShapingParams {
438 size,
439 ..Default::default()
440 };
441
442 let result = shaper.shape(text, font.clone(), ¶ms).unwrap();
443 assert_eq!(result.glyphs.len(), 1);
444 assert_eq!(result.advance_height, size);
445 }
446 }
447
448 #[test]
449 fn test_opentype_features() {
450 let shaper = HarfBuzzShaper::new();
451 let font = Arc::new(TestFont { data: vec![] });
452
453 let params_liga = ShapingParams {
455 features: vec![("liga".to_string(), 1)],
456 ..Default::default()
457 };
458
459 let result = shaper.shape("fi", font.clone(), ¶ms_liga).unwrap();
460 assert_eq!(result.glyphs.len(), 2); let params_kern = ShapingParams {
464 features: vec![("kern".to_string(), 1)],
465 ..Default::default()
466 };
467
468 let result = shaper.shape("AV", font.clone(), ¶ms_kern).unwrap();
469 assert_eq!(result.glyphs.len(), 2);
470
471 let params_multi = ShapingParams {
473 features: vec![
474 ("liga".to_string(), 1),
475 ("kern".to_string(), 1),
476 ("smcp".to_string(), 1), ],
478 ..Default::default()
479 };
480
481 let result = shaper.shape("Test", font, ¶ms_multi).unwrap();
482 assert_eq!(result.glyphs.len(), 4);
483 }
484
485 #[test]
486 fn test_language_and_script() {
487 let shaper = HarfBuzzShaper::new();
488 let font = Arc::new(TestFont { data: vec![] });
489
490 let params_lang = ShapingParams {
492 language: Some("en".to_string()),
493 ..Default::default()
494 };
495
496 let result = shaper.shape("Hello", font.clone(), ¶ms_lang).unwrap();
497 assert_eq!(result.glyphs.len(), 5);
498
499 let params_script = ShapingParams {
501 script: Some("latn".to_string()),
502 ..Default::default()
503 };
504
505 let result = shaper.shape("Test", font.clone(), ¶ms_script).unwrap();
506 assert_eq!(result.glyphs.len(), 4);
507
508 let params_both = ShapingParams {
510 language: Some("ar".to_string()),
511 script: Some("arab".to_string()),
512 ..Default::default()
513 };
514
515 let result = shaper.shape("text", font, ¶ms_both).unwrap();
516 assert!(!result.glyphs.is_empty());
517 }
518
519 #[test]
520 #[cfg(target_os = "macos")]
521 fn test_features_with_real_font() {
522 use std::fs;
523
524 let font_path = "/System/Library/Fonts/Helvetica.ttc";
525 if let Ok(font_data) = fs::read(font_path) {
526 let font = Arc::new(TestFont { data: font_data });
527 let shaper = HarfBuzzShaper::new();
528
529 let params_no_liga = ShapingParams {
531 features: vec![("liga".to_string(), 0)], ..Default::default()
533 };
534
535 let result_no_liga = shaper
536 .shape("fi fl", font.clone(), ¶ms_no_liga)
537 .unwrap();
538
539 let params_liga = ShapingParams {
540 features: vec![("liga".to_string(), 1)], ..Default::default()
542 };
543
544 let result_liga = shaper.shape("fi fl", font, ¶ms_liga).unwrap();
545
546 assert!(!result_no_liga.glyphs.is_empty());
548 assert!(!result_liga.glyphs.is_empty());
549 }
550 }
551
552 #[test]
553 fn test_arabic_shaping() {
554 let shaper = HarfBuzzShaper::new();
555 let font = Arc::new(TestFont { data: vec![] });
556
557 let params = ShapingParams {
559 language: Some("ar".to_string()),
560 script: Some("arab".to_string()),
561 direction: Direction::RightToLeft,
562 ..Default::default()
563 };
564
565 let result = shaper.shape("مرحبا", font, ¶ms).unwrap();
567 assert_eq!(result.direction, Direction::RightToLeft);
568 assert!(!result.glyphs.is_empty());
569 assert!(result.advance_width > 0.0);
571 }
572
573 #[test]
574 fn test_devanagari_shaping() {
575 let shaper = HarfBuzzShaper::new();
576 let font = Arc::new(TestFont { data: vec![] });
577
578 let params = ShapingParams {
580 language: Some("hi".to_string()),
581 script: Some("deva".to_string()),
582 direction: Direction::LeftToRight,
583 ..Default::default()
584 };
585
586 let result = shaper.shape("नमस्ते", font, ¶ms).unwrap();
588 assert_eq!(result.direction, Direction::LeftToRight);
589 assert!(!result.glyphs.is_empty());
590 assert!(result.advance_width > 0.0);
592 }
593
594 #[test]
595 fn test_hebrew_shaping() {
596 let shaper = HarfBuzzShaper::new();
597 let font = Arc::new(TestFont { data: vec![] });
598
599 let params = ShapingParams {
601 language: Some("he".to_string()),
602 script: Some("hebr".to_string()),
603 direction: Direction::RightToLeft,
604 ..Default::default()
605 };
606
607 let result = shaper.shape("שלום", font, ¶ms).unwrap();
609 assert_eq!(result.direction, Direction::RightToLeft);
610 assert_eq!(result.glyphs.len(), 4); assert!(result.advance_width > 0.0);
612 }
613
614 #[test]
615 fn test_thai_shaping() {
616 let shaper = HarfBuzzShaper::new();
617 let font = Arc::new(TestFont { data: vec![] });
618
619 let params = ShapingParams {
621 language: Some("th".to_string()),
622 script: Some("thai".to_string()),
623 ..Default::default()
624 };
625
626 let result = shaper.shape("สวัสดี", font, ¶ms).unwrap();
628 assert_eq!(result.direction, Direction::LeftToRight);
629 assert!(!result.glyphs.is_empty());
630 assert!(result.advance_width > 0.0);
632 }
633
634 #[test]
635 fn test_cjk_shaping() {
636 let shaper = HarfBuzzShaper::new();
637 let font = Arc::new(TestFont { data: vec![] });
638
639 let params = ShapingParams {
641 language: Some("zh".to_string()),
642 script: Some("hani".to_string()),
643 ..Default::default()
644 };
645
646 let result = shaper.shape("你好", font.clone(), ¶ms).unwrap();
648 assert_eq!(result.direction, Direction::LeftToRight);
649 assert_eq!(result.glyphs.len(), 2); assert!(result.advance_width > 0.0);
651
652 let params_ja = ShapingParams {
654 language: Some("ja".to_string()),
655 script: Some("hani".to_string()),
656 ..Default::default()
657 };
658
659 let result = shaper.shape("こんにちは", font, ¶ms_ja).unwrap();
661 assert_eq!(result.glyphs.len(), 5);
662 assert!(result.advance_width > 0.0);
663 }
664
665 #[test]
666 fn test_mixed_script_text() {
667 let shaper = HarfBuzzShaper::new();
668 let font = Arc::new(TestFont { data: vec![] });
669
670 let params = ShapingParams {
672 direction: Direction::LeftToRight, ..Default::default()
674 };
675
676 let result = shaper.shape("Hello مرحبا World", font, ¶ms).unwrap();
677 assert!(!result.glyphs.is_empty());
678 assert!(result.advance_width > 0.0);
680 }
681
682 #[test]
685 fn test_shaper_with_cache() {
686 let _guard = typf_core::cache_config::scoped_caching_enabled(true);
687
688 let shaper = HarfBuzzShaper::with_cache();
689 let font = Arc::new(TestFont { data: vec![] });
690 let params = ShapingParams::default();
691
692 let result1 = shaper.shape("Hello", font.clone(), ¶ms).unwrap();
694 assert_eq!(result1.glyphs.len(), 5);
695
696 let result2 = shaper.shape("Hello", font.clone(), ¶ms).unwrap();
698 assert_eq!(result2.glyphs.len(), 5);
699
700 assert_eq!(result1.advance_width, result2.advance_width);
702
703 let hit_rate = shaper.cache_hit_rate().unwrap();
705 assert!(
706 hit_rate > 0.0,
707 "Cache hit rate should be > 0 after repeat query"
708 );
709 }
710
711 #[test]
712 fn test_shaper_without_cache() {
713 let shaper = HarfBuzzShaper::new();
714
715 assert!(shaper.cache_stats().is_none());
717 assert!(shaper.cache_hit_rate().is_none());
718 }
719
720 #[test]
721 fn test_cache_stats() {
722 let _guard = typf_core::cache_config::scoped_caching_enabled(true);
723
724 let shaper = HarfBuzzShaper::with_cache();
725 let font = Arc::new(TestFont { data: vec![] });
726 let params = ShapingParams::default();
727
728 let stats = shaper.cache_stats().unwrap();
730 assert_eq!(stats.hits, 0);
731 assert_eq!(stats.misses, 0);
732
733 shaper.shape("Test", font.clone(), ¶ms).unwrap();
735
736 shaper.shape("Test", font.clone(), ¶ms).unwrap();
738
739 let stats = shaper.cache_stats().unwrap();
740 assert!(stats.hits >= 1, "Should have at least one hit");
741 }
742
743 #[test]
744 fn test_shared_cache_across_shapers() {
745 let _guard = typf_core::cache_config::scoped_caching_enabled(true);
746
747 use std::sync::RwLock;
748
749 let shared_cache: SharedShapingCache = Arc::new(RwLock::new(ShapingCache::new()));
751
752 let shaper1 = HarfBuzzShaper::with_shared_cache(shared_cache.clone());
754 let shaper2 = HarfBuzzShaper::with_shared_cache(shared_cache.clone());
755
756 let font = Arc::new(TestFont { data: vec![] });
757 let params = ShapingParams::default();
758
759 let result1 = shaper1.shape("Shared", font.clone(), ¶ms).unwrap();
761
762 let result2 = shaper2.shape("Shared", font.clone(), ¶ms).unwrap();
764
765 assert_eq!(result1.glyphs.len(), result2.glyphs.len());
767 assert_eq!(result1.advance_width, result2.advance_width);
768
769 let shared_stats = shared_cache.read().unwrap().stats();
771 assert!(
772 shared_stats.hits >= 1,
773 "Shared cache should have at least one hit"
774 );
775 }
776
777 #[test]
778 fn test_clear_cache() {
779 let _guard = typf_core::cache_config::scoped_caching_enabled(true);
780
781 let shaper = HarfBuzzShaper::with_cache();
782 let font = Arc::new(TestFont { data: vec![] });
783 let params = ShapingParams::default();
784
785 shaper.shape("ClearTest", font.clone(), ¶ms).unwrap();
787 shaper.shape("ClearTest", font.clone(), ¶ms).unwrap(); shaper.clear_cache();
791
792 let stats_after = shaper.cache_stats().unwrap();
794 assert_eq!(stats_after.hits, 0, "Stats should be reset after clear");
795 assert_eq!(stats_after.misses, 0, "Stats should be reset after clear");
796 }
797
798 #[test]
799 fn test_cache_different_params() {
800 let _guard = typf_core::cache_config::scoped_caching_enabled(true);
801
802 let shaper = HarfBuzzShaper::with_cache();
803 let font = Arc::new(TestFont { data: vec![] });
804
805 let params1 = ShapingParams {
806 size: 12.0,
807 ..Default::default()
808 };
809
810 let params2 = ShapingParams {
811 size: 24.0,
812 ..Default::default()
813 };
814
815 let result1 = shaper.shape("Size", font.clone(), ¶ms1).unwrap();
817 let result2 = shaper.shape("Size", font.clone(), ¶ms2).unwrap();
818
819 assert_eq!(result1.advance_height, 12.0);
821 assert_eq!(result2.advance_height, 24.0);
822
823 assert_ne!(
827 result1.advance_height, result2.advance_height,
828 "Different sizes should produce different results"
829 );
830 }
831}