1use std::{path::Path, sync::Arc};
11
12use allsorts::{
13 gpos,
14 gsub::{self, Feature, FeatureInfo, FeatureMask, FeatureMaskExt},
15};
16use azul_core::geom::LogicalSize;
17use azul_css::props::basic::FontRef;
18
19use crate::{
20 font::parsed::ParsedFont,
21 text3::{
22 cache::{
23 BidiDirection, BidiLevel, FontManager, FontSelector, FontVariantCaps,
24 FontVariantLigatures, FontVariantNumeric, Glyph, GlyphOrientation, GlyphSource,
25 LayoutError, LayoutFontMetrics, ParsedFontTrait, Point, ShallowClone, StyleProperties,
26 TextCombineUpright, TextDecoration, TextOrientation, VerticalMetrics, WritingMode,
27 },
28 script::Script,
29 },
30};
31
32#[must_use] pub fn font_ref_from_bytes(
45 font_bytes: &[u8],
46 font_index: usize,
47 parse_outlines: bool,
48) -> Option<FontRef> {
49 let mut warnings = Vec::new();
51 let parsed_font = ParsedFont::from_bytes(font_bytes, font_index, &mut warnings)?;
52
53 Some(crate::parsed_font_to_font_ref(parsed_font))
54}
55
56#[derive(Copy, Debug, Default, Clone)]
61pub struct PathLoader;
62
63impl PathLoader {
64 #[must_use] pub const fn new() -> Self {
66 Self
67 }
68
69 pub(crate) fn load_from_path(self, path: &Path, font_index: usize) -> Result<FontRef, LayoutError> {
74 let font_bytes = std::fs::read(path).map_err(|_| {
75 LayoutError::FontNotFound(FontSelector {
76 family: path.to_string_lossy().into_owned(),
77 weight: rust_fontconfig::FcWeight::Normal,
78 style: crate::text3::cache::FontStyle::Normal,
79 unicode_ranges: Vec::new(),
80 })
81 })?;
82 let arc_owned = Arc::<[u8]>::from(font_bytes);
83 let bytes = Arc::new(rust_fontconfig::FontBytes::Owned(arc_owned));
84 self.load_font_shared(bytes, font_index)
85 }
86
87 pub fn load_font_shared(
104 &self,
105 font_bytes: Arc<rust_fontconfig::FontBytes>,
106 font_index: usize,
107 ) -> Result<FontRef, LayoutError> {
108 let mut warnings = Vec::new();
109 let parsed_font = ParsedFont::from_bytes_shared(font_bytes, font_index, &mut warnings)
110 .ok_or_else(|| {
111 LayoutError::ShapingError("Failed to parse font with allsorts".to_string())
112 })?;
113 Ok(crate::parsed_font_to_font_ref(parsed_font))
114 }
115}
116
117impl FontManager<FontRef> {
118 #[allow(clippy::cast_possible_truncation)] pub fn evict_unused(&self, idle: std::time::Duration) -> usize {
130 use crate::font::parsed::ParsedFont;
131 let Ok(parsed) = self.parsed_fonts.lock() else {
132 return 0;
133 };
134 let cutoff = idle.as_nanos() as u64;
139 let now_nanos = crate::font::parsed::monotonic_now_nanos();
140 let mut evicted = 0usize;
141 for font_ref in parsed.values() {
142 let font: &ParsedFont = crate::font_ref_to_parsed_font(font_ref);
143 let last = font.last_used_nanos();
144 let stale = last == 0 || now_nanos.saturating_sub(last) >= cutoff;
147 if stale && font.evict_loca_glyf() {
148 evicted += 1;
149 }
150 }
151 evicted
152 }
153}
154
155
156impl ShallowClone for FontRef {
160 fn shallow_clone(&self) -> Self {
161 self.clone()
163 }
164}
165
166impl ParsedFontTrait for FontRef {
169 fn shape_text(
171 &self,
172 text: &str,
173 script: Script,
174 language: crate::text3::script::Language,
175 direction: BidiDirection,
176 style: &StyleProperties,
177 ) -> Result<Vec<Glyph>, LayoutError> {
178 let parsed = crate::font_ref_to_parsed_font(self);
180 parsed.shape_text_for_font_ref(self, text, script, language, direction, style)
181 }
182
183 fn get_hash(&self) -> u64 {
184 crate::font_ref_to_parsed_font(self).hash
185 }
186
187 fn get_glyph_size(&self, glyph_id: u16, font_size: f32) -> Option<LogicalSize> {
188 crate::font_ref_to_parsed_font(self).get_glyph_size(glyph_id, font_size)
189 }
190
191 fn get_hyphen_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
192 crate::font_ref_to_parsed_font(self).get_hyphen_glyph_and_advance(font_size)
193 }
194
195 fn get_kashida_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
196 crate::font_ref_to_parsed_font(self).get_kashida_glyph_and_advance(font_size)
197 }
198
199 fn has_glyph(&self, codepoint: u32) -> bool {
200 crate::font_ref_to_parsed_font(self).has_glyph(codepoint)
201 }
202
203 fn get_vertical_metrics(&self, glyph_id: u16) -> Option<VerticalMetrics> {
204 crate::font_ref_to_parsed_font(self).get_vertical_metrics(glyph_id)
205 }
206
207 fn get_font_metrics(&self) -> LayoutFontMetrics {
208 crate::font_ref_to_parsed_font(self).font_metrics
209 }
210
211 fn num_glyphs(&self) -> u16 {
212 crate::font_ref_to_parsed_font(self).num_glyphs
213 }
214
215 fn get_space_width(&self) -> Option<usize> {
216 crate::font_ref_to_parsed_font(self).get_space_width()
217 }
218}
219
220pub trait FontRefExt {
224 fn get_bytes(&self) -> &[u8];
230 fn get_full_font_metrics(&self) -> azul_css::props::basic::FontMetrics;
232}
233
234impl FontRefExt for FontRef {
235 fn get_bytes(&self) -> &[u8] {
236 crate::font_ref_to_parsed_font(self)
237 .original_bytes
238 .as_ref()
239 .map_or(&[], |b| b.as_slice())
240 }
241
242 fn get_full_font_metrics(&self) -> azul_css::props::basic::FontMetrics {
243 use azul_css::{OptionI16, OptionU16, OptionU32};
244
245 let parsed = crate::font_ref_to_parsed_font(self);
246 let pdf = &parsed.pdf_font_metrics;
247
248 azul_css::props::basic::FontMetrics {
250 ul_code_page_range1: OptionU32::None,
252 ul_code_page_range2: OptionU32::None,
253
254 ul_unicode_range1: 0, ul_unicode_range2: 0, ul_unicode_range3: 0, ul_unicode_range4: 0, ach_vend_id: 0, s_typo_ascender: OptionI16::None,
263 s_typo_descender: OptionI16::None,
264 s_typo_line_gap: OptionI16::None,
265 us_win_ascent: OptionU16::None,
266 us_win_descent: OptionU16::None,
267
268 sx_height: OptionI16::None,
270 s_cap_height: OptionI16::None,
271 us_default_char: OptionU16::None,
272 us_break_char: OptionU16::None,
273 us_max_context: OptionU16::None,
274
275 us_lower_optical_point_size: OptionU16::None,
277 us_upper_optical_point_size: OptionU16::None,
278
279 units_per_em: pdf.units_per_em,
281 font_flags: pdf.font_flags,
282 x_min: pdf.x_min,
283 y_min: pdf.y_min,
284 x_max: pdf.x_max,
285 y_max: pdf.y_max,
286
287 ascender: pdf.ascender,
289 descender: pdf.descender,
290 line_gap: pdf.line_gap,
291 advance_width_max: pdf.advance_width_max,
292 min_left_side_bearing: 0, min_right_side_bearing: 0, x_max_extent: 0, caret_slope_rise: pdf.caret_slope_rise,
296 caret_slope_run: pdf.caret_slope_run,
297 caret_offset: 0, num_h_metrics: 0, x_avg_char_width: pdf.x_avg_char_width,
302 us_weight_class: pdf.us_weight_class,
303 us_width_class: pdf.us_width_class,
304 fs_type: 0, y_subscript_x_size: 0, y_subscript_y_size: 0, y_subscript_x_offset: 0, y_subscript_y_offset: 0, y_superscript_x_size: 0, y_superscript_y_size: 0, y_superscript_x_offset: 0, y_superscript_y_offset: 0, y_strikeout_size: pdf.y_strikeout_size,
314 y_strikeout_position: pdf.y_strikeout_position,
315 s_family_class: 0, fs_selection: 0, us_first_char_index: 0, us_last_char_index: 0, panose: azul_css::props::basic::Panose::zero(),
322 }
323 }
324}
325
326impl ParsedFont {
334 fn shape_text_for_font_ref(
337 &self,
338 _font_ref: &FontRef,
339 text: &str,
340 script: Script,
341 language: crate::text3::script::Language,
342 direction: BidiDirection,
343 style: &StyleProperties,
344 ) -> Result<Vec<Glyph>, LayoutError> {
345 shape_text_internal(self, text, script, language, direction, style)
349 }
350
351 const fn get_hash(&self) -> u64 {
352 self.hash
353 }
354
355 fn get_glyph_size(&self, glyph_id: u16, font_size_px: f32) -> Option<LogicalSize> {
356 self.get_or_decode_glyph(glyph_id).map(|record| {
357 let units_per_em = f32::from(self.font_metrics.units_per_em);
358 let scale_factor = if units_per_em > 0.0 {
359 font_size_px / units_per_em
360 } else {
361 FALLBACK_SCALE
362 };
363
364 let bbox = &record.bounding_box;
366
367 LogicalSize {
368 width: f32::from(bbox.max_x - bbox.min_x) * scale_factor,
369 height: f32::from(bbox.max_y - bbox.min_y) * scale_factor,
370 }
371 })
372 }
373
374 fn get_hyphen_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
375 let glyph_id = self.lookup_glyph_index('-' as u32)?;
376 let advance_units = self.get_horizontal_advance(glyph_id);
377 let scale_factor = if self.font_metrics.units_per_em > 0 {
378 font_size / f32::from(self.font_metrics.units_per_em)
379 } else {
380 return None;
381 };
382 let scaled_advance = f32::from(advance_units) * scale_factor;
383 Some((glyph_id, scaled_advance))
384 }
385
386 fn get_kashida_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
387 let glyph_id = self.lookup_glyph_index('\u{0640}' as u32)?;
389 let advance_units = self.get_horizontal_advance(glyph_id);
390 let scale_factor = if self.font_metrics.units_per_em > 0 {
391 font_size / f32::from(self.font_metrics.units_per_em)
392 } else {
393 return None;
394 };
395 let scaled_advance = f32::from(advance_units) * scale_factor;
396 Some((glyph_id, scaled_advance))
397 }
398}
399
400const FALLBACK_SCALE: f32 = 0.01;
402
403#[allow(clippy::match_same_arms)] fn build_feature_mask_for_script(script: Script) -> FeatureMask {
416 use Script::{Arabic, Devanagari, Bengali, Gujarati, Gurmukhi, Kannada, Malayalam, Oriya, Tamil, Telugu, Myanmar, Khmer, Thai, Hebrew, Hangul, Ethiopic, Latin, Greek, Cyrillic, Georgian, Hiragana, Katakana, Mandarin, Sinhala};
417
418 let mut mask = FeatureMask::default_mask(); match script {
423 Arabic => {
425 mask |= Feature::INIT; mask |= Feature::MEDI; mask |= Feature::FINA; mask |= Feature::ISOL; }
432
433 Devanagari | Bengali | Gujarati | Gurmukhi | Kannada | Malayalam | Oriya | Tamil
435 | Telugu => {
436 mask |= Feature::NUKT; mask |= Feature::AKHN; mask |= Feature::RPHF; mask |= Feature::RKRF; mask |= Feature::PREF; mask |= Feature::BLWF; mask |= Feature::ABVF; mask |= Feature::HALF; mask |= Feature::PSTF; mask |= Feature::VATU; mask |= Feature::CJCT; }
448
449 Myanmar => {
451 mask |= Feature::PREF; mask |= Feature::BLWF; mask |= Feature::PSTF; }
455
456 Khmer => {
458 mask |= Feature::PREF; mask |= Feature::BLWF; mask |= Feature::ABVF; mask |= Feature::PSTF; }
463
464 Thai => {
466 }
469
470 Hebrew => {
472 }
475
476 Hangul => {
478 }
482
483 Ethiopic => {
485 }
488
489 Latin | Greek | Cyrillic => {
491 }
497
498 Georgian => {
500 }
502
503 Hiragana | Katakana | Mandarin => {
505 }
509
510 Sinhala => {
512 mask |= Feature::AKHN; mask |= Feature::RPHF; mask |= Feature::VATU; }
516 }
517
518 mask
519}
520
521#[allow(clippy::match_same_arms)] const fn to_opentype_script_tag(script: Script) -> u32 {
524 use Script::{Arabic, Bengali, Cyrillic, Devanagari, Ethiopic, Georgian, Greek, Gujarati, Gurmukhi, Hangul, Hebrew, Hiragana, Kannada, Katakana, Khmer, Latin, Malayalam, Mandarin, Myanmar, Oriya, Sinhala, Tamil, Telugu, Thai};
525 match script {
527 Arabic => u32::from_be_bytes(*b"arab"),
528 Bengali => u32::from_be_bytes(*b"beng"),
529 Cyrillic => u32::from_be_bytes(*b"cyrl"),
530 Devanagari => u32::from_be_bytes(*b"deva"),
531 Ethiopic => u32::from_be_bytes(*b"ethi"),
532 Georgian => u32::from_be_bytes(*b"geor"),
533 Greek => u32::from_be_bytes(*b"grek"),
534 Gujarati => u32::from_be_bytes(*b"gujr"),
535 Gurmukhi => u32::from_be_bytes(*b"guru"),
536 Hangul => u32::from_be_bytes(*b"hang"),
537 Hebrew => u32::from_be_bytes(*b"hebr"),
538 Hiragana => u32::from_be_bytes(*b"kana"),
541 Kannada => u32::from_be_bytes(*b"knda"),
542 Katakana => u32::from_be_bytes(*b"kana"),
543 Khmer => u32::from_be_bytes(*b"khmr"),
544 Latin => u32::from_be_bytes(*b"latn"),
545 Malayalam => u32::from_be_bytes(*b"mlym"),
546 Mandarin => u32::from_be_bytes(*b"hani"),
547 Myanmar => u32::from_be_bytes(*b"mymr"),
548 Oriya => u32::from_be_bytes(*b"orya"),
549 Sinhala => u32::from_be_bytes(*b"sinh"),
550 Tamil => u32::from_be_bytes(*b"taml"),
551 Telugu => u32::from_be_bytes(*b"telu"),
552 Thai => u32::from_be_bytes(*b"thai"),
553 }
554}
555
556fn parse_font_feature(feature_str: &str) -> Option<(u32, u32)> {
559 let mut parts = feature_str.split('=');
560 let tag_str = parts.next()?.trim();
561 let value_str = parts.next().unwrap_or("1").trim(); if tag_str.len() > 4 {
565 return None;
566 }
567 let padded_tag_str = format!("{tag_str:<4}");
569
570 let tag = u32::from_be_bytes(padded_tag_str.as_bytes().try_into().ok()?);
571 let value = value_str.parse::<u32>().ok()?;
572
573 Some((tag, value))
574}
575
576fn add_variant_features(style: &StyleProperties, features: &mut Vec<FeatureInfo>) {
578 let mut add_on = |tag_str: &[u8; 4]| {
580 features.push(FeatureInfo {
581 feature_tag: u32::from_be_bytes(*tag_str),
582 alternate: None,
583 });
584 };
585
586 match style.font_variant_ligatures {
594 FontVariantLigatures::Discretionary => add_on(b"dlig"),
595 FontVariantLigatures::Historical => add_on(b"hlig"),
596 FontVariantLigatures::Contextual => add_on(b"calt"),
597 _ => {} }
599
600 match style.font_variant_caps {
602 FontVariantCaps::SmallCaps => add_on(b"smcp"),
603 FontVariantCaps::AllSmallCaps => {
604 add_on(b"c2sc");
605 add_on(b"smcp");
606 }
607 FontVariantCaps::PetiteCaps => add_on(b"pcap"),
608 FontVariantCaps::AllPetiteCaps => {
609 add_on(b"c2pc");
610 add_on(b"pcap");
611 }
612 FontVariantCaps::Unicase => add_on(b"unic"),
613 FontVariantCaps::TitlingCaps => add_on(b"titl"),
614 FontVariantCaps::Normal => {}
615 }
616
617 match style.font_variant_numeric {
619 FontVariantNumeric::LiningNums => add_on(b"lnum"),
620 FontVariantNumeric::OldstyleNums => add_on(b"onum"),
621 FontVariantNumeric::ProportionalNums => add_on(b"pnum"),
622 FontVariantNumeric::TabularNums => add_on(b"tnum"),
623 FontVariantNumeric::DiagonalFractions => add_on(b"frac"),
624 FontVariantNumeric::StackedFractions => add_on(b"afrc"),
625 FontVariantNumeric::Ordinal => add_on(b"ordn"),
626 FontVariantNumeric::SlashedZero => add_on(b"zero"),
627 FontVariantNumeric::Normal => {}
628 }
629}
630
631#[cfg(feature = "text_layout_hyphenation")]
633#[allow(clippy::match_same_arms)] const fn to_opentype_lang_tag(lang: hyphenation::Language) -> u32 {
635 use hyphenation::Language::{Afrikaans, Albanian, Armenian, Assamese, Basque, Belarusian, Bengali, Bulgarian, Catalan, Chinese, Coptic, Croatian, Czech, Danish, Dutch, EnglishGB, EnglishUS, Esperanto, Estonian, Ethiopic, Finnish, FinnishScholastic, French, Friulan, Galician, Georgian, German1901, German1996, GermanSwiss, GreekAncient, GreekMono, GreekPoly, Gujarati, Hindi, Hungarian, Icelandic, Indonesian, Interlingua, Irish, Italian, Kannada, Kurmanji, Latin, LatinClassic, LatinLiturgical, Latvian, Lithuanian, Macedonian, Malayalam, Marathi, Mongolian, NorwegianBokmal, NorwegianNynorsk, Occitan, Oriya, Pali, Panjabi, Piedmontese, Polish, Portuguese, Romanian, Romansh, Russian, Sanskrit, SerbianCyrillic, SerbocroatianCyrillic, SerbocroatianLatin, SlavonicChurch, Slovak, Slovenian, Spanish, Swedish, Tamil, Telugu, Thai, Turkish, Turkmen, Ukrainian, Uppersorbian, Welsh};
636 let tag_bytes = match lang {
639 Afrikaans => *b"AFK ",
640 Albanian => *b"SQI ",
641 Armenian => *b"HYE ",
642 Assamese => *b"ASM ",
643 Basque => *b"EUQ ",
644 Belarusian => *b"BEL ",
645 Bengali => *b"BEN ",
646 Bulgarian => *b"BGR ",
647 Catalan => *b"CAT ",
648 Chinese => *b"ZHS ",
649 Coptic => *b"COP ",
650 Croatian => *b"HRV ",
651 Czech => *b"CSY ",
652 Danish => *b"DAN ",
653 Dutch => *b"NLD ",
654 EnglishGB => *b"ENG ",
655 EnglishUS => *b"ENU ",
656 Esperanto => *b"ESP ",
657 Estonian => *b"ETI ",
658 Ethiopic => *b"ETH ",
659 Finnish => *b"FIN ",
660 FinnishScholastic => *b"FIN ",
661 French => *b"FRA ",
662 Friulan => *b"FRL ",
663 Galician => *b"GLC ",
664 Georgian => *b"KAT ",
665 German1901 => *b"DEU ",
666 German1996 => *b"DEU ",
667 GermanSwiss => *b"DES ",
668 GreekAncient => *b"GRC ",
669 GreekMono => *b"ELL ",
670 GreekPoly => *b"ELL ",
671 Gujarati => *b"GUJ ",
672 Hindi => *b"HIN ",
673 Hungarian => *b"HUN ",
674 Icelandic => *b"ISL ",
675 Indonesian => *b"IND ",
676 Interlingua => *b"INA ",
677 Irish => *b"IRI ",
678 Italian => *b"ITA ",
679 Kannada => *b"KAN ",
680 Kurmanji => *b"KUR ",
681 Latin => *b"LAT ",
682 LatinClassic => *b"LAT ",
683 LatinLiturgical => *b"LAT ",
684 Latvian => *b"LVI ",
685 Lithuanian => *b"LTH ",
686 Macedonian => *b"MKD ",
687 Malayalam => *b"MAL ",
688 Marathi => *b"MAR ",
689 Mongolian => *b"MNG ",
690 NorwegianBokmal => *b"NOR ",
691 NorwegianNynorsk => *b"NYN ",
692 Occitan => *b"OCI ",
693 Oriya => *b"ORI ",
694 Pali => *b"PLI ",
695 Panjabi => *b"PAN ",
696 Piedmontese => *b"PMS ",
697 Polish => *b"PLK ",
698 Portuguese => *b"PTG ",
699 Romanian => *b"ROM ",
700 Romansh => *b"RMC ",
701 Russian => *b"RUS ",
702 Sanskrit => *b"SAN ",
703 SerbianCyrillic => *b"SRB ",
704 SerbocroatianCyrillic => *b"SHC ",
705 SerbocroatianLatin => *b"SHL ",
706 SlavonicChurch => *b"CSL ",
707 Slovak => *b"SKY ",
708 Slovenian => *b"SLV ",
709 Spanish => *b"ESP ",
710 Swedish => *b"SVE ",
711 Tamil => *b"TAM ",
712 Telugu => *b"TEL ",
713 Thai => *b"THA ",
714 Turkish => *b"TRK ",
715 Turkmen => *b"TUK ",
716 Ukrainian => *b"UKR ",
717 Uppersorbian => *b"HSB ",
718 Welsh => *b"CYM ",
719 };
720 u32::from_be_bytes(tag_bytes)
721}
722
723#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss, clippy::cast_sign_loss)] #[allow(clippy::similar_names)] #[allow(clippy::too_many_lines)] fn shape_text_internal(
729 parsed_font: &ParsedFont,
730 text: &str,
731 script: Script,
732 language: crate::text3::script::Language,
733 direction: BidiDirection,
734 style: &StyleProperties,
735) -> Result<Vec<Glyph>, LayoutError> {
736 let script_tag = to_opentype_script_tag(script);
737 #[cfg(feature = "text_layout_hyphenation")]
738 let lang_tag = to_opentype_lang_tag(language);
739 #[cfg(not(feature = "text_layout_hyphenation"))]
740 let lang_tag = 0u32;
741
742 let mut user_features: Vec<FeatureInfo> = style
745 .font_features
746 .iter()
747 .filter_map(|s| parse_font_feature(s))
748 .map(|(tag, value)| FeatureInfo {
749 feature_tag: tag,
750 alternate: if value > 1 {
751 Some(value as usize)
752 } else {
753 None
754 },
755 })
756 .collect();
757 add_variant_features(style, &mut user_features);
758
759 let opt_gdef = parsed_font.opt_gdef_table.as_deref();
760
761 let mut raw_glyphs: Vec<gsub::RawGlyph<()>> = Vec::new();
762 {
763 let mut ci = 0usize;
764 while ci < text.len() {
765 let Some(ch) = text[ci..].chars().next() else {
766 break;
767 };
768 let glyph_index = parsed_font.lookup_glyph_index(ch as u32).unwrap_or(0);
769 raw_glyphs.push(gsub::RawGlyph {
780 unicodes: tinyvec::tiny_vec![[char; 1] => ch],
781 glyph_index,
782 liga_component_pos: 0,
783 glyph_origin: gsub::GlyphOrigin::Char(ch),
784 flags: gsub::RawGlyphFlags::empty(),
785 extra_data: (),
786 variation: None,
787 });
788 ci += ch.len_utf8();
789 }
790 }
791
792 if let Some(gsub) = parsed_font.gsub() {
793 let feature_mask = build_feature_mask_for_script(script);
801 let custom_features: &[FeatureInfo] = user_features.as_slice();
802
803 let dotted_circle_index = parsed_font
804 .lookup_glyph_index(allsorts::DOTTED_CIRCLE as u32)
805 .unwrap_or(0);
806 gsub::apply(
807 dotted_circle_index,
808 gsub,
809 opt_gdef,
810 script_tag,
811 Some(lang_tag),
812 feature_mask,
813 custom_features,
814 None,
815 parsed_font.num_glyphs(),
816 &mut raw_glyphs,
817 )
818 .map_err(|e| LayoutError::ShapingError(e.to_string()))?;
819 }
820
821 let mut infos = gpos::Info::init_from_glyphs(opt_gdef, raw_glyphs);
822
823 if let Some(gpos) = parsed_font.gpos() {
824 let kern_table = parsed_font
825 .opt_kern_table
826 .as_ref()
827 .map(|kt| kt.as_borrowed());
828 let apply_kerning = true; gpos::apply(
830 gpos,
831 opt_gdef,
832 kern_table,
833 apply_kerning,
834 FeatureMask::empty(),
835 &user_features,
836 None,
837 script_tag,
838 Some(lang_tag),
839 &mut infos,
840 )
841 .map_err(|e| LayoutError::ShapingError(e.to_string()))?;
842 } else {
843 let kern_table = parsed_font
848 .opt_kern_table
849 .as_ref()
850 .map(|kt| kt.as_borrowed());
851 gpos::apply_fallback(kern_table, script_tag, &mut infos)
852 .map_err(|e| LayoutError::ShapingError(e.to_string()))?;
853 }
854
855 let font_size = style.font_size_px;
856 let scale_factor = if parsed_font.font_metrics.units_per_em > 0 {
857 font_size / f32::from(parsed_font.font_metrics.units_per_em)
858 } else {
859 FALLBACK_SCALE
860 };
861
862 let font_hash = parsed_font.get_hash();
863 let font_metrics = LayoutFontMetrics {
864 ascent: parsed_font.font_metrics.ascent,
865 descent: parsed_font.font_metrics.descent,
866 line_gap: parsed_font.font_metrics.line_gap,
867 units_per_em: parsed_font.font_metrics.units_per_em,
868 x_height: parsed_font.font_metrics.x_height,
869 cap_height: parsed_font.font_metrics.cap_height,
870 };
871 let style_arc = Arc::new(style.clone());
872 let bidi_level = BidiLevel::new(u8::from(direction.is_rtl()));
873
874 let mut shaped_glyphs = Vec::new();
875 let mut byte_cursor = 0usize;
883 for info in &infos {
884 let uni_len: usize = info.glyph.unicodes.iter().map(|c| c.len_utf8()).sum();
885 let (byte_index, byte_len) = if info.glyph.multi_subst_dup() || uni_len == 0 {
886 (byte_cursor.min(text.len()), 0)
887 } else {
888 let start = byte_cursor.min(text.len());
889 byte_cursor = (byte_cursor + uni_len).min(text.len());
890 (start, uni_len)
891 };
892 let cluster = byte_index as u32;
893 let source_char = info
894 .glyph
895 .unicodes
896 .first()
897 .copied()
898 .or_else(|| text.get(byte_index..).and_then(|s| s.chars().next()))
899 .unwrap_or('\u{FFFD}');
900
901 let base_advance = parsed_font.get_horizontal_advance(info.glyph.glyph_index);
902 let ppem = font_size.round().max(1.0) as u16;
908 let advance = parsed_font
909 .get_hinted_advance_px(info.glyph.glyph_index, ppem)
910 .map_or_else(
911 || f32::from(base_advance) * scale_factor,
912 |hinted| hinted * font_size / f32::from(ppem),
913 );
914 let kerning = f32::from(info.kerning) * scale_factor;
915
916 let (offset_x_units, offset_y_units) =
917 if let gpos::Placement::Distance(x, y) = info.placement {
918 (x, y)
919 } else {
920 (0, 0)
921 };
922 let offset_x = offset_x_units as f32 * scale_factor;
923 let offset_y = offset_y_units as f32 * scale_factor;
924
925 let vert = parsed_font.get_vertical_metrics(info.glyph.glyph_index);
926 let glyph = Glyph {
927 glyph_id: info.glyph.glyph_index,
928 codepoint: source_char,
929 font_hash,
930 font_metrics,
931 style: Arc::clone(&style_arc),
932 source: GlyphSource::Char,
933 logical_byte_index: byte_index,
934 logical_byte_len: byte_len,
935 content_index: 0,
936 cluster,
937 advance,
938 kerning,
939 offset: Point {
940 x: offset_x,
941 y: offset_y,
942 },
943 vertical_advance: vert.as_ref().map_or(0.0, |v| v.advance * font_size),
944 vertical_origin_y: vert.as_ref().map_or(0.0, |v| v.origin_y * font_size),
945 vertical_bearing: vert
946 .map_or(Point { x: 0.0, y: 0.0 }, |v| Point { x: v.bearing_x * font_size, y: v.bearing_y * font_size }),
947 orientation: GlyphOrientation::Horizontal,
948 script,
949 bidi_level,
950 };
951 shaped_glyphs.push(glyph);
952 }
953
954 Ok(shaped_glyphs)
955}
956
957pub fn shape_text_for_parsed_font(
963 parsed_font: &ParsedFont,
964 text: &str,
965 script: Script,
966 language: crate::text3::script::Language,
967 direction: BidiDirection,
968 style: &StyleProperties,
969) -> Result<Vec<Glyph>, LayoutError> {
970 shape_text_internal(parsed_font, text, script, language, direction, style)
972}
973
974#[cfg(test)]
975#[allow(clippy::float_cmp, clippy::cast_lossless, clippy::unreadable_literal)]
976mod autotest_generated {
977 use std::time::Duration;
978
979 use rust_fontconfig::{FcFontCache, FontBytes, FontId};
980
981 use super::*;
982 use crate::text3::script::Language;
983
984 const MOCK_MONO: &[u8] = crate::text3::mock_fonts::MOCK_MONO_TTF;
986
987 const ALL_SCRIPTS: [Script; 24] = [
990 Script::Arabic,
991 Script::Bengali,
992 Script::Cyrillic,
993 Script::Devanagari,
994 Script::Ethiopic,
995 Script::Georgian,
996 Script::Greek,
997 Script::Gujarati,
998 Script::Gurmukhi,
999 Script::Hangul,
1000 Script::Hebrew,
1001 Script::Hiragana,
1002 Script::Kannada,
1003 Script::Katakana,
1004 Script::Khmer,
1005 Script::Latin,
1006 Script::Malayalam,
1007 Script::Mandarin,
1008 Script::Myanmar,
1009 Script::Oriya,
1010 Script::Sinhala,
1011 Script::Tamil,
1012 Script::Telugu,
1013 Script::Thai,
1014 ];
1015
1016 fn mock() -> ParsedFont {
1018 let mut warnings = Vec::new();
1019 ParsedFont::from_bytes(MOCK_MONO, 0, &mut warnings).expect("Azul Mock Mono must parse")
1020 }
1021
1022 fn mock_deferred() -> ParsedFont {
1024 let bytes = Arc::new(FontBytes::Owned(Arc::from(MOCK_MONO.to_vec())));
1025 let mut warnings = Vec::new();
1026 ParsedFont::from_bytes_shared(bytes, 0, &mut warnings)
1027 .expect("from_bytes_shared must parse the positive control")
1028 }
1029
1030 fn style_at(font_size_px: f32) -> StyleProperties {
1031 StyleProperties {
1032 font_size_px,
1033 ..StyleProperties::default()
1034 }
1035 }
1036
1037 fn shape(font: &ParsedFont, text: &str) -> Result<Vec<Glyph>, LayoutError> {
1038 shape_text_internal(
1039 font,
1040 text,
1041 Script::Latin,
1042 Language::EnglishUS,
1043 BidiDirection::Ltr,
1044 &style_at(16.0),
1045 )
1046 }
1047
1048 fn assert_spans_are_sane(glyphs: &[Glyph], text: &str) {
1052 let mut prev_index = 0usize;
1053 for g in glyphs {
1054 assert!(
1055 g.logical_byte_index <= text.len(),
1056 "byte index {} escapes the {}-byte source",
1057 g.logical_byte_index,
1058 text.len()
1059 );
1060 let end = g.logical_byte_index + g.logical_byte_len;
1061 assert!(
1062 end <= text.len(),
1063 "span {}..{end} escapes the {}-byte source",
1064 g.logical_byte_index,
1065 text.len()
1066 );
1067 assert!(
1068 text.is_char_boundary(g.logical_byte_index) && text.is_char_boundary(end),
1069 "span {}..{end} splits a UTF-8 sequence",
1070 g.logical_byte_index
1071 );
1072 assert!(
1073 g.logical_byte_index >= prev_index,
1074 "byte cursor ran backwards: {} after {prev_index}",
1075 g.logical_byte_index
1076 );
1077 prev_index = g.logical_byte_index;
1078 assert_eq!(
1079 u64::from(g.cluster),
1080 g.logical_byte_index as u64,
1081 "cluster must mirror the logical byte index"
1082 );
1083 }
1084 }
1085
1086 #[test]
1091 fn font_ref_from_bytes_rejects_empty_and_malformed_input() {
1092 assert!(font_ref_from_bytes(b"", 0, false).is_none());
1094 assert!(font_ref_from_bytes(b" \t\n", 0, false).is_none());
1095 assert!(font_ref_from_bytes(&[0xFF, 0xFE, 0x00], 0, false).is_none());
1096 assert!(font_ref_from_bytes(b"not a font at all, just prose", 0, false).is_none());
1097 assert!(font_ref_from_bytes(&[0x00, 0x01, 0x00, 0x00], 0, false).is_none());
1099 assert!(font_ref_from_bytes(&MOCK_MONO[..12], 0, false).is_none());
1101 assert!(font_ref_from_bytes(&MOCK_MONO[..64], 0, false).is_none());
1102 let mut prefixed = vec![0xABu8; 32];
1104 prefixed.extend_from_slice(MOCK_MONO);
1105 assert!(font_ref_from_bytes(&prefixed, 0, false).is_none());
1106 }
1107
1108 #[test]
1109 fn font_ref_from_bytes_extremely_long_garbage_terminates() {
1110 assert!(font_ref_from_bytes(&vec![0u8; 1_000_000], 0, false).is_none());
1112 let mut ttc_junk = b"ttcf".to_vec();
1114 ttc_junk.extend_from_slice(&vec![0xCDu8; 1_000_000]);
1115 assert!(font_ref_from_bytes(&ttc_junk, 0, false).is_none());
1116 }
1117
1118 #[test]
1119 fn font_ref_from_bytes_valid_minimal_positive_control() {
1120 let font_ref =
1121 font_ref_from_bytes(MOCK_MONO, 0, false).expect("Azul Mock Mono must parse into a FontRef");
1122 assert!(font_ref.num_glyphs() > 0, "a real font has glyphs");
1123 assert_eq!(font_ref.get_hash(), mock().get_hash());
1124 assert!(font_ref.has_glyph('a' as u32));
1125 }
1126
1127 #[test]
1128 fn font_ref_from_bytes_ignores_the_parse_outlines_flag() {
1129 let with = font_ref_from_bytes(MOCK_MONO, 0, true).expect("parse with outlines");
1133 let without = font_ref_from_bytes(MOCK_MONO, 0, false).expect("parse without outlines");
1134 assert_eq!(with.get_hash(), without.get_hash());
1135 assert_eq!(with.num_glyphs(), without.num_glyphs());
1136 assert_eq!(with.get_space_width(), without.get_space_width());
1137 }
1138
1139 #[test]
1140 fn font_ref_from_bytes_extreme_font_index_does_not_panic() {
1141 for index in [0usize, 1, 255, usize::MAX / 2, usize::MAX] {
1144 let _ = font_ref_from_bytes(MOCK_MONO, index, false);
1145 let _ = font_ref_from_bytes(b"", index, false);
1146 }
1147 }
1148
1149 #[test]
1154 fn path_loader_new_is_a_zero_sized_stateless_handle() {
1155 assert_eq!(core::mem::size_of::<PathLoader>(), 0);
1156 let a = PathLoader::new();
1157 let b = PathLoader;
1158 assert!(a.load_from_path(Path::new("/nonexistent/azul/x.ttf"), 0).is_err());
1160 assert!(b.load_from_path(Path::new("/nonexistent/azul/x.ttf"), 0).is_err());
1161 }
1162
1163 #[test]
1164 fn load_from_path_missing_empty_and_directory_paths_are_font_not_found() {
1165 let loader = PathLoader::new();
1166 for path in [
1167 "",
1168 "/nonexistent/definitely/not/a/font.ttf",
1169 "/dev/null",
1170 env!("CARGO_MANIFEST_DIR"), ] {
1172 match loader.load_from_path(Path::new(path), 0) {
1173 Err(LayoutError::FontNotFound(selector)) => {
1174 assert_eq!(selector.family, path, "the failing path is reported back");
1175 assert!(selector.unicode_ranges.is_empty());
1176 }
1177 Err(LayoutError::ShapingError(_)) => {}
1179 other => panic!("{path:?} must not load a font: {other:?}"),
1180 }
1181 }
1182 }
1183
1184 #[test]
1185 fn load_from_path_parses_a_real_font_and_survives_extreme_indices() {
1186 let loader = PathLoader::new();
1187 let path = concat!(
1188 env!("CARGO_MANIFEST_DIR"),
1189 "/assets/fonts/test/azul-mock-mono.ttf"
1190 );
1191 let font_ref = loader
1192 .load_from_path(Path::new(path), 0)
1193 .expect("the positive control must load from disk");
1194 assert_eq!(font_ref.num_glyphs(), mock().num_glyphs());
1195
1196 for index in [0usize, 1, usize::MAX] {
1198 let _ = loader.load_from_path(Path::new(path), index);
1199 }
1200 }
1201
1202 #[test]
1203 fn load_font_shared_rejects_empty_and_garbage_byte_blobs() {
1204 let loader = PathLoader::new();
1205 let cases: Vec<Vec<u8>> = vec![
1206 Vec::new(),
1207 b" \t\n".to_vec(),
1208 vec![0xFF, 0xFE, 0x00],
1209 MOCK_MONO[..32].to_vec(),
1210 vec![0u8; 1_000_000],
1211 ];
1212 for bytes in cases {
1213 let shared = Arc::new(FontBytes::Owned(Arc::from(bytes)));
1214 match loader.load_font_shared(shared, 0) {
1215 Err(LayoutError::ShapingError(msg)) => assert!(!msg.is_empty()),
1216 other => panic!("garbage must not parse: {other:?}"),
1217 }
1218 }
1219 }
1220
1221 #[test]
1222 fn load_font_shared_matches_the_eager_parse_and_tolerates_extreme_indices() {
1223 let loader = PathLoader::new();
1224 let shared = Arc::new(FontBytes::Owned(Arc::from(MOCK_MONO.to_vec())));
1225 let font_ref = loader
1226 .load_font_shared(Arc::clone(&shared), 0)
1227 .expect("the positive control must parse");
1228 let eager = mock();
1229 assert_eq!(font_ref.num_glyphs(), eager.num_glyphs());
1230 assert_eq!(font_ref.get_hash(), eager.get_hash());
1231
1232 for index in [0usize, 1, usize::MAX] {
1234 let _ = loader.load_font_shared(Arc::clone(&shared), index);
1235 }
1236 }
1237
1238 #[test]
1243 fn evict_unused_on_an_empty_manager_is_zero_for_extreme_durations() {
1244 let manager: FontManager<FontRef> =
1245 FontManager::new(FcFontCache::default()).expect("an empty FontManager must build");
1246 for idle in [
1249 Duration::ZERO,
1250 Duration::from_nanos(1),
1251 Duration::from_secs(3600),
1252 Duration::MAX,
1253 ] {
1254 assert_eq!(manager.evict_unused(idle), 0);
1255 }
1256 }
1257
1258 #[test]
1259 fn evict_unused_only_reclaims_stale_deferred_faces() {
1260 let manager: FontManager<FontRef> =
1261 FontManager::new(FcFontCache::default()).expect("an empty FontManager must build");
1262
1263 let deferred = crate::parsed_font_to_font_ref(mock_deferred());
1264 let eager = crate::parsed_font_to_font_ref(mock());
1265 {
1266 let mut fonts = manager.parsed_fonts.lock().unwrap();
1267 fonts.insert(FontId::new(), deferred.clone());
1268 fonts.insert(FontId::new(), eager.clone());
1269 }
1270
1271 assert_eq!(manager.evict_unused(Duration::ZERO), 0);
1274
1275 let parsed = crate::font_ref_to_parsed_font(&deferred);
1277 assert!(parsed.get_or_decode_glyph(1).is_some(), "gid 1 must decode");
1278 let _ = crate::font_ref_to_parsed_font(&eager).get_or_decode_glyph(1);
1279
1280 assert_eq!(manager.evict_unused(Duration::from_secs(3600)), 0);
1282
1283 assert_eq!(manager.evict_unused(Duration::ZERO), 1);
1286 assert_eq!(manager.evict_unused(Duration::ZERO), 0);
1288
1289 assert!(parsed.get_or_decode_glyph(2).is_some());
1291 }
1292
1293 #[test]
1298 fn shape_text_empty_input_yields_no_glyphs() {
1299 let font = mock();
1300 let glyphs = shape(&font, "").expect("empty text is not an error");
1301 assert!(glyphs.is_empty());
1302
1303 let via_public = shape_text_for_parsed_font(
1304 &font,
1305 "",
1306 Script::Latin,
1307 Language::EnglishUS,
1308 BidiDirection::Ltr,
1309 &style_at(16.0),
1310 )
1311 .expect("empty text is not an error");
1312 assert!(via_public.is_empty());
1313
1314 let font_ref = crate::parsed_font_to_font_ref(mock());
1315 let via_ref = font_ref
1316 .shape_text(
1317 "",
1318 Script::Latin,
1319 Language::EnglishUS,
1320 BidiDirection::Ltr,
1321 &style_at(16.0),
1322 )
1323 .expect("empty text is not an error");
1324 assert!(via_ref.is_empty());
1325 }
1326
1327 #[test]
1328 fn shape_text_valid_minimal_input_maps_bytes_one_to_one() {
1329 let font = mock();
1330 let glyphs = shape(&font, "abc").expect("plain ASCII must shape");
1331 assert_eq!(glyphs.len(), 3);
1332 assert_spans_are_sane(&glyphs, "abc");
1333
1334 for (i, (g, ch)) in glyphs.iter().zip("abc".chars()).enumerate() {
1335 assert_eq!(g.codepoint, ch);
1336 assert_eq!(g.logical_byte_index, i);
1337 assert_eq!(g.logical_byte_len, 1);
1338 assert_eq!(g.glyph_id, font.lookup_glyph_index(ch as u32).unwrap_or(0));
1339 assert!(g.advance > 0.0, "a real glyph has a positive advance");
1340 assert!(g.advance.is_finite());
1341 assert_eq!(g.font_hash, font.get_hash());
1342 assert_eq!(g.script, Script::Latin);
1343 }
1344 }
1345
1346 #[test]
1347 fn shape_text_whitespace_only_input_is_shaped_not_trimmed() {
1348 let font = mock();
1349 let text = " \t\n\r ";
1350 let glyphs = shape(&font, text).expect("whitespace must shape");
1351 assert!(!glyphs.is_empty(), "whitespace is not silently dropped");
1352 assert_spans_are_sane(&glyphs, text);
1353 for g in &glyphs {
1354 assert!(g.advance.is_finite());
1355 }
1356 }
1357
1358 #[test]
1359 fn shape_text_unicode_and_missing_glyphs_keep_multibyte_spans_intact() {
1360 let font = mock();
1361 for text in [
1364 "\u{1F600}",
1365 "e\u{0301}\u{0327}",
1366 "\u{0627}\u{0644}\u{0639}\u{0631}\u{0628}\u{064A}\u{0629}",
1367 "\u{4E2D}\u{6587}",
1368 "\u{FFFD}\u{FDD0}\u{F0000}",
1369 "a\u{200B}b\u{00AD}c",
1370 ] {
1371 let glyphs = shape(&font, text).unwrap_or_else(|e| panic!("{text:?} must shape: {e:?}"));
1372 assert!(!glyphs.is_empty(), "{text:?} produced no glyphs");
1373 assert_spans_are_sane(&glyphs, text);
1374 }
1375
1376 let emoji = "\u{1F600}";
1379 let glyphs = shape(&font, emoji).expect("emoji must shape");
1380 assert_eq!(glyphs.len(), 1);
1381 assert_eq!(glyphs[0].codepoint, '\u{1F600}');
1382 assert_eq!(glyphs[0].logical_byte_index, 0);
1383 assert_eq!(glyphs[0].logical_byte_len, 4, "the whole 4-byte char is covered");
1384 assert_eq!(
1385 glyphs[0].glyph_id,
1386 font.lookup_glyph_index('\u{1F600}' as u32).unwrap_or(0)
1387 );
1388 }
1389
1390 #[test]
1391 fn shape_text_extremely_long_input_terminates() {
1392 let font = mock();
1393 let text = "a".repeat(10_000);
1394 let glyphs = shape(&font, &text).expect("a long run must shape");
1395 assert_eq!(glyphs.len(), 10_000);
1396 assert_spans_are_sane(&glyphs, &text);
1397 assert_eq!(glyphs.last().unwrap().logical_byte_index, 9_999);
1399 }
1400
1401 #[test]
1402 fn shape_text_deeply_nested_brackets_does_not_stack_overflow() {
1403 let font = mock();
1404 let text = format!("{}{}", "[".repeat(10_000), "]".repeat(10_000));
1405 let glyphs = shape(&font, &text).expect("nested brackets are just characters");
1406 assert_eq!(glyphs.len(), 20_000);
1407 assert_spans_are_sane(&glyphs, &text);
1408 }
1409
1410 #[test]
1411 fn shape_text_boundary_numeric_text_is_shaped_verbatim() {
1412 let font = mock();
1413 let text = "0 -0 9223372036854775807 -9223372036854775808 NaN inf 1e309 0.0000001";
1414 let glyphs = shape(&font, text).expect("numeric-looking text is still text");
1415 assert_spans_are_sane(&glyphs, text);
1416 assert!(!glyphs.is_empty());
1417 for g in &glyphs {
1418 assert!(g.advance.is_finite(), "a 16px advance must stay finite");
1419 }
1420 }
1421
1422 #[test]
1423 fn shape_text_nan_and_infinite_font_sizes_do_not_panic() {
1424 let font = mock();
1425 for size in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
1428 let glyphs = shape_text_internal(
1429 &font,
1430 "ab",
1431 Script::Latin,
1432 Language::EnglishUS,
1433 BidiDirection::Ltr,
1434 &style_at(size),
1435 )
1436 .unwrap_or_else(|e| panic!("font_size {size} must not fail shaping: {e:?}"));
1437 assert_eq!(glyphs.len(), 2);
1438 assert_spans_are_sane(&glyphs, "ab");
1439 for g in &glyphs {
1440 assert!(
1441 !g.advance.is_finite(),
1442 "a non-finite font size must not manufacture a finite advance ({size})"
1443 );
1444 }
1445 }
1446
1447 for size in [f32::MAX, -f32::MAX, 1e30f32] {
1451 let glyphs = shape_text_internal(
1452 &font,
1453 "ab",
1454 Script::Latin,
1455 Language::EnglishUS,
1456 BidiDirection::Ltr,
1457 &style_at(size),
1458 )
1459 .unwrap_or_else(|e| panic!("font_size {size} must not fail shaping: {e:?}"));
1460 assert_eq!(glyphs.len(), 2);
1461 for g in &glyphs {
1462 assert!(!g.advance.is_nan(), "size {size} produced a NaN advance");
1463 }
1464 }
1465 }
1466
1467 #[test]
1468 fn shape_text_zero_and_tiny_font_sizes_produce_zero_or_finite_advances() {
1469 let font = mock();
1470 for (size, expect_zero) in [(0.0f32, true), (-0.0f32, true), (f32::MIN_POSITIVE, false)] {
1471 let glyphs = shape_text_internal(
1472 &font,
1473 "ab",
1474 Script::Latin,
1475 Language::EnglishUS,
1476 BidiDirection::Ltr,
1477 &style_at(size),
1478 )
1479 .expect("degenerate font sizes must still shape");
1480 assert_eq!(glyphs.len(), 2);
1481 for g in &glyphs {
1482 assert!(g.advance.is_finite(), "size {size} produced {}", g.advance);
1483 if expect_zero {
1484 assert_eq!(g.advance, 0.0, "a 0px font has zero-width glyphs");
1485 assert_eq!(g.kerning, 0.0);
1486 }
1487 }
1488 }
1489 }
1490
1491 #[test]
1492 fn shape_text_negative_font_size_mirrors_the_advance_sign() {
1493 let font = mock();
1494 let positive = shape_text_internal(
1495 &font,
1496 "a",
1497 Script::Latin,
1498 Language::EnglishUS,
1499 BidiDirection::Ltr,
1500 &style_at(16.0),
1501 )
1502 .expect("shape at +16px");
1503 let negative = shape_text_internal(
1504 &font,
1505 "a",
1506 Script::Latin,
1507 Language::EnglishUS,
1508 BidiDirection::Ltr,
1509 &style_at(-16.0),
1510 )
1511 .expect("a negative font size must not panic");
1512 assert_eq!(positive.len(), 1);
1513 assert_eq!(negative.len(), 1);
1514 assert_eq!(positive[0].glyph_id, negative[0].glyph_id);
1515 assert!(negative[0].advance.is_finite());
1516 assert!(
1517 negative[0].advance <= 0.0,
1518 "a negative size cannot yield a positive advance"
1519 );
1520 }
1521
1522 #[test]
1523 fn shape_text_garbage_font_features_are_skipped_not_fatal() {
1524 let font = mock();
1525 let style = StyleProperties {
1526 font_features: vec![
1527 String::new(),
1528 " ".to_string(),
1529 "waytoolongtag".to_string(),
1530 "liga=-1".to_string(),
1531 "liga=99999999999999999999".to_string(),
1532 "\u{1F600}".to_string(),
1533 "liga".to_string(), "ss01=2".to_string(),
1535 ],
1536 ..StyleProperties::default()
1537 };
1538 let glyphs = shape_text_internal(
1539 &font,
1540 "abc",
1541 Script::Latin,
1542 Language::EnglishUS,
1543 BidiDirection::Ltr,
1544 &style,
1545 )
1546 .expect("malformed feature strings must be dropped, not fatal");
1547 assert_eq!(glyphs.len(), 3);
1548 assert_spans_are_sane(&glyphs, "abc");
1549 }
1550
1551 #[test]
1552 fn shape_text_direction_drives_the_bidi_level() {
1553 let font = mock();
1554 for (direction, expected) in [(BidiDirection::Ltr, 0u8), (BidiDirection::Rtl, 1u8)] {
1555 let glyphs = shape_text_internal(
1556 &font,
1557 "abc",
1558 Script::Latin,
1559 Language::EnglishUS,
1560 direction,
1561 &style_at(16.0),
1562 )
1563 .expect("both directions must shape");
1564 assert!(!glyphs.is_empty());
1565 for g in &glyphs {
1566 assert_eq!(g.bidi_level.level(), expected);
1567 assert_eq!(g.bidi_level.is_rtl(), direction.is_rtl());
1568 }
1569 }
1570 }
1571
1572 #[test]
1573 fn shape_text_every_script_and_language_pairing_is_shapeable() {
1574 let font = mock();
1575 for script in ALL_SCRIPTS {
1578 let glyphs = shape_text_internal(
1579 &font,
1580 "Hello \u{0e2a}\u{0e27}\u{0e31}\u{0e2a}\u{0e14}\u{0e35}",
1581 script,
1582 Language::EnglishUS,
1583 BidiDirection::Ltr,
1584 &style_at(16.0),
1585 )
1586 .unwrap_or_else(|e| panic!("{script:?} must shape: {e:?}"));
1587 assert!(!glyphs.is_empty(), "{script:?} produced no glyphs");
1588 for g in &glyphs {
1589 assert_eq!(g.script, script, "the requested script is stamped on the glyph");
1590 }
1591 }
1592 }
1593
1594 #[test]
1595 fn shape_text_public_internal_and_font_ref_paths_agree() {
1596 let font = mock();
1599 let font_ref = crate::parsed_font_to_font_ref(mock());
1600 let text = "Wafer fi\u{0301}x \u{1F600}";
1601 let style = style_at(13.5);
1602
1603 let internal = shape_text_internal(
1604 &font,
1605 text,
1606 Script::Latin,
1607 Language::EnglishUS,
1608 BidiDirection::Ltr,
1609 &style,
1610 )
1611 .expect("internal shaping");
1612 let public = shape_text_for_parsed_font(
1613 &font,
1614 text,
1615 Script::Latin,
1616 Language::EnglishUS,
1617 BidiDirection::Ltr,
1618 &style,
1619 )
1620 .expect("public shaping");
1621 let via_ref = font_ref
1622 .shape_text(
1623 text,
1624 Script::Latin,
1625 Language::EnglishUS,
1626 BidiDirection::Ltr,
1627 &style,
1628 )
1629 .expect("FontRef shaping");
1630 let via_helper = font
1631 .shape_text_for_font_ref(
1632 &font_ref,
1633 text,
1634 Script::Latin,
1635 Language::EnglishUS,
1636 BidiDirection::Ltr,
1637 &style,
1638 )
1639 .expect("shape_text_for_font_ref");
1640
1641 assert_eq!(internal.len(), public.len());
1642 assert_eq!(internal.len(), via_ref.len());
1643 assert_eq!(internal.len(), via_helper.len());
1644 for (((a, b), c), d) in internal
1645 .iter()
1646 .zip(public.iter())
1647 .zip(via_ref.iter())
1648 .zip(via_helper.iter())
1649 {
1650 for other in [b, c, d] {
1651 assert_eq!(a.glyph_id, other.glyph_id);
1652 assert_eq!(a.codepoint, other.codepoint);
1653 assert_eq!(a.advance, other.advance);
1654 assert_eq!(a.kerning, other.kerning);
1655 assert_eq!(a.logical_byte_index, other.logical_byte_index);
1656 assert_eq!(a.logical_byte_len, other.logical_byte_len);
1657 assert_eq!(a.font_hash, other.font_hash);
1658 }
1659 }
1660 assert_spans_are_sane(&internal, text);
1661 }
1662
1663 #[test]
1668 fn get_hash_is_stable_and_shared_with_the_font_ref_view() {
1669 let a = mock();
1670 let b = mock();
1671 assert_eq!(a.get_hash(), b.get_hash(), "parsing is deterministic");
1672 assert_eq!(a.get_hash(), a.hash, "the getter reads the cached field");
1673
1674 let font_ref = crate::parsed_font_to_font_ref(mock());
1675 assert_eq!(font_ref.get_hash(), a.get_hash());
1676
1677 assert_eq!(mock_deferred().get_hash(), a.get_hash());
1679 }
1680
1681 #[test]
1686 fn get_glyph_size_out_of_range_glyph_ids_are_none() {
1687 let font = mock();
1688 assert!(font.get_glyph_size(u16::MAX, 16.0).is_none());
1689 assert!(font.get_glyph_size(font.num_glyphs, 16.0).is_none());
1690 let gid = font.lookup_glyph_index('a' as u32).expect("'a' must be mapped");
1692 assert!(gid < font.num_glyphs);
1693 assert!(font.get_glyph_size(gid, 16.0).is_some());
1694 }
1695
1696 #[test]
1697 fn get_glyph_size_zero_negative_and_non_finite_font_sizes() {
1698 let font = mock();
1699 let gid = font.lookup_glyph_index('a' as u32).expect("'a' must be mapped");
1700
1701 let zero = font.get_glyph_size(gid, 0.0).expect("gid decodes");
1702 assert_eq!(zero.width, 0.0);
1703 assert_eq!(zero.height, 0.0);
1704
1705 let base = font.get_glyph_size(gid, 16.0).expect("gid decodes");
1706 assert!(base.width > 0.0 && base.height > 0.0);
1707
1708 let doubled = font.get_glyph_size(gid, 32.0).expect("gid decodes");
1710 assert!((doubled.width - 2.0 * base.width).abs() <= 1e-3 * base.width.max(1.0));
1711
1712 let negative = font.get_glyph_size(gid, -16.0).expect("gid decodes");
1713 assert!(negative.width <= 0.0 && negative.width.is_finite());
1714
1715 for size in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN_POSITIVE] {
1716 let size_result = font
1717 .get_glyph_size(gid, size)
1718 .unwrap_or_else(|| panic!("gid must still decode at {size}"));
1719 assert!(
1720 !size_result.width.is_nan() || size.is_nan(),
1721 "only a NaN input may produce a NaN width"
1722 );
1723 }
1724 }
1725
1726 #[test]
1727 fn get_glyph_size_zero_units_per_em_uses_the_constant_fallback_scale() {
1728 assert_eq!(FALLBACK_SCALE, 0.01);
1729 let mut font = mock();
1730 let gid = font.lookup_glyph_index('a' as u32).expect("'a' must be mapped");
1731 font.font_metrics.units_per_em = 0; let small = font.get_glyph_size(gid, 16.0).expect("gid decodes");
1736 let huge = font.get_glyph_size(gid, 1000.0).expect("gid decodes");
1737 assert!(small.width > 0.0 && small.height > 0.0, "no divide-by-zero NaN");
1738 assert_eq!(small.width, huge.width);
1739 assert_eq!(small.height, huge.height);
1740 }
1741
1742 #[test]
1747 fn get_hyphen_glyph_and_advance_follows_the_cmap_and_scales_linearly() {
1748 let font = mock();
1749 let expected_gid = font
1750 .lookup_glyph_index('-' as u32)
1751 .expect("the positive control has a hyphen");
1752
1753 let (gid, zero_advance) = font.get_hyphen_glyph_and_advance(0.0).expect("hyphen at 0px");
1754 assert_eq!(gid, expected_gid);
1755 assert_eq!(zero_advance, 0.0, "a 0px font gives a 0px advance");
1756
1757 let (_, a16) = font.get_hyphen_glyph_and_advance(16.0).expect("hyphen at 16px");
1758 let (_, a32) = font.get_hyphen_glyph_and_advance(32.0).expect("hyphen at 32px");
1759 assert!(a16 > 0.0 && a16.is_finite());
1760 assert!((a32 - 2.0 * a16).abs() <= 1e-3 * a16, "advance is linear in font size");
1761
1762 let (_, negative) = font.get_hyphen_glyph_and_advance(-16.0).expect("hyphen at -16px");
1763 assert!(negative.is_finite() && negative <= 0.0);
1764 }
1765
1766 #[test]
1767 fn get_kashida_glyph_and_advance_presence_matches_the_cmap() {
1768 let font = mock();
1769 let has_tatweel = font.has_glyph(0x0640);
1771 let result = font.get_kashida_glyph_and_advance(16.0);
1772 assert_eq!(
1773 result.is_some(),
1774 has_tatweel,
1775 "kashida availability must track the cmap"
1776 );
1777 if let Some((gid, advance)) = result {
1778 assert_eq!(Some(gid), font.lookup_glyph_index(0x0640));
1779 assert!(advance.is_finite() && advance >= 0.0);
1780 let (_, doubled) = font.get_kashida_glyph_and_advance(32.0).expect("still mapped");
1781 assert!((doubled - 2.0 * advance).abs() <= 1e-3 * advance.max(1.0));
1782 }
1783 }
1784
1785 #[test]
1786 fn hyphen_and_kashida_survive_nan_inf_and_extreme_font_sizes() {
1787 let font = mock();
1788 let hyphen_gid = font
1789 .lookup_glyph_index('-' as u32)
1790 .expect("the positive control has a hyphen");
1791 assert!(
1792 font.get_horizontal_advance(hyphen_gid) > 0,
1793 "the hyphen must have a non-zero advance for this test to mean anything"
1794 );
1795
1796 for size in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
1798 let (gid, advance) = font
1799 .get_hyphen_glyph_and_advance(size)
1800 .expect("the glyph id does not depend on the font size");
1801 assert_eq!(gid, hyphen_gid);
1802 assert!(!advance.is_finite(), "{size} produced a finite advance");
1803 let _ = font.get_kashida_glyph_and_advance(size);
1804 }
1805
1806 for size in [f32::MAX, -f32::MAX, f32::MIN_POSITIVE, -f32::MIN_POSITIVE] {
1809 let (gid, advance) = font
1810 .get_hyphen_glyph_and_advance(size)
1811 .expect("the glyph id does not depend on the font size");
1812 assert_eq!(gid, hyphen_gid);
1813 assert!(!advance.is_nan(), "size {size} produced a NaN advance");
1814 let _ = font.get_kashida_glyph_and_advance(size);
1815 }
1816 }
1817
1818 #[test]
1819 fn hyphen_and_kashida_return_none_when_units_per_em_is_zero() {
1820 let mut font = mock();
1821 font.font_metrics.units_per_em = 0;
1822 assert!(font.get_hyphen_glyph_and_advance(16.0).is_none());
1824 assert!(font.get_kashida_glyph_and_advance(16.0).is_none());
1825 assert!(font.get_hyphen_glyph_and_advance(f32::NAN).is_none());
1826 }
1827
1828 #[test]
1833 fn build_feature_mask_always_contains_the_default_mask() {
1834 let default_bits = FeatureMask::default_mask().bits();
1835 for script in ALL_SCRIPTS {
1836 let mask = build_feature_mask_for_script(script);
1837 assert_eq!(
1838 mask.bits() & default_bits,
1839 default_bits,
1840 "{script:?} dropped a default feature"
1841 );
1842 assert!(
1843 mask.contains(Feature::LIGA) && mask.contains(Feature::CCMP),
1844 "{script:?} must keep LIGA/CCMP"
1845 );
1846 }
1847 }
1848
1849 #[test]
1850 fn build_feature_mask_adds_the_script_specific_features() {
1851 let arabic = build_feature_mask_for_script(Script::Arabic);
1853 for feature in [Feature::INIT, Feature::MEDI, Feature::FINA, Feature::ISOL] {
1854 assert!(arabic.contains(feature), "Arabic is missing a positional form");
1855 }
1856
1857 for script in [
1859 Script::Devanagari,
1860 Script::Bengali,
1861 Script::Gujarati,
1862 Script::Gurmukhi,
1863 Script::Kannada,
1864 Script::Malayalam,
1865 Script::Oriya,
1866 Script::Tamil,
1867 Script::Telugu,
1868 ] {
1869 let mask = build_feature_mask_for_script(script);
1870 for feature in [Feature::CJCT, Feature::HALF, Feature::RPHF, Feature::NUKT] {
1871 assert!(mask.contains(feature), "{script:?} is missing an Indic feature");
1872 }
1873 }
1874
1875 let sinhala = build_feature_mask_for_script(Script::Sinhala);
1877 assert!(sinhala.contains(Feature::AKHN) && sinhala.contains(Feature::RPHF));
1878 assert!(!sinhala.contains(Feature::CJCT));
1879
1880 assert!(build_feature_mask_for_script(Script::Myanmar).contains(Feature::PSTF));
1882 assert!(build_feature_mask_for_script(Script::Khmer).contains(Feature::ABVF));
1883
1884 for script in [Script::Latin, Script::Greek, Script::Cyrillic, Script::Georgian] {
1886 assert_eq!(
1887 build_feature_mask_for_script(script).bits(),
1888 FeatureMask::default_mask().bits(),
1889 "{script:?} must not add script-specific features"
1890 );
1891 }
1892 }
1893
1894 #[test]
1899 fn script_tags_are_four_printable_ascii_bytes() {
1900 for script in ALL_SCRIPTS {
1901 let tag = to_opentype_script_tag(script);
1902 let bytes = tag.to_be_bytes();
1903 assert_eq!(bytes.len(), 4);
1904 for b in bytes {
1905 assert!(
1906 b.is_ascii_lowercase() || b.is_ascii_digit(),
1907 "{script:?} -> {tag:#010x} is not a lowercase OpenType tag"
1908 );
1909 }
1910 assert_ne!(tag, 0, "{script:?} must not map to the null tag");
1911 }
1912 }
1913
1914 #[test]
1915 fn script_tags_match_the_opentype_registry_and_alias_kana() {
1916 assert_eq!(to_opentype_script_tag(Script::Latin), u32::from_be_bytes(*b"latn"));
1917 assert_eq!(to_opentype_script_tag(Script::Arabic), u32::from_be_bytes(*b"arab"));
1918 assert_eq!(to_opentype_script_tag(Script::Mandarin), u32::from_be_bytes(*b"hani"));
1919 assert_eq!(to_opentype_script_tag(Script::Devanagari), u32::from_be_bytes(*b"deva"));
1920
1921 assert_eq!(
1923 to_opentype_script_tag(Script::Hiragana),
1924 to_opentype_script_tag(Script::Katakana)
1925 );
1926 assert_eq!(to_opentype_script_tag(Script::Hiragana), u32::from_be_bytes(*b"kana"));
1927
1928 let mut tags: Vec<u32> = ALL_SCRIPTS.iter().map(|s| to_opentype_script_tag(*s)).collect();
1930 tags.sort_unstable();
1931 tags.dedup();
1932 assert_eq!(tags.len(), 23, "only the kana pair may collide");
1933 }
1934
1935 #[test]
1940 fn parse_font_feature_valid_minimal_inputs() {
1941 assert_eq!(
1942 parse_font_feature("liga"),
1943 Some((u32::from_be_bytes(*b"liga"), 1)),
1944 "a bare tag defaults to value 1"
1945 );
1946 assert_eq!(parse_font_feature("liga=0"), Some((u32::from_be_bytes(*b"liga"), 0)));
1947 assert_eq!(parse_font_feature("ss01"), Some((u32::from_be_bytes(*b"ss01"), 1)));
1948 assert_eq!(parse_font_feature("smcp=2"), Some((u32::from_be_bytes(*b"smcp"), 2)));
1949 assert_eq!(
1951 parse_font_feature("ss01=4294967295"),
1952 Some((u32::from_be_bytes(*b"ss01"), u32::MAX))
1953 );
1954 }
1955
1956 #[test]
1957 fn parse_font_feature_pads_short_tags_with_spaces() {
1958 assert_eq!(parse_font_feature("aa"), Some((u32::from_be_bytes(*b"aa "), 1)));
1959 assert_eq!(parse_font_feature("a"), Some((u32::from_be_bytes(*b"a "), 1)));
1960 assert_eq!(parse_font_feature("abc=3"), Some((u32::from_be_bytes(*b"abc "), 3)));
1961 }
1962
1963 #[test]
1964 fn parse_font_feature_trims_surrounding_whitespace() {
1965 assert_eq!(parse_font_feature(" liga "), Some((u32::from_be_bytes(*b"liga"), 1)));
1966 assert_eq!(parse_font_feature("\tliga\n=\t2 "), Some((u32::from_be_bytes(*b"liga"), 2)));
1967 }
1968
1969 #[test]
1970 fn parse_font_feature_empty_and_whitespace_only_yield_the_all_space_tag() {
1971 let space_tag = u32::from_be_bytes(*b" ");
1974 assert_eq!(parse_font_feature(""), Some((space_tag, 1)));
1975 assert_eq!(parse_font_feature(" "), Some((space_tag, 1)));
1976 assert_eq!(parse_font_feature("\t\n"), Some((space_tag, 1)));
1977 assert_eq!(parse_font_feature("="), None);
1979 assert_eq!(parse_font_feature("liga="), None);
1980 assert_eq!(parse_font_feature("liga= "), None);
1981 }
1982
1983 #[test]
1984 fn parse_font_feature_rejects_over_long_tags_and_junk() {
1985 assert_eq!(parse_font_feature("toolongtag"), None);
1986 assert_eq!(parse_font_feature("lig a"), None); assert_eq!(parse_font_feature("liga;garbage"), None);
1988 assert_eq!(parse_font_feature("valid;garbage=1"), None);
1989 assert_eq!(parse_font_feature(&"x".repeat(1_000_000)), None);
1991 assert_eq!(parse_font_feature(&format!("{}=1", "x".repeat(1_000_000))), None);
1992 }
1993
1994 #[test]
1995 fn parse_font_feature_rejects_boundary_and_non_numeric_values() {
1996 for bad in [
1997 "liga=-1",
1998 "liga=-0",
1999 "liga=1.5",
2000 "liga=NaN",
2001 "liga=inf",
2002 "liga=0x1",
2003 "liga=4294967296", "liga=9223372036854775807", "liga=99999999999999999999999999",
2006 "liga= 1 2",
2007 ] {
2008 assert_eq!(parse_font_feature(bad), None, "{bad:?} must be rejected");
2009 }
2010 assert_eq!(parse_font_feature("liga=+1"), Some((u32::from_be_bytes(*b"liga"), 1)));
2012 assert_eq!(parse_font_feature("liga=1=2"), Some((u32::from_be_bytes(*b"liga"), 1)));
2014 }
2015
2016 #[test]
2017 fn parse_font_feature_unicode_input_does_not_panic() {
2018 for input in [
2022 "\u{1F600}", "\u{1F600}\u{1F600}",
2024 "é",
2025 "ß=1",
2026 "e\u{0301}", "\u{202E}liga", "\u{0000}\u{0001}",
2029 ] {
2030 let _ = parse_font_feature(input); }
2032 assert_eq!(parse_font_feature("\u{1F600}"), None);
2033 assert_eq!(parse_font_feature("é"), None);
2034 }
2035
2036 #[test]
2041 fn add_variant_features_maps_css_variants_to_opentype_tags() {
2042 let tags = |style: &StyleProperties| -> Vec<u32> {
2043 let mut features = Vec::new();
2044 add_variant_features(style, &mut features);
2045 assert!(
2046 features.iter().all(|f| f.alternate.is_none()),
2047 "variant features are on/off, never alternates"
2048 );
2049 features.iter().map(|f| f.feature_tag).collect()
2050 };
2051
2052 assert!(tags(&StyleProperties::default()).is_empty());
2054
2055 let small_caps = StyleProperties {
2056 font_variant_caps: FontVariantCaps::SmallCaps,
2057 ..StyleProperties::default()
2058 };
2059 assert_eq!(tags(&small_caps), vec![u32::from_be_bytes(*b"smcp")]);
2060
2061 let all_small = StyleProperties {
2062 font_variant_caps: FontVariantCaps::AllSmallCaps,
2063 ..StyleProperties::default()
2064 };
2065 assert_eq!(
2066 tags(&all_small),
2067 vec![u32::from_be_bytes(*b"c2sc"), u32::from_be_bytes(*b"smcp")]
2068 );
2069
2070 let combined = StyleProperties {
2071 font_variant_ligatures: FontVariantLigatures::Discretionary,
2072 font_variant_numeric: FontVariantNumeric::TabularNums,
2073 font_variant_caps: FontVariantCaps::TitlingCaps,
2074 ..StyleProperties::default()
2075 };
2076 assert_eq!(
2077 tags(&combined),
2078 vec![
2079 u32::from_be_bytes(*b"dlig"),
2080 u32::from_be_bytes(*b"titl"),
2081 u32::from_be_bytes(*b"tnum"),
2082 ],
2083 "ligature, caps and numeric features are all emitted, in that order"
2084 );
2085 }
2086
2087 #[test]
2088 fn add_variant_features_is_additive_and_never_panics_for_any_variant() {
2089 let ligatures = [
2090 FontVariantLigatures::Normal,
2091 FontVariantLigatures::None,
2092 FontVariantLigatures::Common,
2093 FontVariantLigatures::NoCommon,
2094 FontVariantLigatures::Discretionary,
2095 FontVariantLigatures::NoDiscretionary,
2096 FontVariantLigatures::Historical,
2097 FontVariantLigatures::NoHistorical,
2098 FontVariantLigatures::Contextual,
2099 FontVariantLigatures::NoContextual,
2100 ];
2101 let caps = [
2102 FontVariantCaps::Normal,
2103 FontVariantCaps::SmallCaps,
2104 FontVariantCaps::AllSmallCaps,
2105 FontVariantCaps::PetiteCaps,
2106 FontVariantCaps::AllPetiteCaps,
2107 FontVariantCaps::Unicase,
2108 FontVariantCaps::TitlingCaps,
2109 ];
2110 let numeric = [
2111 FontVariantNumeric::Normal,
2112 FontVariantNumeric::LiningNums,
2113 FontVariantNumeric::OldstyleNums,
2114 FontVariantNumeric::ProportionalNums,
2115 FontVariantNumeric::TabularNums,
2116 FontVariantNumeric::DiagonalFractions,
2117 FontVariantNumeric::StackedFractions,
2118 FontVariantNumeric::Ordinal,
2119 FontVariantNumeric::SlashedZero,
2120 ];
2121
2122 let sentinel = FeatureInfo {
2124 feature_tag: u32::from_be_bytes(*b"kern"),
2125 alternate: Some(7),
2126 };
2127 for l in ligatures {
2128 for c in caps {
2129 for n in numeric {
2130 let style = StyleProperties {
2131 font_variant_ligatures: l,
2132 font_variant_caps: c,
2133 font_variant_numeric: n,
2134 ..StyleProperties::default()
2135 };
2136 let mut features = vec![sentinel];
2137 add_variant_features(&style, &mut features);
2138 assert_eq!(features[0].feature_tag, sentinel.feature_tag);
2139 assert_eq!(features[0].alternate, Some(7));
2140 assert!(features.len() <= 5, "{l:?}/{c:?}/{n:?} emitted too many features");
2142 for f in &features[1..] {
2143 assert!(f.feature_tag.to_be_bytes().iter().all(u8::is_ascii_graphic));
2144 }
2145 }
2146 }
2147 }
2148 }
2149
2150 #[cfg(feature = "text_layout_hyphenation")]
2155 #[test]
2156 fn lang_tags_are_four_byte_uppercase_padded_tags() {
2157 use hyphenation::Language as HL;
2158
2159 let sample = [
2162 HL::EnglishUS,
2163 HL::EnglishGB,
2164 HL::German1901,
2165 HL::German1996,
2166 HL::French,
2167 HL::Russian,
2168 HL::Finnish,
2169 HL::FinnishScholastic,
2170 HL::Latin,
2171 HL::LatinClassic,
2172 HL::Welsh,
2173 HL::Thai,
2174 ];
2175 for lang in sample {
2176 let tag = to_opentype_lang_tag(lang);
2177 let bytes = tag.to_be_bytes();
2178 for b in bytes {
2179 assert!(
2180 b.is_ascii_uppercase() || b == b' ',
2181 "{lang:?} -> {tag:#010x} is not an uppercase, space-padded tag"
2182 );
2183 }
2184 assert_ne!(tag, 0);
2185 }
2186
2187 assert_eq!(to_opentype_lang_tag(HL::EnglishUS), u32::from_be_bytes(*b"ENU "));
2188 assert_eq!(to_opentype_lang_tag(HL::EnglishGB), u32::from_be_bytes(*b"ENG "));
2189 assert_eq!(to_opentype_lang_tag(HL::German1996), u32::from_be_bytes(*b"DEU "));
2190 assert_eq!(to_opentype_lang_tag(HL::French), u32::from_be_bytes(*b"FRA "));
2191 assert_eq!(to_opentype_lang_tag(HL::Russian), u32::from_be_bytes(*b"RUS "));
2192 assert_eq!(
2194 to_opentype_lang_tag(HL::German1901),
2195 to_opentype_lang_tag(HL::German1996)
2196 );
2197 assert_eq!(
2198 to_opentype_lang_tag(HL::Finnish),
2199 to_opentype_lang_tag(HL::FinnishScholastic)
2200 );
2201 }
2202
2203 #[test]
2208 fn font_ref_trait_getters_delegate_to_the_inner_parsed_font() {
2209 let parsed = mock();
2210 let font_ref = crate::parsed_font_to_font_ref(mock());
2211
2212 assert_eq!(font_ref.num_glyphs(), parsed.num_glyphs);
2213 assert_eq!(font_ref.get_space_width(), parsed.get_space_width());
2214 assert_eq!(font_ref.get_font_metrics().units_per_em, parsed.font_metrics.units_per_em);
2215 assert!(font_ref.has_glyph('a' as u32) == parsed.has_glyph('a' as u32));
2216 assert!(!font_ref.has_glyph(0x0011_0000), "an invalid scalar value has no glyph");
2217
2218 let gid = parsed.lookup_glyph_index('a' as u32).expect("'a' must be mapped");
2219 let via_ref = font_ref.get_glyph_size(gid, 16.0).expect("gid decodes");
2220 let via_parsed = parsed.get_glyph_size(gid, 16.0).expect("gid decodes");
2221 assert_eq!(via_ref.width, via_parsed.width);
2222 assert_eq!(via_ref.height, via_parsed.height);
2223
2224 assert_eq!(
2225 font_ref.get_hyphen_glyph_and_advance(16.0).map(|(g, _)| g),
2226 parsed.get_hyphen_glyph_and_advance(16.0).map(|(g, _)| g)
2227 );
2228 assert_eq!(
2229 font_ref.get_kashida_glyph_and_advance(16.0).map(|(g, _)| g),
2230 parsed.get_kashida_glyph_and_advance(16.0).map(|(g, _)| g)
2231 );
2232
2233 let cloned = font_ref.shallow_clone();
2235 assert_eq!(cloned.get_hash(), font_ref.get_hash());
2236 }
2237}