1mod autohint;
82mod cff;
83mod glyf;
84mod hint;
85mod hint_reliant;
86mod memory;
87mod metrics;
88mod path;
89mod unscaled;
90
91#[cfg(test)]
92mod testing;
93
94pub mod error;
95pub mod pen;
96
97pub use autohint::GlyphStyles;
98pub use hint::{
99 Engine, HintingInstance, HintingMode, HintingOptions, LcdLayout, SmoothMode, Target,
100};
101use metrics::GlyphHMetrics;
102use raw::FontRef;
103#[doc(inline)]
104pub use {error::DrawError, pen::OutlinePen};
105
106use self::glyf::{FreeTypeScaler, HarfBuzzScaler};
107use super::{
108 instance::{LocationRef, NormalizedCoord, Size},
109 GLYF_COMPOSITE_RECURSION_LIMIT,
110};
111use core::fmt::Debug;
112use pen::PathStyle;
113use read_fonts::{types::GlyphId, TableProvider};
114
115#[cfg(feature = "libm")]
116#[allow(unused_imports)]
117use core_maths::CoreFloat;
118
119#[derive(Copy, Clone, PartialEq, Eq, Debug)]
121pub enum OutlineGlyphFormat {
122 Glyf,
124 Cff,
126 Cff2,
128}
129
130#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
132pub enum Hinting {
133 #[default]
135 None,
136 Embedded,
142}
143
144#[derive(Copy, Clone, Default, Debug)]
150pub struct AdjustedMetrics {
151 pub has_overlaps: bool,
154 pub lsb: Option<f32>,
160 pub advance_width: Option<f32>,
166}
167
168pub struct DrawSettings<'a> {
171 instance: DrawInstance<'a>,
172 memory: Option<&'a mut [u8]>,
173 path_style: PathStyle,
174}
175
176impl<'a> DrawSettings<'a> {
177 pub fn unhinted(size: Size, location: impl Into<LocationRef<'a>>) -> Self {
180 Self {
181 instance: DrawInstance::Unhinted(size, location.into()),
182 memory: None,
183 path_style: PathStyle::default(),
184 }
185 }
186
187 pub fn hinted(instance: &'a HintingInstance, is_pedantic: bool) -> Self {
197 Self {
198 instance: DrawInstance::Hinted {
199 instance,
200 is_pedantic,
201 },
202 memory: None,
203 path_style: PathStyle::default(),
204 }
205 }
206
207 pub fn with_memory(mut self, memory: Option<&'a mut [u8]>) -> Self {
215 self.memory = memory;
216 self
217 }
218
219 pub fn with_path_style(mut self, path_style: PathStyle) -> Self {
223 self.path_style = path_style;
224 self
225 }
226}
227
228enum DrawInstance<'a> {
229 Unhinted(Size, LocationRef<'a>),
230 Hinted {
231 instance: &'a HintingInstance,
232 is_pedantic: bool,
233 },
234}
235
236impl<'a, L> From<(Size, L)> for DrawSettings<'a>
237where
238 L: Into<LocationRef<'a>>,
239{
240 fn from(value: (Size, L)) -> Self {
241 DrawSettings::unhinted(value.0, value.1.into())
242 }
243}
244
245impl From<Size> for DrawSettings<'_> {
246 fn from(value: Size) -> Self {
247 DrawSettings::unhinted(value, LocationRef::default())
248 }
249}
250
251impl<'a> From<&'a HintingInstance> for DrawSettings<'a> {
252 fn from(value: &'a HintingInstance) -> Self {
253 DrawSettings::hinted(value, false)
254 }
255}
256
257#[derive(Clone)]
265pub struct OutlineGlyph<'a> {
266 kind: OutlineKind<'a>,
267}
268
269impl<'a> OutlineGlyph<'a> {
270 pub fn format(&self) -> OutlineGlyphFormat {
272 match &self.kind {
273 OutlineKind::Glyf(..) => OutlineGlyphFormat::Glyf,
274 OutlineKind::Cff(cff, ..) => {
275 if cff.is_cff2() {
276 OutlineGlyphFormat::Cff2
277 } else {
278 OutlineGlyphFormat::Cff
279 }
280 }
281 }
282 }
283
284 pub fn glyph_id(&self) -> GlyphId {
286 match &self.kind {
287 OutlineKind::Glyf(_, glyph) => glyph.glyph_id,
288 OutlineKind::Cff(_, gid, _) => *gid,
289 }
290 }
291
292 pub fn has_overlaps(&self) -> Option<bool> {
297 match &self.kind {
298 OutlineKind::Glyf(_, outline) => Some(outline.has_overlaps),
299 _ => None,
300 }
301 }
302
303 pub fn has_hinting(&self) -> Option<bool> {
309 match &self.kind {
310 OutlineKind::Glyf(_, outline) => Some(outline.has_hinting),
311 _ => None,
312 }
313 }
314
315 pub fn draw_memory_size(&self, hinting: Hinting) -> usize {
331 match &self.kind {
332 OutlineKind::Glyf(_, outline) => outline.required_buffer_size(hinting),
333 _ => 0,
334 }
335 }
336
337 pub fn draw<'s>(
340 &self,
341 settings: impl Into<DrawSettings<'a>>,
342 pen: &mut impl OutlinePen,
343 ) -> Result<AdjustedMetrics, DrawError> {
344 let settings: DrawSettings<'a> = settings.into();
345 match (settings.instance, settings.path_style) {
346 (DrawInstance::Unhinted(size, location), PathStyle::FreeType) => {
347 self.draw_unhinted(size, location, settings.memory, settings.path_style, pen)
348 }
349 (DrawInstance::Unhinted(size, location), PathStyle::HarfBuzz) => {
350 self.draw_unhinted(size, location, settings.memory, settings.path_style, pen)
351 }
352 (
353 DrawInstance::Hinted {
354 instance: hinting_instance,
355 is_pedantic,
356 },
357 PathStyle::FreeType,
358 ) => {
359 if hinting_instance.is_enabled() {
360 hinting_instance.draw(
361 self,
362 settings.memory,
363 settings.path_style,
364 pen,
365 is_pedantic,
366 )
367 } else {
368 let mut metrics = self.draw_unhinted(
369 hinting_instance.size(),
370 hinting_instance.location(),
371 settings.memory,
372 settings.path_style,
373 pen,
374 )?;
375 if let Some(advance) = metrics.advance_width.as_mut() {
378 *advance = advance.round();
379 }
380 Ok(metrics)
381 }
382 }
383 (DrawInstance::Hinted { .. }, PathStyle::HarfBuzz) => {
384 Err(DrawError::HarfBuzzHintingUnsupported)
385 }
386 }
387 }
388
389 fn draw_unhinted(
390 &self,
391 size: Size,
392 location: impl Into<LocationRef<'a>>,
393 user_memory: Option<&mut [u8]>,
394 path_style: PathStyle,
395 pen: &mut impl OutlinePen,
396 ) -> Result<AdjustedMetrics, DrawError> {
397 let ppem = size.ppem();
398 let coords = location.into().effective_coords();
399 match &self.kind {
400 OutlineKind::Glyf(glyf, outline) => {
401 with_glyf_memory(outline, Hinting::None, user_memory, |buf| {
402 let (lsb, advance_width) = match path_style {
403 PathStyle::FreeType => {
404 let scaled_outline =
405 FreeTypeScaler::unhinted(glyf, outline, buf, ppem, coords)?
406 .scale(&outline.glyph, outline.glyph_id)?;
407 scaled_outline.to_path(path_style, pen)?;
408 (
409 scaled_outline.adjusted_lsb().to_f32(),
410 scaled_outline.adjusted_advance_width().to_f32(),
411 )
412 }
413 PathStyle::HarfBuzz => {
414 let scaled_outline =
415 HarfBuzzScaler::unhinted(glyf, outline, buf, ppem, coords)?
416 .scale(&outline.glyph, outline.glyph_id)?;
417 scaled_outline.to_path(path_style, pen)?;
418 (
419 scaled_outline.adjusted_lsb(),
420 scaled_outline.adjusted_advance_width(),
421 )
422 }
423 };
424
425 Ok(AdjustedMetrics {
426 has_overlaps: outline.has_overlaps,
427 lsb: Some(lsb),
428 advance_width: Some(advance_width),
429 })
430 })
431 }
432 OutlineKind::Cff(cff, glyph_id, subfont_ix) => {
433 let subfont = cff.subfont(*subfont_ix, ppem, coords)?;
434 cff.draw(&subfont, *glyph_id, coords, false, pen)?;
435 Ok(AdjustedMetrics::default())
436 }
437 }
438 }
439
440 #[allow(dead_code)]
443 fn draw_unscaled(
444 &self,
445 location: impl Into<LocationRef<'a>>,
446 user_memory: Option<&mut [u8]>,
447 sink: &mut impl unscaled::UnscaledOutlineSink,
448 ) -> Result<i32, DrawError> {
449 let coords = location.into().effective_coords();
450 let ppem = None;
451 match &self.kind {
452 OutlineKind::Glyf(glyf, outline) => {
453 with_glyf_memory(outline, Hinting::None, user_memory, |buf| {
454 let outline = FreeTypeScaler::unhinted(glyf, outline, buf, ppem, coords)?
455 .scale(&outline.glyph, outline.glyph_id)?;
456 sink.try_reserve(outline.points.len())?;
457 let mut contour_start = 0;
458 for contour_end in outline.contours.iter().map(|contour| *contour as usize) {
459 if contour_end >= contour_start {
460 if let Some(points) = outline.points.get(contour_start..=contour_end) {
461 let flags = &outline.flags[contour_start..=contour_end];
462 sink.extend(points.iter().zip(flags).enumerate().map(
463 |(ix, (point, flags))| {
464 unscaled::UnscaledPoint::from_glyf_point(
465 *point,
466 *flags,
467 ix == 0,
468 )
469 },
470 ))?;
471 }
472 }
473 contour_start = contour_end + 1;
474 }
475 Ok(outline.adjusted_advance_width().to_bits() >> 6)
476 })
477 }
478 OutlineKind::Cff(cff, glyph_id, subfont_ix) => {
479 let subfont = cff.subfont(*subfont_ix, ppem, coords)?;
480 let mut adapter = unscaled::UnscaledPenAdapter::new(sink);
481 cff.draw(&subfont, *glyph_id, coords, false, &mut adapter)?;
482 adapter.finish()?;
483 let advance = cff.glyph_metrics.advance_width(*glyph_id, coords);
484 Ok(advance)
485 }
486 }
487 }
488
489 pub(crate) fn font(&self) -> &FontRef<'a> {
490 match &self.kind {
491 OutlineKind::Glyf(glyf, ..) => &glyf.font,
492 OutlineKind::Cff(cff, ..) => &cff.font,
493 }
494 }
495
496 fn units_per_em(&self) -> u16 {
497 match &self.kind {
498 OutlineKind::Cff(cff, ..) => cff.units_per_em(),
499 OutlineKind::Glyf(glyf, ..) => glyf.units_per_em(),
500 }
501 }
502}
503
504#[derive(Clone)]
505enum OutlineKind<'a> {
506 Glyf(glyf::Outlines<'a>, glyf::Outline<'a>),
507 Cff(cff::Outlines<'a>, GlyphId, u32),
509}
510
511impl Debug for OutlineKind<'_> {
512 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
513 match self {
514 Self::Glyf(_, outline) => f.debug_tuple("Glyf").field(&outline.glyph_id).finish(),
515 Self::Cff(_, gid, subfont_index) => f
516 .debug_tuple("Cff")
517 .field(gid)
518 .field(subfont_index)
519 .finish(),
520 }
521 }
522}
523
524#[derive(Debug, Clone)]
526pub struct OutlineGlyphCollection<'a> {
527 kind: OutlineCollectionKind<'a>,
528}
529
530impl<'a> OutlineGlyphCollection<'a> {
531 pub fn new(font: &FontRef<'a>) -> Self {
533 let kind = if let Some(glyf) = glyf::Outlines::new(font) {
534 OutlineCollectionKind::Glyf(glyf)
535 } else if let Some(cff) = cff::Outlines::new(font) {
536 OutlineCollectionKind::Cff(cff)
537 } else {
538 OutlineCollectionKind::None
539 };
540 Self { kind }
541 }
542
543 pub fn with_format(font: &FontRef<'a>, format: OutlineGlyphFormat) -> Option<Self> {
549 let kind = match format {
550 OutlineGlyphFormat::Glyf => OutlineCollectionKind::Glyf(glyf::Outlines::new(font)?),
551 OutlineGlyphFormat::Cff => {
552 let upem = font.head().ok()?.units_per_em();
553 OutlineCollectionKind::Cff(cff::Outlines::from_cff(font, upem)?)
554 }
555 OutlineGlyphFormat::Cff2 => {
556 let upem = font.head().ok()?.units_per_em();
557 OutlineCollectionKind::Cff(cff::Outlines::from_cff2(font, upem)?)
558 }
559 };
560 Some(Self { kind })
561 }
562
563 pub fn format(&self) -> Option<OutlineGlyphFormat> {
565 match &self.kind {
566 OutlineCollectionKind::Glyf(..) => Some(OutlineGlyphFormat::Glyf),
567 OutlineCollectionKind::Cff(cff) => cff
568 .is_cff2()
569 .then_some(OutlineGlyphFormat::Cff2)
570 .or(Some(OutlineGlyphFormat::Cff)),
571 _ => None,
572 }
573 }
574
575 pub fn get(&self, glyph_id: GlyphId) -> Option<OutlineGlyph<'a>> {
577 match &self.kind {
578 OutlineCollectionKind::None => None,
579 OutlineCollectionKind::Glyf(glyf) => Some(OutlineGlyph {
580 kind: OutlineKind::Glyf(glyf.clone(), glyf.outline(glyph_id).ok()?),
581 }),
582 OutlineCollectionKind::Cff(cff) => Some(OutlineGlyph {
583 kind: OutlineKind::Cff(cff.clone(), glyph_id, cff.subfont_index(glyph_id)),
584 }),
585 }
586 }
587
588 pub fn iter(&self) -> impl Iterator<Item = (GlyphId, OutlineGlyph<'a>)> + 'a + Clone {
590 let len = match &self.kind {
591 OutlineCollectionKind::Glyf(glyf) => glyf.glyph_count(),
592 OutlineCollectionKind::Cff(cff) => cff.glyph_count(),
593 _ => 0,
594 } as u16;
595 let copy = self.clone();
596 (0..len).filter_map(move |gid| {
597 let gid = GlyphId::from(gid);
598 let glyph = copy.get(gid)?;
599 Some((gid, glyph))
600 })
601 }
602
603 pub fn prefer_interpreter(&self) -> bool {
617 match &self.kind {
618 OutlineCollectionKind::Glyf(glyf) => glyf.prefer_interpreter(),
619 _ => true,
620 }
621 }
622
623 pub fn require_interpreter(&self) -> bool {
639 self.font()
640 .map(|font| hint_reliant::require_interpreter(font))
641 .unwrap_or_default()
642 }
643
644 pub fn fractional_size_hinting(&self) -> bool {
649 match &self.kind {
650 OutlineCollectionKind::Glyf(glyf) => glyf.fractional_size_hinting,
651 _ => true,
652 }
653 }
654
655 pub(crate) fn font(&self) -> Option<&FontRef<'a>> {
656 match &self.kind {
657 OutlineCollectionKind::Glyf(glyf) => Some(&glyf.font),
658 OutlineCollectionKind::Cff(cff) => Some(&cff.font),
659 _ => None,
660 }
661 }
662}
663
664#[derive(Clone)]
665enum OutlineCollectionKind<'a> {
666 None,
667 Glyf(glyf::Outlines<'a>),
668 Cff(cff::Outlines<'a>),
669}
670
671impl Debug for OutlineCollectionKind<'_> {
672 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
673 match self {
674 Self::None => write!(f, "None"),
675 Self::Glyf(..) => f.debug_tuple("Glyf").finish(),
676 Self::Cff(..) => f.debug_tuple("Cff").finish(),
677 }
678 }
679}
680
681pub(super) fn with_glyf_memory<R>(
684 outline: &glyf::Outline,
685 hinting: Hinting,
686 memory: Option<&mut [u8]>,
687 mut f: impl FnMut(&mut [u8]) -> R,
688) -> R {
689 match memory {
690 Some(buf) => f(buf),
691 None => {
692 let buf_size = outline.required_buffer_size(hinting);
693 memory::with_temporary_memory(buf_size, f)
694 }
695 }
696}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
701 use crate::{instance::Location, outline::pen::SvgPen, MetadataProvider};
702 use kurbo::{Affine, BezPath, PathEl, Point};
703 use read_fonts::{types::GlyphId, FontRef, TableProvider};
704
705 use pretty_assertions::assert_eq;
706
707 const PERIOD: u32 = 0x2E_u32;
708 const COMMA: u32 = 0x2C_u32;
709
710 #[test]
711 fn outline_glyph_formats() {
712 let font_format_pairs = [
713 (font_test_data::VAZIRMATN_VAR, OutlineGlyphFormat::Glyf),
714 (
715 font_test_data::CANTARELL_VF_TRIMMED,
716 OutlineGlyphFormat::Cff2,
717 ),
718 (
719 font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
720 OutlineGlyphFormat::Cff,
721 ),
722 (font_test_data::COLRV0V1_VARIABLE, OutlineGlyphFormat::Glyf),
723 ];
724 for (font_data, format) in font_format_pairs {
725 assert_eq!(
726 FontRef::new(font_data).unwrap().outline_glyphs().format(),
727 Some(format)
728 );
729 }
730 }
731
732 #[test]
733 fn vazirmatin_var() {
734 compare_glyphs(
735 font_test_data::VAZIRMATN_VAR,
736 font_test_data::VAZIRMATN_VAR_GLYPHS,
737 );
738 }
739
740 #[test]
741 fn cantarell_vf() {
742 compare_glyphs(
743 font_test_data::CANTARELL_VF_TRIMMED,
744 font_test_data::CANTARELL_VF_TRIMMED_GLYPHS,
745 );
746 }
747
748 #[test]
749 fn noto_serif_display() {
750 compare_glyphs(
751 font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
752 font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS,
753 );
754 }
755
756 #[test]
757 fn overlap_flags() {
758 let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
759 let outlines = font.outline_glyphs();
760 let glyph_count = font.maxp().unwrap().num_glyphs();
761 let expected_gids_with_overlap = vec![2, 3];
764 assert_eq!(
765 expected_gids_with_overlap,
766 (0..glyph_count)
767 .filter(
768 |gid| outlines.get(GlyphId::from(*gid)).unwrap().has_overlaps() == Some(true)
769 )
770 .collect::<Vec<_>>()
771 );
772 }
773
774 fn compare_glyphs(font_data: &[u8], expected_outlines: &str) {
775 let font = FontRef::new(font_data).unwrap();
776 let expected_outlines = testing::parse_glyph_outlines(expected_outlines);
777 let mut path = testing::Path::default();
778 for expected_outline in &expected_outlines {
779 if expected_outline.size == 0.0 && !expected_outline.coords.is_empty() {
780 continue;
781 }
782 let size = if expected_outline.size != 0.0 {
783 Size::new(expected_outline.size)
784 } else {
785 Size::unscaled()
786 };
787 path.elements.clear();
788 font.outline_glyphs()
789 .get(expected_outline.glyph_id)
790 .unwrap()
791 .draw(
792 DrawSettings::unhinted(size, expected_outline.coords.as_slice()),
793 &mut path,
794 )
795 .unwrap();
796 assert_eq!(path.elements, expected_outline.path, "mismatch in glyph path for id {} (size: {}, coords: {:?}): path: {:?} expected_path: {:?}",
797 expected_outline.glyph_id,
798 expected_outline.size,
799 expected_outline.coords,
800 &path.elements,
801 &expected_outline.path
802 );
803 }
804 }
805
806 #[derive(Copy, Clone, Debug, PartialEq)]
807 enum GlyphPoint {
808 On { x: f32, y: f32 },
809 Off { x: f32, y: f32 },
810 }
811
812 impl GlyphPoint {
813 fn implied_oncurve(&self, other: Self) -> Self {
814 let (x1, y1) = self.xy();
815 let (x2, y2) = other.xy();
816 Self::On {
817 x: (x1 + x2) / 2.0,
818 y: (y1 + y2) / 2.0,
819 }
820 }
821
822 fn xy(&self) -> (f32, f32) {
823 match self {
824 GlyphPoint::On { x, y } | GlyphPoint::Off { x, y } => (*x, *y),
825 }
826 }
827 }
828
829 #[derive(Debug)]
830 struct PointPen {
831 points: Vec<GlyphPoint>,
832 }
833
834 impl PointPen {
835 fn new() -> Self {
836 Self { points: Vec::new() }
837 }
838
839 fn into_points(self) -> Vec<GlyphPoint> {
840 self.points
841 }
842 }
843
844 impl OutlinePen for PointPen {
845 fn move_to(&mut self, x: f32, y: f32) {
846 self.points.push(GlyphPoint::On { x, y });
847 }
848
849 fn line_to(&mut self, x: f32, y: f32) {
850 self.points.push(GlyphPoint::On { x, y });
851 }
852
853 fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
854 self.points.push(GlyphPoint::Off { x: cx0, y: cy0 });
855 self.points.push(GlyphPoint::On { x, y });
856 }
857
858 fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
859 self.points.push(GlyphPoint::Off { x: cx0, y: cy0 });
860 self.points.push(GlyphPoint::Off { x: cx1, y: cy1 });
861 self.points.push(GlyphPoint::On { x, y });
862 }
863
864 fn close(&mut self) {
865 let np = self.points.len();
872 if np > 2
874 && self.points[0] == self.points[np - 1]
875 && matches!(
876 (self.points[0], self.points[np - 2]),
877 (GlyphPoint::On { .. }, GlyphPoint::Off { .. })
878 )
879 {
880 self.points.pop();
881 }
882 }
883 }
884
885 const STARTING_OFF_CURVE_POINTS: [GlyphPoint; 4] = [
886 GlyphPoint::Off { x: 278.0, y: 710.0 },
887 GlyphPoint::On { x: 278.0, y: 470.0 },
888 GlyphPoint::On { x: 998.0, y: 470.0 },
889 GlyphPoint::On { x: 998.0, y: 710.0 },
890 ];
891
892 const MOSTLY_OFF_CURVE_POINTS: [GlyphPoint; 5] = [
893 GlyphPoint::Off { x: 278.0, y: 710.0 },
894 GlyphPoint::Off { x: 278.0, y: 470.0 },
895 GlyphPoint::On { x: 998.0, y: 470.0 },
896 GlyphPoint::Off { x: 998.0, y: 710.0 },
897 GlyphPoint::Off { x: 750.0, y: 500.0 },
898 ];
899
900 #[derive(Default, Debug)]
904 struct CommandPen {
905 commands: String,
906 }
907
908 impl OutlinePen for CommandPen {
909 fn move_to(&mut self, _x: f32, _y: f32) {
910 self.commands.push('M');
911 }
912
913 fn line_to(&mut self, _x: f32, _y: f32) {
914 self.commands.push('L');
915 }
916
917 fn quad_to(&mut self, _cx0: f32, _cy0: f32, _x: f32, _y: f32) {
918 self.commands.push('Q');
919 }
920
921 fn curve_to(&mut self, _cx0: f32, _cy0: f32, _cx1: f32, _cy1: f32, _x: f32, _y: f32) {
922 self.commands.push('C');
923 }
924
925 fn close(&mut self) {
926 self.commands.push('Z');
927 }
928 }
929
930 fn draw_to_pen(font: &[u8], codepoint: u32, settings: DrawSettings, pen: &mut impl OutlinePen) {
931 let font = FontRef::new(font).unwrap();
932 let gid = font
933 .cmap()
934 .unwrap()
935 .map_codepoint(codepoint)
936 .unwrap_or_else(|| panic!("No gid for 0x{codepoint:04x}"));
937 let outlines = font.outline_glyphs();
938 let outline = outlines.get(gid).unwrap_or_else(|| {
939 panic!(
940 "No outline for {gid:?} in collection of {:?}",
941 outlines.format()
942 )
943 });
944
945 outline.draw(settings, pen).unwrap();
946 }
947
948 fn draw_commands(font: &[u8], codepoint: u32, settings: DrawSettings) -> String {
949 let mut pen = CommandPen::default();
950 draw_to_pen(font, codepoint, settings, &mut pen);
951 pen.commands
952 }
953
954 fn drawn_points(font: &[u8], codepoint: u32, settings: DrawSettings) -> Vec<GlyphPoint> {
955 let mut pen = PointPen::new();
956 draw_to_pen(font, codepoint, settings, &mut pen);
957 pen.into_points()
958 }
959
960 fn insert_implicit_oncurve(pointstream: &[GlyphPoint]) -> Vec<GlyphPoint> {
961 let mut expanded_points = Vec::new();
962
963 for i in 0..pointstream.len() - 1 {
964 expanded_points.push(pointstream[i]);
965 if matches!(
966 (pointstream[i], pointstream[i + 1]),
967 (GlyphPoint::Off { .. }, GlyphPoint::Off { .. })
968 ) {
969 expanded_points.push(pointstream[i].implied_oncurve(pointstream[i + 1]));
970 }
971 }
972
973 expanded_points.push(*pointstream.last().unwrap());
974
975 expanded_points
976 }
977
978 fn as_on_off_sequence(points: &[GlyphPoint]) -> Vec<&'static str> {
979 points
980 .iter()
981 .map(|p| match p {
982 GlyphPoint::On { .. } => "On",
983 GlyphPoint::Off { .. } => "Off",
984 })
985 .collect()
986 }
987
988 #[test]
989 fn always_get_closing_lines() {
990 let period = draw_commands(
992 font_test_data::INTERPOLATE_THIS,
993 PERIOD,
994 Size::unscaled().into(),
995 );
996 let comma = draw_commands(
997 font_test_data::INTERPOLATE_THIS,
998 COMMA,
999 Size::unscaled().into(),
1000 );
1001
1002 assert_eq!(
1003 period, comma,
1004 "Incompatible\nperiod\n{period:#?}\ncomma\n{comma:#?}\n"
1005 );
1006 assert_eq!(
1007 "MLLLZ", period,
1008 "We should get an explicit L for close even when it's a nop"
1009 );
1010 }
1011
1012 #[test]
1013 fn triangle_and_square_retain_compatibility() {
1014 let period = drawn_points(
1016 font_test_data::INTERPOLATE_THIS,
1017 PERIOD,
1018 Size::unscaled().into(),
1019 );
1020 let comma = drawn_points(
1021 font_test_data::INTERPOLATE_THIS,
1022 COMMA,
1023 Size::unscaled().into(),
1024 );
1025
1026 assert_ne!(period, comma);
1027 assert_eq!(
1028 as_on_off_sequence(&period),
1029 as_on_off_sequence(&comma),
1030 "Incompatible\nperiod\n{period:#?}\ncomma\n{comma:#?}\n"
1031 );
1032 assert_eq!(
1033 4,
1034 period.len(),
1035 "we should have the same # of points we started with"
1036 );
1037 }
1038
1039 fn assert_walked_backwards_like_freetype(pointstream: &[GlyphPoint], font: &[u8]) {
1040 assert!(
1041 matches!(pointstream[0], GlyphPoint::Off { .. }),
1042 "Bad testdata, should start off curve"
1043 );
1044
1045 let mut expected_points = pointstream.to_vec();
1047 let last = *expected_points.last().unwrap();
1048 let first_move = if matches!(last, GlyphPoint::Off { .. }) {
1049 expected_points[0].implied_oncurve(last)
1050 } else {
1051 expected_points.pop().unwrap()
1052 };
1053 expected_points.insert(0, first_move);
1054
1055 expected_points = insert_implicit_oncurve(&expected_points);
1056 let actual = drawn_points(font, PERIOD, Size::unscaled().into());
1057 assert_eq!(
1058 expected_points, actual,
1059 "expected\n{expected_points:#?}\nactual\n{actual:#?}"
1060 );
1061 }
1062
1063 fn assert_walked_forwards_like_harfbuzz(pointstream: &[GlyphPoint], font: &[u8]) {
1064 assert!(
1065 matches!(pointstream[0], GlyphPoint::Off { .. }),
1066 "Bad testdata, should start off curve"
1067 );
1068
1069 let mut expected_points = pointstream.to_vec();
1071 let first = expected_points.remove(0);
1072 expected_points.push(first);
1073 if matches!(expected_points[0], GlyphPoint::Off { .. }) {
1074 expected_points.insert(0, first.implied_oncurve(expected_points[0]))
1075 };
1076
1077 expected_points = insert_implicit_oncurve(&expected_points);
1078
1079 let settings: DrawSettings = Size::unscaled().into();
1080 let settings = settings.with_path_style(PathStyle::HarfBuzz);
1081 let actual = drawn_points(font, PERIOD, settings);
1082 assert_eq!(
1083 expected_points, actual,
1084 "expected\n{expected_points:#?}\nactual\n{actual:#?}"
1085 );
1086 }
1087
1088 #[test]
1089 fn starting_off_curve_walk_backwards_like_freetype() {
1090 assert_walked_backwards_like_freetype(
1091 &STARTING_OFF_CURVE_POINTS,
1092 font_test_data::STARTING_OFF_CURVE,
1093 );
1094 }
1095
1096 #[test]
1097 fn mostly_off_curve_walk_backwards_like_freetype() {
1098 assert_walked_backwards_like_freetype(
1099 &MOSTLY_OFF_CURVE_POINTS,
1100 font_test_data::MOSTLY_OFF_CURVE,
1101 );
1102 }
1103
1104 #[test]
1105 fn starting_off_curve_walk_forwards_like_hbdraw() {
1106 assert_walked_forwards_like_harfbuzz(
1107 &STARTING_OFF_CURVE_POINTS,
1108 font_test_data::STARTING_OFF_CURVE,
1109 );
1110 }
1111
1112 #[test]
1113 fn mostly_off_curve_walk_forwards_like_hbdraw() {
1114 assert_walked_forwards_like_harfbuzz(
1115 &MOSTLY_OFF_CURVE_POINTS,
1116 font_test_data::MOSTLY_OFF_CURVE,
1117 );
1118 }
1119
1120 fn icon_loc_off_default(font: &FontRef) -> Location {
1123 font.axes().location(&[
1124 ("wght", 700.0),
1125 ("opsz", 48.0),
1126 ("GRAD", 200.0),
1127 ("FILL", 1.0),
1128 ])
1129 }
1130
1131 fn pt(x: f32, y: f32) -> Point {
1132 (x as f64, y as f64).into()
1133 }
1134
1135 fn svg_commands(elements: &[PathEl]) -> Vec<String> {
1137 elements
1138 .iter()
1139 .map(|e| match e {
1140 PathEl::MoveTo(p) => format!("M{:.2},{:.2}", p.x, p.y),
1141 PathEl::LineTo(p) => format!("L{:.2},{:.2}", p.x, p.y),
1142 PathEl::QuadTo(c0, p) => format!("Q{:.2},{:.2} {:.2},{:.2}", c0.x, c0.y, p.x, p.y),
1143 PathEl::CurveTo(c0, c1, p) => format!(
1144 "C{:.2},{:.2} {:.2},{:.2} {:.2},{:.2}",
1145 c0.x, c0.y, c1.x, c1.y, p.x, p.y
1146 ),
1147 PathEl::ClosePath => "Z".to_string(),
1148 })
1149 .collect()
1150 }
1151
1152 #[derive(Default)]
1154 struct BezPen {
1155 path: BezPath,
1156 }
1157
1158 impl OutlinePen for BezPen {
1159 fn move_to(&mut self, x: f32, y: f32) {
1160 self.path.move_to(pt(x, y));
1161 }
1162
1163 fn line_to(&mut self, x: f32, y: f32) {
1164 self.path.line_to(pt(x, y));
1165 }
1166
1167 fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
1168 self.path.quad_to(pt(cx0, cy0), pt(x, y));
1169 }
1170
1171 fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
1172 self.path.curve_to(pt(cx0, cy0), pt(cx1, cy1), pt(x, y));
1173 }
1174
1175 fn close(&mut self) {
1176 self.path.close_path();
1177 }
1178 }
1179
1180 fn assert_glyph_path_start_with(
1182 font: &FontRef,
1183 gid: GlyphId,
1184 loc: Location,
1185 path_style: PathStyle,
1186 expected_path_start: &[PathEl],
1187 ) {
1188 let glyph = font
1189 .outline_glyphs()
1190 .get(gid)
1191 .unwrap_or_else(|| panic!("No glyph for {gid}"));
1192
1193 let mut pen = BezPen::default();
1194 glyph
1195 .draw(
1196 DrawSettings::unhinted(Size::unscaled(), &loc).with_path_style(path_style),
1197 &mut pen,
1198 )
1199 .unwrap_or_else(|e| panic!("Unable to draw {gid}: {e}"));
1200 let bez = Affine::FLIP_Y * pen.path; let actual_path_start = &bez.elements()[..expected_path_start.len()];
1202 assert_eq!(
1205 svg_commands(expected_path_start),
1206 svg_commands(actual_path_start)
1207 );
1208 }
1209
1210 const MATERIAL_SYMBOL_GID_MAIL_AT_DEFAULT: GlyphId = GlyphId::new(1);
1211 const MATERIAL_SYMBOL_GID_MAIL_OFF_DEFAULT: GlyphId = GlyphId::new(2);
1212
1213 #[test]
1214 fn draw_icon_freetype_style_at_default() {
1215 let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1216 assert_glyph_path_start_with(
1217 &font,
1218 MATERIAL_SYMBOL_GID_MAIL_AT_DEFAULT,
1219 Location::default(),
1220 PathStyle::FreeType,
1221 &[
1222 PathEl::MoveTo((160.0, -160.0).into()),
1223 PathEl::QuadTo((127.0, -160.0).into(), (103.5, -183.5).into()),
1224 PathEl::QuadTo((80.0, -207.0).into(), (80.0, -240.0).into()),
1225 ],
1226 );
1227 }
1228
1229 #[test]
1230 fn draw_icon_harfbuzz_style_at_default() {
1231 let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1232 assert_glyph_path_start_with(
1233 &font,
1234 MATERIAL_SYMBOL_GID_MAIL_AT_DEFAULT,
1235 Location::default(),
1236 PathStyle::HarfBuzz,
1237 &[
1238 PathEl::MoveTo((160.0, -160.0).into()),
1239 PathEl::QuadTo((127.0, -160.0).into(), (103.5, -183.5).into()),
1240 PathEl::QuadTo((80.0, -207.0).into(), (80.0, -240.0).into()),
1241 ],
1242 );
1243 }
1244
1245 #[test]
1246 fn draw_icon_freetype_style_off_default() {
1247 let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1248 assert_glyph_path_start_with(
1249 &font,
1250 MATERIAL_SYMBOL_GID_MAIL_OFF_DEFAULT,
1251 icon_loc_off_default(&font),
1252 PathStyle::FreeType,
1253 &[
1254 PathEl::MoveTo((150.0, -138.0).into()),
1255 PathEl::QuadTo((113.0, -138.0).into(), (86.0, -165.5).into()),
1256 PathEl::QuadTo((59.0, -193.0).into(), (59.0, -229.0).into()),
1257 ],
1258 );
1259 }
1260
1261 #[test]
1262 fn draw_icon_harfbuzz_style_off_default() {
1263 let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1264 assert_glyph_path_start_with(
1265 &font,
1266 MATERIAL_SYMBOL_GID_MAIL_OFF_DEFAULT,
1267 icon_loc_off_default(&font),
1268 PathStyle::HarfBuzz,
1269 &[
1270 PathEl::MoveTo((150.0, -138.0).into()),
1271 PathEl::QuadTo((113.22, -138.0).into(), (86.11, -165.61).into()),
1272 PathEl::QuadTo((59.0, -193.22).into(), (59.0, -229.0).into()),
1273 ],
1274 );
1275 }
1276
1277 const GLYF_COMPONENT_GID_NON_UNIFORM_SCALE: GlyphId = GlyphId::new(3);
1278 const GLYF_COMPONENT_GID_SCALED_COMPONENT_OFFSET: GlyphId = GlyphId::new(7);
1279 const GLYF_COMPONENT_GID_NO_SCALED_COMPONENT_OFFSET: GlyphId = GlyphId::new(8);
1280
1281 #[test]
1282 fn draw_nonuniform_scale_component_freetype() {
1283 let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1284 assert_glyph_path_start_with(
1285 &font,
1286 GLYF_COMPONENT_GID_NON_UNIFORM_SCALE,
1287 Location::default(),
1288 PathStyle::FreeType,
1289 &[
1290 PathEl::MoveTo((-138.0, -185.0).into()),
1291 PathEl::LineTo((-32.0, -259.0).into()),
1292 PathEl::LineTo((26.0, -175.0).into()),
1293 PathEl::LineTo((-80.0, -101.0).into()),
1294 PathEl::ClosePath,
1295 ],
1296 );
1297 }
1298
1299 #[test]
1300 fn draw_nonuniform_scale_component_harfbuzz() {
1301 let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1302 assert_glyph_path_start_with(
1303 &font,
1304 GLYF_COMPONENT_GID_NON_UNIFORM_SCALE,
1305 Location::default(),
1306 PathStyle::HarfBuzz,
1307 &[
1308 PathEl::MoveTo((-137.8, -184.86).into()),
1309 PathEl::LineTo((-32.15, -258.52).into()),
1310 PathEl::LineTo((25.9, -175.24).into()),
1311 PathEl::LineTo((-79.75, -101.58).into()),
1312 PathEl::ClosePath,
1313 ],
1314 );
1315 }
1316
1317 #[test]
1318 fn draw_scaled_component_offset_freetype() {
1319 let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1320 assert_glyph_path_start_with(
1321 &font,
1322 GLYF_COMPONENT_GID_SCALED_COMPONENT_OFFSET,
1323 Location::default(),
1324 PathStyle::FreeType,
1325 &[
1326 PathEl::MoveTo((715.0, -360.0).into()),
1328 ],
1329 );
1330 }
1331
1332 #[test]
1333 fn draw_no_scaled_component_offset_freetype() {
1334 let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1335 assert_glyph_path_start_with(
1336 &font,
1337 GLYF_COMPONENT_GID_NO_SCALED_COMPONENT_OFFSET,
1338 Location::default(),
1339 PathStyle::FreeType,
1340 &[PathEl::MoveTo((705.0, -340.0).into())],
1341 );
1342 }
1343
1344 #[test]
1345 fn draw_scaled_component_offset_harfbuzz() {
1346 let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1347 assert_glyph_path_start_with(
1348 &font,
1349 GLYF_COMPONENT_GID_SCALED_COMPONENT_OFFSET,
1350 Location::default(),
1351 PathStyle::HarfBuzz,
1352 &[
1353 PathEl::MoveTo((714.97, -360.0).into()),
1355 ],
1356 );
1357 }
1358
1359 #[test]
1360 fn draw_no_scaled_component_offset_harfbuzz() {
1361 let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1362 assert_glyph_path_start_with(
1363 &font,
1364 GLYF_COMPONENT_GID_NO_SCALED_COMPONENT_OFFSET,
1365 Location::default(),
1366 PathStyle::HarfBuzz,
1367 &[PathEl::MoveTo((704.97, -340.0).into())],
1368 );
1369 }
1370
1371 #[cfg(feature = "spec_next")]
1372 const CUBIC_GLYPH: GlyphId = GlyphId::new(2);
1373
1374 #[test]
1375 #[cfg(feature = "spec_next")]
1376 fn draw_cubic() {
1377 let font = FontRef::new(font_test_data::CUBIC_GLYF).unwrap();
1378 assert_glyph_path_start_with(
1379 &font,
1380 CUBIC_GLYPH,
1381 Location::default(),
1382 PathStyle::FreeType,
1383 &[
1384 PathEl::MoveTo((278.0, -710.0).into()),
1385 PathEl::LineTo((278.0, -470.0).into()),
1386 PathEl::CurveTo(
1387 (300.0, -500.0).into(),
1388 (800.0, -500.0).into(),
1389 (998.0, -470.0).into(),
1390 ),
1391 PathEl::LineTo((998.0, -710.0).into()),
1392 ],
1393 );
1394 }
1395
1396 #[test]
1400 fn tthint_with_subset() {
1401 let font = FontRef::new(font_test_data::TTHINT_SUBSET).unwrap();
1402 let glyphs = font.outline_glyphs();
1403 let hinting = HintingInstance::new(
1404 &glyphs,
1405 Size::new(16.0),
1406 LocationRef::default(),
1407 HintingOptions::default(),
1408 )
1409 .unwrap();
1410 let glyph = glyphs.get(GlyphId::new(1)).unwrap();
1411 glyph
1413 .draw(DrawSettings::hinted(&hinting, true), &mut BezPen::default())
1414 .unwrap();
1415 }
1416
1417 #[test]
1418 fn empty_glyph_advance_unhinted() {
1419 let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
1420 let outlines = font.outline_glyphs();
1421 let coords = [NormalizedCoord::from_f32(0.5)];
1422 let gid = font.charmap().map(' ').unwrap();
1423 let outline = outlines.get(gid).unwrap();
1424 let advance = outline
1425 .draw(
1426 (Size::new(24.0), LocationRef::new(&coords)),
1427 &mut super::pen::NullPen,
1428 )
1429 .unwrap()
1430 .advance_width
1431 .unwrap();
1432 assert_eq!(advance, 10.796875);
1433 }
1434
1435 #[test]
1436 fn empty_glyph_advance_hinted() {
1437 let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
1438 let outlines = font.outline_glyphs();
1439 let coords = [NormalizedCoord::from_f32(0.5)];
1440 let hinter = HintingInstance::new(
1441 &outlines,
1442 Size::new(24.0),
1443 LocationRef::new(&coords),
1444 HintingOptions::default(),
1445 )
1446 .unwrap();
1447 let gid = font.charmap().map(' ').unwrap();
1448 let outline = outlines.get(gid).unwrap();
1449 let advance = outline
1450 .draw(&hinter, &mut super::pen::NullPen)
1451 .unwrap()
1452 .advance_width
1453 .unwrap();
1454 assert_eq!(advance, 11.0);
1455 }
1456
1457 #[test]
1460 fn fractional_size_hinting_matters() {
1461 let font = FontRef::from_index(font_test_data::TINOS_SUBSET, 0).unwrap();
1462 let mut outlines = font.outline_glyphs();
1463 let instance = HintingInstance::new(
1464 &outlines,
1465 Size::new(24.8),
1466 LocationRef::default(),
1467 HintingOptions::default(),
1468 )
1469 .unwrap();
1470 let gid = GlyphId::new(2);
1471 let outline_with_fractional = {
1472 let OutlineCollectionKind::Glyf(glyf) = &mut outlines.kind else {
1473 panic!("this is definitely a TrueType font");
1474 };
1475 glyf.fractional_size_hinting = true;
1476 let mut pen = SvgPen::new();
1477 let outline = outlines.get(gid).unwrap();
1478 outline.draw(&instance, &mut pen).unwrap();
1479 pen.to_string()
1480 };
1481 let outline_without_fractional = {
1482 let OutlineCollectionKind::Glyf(glyf) = &mut outlines.kind else {
1483 panic!("this is definitely a TrueType font");
1484 };
1485 glyf.fractional_size_hinting = false;
1486 let mut pen = SvgPen::new();
1487 let outline = outlines.get(gid).unwrap();
1488 outline.draw(&instance, &mut pen).unwrap();
1489 pen.to_string()
1490 };
1491 assert_ne!(outline_with_fractional, outline_without_fractional);
1492 }
1493}