vello_common/
glyph.rs

1// Copyright 2025 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Processing and drawing glyphs.
5
6use crate::kurbo::{Affine, BezPath, Vec2};
7use crate::peniko::Font;
8use alloc::boxed::Box;
9use core::fmt::{Debug, Formatter};
10use skrifa::instance::{LocationRef, Size};
11use skrifa::outline::DrawSettings;
12use skrifa::raw::TableProvider;
13use skrifa::{FontRef, OutlineGlyphCollection};
14use skrifa::{
15    GlyphId, MetadataProvider,
16    outline::{HintingInstance, HintingOptions, OutlinePen},
17};
18
19use crate::colr::convert_bounding_box;
20use crate::encode::x_y_advances;
21use crate::kurbo::Rect;
22use crate::pixmap::Pixmap;
23use skrifa::bitmap::{BitmapData, BitmapFormat, BitmapStrikes, Origin};
24
25#[cfg(not(feature = "std"))]
26use peniko::kurbo::common::FloatFuncs as _;
27
28/// Positioned glyph.
29#[derive(Copy, Clone, Default, Debug)]
30pub struct Glyph {
31    /// The font-specific identifier for this glyph.
32    ///
33    /// This ID is specific to the font being used and corresponds to the
34    /// glyph index within that font. It is *not* a Unicode code point.
35    pub id: u32,
36    /// X-offset in run, relative to transform.
37    pub x: f32,
38    /// Y-offset in run, relative to transform.
39    pub y: f32,
40}
41
42/// A type of glyph.
43#[derive(Debug)]
44pub enum GlyphType<'a> {
45    /// An outline glyph.
46    Outline(OutlineGlyph<'a>),
47    /// A bitmap glyph.
48    Bitmap(BitmapGlyph),
49    /// A COLR glyph.
50    Colr(Box<ColorGlyph<'a>>),
51}
52
53/// A simplified representation of a glyph, prepared for easy rendering.
54#[derive(Debug)]
55pub struct PreparedGlyph<'a> {
56    /// The type of glyph.
57    pub glyph_type: GlyphType<'a>,
58    /// The global transform of the glyph.
59    pub transform: Affine,
60}
61
62/// A glyph defined by a path (its outline) and a local transform.
63#[derive(Debug)]
64pub struct OutlineGlyph<'a> {
65    /// The path of the glyph.
66    pub path: &'a BezPath,
67}
68
69/// A glyph defined by a bitmap.
70#[derive(Debug)]
71pub struct BitmapGlyph {
72    /// The pixmap of the glyph.
73    pub pixmap: Pixmap,
74    /// The rectangular area that should be filled with the bitmap when painting.
75    pub area: Rect,
76}
77
78/// A glyph defined by a COLR glyph description.
79///
80/// Clients are supposed to first draw the glyph into an intermediate image texture/pixmap
81/// and then render that into the actual scene, in a similar fashion to
82/// bitmap glyphs.
83pub struct ColorGlyph<'a> {
84    pub(crate) skrifa_glyph: skrifa::color::ColorGlyph<'a>,
85    pub(crate) location: LocationRef<'a>,
86    pub(crate) font_ref: &'a FontRef<'a>,
87    pub(crate) draw_transform: Affine,
88    /// The rectangular area that should be filled with the rendered representation of the
89    /// COLR glyph when painting.
90    pub area: Rect,
91    /// The width of the pixmap/texture in pixels to which the glyph should be rendered to.
92    pub pix_width: u16,
93    /// The height of the pixmap/texture in pixels to which the glyph should be rendered to.
94    pub pix_height: u16,
95}
96
97impl Debug for ColorGlyph<'_> {
98    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
99        write!(f, "ColorGlyph")
100    }
101}
102
103/// Trait for types that can render glyphs.
104pub trait GlyphRenderer {
105    /// Fill glyphs with the current paint and fill rule.
106    fn fill_glyph(&mut self, glyph: PreparedGlyph<'_>);
107
108    /// Stroke glyphs with the current paint and stroke settings.
109    fn stroke_glyph(&mut self, glyph: PreparedGlyph<'_>);
110}
111
112/// A builder for configuring and drawing glyphs.
113#[derive(Debug)]
114#[must_use = "Methods on the builder don't do anything until `render` is called."]
115pub struct GlyphRunBuilder<'a, T: GlyphRenderer + 'a> {
116    run: GlyphRun<'a>,
117    renderer: &'a mut T,
118}
119
120impl<'a, T: GlyphRenderer + 'a> GlyphRunBuilder<'a, T> {
121    /// Creates a new builder for drawing glyphs.
122    pub fn new(font: Font, transform: Affine, renderer: &'a mut T) -> Self {
123        Self {
124            run: GlyphRun {
125                font,
126                font_size: 16.0,
127                transform,
128                glyph_transform: None,
129                hint: true,
130                normalized_coords: &[],
131            },
132            renderer,
133        }
134    }
135
136    /// Set the font size in pixels per em.
137    pub fn font_size(mut self, size: f32) -> Self {
138        self.run.font_size = size;
139        self
140    }
141
142    /// Set the per-glyph transform. Use `Affine::skew` with a horizontal-only skew to simulate
143    /// italic text.
144    pub fn glyph_transform(mut self, transform: Affine) -> Self {
145        self.run.glyph_transform = Some(transform);
146        self
147    }
148
149    /// Set whether font hinting is enabled.
150    ///
151    /// This performs vertical hinting only. Hinting is performed only if the combined `transform`
152    /// and `glyph_transform` have a uniform scale and no vertical skew or rotation.
153    pub fn hint(mut self, hint: bool) -> Self {
154        self.run.hint = hint;
155        self
156    }
157
158    /// Set normalized variation coordinates for variable fonts.
159    pub fn normalized_coords(mut self, coords: &'a [NormalizedCoord]) -> Self {
160        self.run.normalized_coords = bytemuck::cast_slice(coords);
161        self
162    }
163
164    /// Consumes the builder and fills the glyphs with the current configuration.
165    pub fn fill_glyphs(self, glyphs: impl Iterator<Item = Glyph>) {
166        self.render(glyphs, Style::Fill);
167    }
168
169    /// Consumes the builder and strokes the glyphs with the current configuration.
170    pub fn stroke_glyphs(self, glyphs: impl Iterator<Item = Glyph>) {
171        self.render(glyphs, Style::Stroke);
172    }
173
174    fn render(self, glyphs: impl Iterator<Item = Glyph>, style: Style) {
175        let font_ref =
176            FontRef::from_index(self.run.font.data.as_ref(), self.run.font.index).unwrap();
177
178        let upem: f32 = font_ref.head().map(|h| h.units_per_em()).unwrap().into();
179
180        let outlines = font_ref.outline_glyphs();
181        let color_glyphs = font_ref.color_glyphs();
182        let bitmaps = font_ref.bitmap_strikes();
183
184        let PreparedGlyphRun {
185            transform: initial_transform,
186            size,
187            normalized_coords,
188            hinting_instance,
189        } = prepare_glyph_run(&self.run, &outlines);
190
191        let render_glyph = match style {
192            Style::Fill => GlyphRenderer::fill_glyph,
193            Style::Stroke => GlyphRenderer::stroke_glyph,
194        };
195
196        // Reuse the same `path` allocation for each glyph.
197        let mut outline_path = OutlinePath::new();
198
199        for glyph in glyphs {
200            let bitmap_data = bitmaps
201                .glyph_for_size(Size::new(self.run.font_size), GlyphId::new(glyph.id))
202                .and_then(|g| match g.data {
203                    #[cfg(feature = "png")]
204                    BitmapData::Png(data) => Pixmap::from_png(data).ok().map(|d| (g, d)),
205                    #[cfg(not(feature = "png"))]
206                    BitmapData::Png(_) => None,
207                    // The others are not worth implementing for now (unless we can find a test case),
208                    // they should be very rare.
209                    BitmapData::Bgra(_) => None,
210                    BitmapData::Mask(_) => None,
211                });
212
213            let (glyph_type, transform) =
214                if let Some(color_glyph) = color_glyphs.get(GlyphId::new(glyph.id)) {
215                    prepare_colr_glyph(
216                        &font_ref,
217                        glyph,
218                        self.run.font_size,
219                        upem,
220                        initial_transform,
221                        color_glyph,
222                        normalized_coords,
223                    )
224                } else if let Some((bitmap_glyph, pixmap)) = bitmap_data {
225                    prepare_bitmap_glyph(
226                        &bitmaps,
227                        glyph,
228                        pixmap,
229                        self.run.font_size,
230                        upem,
231                        initial_transform,
232                        bitmap_glyph,
233                    )
234                } else {
235                    let Some(outline) = outlines.get(GlyphId::new(glyph.id)) else {
236                        continue;
237                    };
238
239                    prepare_outline_glyph(
240                        glyph,
241                        size,
242                        initial_transform,
243                        self.run.transform,
244                        &mut outline_path,
245                        &outline,
246                        hinting_instance.as_ref(),
247                        normalized_coords,
248                    )
249                };
250
251            let prepared_glyph = PreparedGlyph {
252                glyph_type,
253                transform,
254            };
255
256            render_glyph(self.renderer, prepared_glyph);
257        }
258    }
259}
260
261fn prepare_outline_glyph<'a>(
262    glyph: Glyph,
263    size: Size,
264    // The transform of the run + the per-glyph transform.
265    initial_transform: Affine,
266    // The transform of the run, without the per-glyph transform.
267    run_transform: Affine,
268    path: &'a mut OutlinePath,
269    outline_glyph: &skrifa::outline::OutlineGlyph<'a>,
270    hinting_instance: Option<&HintingInstance>,
271    normalized_coords: &[skrifa::instance::NormalizedCoord],
272) -> (GlyphType<'a>, Affine) {
273    let draw_settings = if let Some(hinting_instance) = hinting_instance {
274        DrawSettings::hinted(hinting_instance, false)
275    } else {
276        DrawSettings::unhinted(size, normalized_coords)
277    };
278
279    path.0.truncate(0);
280    let _ = outline_glyph.draw(draw_settings, path);
281
282    // Calculate the global glyph translation based on the glyph's local position within
283    // the run and the run's global transform.
284    //
285    // This is a partial affine matrix multiplication, calculating only the translation
286    // component that we need. It is added below to calculate the total transform of this
287    // glyph.
288    let [a, b, c, d, _, _] = run_transform.as_coeffs();
289    let translation = Vec2::new(
290        a * f64::from(glyph.x) + c * f64::from(glyph.y),
291        b * f64::from(glyph.x) + d * f64::from(glyph.y),
292    );
293
294    // When hinting, ensure the y-offset is integer. The x-offset doesn't matter, as we
295    // perform vertical-only hinting.
296    let mut final_transform = initial_transform
297        .then_translate(translation)
298        // Account for the fact that the coordinate system of fonts
299        // is upside down.
300        .pre_scale_non_uniform(1.0, -1.0)
301        .as_coeffs();
302
303    if hinting_instance.is_some() {
304        final_transform[5] = final_transform[5].round();
305    }
306
307    (
308        GlyphType::Outline(OutlineGlyph { path: &path.0 }),
309        Affine::new(final_transform),
310    )
311}
312
313fn prepare_bitmap_glyph<'a>(
314    bitmaps: &BitmapStrikes<'_>,
315    glyph: Glyph,
316    pixmap: Pixmap,
317    font_size: f32,
318    upem: f32,
319    initial_transform: Affine,
320    bitmap_glyph: skrifa::bitmap::BitmapGlyph<'a>,
321) -> (GlyphType<'a>, Affine) {
322    let x_scale_factor = font_size / bitmap_glyph.ppem_x;
323    let y_scale_factor = font_size / bitmap_glyph.ppem_y;
324    let font_units_to_size = font_size / upem;
325
326    // CoreText appears to special case Apple Color Emoji, adding
327    // a 100 font unit vertical offset. We do the same but only
328    // when both vertical offsets are 0 to avoid incorrect
329    // rendering if Apple ever does encode the offset directly in
330    // the font.
331    let bearing_y = if bitmap_glyph.bearing_y == 0.0 && bitmaps.format() == Some(BitmapFormat::Sbix)
332    {
333        100.0
334    } else {
335        bitmap_glyph.bearing_y
336    };
337
338    let origin_shift = match bitmap_glyph.placement_origin {
339        Origin::TopLeft => Vec2::default(),
340        Origin::BottomLeft => Vec2 {
341            x: 0.,
342            y: -f64::from(pixmap.height()),
343        },
344    };
345
346    let transform = initial_transform
347        .pre_translate(Vec2::new(glyph.x.into(), glyph.y.into()))
348        // Apply outer bearings.
349        .pre_translate(Vec2 {
350            x: (-bitmap_glyph.bearing_x * font_units_to_size).into(),
351            y: (bearing_y * font_units_to_size).into(),
352        })
353        // Scale to pixel-space.
354        .pre_scale_non_uniform(f64::from(x_scale_factor), f64::from(y_scale_factor))
355        // Apply inner bearings.
356        .pre_translate(Vec2 {
357            x: (-bitmap_glyph.inner_bearing_x).into(),
358            y: (-bitmap_glyph.inner_bearing_y).into(),
359        })
360        .pre_translate(origin_shift);
361
362    // Scale factor already accounts for ppem, so we can just draw in the size of the
363    // actual image
364    let area = Rect::new(
365        0.0,
366        0.0,
367        f64::from(pixmap.width()),
368        f64::from(pixmap.height()),
369    );
370
371    (GlyphType::Bitmap(BitmapGlyph { pixmap, area }), transform)
372}
373
374fn prepare_colr_glyph<'a>(
375    font_ref: &'a FontRef<'a>,
376    glyph: Glyph,
377    font_size: f32,
378    upem: f32,
379    run_transform: Affine,
380    color_glyph: skrifa::color::ColorGlyph<'a>,
381    normalized_coords: &'a [skrifa::instance::NormalizedCoord],
382) -> (GlyphType<'a>, Affine) {
383    // A couple of notes on the implementation here:
384    //
385    // Firstly, COLR glyphs, similarly to normal outline
386    // glyphs, are by default specified in an upside-down coordinate system. They operate
387    // on a layer-based push/pop system, where you push new clip or blend layers and then
388    // fill the whole available area (within the current clipping area) with a specific paint.
389    // Rendering those glyphs in the main scene would be very expensive, as we have to push/pop
390    // layers on the whole canvas just to draw a small glyph (at least with the current architecture).
391    // Because of this, clients are instead supposed to create an intermediate texture to render the
392    // glyph onto and then render it similarly to a bitmap glyph. This also makes it possible to cache
393    // the glyphs.
394    //
395    // Next, there is a problem when rendering COLR glyphs to an intermediate pixmap: The bounding box
396    // of a glyph can reach into the negative, meaning that parts of it might be cut off when
397    // rendering it directly. Because of this, before drawing we first apply a shift transform so
398    // that the bounding box of the glyph starts at (0, 0), then we draw the whole glyph, and
399    // finally when positioning the actual pixmap in the scene, we reverse that transform so that
400    // the position stays the same as the original one.
401
402    let scale = font_size / upem;
403
404    let transform = run_transform.pre_translate(Vec2::new(glyph.x.into(), glyph.y.into()));
405
406    // Estimate the size of the intermediate pixmap. Ideally, the intermediate bitmap should have
407    // exactly one pixel (or more) per device pixel, to ensure that no quality is lost. Therefore,
408    // we simply use the scaling/skewing factor to calculate how much to scale by, and use the
409    // maximum of both dimensions.
410    let scale_factor = {
411        let (x_vec, y_vec) = x_y_advances(&transform.pre_scale(f64::from(scale)));
412        x_vec.length().max(y_vec.length())
413    };
414
415    let bbox = color_glyph
416        .bounding_box(LocationRef::default(), Size::unscaled())
417        .map(convert_bounding_box)
418        .unwrap_or(Rect::new(0.0, 0.0, f64::from(upem), f64::from(upem)));
419
420    // Calculate the position of the rectangle that will contain the rendered pixmap in device
421    // coordinates.
422    let scaled_bbox = bbox.scale_from_origin(scale_factor);
423
424    let glyph_transform = transform
425        // There are two things going on here:
426        // - On the one hand, for images, the position (0, 0) will be at the top-left, while
427        //   for images, the position will be at the bottom-left.
428        // - COLR glyphs have a flipped y-axis, so in the intermediate image they will be
429        //   upside down.
430        // Because of both of these, all we simply need to do is to flip the image on the y-axis.
431        // This will ensure that the glyph in the image isn't upside down anymore, and at the same
432        // time also flips from having the origin in the top-left to having the origin in the
433        // bottom-right.
434        * Affine::scale_non_uniform(1.0, -1.0)
435        // Shift the pixmap back so that the bbox aligns with the original position
436        // of where the glyph should be placed.
437        * Affine::translate((scaled_bbox.x0, scaled_bbox.y0));
438
439    let (pix_width, pix_height) = (
440        scaled_bbox.width().ceil() as u16,
441        scaled_bbox.height().ceil() as u16,
442    );
443
444    let draw_transform =
445        // Shift everything so that the bbox starts at (0, 0) and the whole visible area of
446        // the glyph will be contained in the intermediate pixmap.
447        Affine::translate((-scaled_bbox.x0, -scaled_bbox.y0)) *
448        // Scale down to the actual size that the COLR glyph will have in device units.
449        Affine::scale(scale_factor);
450
451    // The shift-back happens in `glyph_transform`, so here we can assume (0.0, 0.0) as the origin
452    // of the area we want to draw to.
453    let area = Rect::new(0.0, 0.0, scaled_bbox.width(), scaled_bbox.height());
454
455    (
456        GlyphType::Colr(Box::new(ColorGlyph {
457            skrifa_glyph: color_glyph,
458            font_ref,
459            location: LocationRef::new(normalized_coords),
460            area,
461            pix_width,
462            pix_height,
463            draw_transform,
464        })),
465        glyph_transform,
466    )
467}
468
469/// Rendering style for glyphs.
470#[derive(Debug, Clone, Copy, PartialEq, Eq)]
471pub enum Style {
472    /// Fill the glyph.
473    Fill,
474    /// Stroke the glyph.
475    Stroke,
476}
477
478/// A sequence of glyphs with shared rendering properties.
479#[derive(Clone, Debug)]
480struct GlyphRun<'a> {
481    /// Font for all glyphs in the run.
482    font: Font,
483    /// Size of the font in pixels per em.
484    font_size: f32,
485    /// Global transform.
486    transform: Affine,
487    /// Per-glyph transform. Use [`Affine::skew`] with horizontal-skew only to simulate italic
488    /// text.
489    glyph_transform: Option<Affine>,
490    /// Normalized variation coordinates for variable fonts.
491    normalized_coords: &'a [skrifa::instance::NormalizedCoord],
492    /// Controls whether font hinting is enabled.
493    hint: bool,
494}
495
496struct PreparedGlyphRun<'a> {
497    /// The total transform (`global_transform * glyph_transform`), not accounting for glyph
498    /// translation.
499    transform: Affine,
500    /// The font size to generate glyph outlines for.
501    size: Size,
502    normalized_coords: &'a [skrifa::instance::NormalizedCoord],
503    hinting_instance: Option<HintingInstance>,
504}
505
506/// Prepare a glyph run for rendering.
507///
508/// This function calculates the appropriate transform, size, and scaling parameters
509/// for proper font hinting when enabled and possible.
510fn prepare_glyph_run<'a>(
511    run: &GlyphRun<'a>,
512    outlines: &OutlineGlyphCollection<'_>,
513) -> PreparedGlyphRun<'a> {
514    if !run.hint {
515        return PreparedGlyphRun {
516            transform: run.transform * run.glyph_transform.unwrap_or(Affine::IDENTITY),
517            size: Size::new(run.font_size),
518            normalized_coords: run.normalized_coords,
519            hinting_instance: None,
520        };
521    }
522
523    // We perform vertical-only hinting.
524    //
525    // Hinting doesn't make sense if we later scale the glyphs via some transform. So we extract
526    // the scale from the global transform and glyph transform and apply it to the font size for
527    // hinting. We do require the scaling to be uniform: simply using the vertical scale as font
528    // size and then transforming by the relative horizontal scale can cause, e.g., overlapping
529    // glyphs. Note that this extracted scale should be later applied to the glyph's position.
530    //
531    // As the hinting is vertical-only, we can handle horizontal skew, but not vertical skew or
532    // rotations.
533
534    let total_transform = run.transform * run.glyph_transform.unwrap_or(Affine::IDENTITY);
535    let [t_a, t_b, t_c, t_d, t_e, t_f] = total_transform.as_coeffs();
536
537    let uniform_scale = t_a == t_d;
538    let vertically_uniform = t_b == 0.;
539
540    if uniform_scale && vertically_uniform {
541        let vertical_font_size = run.font_size * t_d as f32;
542        let size = Size::new(vertical_font_size);
543        let hinting_instance =
544            HintingInstance::new(outlines, size, run.normalized_coords, HINTING_OPTIONS).ok();
545        PreparedGlyphRun {
546            transform: Affine::new([1., 0., t_c, 1., t_e, t_f]),
547            size,
548            normalized_coords: run.normalized_coords,
549            hinting_instance,
550        }
551    } else {
552        PreparedGlyphRun {
553            transform: run.transform * run.glyph_transform.unwrap_or(Affine::IDENTITY),
554            size: Size::new(run.font_size),
555            normalized_coords: run.normalized_coords,
556            hinting_instance: None,
557        }
558    }
559}
560
561// TODO: Although these are sane defaults, we might want to make them
562// configurable.
563const HINTING_OPTIONS: HintingOptions = HintingOptions {
564    engine: skrifa::outline::Engine::AutoFallback,
565    target: skrifa::outline::Target::Smooth {
566        mode: skrifa::outline::SmoothMode::Lcd,
567        symmetric_rendering: false,
568        preserve_linear_metrics: true,
569    },
570};
571
572pub(crate) struct OutlinePath(pub(crate) BezPath);
573
574impl OutlinePath {
575    pub(crate) fn new() -> Self {
576        Self(BezPath::new())
577    }
578}
579
580// Note that we flip the y-axis to match our coordinate system.
581impl OutlinePen for OutlinePath {
582    #[inline]
583    fn move_to(&mut self, x: f32, y: f32) {
584        self.0.move_to((x, y));
585    }
586
587    #[inline]
588    fn line_to(&mut self, x: f32, y: f32) {
589        self.0.line_to((x, y));
590    }
591
592    #[inline]
593    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
594        self.0.curve_to((cx0, cy0), (cx1, cy1), (x, y));
595    }
596
597    #[inline]
598    fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
599        self.0.quad_to((cx, cy), (x, y));
600    }
601
602    #[inline]
603    fn close(&mut self) {
604        self.0.close_path();
605    }
606}
607
608/// A normalized variation coordinate (for variable fonts) in 2.14 fixed point format.
609///
610/// In most cases, this can be [cast](bytemuck::cast_slice) from the
611/// normalised coords provided by your text layout library.
612///
613/// Equivalent to [`skrifa::instance::NormalizedCoord`], but defined
614/// in Vello so that Skrifa is not part of Vello's public API.
615/// This allows Vello to update its Skrifa in a patch release, and limits
616/// the need for updates only to align Skrifa versions.
617pub type NormalizedCoord = i16;
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622
623    const _NORMALISED_COORD_SIZE_MATCHES: () =
624        assert!(size_of::<skrifa::instance::NormalizedCoord>() == size_of::<NormalizedCoord>());
625}