1#![forbid(unsafe_code)]
3
4use std::collections::BTreeMap;
5use std::fmt;
6use std::sync::{Arc, OnceLock};
7
8pub use mathtex_ir::FontKey;
9use mathtex_ir::{GlyphId, GlyphOutline, Length, OutlineCommand};
10
11pub use rustybuzz;
13pub use ttf_parser;
14
15pub trait FontLoader {
17 fn load(&self, spec: &FontSpec) -> Result<FontData, FontError>;
19}
20
21impl<T> FontLoader for &T
22where
23 T: FontLoader + ?Sized,
24{
25 fn load(&self, spec: &FontSpec) -> Result<FontData, FontError> {
26 (**self).load(spec)
27 }
28}
29
30#[derive(Clone, Debug, PartialEq)]
32pub struct FontSpec {
33 spec: String,
34 name: String,
35 file: bool,
36 face_index: u32,
37 variants: Vec<String>,
38 size: Length,
39 script: Option<[u8; 4]>,
40 language: Option<String>,
41 features: Vec<ShapeFeature>,
42 options: Vec<(String, String)>,
43 vertical: bool,
44 unknown: Vec<String>,
45}
46
47impl FontSpec {
48 #[must_use]
50 pub fn parse(spec: &str, size: Length) -> Self {
51 let (name, file, face_index, variant, features) = split_font_name(spec);
52 let mut parsed = Self {
53 spec: spec.to_string(),
54 name,
55 file,
56 face_index,
57 variants: variant
58 .split('/')
59 .filter(|part| !part.is_empty())
60 .map(str::to_string)
61 .collect(),
62 size,
63 script: None,
64 language: None,
65 features: Vec::new(),
66 options: Vec::new(),
67 vertical: false,
68 unknown: Vec::new(),
69 };
70 for option in features.split([':', ';', ',']) {
71 parsed.read_option(option.trim_start_matches([' ', '\t']));
72 }
73 parsed
74 }
75
76 fn read_option(&mut self, option: &str) {
77 if option.is_empty() {
78 return;
79 }
80 if let Some((key, value)) = option.split_once('=') {
81 match key {
82 "script" => return self.script = Some(ot_tag(value)),
83 "language" => return self.language = Some(value.to_string()),
84 "mapping" | "extend" | "slant" | "embolden" | "letterspace" | "color"
85 | "shaper" => return self.options.push((key.to_string(), value.to_string())),
86 _ => {}
87 }
88 }
89 if let Some(rest) = option.strip_prefix('+') {
90 let (tag, param) = rest
92 .split_once('=')
93 .map_or((rest, 0i64), |(tag, value)| (tag, leading_int(value)));
94 let value = if param >= 0 { param + 1 } else { param };
95 self.features.push(ShapeFeature {
96 tag: ot_tag(tag),
97 value: value as u32,
98 });
99 } else if let Some(tag) = option.strip_prefix('-') {
100 self.features.push(ShapeFeature {
101 tag: ot_tag(tag),
102 value: 0,
103 });
104 } else if let Some((tag, value)) = option.split_once('=') {
105 match value.trim().parse::<u32>() {
106 Ok(value) => self.features.push(ShapeFeature {
107 tag: ot_tag(tag),
108 value,
109 }),
110 Err(_) => self.unknown.push(option.to_string()),
111 }
112 } else if option.trim_end() == "vertical" {
113 self.vertical = true;
114 } else {
115 self.unknown.push(option.to_string());
116 }
117 }
118
119 #[must_use]
121 pub fn as_str(&self) -> &str {
122 &self.spec
123 }
124
125 #[must_use]
127 pub fn name(&self) -> &str {
128 &self.name
129 }
130
131 #[must_use]
133 pub fn is_file(&self) -> bool {
134 self.file
135 }
136
137 #[must_use]
139 pub fn face_index(&self) -> u32 {
140 self.face_index
141 }
142
143 #[must_use]
145 pub fn variants(&self) -> &[String] {
146 &self.variants
147 }
148
149 #[must_use]
151 pub fn size(&self) -> Length {
152 self.size
153 }
154
155 #[must_use]
157 pub fn script(&self) -> Option<[u8; 4]> {
158 self.script
159 }
160
161 #[must_use]
163 pub fn language(&self) -> Option<&str> {
164 self.language.as_deref()
165 }
166
167 #[must_use]
169 pub fn features(&self) -> &[ShapeFeature] {
170 &self.features
171 }
172
173 #[must_use]
175 pub fn option(&self, key: &str) -> Option<&str> {
176 self.options
177 .iter()
178 .find(|(name, _)| name == key)
179 .map(|(_, value)| value.as_str())
180 }
181
182 #[must_use]
184 pub fn vertical(&self) -> bool {
185 self.vertical
186 }
187
188 #[must_use]
190 pub fn unknown_options(&self) -> &[String] {
191 &self.unknown
192 }
193
194 #[must_use]
196 pub fn file_candidates(&self) -> Vec<String> {
197 let mut candidates = vec![self.name.clone()];
198 let base = self.name.rsplit(['/', '\\']).next().unwrap_or(&self.name);
199 if !base.contains('.') {
200 candidates.push(format!("{}.otf", self.name));
201 candidates.push(format!("{}.ttf", self.name));
202 }
203 candidates
204 }
205}
206
207fn split_font_name(spec: &str) -> (String, bool, u32, &str, &str) {
209 if let Some(inner) = spec.strip_prefix('[') {
210 let (path, after) = inner.split_once(']').unwrap_or((inner, ""));
211 let (path, face_index) = match path.rsplit_once(':') {
212 Some((path, index)) if index.bytes().all(|byte| byte.is_ascii_digit()) => {
213 (path, index.parse().unwrap_or(0))
214 }
215 _ => (path, 0),
216 };
217 let (variant, features) = after.split_once(':').unwrap_or((after, ""));
218 return (path.to_string(), true, face_index, variant, features);
219 }
220 let (head, features) = spec.split_once(':').unwrap_or((spec, ""));
221 let (name, variant) = head.split_once('/').unwrap_or((head, ""));
222 (name.to_string(), false, 0, variant, features)
223}
224
225fn ot_tag(tag: &str) -> [u8; 4] {
227 let bytes = tag.trim().as_bytes();
228 std::array::from_fn(|index| bytes.get(index).copied().unwrap_or(b' '))
229}
230
231fn leading_int(text: &str) -> i64 {
233 let (negative, digits) = text
234 .strip_prefix('-')
235 .map_or((false, text), |rest| (true, rest));
236 let value = digits
237 .bytes()
238 .take_while(u8::is_ascii_digit)
239 .fold(0i64, |value, digit| {
240 value
241 .saturating_mul(10)
242 .saturating_add(i64::from(digit - b'0'))
243 });
244 if negative {
245 -value
246 } else {
247 value
248 }
249}
250
251#[derive(Clone, Copy, Debug, PartialEq, Eq)]
253pub struct ShapeFeature {
254 pub tag: [u8; 4],
256 pub value: u32,
258}
259
260#[derive(Clone, Debug, Default)]
262pub struct InMemoryFontLoader {
263 fonts: BTreeMap<String, FontData>,
264 next_key: u64,
265}
266
267impl InMemoryFontLoader {
268 #[must_use]
270 pub fn new() -> Self {
271 Self::default()
272 }
273
274 #[must_use]
276 pub fn with_font(mut self, file: impl Into<String>, bytes: impl Into<Arc<[u8]>>) -> Self {
277 self.insert(file, bytes);
278 self
279 }
280
281 pub fn insert(&mut self, file: impl Into<String>, bytes: impl Into<Arc<[u8]>>) -> FontKey {
283 self.next_key += 1;
284 let key = FontKey(self.next_key);
285 self.fonts.insert(file.into(), FontData::new(key, bytes));
286 key
287 }
288
289 pub fn insert_font_data(&mut self, file: impl Into<String>, font: FontData) {
291 self.fonts.insert(file.into(), font);
292 }
293}
294
295impl FontLoader for InMemoryFontLoader {
296 fn load(&self, spec: &FontSpec) -> Result<FontData, FontError> {
297 spec.file_candidates()
298 .iter()
299 .find_map(|name| self.fonts.get(name))
300 .cloned()
301 .ok_or_else(|| FontError::NotFound {
302 name: spec.name().to_string(),
303 })
304 }
305}
306
307#[derive(Clone, Copy, Debug, PartialEq, Eq)]
309pub enum MathKernCorner {
310 TopRight,
312 TopLeft,
314 BottomRight,
316 BottomLeft,
318}
319
320#[derive(Clone, Copy, Debug, PartialEq, Eq)]
322pub struct MathVariant {
323 pub glyph: GlyphId,
325 pub advance: i32,
327}
328
329#[derive(Clone, Copy, Debug, PartialEq, Eq)]
331pub struct MathAssemblyPart {
332 pub glyph: GlyphId,
334 pub start_connector: i32,
336 pub end_connector: i32,
338 pub full_advance: i32,
340 pub extender: bool,
342}
343
344pub trait SharedFace: Send + Sync {
346 fn rustybuzz_face(&self) -> &rustybuzz::Face<'_>;
348
349 fn ttf_face(&self) -> &ttf_parser::Face<'_> {
351 self.rustybuzz_face()
352 }
353}
354
355#[derive(Clone)]
357enum FaceSource {
358 Bytes {
359 bytes: Arc<[u8]>,
360 ttf: Arc<OnceLock<ParsedFace>>,
362 rustybuzz: Arc<OnceLock<ParsedRustybuzzFace>>,
363 },
364 Shared(Arc<dyn SharedFace>),
365}
366
367#[derive(Clone)]
369pub struct FontData {
370 pub key: FontKey,
372 source: FaceSource,
373}
374
375impl PartialEq for FontData {
376 fn eq(&self, other: &Self) -> bool {
377 let same_face = match (&self.source, &other.source) {
378 (FaceSource::Bytes { bytes: a, .. }, FaceSource::Bytes { bytes: b, .. }) => a == b,
379 (FaceSource::Shared(a), FaceSource::Shared(b)) => Arc::ptr_eq(a, b),
380 _ => false,
381 };
382 self.key == other.key && same_face
383 }
384}
385
386impl Eq for FontData {}
387
388impl fmt::Debug for FontData {
389 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390 f.debug_struct("FontData")
391 .field("key", &self.key)
392 .finish_non_exhaustive()
393 }
394}
395
396impl FontData {
397 #[must_use]
399 pub fn new(key: FontKey, bytes: impl Into<Arc<[u8]>>) -> Self {
400 Self {
401 key,
402 source: FaceSource::Bytes {
403 bytes: bytes.into(),
404 ttf: Arc::default(),
405 rustybuzz: Arc::default(),
406 },
407 }
408 }
409
410 #[must_use]
412 pub fn from_shared_face(key: FontKey, face: Arc<dyn SharedFace>) -> Self {
413 Self {
414 key,
415 source: FaceSource::Shared(face),
416 }
417 }
418
419 #[must_use]
421 pub fn bytes(&self) -> Option<&Arc<[u8]>> {
422 match &self.source {
423 FaceSource::Bytes { bytes, .. } => Some(bytes),
424 FaceSource::Shared(_) => None,
425 }
426 }
427
428 pub fn with_ttf_face<R>(
430 &self,
431 f: impl FnOnce(&ttf_parser::Face<'_>) -> R,
432 ) -> Result<R, FontError> {
433 Ok(f(self.ttf_face()?))
434 }
435
436 pub fn with_rustybuzz_face<R>(
438 &self,
439 f: impl FnOnce(&rustybuzz::Face<'_>) -> R,
440 ) -> Result<R, FontError> {
441 Ok(f(self.rustybuzz_face()?))
442 }
443
444 pub fn metrics(&self, size: Length) -> Result<FontMetrics, FontError> {
446 let face = self.ttf_face()?;
447 let upem = units_per_em(face);
448 let angle = f64::from(face.italic_angle());
450 Ok(FontMetrics {
451 ascent: scale_font_units(i32::from(face.ascender()), size, upem),
452 descent: -scale_font_units(i32::from(face.descender()), size, upem),
453 xheight: scale_font_units(i32::from(face.x_height().unwrap_or(0)), size, upem),
454 capheight: scale_font_units(i32::from(face.capital_height().unwrap_or(0)), size, upem),
455 slant: d2fix((-angle).to_radians().tan()),
456 })
457 }
458
459 pub fn glyph_metrics(
461 &self,
462 glyph: GlyphId,
463 size: Length,
464 ) -> Result<FontGlyphMetrics, FontError> {
465 let face = self.ttf_face()?;
466 let upem = units_per_em(face);
467 let Some(glyph) = ttf_glyph(glyph) else {
468 return Ok(FontGlyphMetrics::default());
469 };
470 let width = face.glyph_hor_advance(glyph).map_or(0, |advance| {
471 scale_font_units(i32::from(advance), size, upem)
472 });
473 let (height, depth) = face.glyph_bounding_box(glyph).map_or((0, 0), |bbox| {
474 (
475 scale_font_units(i32::from(bbox.y_max).max(0), size, upem),
476 scale_font_units((-i32::from(bbox.y_min)).max(0), size, upem),
477 )
478 });
479 Ok(FontGlyphMetrics {
480 width,
481 height,
482 depth,
483 })
484 }
485
486 pub fn glyph_bounds_points(
488 &self,
489 glyph: GlyphId,
490 size: Length,
491 ) -> Result<GlyphBoundsPoints, FontError> {
492 let face = self.ttf_face()?;
493 let upem = f32::from(face.units_per_em().max(1));
495 let point_size = point_size(size);
496 let points = |units: f32| (units * point_size) / upem;
497 let Some(glyph) = ttf_glyph(glyph) else {
498 return Ok(GlyphBoundsPoints::default());
499 };
500 let advance = face
501 .glyph_hor_advance(glyph)
502 .map_or(0.0, |advance| points(f32::from(advance)));
503 Ok(face.glyph_bounding_box(glyph).map_or(
504 GlyphBoundsPoints {
505 advance,
506 ..GlyphBoundsPoints::default()
507 },
508 |bbox| GlyphBoundsPoints {
509 advance,
510 x_min: points(f32::from(bbox.x_min)),
511 y_min: points(f32::from(bbox.y_min)),
512 x_max: points(f32::from(bbox.x_max)),
513 y_max: points(f32::from(bbox.y_max)),
514 },
515 ))
516 }
517
518 pub fn char_code_range(&self) -> Result<Option<(u32, u32)>, FontError> {
520 let face = self.ttf_face()?;
521 let Some(cmap) = face.tables().cmap else {
522 return Ok(None);
523 };
524 let mut range: Option<(u32, u32)> = None;
525 for subtable in cmap.subtables {
526 if !subtable.is_unicode() {
527 continue;
528 }
529 subtable.codepoints(|codepoint| {
530 if subtable
531 .glyph_index(codepoint)
532 .is_some_and(|glyph| glyph.0 != 0)
533 {
534 range = Some(range.map_or((codepoint, codepoint), |(low, high)| {
535 (low.min(codepoint), high.max(codepoint))
536 }));
537 }
538 });
539 }
540 Ok(range)
541 }
542
543 pub fn glyph_outlines(
545 &self,
546 glyphs: &[GlyphId],
547 ) -> Result<Vec<Option<GlyphOutline>>, FontError> {
548 let face = self.ttf_face()?;
549 let units_per_em = face.units_per_em();
550 Ok(glyphs
551 .iter()
552 .map(|&glyph| {
553 let glyph = ttf_glyph(glyph)?;
554 let mut collector = OutlineCollector {
555 commands: Vec::new(),
556 };
557 face.outline_glyph(glyph, &mut collector)?;
558 Some(GlyphOutline {
559 units_per_em,
560 commands: collector.commands,
561 })
562 })
563 .collect())
564 }
565
566 pub fn glyph_index(&self, codepoint: char) -> Result<Option<GlyphId>, FontError> {
568 Ok(self
569 .ttf_face()?
570 .glyph_index(codepoint)
571 .map(|glyph| GlyphId(u32::from(glyph.0))))
572 }
573
574 pub fn glyph_index_by_name(&self, name: &str) -> Result<Option<GlyphId>, FontError> {
576 Ok(self
577 .ttf_face()?
578 .glyph_index_by_name(name)
579 .map(|glyph| GlyphId(u32::from(glyph.0))))
580 }
581
582 pub fn has_opentype_math(&self) -> Result<bool, FontError> {
584 Ok(self.ttf_face()?.tables().math.is_some())
585 }
586
587 pub fn ot_glyph_count(&self) -> Result<u32, FontError> {
589 Ok(u32::from(self.ttf_face()?.number_of_glyphs()))
590 }
591
592 pub fn ot_script_count(&self) -> Result<u32, FontError> {
594 Ok(larger_script_list(self.ttf_face()?).map_or(0, |scripts| u32::from(scripts.len())))
595 }
596
597 pub fn ot_script_tag(&self, index: u32) -> Result<u32, FontError> {
599 let Ok(index) = u16::try_from(index) else {
600 return Ok(0);
601 };
602 Ok(larger_script_list(self.ttf_face()?)
603 .and_then(|scripts| scripts.get(index))
604 .map_or(0, |script| script.tag.0))
605 }
606
607 pub fn ot_language_count(&self, script_tag: u32) -> Result<u32, FontError> {
609 let face = self.ttf_face()?;
610 let script_tag = ttf_parser::Tag(script_tag);
611 let mut count = 0u32;
612 for table in layout_tables(face) {
613 if let Some(script) = table
614 .scripts
615 .index(script_tag)
616 .and_then(|index| table.scripts.get(index))
617 {
618 count += u32::from(script.languages.len());
619 }
620 }
621 Ok(count)
622 }
623
624 pub fn ot_language_tag(&self, script_tag: u32, index: u32) -> Result<u32, FontError> {
626 let Ok(index) = u16::try_from(index) else {
627 return Ok(0);
628 };
629 let face = self.ttf_face()?;
630 let script_tag = ttf_parser::Tag(script_tag);
631 for table in layout_tables(face) {
632 if let Some(script) = table
633 .scripts
634 .index(script_tag)
635 .and_then(|script_index| table.scripts.get(script_index))
636 {
637 if index < script.languages.len() {
638 return Ok(script.languages.get(index).map_or(0, |lang| lang.tag.0));
639 }
640 }
641 }
642 Ok(0)
643 }
644
645 pub fn ot_feature_count(&self, script_tag: u32, language_tag: u32) -> Result<u32, FontError> {
647 let face = self.ttf_face()?;
648 let script_tag = ttf_parser::Tag(script_tag);
649 let mut count = 0u32;
650 for table in layout_tables(face) {
651 if let Some(langsys) = language_system(table, script_tag, language_tag) {
652 count += u32::from(langsys.feature_indices.len());
653 }
654 }
655 Ok(count)
656 }
657
658 pub fn ot_feature_tag(
660 &self,
661 script_tag: u32,
662 language_tag: u32,
663 index: u32,
664 ) -> Result<u32, FontError> {
665 let Ok(index) = u16::try_from(index) else {
666 return Ok(0);
667 };
668 let face = self.ttf_face()?;
669 let script_tag = ttf_parser::Tag(script_tag);
670 for table in layout_tables(face) {
671 if let Some(langsys) = language_system(table, script_tag, language_tag) {
672 if let Some(feature_index) = langsys.feature_indices.get(index) {
673 return Ok(table
674 .features
675 .get(feature_index)
676 .map_or(0, |feature| feature.tag.0));
677 }
678 }
679 }
680 Ok(0)
681 }
682
683 pub fn opentype_math_constant(&self, constant: i32, size: Length) -> Result<i32, FontError> {
685 let face = self.ttf_face()?;
686 let Some(constants) = face.tables().math.and_then(|table| table.constants) else {
687 return Ok(0);
688 };
689 let Some(value) = math_constant_value(constants, constant) else {
690 return Ok(0);
691 };
692 if is_math_constant_percentage(constant) {
693 Ok(value)
694 } else {
695 Ok(scale_font_units(value, size, units_per_em(face)))
696 }
697 }
698
699 pub fn math_italic_correction(&self, glyph: GlyphId, size: Length) -> Result<i32, FontError> {
701 let face = self.ttf_face()?;
702 let value = ttf_glyph(glyph).and_then(|glyph| {
703 face.tables()
704 .math?
705 .glyph_info?
706 .italic_corrections?
707 .get(glyph)
708 });
709 Ok(value.map_or(0, |value| {
710 scale_font_units(i32::from(value.value), size, units_per_em(face))
711 }))
712 }
713
714 pub fn math_kern_at(
716 &self,
717 glyph: GlyphId,
718 corner: MathKernCorner,
719 correction_height: i32,
720 size: Length,
721 ) -> Result<i32, FontError> {
722 let height = self.points_to_units((correction_height as f32) / 65536.0, size)? as i32;
723 let kern = self.math_kern_units(glyph, corner, height)?;
724 self.units_to_scaled(kern, size)
725 }
726
727 pub fn math_kern_units(
729 &self,
730 glyph: GlyphId,
731 corner: MathKernCorner,
732 correction_height: i32,
733 ) -> Result<i32, FontError> {
734 let face = self.ttf_face()?;
735 let Some(kern_info) = ttf_glyph(glyph)
736 .and_then(|glyph| face.tables().math?.glyph_info?.kern_infos?.get(glyph))
737 else {
738 return Ok(0);
739 };
740 let kern = match corner {
741 MathKernCorner::TopRight => kern_info.top_right,
742 MathKernCorner::TopLeft => kern_info.top_left,
743 MathKernCorner::BottomRight => kern_info.bottom_right,
744 MathKernCorner::BottomLeft => kern_info.bottom_left,
745 };
746 let Some(kern) = kern else {
747 return Ok(0);
748 };
749 let mut index = 0u16;
750 while index < kern.count() {
751 match kern.height(index) {
752 Some(height) if correction_height < i32::from(height.value) => break,
753 _ => index += 1,
754 }
755 }
756 Ok(kern.kern(index).map_or(0, |value| i32::from(value.value)))
757 }
758
759 pub fn math_variant(
761 &self,
762 glyph: GlyphId,
763 index: u16,
764 horizontal: bool,
765 size: Length,
766 ) -> Result<Option<MathVariant>, FontError> {
767 let face = self.ttf_face()?;
768 let variant = ttf_glyph(glyph).and_then(|glyph| {
769 math_constructions(face, horizontal)?
770 .get(glyph)?
771 .variants
772 .get(index)
773 });
774 Ok(variant.map(|variant| MathVariant {
775 glyph: GlyphId(u32::from(variant.variant_glyph.0)),
776 advance: scale_font_units(
777 i32::from(variant.advance_measurement),
778 size,
779 units_per_em(face),
780 ),
781 }))
782 }
783
784 pub fn math_assembly(
786 &self,
787 glyph: GlyphId,
788 horizontal: bool,
789 size: Length,
790 ) -> Result<Vec<MathAssemblyPart>, FontError> {
791 let face = self.ttf_face()?;
792 let upem = units_per_em(face);
793 let Some(assembly) = ttf_glyph(glyph)
794 .and_then(|glyph| math_constructions(face, horizontal)?.get(glyph)?.assembly)
795 else {
796 return Ok(Vec::new());
797 };
798 let scale = |units: u16| scale_font_units(i32::from(units), size, upem);
799 Ok(assembly
800 .parts
801 .into_iter()
802 .map(|part| MathAssemblyPart {
803 glyph: GlyphId(u32::from(part.glyph_id.0)),
804 start_connector: scale(part.start_connector_length),
805 end_connector: scale(part.end_connector_length),
806 full_advance: scale(part.full_advance),
807 extender: part.part_flags.extender(),
808 })
809 .collect())
810 }
811
812 pub fn math_min_connector_overlap(&self, size: Length) -> Result<i32, FontError> {
814 let face = self.ttf_face()?;
815 let overlap = face
816 .tables()
817 .math
818 .and_then(|table| table.variants)
819 .map_or(0, |variants| i32::from(variants.min_connector_overlap));
820 Ok(scale_font_units(overlap, size, units_per_em(face)))
821 }
822
823 pub fn points_to_units(&self, points: f32, size: Length) -> Result<f32, FontError> {
825 let upem = units_per_em(self.ttf_face()?);
826 let point_size = point_size(size);
827 if point_size == 0.0 {
828 return Ok(0.0);
829 }
830 Ok((points * upem as f32) / point_size)
831 }
832
833 pub fn units_to_scaled(&self, units: i32, size: Length) -> Result<i32, FontError> {
835 Ok(scale_font_units(
836 units,
837 size,
838 units_per_em(self.ttf_face()?),
839 ))
840 }
841
842 pub fn opentype_math_accent_position(
844 &self,
845 glyph: GlyphId,
846 size: Length,
847 ) -> Result<i32, FontError> {
848 let face = self.ttf_face()?;
849 let value = ttf_glyph(glyph).and_then(|glyph| {
850 face.tables()
851 .math?
852 .glyph_info?
853 .top_accent_attachments?
854 .get(glyph)
855 });
856 Ok(value.map_or(0, |value| {
857 scale_font_units(i32::from(value.value), size, units_per_em(face))
858 }))
859 }
860
861 pub fn math_symbol_parameter(&self, parameter: i32, size: Length) -> Result<i32, FontError> {
863 match parameter {
864 5 => self.opentype_math_constant(6, size),
865 6 => Ok(size.0),
866 8 => self.opentype_math_constant(33, size),
867 9 => self.opentype_math_constant(32, size),
868 10 => self.opentype_math_constant(22, size),
869 11 => self.opentype_math_constant(35, size),
870 12 => self.opentype_math_constant(34, size),
871 13 | 14 => self.opentype_math_constant(11, size),
872 15 => self.opentype_math_constant(12, size),
873 16 | 17 => self.opentype_math_constant(8, size),
874 18 => self.opentype_math_constant(14, size),
875 19 => self.opentype_math_constant(10, size),
876 20 => self.opentype_math_constant(2, size),
877 21 => {
878 let delim1 = self.math_symbol_parameter(20, size)?;
879 Ok(((i64::from(size.0) * 3) / 2)
880 .min(i64::from(delim1))
881 .clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32)
882 }
883 22 => self.opentype_math_constant(5, size),
884 _ => Ok(0),
885 }
886 }
887
888 pub fn math_extension_parameter(&self, parameter: i32, size: Length) -> Result<i32, FontError> {
890 match parameter {
891 5 => self.opentype_math_constant(6, size),
892 6 => Ok(size.0),
893 8 => self.opentype_math_constant(38, size),
894 9 => self.opentype_math_constant(18, size),
895 10 => self.opentype_math_constant(20, size),
896 11 => self.opentype_math_constant(19, size),
897 12 => self.opentype_math_constant(21, size),
898 13 => self.opentype_math_constant(26, size),
899 _ => Ok(0),
900 }
901 }
902
903 fn invalid(&self, message: String) -> FontError {
904 FontError::Invalid {
905 name: format!("font key {}", self.key.0),
906 message,
907 }
908 }
909
910 fn ttf_face(&self) -> Result<&ttf_parser::Face<'_>, FontError> {
911 match &self.source {
912 FaceSource::Shared(face) => Ok(face.ttf_face()),
913 FaceSource::Bytes { bytes, ttf, .. } => {
914 if ttf.get().is_none() {
915 let parsed = ParsedFace::try_new(Arc::clone(bytes), |bytes| {
916 ttf_parser::Face::parse(bytes, 0)
917 .map_err(|error| self.invalid(format!("invalid font data: {error}")))
918 })?;
919 let _ = ttf.set(parsed);
921 }
922 Ok(ttf
923 .get()
924 .expect("ttf cache populated above")
925 .borrow_dependent())
926 }
927 }
928 }
929
930 fn rustybuzz_face(&self) -> Result<&rustybuzz::Face<'_>, FontError> {
931 match &self.source {
932 FaceSource::Shared(face) => Ok(face.rustybuzz_face()),
933 FaceSource::Bytes {
934 bytes, rustybuzz, ..
935 } => {
936 if rustybuzz.get().is_none() {
937 let parsed = ParsedRustybuzzFace::try_new(Arc::clone(bytes), |bytes| {
938 rustybuzz::Face::from_slice(bytes, 0)
939 .ok_or_else(|| self.invalid("invalid font data".to_string()))
940 })?;
941 let _ = rustybuzz.set(parsed);
942 }
943 Ok(rustybuzz
944 .get()
945 .expect("rustybuzz cache populated above")
946 .borrow_dependent())
947 }
948 }
949 }
950}
951
952#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
954pub struct FontMetrics {
955 pub ascent: i32,
957 pub descent: i32,
959 pub xheight: i32,
961 pub capheight: i32,
963 pub slant: i32,
965}
966
967#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
969pub struct FontGlyphMetrics {
970 pub width: i32,
972 pub height: i32,
974 pub depth: i32,
976}
977
978#[derive(Clone, Copy, Debug, Default, PartialEq)]
980pub struct GlyphBoundsPoints {
981 pub advance: f32,
983 pub x_min: f32,
985 pub y_min: f32,
987 pub x_max: f32,
989 pub y_max: f32,
991}
992
993#[derive(Clone, Debug, PartialEq, Eq)]
995#[non_exhaustive]
996pub enum FontError {
997 NotFound {
999 name: String,
1001 },
1002 Invalid {
1004 name: String,
1006 message: String,
1008 },
1009}
1010
1011impl fmt::Display for FontError {
1012 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1013 match self {
1014 Self::NotFound { name } => write!(f, "font not found: {name}"),
1015 Self::Invalid { name, message } => write!(f, "font {name} is unusable: {message}"),
1016 }
1017 }
1018}
1019
1020impl std::error::Error for FontError {}
1021
1022type TtfFace<'a> = ttf_parser::Face<'a>;
1024
1025self_cell::self_cell!(
1026 struct ParsedFace {
1028 owner: Arc<[u8]>,
1029 #[covariant]
1030 dependent: TtfFace,
1031 }
1032);
1033
1034type RbFace<'a> = rustybuzz::Face<'a>;
1036
1037self_cell::self_cell!(
1038 struct ParsedRustybuzzFace {
1040 owner: Arc<[u8]>,
1041 #[covariant]
1042 dependent: RbFace,
1043 }
1044);
1045
1046fn units_per_em(face: &ttf_parser::Face<'_>) -> i32 {
1047 i32::from(face.units_per_em()).max(1)
1048}
1049
1050fn ttf_glyph(glyph: GlyphId) -> Option<ttf_parser::GlyphId> {
1051 u16::try_from(glyph.0).ok().map(ttf_parser::GlyphId)
1052}
1053
1054fn point_size(size: Length) -> f32 {
1056 (f64::from(size.0) / 65536.0) as f32
1057}
1058
1059fn d2fix(value: f64) -> i32 {
1061 (value * 65536.0 + 0.5)
1062 .trunc()
1063 .clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32
1064}
1065
1066fn scale_font_units(value: i32, size: Length, units_per_em: i32) -> i32 {
1068 let points = (value as f32 * point_size(size)) / (units_per_em.max(1) as f32);
1069 d2fix(f64::from(points))
1070}
1071
1072fn layout_tables<'a>(
1073 face: &ttf_parser::Face<'a>,
1074) -> impl Iterator<Item = ttf_parser::opentype_layout::LayoutTable<'a>> {
1075 [face.tables().gsub, face.tables().gpos]
1076 .into_iter()
1077 .flatten()
1078}
1079
1080fn larger_script_list<'a>(
1082 face: &ttf_parser::Face<'a>,
1083) -> Option<ttf_parser::opentype_layout::ScriptList<'a>> {
1084 let gsub = face.tables().gsub.map(|table| table.scripts);
1085 let gpos = face.tables().gpos.map(|table| table.scripts);
1086 match (gsub, gpos) {
1087 (Some(sub), Some(pos)) => Some(if pos.len() > sub.len() { pos } else { sub }),
1088 (sub, pos) => sub.or(pos),
1089 }
1090}
1091
1092fn language_system<'a>(
1094 table: ttf_parser::opentype_layout::LayoutTable<'a>,
1095 script_tag: ttf_parser::Tag,
1096 language_tag: u32,
1097) -> Option<ttf_parser::opentype_layout::LanguageSystem<'a>> {
1098 let script = table
1099 .scripts
1100 .index(script_tag)
1101 .and_then(|index| table.scripts.get(index))?;
1102 if language_tag == 0 {
1103 script.default_language
1104 } else {
1105 script
1106 .languages
1107 .index(ttf_parser::Tag(language_tag))
1108 .and_then(|index| script.languages.get(index))
1109 .or(script.default_language)
1110 }
1111}
1112
1113fn math_constructions<'a>(
1114 face: &ttf_parser::Face<'a>,
1115 horizontal: bool,
1116) -> Option<ttf_parser::math::GlyphConstructions<'a>> {
1117 let variants = face.tables().math?.variants?;
1118 Some(if horizontal {
1119 variants.horizontal_constructions
1120 } else {
1121 variants.vertical_constructions
1122 })
1123}
1124
1125struct OutlineCollector {
1127 commands: Vec<OutlineCommand>,
1128}
1129
1130impl ttf_parser::OutlineBuilder for OutlineCollector {
1131 fn move_to(&mut self, x: f32, y: f32) {
1132 self.commands.push(OutlineCommand::MoveTo { x, y });
1133 }
1134
1135 fn line_to(&mut self, x: f32, y: f32) {
1136 self.commands.push(OutlineCommand::LineTo { x, y });
1137 }
1138
1139 fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
1140 self.commands.push(OutlineCommand::QuadTo { cx, cy, x, y });
1141 }
1142
1143 fn curve_to(&mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) {
1144 self.commands.push(OutlineCommand::CurveTo {
1145 c1x,
1146 c1y,
1147 c2x,
1148 c2y,
1149 x,
1150 y,
1151 });
1152 }
1153
1154 fn close(&mut self) {
1155 self.commands.push(OutlineCommand::Close);
1156 }
1157}
1158
1159fn math_constant_value(constants: ttf_parser::math::Constants<'_>, constant: i32) -> Option<i32> {
1160 let value = match constant {
1161 0 => i32::from(constants.script_percent_scale_down()),
1162 1 => i32::from(constants.script_script_percent_scale_down()),
1163 2 => i32::from(constants.delimited_sub_formula_min_height()),
1164 3 => i32::from(constants.display_operator_min_height()),
1165 4 => i32::from(constants.math_leading().value),
1166 5 => i32::from(constants.axis_height().value),
1167 6 => i32::from(constants.accent_base_height().value),
1168 7 => i32::from(constants.flattened_accent_base_height().value),
1169 8 => i32::from(constants.subscript_shift_down().value),
1170 9 => i32::from(constants.subscript_top_max().value),
1171 10 => i32::from(constants.subscript_baseline_drop_min().value),
1172 11 => i32::from(constants.superscript_shift_up().value),
1173 12 => i32::from(constants.superscript_shift_up_cramped().value),
1174 13 => i32::from(constants.superscript_bottom_min().value),
1175 14 => i32::from(constants.superscript_baseline_drop_max().value),
1176 15 => i32::from(constants.sub_superscript_gap_min().value),
1177 16 => i32::from(constants.superscript_bottom_max_with_subscript().value),
1178 17 => i32::from(constants.space_after_script().value),
1179 18 => i32::from(constants.upper_limit_gap_min().value),
1180 19 => i32::from(constants.upper_limit_baseline_rise_min().value),
1181 20 => i32::from(constants.lower_limit_gap_min().value),
1182 21 => i32::from(constants.lower_limit_baseline_drop_min().value),
1183 22 => i32::from(constants.stack_top_shift_up().value),
1184 23 => i32::from(constants.stack_top_display_style_shift_up().value),
1185 24 => i32::from(constants.stack_bottom_shift_down().value),
1186 25 => i32::from(constants.stack_bottom_display_style_shift_down().value),
1187 26 => i32::from(constants.stack_gap_min().value),
1188 27 => i32::from(constants.stack_display_style_gap_min().value),
1189 28 => i32::from(constants.stretch_stack_top_shift_up().value),
1190 29 => i32::from(constants.stretch_stack_bottom_shift_down().value),
1191 30 => i32::from(constants.stretch_stack_gap_above_min().value),
1192 31 => i32::from(constants.stretch_stack_gap_below_min().value),
1193 32 => i32::from(constants.fraction_numerator_shift_up().value),
1194 33 => i32::from(constants.fraction_numerator_display_style_shift_up().value),
1195 34 => i32::from(constants.fraction_denominator_shift_down().value),
1196 35 => i32::from(
1197 constants
1198 .fraction_denominator_display_style_shift_down()
1199 .value,
1200 ),
1201 36 => i32::from(constants.fraction_numerator_gap_min().value),
1202 37 => i32::from(constants.fraction_num_display_style_gap_min().value),
1203 38 => i32::from(constants.fraction_rule_thickness().value),
1204 39 => i32::from(constants.fraction_denominator_gap_min().value),
1205 40 => i32::from(constants.fraction_denom_display_style_gap_min().value),
1206 41 => i32::from(constants.skewed_fraction_horizontal_gap().value),
1207 42 => i32::from(constants.skewed_fraction_vertical_gap().value),
1208 43 => i32::from(constants.overbar_vertical_gap().value),
1209 44 => i32::from(constants.overbar_rule_thickness().value),
1210 45 => i32::from(constants.overbar_extra_ascender().value),
1211 46 => i32::from(constants.underbar_vertical_gap().value),
1212 47 => i32::from(constants.underbar_rule_thickness().value),
1213 48 => i32::from(constants.underbar_extra_descender().value),
1214 49 => i32::from(constants.radical_vertical_gap().value),
1215 50 => i32::from(constants.radical_display_style_vertical_gap().value),
1216 51 => i32::from(constants.radical_rule_thickness().value),
1217 52 => i32::from(constants.radical_extra_ascender().value),
1218 53 => i32::from(constants.radical_kern_before_degree().value),
1219 54 => i32::from(constants.radical_kern_after_degree().value),
1220 55 => i32::from(constants.radical_degree_bottom_raise_percent()),
1221 _ => return None,
1222 };
1223 Some(value)
1224}
1225
1226fn is_math_constant_percentage(constant: i32) -> bool {
1227 matches!(constant, 0 | 1 | 55)
1228}
1229
1230#[cfg(test)]
1231mod tests {
1232 use super::*;
1233 use std::path::Path;
1234
1235 const PT: i32 = 65_536;
1236 const LM_MATH: &str = "fonts/opentype/public/lm-math/latinmodern-math.otf";
1237 const LM_ITALIC: &str = "fonts/opentype/public/lm/lmroman10-italic.otf";
1238 const STIX_MATH: &str = "fonts/opentype/public/stix2-otf/STIXTwoMath-Regular.otf";
1239
1240 fn texlive_font(relative: &str) -> Option<FontData> {
1242 let Some(root) = std::env::var_os("MATHTEX_TEXMF_ROOT") else {
1243 assert!(
1244 std::env::var("MATHTEX_REQUIRE_TEXLIVE").as_deref() != Ok("1"),
1245 "MATHTEX_REQUIRE_TEXLIVE=1 but MATHTEX_TEXMF_ROOT is not set"
1246 );
1247 eprintln!("skipping: MATHTEX_TEXMF_ROOT is not set");
1248 return None;
1249 };
1250 let path = Path::new(&root).join(relative);
1251 let bytes = std::fs::read(&path).unwrap_or_else(|error| {
1252 panic!(
1253 "MATHTEX_TEXMF_ROOT is set but {} is unreadable: {error}",
1254 path.display()
1255 )
1256 });
1257 Some(FontData::new(FontKey(1), bytes))
1258 }
1259
1260 fn fixture_font(relative: &str) -> Option<FontData> {
1262 mathtex_test_fixtures::read(relative).map(|bytes| FontData::new(FontKey(2), bytes))
1263 }
1264
1265 fn ten_pt() -> Length {
1266 Length(10 * PT)
1267 }
1268
1269 #[test]
1270 fn spec_parses_files_features_and_suffixes() {
1271 let spec = FontSpec::parse(
1272 "[latinmodern-math.otf]:script=math;+ssty=0;-liga,language=DEU;mapping=tex-text;kern=2",
1273 ten_pt(),
1274 );
1275 assert!(spec.is_file());
1276 assert_eq!(spec.name(), "latinmodern-math.otf");
1277 assert_eq!(spec.script(), Some(*b"math"));
1278 assert_eq!(spec.language(), Some("DEU"));
1279 assert_eq!(spec.option("mapping"), Some("tex-text"));
1280 assert_eq!(
1281 spec.features(),
1282 [
1283 ShapeFeature {
1284 tag: *b"ssty",
1285 value: 1
1286 },
1287 ShapeFeature {
1288 tag: *b"liga",
1289 value: 0
1290 },
1291 ShapeFeature {
1292 tag: *b"kern",
1293 value: 2
1294 },
1295 ]
1296 );
1297 assert_eq!(spec.file_candidates(), ["latinmodern-math.otf"]);
1298
1299 let spec = FontSpec::parse("Latin Modern Roman/B/OT:+smcp;vertical;bogus", ten_pt());
1300 assert!(!spec.is_file());
1301 assert_eq!(spec.name(), "Latin Modern Roman");
1302 assert_eq!(spec.variants(), ["B", "OT"]);
1303 assert_eq!(
1304 spec.features(),
1305 [ShapeFeature {
1306 tag: *b"smcp",
1307 value: 1
1308 }]
1309 );
1310 assert!(spec.vertical());
1311 assert_eq!(spec.unknown_options(), ["bogus"]);
1312
1313 let spec = FontSpec::parse("[fonts/collection.ttc:2]/AAT:color=FF0000", ten_pt());
1314 assert_eq!(spec.name(), "fonts/collection.ttc");
1315 assert_eq!(spec.face_index(), 2);
1316 assert_eq!(spec.variants(), ["AAT"]);
1317 assert_eq!(spec.option("color"), Some("FF0000"));
1318 assert_eq!(spec.size(), ten_pt());
1319
1320 let spec = FontSpec::parse("lmroman10-regular:+tlig=-1", ten_pt());
1321 assert_eq!(
1322 spec.file_candidates(),
1323 [
1324 "lmroman10-regular",
1325 "lmroman10-regular.otf",
1326 "lmroman10-regular.ttf"
1327 ]
1328 );
1329 assert_eq!(spec.features()[0].value, u32::MAX);
1330 }
1331
1332 #[test]
1333 fn in_memory_loader_resolves_candidates_and_never_reuses_keys() {
1334 let mut fonts = InMemoryFontLoader::new();
1335 let first = fonts.insert("a.otf", b"one".to_vec());
1336 let second = fonts.insert("b.otf", b"two".to_vec());
1337 let replaced = fonts.insert("a.otf", b"three".to_vec());
1338 assert_ne!(first, second);
1339 assert_ne!(replaced, first);
1340 assert_ne!(replaced, second);
1341
1342 let font = fonts.load(&FontSpec::parse("a", ten_pt())).expect("a.otf");
1343 assert_eq!(font.key, replaced);
1344 assert_eq!(&**font.bytes().expect("owned bytes"), b"three");
1345 assert_eq!(
1346 fonts.load(&FontSpec::parse("[missing.otf]", ten_pt())),
1347 Err(FontError::NotFound {
1348 name: "missing.otf".into()
1349 })
1350 );
1351 }
1352
1353 #[test]
1354 fn scale_font_units_matches_xetex_d2fix_rounding() {
1355 assert_eq!(scale_font_units(666, ten_pt(), 1000), 436_470);
1357 assert_eq!(scale_font_units(431, ten_pt(), 1000), 282_460);
1358 assert_eq!(scale_font_units(528, ten_pt(), 1000), 346_030);
1359 assert_eq!(scale_font_units(16, ten_pt(), 1000), 10_486);
1360 assert_eq!(scale_font_units(-16, ten_pt(), 1000), -10_485);
1362 assert_eq!(scale_font_units(0, ten_pt(), 1000), 0);
1363 }
1364
1365 #[test]
1366 fn font_data_clones_share_both_parsed_faces() {
1367 let Some(font) = fixture_font(mathtex_test_fixtures::DEJAVU_SANS) else {
1368 return;
1369 };
1370 let clone = font.clone();
1371 let ttf = |font: &FontData| {
1372 font.with_ttf_face(|face| face as *const ttf_parser::Face<'_> as usize)
1373 .expect("ttf parse")
1374 };
1375 let rb = |font: &FontData| {
1376 font.with_rustybuzz_face(|face| face as *const rustybuzz::Face<'_> as usize)
1377 .expect("rustybuzz parse")
1378 };
1379 assert_eq!(ttf(&font), ttf(&clone));
1380 assert_eq!(rb(&font), rb(&clone));
1381 assert!(Arc::ptr_eq(
1382 font.bytes().expect("owned bytes"),
1383 clone.bytes().expect("owned bytes")
1384 ));
1385 }
1386
1387 self_cell::self_cell!(
1388 struct AppOwnedFace {
1389 owner: Arc<[u8]>,
1390 #[covariant]
1391 dependent: RbFace,
1392 }
1393 );
1394
1395 struct AppShared(AppOwnedFace);
1396
1397 impl SharedFace for AppShared {
1398 fn rustybuzz_face(&self) -> &rustybuzz::Face<'_> {
1399 self.0.borrow_dependent()
1400 }
1401 }
1402
1403 #[test]
1404 fn host_owned_face_is_borrowed_and_never_parsed_by_the_library() {
1405 let Some(owned) = fixture_font(mathtex_test_fixtures::LM_MONO) else {
1406 return;
1407 };
1408 let bytes = Arc::clone(owned.bytes().expect("owned bytes"));
1409 let app_face = AppOwnedFace::try_new(bytes, |bytes| {
1410 rustybuzz::Face::from_slice(bytes, 0).ok_or("host parse failed")
1411 })
1412 .expect("host parses its own font");
1413 let app_face: Arc<dyn SharedFace> = Arc::new(AppShared(app_face));
1414 let app_ptr = app_face.rustybuzz_face() as *const rustybuzz::Face<'_> as usize;
1415 let font = FontData::from_shared_face(FontKey(3), Arc::clone(&app_face));
1416
1417 assert!(font.bytes().is_none());
1418 let lib_ptr = font
1419 .with_rustybuzz_face(|face| face as *const rustybuzz::Face<'_> as usize)
1420 .expect("borrow shared face");
1421 assert_eq!(lib_ptr, app_ptr);
1422 assert!(font.metrics(ten_pt()).expect("metrics").ascent > 0);
1423 }
1424
1425 #[test]
1426 fn font_data_is_send_and_sync() {
1427 fn assert_send_sync<T: Send + Sync>() {}
1428 assert_send_sync::<FontData>();
1429 }
1430
1431 #[test]
1432 fn metrics_slant_is_the_tangent_of_the_italic_angle_and_descent_is_positive() {
1433 let (Some(italic), Some(math)) = (texlive_font(LM_ITALIC), texlive_font(LM_MATH)) else {
1434 return;
1435 };
1436 assert_eq!(italic.metrics(ten_pt()).expect("metrics").slant, PT / 4);
1438 let metrics = math.metrics(ten_pt()).expect("metrics");
1439 assert_eq!(metrics.slant, 0);
1440 assert!(metrics.descent > 0, "descent {}", metrics.descent);
1441 }
1442
1443 #[test]
1444 fn glyph_outlines_extract_design_unit_contours_from_cff_and_truetype() {
1445 for (file, units_per_em) in [
1446 (mathtex_test_fixtures::LM_MONO, 1000),
1447 (mathtex_test_fixtures::DEJAVU_SANS, 2048),
1448 ] {
1449 let Some(font) = fixture_font(file) else {
1450 return;
1451 };
1452 let x = font.glyph_index('x').unwrap().unwrap();
1453 let space = font.glyph_index(' ').unwrap().unwrap();
1454 let outlines = font.glyph_outlines(&[x, space]).unwrap();
1455 let x_outline = outlines[0].as_ref().expect("x has an outline");
1456 assert_eq!(x_outline.units_per_em, units_per_em, "{file}");
1457 assert!(matches!(
1458 x_outline.commands[0],
1459 OutlineCommand::MoveTo { .. }
1460 ));
1461 assert!(outlines[1].is_none(), "space has no outline in {file}");
1462 let o = font.glyph_index('o').unwrap().unwrap();
1464 let o_outline = font.glyph_outlines(&[o]).unwrap().remove(0).expect("o");
1465 let cubic =
1466 |command: &OutlineCommand| matches!(command, OutlineCommand::CurveTo { .. });
1467 let quadratic =
1468 |command: &OutlineCommand| matches!(command, OutlineCommand::QuadTo { .. });
1469 let cff = file == mathtex_test_fixtures::LM_MONO;
1470 assert_eq!(o_outline.commands.iter().any(cubic), cff, "{file}");
1471 assert_eq!(o_outline.commands.iter().any(quadratic), !cff, "{file}");
1472 }
1473 }
1474
1475 #[test]
1476 fn math_variant_returns_larger_paren_glyphs_from_latinmodern() {
1477 let Some(font) = texlive_font(LM_MATH) else {
1478 return;
1479 };
1480 let paren = font.glyph_index('(').unwrap().unwrap();
1482 assert_eq!(paren, GlyphId(9));
1483 assert_eq!(
1484 font.math_variant(paren, 4, false, ten_pt()).unwrap(),
1485 Some(MathVariant {
1486 glyph: GlyphId(2433),
1487 advance: 1_175_061
1488 })
1489 );
1490 assert_eq!(
1491 font.math_variant(paren, 0, false, ten_pt()).unwrap(),
1492 Some(MathVariant {
1493 glyph: GlyphId(9),
1494 advance: 653_394
1495 })
1496 );
1497 assert_eq!(font.math_variant(paren, 99, false, ten_pt()).unwrap(), None);
1498 }
1499
1500 #[test]
1501 fn math_assembly_returns_paren_parts_from_latinmodern() {
1502 let Some(font) = texlive_font(LM_MATH) else {
1503 return;
1504 };
1505 let paren = font.glyph_index('(').unwrap().unwrap();
1506 let parts = font.math_assembly(paren, false, ten_pt()).unwrap();
1507 let part =
1508 |glyph, start_connector, end_connector, full_advance, extender| MathAssemblyPart {
1509 glyph: GlyphId(glyph),
1510 start_connector,
1511 end_connector,
1512 full_advance,
1513 extender,
1514 };
1515 assert_eq!(
1517 parts,
1518 [
1519 part(2503, 0, 163_185, 979_763, false),
1520 part(2504, 326_369, 326_369, 326_369, true),
1521 part(2505, 163_185, 0, 979_763, false),
1522 ]
1523 );
1524 assert_eq!(font.math_min_connector_overlap(ten_pt()).unwrap(), 13_107);
1526 }
1527
1528 #[test]
1529 fn math_kern_reads_stix_cut_ins_in_units_and_scaled_points() {
1530 let Some(font) = texlive_font(STIX_MATH) else {
1531 return;
1532 };
1533 let f = font.glyph_index('F').unwrap().unwrap();
1535 assert_eq!(
1536 font.math_kern_units(f, MathKernCorner::TopRight, 0)
1537 .unwrap(),
1538 44
1539 );
1540 assert_eq!(
1541 font.math_kern_units(f, MathKernCorner::TopRight, 100_000)
1542 .unwrap(),
1543 44
1544 );
1545 let v = font.glyph_index('V').unwrap().unwrap();
1547 let bottom_right = |height| {
1548 font.math_kern_units(v, MathKernCorner::BottomRight, height)
1549 .unwrap()
1550 };
1551 assert_eq!(bottom_right(0), -193);
1552 assert_eq!(bottom_right(200), -119);
1553 assert_eq!(bottom_right(300), 56);
1554 assert_eq!(
1555 font.math_kern_units(v, MathKernCorner::TopRight, 0)
1556 .unwrap(),
1557 0
1558 );
1559 let scaled = font
1561 .math_kern_at(v, MathKernCorner::BottomRight, 2 * PT, ten_pt())
1562 .unwrap();
1563 assert_eq!(scaled, scale_font_units(-119, ten_pt(), 1000));
1564 }
1565
1566 #[test]
1567 fn latinmodern_has_no_math_kern_info() {
1568 let Some(font) = texlive_font(LM_MATH) else {
1569 return;
1570 };
1571 let x = font.glyph_index('x').unwrap().unwrap();
1572 for corner in [
1573 MathKernCorner::TopRight,
1574 MathKernCorner::TopLeft,
1575 MathKernCorner::BottomRight,
1576 MathKernCorner::BottomLeft,
1577 ] {
1578 assert_eq!(font.math_kern_units(x, corner, 0).unwrap(), 0);
1579 }
1580 }
1581}