1 //! Loading, scaling and hinting of glyph outlines.
2 //!
3 //! This module provides support for retrieving (optionally scaled and hinted)
4 //! glyph outlines in the form of vector paths.
5 //!
6 //! # Drawing a glyph
7 //!
8 //! Generating SVG style path data for a character (this assumes a local
9 //! variable `font` of type [`FontRef`](crate::FontRef)):
10 //!
11 //! ```rust
12 //! use skrifa::{
13 //! instance::{LocationRef, Size},
14 //! outline::{DrawSettings, OutlinePen},
15 //! FontRef, MetadataProvider,
16 //! };
17 //!
18 //! # fn wrapper(font: FontRef) {
19 //! // First, grab the set of outline glyphs from the font.
20 //! let outlines = font.outline_glyphs();
21 //!
22 //! // Find the glyph identifier for our character.
23 //! let glyph_id = font.charmap().map('Q').unwrap();
24 //!
25 //! // Grab the outline glyph.
26 //! let glyph = outlines.get(glyph_id).unwrap();
27 //!
28 //! // Define how we want the glyph to be drawn. This creates
29 //! // settings for an instance without hinting at a size of
30 //! // 16px with no variations applied.
31 //! let settings = DrawSettings::unhinted(Size::new(16.0), LocationRef::default());
32 //!
33 //! // Alternatively, we can apply variations like so:
34 //! let var_location = font.axes().location(&[("wght", 650.0), ("wdth", 100.0)]);
35 //! let settings = DrawSettings::unhinted(Size::new(16.0), &var_location);
36 //!
37 //! // At this point, we need a "sink" to receive the resulting path. This
38 //! // is done by creating an implementation of the OutlinePen trait.
39 //!
40 //! // Let's make one that generates SVG path data.
41 //! #[derive(Default)]
42 //! struct SvgPath(String);
43 //!
44 //! // Implement the OutlinePen trait for this type. This emits the appropriate
45 //! // SVG path commands for each element type.
46 //! impl OutlinePen for SvgPath {
47 //! fn move_to(&mut self, x: f32, y: f32) {
48 //! self.0.push_str(&format!("M{x:.1},{y:.1} "));
49 //! }
50 //!
51 //! fn line_to(&mut self, x: f32, y: f32) {
52 //! self.0.push_str(&format!("L{x:.1},{y:.1} "));
53 //! }
54 //!
55 //! fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
56 //! self.0
57 //! .push_str(&format!("Q{cx0:.1},{cy0:.1} {x:.1},{y:.1} "));
58 //! }
59 //!
60 //! fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
61 //! self.0.push_str(&format!(
62 //! "C{cx0:.1},{cy0:.1} {cx1:.1},{cy1:.1} {x:.1},{y:.1} "
63 //! ));
64 //! }
65 //!
66 //! fn close(&mut self) {
67 //! self.0.push_str("Z ");
68 //! }
69 //! }
70 //! // Now, construct an instance of our pen.
71 //! let mut svg_path = SvgPath::default();
72 //!
73 //! // And draw the glyph!
74 //! glyph.draw(settings, &mut svg_path).unwrap();
75 //!
76 //! // See what we've drawn.
77 //! println!("{}", svg_path.0);
78 //! # }
79 //! ```
80
81 mod cff;
82 mod embedded_hinting;
83 mod glyf;
84
85 pub mod error;
86
87 use read_fonts::{types::GlyphId, TableProvider};
88
89 pub use embedded_hinting::{EmbeddedHinting, EmbeddedHintingInstance};
90 #[doc(inline)]
91 pub use error::DrawError;
92
93 pub use read_fonts::types::Pen as OutlinePen;
94
95 use super::{
96 instance::{LocationRef, NormalizedCoord, Size},
97 GLYF_COMPOSITE_RECURSION_LIMIT,
98 };
99
100 /// Source format for an outline glyph.
101 #[derive(Copy, Clone, PartialEq, Eq, Debug)]
102 pub enum OutlineGlyphFormat {
103 /// TrueType outlines sourced from the `glyf` table.
104 Glyf,
105 /// PostScript outlines sourced from the `CFF` table.
106 Cff,
107 /// PostScript outlines sourced from the `CFF2` table.
108 Cff2,
109 }
110
111 /// Specifies the hinting strategy for memory size calculations.
112 #[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
113 pub enum Hinting {
114 /// Hinting is disabled.
115 #[default]
116 None,
117 /// Application of hints that are embedded in the font.
118 ///
119 /// For TrueType, these are bytecode instructions associated with each
120 /// glyph outline. For PostScript (CFF/CFF2), these are stem hints
121 /// encoded in the character string.
122 Embedded,
123 }
124
125 /// Information and adjusted metrics generated while drawing an outline glyph.
126 ///
127 /// When applying hints to a TrueType glyph, the outline may be shifted in
128 /// the horizontal direction, affecting the left side bearing and advance width
129 /// of the glyph. This captures those metrics.
130 #[derive(Copy, Clone, Default, Debug)]
131 pub struct AdjustedMetrics {
132 /// True if the underlying glyph contains flags indicating the
133 /// presence of overlapping contours or components.
134 pub has_overlaps: bool,
135 /// If present, an adjusted left side bearing value generated by the
136 /// scaler.
137 ///
138 /// This is equivalent to the `horiBearingX` value in
139 /// [`FT_Glyph_Metrics`](https://freetype.org/freetype2/docs/reference/ft2-glyph_retrieval.html#ft_glyph_metrics).
140 pub lsb: Option<f32>,
141 /// If present, an adjusted advance width value generated by the
142 /// scaler.
143 ///
144 /// This is equivalent to the `advance.x` value in
145 /// [`FT_GlyphSlotRec`](https://freetype.org/freetype2/docs/reference/ft2-glyph_retrieval.html#ft_glyphslotrec).
146 pub advance_width: Option<f32>,
147 }
148
149 /// Options that define how a [glyph](OutlineGlyph) is drawn to a
150 /// [pen](OutlinePen).
151 pub struct DrawSettings<'a> {
152 instance: DrawInstance<'a>,
153 memory: Option<&'a mut [u8]>,
154 }
155
156 impl<'a> DrawSettings<'a> {
157 /// Creates settings for an unhinted draw operation with the given size and
158 /// location in variation space.
unhinted(size: Size, location: impl Into<LocationRef<'a>>) -> Self159 pub fn unhinted(size: Size, location: impl Into<LocationRef<'a>>) -> Self {
160 Self {
161 instance: DrawInstance::Unhinted(size, location.into()),
162 memory: None,
163 }
164 }
165
166 /// Creates settings for a hinted draw operation using embedded hinting.
167 ///
168 /// The font size, location in variation space and hinting mode are
169 /// defined by the current configuration of the given hinting instance.
embedded_hinting(instance: &'a EmbeddedHintingInstance) -> Self170 pub fn embedded_hinting(instance: &'a EmbeddedHintingInstance) -> Self {
171 Self {
172 instance: DrawInstance::EmbeddedHinted(instance),
173 memory: None,
174 }
175 }
176
177 /// Builder method to associate a user memory buffer to be used for
178 /// temporary allocations during drawing.
179 ///
180 /// The required size of this buffer can be computed using the
181 /// [`OutlineGlyph::draw_memory_size`] method.
182 ///
183 /// If not provided, any necessary memory will be allocated internally.
with_memory(mut self, memory: Option<&'a mut [u8]>) -> Self184 pub fn with_memory(mut self, memory: Option<&'a mut [u8]>) -> Self {
185 self.memory = memory;
186 self
187 }
188 }
189
190 enum DrawInstance<'a> {
191 Unhinted(Size, LocationRef<'a>),
192 EmbeddedHinted(&'a EmbeddedHintingInstance),
193 }
194
195 impl<'a, L> From<(Size, L)> for DrawSettings<'a>
196 where
197 L: Into<LocationRef<'a>>,
198 {
from(value: (Size, L)) -> Self199 fn from(value: (Size, L)) -> Self {
200 DrawSettings::unhinted(value.0, value.1.into())
201 }
202 }
203
204 impl From<Size> for DrawSettings<'_> {
from(value: Size) -> Self205 fn from(value: Size) -> Self {
206 DrawSettings::unhinted(value, LocationRef::default())
207 }
208 }
209
210 impl<'a> From<&'a EmbeddedHintingInstance> for DrawSettings<'a> {
from(value: &'a EmbeddedHintingInstance) -> Self211 fn from(value: &'a EmbeddedHintingInstance) -> Self {
212 DrawSettings::embedded_hinting(value)
213 }
214 }
215
216 /// A scalable glyph outline.
217 ///
218 /// This can be sourced from the [`glyf`](https://learn.microsoft.com/en-us/typography/opentype/spec/glyf),
219 /// [`CFF`](https://learn.microsoft.com/en-us/typography/opentype/spec/cff) or
220 /// [`CFF2`](https://learn.microsoft.com/en-us/typography/opentype/spec/cff2)
221 /// tables. Use the [`format`](OutlineGlyph::format) method to determine which
222 /// was chosen for this glyph.
223 #[derive(Clone)]
224 pub struct OutlineGlyph<'a> {
225 kind: OutlineKind<'a>,
226 }
227
228 impl<'a> OutlineGlyph<'a> {
229 /// Returns the underlying source format for this outline.
format(&self) -> OutlineGlyphFormat230 pub fn format(&self) -> OutlineGlyphFormat {
231 match &self.kind {
232 OutlineKind::Glyf(..) => OutlineGlyphFormat::Glyf,
233 OutlineKind::Cff(cff, ..) => {
234 if cff.is_cff2() {
235 OutlineGlyphFormat::Cff2
236 } else {
237 OutlineGlyphFormat::Cff
238 }
239 }
240 }
241 }
242
243 /// Returns a value indicating if the outline may contain overlapping
244 /// contours or components.
245 ///
246 /// For CFF outlines, returns `None` since this information is unavailable.
has_overlaps(&self) -> Option<bool>247 pub fn has_overlaps(&self) -> Option<bool> {
248 match &self.kind {
249 OutlineKind::Glyf(_, outline) => Some(outline.has_overlaps),
250 _ => None,
251 }
252 }
253
254 /// Returns a value indicating whether the outline has hinting
255 /// instructions.
256 ///
257 /// For CFF outlines, returns `None` since this is unknown prior
258 /// to loading the outline.
has_hinting(&self) -> Option<bool>259 pub fn has_hinting(&self) -> Option<bool> {
260 match &self.kind {
261 OutlineKind::Glyf(_, outline) => Some(outline.has_hinting),
262 _ => None,
263 }
264 }
265
266 /// Returns the size (in bytes) of the temporary memory required to draw
267 /// this outline.
268 ///
269 /// This is used to compute the size of the memory buffer required for the
270 /// [`DrawSettings::with_memory`] method.
271 ///
272 /// The `hinting` parameter determines which hinting method, if any, will
273 /// be used for drawing which has an effect on memory requirements.
274 ///
275 /// The appropriate hinting types are as follows:
276 ///
277 /// | For draw settings | Use hinting |
278 /// |------------------------------------|-----------------------|
279 /// | [`DrawSettings::unhinted`] | [`Hinting::None`] |
280 /// | [`DrawSettings::embedded_hinting`] | [`Hinting::Embedded`] |
draw_memory_size(&self, hinting: Hinting) -> usize281 pub fn draw_memory_size(&self, hinting: Hinting) -> usize {
282 match &self.kind {
283 OutlineKind::Glyf(_, outline) => outline.required_buffer_size(hinting),
284 _ => 0,
285 }
286 }
287
288 /// Draws the outline glyph with the given settings and emits the resulting
289 /// path commands to the specified pen.
draw<'s>( &self, settings: impl Into<DrawSettings<'a>>, pen: &mut impl OutlinePen, ) -> Result<AdjustedMetrics, DrawError>290 pub fn draw<'s>(
291 &self,
292 settings: impl Into<DrawSettings<'a>>,
293 pen: &mut impl OutlinePen,
294 ) -> Result<AdjustedMetrics, DrawError> {
295 let settings = settings.into();
296 match settings.instance {
297 DrawInstance::Unhinted(size, location) => {
298 self.draw_unhinted(size, location, settings.memory, pen)
299 }
300 DrawInstance::EmbeddedHinted(hinter) => hinter.draw(self, settings.memory, pen),
301 }
302 }
303
draw_unhinted( &self, size: Size, location: impl Into<LocationRef<'a>>, memory: Option<&mut [u8]>, pen: &mut impl OutlinePen, ) -> Result<AdjustedMetrics, DrawError>304 fn draw_unhinted(
305 &self,
306 size: Size,
307 location: impl Into<LocationRef<'a>>,
308 memory: Option<&mut [u8]>,
309 pen: &mut impl OutlinePen,
310 ) -> Result<AdjustedMetrics, DrawError> {
311 let ppem = size.ppem();
312 let coords = location.into().coords();
313 match &self.kind {
314 OutlineKind::Glyf(glyf, outline) => {
315 with_glyf_memory(outline, Hinting::None, memory, |buf| {
316 let mem = outline
317 .memory_from_buffer(buf, Hinting::None)
318 .ok_or(DrawError::InsufficientMemory)?;
319 let scaled_outline = glyf.draw(mem, outline, ppem, coords)?;
320 scaled_outline.to_path(pen)?;
321 Ok(AdjustedMetrics {
322 has_overlaps: outline.has_overlaps,
323 lsb: Some(scaled_outline.adjusted_lsb().to_f32()),
324 advance_width: Some(scaled_outline.adjusted_advance_width().to_f32()),
325 })
326 })
327 }
328 OutlineKind::Cff(cff, glyph_id, subfont_ix) => {
329 let subfont = cff.subfont(*subfont_ix, ppem, coords)?;
330 cff.draw(&subfont, *glyph_id, coords, false, pen)?;
331 Ok(AdjustedMetrics::default())
332 }
333 }
334 }
335 }
336
337 #[derive(Clone)]
338 enum OutlineKind<'a> {
339 Glyf(glyf::Outlines<'a>, glyf::Outline<'a>),
340 // Third field is subfont index
341 Cff(cff::Outlines<'a>, GlyphId, u32),
342 }
343
344 /// Collection of scalable glyph outlines.
345 #[derive(Clone)]
346 pub struct OutlineGlyphCollection<'a> {
347 kind: OutlineCollectionKind<'a>,
348 }
349
350 impl<'a> OutlineGlyphCollection<'a> {
351 /// Creates a new outline collection for the given font.
new(font: &impl TableProvider<'a>) -> Self352 pub fn new(font: &impl TableProvider<'a>) -> Self {
353 let kind = if let Some(glyf) = glyf::Outlines::new(font) {
354 OutlineCollectionKind::Glyf(glyf)
355 } else if let Ok(cff) = cff::Outlines::new(font) {
356 OutlineCollectionKind::Cff(cff)
357 } else {
358 OutlineCollectionKind::None
359 };
360 Self { kind }
361 }
362
363 /// Creates a new outline collection for the given font and outline
364 /// format.
365 ///
366 /// Returns `None` if the font does not contain outlines in the requested
367 /// format.
with_format(font: &impl TableProvider<'a>, format: OutlineGlyphFormat) -> Option<Self>368 pub fn with_format(font: &impl TableProvider<'a>, format: OutlineGlyphFormat) -> Option<Self> {
369 let kind = match format {
370 OutlineGlyphFormat::Glyf => OutlineCollectionKind::Glyf(glyf::Outlines::new(font)?),
371 OutlineGlyphFormat::Cff => {
372 let upem = font.head().ok()?.units_per_em();
373 OutlineCollectionKind::Cff(cff::Outlines::from_cff(font.cff().ok()?, 0, upem).ok()?)
374 }
375 OutlineGlyphFormat::Cff2 => {
376 let upem = font.head().ok()?.units_per_em();
377 OutlineCollectionKind::Cff(cff::Outlines::from_cff2(font.cff2().ok()?, upem).ok()?)
378 }
379 };
380 Some(Self { kind })
381 }
382
383 /// Returns the underlying format of the source outline tables.
format(&self) -> Option<OutlineGlyphFormat>384 pub fn format(&self) -> Option<OutlineGlyphFormat> {
385 match &self.kind {
386 OutlineCollectionKind::Glyf(..) => Some(OutlineGlyphFormat::Glyf),
387 OutlineCollectionKind::Cff(cff) => cff
388 .is_cff2()
389 .then_some(OutlineGlyphFormat::Cff2)
390 .or(Some(OutlineGlyphFormat::Cff)),
391 _ => None,
392 }
393 }
394
395 /// Returns the outline for the given glyph identifier.
get(&self, glyph_id: GlyphId) -> Option<OutlineGlyph<'a>>396 pub fn get(&self, glyph_id: GlyphId) -> Option<OutlineGlyph<'a>> {
397 match &self.kind {
398 OutlineCollectionKind::None => None,
399 OutlineCollectionKind::Glyf(glyf) => Some(OutlineGlyph {
400 kind: OutlineKind::Glyf(glyf.clone(), glyf.outline(glyph_id).ok()?),
401 }),
402 OutlineCollectionKind::Cff(cff) => Some(OutlineGlyph {
403 kind: OutlineKind::Cff(cff.clone(), glyph_id, cff.subfont_index(glyph_id)),
404 }),
405 }
406 }
407
408 /// Returns an iterator over all of the outline glyphs in the collection.
iter(&self) -> impl Iterator<Item = (GlyphId, OutlineGlyph<'a>)> + 'a + Clone409 pub fn iter(&self) -> impl Iterator<Item = (GlyphId, OutlineGlyph<'a>)> + 'a + Clone {
410 let len = match &self.kind {
411 OutlineCollectionKind::Glyf(glyf) => glyf.glyph_count(),
412 OutlineCollectionKind::Cff(cff) => cff.glyph_count(),
413 _ => 0,
414 } as u16;
415 let copy = self.clone();
416 (0..len).filter_map(move |gid| {
417 let gid = GlyphId::new(gid);
418 let glyph = copy.get(gid)?;
419 Some((gid, glyph))
420 })
421 }
422 }
423
424 #[derive(Clone)]
425 enum OutlineCollectionKind<'a> {
426 None,
427 Glyf(glyf::Outlines<'a>),
428 Cff(cff::Outlines<'a>),
429 }
430
431 /// Arbitrarily chosen smallish size for stack allocation to avoid the heap
432 /// when possible while drawing glyf outlines.
433 ///
434 /// Upcoming work on TrueType hinting will likely adjust this to use bucketed
435 /// sizes based on actual data captured from fonts.
436 const GLYF_DRAW_STACK_BUFFER_SIZE: usize = 4096;
437
438 /// Invokes the callback with a memory buffer suitable for drawing
439 /// the given TrueType outline.
with_glyf_memory<R>( outline: &glyf::Outline, hinting: Hinting, memory: Option<&mut [u8]>, mut f: impl FnMut(&mut [u8]) -> R, ) -> R440 pub(super) fn with_glyf_memory<R>(
441 outline: &glyf::Outline,
442 hinting: Hinting,
443 memory: Option<&mut [u8]>,
444 mut f: impl FnMut(&mut [u8]) -> R,
445 ) -> R {
446 // Wrap in a function and prevent inlining to avoid stack allocation
447 // and zeroing if we don't take this code path.
448 #[inline(never)]
449 fn stack_mem<R>(mut f: impl FnMut(&mut [u8]) -> R) -> R {
450 f(&mut [0u8; GLYF_DRAW_STACK_BUFFER_SIZE])
451 }
452 match memory {
453 Some(buf) => f(buf),
454 None => {
455 let buf_size = outline.required_buffer_size(hinting);
456 if buf_size <= GLYF_DRAW_STACK_BUFFER_SIZE {
457 stack_mem(f)
458 } else {
459 f(&mut vec![0u8; buf_size])
460 }
461 }
462 }
463 }
464
465 #[cfg(test)]
466 mod tests {
467 use super::*;
468 use crate::MetadataProvider;
469 use read_fonts::{scaler_test, types::GlyphId, FontRef, TableProvider};
470
471 #[test]
outline_glyph_formats()472 fn outline_glyph_formats() {
473 let font_format_pairs = [
474 (font_test_data::VAZIRMATN_VAR, OutlineGlyphFormat::Glyf),
475 (
476 font_test_data::CANTARELL_VF_TRIMMED,
477 OutlineGlyphFormat::Cff2,
478 ),
479 (
480 font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
481 OutlineGlyphFormat::Cff,
482 ),
483 (font_test_data::COLRV0V1_VARIABLE, OutlineGlyphFormat::Glyf),
484 ];
485 for (font_data, format) in font_format_pairs {
486 assert_eq!(
487 FontRef::new(font_data).unwrap().outline_glyphs().format(),
488 Some(format)
489 );
490 }
491 }
492
493 #[test]
vazirmatin_var()494 fn vazirmatin_var() {
495 compare_glyphs(
496 font_test_data::VAZIRMATN_VAR,
497 font_test_data::VAZIRMATN_VAR_GLYPHS,
498 );
499 }
500
501 #[test]
cantarell_vf()502 fn cantarell_vf() {
503 compare_glyphs(
504 font_test_data::CANTARELL_VF_TRIMMED,
505 font_test_data::CANTARELL_VF_TRIMMED_GLYPHS,
506 );
507 }
508
509 #[test]
noto_serif_display()510 fn noto_serif_display() {
511 compare_glyphs(
512 font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
513 font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS,
514 );
515 }
516
517 #[test]
overlap_flags()518 fn overlap_flags() {
519 let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
520 let outlines = font.outline_glyphs();
521 let glyph_count = font.maxp().unwrap().num_glyphs();
522 // GID 2 is a composite glyph with the overlap bit on a component
523 // GID 3 is a simple glyph with the overlap bit on the first flag
524 let expected_gids_with_overlap = vec![2, 3];
525 assert_eq!(
526 expected_gids_with_overlap,
527 (0..glyph_count)
528 .filter(
529 |gid| outlines.get(GlyphId::new(*gid)).unwrap().has_overlaps() == Some(true)
530 )
531 .collect::<Vec<_>>()
532 );
533 }
534
compare_glyphs(font_data: &[u8], expected_outlines: &str)535 fn compare_glyphs(font_data: &[u8], expected_outlines: &str) {
536 let font = FontRef::new(font_data).unwrap();
537 let expected_outlines = scaler_test::parse_glyph_outlines(expected_outlines);
538 let mut path = scaler_test::Path::default();
539 for expected_outline in &expected_outlines {
540 if expected_outline.size == 0.0 && !expected_outline.coords.is_empty() {
541 continue;
542 }
543 let size = if expected_outline.size != 0.0 {
544 Size::new(expected_outline.size)
545 } else {
546 Size::unscaled()
547 };
548 path.elements.clear();
549 font.outline_glyphs()
550 .get(expected_outline.glyph_id)
551 .unwrap()
552 .draw(
553 DrawSettings::unhinted(size, expected_outline.coords.as_slice()),
554 &mut path,
555 )
556 .unwrap();
557 if path.elements != expected_outline.path {
558 panic!(
559 "mismatch in glyph path for id {} (size: {}, coords: {:?}): path: {:?} expected_path: {:?}",
560 expected_outline.glyph_id,
561 expected_outline.size,
562 expected_outline.coords,
563 &path.elements,
564 &expected_outline.path
565 );
566 }
567 }
568 }
569 }
570