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