1#![cfg_attr(not(feature = "std"), no_std)]
3#![forbid(unsafe_code)]
4
5extern crate alloc;
6
7use core::cell::RefCell;
8
9use alloc::collections::BTreeMap;
10use alloc::format;
11use alloc::string::{String, ToString};
12use alloc::sync::Arc;
13use alloc::vec::Vec;
14
15use mathtex_ir::{
16 ByteSpan, Direction, FontId, GlyphId, GlyphOutline, Length, OutlineCommand, Point,
17 PositionedGlyph,
18};
19
20pub use rustybuzz;
22pub use ttf_parser;
23
24pub trait FontSystem {
26 fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError>;
28
29 fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError>;
31}
32
33pub trait FontLoader {
35 fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError>;
37}
38
39pub trait TextShaper {
41 fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError>;
43}
44
45impl<T> FontSystem for &T
46where
47 T: FontSystem,
48{
49 fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
50 (*self).load_font(query)
51 }
52
53 fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
54 (*self).shape_text(request)
55 }
56}
57
58impl<T> FontLoader for &T
59where
60 T: FontLoader,
61{
62 fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
63 (*self).load_font(query)
64 }
65}
66
67impl<T> TextShaper for &T
68where
69 T: TextShaper,
70{
71 fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
72 (*self).shape_text(request)
73 }
74}
75
76#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct ComposedFontSystem<L, S> {
79 loader: L,
80 shaper: S,
81}
82
83impl<L, S> ComposedFontSystem<L, S> {
84 #[must_use]
86 pub fn new(loader: L, shaper: S) -> Self {
87 Self { loader, shaper }
88 }
89
90 #[must_use]
92 pub fn loader(&self) -> &L {
93 &self.loader
94 }
95
96 #[must_use]
98 pub fn shaper(&self) -> &S {
99 &self.shaper
100 }
101
102 #[must_use]
104 pub fn into_parts(self) -> (L, S) {
105 (self.loader, self.shaper)
106 }
107}
108
109impl<L, S> FontSystem for ComposedFontSystem<L, S>
110where
111 L: FontLoader,
112 S: TextShaper,
113{
114 fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
115 self.loader.load_font(query)
116 }
117
118 fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
119 self.shaper.shape_text(request)
120 }
121}
122
123#[derive(Debug)]
125pub struct RustybuzzFontSystem<L> {
126 loader: L,
127 loaded_fonts: RefCell<BTreeMap<u32, CachedFont>>,
128}
129
130#[derive(Clone, Debug, PartialEq, Eq)]
131struct CachedFont {
132 data: FontData,
133 size: Length,
134}
135
136impl<L> RustybuzzFontSystem<L> {
137 #[must_use]
139 pub fn new(loader: L) -> Self {
140 Self {
141 loader,
142 loaded_fonts: RefCell::new(BTreeMap::new()),
143 }
144 }
145
146 #[must_use]
148 pub fn loader(&self) -> &L {
149 &self.loader
150 }
151
152 #[must_use]
154 pub fn into_loader(self) -> L {
155 self.loader
156 }
157}
158
159impl<L> FontSystem for RustybuzzFontSystem<L>
160where
161 L: FontLoader,
162{
163 fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
164 let font = self.loader.load_font(query)?;
165 validate_font(&font)?;
166 self.loaded_fonts.borrow_mut().insert(
167 font.id.0,
168 CachedFont {
169 data: font.clone(),
170 size: query.size,
171 },
172 );
173 Ok(font)
174 }
175
176 fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
177 let cached = self
178 .loaded_fonts
179 .borrow()
180 .get(&request.font.0)
181 .cloned()
182 .ok_or_else(|| FontError::NotFound {
183 family: format!("font id {}", request.font.0),
184 })?;
185 shape_with_rustybuzz(&cached, request)
186 }
187}
188
189impl<L> FontLoader for RustybuzzFontSystem<L>
190where
191 L: FontLoader,
192{
193 fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
194 FontSystem::load_font(self, query)
195 }
196}
197
198impl<L> TextShaper for RustybuzzFontSystem<L>
199where
200 L: FontLoader,
201{
202 fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
203 FontSystem::shape_text(self, request)
204 }
205}
206
207#[derive(Clone, Debug, Default, PartialEq, Eq)]
209pub struct FontQuery {
210 pub family: String,
212 pub size: Length,
214 pub math: bool,
216}
217
218#[derive(Clone, Copy, Debug, PartialEq, Eq)]
220pub enum MathKernCorner {
221 TopRight,
223 TopLeft,
225 BottomRight,
227 BottomLeft,
229}
230
231#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub struct MathAssemblyPart {
234 pub glyph: u32,
236 pub start_connector: i32,
238 pub end_connector: i32,
240 pub full_advance: i32,
242 pub extender: bool,
244}
245
246pub trait SharedFace: Send + Sync {
248 fn rustybuzz_face(&self) -> &rustybuzz::Face<'_>;
250
251 fn ttf_face(&self) -> &ttf_parser::Face<'_> {
253 self.rustybuzz_face()
254 }
255}
256
257enum FaceSource {
259 Bytes {
260 bytes: Arc<[u8]>,
261 ttf: Arc<once_cell::race::OnceBox<ParsedFace>>,
263 rustybuzz: Arc<once_cell::race::OnceBox<ParsedRustybuzzFace>>,
264 },
265 Shared(Arc<dyn SharedFace>),
266}
267
268impl Clone for FaceSource {
269 fn clone(&self) -> Self {
270 match self {
271 Self::Bytes {
272 bytes,
273 ttf,
274 rustybuzz,
275 } => Self::Bytes {
276 bytes: Arc::clone(bytes),
277 ttf: Arc::clone(ttf),
278 rustybuzz: Arc::clone(rustybuzz),
279 },
280 Self::Shared(face) => Self::Shared(Arc::clone(face)),
281 }
282 }
283}
284
285pub struct FontData {
287 pub id: FontId,
289 pub canonical_name: String,
291 source: FaceSource,
292}
293
294impl Clone for FontData {
295 fn clone(&self) -> Self {
296 Self {
297 id: self.id,
298 canonical_name: self.canonical_name.clone(),
299 source: self.source.clone(),
300 }
301 }
302}
303
304impl PartialEq for FontData {
305 fn eq(&self, other: &Self) -> bool {
306 let sources_equal = match (&self.source, &other.source) {
307 (FaceSource::Bytes { bytes: a, .. }, FaceSource::Bytes { bytes: b, .. }) => a == b,
308 (FaceSource::Shared(a), FaceSource::Shared(b)) => Arc::ptr_eq(a, b),
309 _ => false,
310 };
311 self.id == other.id && self.canonical_name == other.canonical_name && sources_equal
312 }
313}
314
315impl Eq for FontData {}
316
317impl core::fmt::Debug for FontData {
318 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
319 f.debug_struct("FontData")
320 .field("id", &self.id)
321 .field("canonical_name", &self.canonical_name)
322 .finish_non_exhaustive()
323 }
324}
325
326impl FontData {
327 #[must_use]
329 pub fn new(id: FontId, canonical_name: impl Into<String>, bytes: impl Into<Arc<[u8]>>) -> Self {
330 Self {
331 id,
332 canonical_name: canonical_name.into(),
333 source: FaceSource::Bytes {
334 bytes: bytes.into(),
335 ttf: Arc::new(once_cell::race::OnceBox::new()),
336 rustybuzz: Arc::new(once_cell::race::OnceBox::new()),
337 },
338 }
339 }
340
341 #[must_use]
343 pub fn from_shared_face(
344 id: FontId,
345 canonical_name: impl Into<String>,
346 face: Arc<dyn SharedFace>,
347 ) -> Self {
348 Self {
349 id,
350 canonical_name: canonical_name.into(),
351 source: FaceSource::Shared(face),
352 }
353 }
354
355 #[must_use]
357 pub fn bytes(&self) -> Option<&Arc<[u8]>> {
358 match &self.source {
359 FaceSource::Bytes { bytes, .. } => Some(bytes),
360 FaceSource::Shared(_) => None,
361 }
362 }
363
364 pub fn with_ttf_face<R>(
366 &self,
367 f: impl FnOnce(&ttf_parser::Face<'_>) -> R,
368 ) -> Result<R, FontError> {
369 Ok(f(parse_face(self)?))
370 }
371
372 pub fn with_rustybuzz_face<R>(
374 &self,
375 f: impl FnOnce(&rustybuzz::Face<'_>) -> R,
376 ) -> Result<R, FontError> {
377 Ok(f(parse_rustybuzz_face(self)?))
378 }
379
380 pub fn metrics(&self, size: Length) -> Result<FontMetrics, FontError> {
382 let face = parse_face(self)?;
383 let units_per_em = i32::from(face.units_per_em()).max(1);
384 Ok(FontMetrics {
385 ascent: scale_font_units(i32::from(face.ascender()), size, units_per_em),
386 descent: scale_font_units(i32::from(face.descender()), size, units_per_em),
387 xheight: scale_font_units(i32::from(face.x_height().unwrap_or(0)), size, units_per_em),
388 capheight: scale_font_units(
389 i32::from(face.capital_height().unwrap_or(0)),
390 size,
391 units_per_em,
392 ),
393 slant: (face.italic_angle() * 65_536.0) as i32,
394 })
395 }
396
397 pub fn glyph_metrics(
399 &self,
400 glyph: GlyphId,
401 size: Length,
402 ) -> Result<FontGlyphMetrics, FontError> {
403 let face = parse_face(self)?;
404 let units_per_em = i32::from(face.units_per_em()).max(1);
405 let Ok(glyph_id) = u16::try_from(glyph.0) else {
406 return Ok(FontGlyphMetrics::default());
407 };
408 let glyph_id = ttf_parser::GlyphId(glyph_id);
409 let width = face
410 .glyph_hor_advance(glyph_id)
411 .map(|advance| scale_font_units(i32::from(advance), size, units_per_em))
412 .unwrap_or(0);
413 let (height, depth) = face
414 .glyph_bounding_box(glyph_id)
415 .map(|bbox| {
416 (
417 scale_font_units(i32::from(bbox.y_max).max(0), size, units_per_em),
418 scale_font_units((-i32::from(bbox.y_min)).max(0), size, units_per_em),
419 )
420 })
421 .unwrap_or((0, 0));
422 Ok(FontGlyphMetrics {
423 width,
424 height,
425 depth,
426 })
427 }
428
429 pub fn glyph_outlines(
431 &self,
432 glyphs: &[GlyphId],
433 ) -> Result<Vec<Option<GlyphOutline>>, FontError> {
434 let face = parse_face(self)?;
435 let units_per_em = face.units_per_em();
436 let mut out = Vec::with_capacity(glyphs.len());
437 for glyph in glyphs.iter().copied() {
438 let Ok(glyph_id) = u16::try_from(glyph.0) else {
439 out.push(None);
440 continue;
441 };
442 let mut collector = OutlineCollector {
443 commands: Vec::new(),
444 };
445 if face
447 .outline_glyph(ttf_parser::GlyphId(glyph_id), &mut collector)
448 .is_none()
449 {
450 out.push(None);
451 } else {
452 out.push(Some(GlyphOutline {
453 units_per_em,
454 commands: collector.commands,
455 }));
456 }
457 }
458 Ok(out)
459 }
460
461 pub fn glyph_index(&self, codepoint: char) -> Result<Option<GlyphId>, FontError> {
463 let face = parse_face(self)?;
464 Ok(face
465 .glyph_index(codepoint)
466 .map(|glyph| GlyphId(u32::from(glyph.0))))
467 }
468
469 pub fn glyph_index_by_name(&self, name: &str) -> Result<Option<GlyphId>, FontError> {
471 let face = parse_face(self)?;
472 Ok(face
473 .glyph_index_by_name(name)
474 .map(|glyph| GlyphId(u32::from(glyph.0))))
475 }
476
477 pub fn has_opentype_math(&self) -> Result<bool, FontError> {
479 Ok(parse_face(self)?.tables().math.is_some())
480 }
481
482 pub fn ot_glyph_count(&self) -> Result<u32, FontError> {
484 Ok(u32::from(parse_face(self)?.number_of_glyphs()))
485 }
486
487 pub fn ot_script_count(&self) -> Result<u32, FontError> {
489 let face = parse_face(self)?;
490 Ok(larger_script_list(face).map_or(0, |scripts| u32::from(scripts.len())))
491 }
492
493 pub fn ot_script_tag(&self, index: u32) -> Result<u32, FontError> {
495 let Ok(index) = u16::try_from(index) else {
496 return Ok(0);
497 };
498 let face = parse_face(self)?;
499 Ok(larger_script_list(face)
500 .and_then(|scripts| scripts.get(index))
501 .map_or(0, |script| script.tag.0))
502 }
503
504 pub fn ot_language_count(&self, script_tag: u32) -> Result<u32, FontError> {
506 let face = parse_face(self)?;
507 let script_tag = ttf_parser::Tag(script_tag);
508 let mut count = 0u32;
509 for table in [face.tables().gsub, face.tables().gpos].into_iter().flatten() {
510 if let Some(script) = table
511 .scripts
512 .index(script_tag)
513 .and_then(|index| table.scripts.get(index))
514 {
515 count += u32::from(script.languages.len());
516 }
517 }
518 Ok(count)
519 }
520
521 pub fn ot_language_tag(&self, script_tag: u32, index: u32) -> Result<u32, FontError> {
523 let Ok(index) = u16::try_from(index) else {
524 return Ok(0);
525 };
526 let face = parse_face(self)?;
527 let script_tag = ttf_parser::Tag(script_tag);
528 for table in [face.tables().gsub, face.tables().gpos].into_iter().flatten() {
529 if let Some(script) = table
530 .scripts
531 .index(script_tag)
532 .and_then(|script_index| table.scripts.get(script_index))
533 {
534 if index < script.languages.len() {
535 return Ok(script.languages.get(index).map_or(0, |lang| lang.tag.0));
536 }
537 }
538 }
539 Ok(0)
540 }
541
542 pub fn ot_feature_count(&self, script_tag: u32, language_tag: u32) -> Result<u32, FontError> {
544 let face = parse_face(self)?;
545 let script_tag = ttf_parser::Tag(script_tag);
546 let mut count = 0u32;
547 for table in [face.tables().gsub, face.tables().gpos].into_iter().flatten() {
548 if let Some(langsys) = language_system(table, script_tag, language_tag) {
549 count += u32::from(langsys.feature_indices.len());
550 }
551 }
552 Ok(count)
553 }
554
555 pub fn ot_feature_tag(
557 &self,
558 script_tag: u32,
559 language_tag: u32,
560 index: u32,
561 ) -> Result<u32, FontError> {
562 let Ok(index) = u16::try_from(index) else {
563 return Ok(0);
564 };
565 let face = parse_face(self)?;
566 let script_tag = ttf_parser::Tag(script_tag);
567 for table in [face.tables().gsub, face.tables().gpos].into_iter().flatten() {
568 if let Some(langsys) = language_system(table, script_tag, language_tag) {
569 if index < langsys.feature_indices.len() {
570 if let Some(feature_index) = langsys.feature_indices.get(index) {
571 return Ok(table
572 .features
573 .get(feature_index)
574 .map_or(0, |feature| feature.tag.0));
575 }
576 }
577 }
578 }
579 Ok(0)
580 }
581
582 pub fn opentype_math_constant(&self, constant: i32, size: Length) -> Result<i32, FontError> {
584 let face = parse_face(self)?;
585 let Some(constants) = face.tables().math.and_then(|table| table.constants) else {
586 return Ok(0);
587 };
588 let units_per_em = i32::from(face.units_per_em()).max(1);
589 let Some(value) = math_constant_value(constants, constant) else {
590 return Ok(0);
591 };
592 if is_math_constant_percentage(constant) {
593 Ok(value)
594 } else {
595 Ok(scale_font_units(value, size, units_per_em))
596 }
597 }
598
599 pub fn math_italic_correction(&self, glyph: GlyphId, size: Length) -> Result<i32, FontError> {
601 let face = parse_face(self)?;
602 let units_per_em = i32::from(face.units_per_em()).max(1);
603 let Ok(glyph_id) = u16::try_from(glyph.0) else {
604 return Ok(0);
605 };
606 let Some(corrections) = face
607 .tables()
608 .math
609 .and_then(|table| table.glyph_info)
610 .and_then(|info| info.italic_corrections)
611 else {
612 return Ok(0);
613 };
614 let Some(value) = corrections.get(ttf_parser::GlyphId(glyph_id)) else {
615 return Ok(0);
616 };
617 Ok(scale_font_units(i32::from(value.value), size, units_per_em))
618 }
619
620 pub fn math_kern_at(
622 &self,
623 glyph: GlyphId,
624 corner: MathKernCorner,
625 correction_height: i32,
626 ) -> Result<i32, FontError> {
627 let face = parse_face(self)?;
628 let Ok(glyph_id) = u16::try_from(glyph.0) else {
629 return Ok(0);
630 };
631 let Some(kern_info) = face
632 .tables()
633 .math
634 .and_then(|table| table.glyph_info)
635 .and_then(|info| info.kern_infos)
636 .and_then(|infos| infos.get(ttf_parser::GlyphId(glyph_id)))
637 else {
638 return Ok(0);
639 };
640 let kern = match corner {
641 MathKernCorner::TopRight => kern_info.top_right,
642 MathKernCorner::TopLeft => kern_info.top_left,
643 MathKernCorner::BottomRight => kern_info.bottom_right,
644 MathKernCorner::BottomLeft => kern_info.bottom_left,
645 };
646 let Some(kern) = kern else {
647 return Ok(0);
648 };
649 let count = kern.count();
650 let mut i = 0u16;
651 while i < count {
652 match kern.height(i) {
653 Some(h) if correction_height < i32::from(h.value) => break,
654 _ => i += 1,
655 }
656 }
657 Ok(kern.kern(i).map(|v| i32::from(v.value)).unwrap_or(0))
658 }
659
660 pub fn math_variant(
662 &self,
663 glyph: GlyphId,
664 index: u16,
665 horizontal: bool,
666 size: Length,
667 ) -> Result<Option<(u32, i32)>, FontError> {
668 let face = parse_face(self)?;
669 let units_per_em = i32::from(face.units_per_em()).max(1);
670 let Ok(glyph_id) = u16::try_from(glyph.0) else {
671 return Ok(None);
672 };
673 let Some(variants) = face.tables().math.and_then(|table| table.variants) else {
674 return Ok(None);
675 };
676 let constructions = if horizontal {
677 variants.horizontal_constructions
678 } else {
679 variants.vertical_constructions
680 };
681 let Some(construction) = constructions.get(ttf_parser::GlyphId(glyph_id)) else {
682 return Ok(None);
683 };
684 let Some(variant) = construction.variants.get(index) else {
685 return Ok(None);
686 };
687 let advance = scale_font_units(
688 i32::from(variant.advance_measurement),
689 size,
690 units_per_em,
691 );
692 Ok(Some((u32::from(variant.variant_glyph.0), advance)))
693 }
694
695 pub fn math_assembly(
697 &self,
698 glyph: GlyphId,
699 horizontal: bool,
700 size: Length,
701 ) -> Result<Vec<MathAssemblyPart>, FontError> {
702 let face = parse_face(self)?;
703 let units_per_em = i32::from(face.units_per_em()).max(1);
704 let Ok(glyph_id) = u16::try_from(glyph.0) else {
705 return Ok(Vec::new());
706 };
707 let Some(variants) = face.tables().math.and_then(|table| table.variants) else {
708 return Ok(Vec::new());
709 };
710 let constructions = if horizontal {
711 variants.horizontal_constructions
712 } else {
713 variants.vertical_constructions
714 };
715 let Some(assembly) = constructions
716 .get(ttf_parser::GlyphId(glyph_id))
717 .and_then(|construction| construction.assembly)
718 else {
719 return Ok(Vec::new());
720 };
721 let parts = assembly
722 .parts
723 .into_iter()
724 .map(|part| MathAssemblyPart {
725 glyph: u32::from(part.glyph_id.0),
726 start_connector: scale_font_units(
727 i32::from(part.start_connector_length),
728 size,
729 units_per_em,
730 ),
731 end_connector: scale_font_units(
732 i32::from(part.end_connector_length),
733 size,
734 units_per_em,
735 ),
736 full_advance: scale_font_units(
737 i32::from(part.full_advance),
738 size,
739 units_per_em,
740 ),
741 extender: part.part_flags.extender(),
742 })
743 .collect();
744 Ok(parts)
745 }
746
747 pub fn math_min_connector_overlap(&self, size: Length) -> Result<i32, FontError> {
749 let face = parse_face(self)?;
750 let units_per_em = i32::from(face.units_per_em()).max(1);
751 let overlap = face
752 .tables()
753 .math
754 .and_then(|table| table.variants)
755 .map(|variants| i32::from(variants.min_connector_overlap))
756 .unwrap_or(0);
757 Ok(scale_font_units(overlap, size, units_per_em))
758 }
759
760 pub fn points_to_units(&self, points: f32, size: Length) -> Result<f32, FontError> {
762 let face = parse_face(self)?;
763 let units_per_em = i32::from(face.units_per_em()).max(1);
764 let point_size = (f64::from(size.0) / 65536.0) as f32;
765 if point_size == 0.0 {
766 return Ok(0.0);
767 }
768 Ok((points * units_per_em as f32) / point_size)
769 }
770
771 pub fn units_to_scaled(&self, units: i32, size: Length) -> Result<i32, FontError> {
773 let face = parse_face(self)?;
774 let units_per_em = i32::from(face.units_per_em()).max(1);
775 Ok(scale_font_units(units, size, units_per_em))
776 }
777
778 pub fn opentype_math_accent_position(
780 &self,
781 glyph: GlyphId,
782 size: Length,
783 ) -> Result<i32, FontError> {
784 let face = parse_face(self)?;
785 let units_per_em = i32::from(face.units_per_em()).max(1);
786 let Ok(glyph_id) = u16::try_from(glyph.0) else {
787 return Ok(0);
788 };
789 let Some(attachments) = face
790 .tables()
791 .math
792 .and_then(|table| table.glyph_info)
793 .and_then(|info| info.top_accent_attachments)
794 else {
795 return Ok(0);
796 };
797 let Some(value) = attachments.get(ttf_parser::GlyphId(glyph_id)) else {
798 return Ok(0);
799 };
800 Ok(scale_font_units(i32::from(value.value), size, units_per_em))
801 }
802
803 pub fn math_symbol_parameter(&self, parameter: i32, size: Length) -> Result<i32, FontError> {
805 match parameter {
806 5 => self.opentype_math_constant(6, size),
807 6 => Ok(size.0),
808 8 => self.opentype_math_constant(33, size),
809 9 => self.opentype_math_constant(32, size),
810 10 => self.opentype_math_constant(22, size),
811 11 => self.opentype_math_constant(35, size),
812 12 => self.opentype_math_constant(34, size),
813 13 | 14 => self.opentype_math_constant(11, size),
814 15 => self.opentype_math_constant(12, size),
815 16 | 17 => self.opentype_math_constant(8, size),
816 18 => self.opentype_math_constant(14, size),
817 19 => self.opentype_math_constant(10, size),
818 20 => self.opentype_math_constant(2, size),
819 21 => {
820 let delim1 = self.math_symbol_parameter(20, size)?;
821 Ok(((i64::from(size.0) * 3) / 2)
822 .min(i64::from(delim1))
823 .clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32)
824 }
825 22 => self.opentype_math_constant(5, size),
826 _ => Ok(0),
827 }
828 }
829
830 pub fn math_extension_parameter(&self, parameter: i32, size: Length) -> Result<i32, FontError> {
832 match parameter {
833 5 => self.opentype_math_constant(6, size),
834 6 => Ok(size.0),
835 8 => self.opentype_math_constant(38, size),
836 9 => self.opentype_math_constant(18, size),
837 10 => self.opentype_math_constant(20, size),
838 11 => self.opentype_math_constant(19, size),
839 12 => self.opentype_math_constant(21, size),
840 13 => self.opentype_math_constant(26, size),
841 _ => Ok(0),
842 }
843 }
844}
845
846#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
848pub struct FontMetrics {
849 pub ascent: i32,
851 pub descent: i32,
853 pub xheight: i32,
855 pub capheight: i32,
857 pub slant: i32,
859}
860
861#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
863pub struct FontGlyphMetrics {
864 pub width: i32,
866 pub height: i32,
868 pub depth: i32,
870}
871
872#[derive(Clone, Copy, Debug, PartialEq, Eq)]
874pub struct ShapeFeature {
875 pub tag: [u8; 4],
877 pub value: u32,
879}
880
881#[derive(Clone, Debug, PartialEq, Eq)]
883pub struct ShapeRequest<'a> {
884 pub font: FontId,
886 pub text: &'a str,
888 pub direction: Direction,
890 pub source: Option<ByteSpan>,
892 pub script: Option<[u8; 4]>,
894 pub features: Vec<ShapeFeature>,
896}
897
898#[derive(Clone, Debug, Default, PartialEq, Eq)]
900pub struct ShapedText {
901 pub glyphs: Vec<PositionedGlyph>,
903}
904
905impl ShapedText {
906 #[must_use]
908 pub fn new(glyphs: impl Into<Vec<PositionedGlyph>>) -> Self {
909 Self {
910 glyphs: glyphs.into(),
911 }
912 }
913}
914
915#[derive(Clone, Debug, PartialEq, Eq)]
917#[non_exhaustive]
918pub enum FontError {
919 NotFound {
921 family: String,
923 },
924 Invalid {
926 family: String,
928 message: String,
930 },
931 ShapingUnsupported {
933 message: String,
935 },
936}
937
938type TtfFace<'a> = ttf_parser::Face<'a>;
940
941self_cell::self_cell!(
942 struct ParsedFace {
944 owner: Arc<[u8]>,
945 #[covariant]
946 dependent: TtfFace,
947 }
948);
949
950type RbFace<'a> = rustybuzz::Face<'a>;
952
953self_cell::self_cell!(
954 struct ParsedRustybuzzFace {
956 owner: Arc<[u8]>,
957 #[covariant]
958 dependent: RbFace,
959 }
960);
961
962fn parse_rustybuzz_face(font: &FontData) -> Result<&rustybuzz::Face<'_>, FontError> {
964 match &font.source {
965 FaceSource::Shared(face) => Ok(face.rustybuzz_face()),
966 FaceSource::Bytes {
967 bytes, rustybuzz, ..
968 } => {
969 if rustybuzz.get().is_none() {
970 let parsed = ParsedRustybuzzFace::try_new(Arc::clone(bytes), |bytes| {
971 rustybuzz::Face::from_slice(bytes, 0).ok_or_else(|| FontError::Invalid {
972 family: font.canonical_name.clone(),
973 message: "invalid font data".to_string(),
974 })
975 })?;
976 let _ = rustybuzz.set(alloc::boxed::Box::new(parsed));
977 }
978 Ok(rustybuzz
979 .get()
980 .expect("rustybuzz cache populated above")
981 .borrow_dependent())
982 }
983 }
984}
985
986fn validate_font(font: &FontData) -> Result<(), FontError> {
987 parse_face(font).map(|_| ())
988}
989
990fn larger_script_list<'a>(
992 face: &ttf_parser::Face<'a>,
993) -> Option<ttf_parser::opentype_layout::ScriptList<'a>> {
994 let gsub = face.tables().gsub.map(|table| table.scripts);
995 let gpos = face.tables().gpos.map(|table| table.scripts);
996 match (gsub, gpos) {
997 (Some(sub), Some(pos)) => Some(if pos.len() > sub.len() { pos } else { sub }),
998 (sub, pos) => sub.or(pos),
999 }
1000}
1001
1002fn language_system<'a>(
1004 table: ttf_parser::opentype_layout::LayoutTable<'a>,
1005 script_tag: ttf_parser::Tag,
1006 language_tag: u32,
1007) -> Option<ttf_parser::opentype_layout::LanguageSystem<'a>> {
1008 let script = table
1009 .scripts
1010 .index(script_tag)
1011 .and_then(|index| table.scripts.get(index))?;
1012 if language_tag == 0 {
1013 script.default_language
1014 } else {
1015 script
1016 .languages
1017 .index(ttf_parser::Tag(language_tag))
1018 .and_then(|index| script.languages.get(index))
1019 .or(script.default_language)
1020 }
1021}
1022
1023fn parse_face(font: &FontData) -> Result<&ttf_parser::Face<'_>, FontError> {
1025 let invalid = |error: ttf_parser::FaceParsingError| FontError::Invalid {
1026 family: font.canonical_name.clone(),
1027 message: format!("invalid font data: {error:?}"),
1028 };
1029
1030 match &font.source {
1031 FaceSource::Shared(face) => Ok(face.ttf_face()),
1032 FaceSource::Bytes { bytes, ttf, .. } => {
1033 if ttf.get().is_none() {
1034 let parsed = ParsedFace::try_new(Arc::clone(bytes), |bytes| {
1035 ttf_parser::Face::parse(bytes, 0).map_err(invalid)
1036 })?;
1037 let _ = ttf.set(alloc::boxed::Box::new(parsed));
1039 }
1040 Ok(ttf.get().expect("ttf cache populated above").borrow_dependent())
1041 }
1042 }
1043}
1044
1045struct OutlineCollector {
1047 commands: Vec<OutlineCommand>,
1048}
1049
1050impl ttf_parser::OutlineBuilder for OutlineCollector {
1051 fn move_to(&mut self, x: f32, y: f32) {
1052 self.commands.push(OutlineCommand::MoveTo { x, y });
1053 }
1054
1055 fn line_to(&mut self, x: f32, y: f32) {
1056 self.commands.push(OutlineCommand::LineTo { x, y });
1057 }
1058
1059 fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
1060 self.commands.push(OutlineCommand::QuadTo { cx, cy, x, y });
1061 }
1062
1063 fn curve_to(&mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) {
1064 self.commands.push(OutlineCommand::CurveTo {
1065 c1x,
1066 c1y,
1067 c2x,
1068 c2y,
1069 x,
1070 y,
1071 });
1072 }
1073
1074 fn close(&mut self) {
1075 self.commands.push(OutlineCommand::Close);
1076 }
1077}
1078
1079fn math_constant_value(constants: ttf_parser::math::Constants<'_>, constant: i32) -> Option<i32> {
1080 let value = match constant {
1081 0 => i32::from(constants.script_percent_scale_down()),
1082 1 => i32::from(constants.script_script_percent_scale_down()),
1083 2 => i32::from(constants.delimited_sub_formula_min_height()),
1084 3 => i32::from(constants.display_operator_min_height()),
1085 4 => i32::from(constants.math_leading().value),
1086 5 => i32::from(constants.axis_height().value),
1087 6 => i32::from(constants.accent_base_height().value),
1088 7 => i32::from(constants.flattened_accent_base_height().value),
1089 8 => i32::from(constants.subscript_shift_down().value),
1090 9 => i32::from(constants.subscript_top_max().value),
1091 10 => i32::from(constants.subscript_baseline_drop_min().value),
1092 11 => i32::from(constants.superscript_shift_up().value),
1093 12 => i32::from(constants.superscript_shift_up_cramped().value),
1094 13 => i32::from(constants.superscript_bottom_min().value),
1095 14 => i32::from(constants.superscript_baseline_drop_max().value),
1096 15 => i32::from(constants.sub_superscript_gap_min().value),
1097 16 => i32::from(constants.superscript_bottom_max_with_subscript().value),
1098 17 => i32::from(constants.space_after_script().value),
1099 18 => i32::from(constants.upper_limit_gap_min().value),
1100 19 => i32::from(constants.upper_limit_baseline_rise_min().value),
1101 20 => i32::from(constants.lower_limit_gap_min().value),
1102 21 => i32::from(constants.lower_limit_baseline_drop_min().value),
1103 22 => i32::from(constants.stack_top_shift_up().value),
1104 23 => i32::from(constants.stack_top_display_style_shift_up().value),
1105 24 => i32::from(constants.stack_bottom_shift_down().value),
1106 25 => i32::from(constants.stack_bottom_display_style_shift_down().value),
1107 26 => i32::from(constants.stack_gap_min().value),
1108 27 => i32::from(constants.stack_display_style_gap_min().value),
1109 28 => i32::from(constants.stretch_stack_top_shift_up().value),
1110 29 => i32::from(constants.stretch_stack_bottom_shift_down().value),
1111 30 => i32::from(constants.stretch_stack_gap_above_min().value),
1112 31 => i32::from(constants.stretch_stack_gap_below_min().value),
1113 32 => i32::from(constants.fraction_numerator_shift_up().value),
1114 33 => i32::from(constants.fraction_numerator_display_style_shift_up().value),
1115 34 => i32::from(constants.fraction_denominator_shift_down().value),
1116 35 => i32::from(
1117 constants
1118 .fraction_denominator_display_style_shift_down()
1119 .value,
1120 ),
1121 36 => i32::from(constants.fraction_numerator_gap_min().value),
1122 37 => i32::from(constants.fraction_num_display_style_gap_min().value),
1123 38 => i32::from(constants.fraction_rule_thickness().value),
1124 39 => i32::from(constants.fraction_denominator_gap_min().value),
1125 40 => i32::from(constants.fraction_denom_display_style_gap_min().value),
1126 41 => i32::from(constants.skewed_fraction_horizontal_gap().value),
1127 42 => i32::from(constants.skewed_fraction_vertical_gap().value),
1128 43 => i32::from(constants.overbar_vertical_gap().value),
1129 44 => i32::from(constants.overbar_rule_thickness().value),
1130 45 => i32::from(constants.overbar_extra_ascender().value),
1131 46 => i32::from(constants.underbar_vertical_gap().value),
1132 47 => i32::from(constants.underbar_rule_thickness().value),
1133 48 => i32::from(constants.underbar_extra_descender().value),
1134 49 => i32::from(constants.radical_vertical_gap().value),
1135 50 => i32::from(constants.radical_display_style_vertical_gap().value),
1136 51 => i32::from(constants.radical_rule_thickness().value),
1137 52 => i32::from(constants.radical_extra_ascender().value),
1138 53 => i32::from(constants.radical_kern_before_degree().value),
1139 54 => i32::from(constants.radical_kern_after_degree().value),
1140 55 => i32::from(constants.radical_degree_bottom_raise_percent()),
1141 _ => return None,
1142 };
1143 Some(value)
1144}
1145
1146fn is_math_constant_percentage(constant: i32) -> bool {
1147 matches!(constant, 0 | 1 | 55)
1148}
1149
1150fn shape_with_rustybuzz(
1151 cached: &CachedFont,
1152 request: &ShapeRequest<'_>,
1153) -> Result<ShapedText, FontError> {
1154 let face = parse_rustybuzz_face(&cached.data)?;
1155 let mut buffer = rustybuzz::UnicodeBuffer::new();
1156 buffer.push_str(request.text);
1157 buffer.set_direction(match request.direction {
1158 Direction::LeftToRight => rustybuzz::Direction::LeftToRight,
1159 Direction::RightToLeft => rustybuzz::Direction::RightToLeft,
1160 Direction::TopToBottom => rustybuzz::Direction::TopToBottom,
1161 _ => rustybuzz::Direction::LeftToRight,
1162 });
1163 buffer.guess_segment_properties();
1164 if let Some(script_tag) = request.script {
1165 if let Some(script) =
1167 rustybuzz::Script::from_iso15924_tag(ttf_parser::Tag::from_bytes(&script_tag))
1168 {
1169 buffer.set_script(script);
1170 }
1171 }
1172
1173 let features: Vec<rustybuzz::Feature> = request
1174 .features
1175 .iter()
1176 .map(|feature| {
1177 rustybuzz::Feature::new(ttf_parser::Tag::from_bytes(&feature.tag), feature.value, ..)
1178 })
1179 .collect();
1180 let shaped = rustybuzz::shape(face, &features, buffer);
1181 let infos = shaped.glyph_infos();
1182 let positions = shaped.glyph_positions();
1183 let units_per_em = face.units_per_em().max(1);
1184 let glyphs = infos
1185 .iter()
1186 .zip(positions.iter())
1187 .map(|(info, position)| PositionedGlyph {
1188 glyph_id: GlyphId(info.glyph_id),
1189 advance: Point {
1190 x: scaled_font_units(position.x_advance, cached.size, units_per_em),
1191 y: scaled_font_units(position.y_advance, cached.size, units_per_em),
1192 },
1193 offset: Point {
1194 x: scaled_font_units(position.x_offset, cached.size, units_per_em),
1195 y: scaled_font_units(position.y_offset, cached.size, units_per_em),
1196 },
1197 cluster: request
1198 .source
1199 .map(|source| cluster_source_span(request.text, source, info.cluster)),
1200 })
1201 .collect();
1202
1203 Ok(ShapedText { glyphs })
1204}
1205
1206fn scaled_font_units(value: i32, size: Length, units_per_em: i32) -> Length {
1207 Length::from_scaled_points(scale_font_units(value, size, units_per_em))
1208}
1209
1210 fn scale_font_units(value: i32, size: Length, units_per_em: i32) -> i32 {
1212 let point_size = (f64::from(size.0) / 65536.0) as f32;
1214 let points = (value as f32 * point_size) / (units_per_em.max(1) as f32);
1215 let fixed = (f64::from(points) * 65536.0 + 0.5).trunc();
1217 fixed.clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32
1218}
1219
1220fn cluster_source_span(text: &str, source: ByteSpan, cluster: u32) -> ByteSpan {
1221 let start = usize::try_from(cluster)
1222 .ok()
1223 .map(|cluster| cluster.min(text.len()))
1224 .unwrap_or(text.len());
1225 let mut end = text.len();
1226 for (index, _) in text.char_indices() {
1227 if index > start {
1228 end = index;
1229 break;
1230 }
1231 }
1232 ByteSpan {
1233 start: source.start.saturating_add(start as u32),
1234 end: source.start.saturating_add(end as u32).min(source.end),
1235 }
1236}
1237
1238#[derive(Clone, Copy, Debug, Default)]
1240pub struct NoFontSystem;
1241
1242impl FontSystem for NoFontSystem {
1243 fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
1244 Err(FontError::NotFound {
1245 family: query.family.clone(),
1246 })
1247 }
1248
1249 fn shape_text(&self, _request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
1250 Err(FontError::ShapingUnsupported {
1251 message: "no font shaper configured".to_string(),
1252 })
1253 }
1254}
1255
1256#[derive(Clone, Debug, Default, PartialEq, Eq)]
1258pub struct InMemoryFontSystem {
1259 fonts: BTreeMap<String, FontData>,
1260 shape_without_native_engine: bool,
1261}
1262
1263impl InMemoryFontSystem {
1264 #[must_use]
1266 pub fn new() -> Self {
1267 Self::default()
1268 }
1269
1270 #[must_use]
1272 pub fn with_fallback_shaping(mut self) -> Self {
1273 self.shape_without_native_engine = true;
1274 self
1275 }
1276
1277 #[must_use]
1279 pub fn with_font(mut self, family: impl Into<String>, bytes: impl Into<Vec<u8>>) -> Self {
1280 self.insert(family, bytes);
1281 self
1282 }
1283
1284 pub fn insert(&mut self, family: impl Into<String>, bytes: impl Into<Vec<u8>>) {
1286 let family = family.into();
1287 let id = FontId(self.fonts.len() as u32);
1288 let bytes: Vec<u8> = bytes.into();
1289 self.fonts
1290 .insert(family.clone(), FontData::new(id, family, bytes));
1291 }
1292
1293 #[must_use]
1295 pub fn with_font_data(mut self, font: FontData) -> Self {
1296 self.insert_font_data(font);
1297 self
1298 }
1299
1300 pub fn insert_font_data(&mut self, font: FontData) {
1302 self.fonts.insert(font.canonical_name.clone(), font);
1303 }
1304}
1305
1306impl FontSystem for InMemoryFontSystem {
1307 fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
1308 self.fonts
1309 .get(&query.family)
1310 .cloned()
1311 .ok_or_else(|| FontError::NotFound {
1312 family: query.family.clone(),
1313 })
1314 }
1315
1316 fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
1317 if !self.shape_without_native_engine {
1318 return Err(FontError::ShapingUnsupported {
1319 message: "fallback shaping is disabled".to_string(),
1320 });
1321 }
1322
1323 let glyphs = request
1324 .text
1325 .char_indices()
1326 .enumerate()
1327 .map(|(index, (byte_offset, ch))| {
1328 let start = request
1329 .source
1330 .map_or(byte_offset as u32, |span| span.start + byte_offset as u32);
1331 PositionedGlyph {
1332 glyph_id: GlyphId(ch as u32),
1333 offset: Point {
1334 x: Length::from_scaled_points((index as i32) * 65_536),
1335 y: Length::ZERO,
1336 },
1337 advance: Point {
1338 x: Length::from_scaled_points(65_536),
1339 y: Length::ZERO,
1340 },
1341 cluster: Some(ByteSpan {
1342 start,
1343 end: start + ch.len_utf8() as u32,
1344 }),
1345 }
1346 })
1347 .collect();
1348
1349 Ok(ShapedText { glyphs })
1350 }
1351}
1352
1353impl FontLoader for InMemoryFontSystem {
1354 fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
1355 FontSystem::load_font(self, query)
1356 }
1357}
1358
1359impl TextShaper for InMemoryFontSystem {
1360 fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
1361 FontSystem::shape_text(self, request)
1362 }
1363}
1364
1365#[cfg(test)]
1366mod tests {
1367 use super::*;
1368
1369 const DEJAVU: &[u8] =
1370 include_bytes!("../../../vendor/texlive-source/libs/gd/libgd-src/tests/freetype/DejaVuSans.ttf");
1371
1372 #[test]
1373 fn font_data_clones_share_both_parsed_faces() {
1374 let font = FontData::new(FontId(7), "DejaVu Sans", DEJAVU.to_vec());
1375 let clone = font.clone();
1376
1377 let ttf_ptr = font
1378 .with_ttf_face(|face| face as *const ttf_parser::Face<'_> as usize)
1379 .expect("ttf parse");
1380 let clone_ttf_ptr = clone
1381 .with_ttf_face(|face| face as *const ttf_parser::Face<'_> as usize)
1382 .expect("ttf parse via clone");
1383 assert_eq!(ttf_ptr, clone_ttf_ptr, "clone must reuse the same ttf parse");
1384
1385 let rb_ptr = font
1386 .with_rustybuzz_face(|face| face as *const rustybuzz::Face<'_> as usize)
1387 .expect("rustybuzz parse");
1388 let clone_rb_ptr = clone
1389 .with_rustybuzz_face(|face| face as *const rustybuzz::Face<'_> as usize)
1390 .expect("rustybuzz parse via clone");
1391 assert_eq!(rb_ptr, clone_rb_ptr, "clone must reuse the same rustybuzz parse");
1392
1393 assert!(Arc::ptr_eq(
1394 font.bytes().expect("library owned bytes"),
1395 clone.bytes().expect("library owned bytes"),
1396 ));
1397 }
1398
1399 self_cell::self_cell!(
1400 struct AppOwnedFace {
1401 owner: Arc<[u8]>,
1402 #[covariant]
1403 dependent: RbFace,
1404 }
1405 );
1406
1407 struct AppShared(AppOwnedFace);
1408
1409 impl SharedFace for AppShared {
1410 fn rustybuzz_face(&self) -> &rustybuzz::Face<'_> {
1411 self.0.borrow_dependent()
1412 }
1413 }
1414
1415 #[test]
1416 fn application_owned_face_is_borrowed_and_never_parsed_by_the_library() {
1417 let bytes: Arc<[u8]> = DEJAVU.to_vec().into();
1418 let app_face = AppShared(
1419 AppOwnedFace::try_new(Arc::clone(&bytes), |bytes| {
1420 rustybuzz::Face::from_slice(bytes, 0).ok_or("app parse failed")
1421 })
1422 .expect("application parses its own font"),
1423 );
1424 let app_face: Arc<dyn SharedFace> = Arc::new(app_face);
1425 let app_ptr = app_face.rustybuzz_face() as *const rustybuzz::Face<'_> as usize;
1426
1427 let font = FontData::from_shared_face(FontId(3), "DejaVu Sans", Arc::clone(&app_face));
1428
1429 assert!(font.bytes().is_none(), "the library holds no bytes");
1430 let lib_ptr = font
1431 .with_rustybuzz_face(|face| face as *const rustybuzz::Face<'_> as usize)
1432 .expect("borrow shared face");
1433 assert_eq!(lib_ptr, app_ptr, "the library must borrow the application face");
1434
1435 let metrics = font.metrics(Length::from_scaled_points(655_360)).expect("metrics");
1436 assert!(metrics.ascent > 0);
1437 let glyph = font.glyph_index('A').expect("glyph lookup");
1438 assert!(glyph.is_some());
1439 }
1440
1441 #[test]
1442 fn font_data_is_send_and_sync() {
1443 fn assert_send_sync<T: Send + Sync>() {}
1444 assert_send_sync::<FontData>();
1445 }
1446
1447 #[test]
1448 fn fallback_shaping_preserves_source_clusters() {
1449 let fonts = InMemoryFontSystem::new().with_fallback_shaping();
1450 let shaped = FontSystem::shape_text(
1451 &fonts,
1452 &ShapeRequest {
1453 font: FontId(0),
1454 text: "ab",
1455 direction: Direction::LeftToRight,
1456 source: Some(ByteSpan { start: 4, end: 6 }),
1457 script: None,
1458 features: Vec::new(),
1459 },
1460 )
1461 .expect("fallback shaping should work");
1462
1463 assert_eq!(shaped.glyphs.len(), 2);
1464 assert_eq!(shaped.glyphs[0].glyph_id, GlyphId('a' as u32));
1465 assert_eq!(
1466 shaped.glyphs[0].cluster,
1467 Some(ByteSpan { start: 4, end: 5 })
1468 );
1469 assert_eq!(
1470 shaped.glyphs[1].cluster,
1471 Some(ByteSpan { start: 5, end: 6 })
1472 );
1473 }
1474
1475 #[test]
1476 fn no_font_system_makes_missing_font_explicit() {
1477 let error = NoFontSystem
1478 .load_font(&FontQuery {
1479 family: "missing".to_string(),
1480 size: Length::ZERO,
1481 math: false,
1482 })
1483 .expect_err("font should be missing");
1484
1485 assert_eq!(
1486 error,
1487 FontError::NotFound {
1488 family: "missing".to_string(),
1489 }
1490 );
1491 }
1492
1493 #[derive(Clone, Debug, Default, PartialEq, Eq)]
1494 struct TestLoader;
1495
1496 impl FontLoader for TestLoader {
1497 fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
1498 Ok(FontData::new(
1499 FontId(9),
1500 query.family.clone(),
1501 b"parsed-by-rust-loader".to_vec(),
1502 ))
1503 }
1504 }
1505
1506 #[derive(Clone, Debug, Default, PartialEq, Eq)]
1507 struct TestShaper;
1508
1509 impl TextShaper for TestShaper {
1510 fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
1511 Ok(ShapedText::new(alloc::vec![PositionedGlyph {
1512 glyph_id: GlyphId(100),
1513 offset: Point::default(),
1514 advance: Point {
1515 x: Length::from_scaled_points(request.text.len() as i32),
1516 y: Length::ZERO,
1517 },
1518 cluster: request.source,
1519 }]))
1520 }
1521 }
1522
1523 #[test]
1524 fn composed_font_system_splits_loading_from_shaping() {
1525 let fonts = ComposedFontSystem::new(TestLoader, TestShaper);
1526
1527 let font = fonts
1528 .load_font(&FontQuery {
1529 family: "Latin Modern Math".to_string(),
1530 size: Length::from_scaled_points(12 * 65_536),
1531 math: true,
1532 })
1533 .expect("loader should resolve font");
1534 let shaped = fonts
1535 .shape_text(&ShapeRequest {
1536 font: font.id,
1537 text: "xy",
1538 direction: Direction::LeftToRight,
1539 source: Some(ByteSpan { start: 2, end: 4 }),
1540 script: None,
1541 features: Vec::new(),
1542 })
1543 .expect("shaper should shape text");
1544
1545 assert_eq!(font.id, FontId(9));
1546 assert_eq!(
1547 &**font.bytes().expect("library owned bytes"),
1548 b"parsed-by-rust-loader"
1549 );
1550 assert_eq!(shaped.glyphs[0].glyph_id, GlyphId(100));
1551 assert_eq!(
1552 shaped.glyphs[0].cluster,
1553 Some(ByteSpan { start: 2, end: 4 })
1554 );
1555 }
1556
1557 #[test]
1558 fn scale_font_units_matches_xetex_d2fix_rounding() {
1559 let ten_pt = Length::from_scaled_points(10 * 65_536);
1561 assert_eq!(
1562 scale_font_units(666, ten_pt, 1000),
1563 436_470,
1564 "D2Fix must round 436469.76sp up to 436470 (xetex parity)"
1565 );
1566 assert_eq!(scale_font_units(431, ten_pt, 1000), 282_460);
1568 assert_eq!(scale_font_units(528, ten_pt, 1000), 346_030);
1570 assert_eq!(scale_font_units(16, ten_pt, 1000), 10_486);
1572 assert_eq!(scale_font_units(-16, ten_pt, 1000), -10_485);
1574 assert_eq!(scale_font_units(0, ten_pt, 1000), 0);
1575 }
1576
1577 #[test]
1578 fn rustybuzz_font_system_shapes_loaded_ttf_and_reads_metrics() {
1579 let fonts = RustybuzzFontSystem::new(InMemoryFontSystem::new().with_font(
1580 "DejaVu Sans",
1581 include_bytes!("../../../vendor/texlive-source/libs/gd/libgd-src/tests/freetype/DejaVuSans.ttf")
1582 .as_slice(),
1583 ));
1584 let font = FontSystem::load_font(
1585 &fonts,
1586 &FontQuery {
1587 family: "DejaVu Sans".to_string(),
1588 size: Length::from_scaled_points(10 * 65_536),
1589 math: false,
1590 },
1591 )
1592 .expect("real TTF should parse");
1593
1594 let metrics = font
1595 .metrics(Length::from_scaled_points(10 * 65_536))
1596 .expect("metrics should parse");
1597 let shaped = FontSystem::shape_text(
1598 &fonts,
1599 &ShapeRequest {
1600 font: font.id,
1601 text: "AV",
1602 direction: Direction::LeftToRight,
1603 source: Some(ByteSpan { start: 7, end: 9 }),
1604 script: None,
1605 features: Vec::new(),
1606 },
1607 )
1608 .expect("rustybuzz should shape cached font");
1609
1610 assert!(metrics.ascent > 0);
1611 assert!(shaped.glyphs.len() >= 2);
1612 assert!(shaped.glyphs[0].advance.x > Length::ZERO);
1613 assert_eq!(
1614 shaped.glyphs[0].cluster,
1615 Some(ByteSpan { start: 7, end: 8 })
1616 );
1617 }
1618
1619 const LM_MATH: &str = "/usr/local/texlive/2025/texmf-dist/fonts/opentype/public/lm-math/latinmodern-math.otf";
1620 const STIX_MATH: &str =
1621 "/usr/local/texlive/2025/texmf-dist/fonts/opentype/public/stix2-otf/STIXTwoMath-Regular.otf";
1622
1623 fn load_system_font(path: &str) -> Option<FontData> {
1624 let bytes = std::fs::read(path).ok()?;
1625 Some(FontData::new(FontId(1), path, bytes))
1626 }
1627
1628 fn ten_pt() -> Length {
1629 Length::from_scaled_points(10 * 65_536)
1630 }
1631
1632 #[test]
1633 fn glyph_outlines_extract_design_unit_contours_from_latinmodern() {
1634 let Some(font) = load_system_font(LM_MATH) else {
1635 eprintln!("SKIP: {LM_MATH} not found");
1636 return;
1637 };
1638 let x = font.glyph_index('x').unwrap().unwrap();
1639 let space = font.glyph_index(' ').unwrap().unwrap();
1641 let outlines = font.glyph_outlines(&[x, space]).unwrap();
1642 assert_eq!(outlines.len(), 2);
1643
1644 let x_outline = outlines[0].as_ref().expect("'x' has an outline");
1645 assert_eq!(x_outline.units_per_em, 1000, "lm-math upem");
1646 assert!(
1647 !x_outline.commands.is_empty(),
1648 "'x' outline has contour commands"
1649 );
1650 assert!(
1651 matches!(x_outline.commands[0], OutlineCommand::MoveTo { .. }),
1652 "a contour starts with MoveTo"
1653 );
1654
1655 assert!(outlines[1].is_none(), "space has no outline");
1656 }
1657
1658 #[test]
1659 fn math_variant_returns_larger_paren_glyphs_from_latinmodern() {
1660 let Some(font) = load_system_font(LM_MATH) else {
1661 eprintln!("SKIP: {LM_MATH} not found");
1662 return;
1663 };
1664 let paren = font.glyph_index('(').unwrap().unwrap();
1666 assert_eq!(paren.0, 9);
1667 let variant = font
1668 .math_variant(paren, 4, false, ten_pt())
1669 .unwrap()
1670 .expect("'(' has a 5th vertical variant");
1671 assert_eq!(variant.0, 2433, "variant glyph id");
1672 assert_eq!(variant.1, 1_175_061, "variant advance sp");
1673 let base = font.math_variant(paren, 0, false, ten_pt()).unwrap().unwrap();
1675 assert_eq!(base, (9, 653_394));
1676 assert!(font.math_variant(paren, 99, false, ten_pt()).unwrap().is_none());
1678 }
1679
1680 #[test]
1681 fn math_assembly_returns_paren_parts_from_latinmodern() {
1682 let Some(font) = load_system_font(LM_MATH) else {
1683 eprintln!("SKIP: {LM_MATH} not found");
1684 return;
1685 };
1686 let paren = font.glyph_index('(').unwrap().unwrap();
1687 let parts = font.math_assembly(paren, false, ten_pt()).unwrap();
1688 assert_eq!(parts.len(), 3, "'(' assembly has 3 parts");
1690 assert_eq!(
1691 parts[0],
1692 MathAssemblyPart {
1693 glyph: 2503,
1694 start_connector: 0,
1695 end_connector: 163_185, full_advance: 979_763, extender: false,
1698 }
1699 );
1700 assert_eq!(
1701 parts[1],
1702 MathAssemblyPart {
1703 glyph: 2504,
1704 start_connector: 326_369, end_connector: 326_369,
1706 full_advance: 326_369,
1707 extender: true,
1708 }
1709 );
1710 assert_eq!(
1711 parts[2],
1712 MathAssemblyPart {
1713 glyph: 2505,
1714 start_connector: 163_185,
1715 end_connector: 0,
1716 full_advance: 979_763,
1717 extender: false,
1718 }
1719 );
1720 assert_eq!(font.math_min_connector_overlap(ten_pt()).unwrap(), 13_107);
1722 }
1723
1724 #[test]
1725 fn math_kern_reads_stix_cut_ins() {
1726 let Some(font) = load_system_font(STIX_MATH) else {
1727 eprintln!("SKIP: {STIX_MATH} not found");
1728 return;
1729 };
1730 let f = font.glyph_index('F').unwrap().unwrap();
1732 assert_eq!(f.0, 8);
1733 assert_eq!(
1734 font.math_kern_at(f, MathKernCorner::TopRight, 0).unwrap(),
1735 44
1736 );
1737 assert_eq!(
1738 font.math_kern_at(f, MathKernCorner::TopRight, 100_000).unwrap(),
1739 44
1740 );
1741 let v = font.glyph_index('V').unwrap().unwrap();
1743 assert_eq!(v.0, 24);
1744 assert_eq!(
1745 font.math_kern_at(v, MathKernCorner::BottomRight, 0).unwrap(),
1746 -193,
1747 "below first height -> kern[0]"
1748 );
1749 assert_eq!(
1750 font.math_kern_at(v, MathKernCorner::BottomRight, 200).unwrap(),
1751 -119,
1752 "between heights -> kern[1]"
1753 );
1754 assert_eq!(
1755 font.math_kern_at(v, MathKernCorner::BottomRight, 300).unwrap(),
1756 56,
1757 "above last height -> kern[last]"
1758 );
1759 assert_eq!(
1761 font.math_kern_at(v, MathKernCorner::TopRight, 0).unwrap(),
1762 0
1763 );
1764 }
1765
1766 #[test]
1767 fn latinmodern_has_no_math_kern_info() {
1768 let Some(font) = load_system_font(LM_MATH) else {
1770 eprintln!("SKIP: {LM_MATH} not found");
1771 return;
1772 };
1773 let x = font.glyph_index('x').unwrap().unwrap();
1774 for corner in [
1775 MathKernCorner::TopRight,
1776 MathKernCorner::TopLeft,
1777 MathKernCorner::BottomRight,
1778 MathKernCorner::BottomLeft,
1779 ] {
1780 assert_eq!(font.math_kern_at(x, corner, 0).unwrap(), 0);
1781 }
1782 }
1783}