1pub mod autohint;
82pub(crate) mod cff;
83pub(crate) mod glyf;
84mod hint;
85mod metrics;
86mod unscaled;
87mod varc;
88
89#[cfg(test)]
90mod testing;
91
92pub mod error;
93pub mod pen;
94
95pub use autohint::GlyphStyles;
96pub use hint::{
97 Engine, HintingInstance, HintingMode, HintingOptions, LcdLayout, SmoothMode, Target,
98};
99use metrics::GlyphHMetrics;
100use raw::FontRef;
101#[doc(inline)]
102pub use {error::DrawError, pen::OutlinePen};
103
104struct ErasedPen<'a>(&'a mut dyn OutlinePen);
106
107impl OutlinePen for ErasedPen<'_> {
108 fn move_to(&mut self, x: f32, y: f32) {
109 self.0.move_to(x, y);
110 }
111 fn line_to(&mut self, x: f32, y: f32) {
112 self.0.line_to(x, y);
113 }
114 fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
115 self.0.quad_to(cx, cy, x, y);
116 }
117 fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
118 self.0.curve_to(cx0, cy0, cx1, cy1, x, y);
119 }
120 fn close(&mut self) {
121 self.0.close();
122 }
123}
124
125use self::glyf::{FreeTypeScaler, HarfBuzzScaler};
126use super::instance::{LocationRef, NormalizedCoord, Size};
127use core::fmt::Debug;
128use pen::PathStyle;
129use read_fonts::{types::GlyphId, TableProvider};
130
131#[cfg(feature = "libm")]
132#[allow(unused_imports)]
133use core_maths::CoreFloat;
134
135#[derive(Copy, Clone, PartialEq, Eq, Debug)]
137pub enum OutlineGlyphFormat {
138 Glyf,
140 Cff,
142 Cff2,
144 Varc,
146}
147
148#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
150pub enum Hinting {
151 #[default]
153 None,
154 Embedded,
160}
161
162#[derive(Copy, Clone, Default, Debug)]
168pub struct AdjustedMetrics {
169 pub has_overlaps: bool,
172 pub lsb: Option<f32>,
178 pub advance_width: Option<f32>,
191}
192
193pub struct DrawSettings<'a> {
196 instance: DrawInstance<'a>,
197 memory: Option<&'a mut [u8]>,
198 path_style: PathStyle,
199}
200
201impl<'a> DrawSettings<'a> {
202 pub fn unhinted(size: Size, location: impl Into<LocationRef<'a>>) -> Self {
205 Self {
206 instance: DrawInstance::Unhinted(size, location.into()),
207 memory: None,
208 path_style: PathStyle::default(),
209 }
210 }
211
212 pub fn hinted(instance: &'a HintingInstance, is_pedantic: bool) -> Self {
222 Self {
223 instance: DrawInstance::Hinted {
224 instance,
225 is_pedantic,
226 },
227 memory: None,
228 path_style: PathStyle::default(),
229 }
230 }
231
232 pub fn with_memory(mut self, memory: Option<&'a mut [u8]>) -> Self {
240 self.memory = memory;
241 self
242 }
243
244 pub fn with_path_style(mut self, path_style: PathStyle) -> Self {
248 self.path_style = path_style;
249 self
250 }
251}
252
253enum DrawInstance<'a> {
254 Unhinted(Size, LocationRef<'a>),
255 Hinted {
256 instance: &'a HintingInstance,
257 is_pedantic: bool,
258 },
259}
260
261impl<'a, L> From<(Size, L)> for DrawSettings<'a>
262where
263 L: Into<LocationRef<'a>>,
264{
265 fn from(value: (Size, L)) -> Self {
266 DrawSettings::unhinted(value.0, value.1.into())
267 }
268}
269
270impl From<Size> for DrawSettings<'_> {
271 fn from(value: Size) -> Self {
272 DrawSettings::unhinted(value, LocationRef::default())
273 }
274}
275
276impl<'a> From<&'a HintingInstance> for DrawSettings<'a> {
277 fn from(value: &'a HintingInstance) -> Self {
278 DrawSettings::hinted(value, false)
279 }
280}
281
282#[derive(Clone)]
290pub struct OutlineGlyph<'a> {
291 kind: OutlineKind<'a>,
292}
293
294impl<'a> OutlineGlyph<'a> {
295 pub fn format(&self) -> OutlineGlyphFormat {
297 match &self.kind {
298 OutlineKind::Glyf(..) => OutlineGlyphFormat::Glyf,
299 OutlineKind::Cff(cff, ..) => {
300 if cff.is_cff2() {
301 OutlineGlyphFormat::Cff2
302 } else {
303 OutlineGlyphFormat::Cff
304 }
305 }
306 OutlineKind::Varc(..) => OutlineGlyphFormat::Varc,
307 }
308 }
309
310 pub fn glyph_id(&self) -> GlyphId {
312 match &self.kind {
313 OutlineKind::Glyf(_, glyph) => glyph.glyph_id,
314 OutlineKind::Cff(_, gid, _) => *gid,
315 OutlineKind::Varc(_, outline) => outline.glyph_id,
316 }
317 }
318
319 pub fn has_overlaps(&self) -> Option<bool> {
324 match &self.kind {
325 OutlineKind::Glyf(_, outline) => Some(outline.has_overlaps),
326 _ => None,
327 }
328 }
329
330 pub fn has_hinting(&self) -> Option<bool> {
336 match &self.kind {
337 OutlineKind::Glyf(_, outline) => Some(outline.has_hinting),
338 OutlineKind::Varc(..) => Some(false),
339 _ => None,
340 }
341 }
342
343 pub fn draw_memory_size(&self, hinting: Hinting) -> usize {
359 match &self.kind {
360 OutlineKind::Glyf(_, outline) => outline.required_buffer_size(hinting),
361 OutlineKind::Varc(_, outline) => outline.required_buffer_size(),
362 _ => 0,
363 }
364 }
365
366 pub fn draw<'s>(
369 &self,
370 settings: impl Into<DrawSettings<'a>>,
371 pen: &mut impl OutlinePen,
372 ) -> Result<AdjustedMetrics, DrawError> {
373 let mut pen = ErasedPen(pen);
374 self.draw_dyn(settings.into(), &mut pen)
375 }
376
377 #[inline(never)]
379 fn draw_dyn(
380 &self,
381 settings: DrawSettings<'a>,
382 pen: &mut ErasedPen<'_>,
383 ) -> Result<AdjustedMetrics, DrawError> {
384 match (settings.instance, settings.path_style) {
385 (DrawInstance::Unhinted(size, location), PathStyle::FreeType) => {
386 self.draw_unhinted(size, location, settings.memory, settings.path_style, pen)
387 }
388 (DrawInstance::Unhinted(size, location), PathStyle::HarfBuzz) => {
389 self.draw_unhinted(size, location, settings.memory, settings.path_style, pen)
390 }
391 (
392 DrawInstance::Hinted {
393 instance: hinting_instance,
394 is_pedantic,
395 },
396 PathStyle::FreeType,
397 ) => {
398 if hinting_instance.is_enabled() {
399 hinting_instance.draw(
400 self,
401 settings.memory,
402 settings.path_style,
403 pen,
404 is_pedantic,
405 )
406 } else {
407 let mut metrics = self.draw_unhinted(
408 hinting_instance.size(),
409 hinting_instance.location(),
410 settings.memory,
411 settings.path_style,
412 pen,
413 )?;
414 if let Some(advance) = metrics.advance_width.as_mut() {
417 *advance = advance.round();
418 }
419 Ok(metrics)
420 }
421 }
422 (DrawInstance::Hinted { .. }, PathStyle::HarfBuzz) => {
423 Err(DrawError::HarfBuzzHintingUnsupported)
424 }
425 }
426 }
427
428 fn draw_unhinted(
429 &self,
430 size: Size,
431 location: impl Into<LocationRef<'a>>,
432 user_memory: Option<&mut [u8]>,
433 path_style: PathStyle,
434 pen: &mut impl OutlinePen,
435 ) -> Result<AdjustedMetrics, DrawError> {
436 let ppem = size.ppem();
437 let coords = location.into().effective_coords();
438 match &self.kind {
439 OutlineKind::Glyf(glyf, outline) => {
440 with_temporary_memory(self, Hinting::None, user_memory, |buf| {
441 let (lsb, advance_width) = match path_style {
442 PathStyle::FreeType => {
443 let scaled_outline =
444 FreeTypeScaler::unhinted(glyf, outline, buf, ppem, coords)?
445 .scale(&outline.glyph, outline.glyph_id)?;
446 scaled_outline.to_path(path_style, pen)?;
447 (
448 scaled_outline.adjusted_lsb().to_f32(),
449 scaled_outline.adjusted_advance_width().to_f32(),
450 )
451 }
452 PathStyle::HarfBuzz => {
453 let scaled_outline =
454 HarfBuzzScaler::unhinted(glyf, outline, buf, ppem, coords)?
455 .scale(&outline.glyph, outline.glyph_id)?;
456 scaled_outline.to_path(path_style, pen)?;
457 (
458 scaled_outline.adjusted_lsb(),
459 scaled_outline.adjusted_advance_width(),
460 )
461 }
462 };
463
464 Ok(AdjustedMetrics {
465 has_overlaps: outline.has_overlaps,
466 lsb: Some(lsb),
467 advance_width: Some(advance_width),
468 })
469 })
470 }
471 OutlineKind::Cff(cff, glyph_id, subfont_ix) => {
472 let subfont = cff.subfont(*subfont_ix, ppem, coords)?;
473 let advance_width = cff.draw(&subfont, *glyph_id, coords, false, pen)?;
474 Ok(AdjustedMetrics {
475 has_overlaps: false,
476 lsb: None,
477 advance_width,
478 })
479 }
480 OutlineKind::Varc(varc, outline) => {
481 with_temporary_memory(self, Hinting::None, user_memory, |buf| {
482 varc.draw(outline, buf, size, coords, path_style, pen)?;
483 Ok(AdjustedMetrics::default())
484 })
485 }
486 }
487 }
488
489 #[allow(dead_code)]
492 fn draw_unscaled(
493 &self,
494 location: impl Into<LocationRef<'a>>,
495 user_memory: Option<&mut [u8]>,
496 sink: &mut impl unscaled::UnscaledOutlineSink,
497 ) -> Result<i32, DrawError> {
498 let coords = location.into().effective_coords();
499 let ppem = None;
500 match &self.kind {
501 OutlineKind::Glyf(glyf, outline) => {
502 with_temporary_memory(self, Hinting::None, user_memory, |buf| {
503 let outline = FreeTypeScaler::unhinted(glyf, outline, buf, ppem, coords)?
504 .scale(&outline.glyph, outline.glyph_id)?;
505 let mut contour_start = 0;
506 for contour_end in outline.contours.iter().map(|contour| *contour as usize) {
507 if contour_end >= contour_start {
508 if let Some(points) = outline.points.get(contour_start..=contour_end) {
509 let flags = &outline.flags[contour_start..=contour_end];
510 sink.start_contour();
511 for (point, flags) in points.iter().zip(flags) {
512 sink.point(
513 point.x.to_bits() >> 6,
514 point.y.to_bits() >> 6,
515 flags.without_markers(),
516 );
517 }
518 sink.end_contour();
519 }
520 }
521 contour_start = contour_end + 1;
522 }
523 Ok(outline.adjusted_advance_width().to_bits() >> 6)
524 })
525 }
526 OutlineKind::Cff(cff, glyph_id, subfont_ix) => {
527 let subfont = cff.subfont(*subfont_ix, ppem, coords)?;
528 let mut adapter = unscaled::PointsFromPath::new(sink);
529 cff.draw(&subfont, *glyph_id, coords, false, &mut adapter)?;
530 adapter.finish();
531 let advance = cff.glyph_metrics.advance_width(*glyph_id, coords);
532 Ok(advance)
533 }
534 OutlineKind::Varc(varc, outline) => {
535 with_temporary_memory(self, Hinting::None, user_memory, |buf| {
536 let mut adapter = unscaled::PointsFromPath::new(sink);
537 let advance = varc.draw_unscaled(outline, buf, coords, &mut adapter)?;
538 adapter.finish();
539 Ok(advance)
540 })
541 }
542 }
543 }
544
545 pub fn with_scaled_glyf_outline<R>(
547 &self,
548 size: Size,
549 location: impl Into<LocationRef<'a>>,
550 user_memory: Option<&mut [u8]>,
551 mut callback: impl FnMut(&glyf::ScaledOutline<'_, raw::types::F26Dot6>) -> Result<R, DrawError>,
552 ) -> Result<R, DrawError> {
553 let ppem = size.ppem();
554 let coords = location.into().effective_coords();
555
556 match &self.kind {
557 OutlineKind::Glyf(glyf, outline) => {
558 with_temporary_memory(self, Hinting::None, user_memory, |buf| {
559 let scaled = FreeTypeScaler::unhinted(glyf, outline, buf, ppem, coords)?
560 .scale(&outline.glyph, outline.glyph_id)?;
561 callback(&scaled)
562 })
563 }
564 _ => Err(DrawError::NoSources),
565 }
566 }
567
568 pub(crate) fn font(&self) -> &FontRef<'a> {
569 match &self.kind {
570 OutlineKind::Glyf(glyf, ..) => &glyf.font,
571 OutlineKind::Cff(cff, ..) => &cff.font,
572 OutlineKind::Varc(varc, ..) => varc.font(),
573 }
574 }
575
576 fn units_per_em(&self) -> u16 {
577 match &self.kind {
578 OutlineKind::Cff(cff, ..) => cff.units_per_em(),
579 OutlineKind::Glyf(glyf, ..) => glyf.units_per_em(),
580 OutlineKind::Varc(varc, ..) => varc.units_per_em(),
581 }
582 }
583}
584
585#[derive(Clone)]
586enum OutlineKind<'a> {
587 Glyf(glyf::Outlines<'a>, glyf::Outline<'a>),
588 Cff(cff::Outlines<'a>, GlyphId, u32),
590 Varc(varc::Outlines<'a>, varc::Outline),
591}
592
593impl Debug for OutlineKind<'_> {
594 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
595 match self {
596 Self::Glyf(_, outline) => f.debug_tuple("Glyf").field(&outline.glyph_id).finish(),
597 Self::Cff(_, gid, subfont_index) => f
598 .debug_tuple("Cff")
599 .field(gid)
600 .field(subfont_index)
601 .finish(),
602 Self::Varc(_, outline) => f.debug_tuple("Varc").field(&outline.glyph_id).finish(),
603 }
604 }
605}
606
607#[derive(Debug, Clone)]
609pub struct OutlineGlyphCollection<'a> {
610 kind: OutlineCollectionKind<'a>,
611}
612
613impl<'a> OutlineGlyphCollection<'a> {
614 pub fn new(font: &FontRef<'a>) -> Self {
616 let kind = if let Some(varc) = varc::Outlines::new(font) {
617 OutlineCollectionKind::Varc(varc)
618 } else if let Some(glyf) = glyf::Outlines::new(font) {
619 OutlineCollectionKind::Glyf(glyf)
620 } else if let Some(cff) = cff::Outlines::new(font) {
621 OutlineCollectionKind::Cff(cff)
622 } else {
623 OutlineCollectionKind::None
624 };
625 Self { kind }
626 }
627
628 pub fn with_format(font: &FontRef<'a>, format: OutlineGlyphFormat) -> Option<Self> {
634 let kind = match format {
635 OutlineGlyphFormat::Glyf => OutlineCollectionKind::Glyf(glyf::Outlines::new(font)?),
636 OutlineGlyphFormat::Cff => {
637 let upem = font.head().ok()?.units_per_em();
638 OutlineCollectionKind::Cff(cff::Outlines::from_cff(font, upem)?)
639 }
640 OutlineGlyphFormat::Cff2 => {
641 let upem = font.head().ok()?.units_per_em();
642 OutlineCollectionKind::Cff(cff::Outlines::from_cff2(font, upem)?)
643 }
644 OutlineGlyphFormat::Varc => OutlineCollectionKind::Varc(varc::Outlines::new(font)?),
645 };
646 Some(Self { kind })
647 }
648
649 pub fn format(&self) -> Option<OutlineGlyphFormat> {
651 match &self.kind {
652 OutlineCollectionKind::Glyf(..) => Some(OutlineGlyphFormat::Glyf),
653 OutlineCollectionKind::Cff(cff) => cff
654 .is_cff2()
655 .then_some(OutlineGlyphFormat::Cff2)
656 .or(Some(OutlineGlyphFormat::Cff)),
657 OutlineCollectionKind::Varc(..) => Some(OutlineGlyphFormat::Varc),
658 OutlineCollectionKind::None => None,
659 }
660 }
661
662 pub fn get(&self, glyph_id: GlyphId) -> Option<OutlineGlyph<'a>> {
664 match &self.kind {
665 OutlineCollectionKind::None => None,
666 OutlineCollectionKind::Glyf(glyf) => Some(OutlineGlyph {
667 kind: OutlineKind::Glyf(glyf.clone(), glyf.outline(glyph_id).ok()?),
668 }),
669 OutlineCollectionKind::Cff(cff) => Some(OutlineGlyph {
670 kind: OutlineKind::Cff(cff.clone(), glyph_id, cff.subfont_index(glyph_id)),
671 }),
672 OutlineCollectionKind::Varc(varc) => Some(OutlineGlyph {
673 kind: if let Some(outline) = varc.outline(glyph_id).ok()? {
674 OutlineKind::Varc(varc.clone(), outline)
675 } else {
676 varc.fallback_outline_kind(glyph_id)?
677 },
678 }),
679 }
680 }
681
682 pub fn iter(&self) -> impl Iterator<Item = (GlyphId, OutlineGlyph<'a>)> + 'a + Clone {
684 let len = match &self.kind {
685 OutlineCollectionKind::Glyf(glyf) => glyf.glyph_count() as u32,
686 OutlineCollectionKind::Cff(cff) => cff.glyph_count() as u32,
687 OutlineCollectionKind::Varc(varc) => varc.glyph_count(),
688 OutlineCollectionKind::None => 0,
689 };
690 let copy = self.clone();
691 (0..len).filter_map(move |gid| {
692 let gid = GlyphId::from(gid);
693 let glyph = copy.get(gid)?;
694 Some((gid, glyph))
695 })
696 }
697
698 pub fn prefer_interpreter(&self) -> bool {
712 match &self.kind {
713 OutlineCollectionKind::Glyf(glyf) => glyf.prefer_interpreter(),
714 OutlineCollectionKind::Varc(varc) => varc.prefer_interpreter(),
715 _ => true,
716 }
717 }
718
719 pub fn require_interpreter(&self) -> bool {
735 self.font()
736 .map(raw::tables::glyf::is_hint_reliant)
737 .unwrap_or_default()
738 }
739
740 pub fn fractional_size_hinting(&self) -> bool {
745 match &self.kind {
746 OutlineCollectionKind::Glyf(glyf) => glyf.fractional_size_hinting,
747 OutlineCollectionKind::Varc(varc) => varc.fractional_size_hinting(),
748 _ => true,
749 }
750 }
751
752 pub(crate) fn stored_glyph_count(&self) -> u32 {
755 match &self.kind {
756 OutlineCollectionKind::Glyf(glyf) => glyf.stored_glyph_count(),
757 OutlineCollectionKind::Cff(cff) => cff.glyph_count() as u32,
758 OutlineCollectionKind::Varc(varc) => varc.glyph_count(),
759 OutlineCollectionKind::None => 0,
760 }
761 }
762
763 pub(crate) fn font(&self) -> Option<&FontRef<'a>> {
764 match &self.kind {
765 OutlineCollectionKind::Glyf(glyf) => Some(&glyf.font),
766 OutlineCollectionKind::Cff(cff) => Some(&cff.font),
767 OutlineCollectionKind::Varc(varc) => Some(varc.font()),
768 OutlineCollectionKind::None => None,
769 }
770 }
771}
772
773#[derive(Clone)]
774enum OutlineCollectionKind<'a> {
775 None,
776 Glyf(glyf::Outlines<'a>),
777 Cff(cff::Outlines<'a>),
778 Varc(varc::Outlines<'a>),
779}
780
781impl Debug for OutlineCollectionKind<'_> {
782 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
783 match self {
784 Self::None => write!(f, "None"),
785 Self::Glyf(..) => f.debug_tuple("Glyf").finish(),
786 Self::Cff(..) => f.debug_tuple("Cff").finish(),
787 Self::Varc(..) => f.debug_tuple("Varc").finish(),
788 }
789 }
790}
791
792pub(super) fn with_temporary_memory<R>(
795 outline: &OutlineGlyph<'_>,
796 hinting: Hinting,
797 memory: Option<&mut [u8]>,
798 mut f: impl FnMut(&mut [u8]) -> R,
799) -> R {
800 match memory {
801 Some(buf) => f(buf),
802 None => {
803 let buf_size = outline.draw_memory_size(hinting);
804 read_fonts::mem::with_scratch(buf_size, f)
805 }
806 }
807}
808
809#[cfg(test)]
810mod tests {
811 use super::*;
812 use crate::outline::pen::NullPen;
813 use crate::{instance::Location, outline::pen::SvgPen, MetadataProvider};
814 use kurbo::{Affine, BezPath, PathEl, Point};
815 use read_fonts::{types::GlyphId, FontRef, TableProvider};
816
817 use pretty_assertions::assert_eq;
818
819 const PERIOD: u32 = 0x2E_u32;
820 const COMMA: u32 = 0x2C_u32;
821
822 #[test]
823 fn outline_glyph_formats() {
824 let font_format_pairs = [
825 (font_test_data::VAZIRMATN_VAR, OutlineGlyphFormat::Glyf),
826 (
827 font_test_data::CANTARELL_VF_TRIMMED,
828 OutlineGlyphFormat::Cff2,
829 ),
830 (
831 font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
832 OutlineGlyphFormat::Cff,
833 ),
834 (font_test_data::COLRV0V1_VARIABLE, OutlineGlyphFormat::Glyf),
835 ];
836 for (font_data, format) in font_format_pairs {
837 assert_eq!(
838 FontRef::new(font_data).unwrap().outline_glyphs().format(),
839 Some(format)
840 );
841 }
842 }
843
844 #[test]
845 fn vazirmatin_var() {
846 compare_glyphs(
847 font_test_data::VAZIRMATN_VAR,
848 font_test_data::VAZIRMATN_VAR_GLYPHS,
849 );
850 }
851
852 #[test]
853 fn cantarell_vf() {
854 compare_glyphs(
855 font_test_data::CANTARELL_VF_TRIMMED,
856 font_test_data::CANTARELL_VF_TRIMMED_GLYPHS,
857 );
858 }
859
860 #[test]
861 fn noto_serif_display() {
862 compare_glyphs(
863 font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
864 font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS,
865 );
866 }
867
868 #[test]
869 fn overlap_flags() {
870 let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
871 let outlines = font.outline_glyphs();
872 let glyph_count = font.maxp().unwrap().num_glyphs();
873 let expected_gids_with_overlap = vec![2, 3];
876 assert_eq!(
877 expected_gids_with_overlap,
878 (0..glyph_count)
879 .filter(
880 |gid| outlines.get(GlyphId::from(*gid)).unwrap().has_overlaps() == Some(true)
881 )
882 .collect::<Vec<_>>()
883 );
884 }
885
886 fn compare_glyphs(font_data: &[u8], expected_outlines: &str) {
887 let font = FontRef::new(font_data).unwrap();
888 let expected_outlines = testing::parse_glyph_outlines(expected_outlines);
889 let mut path = testing::Path::default();
890 for expected_outline in &expected_outlines {
891 if expected_outline.size == 0.0 && !expected_outline.coords.is_empty() {
892 continue;
893 }
894 let size = if expected_outline.size != 0.0 {
895 Size::new(expected_outline.size)
896 } else {
897 Size::unscaled()
898 };
899 path.elements.clear();
900 font.outline_glyphs()
901 .get(expected_outline.glyph_id)
902 .unwrap()
903 .draw(
904 DrawSettings::unhinted(size, expected_outline.coords.as_slice()),
905 &mut path,
906 )
907 .unwrap();
908 assert_eq!(path.elements, expected_outline.path, "mismatch in glyph path for id {} (size: {}, coords: {:?}): path: {:?} expected_path: {:?}",
909 expected_outline.glyph_id,
910 expected_outline.size,
911 expected_outline.coords,
912 &path.elements,
913 &expected_outline.path
914 );
915 }
916 }
917
918 #[derive(Copy, Clone, Debug, PartialEq)]
919 enum GlyphPoint {
920 On { x: f32, y: f32 },
921 Off { x: f32, y: f32 },
922 }
923
924 impl GlyphPoint {
925 fn implied_oncurve(&self, other: Self) -> Self {
926 let (x1, y1) = self.xy();
927 let (x2, y2) = other.xy();
928 Self::On {
929 x: (x1 + x2) / 2.0,
930 y: (y1 + y2) / 2.0,
931 }
932 }
933
934 fn xy(&self) -> (f32, f32) {
935 match self {
936 GlyphPoint::On { x, y } | GlyphPoint::Off { x, y } => (*x, *y),
937 }
938 }
939 }
940
941 #[derive(Debug)]
942 struct PointPen {
943 points: Vec<GlyphPoint>,
944 }
945
946 impl PointPen {
947 fn new() -> Self {
948 Self { points: Vec::new() }
949 }
950
951 fn into_points(self) -> Vec<GlyphPoint> {
952 self.points
953 }
954 }
955
956 impl OutlinePen for PointPen {
957 fn move_to(&mut self, x: f32, y: f32) {
958 self.points.push(GlyphPoint::On { x, y });
959 }
960
961 fn line_to(&mut self, x: f32, y: f32) {
962 self.points.push(GlyphPoint::On { x, y });
963 }
964
965 fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
966 self.points.push(GlyphPoint::Off { x: cx0, y: cy0 });
967 self.points.push(GlyphPoint::On { x, y });
968 }
969
970 fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
971 self.points.push(GlyphPoint::Off { x: cx0, y: cy0 });
972 self.points.push(GlyphPoint::Off { x: cx1, y: cy1 });
973 self.points.push(GlyphPoint::On { x, y });
974 }
975
976 fn close(&mut self) {
977 let np = self.points.len();
984 if np > 2
986 && self.points[0] == self.points[np - 1]
987 && matches!(
988 (self.points[0], self.points[np - 2]),
989 (GlyphPoint::On { .. }, GlyphPoint::Off { .. })
990 )
991 {
992 self.points.pop();
993 }
994 }
995 }
996
997 const STARTING_OFF_CURVE_POINTS: [GlyphPoint; 4] = [
998 GlyphPoint::Off { x: 278.0, y: 710.0 },
999 GlyphPoint::On { x: 278.0, y: 470.0 },
1000 GlyphPoint::On { x: 998.0, y: 470.0 },
1001 GlyphPoint::On { x: 998.0, y: 710.0 },
1002 ];
1003
1004 const MOSTLY_OFF_CURVE_POINTS: [GlyphPoint; 5] = [
1005 GlyphPoint::Off { x: 278.0, y: 710.0 },
1006 GlyphPoint::Off { x: 278.0, y: 470.0 },
1007 GlyphPoint::On { x: 998.0, y: 470.0 },
1008 GlyphPoint::Off { x: 998.0, y: 710.0 },
1009 GlyphPoint::Off { x: 750.0, y: 500.0 },
1010 ];
1011
1012 #[derive(Default, Debug)]
1016 struct CommandPen {
1017 commands: String,
1018 }
1019
1020 impl OutlinePen for CommandPen {
1021 fn move_to(&mut self, _x: f32, _y: f32) {
1022 self.commands.push('M');
1023 }
1024
1025 fn line_to(&mut self, _x: f32, _y: f32) {
1026 self.commands.push('L');
1027 }
1028
1029 fn quad_to(&mut self, _cx0: f32, _cy0: f32, _x: f32, _y: f32) {
1030 self.commands.push('Q');
1031 }
1032
1033 fn curve_to(&mut self, _cx0: f32, _cy0: f32, _cx1: f32, _cy1: f32, _x: f32, _y: f32) {
1034 self.commands.push('C');
1035 }
1036
1037 fn close(&mut self) {
1038 self.commands.push('Z');
1039 }
1040 }
1041
1042 fn draw_to_pen(font: &[u8], codepoint: u32, settings: DrawSettings, pen: &mut impl OutlinePen) {
1043 let font = FontRef::new(font).unwrap();
1044 let gid = font
1045 .cmap()
1046 .unwrap()
1047 .map_codepoint(codepoint)
1048 .unwrap_or_else(|| panic!("No gid for 0x{codepoint:04x}"));
1049 let outlines = font.outline_glyphs();
1050 let outline = outlines.get(gid).unwrap_or_else(|| {
1051 panic!(
1052 "No outline for {gid:?} in collection of {:?}",
1053 outlines.format()
1054 )
1055 });
1056
1057 outline.draw(settings, pen).unwrap();
1058 }
1059
1060 fn draw_commands(font: &[u8], codepoint: u32, settings: DrawSettings) -> String {
1061 let mut pen = CommandPen::default();
1062 draw_to_pen(font, codepoint, settings, &mut pen);
1063 pen.commands
1064 }
1065
1066 fn drawn_points(font: &[u8], codepoint: u32, settings: DrawSettings) -> Vec<GlyphPoint> {
1067 let mut pen = PointPen::new();
1068 draw_to_pen(font, codepoint, settings, &mut pen);
1069 pen.into_points()
1070 }
1071
1072 fn insert_implicit_oncurve(pointstream: &[GlyphPoint]) -> Vec<GlyphPoint> {
1073 let mut expanded_points = Vec::new();
1074
1075 for i in 0..pointstream.len() - 1 {
1076 expanded_points.push(pointstream[i]);
1077 if matches!(
1078 (pointstream[i], pointstream[i + 1]),
1079 (GlyphPoint::Off { .. }, GlyphPoint::Off { .. })
1080 ) {
1081 expanded_points.push(pointstream[i].implied_oncurve(pointstream[i + 1]));
1082 }
1083 }
1084
1085 expanded_points.push(*pointstream.last().unwrap());
1086
1087 expanded_points
1088 }
1089
1090 fn as_on_off_sequence(points: &[GlyphPoint]) -> Vec<&'static str> {
1091 points
1092 .iter()
1093 .map(|p| match p {
1094 GlyphPoint::On { .. } => "On",
1095 GlyphPoint::Off { .. } => "Off",
1096 })
1097 .collect()
1098 }
1099
1100 #[test]
1101 fn always_get_closing_lines() {
1102 let period = draw_commands(
1104 font_test_data::INTERPOLATE_THIS,
1105 PERIOD,
1106 Size::unscaled().into(),
1107 );
1108 let comma = draw_commands(
1109 font_test_data::INTERPOLATE_THIS,
1110 COMMA,
1111 Size::unscaled().into(),
1112 );
1113
1114 assert_eq!(
1115 period, comma,
1116 "Incompatible\nperiod\n{period:#?}\ncomma\n{comma:#?}\n"
1117 );
1118 assert_eq!(
1119 "MLLLZ", period,
1120 "We should get an explicit L for close even when it's a nop"
1121 );
1122 }
1123
1124 #[test]
1125 fn triangle_and_square_retain_compatibility() {
1126 let period = drawn_points(
1128 font_test_data::INTERPOLATE_THIS,
1129 PERIOD,
1130 Size::unscaled().into(),
1131 );
1132 let comma = drawn_points(
1133 font_test_data::INTERPOLATE_THIS,
1134 COMMA,
1135 Size::unscaled().into(),
1136 );
1137
1138 assert_ne!(period, comma);
1139 assert_eq!(
1140 as_on_off_sequence(&period),
1141 as_on_off_sequence(&comma),
1142 "Incompatible\nperiod\n{period:#?}\ncomma\n{comma:#?}\n"
1143 );
1144 assert_eq!(
1145 4,
1146 period.len(),
1147 "we should have the same # of points we started with"
1148 );
1149 }
1150
1151 fn assert_walked_backwards_like_freetype(pointstream: &[GlyphPoint], font: &[u8]) {
1152 assert!(
1153 matches!(pointstream[0], GlyphPoint::Off { .. }),
1154 "Bad testdata, should start off curve"
1155 );
1156
1157 let mut expected_points = pointstream.to_vec();
1159 let last = *expected_points.last().unwrap();
1160 let first_move = if matches!(last, GlyphPoint::Off { .. }) {
1161 expected_points[0].implied_oncurve(last)
1162 } else {
1163 expected_points.pop().unwrap()
1164 };
1165 expected_points.insert(0, first_move);
1166
1167 expected_points = insert_implicit_oncurve(&expected_points);
1168 let actual = drawn_points(font, PERIOD, Size::unscaled().into());
1169 assert_eq!(
1170 expected_points, actual,
1171 "expected\n{expected_points:#?}\nactual\n{actual:#?}"
1172 );
1173 }
1174
1175 fn assert_walked_forwards_like_harfbuzz(pointstream: &[GlyphPoint], font: &[u8]) {
1176 assert!(
1177 matches!(pointstream[0], GlyphPoint::Off { .. }),
1178 "Bad testdata, should start off curve"
1179 );
1180
1181 let mut expected_points = pointstream.to_vec();
1183 let first = expected_points.remove(0);
1184 expected_points.push(first);
1185 if matches!(expected_points[0], GlyphPoint::Off { .. }) {
1186 expected_points.insert(0, first.implied_oncurve(expected_points[0]))
1187 };
1188
1189 expected_points = insert_implicit_oncurve(&expected_points);
1190
1191 let settings: DrawSettings = Size::unscaled().into();
1192 let settings = settings.with_path_style(PathStyle::HarfBuzz);
1193 let actual = drawn_points(font, PERIOD, settings);
1194 assert_eq!(
1195 expected_points, actual,
1196 "expected\n{expected_points:#?}\nactual\n{actual:#?}"
1197 );
1198 }
1199
1200 #[test]
1201 fn starting_off_curve_walk_backwards_like_freetype() {
1202 assert_walked_backwards_like_freetype(
1203 &STARTING_OFF_CURVE_POINTS,
1204 font_test_data::STARTING_OFF_CURVE,
1205 );
1206 }
1207
1208 #[test]
1209 fn mostly_off_curve_walk_backwards_like_freetype() {
1210 assert_walked_backwards_like_freetype(
1211 &MOSTLY_OFF_CURVE_POINTS,
1212 font_test_data::MOSTLY_OFF_CURVE,
1213 );
1214 }
1215
1216 #[test]
1217 fn starting_off_curve_walk_forwards_like_hbdraw() {
1218 assert_walked_forwards_like_harfbuzz(
1219 &STARTING_OFF_CURVE_POINTS,
1220 font_test_data::STARTING_OFF_CURVE,
1221 );
1222 }
1223
1224 #[test]
1225 fn mostly_off_curve_walk_forwards_like_hbdraw() {
1226 assert_walked_forwards_like_harfbuzz(
1227 &MOSTLY_OFF_CURVE_POINTS,
1228 font_test_data::MOSTLY_OFF_CURVE,
1229 );
1230 }
1231
1232 fn icon_loc_off_default(font: &FontRef) -> Location {
1235 font.axes().location(&[
1236 ("wght", 700.0),
1237 ("opsz", 48.0),
1238 ("GRAD", 200.0),
1239 ("FILL", 1.0),
1240 ])
1241 }
1242
1243 fn pt(x: f32, y: f32) -> Point {
1244 (x as f64, y as f64).into()
1245 }
1246
1247 fn svg_commands(elements: &[PathEl]) -> Vec<String> {
1249 elements
1250 .iter()
1251 .map(|e| match e {
1252 PathEl::MoveTo(p) => format!("M{:.2},{:.2}", p.x, p.y),
1253 PathEl::LineTo(p) => format!("L{:.2},{:.2}", p.x, p.y),
1254 PathEl::QuadTo(c0, p) => format!("Q{:.2},{:.2} {:.2},{:.2}", c0.x, c0.y, p.x, p.y),
1255 PathEl::CurveTo(c0, c1, p) => format!(
1256 "C{:.2},{:.2} {:.2},{:.2} {:.2},{:.2}",
1257 c0.x, c0.y, c1.x, c1.y, p.x, p.y
1258 ),
1259 PathEl::ClosePath => "Z".to_string(),
1260 })
1261 .collect()
1262 }
1263
1264 #[derive(Default)]
1266 struct BezPen {
1267 path: BezPath,
1268 }
1269
1270 impl OutlinePen for BezPen {
1271 fn move_to(&mut self, x: f32, y: f32) {
1272 self.path.move_to(pt(x, y));
1273 }
1274
1275 fn line_to(&mut self, x: f32, y: f32) {
1276 self.path.line_to(pt(x, y));
1277 }
1278
1279 fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
1280 self.path.quad_to(pt(cx0, cy0), pt(x, y));
1281 }
1282
1283 fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
1284 self.path.curve_to(pt(cx0, cy0), pt(cx1, cy1), pt(x, y));
1285 }
1286
1287 fn close(&mut self) {
1288 self.path.close_path();
1289 }
1290 }
1291
1292 fn assert_glyph_path_start_with(
1294 font: &FontRef,
1295 gid: GlyphId,
1296 loc: Location,
1297 path_style: PathStyle,
1298 expected_path_start: &[PathEl],
1299 ) {
1300 let glyph = font
1301 .outline_glyphs()
1302 .get(gid)
1303 .unwrap_or_else(|| panic!("No glyph for {gid}"));
1304
1305 let mut pen = BezPen::default();
1306 glyph
1307 .draw(
1308 DrawSettings::unhinted(Size::unscaled(), &loc).with_path_style(path_style),
1309 &mut pen,
1310 )
1311 .unwrap_or_else(|e| panic!("Unable to draw {gid}: {e}"));
1312 let bez = Affine::FLIP_Y * pen.path; let actual_path_start = &bez.elements()[..expected_path_start.len()];
1314 assert_eq!(
1317 svg_commands(expected_path_start),
1318 svg_commands(actual_path_start)
1319 );
1320 }
1321
1322 const MATERIAL_SYMBOL_GID_MAIL_AT_DEFAULT: GlyphId = GlyphId::new(1);
1323 const MATERIAL_SYMBOL_GID_MAIL_OFF_DEFAULT: GlyphId = GlyphId::new(2);
1324
1325 #[test]
1326 fn draw_icon_freetype_style_at_default() {
1327 let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1328 assert_glyph_path_start_with(
1329 &font,
1330 MATERIAL_SYMBOL_GID_MAIL_AT_DEFAULT,
1331 Location::default(),
1332 PathStyle::FreeType,
1333 &[
1334 PathEl::MoveTo((160.0, -160.0).into()),
1335 PathEl::QuadTo((127.0, -160.0).into(), (103.5, -183.5).into()),
1336 PathEl::QuadTo((80.0, -207.0).into(), (80.0, -240.0).into()),
1337 ],
1338 );
1339 }
1340
1341 #[test]
1342 fn draw_icon_harfbuzz_style_at_default() {
1343 let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1344 assert_glyph_path_start_with(
1345 &font,
1346 MATERIAL_SYMBOL_GID_MAIL_AT_DEFAULT,
1347 Location::default(),
1348 PathStyle::HarfBuzz,
1349 &[
1350 PathEl::MoveTo((160.0, -160.0).into()),
1351 PathEl::QuadTo((127.0, -160.0).into(), (103.5, -183.5).into()),
1352 PathEl::QuadTo((80.0, -207.0).into(), (80.0, -240.0).into()),
1353 ],
1354 );
1355 }
1356
1357 #[test]
1358 fn draw_icon_freetype_style_off_default() {
1359 let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1360 assert_glyph_path_start_with(
1361 &font,
1362 MATERIAL_SYMBOL_GID_MAIL_OFF_DEFAULT,
1363 icon_loc_off_default(&font),
1364 PathStyle::FreeType,
1365 &[
1366 PathEl::MoveTo((150.0, -138.0).into()),
1367 PathEl::QuadTo((113.0, -138.0).into(), (86.0, -165.5).into()),
1368 PathEl::QuadTo((59.0, -193.0).into(), (59.0, -229.0).into()),
1369 ],
1370 );
1371 }
1372
1373 #[test]
1374 fn draw_icon_harfbuzz_style_off_default() {
1375 let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1376 assert_glyph_path_start_with(
1377 &font,
1378 MATERIAL_SYMBOL_GID_MAIL_OFF_DEFAULT,
1379 icon_loc_off_default(&font),
1380 PathStyle::HarfBuzz,
1381 &[
1382 PathEl::MoveTo((150.0, -138.0).into()),
1383 PathEl::QuadTo((113.22, -138.0).into(), (86.11, -165.61).into()),
1384 PathEl::QuadTo((59.0, -193.22).into(), (59.0, -229.0).into()),
1385 ],
1386 );
1387 }
1388
1389 const GLYF_COMPONENT_GID_NON_UNIFORM_SCALE: GlyphId = GlyphId::new(3);
1390 const GLYF_COMPONENT_GID_SCALED_COMPONENT_OFFSET: GlyphId = GlyphId::new(7);
1391 const GLYF_COMPONENT_GID_NO_SCALED_COMPONENT_OFFSET: GlyphId = GlyphId::new(8);
1392
1393 #[test]
1394 fn draw_nonuniform_scale_component_freetype() {
1395 let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1396 assert_glyph_path_start_with(
1397 &font,
1398 GLYF_COMPONENT_GID_NON_UNIFORM_SCALE,
1399 Location::default(),
1400 PathStyle::FreeType,
1401 &[
1402 PathEl::MoveTo((-138.0, -185.0).into()),
1403 PathEl::LineTo((-32.0, -259.0).into()),
1404 PathEl::LineTo((26.0, -175.0).into()),
1405 PathEl::LineTo((-80.0, -101.0).into()),
1406 PathEl::ClosePath,
1407 ],
1408 );
1409 }
1410
1411 #[test]
1412 fn draw_nonuniform_scale_component_harfbuzz() {
1413 let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1414 assert_glyph_path_start_with(
1415 &font,
1416 GLYF_COMPONENT_GID_NON_UNIFORM_SCALE,
1417 Location::default(),
1418 PathStyle::HarfBuzz,
1419 &[
1420 PathEl::MoveTo((-137.8, -184.86).into()),
1421 PathEl::LineTo((-32.15, -258.52).into()),
1422 PathEl::LineTo((25.9, -175.24).into()),
1423 PathEl::LineTo((-79.75, -101.58).into()),
1424 PathEl::ClosePath,
1425 ],
1426 );
1427 }
1428
1429 #[test]
1430 fn draw_scaled_component_offset_freetype() {
1431 let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1432 assert_glyph_path_start_with(
1433 &font,
1434 GLYF_COMPONENT_GID_SCALED_COMPONENT_OFFSET,
1435 Location::default(),
1436 PathStyle::FreeType,
1437 &[
1438 PathEl::MoveTo((715.0, -360.0).into()),
1440 ],
1441 );
1442 }
1443
1444 #[test]
1445 fn draw_no_scaled_component_offset_freetype() {
1446 let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1447 assert_glyph_path_start_with(
1448 &font,
1449 GLYF_COMPONENT_GID_NO_SCALED_COMPONENT_OFFSET,
1450 Location::default(),
1451 PathStyle::FreeType,
1452 &[PathEl::MoveTo((705.0, -340.0).into())],
1453 );
1454 }
1455
1456 #[test]
1457 fn draw_scaled_component_offset_harfbuzz() {
1458 let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1459 assert_glyph_path_start_with(
1460 &font,
1461 GLYF_COMPONENT_GID_SCALED_COMPONENT_OFFSET,
1462 Location::default(),
1463 PathStyle::HarfBuzz,
1464 &[
1465 PathEl::MoveTo((714.97, -360.0).into()),
1467 ],
1468 );
1469 }
1470
1471 #[test]
1472 fn draw_no_scaled_component_offset_harfbuzz() {
1473 let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1474 assert_glyph_path_start_with(
1475 &font,
1476 GLYF_COMPONENT_GID_NO_SCALED_COMPONENT_OFFSET,
1477 Location::default(),
1478 PathStyle::HarfBuzz,
1479 &[PathEl::MoveTo((704.97, -340.0).into())],
1480 );
1481 }
1482
1483 const CUBIC_GLYPH: GlyphId = GlyphId::new(2);
1484
1485 #[test]
1486 fn draw_cubic() {
1487 let font = FontRef::new(font_test_data::CUBIC_GLYF).unwrap();
1488 assert_glyph_path_start_with(
1489 &font,
1490 CUBIC_GLYPH,
1491 Location::default(),
1492 PathStyle::FreeType,
1493 &[
1494 PathEl::MoveTo((278.0, -710.0).into()),
1495 PathEl::LineTo((278.0, -470.0).into()),
1496 PathEl::CurveTo(
1497 (300.0, -500.0).into(),
1498 (800.0, -500.0).into(),
1499 (998.0, -470.0).into(),
1500 ),
1501 PathEl::LineTo((998.0, -710.0).into()),
1502 ],
1503 );
1504 }
1505
1506 #[test]
1507 fn draw_extended_glyf_with_wide_components_and_independent_metrics() {
1508 use font_test_data::extended::outlines_font;
1509 let expected = "M0.0,40.0 C0.0,0.0 80.0,0.0 80.0,40.0 C80.0,80.0 0.0,80.0 0.0,40.0 Z";
1510 for long_loca in [false, true] {
1511 for legacy in [false, true] {
1512 for extended_metrics in [false, true] {
1513 let data = outlines_font(true, legacy, extended_metrics, long_loca);
1514 let font = FontRef::new(&data).unwrap();
1515 let glyphs = font.outline_glyphs();
1516 for gid in [1, 65536] {
1517 let glyph = glyphs.get(GlyphId::new(gid)).unwrap();
1518 for style in [PathStyle::FreeType, PathStyle::HarfBuzz] {
1519 let mut pen = SvgPen::with_precision(1);
1520 let metrics = glyph
1521 .draw(
1522 DrawSettings::unhinted(
1523 Size::unscaled(),
1524 LocationRef::default(),
1525 )
1526 .with_path_style(style),
1527 &mut pen,
1528 )
1529 .unwrap();
1530 assert_eq!(pen.as_ref(), expected);
1531 assert_eq!(metrics.advance_width, Some(1000.0));
1532 }
1533 }
1534 assert!(glyphs.get(GlyphId::new(65537)).is_none());
1535 }
1536 }
1537 }
1538 let data = outlines_font(false, true, true, false);
1540 let font = FontRef::new(&data).unwrap();
1541 let glyph = font.outline_glyphs().get(GlyphId::new(1)).unwrap();
1542 let mut pen = SvgPen::with_precision(1);
1543 glyph
1544 .draw((Size::unscaled(), LocationRef::default()), &mut pen)
1545 .unwrap();
1546 assert_eq!(pen.as_ref(), "M0.0,0.0 L10.0,0.0 L10.0,10.0 Z");
1547 }
1548
1549 #[test]
1550 fn scaled_extended_cubic_outlines_match_cached_metrics() {
1551 use font_test_data::extended::outlines_font;
1552 use read_fonts::model::Font;
1553
1554 for long_loca in [false, true] {
1555 let bytes = outlines_font(true, true, true, long_loca);
1556 let font = FontRef::new(&bytes).unwrap();
1557 let cached = Font::new(bytes.clone(), 0).unwrap();
1558 let outlines = font.outline_glyphs();
1559 for gid in [1, 65536] {
1560 let id = GlyphId::new(gid);
1561 let extents = cached.glyph_metrics().extents(id).unwrap();
1562 assert_eq!(
1563 (extents.x_bearing, extents.width, extents.height),
1564 (0.0, 80.0, 80.0)
1565 );
1566 for size in [125.0, 1000.0] {
1567 let edge = size * 0.08;
1568 let mid = edge / 2.0;
1569 let expected = format!(
1570 "M0.0,{mid:.1} C0.0,0.0 {edge:.1},0.0 {edge:.1},{mid:.1} C{edge:.1},{edge:.1} 0.0,{edge:.1} 0.0,{mid:.1} Z"
1571 );
1572 for style in [PathStyle::FreeType, PathStyle::HarfBuzz] {
1573 let mut pen = SvgPen::with_precision(1);
1574 let adjusted = outlines
1575 .get(id)
1576 .unwrap()
1577 .draw(
1578 DrawSettings::unhinted(Size::new(size), LocationRef::default())
1579 .with_path_style(style),
1580 &mut pen,
1581 )
1582 .unwrap();
1583 assert_eq!(pen.as_ref(), expected, "gid {gid}, size {size}, {style:?}");
1584 assert_eq!(adjusted.advance_width, Some(size));
1585 assert_eq!(
1586 font.glyph_metrics(Size::new(size), LocationRef::default())
1587 .advance_width(id),
1588 Some(size)
1589 );
1590 }
1591 }
1592 }
1593 }
1594 }
1595
1596 #[test]
1597 fn extended_gvar_draws_high_glyphs_and_varies_metrics() {
1598 use font_test_data::extended::variable_outlines_font;
1599 for long_offsets in [false, true] {
1600 for (extended, legacy) in [(true, false), (true, true), (false, true), (false, false)] {
1601 let bytes = variable_outlines_font(extended, legacy, long_offsets);
1602 let font = FontRef::new(&bytes).unwrap();
1603 let glyphs = font.outline_glyphs();
1604 for coord in [0.0, 0.5, 1.0] {
1605 let coords = [NormalizedCoord::from_f32(coord)];
1606 let location = LocationRef::new(&coords);
1607 let metrics = font.glyph_metrics(Size::unscaled(), location);
1608 for gid in [65536, 1] {
1609 let shift = if gid == 1 {
1610 (if extended { 40.0 } else { 0.0 })
1611 + if extended || legacy { 20.0 } else { 0.0 }
1612 } else if extended {
1613 40.0
1614 } else {
1615 0.0
1616 } * coord;
1617 let advance_delta = if gid == 1 && (extended || legacy) {
1618 60.0
1619 } else if gid == 65536 && extended {
1620 100.0
1621 } else {
1622 0.0
1623 } * coord;
1624 let right = shift + 80.0;
1625 let expected = format!(
1626 "M{shift:.1},40.0 C{shift:.1},0.0 {right:.1},0.0 {right:.1},40.0 C{right:.1},80.0 {shift:.1},80.0 {shift:.1},40.0 Z"
1627 );
1628 let id = GlyphId::new(gid);
1629 assert_eq!(metrics.advance_width(id), Some(1000.0 + advance_delta));
1630 for style in [PathStyle::FreeType, PathStyle::HarfBuzz] {
1631 let mut pen = SvgPen::with_precision(1);
1632 let adjusted = glyphs
1633 .get(id)
1634 .unwrap()
1635 .draw(
1636 DrawSettings::unhinted(Size::unscaled(), location)
1637 .with_path_style(style),
1638 &mut pen,
1639 )
1640 .unwrap();
1641 assert_eq!(
1642 pen.as_ref(),
1643 expected,
1644 "gid {gid}, coord {coord}, {style:?}"
1645 );
1646 assert_eq!(adjusted.advance_width, Some(1000.0 + advance_delta));
1647 }
1648 }
1649 }
1650 }
1651 }
1652 }
1653
1654 #[test]
1655 fn extended_gvar_instances_keep_metrics_and_outlines_in_sync() {
1656 use font_test_data::extended::variable_outlines_font;
1657 use read_fonts::model::Font;
1658
1659 for long_offsets in [false, true] {
1660 let bytes = variable_outlines_font(true, true, long_offsets);
1661 let font = FontRef::new(&bytes).unwrap();
1662 let cached = Font::new(bytes.clone(), 0).unwrap();
1663 let coords = [0.0, 1.0, 0.5, -0.5];
1664 let instances: Vec<_> = coords
1665 .iter()
1666 .map(|&coord| {
1667 cached
1668 .instance_builder()
1669 .normalized_coords([NormalizedCoord::from_f32(coord)])
1670 .build()
1671 })
1672 .collect();
1673 let outlines = font.outline_glyphs();
1674 for gid in [1, 65536] {
1675 let id = GlyphId::new(gid);
1676 let glyph = outlines.get(id).unwrap();
1677 for index in [1, 0, 2, 3, 1, 2, 0] {
1679 let location_coords = [NormalizedCoord::from_f32(coords[index])];
1680 let location = LocationRef::new(&location_coords);
1681 let metrics = instances[index].glyph_metrics();
1682 let scalar = coords[index].max(0.0);
1683 let shift = if gid == 1 { 60.0 } else { 40.0 } * scalar;
1684 let advance = 1000.0 + if gid == 1 { 60.0 } else { 100.0 } * scalar;
1685 assert_eq!(metrics.h_advance(id), advance);
1686 assert_eq!(metrics.extents(id).unwrap().x_bearing, shift);
1687 assert_eq!(
1688 font.glyph_metrics(Size::unscaled(), location)
1689 .advance_width(id),
1690 Some(advance)
1691 );
1692 for style in [PathStyle::FreeType, PathStyle::HarfBuzz] {
1693 let mut pen = SvgPen::with_precision(1);
1694 let adjusted = glyph
1695 .draw(
1696 DrawSettings::unhinted(Size::new(125.0), location)
1697 .with_path_style(style),
1698 &mut pen,
1699 )
1700 .unwrap();
1701 let left = shift / 8.0;
1702 let right = left + 10.0;
1703 assert_eq!(pen.as_ref(), format!(
1704 "M{left:.1},5.0 C{left:.1},0.0 {right:.1},0.0 {right:.1},5.0 C{right:.1},10.0 {left:.1},10.0 {left:.1},5.0 Z"
1705 ), "gid {gid}, coord {}, {style:?}", coords[index]);
1706 assert_eq!(adjusted.advance_width, Some(advance / 8.0));
1707 }
1708 }
1709 }
1710 }
1711 }
1712
1713 #[test]
1717 fn tthint_with_subset() {
1718 let font = FontRef::new(font_test_data::TTHINT_SUBSET).unwrap();
1719 let glyphs = font.outline_glyphs();
1720 let hinting = HintingInstance::new(
1721 &glyphs,
1722 Size::new(16.0),
1723 LocationRef::default(),
1724 HintingOptions::default(),
1725 )
1726 .unwrap();
1727 let glyph = glyphs.get(GlyphId::new(1)).unwrap();
1728 glyph
1730 .draw(DrawSettings::hinted(&hinting, true), &mut BezPen::default())
1731 .unwrap();
1732 }
1733
1734 #[test]
1735 fn empty_glyph_advance_unhinted() {
1736 let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
1737 let outlines = font.outline_glyphs();
1738 let coords = [NormalizedCoord::from_f32(0.5)];
1739 let gid = font.charmap().map(' ').unwrap();
1740 let outline = outlines.get(gid).unwrap();
1741 let advance = outline
1742 .draw(
1743 (Size::new(24.0), LocationRef::new(&coords)),
1744 &mut super::pen::NullPen,
1745 )
1746 .unwrap()
1747 .advance_width
1748 .unwrap();
1749 assert_eq!(advance, 10.796875);
1750 }
1751
1752 #[test]
1753 fn empty_glyph_advance_hinted() {
1754 let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
1755 let outlines = font.outline_glyphs();
1756 let coords = [NormalizedCoord::from_f32(0.5)];
1757 let hinter = HintingInstance::new(
1758 &outlines,
1759 Size::new(24.0),
1760 LocationRef::new(&coords),
1761 HintingOptions::default(),
1762 )
1763 .unwrap();
1764 let gid = font.charmap().map(' ').unwrap();
1765 let outline = outlines.get(gid).unwrap();
1766 let advance = outline
1767 .draw(&hinter, &mut super::pen::NullPen)
1768 .unwrap()
1769 .advance_width
1770 .unwrap();
1771 assert_eq!(advance, 11.0);
1772 }
1773
1774 #[test]
1777 fn fractional_size_hinting_matters() {
1778 let font = FontRef::from_index(font_test_data::TINOS_SUBSET, 0).unwrap();
1779 let mut outlines = font.outline_glyphs();
1780 let instance = HintingInstance::new(
1781 &outlines,
1782 Size::new(24.8),
1783 LocationRef::default(),
1784 HintingOptions::default(),
1785 )
1786 .unwrap();
1787 let gid = GlyphId::new(2);
1788 let outline_with_fractional = {
1789 let OutlineCollectionKind::Glyf(glyf) = &mut outlines.kind else {
1790 panic!("this is definitely a TrueType font");
1791 };
1792 glyf.fractional_size_hinting = true;
1793 let mut pen = SvgPen::new();
1794 let outline = outlines.get(gid).unwrap();
1795 outline.draw(&instance, &mut pen).unwrap();
1796 pen.to_string()
1797 };
1798 let outline_without_fractional = {
1799 let OutlineCollectionKind::Glyf(glyf) = &mut outlines.kind else {
1800 panic!("this is definitely a TrueType font");
1801 };
1802 glyf.fractional_size_hinting = false;
1803 let mut pen = SvgPen::new();
1804 let outline = outlines.get(gid).unwrap();
1805 outline.draw(&instance, &mut pen).unwrap();
1806 pen.to_string()
1807 };
1808 assert_ne!(outline_with_fractional, outline_without_fractional);
1809 }
1810
1811 #[test]
1812 fn cff2_advance_widths() {
1813 check_cff_hinted_and_unhinted_advance_widths(font_test_data::CANTARELL_VF_TRIMMED, true);
1814 }
1815
1816 #[test]
1817 fn cff_advance_widths() {
1818 check_cff_hinted_and_unhinted_advance_widths(
1819 font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
1820 false,
1821 );
1822 }
1823
1824 #[track_caller]
1825 fn check_cff_hinted_and_unhinted_advance_widths(font_data: &[u8], is_cff2: bool) {
1826 let font = FontRef::new(font_data).unwrap();
1827 let outlines = font.outline_glyphs();
1828 let OutlineCollectionKind::Cff(cff_outlines) = &outlines.kind else {
1829 panic!("this should be a CFF outline collection");
1830 };
1831 assert_eq!(cff_outlines.is_cff2(), is_cff2);
1832 let size = Size::new(16.0);
1833 let hinter = HintingInstance::new(
1834 &outlines,
1835 size,
1836 LocationRef::default(),
1837 HintingOptions::default(),
1838 )
1839 .unwrap();
1840 let num_glyphs = font.maxp().unwrap().num_glyphs() as u32;
1841 assert!(num_glyphs > 0);
1842 for gid in 0..num_glyphs {
1843 let outline = outlines.get(gid.into()).unwrap();
1844 let hinted = outline
1845 .draw(&hinter, &mut super::pen::NullPen)
1846 .unwrap()
1847 .advance_width
1848 .unwrap();
1849 let unhinted = outline
1850 .draw(
1851 DrawSettings::unhinted(size, LocationRef::default()),
1852 &mut super::pen::NullPen,
1853 )
1854 .unwrap()
1855 .advance_width
1856 .unwrap();
1857 assert_eq!(hinted.round(), hinted);
1858 assert!(
1859 (hinted - unhinted).abs() <= 0.5,
1860 "advance widths should be close, got hinted={hinted}, unhinted={unhinted} for gid {gid}"
1861 );
1862 }
1863 }
1864
1865 #[test]
1866 fn autohint_completes_with_long_blues() {
1867 static FONT_DATA: [u8; 352] = [
1870 0, 1, 0, 0, 0, 7, 0, 64, 0, 2, 0, 48, 99, 109, 97, 112, 0, 12, 6, 4, 0, 0, 0, 124, 0,
1871 0, 0, 44, 103, 108, 121, 102, 51, 203, 248, 103, 0, 0, 0, 168, 0, 0, 0, 40, 104, 101,
1872 97, 100, 95, 16, 64, 222, 0, 0, 0, 208, 0, 0, 0, 54, 104, 104, 101, 97, 0, 1, 0, 1, 0,
1873 0, 1, 8, 0, 0, 0, 36, 104, 109, 116, 120, 0, 0, 0, 0, 0, 0, 1, 44, 0, 0, 0, 6, 108,
1874 111, 99, 97, 0, 0, 0, 40, 0, 0, 1, 52, 0, 0, 0, 12, 109, 97, 120, 112, 0, 4, 0, 5, 0,
1875 0, 1, 64, 0, 0, 0, 32, 0, 0, 0, 1, 0, 3, 0, 1, 0, 0, 0, 12, 0, 4, 0, 32, 0, 0, 0, 4, 0,
1876 4, 0, 1, 0, 0, 5, 209, 255, 255, 0, 0, 5, 209, 255, 255, 250, 48, 0, 1, 0, 0, 0, 0, 0,
1877 1, 0, 0, 254, 212, 0, 100, 1, 244, 0, 4, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 100, 0, 0,
1878 255, 206, 0, 0, 254, 212, 0, 0, 1, 44, 1, 244, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1879 95, 15, 60, 245, 0, 0, 3, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1880 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1881 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0,
1882 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 0, 1, 0, 0, 0, 2, 0, 5, 0, 1, 0, 0, 0, 0,
1883 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1884 ];
1885 let font = FontRef::new(&FONT_DATA).unwrap();
1886 let outlines = font.outline_glyphs();
1887 let hinter = HintingInstance::new(
1888 &outlines,
1889 Size::new(16.0),
1890 LocationRef::default(),
1891 Engine::Auto(None),
1892 )
1893 .unwrap();
1894 let gid = font.charmap().map('\u{05D1}').unwrap();
1895 let outline = outlines.get(gid).unwrap();
1896 let (tx, rx) = std::sync::mpsc::channel();
1897 std::thread::spawn(move || {
1898 outline.draw(&hinter, &mut NullPen).unwrap();
1899 let _ = tx.send(());
1900 });
1901 let budget = std::time::Duration::from_secs(5);
1902 match rx.recv_timeout(budget) {
1903 Ok(_) => {}
1904 Err(_) => {
1905 panic!("Autohinting did not complete within {budget:?}");
1906 }
1907 }
1908 }
1909}