Skip to main content

glifo/
glyph.rs

1// Copyright 2025 the Vello Authors and the Parley Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Processing and drawing glyphs.
5
6#![allow(
7    clippy::cast_possible_truncation,
8    reason = "We temporarily ignore these because the casts\
9only break in edge cases, and some of them are also only related to conversions from f64 to f32."
10)]
11
12use crate::Pixmap;
13use crate::atlas::AtlasSlot;
14use crate::atlas::GlyphCacheKey;
15use crate::atlas::key::{SUBPIXEL_BITMAP, SUBPIXEL_COLR, pack_color};
16use crate::atlas::{GlyphAtlas, ImageCache};
17use crate::color::PremulRgba8;
18use crate::color::palette::css::BLACK;
19use crate::colr::{convert_bounding_box, get_colr_info};
20use crate::kurbo::Point;
21use crate::kurbo::Rect;
22use crate::kurbo::Vec2;
23use crate::kurbo::{self, Affine, BezPath, Diagonal2, Join, Line, ParamCurve as _, PathSeg, Shape};
24use crate::peniko::FontData;
25use crate::renderer::{fill_glyph, render_cached_glyph, stroke_glyph};
26use crate::util::AffineExt;
27use alloc::boxed::Box;
28use alloc::sync::Arc;
29use alloc::vec::Vec;
30use core::fmt::{Debug, Formatter};
31use core::ops::RangeInclusive;
32#[cfg(not(feature = "std"))]
33use core_maths::CoreFloat as _;
34use hashbrown::hash_map::{Entry, RawEntryMut};
35use hashbrown::{Equivalent, HashMap};
36use skrifa::bitmap::{BitmapData, BitmapFormat, BitmapStrikes, Origin};
37use skrifa::instance::{LocationRef, Size};
38use skrifa::outline::{DrawSettings, OutlineGlyphFormat};
39use skrifa::outline::{HintingInstance, HintingOptions, OutlinePen};
40use skrifa::raw::TableProvider;
41use skrifa::{FontRef, OutlineGlyphCollection};
42use skrifa::{GlyphId, MetadataProvider};
43use smallvec::SmallVec;
44use vello_common::paint::PaintType;
45
46/// Positioned glyph.
47#[derive(Copy, Clone, Default, Debug)]
48pub struct Glyph {
49    /// The font-specific identifier for this glyph.
50    ///
51    /// This ID is specific to the font being used and corresponds to the
52    /// glyph index within that font. It is *not* a Unicode code point.
53    pub id: u32,
54    /// X-offset in run, relative to transform.
55    pub x: f32,
56    /// Y-offset in run, relative to transform.
57    pub y: f32,
58}
59
60/// Synthetic embolden settings for a glyph run.
61#[derive(Clone, Copy, Debug)]
62pub struct FontEmbolden {
63    /// Synthetic embolden amount.
64    pub amount: Diagonal2,
65    /// Join style used when expanding outlines.
66    pub join: Join,
67    /// Miter limit used when expanding outlines.
68    pub miter_limit: f64,
69    /// Tolerance used when expanding outlines.
70    pub tolerance: f64,
71}
72
73impl FontEmbolden {
74    /// Create synthetic embolden settings with default expansion controls.
75    pub fn new(amount: Diagonal2) -> Self {
76        Self {
77            amount,
78            ..Self::default()
79        }
80    }
81
82    /// Set the join style used when expanding outlines.
83    pub fn with_join(mut self, join: Join) -> Self {
84        self.join = join;
85        self
86    }
87
88    /// Set the miter limit used when expanding outlines.
89    pub fn with_miter_limit(mut self, miter_limit: f64) -> Self {
90        self.miter_limit = miter_limit;
91        self
92    }
93
94    /// Set the tolerance used when expanding outlines.
95    pub fn with_tolerance(mut self, tolerance: f64) -> Self {
96        self.tolerance = tolerance;
97        self
98    }
99}
100
101impl Default for FontEmbolden {
102    fn default() -> Self {
103        Self {
104            amount: Diagonal2::new(0.0, 0.0),
105            join: Join::Miter,
106            miter_limit: 4.0,
107            tolerance: 0.1,
108        }
109    }
110}
111
112/// Pre-packed `BLACK` color as a `u32` for use in `GlyphCacheKey`.
113const BLACK_PACKED: u32 = PremulRgba8 {
114    r: 0,
115    g: 0,
116    b: 0,
117    a: 255,
118}
119.to_u32();
120
121/// A type of glyph.
122#[derive(Debug)]
123pub(crate) enum GlyphType<'a> {
124    /// An outline glyph.
125    Outline(GlyphOutline),
126    /// A bitmap glyph.
127    Bitmap(GlyphBitmap),
128    /// A COLR glyph.
129    Colr(Box<GlyphColr<'a>>),
130}
131
132/// Type hint for cached glyph rendering.
133///
134/// Used when rendering directly from the atlas cache to skip glyph preparation.
135#[derive(Debug, Clone, Copy)]
136pub(crate) enum CachedGlyphType {
137    /// An outline glyph cached in the atlas.
138    Outline,
139    /// A bitmap glyph cached in the atlas.
140    Bitmap,
141    /// A COLR glyph cached in the atlas.
142    /// The `Rect` parameter contains the fractional area dimensions
143    /// to preserve sub-pixel accuracy during rendering.
144    Colr(Rect),
145}
146
147/// A simplified representation of a glyph, prepared for easy rendering.
148#[derive(Debug)]
149pub(crate) struct PreparedGlyph<'a> {
150    /// The type of glyph.
151    pub(crate) glyph_type: GlyphType<'a>,
152    /// Per-glyph outline transform: maps the draw-unit glyph outline
153    /// (after font-size absorption) to scene coordinates.
154    pub(crate) outline_transform: Affine,
155    /// The transform of the paint, relative to [`PreparedGlyph::outline_transform`] *
156    /// [`GlyphScaleProperties::draw_scale`].
157    pub(crate) relative_paint_transform: Affine,
158    /// Cache key for renderers that implement glyph caching.
159    /// This is `Some` for glyphs that can be cached, `None` otherwise.
160    ///
161    /// For COLR glyphs, `context_color` is extracted from the renderer's
162    /// current paint during cache key creation.
163    pub(crate) cache_key: Option<GlyphCacheKey>,
164}
165
166/// A glyph defined by a path (its outline) and a local transform.
167#[derive(Debug)]
168pub(crate) struct GlyphOutline {
169    /// The path of the glyph (shared with the outline cache via `Arc`).
170    pub(crate) path: Arc<BezPath>,
171    /// Precise bounding box of the path at the cached outline size.
172    pub(crate) bbox: Rect,
173    /// Scale from the cached outline size to the requested draw size.
174    pub(crate) scale: f64,
175}
176
177/// A glyph defined by a bitmap.
178#[derive(Debug)]
179pub(crate) struct GlyphBitmap {
180    /// The pixmap of the glyph.
181    pub(crate) pixmap: Arc<Pixmap>,
182    /// The rectangular area that should be filled with the bitmap when painting.
183    pub(crate) area: Rect,
184}
185
186/// Basic metadata about a font.
187#[derive(Clone, Copy, Debug)]
188pub(crate) struct FontInfo {
189    /// Unique identifier for the font data.
190    pub(crate) id: u64,
191    /// Index of the font within the font data.
192    pub(crate) index: u32,
193    /// Font units per em.
194    pub(crate) upem: f32,
195}
196
197/// A glyph defined by a COLR glyph description.
198///
199/// Clients are supposed to first draw the glyph into an intermediate image texture/pixmap
200/// and then render that into the actual scene, in a similar fashion to
201/// bitmap glyphs.
202pub struct GlyphColr<'a> {
203    /// The original skrifa color glyph.
204    pub skrifa_glyph: skrifa::color::ColorGlyph<'a>,
205    /// The location of the glyph.
206    pub location: LocationRef<'a>,
207    /// The font reference.
208    pub font_ref: &'a FontRef<'a>,
209    /// Basic metadata about the font.
210    pub(crate) font_info: FontInfo,
211    /// The transform to apply to the glyph.
212    pub draw_transform: Affine,
213    /// The rectangular area that should be filled with the rendered representation of the
214    /// COLR glyph when painting.
215    pub area: Rect,
216    /// The width of the pixmap/texture in pixels to which the glyph should be rendered to.
217    pub pix_width: u16,
218    /// The height of the pixmap/texture in pixels to which the glyph should be rendered to.
219    pub pix_height: u16,
220    /// Whether the glyph paint graph uses a non-default blend mode.
221    pub has_non_default_blend: bool,
222}
223
224impl Debug for GlyphColr<'_> {
225    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
226        write!(f, "GlyphColr")
227    }
228}
229
230/// Caches used for preparing glyph drawing.
231#[derive(Debug, Default)]
232pub struct GlyphPrepCache {
233    /// Caches glyph outlines.
234    pub(crate) outline_cache: OutlineCache,
235    /// Caches hinting instances.
236    pub(crate) hinting_cache: HintCache,
237    /// Horizontal spans excluded from "ink-skipping" underlines.
238    pub(crate) underline_exclusions: Vec<(f64, f64)>,
239}
240
241impl GlyphPrepCache {
242    /// Borrow this cache bundle mutable for glyph run construction.
243    pub fn as_mut(&mut self) -> GlyphPrepCacheMut<'_> {
244        GlyphPrepCacheMut {
245            outline_cache: &mut self.outline_cache,
246            hinting_cache: &mut self.hinting_cache,
247            underline_exclusions: &mut self.underline_exclusions,
248        }
249    }
250
251    /// Clear the glyph preparation caches.
252    pub fn clear(&mut self) {
253        self.outline_cache.clear();
254        self.hinting_cache.clear();
255        self.underline_exclusions.clear();
256    }
257
258    /// Maintain the glyph preparation caches.
259    pub fn maintain(&mut self) {
260        self.outline_cache.maintain();
261    }
262}
263
264/// Mutably borrowed caches used for preparing glyph drawing.
265#[derive(Debug)]
266pub struct GlyphPrepCacheMut<'a> {
267    /// Caches glyph outlines.
268    pub(crate) outline_cache: &'a mut OutlineCache,
269    /// Caches hinting instances .
270    pub(crate) hinting_cache: &'a mut HintCache,
271    /// Horizontal spans excluded from "ink-skipping" underlines.
272    pub(crate) underline_exclusions: &'a mut Vec<(f64, f64)>,
273}
274
275/// Determines whether atlas-backed glyph caching is available for a draw.
276#[derive(Debug)]
277pub enum AtlasCacher<'a> {
278    /// Draw directly without using the atlas cache.
279    Disabled,
280    /// Enable atlas-backed caching using the provided glyph atlas and image
281    /// allocator.
282    Enabled(&'a mut GlyphAtlas, &'a mut ImageCache),
283}
284
285impl AtlasCacher<'_> {
286    fn config(&self) -> Option<&crate::atlas::GlyphCacheConfig> {
287        match self {
288            Self::Disabled => None,
289            Self::Enabled(glyph_atlas, _) => Some(glyph_atlas.config()),
290        }
291    }
292
293    fn get(&mut self, key: &GlyphCacheKey) -> Option<AtlasSlot> {
294        match self {
295            Self::Disabled => None,
296            Self::Enabled(glyph_atlas, _) => glyph_atlas.get(key),
297        }
298    }
299}
300
301/// A backend for glyph run builders.
302pub trait GlyphRunBackend<'a>: Sized {
303    /// Enable or disable atlas-backed glyph caching for the glyph run.
304    ///
305    /// **Note: Atlas caching is currently highly experimental and not
306    /// recommended for external use.**
307    fn atlas_cache(self, enabled: bool) -> Self;
308
309    /// Fill the given glyph sequence using the configured builder state.
310    fn fill_glyphs<Glyphs>(self, run: GlyphRun<'a>, glyphs: Glyphs)
311    where
312        Glyphs: Iterator<Item = Glyph> + Clone;
313
314    /// Stroke the given glyph sequence using the configured builder state.
315    fn stroke_glyphs<Glyphs>(self, run: GlyphRun<'a>, glyphs: Glyphs)
316    where
317        Glyphs: Iterator<Item = Glyph> + Clone;
318
319    /// Render a decoration (e.g. underline) with skip-ink behavior.
320    fn render_decoration<Glyphs>(
321        self,
322        run: GlyphRun<'a>,
323        glyphs: Glyphs,
324        x_range: RangeInclusive<f32>,
325        baseline_y: f32,
326        offset: f32,
327        size: f32,
328        buffer: f32,
329    ) where
330        Glyphs: Iterator<Item = Glyph> + Clone;
331}
332
333/// Helper struct for rendering a prepared glyph run.
334#[derive(Debug)]
335pub struct GlyphRunRenderer<'a, 'b, Glyphs: Iterator<Item = Glyph> + Clone> {
336    prepared_run: PreparedGlyphRun<'a>,
337    outline_cache: &'b mut OutlineCache,
338    underline_span_cache: &'b mut Vec<(f64, f64)>,
339    glyph_iterator: Glyphs,
340    atlas_cacher: AtlasCacher<'b>,
341}
342
343impl<'a, 'b, Glyphs: Iterator<Item = Glyph> + Clone> GlyphRunRenderer<'a, 'b, Glyphs> {
344    /// Fills the glyphs with the current configuration.
345    pub fn fill_glyphs(&mut self, renderer: &mut impl crate::GlyphRenderer) {
346        self.draw_glyphs(Style::Fill, renderer);
347    }
348
349    /// Strokes the glyphs with the current configuration.
350    pub fn stroke_glyphs(&mut self, renderer: &mut impl crate::GlyphRenderer) {
351        self.draw_glyphs(Style::Stroke, renderer);
352    }
353
354    /// Core rendering loop shared by [`fill_glyphs`](Self::fill_glyphs) and
355    /// [`stroke_glyphs`](Self::stroke_glyphs).
356    ///
357    /// Each glyph is resolved through a priority cascade: COLR > bitmap > outline.
358    /// The first matching representation wins. Within each branch the atlas cache
359    /// is checked before falling through to the slow path (rasterization / path
360    /// construction).
361    fn draw_glyphs(&mut self, style: Style, renderer: &mut impl crate::GlyphRenderer) {
362        let font_ref = self.prepared_run.font.as_skrifa();
363
364        let outlines = font_ref.outline_glyphs();
365        let color_glyphs = font_ref.color_glyphs();
366        let bitmaps = font_ref.bitmap_strikes();
367
368        let mut outline_cache_session = OutlineCacheSession::new(
369            self.outline_cache,
370            VarLookupKey::new(self.prepared_run.normalized_coords),
371        );
372        let PreparedGlyphRun {
373            draw_props,
374            scene_paint_transform,
375            run_size: _,
376            font_info,
377            font_embolden,
378            normalized_coords,
379            hinting_instance,
380            ..
381        } = self.prepared_run;
382
383        let hinted = hinting_instance.is_some();
384
385        let colr_bitmap_cache_enabled = self
386            .atlas_cacher
387            .config()
388            .is_some_and(|config| draw_props.font_size <= config.max_cached_font_size);
389        let outline_cache_enabled = colr_bitmap_cache_enabled
390            // Due to the various parameters that would need to be considered in the cache key,
391            // we never cache stroked outlines for now. For COLR and bitmap, this doesn't matter
392            // because they are always filled anyway.
393            && style == Style::Fill
394            // We use image tinting to color cached glyphs, which is not 
395            // supported for complex paints.
396            && matches!(renderer.current_paint(), PaintType::Solid(_));
397
398        let context_color = renderer.get_context_color();
399        let context_color_packed = pack_color(context_color);
400        let scale_props =
401            GlyphScaleProperties::new(draw_props.font_size, font_info.upem, hinted, style);
402
403        for glyph in self.glyph_iterator.clone() {
404            // TODO: Add a mechanism such that glyphs that are completely outside of the viewport
405            // (especially for more expensive COLR glyphs), we don't do any processing in the
406            // first place and cull them.
407            let glyph_id = GlyphId::new(glyph.id);
408
409            // ── Speculative outline cache check ─────────────────────────
410            // ~99% of glyphs are outlines. The transform and cache key are
411            // pure arithmetic, so we probe the cache before the expensive
412            // color_glyphs.get() / bitmaps.glyph_for_size() font-table lookups.
413            // On a miss we keep both for reuse in the outline branch below.
414            let outline_transform =
415                calculate_outline_transform(glyph, draw_props, hinting_instance);
416            let outline_draw_transform = outline_transform.pre_scale(scale_props.draw_scale);
417
418            // We assume that the backend calculates the absolute paint transform
419            // by concatenating scene transform and (relative) paint transform.
420            // (This is currently the case for Vello CPU / Vello Hybrid, but will
421            // also be assumed to be the case for any other potential backend.)
422            // Therefore, we can calculate the relative paint transform for
423            // the glyph by pre-concatenating it with the inverted outline transform.
424            let outline_cache_key = outline_cache_enabled.then(|| {
425                let fractional_x = outline_transform.translation().x.fract() as f32;
426                GlyphCacheKey::new(
427                    font_info.id,
428                    font_info.index,
429                    glyph.id,
430                    draw_props.font_size,
431                    hinted,
432                    fractional_x,
433                    BLACK,
434                    BLACK_PACKED,
435                    font_embolden,
436                    normalized_coords,
437                )
438            });
439            if let Some(ref key) = outline_cache_key
440                && let Some(cached_slot) = self.atlas_cacher.get(key)
441            {
442                render_cached_glyph(
443                    renderer,
444                    cached_slot,
445                    outline_transform,
446                    CachedGlyphType::Outline,
447                );
448                continue;
449            }
450
451            // ── COLR Glyphs ───────────────────────────────────────────
452            if let Some(color_glyph) = color_glyphs.get(glyph_id) {
453                let location = LocationRef::new(normalized_coords);
454                let metrics = calculate_colr_metrics(
455                    draw_props.font_size,
456                    draw_props,
457                    glyph,
458                    &font_ref,
459                    &color_glyph,
460                    location,
461                    &mut outline_cache_session,
462                    font_info,
463                );
464                let outline_transform = calculate_colr_transform(&metrics);
465
466                // COLR glyphs are never hinted and have no sub-pixel offset;
467                // context_color is part of the key because it affects painted layers.
468                let cache_key = colr_bitmap_cache_enabled.then(|| GlyphCacheKey {
469                    font_id: font_info.id,
470                    font_index: font_info.index,
471                    glyph_id: glyph.id,
472                    size_bits: draw_props.font_size.to_bits(),
473                    hinted: false,
474                    subpixel_x: SUBPIXEL_COLR,
475                    context_color,
476                    context_color_packed,
477                    embolden_x_bits: 0,
478                    embolden_y_bits: 0,
479                    embolden_join_bits: join_bits(Join::Miter),
480                    embolden_miter_limit_bits: 4.0_f32.to_bits(),
481                    embolden_tolerance_bits: 0.1_f32.to_bits(),
482                    var_coords: SmallVec::from_slice(normalized_coords),
483                });
484
485                if let Some(ref key) = cache_key
486                    && let Some(cached_slot) = self.atlas_cacher.get(key)
487                {
488                    // Use fractional scaled_bbox dimensions to preserve sub-pixel accuracy.
489                    let area = Rect::new(
490                        0.0,
491                        0.0,
492                        metrics.scaled_bbox.width(),
493                        metrics.scaled_bbox.height(),
494                    );
495                    render_cached_glyph(
496                        renderer,
497                        cached_slot,
498                        outline_transform,
499                        CachedGlyphType::Colr(area),
500                    );
501                    continue;
502                }
503
504                // Cache miss — rasterize the COLR glyph from scratch.
505                let glyph_type = create_colr_glyph(
506                    &font_ref,
507                    &metrics,
508                    color_glyph,
509                    normalized_coords,
510                    font_info,
511                );
512
513                let prepared_glyph = PreparedGlyph {
514                    glyph_type,
515                    outline_transform,
516                    relative_paint_transform: Affine::IDENTITY,
517                    cache_key,
518                };
519                match style {
520                    Style::Fill => fill_glyph(
521                        renderer,
522                        prepared_glyph,
523                        &mut self.atlas_cacher,
524                        &mut outline_cache_session,
525                    ),
526                    Style::Stroke => stroke_glyph(
527                        renderer,
528                        prepared_glyph,
529                        &mut self.atlas_cacher,
530                        &mut outline_cache_session,
531                    ),
532                }
533                continue;
534            }
535
536            // ── Bitmap Glyphs ────────────────────────────────────────────
537            let bitmap_data: Option<(skrifa::bitmap::BitmapGlyph<'_>, Pixmap)> = bitmaps
538                .glyph_for_size(Size::new(draw_props.font_size), glyph_id)
539                .and_then(|g| match g.data {
540                    #[cfg(feature = "png")]
541                    BitmapData::Png(data) => Pixmap::from_png(std::io::Cursor::new(data))
542                        .ok()
543                        .map(|d| (g, d)),
544                    #[cfg(not(feature = "png"))]
545                    BitmapData::Png(_) => None,
546                    // The others are not worth implementing for now (unless we can find a test case),
547                    // they should be very rare.
548                    BitmapData::Bgra(_) => None,
549                    BitmapData::Mask(_) => None,
550                });
551
552            if let Some((bitmap_glyph, pixmap)) = bitmap_data {
553                // Bitmaps use the strike's own ppem, not the run's, because the
554                // image was pre-rendered at that specific size.
555                let bitmap_ppem = bitmap_glyph.ppem_x;
556                let outline_transform = calculate_bitmap_transform(
557                    glyph,
558                    &pixmap,
559                    draw_props,
560                    draw_props.font_size,
561                    font_info.upem,
562                    &bitmap_glyph,
563                    &bitmaps,
564                );
565
566                // Bitmaps are not hinted and have no sub-pixel offset or
567                // context color; variation coords are irrelevant for fixed strikes.
568                let cache_key = colr_bitmap_cache_enabled.then(|| GlyphCacheKey {
569                    font_id: font_info.id,
570                    font_index: font_info.index,
571                    glyph_id: glyph.id,
572                    size_bits: bitmap_ppem.to_bits(),
573                    hinted: false,
574                    subpixel_x: SUBPIXEL_BITMAP,
575                    context_color: BLACK,
576                    context_color_packed: BLACK_PACKED,
577                    embolden_x_bits: 0,
578                    embolden_y_bits: 0,
579                    embolden_join_bits: join_bits(Join::Miter),
580                    embolden_miter_limit_bits: 4.0_f32.to_bits(),
581                    embolden_tolerance_bits: 0.1_f32.to_bits(),
582                    var_coords: SmallVec::new(),
583                });
584
585                if let Some(ref key) = cache_key
586                    && let Some(cached_slot) = self.atlas_cacher.get(key)
587                {
588                    render_cached_glyph(
589                        renderer,
590                        cached_slot,
591                        outline_transform,
592                        CachedGlyphType::Bitmap,
593                    );
594                    continue;
595                }
596
597                // Cache miss — wrap the decoded pixmap for rendering.
598                let glyph_type = create_bitmap_glyph(pixmap);
599
600                let prepared_glyph = PreparedGlyph {
601                    glyph_type,
602                    outline_transform,
603                    relative_paint_transform: Affine::IDENTITY,
604                    cache_key,
605                };
606                match style {
607                    Style::Fill => fill_glyph(
608                        renderer,
609                        prepared_glyph,
610                        &mut self.atlas_cacher,
611                        &mut outline_cache_session,
612                    ),
613                    Style::Stroke => stroke_glyph(
614                        renderer,
615                        prepared_glyph,
616                        &mut self.atlas_cacher,
617                        &mut outline_cache_session,
618                    ),
619                }
620                continue;
621            }
622
623            // ── Outline Glyphs ──────────────────────────────────────────
624            // Transform and cache key were already computed at the top of the
625            // loop for the speculative check. Reuse them here on a cache miss.
626
627            // Cache miss — fetch the outline from skrifa (expensive: parses font
628            // tables), then build the path. Deferred to here so cache hits skip it.
629            let Some(outline) = outlines.get(glyph_id) else {
630                continue;
631            };
632
633            let glyph_type = create_outline_glyph(
634                glyph.id,
635                font_info,
636                &mut outline_cache_session,
637                scale_props.cache_size,
638                scale_props.draw_scale,
639                font_embolden,
640                &outline,
641                hinting_instance,
642                normalized_coords,
643            );
644
645            let relative_paint_transform = outline_draw_transform.inverse() * scene_paint_transform;
646
647            let prepared_glyph = PreparedGlyph {
648                glyph_type,
649                outline_transform,
650                relative_paint_transform,
651                cache_key: outline_cache_key,
652            };
653            match style {
654                Style::Fill => fill_glyph(
655                    renderer,
656                    prepared_glyph,
657                    &mut self.atlas_cacher,
658                    &mut outline_cache_session,
659                ),
660                Style::Stroke => stroke_glyph(
661                    renderer,
662                    prepared_glyph,
663                    &mut self.atlas_cacher,
664                    &mut outline_cache_session,
665                ),
666            }
667        }
668    }
669
670    /// Return the scaling factor that should be applied to the stroke width when stroking this
671    /// glyph run.
672    pub fn stroke_adjustment(&self) -> f64 {
673        let run_size = self.prepared_run.run_size;
674
675        if run_size == 0.0 {
676            1.0
677        } else {
678            f64::from(self.prepared_run.draw_props.font_size / run_size)
679        }
680    }
681
682    /// Render a decoration (like an underline) that skips over glyph descenders.
683    ///
684    /// This implements `text-decoration-skip-ink`-like behavior, where the decoration line is interrupted where it
685    /// would overlap with glyph outlines.
686    ///
687    /// The `x_range` specifies the horizontal position of the decoration, and the `offset` and `size` specify its
688    /// vertical position and height (relative to the baseline). The `buffer` specifies how much horizontal space to
689    /// leave around each descender.
690    pub fn render_decoration(
691        &mut self,
692        x_range: RangeInclusive<f32>,
693        baseline_y: f32,
694        offset: f32,
695        size: f32,
696        buffer: f32,
697        renderer: &mut impl crate::DrawSink,
698    ) {
699        self.decoration_spans(x_range, baseline_y, offset, size, buffer)
700            .for_each(|rect| {
701                renderer.fill_rect(&rect);
702            });
703    }
704
705    fn decoration_spans<'c>(
706        &'c mut self,
707        x_range: RangeInclusive<f32>,
708        baseline_y: f32,
709        offset: f32,
710        size: f32,
711        buffer: f32,
712    ) -> impl Iterator<Item = Rect> + 'c {
713        let font_ref = self.prepared_run.font.as_skrifa();
714        let outlines = font_ref.outline_glyphs();
715
716        let PreparedGlyphRun {
717            draw_props,
718            font_info,
719            font_embolden,
720            hinting_instance,
721            ..
722        } = self.prepared_run;
723
724        // The glyph_transform (e.g. skew for fake italics) affects where the outline points end up. We apply it along
725        // with the Y flip to transform from font space (Y up) to layout space (Y down).
726        //
727        // During the preparation of the glyph run, the transform of the run may be absorbed into
728        // `draw_props.font_size`, outlines are generated in that scaled coordinate space. We scale them back
729        // to the nominal coordinate space. The glyph-drawing path handles this by
730        // simply drawing in global space, but we need to invert it for drawing decorations.
731        let scale_props = GlyphScaleProperties::new(
732            draw_props.font_size,
733            font_info.upem,
734            hinting_instance.is_some(),
735            Style::Fill,
736        );
737        let outline_to_nominal_scale =
738            f64::from(self.prepared_run.run_size / scale_props.cache_size);
739        let outline_transform = self
740            .prepared_run
741            .glyph_transform
742            .unwrap_or(Affine::IDENTITY)
743            * Affine::FLIP_Y
744            * Affine::scale(outline_to_nominal_scale);
745
746        // Buffer to add around each exclusion zone
747        let buffer = f64::from(buffer);
748
749        // X range for the decoration line
750        let x0 = f64::from(*x_range.start());
751        let x1 = f64::from(*x_range.end());
752
753        // Convert offset/size to layout space (Y down).
754        // offset is positive above baseline, so negate for layout coordinates.
755        let layout_y0 = f64::from(-offset);
756        let layout_y1 = f64::from(-offset + size);
757
758        // Get a cache session for this font's variation coordinates
759        let var_key = VarLookupKey::new(self.prepared_run.normalized_coords);
760        let mut outline_cache_session = OutlineCacheSession::new(self.outline_cache, var_key);
761
762        // Collect and merge exclusion zones from all glyphs.
763        let exclusions = &mut self.underline_span_cache;
764        // We `drain` this when creating the iterator, but just in case...
765        exclusions.truncate(0);
766
767        for glyph in self.glyph_iterator.clone() {
768            // TODO: skip ink for color and bitmap glyphs
769            let Some(outline) = outlines.get(GlyphId::new(glyph.id)) else {
770                continue;
771            };
772
773            let cached = outline_cache_session.get_or_insert(
774                glyph.id,
775                font_info,
776                scale_props.cache_size,
777                font_embolden,
778                var_key,
779                &outline,
780                hinting_instance,
781            );
782
783            // If the glyph's bounding box doesn't intersect the underline at all, we don't need to calculate
784            // intersections. This saves a lot of time, since most glyphs don't have descenders.
785            //
786            // We only need the y-extent of the transformed bbox, so we compute it directly using the formula:
787            // y' = b*x + d*y + f
788            let [_, b, _, d, _, f] = outline_transform.as_coeffs();
789            let (y_min, y_max) = {
790                let bx0 = b * cached.bbox.x0;
791                let bx1 = b * cached.bbox.x1;
792                let dy0 = d * cached.bbox.y0;
793                let dy1 = d * cached.bbox.y1;
794                (
795                    f + bx0.min(bx1) + dy0.min(dy1),
796                    f + bx0.max(bx1) + dy0.max(dy1),
797                )
798            };
799            if y_max < layout_y0 || y_min > layout_y1 {
800                continue;
801            }
802
803            let mut rect = Rect {
804                x0: f64::INFINITY,
805                x1: f64::NEG_INFINITY,
806                y0: layout_y0,
807                y1: layout_y1,
808            };
809
810            for seg in cached.path.segments() {
811                // Transform the segment to layout space
812                let seg = outline_transform * seg;
813                expand_rect_with_segment(&mut rect, seg, layout_y0..=layout_y1);
814            }
815
816            // Add glyph position and buffer, then clip to decoration x-range
817            let excl_start = (rect.x0 + f64::from(glyph.x) - buffer).max(x0);
818            let excl_end = (rect.x1 + f64::from(glyph.x) + buffer).min(x1);
819
820            // Skip if no valid exclusion (empty intersection or outside x-range)
821            if excl_start >= excl_end {
822                continue;
823            }
824
825            // Insert in sorted order and merge with overlapping ranges
826            insert_and_merge_range(exclusions, excl_start, excl_end);
827        }
828
829        // Draw decoration segments, skipping the exclusion zones
830        let y0 = f64::from(baseline_y) + layout_y0;
831        let y1 = f64::from(baseline_y) + layout_y1;
832
833        let mut state = Some((exclusions.drain(..), x0));
834        core::iter::from_fn(move || {
835            let (iter, current_x) = state.as_mut()?;
836            let Some((excl_start, excl_end)) = iter.next() else {
837                // Draw the trailing rectangle
838                let final_rect = Rect::new(*current_x, y0, x1, y1);
839                state = None;
840                return (final_rect.width() > 0.0).then_some(final_rect);
841            };
842
843            // Draw segment before this exclusion
844            let rect = Rect::new(*current_x, y0, excl_start, y1);
845            *current_x = excl_end;
846            Some(rect)
847        })
848    }
849}
850
851/// A builder for configuring and drawing glyphs.
852#[derive(Debug)]
853#[must_use = "Methods on the builder don't do anything until `render` is called."]
854pub struct GlyphRunBuilder<'a, B> {
855    run: GlyphRun<'a>,
856    backend: B,
857}
858
859impl<'a, B> GlyphRunBuilder<'a, B> {
860    /// Creates a new builder for drawing glyphs with a pre-bound backend.
861    pub fn new(font: FontData, transform: Affine, paint_transform: Affine, backend: B) -> Self {
862        Self {
863            // Note: This needs to be kept in sync with the default in vello_common!
864            run: GlyphRun {
865                font,
866                font_size: 16.0,
867                font_embolden: FontEmbolden::default(),
868                transform,
869                scene_paint_transform: transform * paint_transform,
870                glyph_transform: None,
871                hint: true,
872                normalized_coords: &[],
873            },
874            backend,
875        }
876    }
877
878    /// Set the font size in pixels per em.
879    pub fn font_size(mut self, size: f32) -> Self {
880        self.run.font_size = size;
881        self
882    }
883
884    /// Set synthetic embolden settings.
885    pub fn font_embolden(mut self, embolden: FontEmbolden) -> Self {
886        self.run.font_embolden = embolden;
887        self
888    }
889
890    /// Set the per-glyph transform. Use `Affine::skew` with a horizontal-only skew to simulate
891    /// italic text.
892    pub fn glyph_transform(mut self, transform: Affine) -> Self {
893        self.run.glyph_transform = Some(transform);
894        self
895    }
896
897    /// Set whether font hinting is enabled.
898    ///
899    /// This performs vertical hinting only. Hinting is performed only if the combined `transform`
900    /// and `glyph_transform` have a uniform scale and no vertical skew or rotation.
901    pub fn hint(mut self, hint: bool) -> Self {
902        self.run.hint = hint;
903        self
904    }
905
906    /// Set normalized variation coordinates for variable fonts.
907    pub fn normalized_coords(mut self, coords: &'a [NormalizedCoord]) -> Self {
908        self.run.normalized_coords = bytemuck::cast_slice(coords);
909        self
910    }
911}
912
913impl<'a> GlyphRun<'a> {
914    // Note: Not sure if we should just remove that method and let each backend
915    // call `prepare_glyph_run` manually, it might allow us to reduce the number of
916    // generics we need to use. But for now, it seems nice to be able to abstract away
917    // the `prepare_glyph_run` method call.
918    /// Returns a renderer that can fill, stroke, and decorate this glyph run.
919    #[doc(hidden)]
920    pub fn build<'b: 'a, Glyphs: Iterator<Item = Glyph> + Clone>(
921        self,
922        glyphs: Glyphs,
923        prep_cache: GlyphPrepCacheMut<'b>,
924        atlas_cacher: AtlasCacher<'b>,
925    ) -> GlyphRunRenderer<'a, 'b, Glyphs> {
926        let prepared_run = prepare_glyph_run(self, prep_cache.hinting_cache);
927        GlyphRunRenderer {
928            prepared_run,
929            glyph_iterator: glyphs,
930            outline_cache: prep_cache.outline_cache,
931            underline_span_cache: prep_cache.underline_exclusions,
932            atlas_cacher,
933        }
934    }
935}
936
937impl<'a, B> GlyphRunBuilder<'a, B>
938where
939    B: GlyphRunBackend<'a>,
940{
941    /// Enable or disable the glyph atlas cache.
942    ///
943    /// **Note: Atlas caching is currently highly experimental and not
944    /// recommended for external use.**
945    pub fn atlas_cache(self, enabled: bool) -> Self {
946        Self {
947            run: self.run,
948            backend: self.backend.atlas_cache(enabled),
949        }
950    }
951
952    /// Fill the glyphs using the current settings.
953    pub fn fill_glyphs<Glyphs>(self, glyphs: Glyphs)
954    where
955        Glyphs: Iterator<Item = Glyph> + Clone,
956    {
957        let GlyphRunBuilder { run, backend } = self;
958        backend.fill_glyphs(run, glyphs);
959    }
960
961    /// Stroke the glyphs using the current settings.
962    pub fn stroke_glyphs<Glyphs>(self, glyphs: Glyphs)
963    where
964        Glyphs: Iterator<Item = Glyph> + Clone,
965    {
966        let GlyphRunBuilder { run, backend } = self;
967        backend.stroke_glyphs(run, glyphs);
968    }
969
970    /// Render a decoration (e.g. underline) with skip-ink behavior.
971    ///
972    /// See [`GlyphRunRenderer::render_decoration`].
973    pub fn render_decoration<Glyphs>(
974        self,
975        glyphs: Glyphs,
976        x_range: RangeInclusive<f32>,
977        baseline_y: f32,
978        offset: f32,
979        size: f32,
980        buffer: f32,
981    ) where
982        Glyphs: Iterator<Item = Glyph> + Clone,
983    {
984        let GlyphRunBuilder { run, backend } = self;
985        backend.render_decoration(run, glyphs, x_range, baseline_y, offset, size, buffer);
986    }
987}
988
989/// Insert a range into a sorted list, merging with any overlapping ranges.
990fn insert_and_merge_range(ranges: &mut Vec<(f64, f64)>, start: f64, end: f64) {
991    // Search backwards from the end to find insertion point. Since glyphs come in visual (left-to-right) order, new
992    // ranges are usually at or near the end, making this O(1) in the common case.
993    let insert_pos = ranges
994        .iter()
995        .rposition(|r| r.0 <= start)
996        .map_or(0, |i| i + 1);
997
998    // Check if we overlap with the previous range
999    let merge_start = insert_pos
1000        .checked_sub(1)
1001        .filter(|&i| ranges[i].1 >= start)
1002        .unwrap_or(insert_pos);
1003
1004    // Find all overlapping ranges and compute merged bounds
1005    let new_end = ranges[merge_start..]
1006        .iter()
1007        .take_while(|(s, _)| *s <= end)
1008        .fold(end, |acc, (_, e)| acc.max(*e));
1009
1010    let merge_end = merge_start
1011        + ranges[merge_start..]
1012            .iter()
1013            .take_while(|(s, _)| *s <= new_end)
1014            .count();
1015
1016    // Replace the overlapping ranges with the merged range
1017    if merge_start < merge_end {
1018        let new_start = start.min(ranges[merge_start].0);
1019        ranges.splice(merge_start..merge_end, [(new_start, new_end)]);
1020    } else {
1021        ranges.insert(insert_pos, (start, end));
1022    }
1023}
1024
1025fn expand_rect_with_segment(rect: &mut Rect, seg: PathSeg, y_span: RangeInclusive<f64>) {
1026    // Calculate the rough bounds of the segment from its control points. This is *not* the same as
1027    // `kurbo::Shape::bounding_box`, which returns a precise bounding box but requires expensively calculating the curve
1028    // extrema.
1029    let (mut x_bounds, y_bounds) = match seg {
1030        PathSeg::Line(line) => (
1031            (line.p0.x.min(line.p1.x), line.p0.x.max(line.p1.x)),
1032            (line.p0.y.min(line.p1.y), line.p0.y.max(line.p1.y)),
1033        ),
1034        PathSeg::Quad(quad) => (
1035            (
1036                quad.p0.x.min(quad.p1.x).min(quad.p2.x),
1037                quad.p0.x.max(quad.p1.x).max(quad.p2.x),
1038            ),
1039            (
1040                quad.p0.y.min(quad.p1.y).min(quad.p2.y),
1041                quad.p0.y.max(quad.p1.y).max(quad.p2.y),
1042            ),
1043        ),
1044        PathSeg::Cubic(cubic) => (
1045            (
1046                cubic.p0.x.min(cubic.p1.x).min(cubic.p2.x).min(cubic.p3.x),
1047                cubic.p0.x.max(cubic.p1.x).max(cubic.p2.x).max(cubic.p3.x),
1048            ),
1049            (
1050                cubic.p0.y.min(cubic.p1.y).min(cubic.p2.y).min(cubic.p3.y),
1051                cubic.p0.y.max(cubic.p1.y).max(cubic.p2.y).max(cubic.p3.y),
1052            ),
1053        ),
1054    };
1055    // Skip segments entirely outside the y_span
1056    if y_bounds.1 < *y_span.start() || y_bounds.0 > *y_span.end() {
1057        return;
1058    }
1059
1060    // All we care about are the x-intersections. The intersection methods don't work on infinitely-long lines, so we
1061    // construct a "long enough" line based on segment bounds. This expansion allows for a little bit of error.
1062    x_bounds.0 -= 1.0;
1063    x_bounds.1 += 1.0;
1064    let top_line = Line::new((x_bounds.0, *y_span.start()), (x_bounds.1, *y_span.start()));
1065    let bottom_line = Line::new((x_bounds.0, *y_span.end()), (x_bounds.1, *y_span.end()));
1066
1067    for intersection in seg.intersect_line(top_line) {
1068        let point = top_line.eval(intersection.line_t);
1069        // There might be some slight inaccuracy calculating `point` from `line_t`, so we only adjust the x-values
1070        // instead of using `union_pt`, which may also expand the y-values.
1071        rect.x0 = rect.x0.min(point.x);
1072        rect.x1 = rect.x1.max(point.x);
1073    }
1074
1075    for intersection in seg.intersect_line(bottom_line) {
1076        let point = bottom_line.eval(intersection.line_t);
1077        rect.x0 = rect.x0.min(point.x);
1078        rect.x1 = rect.x1.max(point.x);
1079    }
1080
1081    // Also check segment endpoints that lie within the y-range
1082    let (seg_start, seg_end) = match seg {
1083        PathSeg::Line(line) => (line.p0, line.p1),
1084        PathSeg::Quad(quad) => (quad.p0, quad.p2),
1085        PathSeg::Cubic(cubic) => (cubic.p0, cubic.p3),
1086    };
1087
1088    for point in [seg_start, seg_end] {
1089        if (*y_span.start()..=*y_span.end()).contains(&point.y) {
1090            rect.x0 = rect.x0.min(point.x);
1091            rect.x1 = rect.x1.max(point.x);
1092        }
1093    }
1094}
1095
1096/// Create outline glyph data from cache.
1097///
1098/// This extracts the glyph path from the outline cache, creating a `GlyphType::Outline`
1099/// without any positioning information.
1100fn create_outline_glyph<'a>(
1101    glyph_id: u32,
1102    font_info: FontInfo,
1103    outline_cache: &mut OutlineCacheSession<'_>,
1104    size: f32,
1105    scale: f64,
1106    embolden: FontEmbolden,
1107    outline_glyph: &skrifa::outline::OutlineGlyph<'a>,
1108    hinting_instance: Option<&HintingInstance>,
1109    normalized_coords: &[skrifa::instance::NormalizedCoord],
1110) -> GlyphType<'a> {
1111    let cached = outline_cache.get_or_insert(
1112        glyph_id,
1113        font_info,
1114        size,
1115        embolden,
1116        VarLookupKey::new(normalized_coords),
1117        outline_glyph,
1118        hinting_instance,
1119    );
1120
1121    GlyphType::Outline(GlyphOutline {
1122        path: Arc::clone(cached.path),
1123        bbox: cached.bbox,
1124        scale,
1125    })
1126}
1127
1128struct GlyphScaleProperties {
1129    /// The size at which the outline was cached.
1130    cache_size: f32,
1131    /// The scale factor that needs to be applied to scale the outline
1132    /// to the draw size.
1133    draw_scale: f64,
1134}
1135
1136impl GlyphScaleProperties {
1137    fn new(draw_font_size: f32, upem: f32, hinted: bool, style: Style) -> Self {
1138        if hinted || style == Style::Stroke {
1139            // For hinting, we need to preserve the original font size since outlines are
1140            // scale-dependent.
1141            // For stroking, we need to preserve the font size because the stroke width would
1142            // be affected by any additional transform (we could support them
1143            // in the future by scaling the outlines directly instead of folding the scale into
1144            // the draw transform, but that would require cloning the `BezPath`).
1145            Self {
1146                cache_size: draw_font_size,
1147                draw_scale: 1.0,
1148            }
1149        } else {
1150            Self {
1151                cache_size: upem,
1152                draw_scale: f64::from(draw_font_size / upem),
1153            }
1154        }
1155    }
1156}
1157
1158/// Calculate transform for outline glyphs.
1159///
1160/// This computes the final positioning transform for an outline glyph, taking into account:
1161/// - Glyph position within the run
1162/// - Run-space glyph positioning
1163/// - Y-axis flip (fonts use upside-down coordinate system)
1164/// - Hinting adjustments (snap y-offset to integer)
1165fn calculate_outline_transform(
1166    glyph: Glyph,
1167    draw_props: DrawProps,
1168    hinting_instance: Option<&HintingInstance>,
1169) -> Affine {
1170    let mut final_transform = draw_props
1171        .positioned_transform(glyph)
1172        .pre_scale_non_uniform(1.0, -1.0)
1173        .as_coeffs();
1174
1175    if hinting_instance.is_some() {
1176        final_transform[5] = final_transform[5].round();
1177    }
1178
1179    Affine::new(final_transform)
1180}
1181
1182/// Create bitmap glyph data.
1183///
1184/// This wraps the pixmap in a `GlyphType::Bitmap` with its display area,
1185/// without any positioning information.
1186fn create_bitmap_glyph(pixmap: Pixmap) -> GlyphType<'static> {
1187    // Scale factor already accounts for ppem, so we can just draw in the size of the
1188    // actual image
1189    let area = Rect::new(
1190        0.0,
1191        0.0,
1192        f64::from(pixmap.width()),
1193        f64::from(pixmap.height()),
1194    );
1195
1196    GlyphType::Bitmap(GlyphBitmap {
1197        pixmap: Arc::new(pixmap),
1198        area,
1199    })
1200}
1201
1202/// Calculate transform for bitmap glyphs.
1203///
1204/// This computes the final positioning transform for a bitmap glyph, taking into account:
1205/// - Glyph position within the run
1206/// - Bitmap scaling to match requested font size
1207/// - Bearing adjustments (outer and inner)
1208/// - Origin placement (top-left vs bottom-left)
1209/// - Special handling for Apple Color Emoji
1210fn calculate_bitmap_transform(
1211    glyph: Glyph,
1212    pixmap: &Pixmap,
1213    draw_props: DrawProps,
1214    font_size: f32,
1215    upem: f32,
1216    bitmap_glyph: &skrifa::bitmap::BitmapGlyph<'_>,
1217    bitmaps: &BitmapStrikes<'_>,
1218) -> Affine {
1219    let x_scale_factor = font_size / bitmap_glyph.ppem_x;
1220    let y_scale_factor = font_size / bitmap_glyph.ppem_y;
1221    let font_units_to_size = font_size / upem;
1222
1223    // CoreText appears to special case Apple Color Emoji, adding
1224    // a 100 font unit vertical offset. We do the same but only
1225    // when both vertical offsets are 0 to avoid incorrect
1226    // rendering if Apple ever does encode the offset directly in
1227    // the font.
1228    let bearing_y = if bitmap_glyph.bearing_y == 0.0 && bitmaps.format() == Some(BitmapFormat::Sbix)
1229    {
1230        100.0
1231    } else {
1232        bitmap_glyph.bearing_y
1233    };
1234
1235    let origin_shift = match bitmap_glyph.placement_origin {
1236        Origin::TopLeft => Vec2::default(),
1237        Origin::BottomLeft => Vec2 {
1238            x: 0.,
1239            y: -f64::from(pixmap.height()),
1240        },
1241    };
1242
1243    draw_props
1244        .positioned_transform(glyph)
1245        // Apply outer bearings.
1246        .pre_translate(Vec2 {
1247            x: (-bitmap_glyph.bearing_x * font_units_to_size).into(),
1248            y: (bearing_y * font_units_to_size).into(),
1249        })
1250        // Scale to pixel-space.
1251        .pre_scale_non_uniform(f64::from(x_scale_factor), f64::from(y_scale_factor))
1252        // Apply inner bearings.
1253        .pre_translate(Vec2 {
1254            x: (-bitmap_glyph.inner_bearing_x).into(),
1255            y: (-bitmap_glyph.inner_bearing_y).into(),
1256        })
1257        .pre_translate(origin_shift)
1258}
1259
1260/// Helper struct containing computed COLR glyph metrics.
1261struct ColrMetrics {
1262    /// Base transform with glyph position applied.
1263    transform: Affine,
1264    /// Scaled bounding box in device coordinates.
1265    scaled_bbox: Rect,
1266    /// Scale factor for x-axis.
1267    scale_factor_x: f64,
1268    /// Scale factor for y-axis.
1269    scale_factor_y: f64,
1270    /// Font size scale (`font_size` / `upem`).
1271    font_size_scale: f64,
1272    has_non_default_blend: bool,
1273}
1274
1275/// Calculate COLR glyph metrics (scale factors, bounding box, etc.).
1276///
1277/// This computes the intermediate values needed for both creating the `GlyphColr`
1278/// and calculating its positioning transform.
1279fn calculate_colr_metrics<'a>(
1280    font_size: f32,
1281    draw_props: DrawProps,
1282    glyph: Glyph,
1283    font_ref: &'a FontRef<'a>,
1284    color_glyph: &skrifa::color::ColorGlyph<'a>,
1285    location: LocationRef<'a>,
1286    outline_cache: &mut OutlineCacheSession<'_>,
1287    font_info: FontInfo,
1288) -> ColrMetrics {
1289    // The scale factor we need to apply to scale from font units to our font size.
1290    let font_size_scale = (font_size / font_info.upem) as f64;
1291    let transform = draw_props.positioned_transform(glyph);
1292
1293    // Estimate the size of the intermediate pixmap. Ideally, the intermediate bitmap should have
1294    // exactly one pixel (or more) per device pixel, to ensure that no quality is lost. Therefore,
1295    // we simply use the scaling/skewing factor to calculate how much to scale each axis by.
1296    let (scale_factor_x, scale_factor_y) = {
1297        let (x_vec, y_vec) = x_y_advances(&transform.pre_scale(font_size_scale));
1298        (x_vec.length(), y_vec.length())
1299    };
1300
1301    // TODO: Cache this across frames.
1302    let colr_info = get_colr_info(font_ref, color_glyph, location, outline_cache, font_info);
1303    let bbox = color_glyph
1304        // First try to get the clip bbox from the COLR table,
1305        // as this one has the highest priority.
1306        .bounding_box(location, Size::unscaled())
1307        .map(convert_bounding_box)
1308        // Otherwise, we use the conservative bounding box we determined before.
1309        .or(colr_info.bbox)
1310        .unwrap_or(Rect::ZERO);
1311
1312    // Calculate the position of the rectangle that will contain the rendered pixmap in device
1313    // coordinates.
1314    let scaled_bbox = Rect {
1315        x0: bbox.x0 * scale_factor_x,
1316        y0: bbox.y0 * scale_factor_y,
1317        x1: bbox.x1 * scale_factor_x,
1318        y1: bbox.y1 * scale_factor_y,
1319    };
1320
1321    ColrMetrics {
1322        transform,
1323        scaled_bbox,
1324        scale_factor_x,
1325        scale_factor_y,
1326        font_size_scale,
1327        has_non_default_blend: colr_info.has_non_default_blend,
1328    }
1329}
1330
1331/// Calculate transform for COLR glyphs.
1332///
1333/// This uses pre-calculated metrics to compute the final positioning transform for a COLR glyph,
1334/// taking into account:
1335/// - Y-axis flip (fonts use upside-down coordinate system)
1336/// - Scale compensation (to avoid double-application of run transform scale)
1337/// - Bounding box alignment
1338fn calculate_colr_transform(metrics: &ColrMetrics) -> Affine {
1339    metrics.transform
1340        // There are two things going on here:
1341        // - On the one hand, for images, the position (0, 0) will be at the top-left, while
1342        //   for images, the position will be at the bottom-left.
1343        // - COLR glyphs have a flipped y-axis, so in the intermediate image they will be
1344        //   upside down.
1345        // Because of both of these, all we simply need to do is to flip the image on the y-axis.
1346        // This will ensure that the glyph in the image isn't upside down anymore, and at the same
1347        // time also flips from having the origin in the top-left to having the origin in the
1348        // bottom-right.
1349        * Affine::scale_non_uniform(1.0, -1.0)
1350        // Overall, the whole pixmap is scaled by `scale_factor_x` and `scale_factor_y`. `scale_factor_x`
1351        // and `scale_factor_y` are composed by the scale necessary to adjust for the glyph size,
1352        // as well as the scale that has been applied to the whole glyph run. However, the scale
1353        // of the whole glyph run will be applied later on in the render context. If
1354        // we didn't do anything, the scales would be applied twice (see https://github.com/linebender/vello/pull/1370).
1355        // Therefore, we apply another scale factor that unapplies the effect of the glyph run transform
1356        // and only retains the transform necessary to account for the size of the glyph.
1357        * Affine::scale_non_uniform(
1358            metrics.font_size_scale / metrics.scale_factor_x,
1359            metrics.font_size_scale / metrics.scale_factor_y,
1360        )
1361        // Shift the pixmap back so that the bbox aligns with the original position
1362        // of where the glyph should be placed.
1363        * Affine::translate((metrics.scaled_bbox.x0, metrics.scaled_bbox.y0))
1364}
1365
1366/// Create COLR glyph data with intermediate texture parameters.
1367///
1368/// This uses pre-calculated metrics to create a `GlyphType::Colr` with all necessary
1369/// data for rendering to an intermediate texture.
1370fn create_colr_glyph<'a>(
1371    font_ref: &'a FontRef<'a>,
1372    metrics: &ColrMetrics,
1373    color_glyph: skrifa::color::ColorGlyph<'a>,
1374    normalized_coords: &'a [skrifa::instance::NormalizedCoord],
1375    font_info: FontInfo,
1376) -> GlyphType<'a> {
1377    let (pix_width, pix_height) = (
1378        metrics.scaled_bbox.width().ceil() as u16,
1379        metrics.scaled_bbox.height().ceil() as u16,
1380    );
1381
1382    let draw_transform =
1383        // Shift everything so that the bbox starts at (0, 0) and the whole visible area of
1384        // the glyph will be contained in the intermediate pixmap.
1385        Affine::translate((-metrics.scaled_bbox.x0, -metrics.scaled_bbox.y0)) *
1386        // Scale down to the actual size that the COLR glyph will have in device units.
1387        Affine::scale_non_uniform(metrics.scale_factor_x, metrics.scale_factor_y);
1388
1389    // The shift-back happens in `glyph_transform`, so here we can assume (0.0, 0.0) as the origin
1390    // of the area we want to draw to.
1391    let area = Rect::new(
1392        0.0,
1393        0.0,
1394        metrics.scaled_bbox.width(),
1395        metrics.scaled_bbox.height(),
1396    );
1397
1398    let location = LocationRef::new(normalized_coords);
1399
1400    GlyphType::Colr(Box::new(GlyphColr {
1401        skrifa_glyph: color_glyph,
1402        font_ref,
1403        location,
1404        area,
1405        pix_width,
1406        pix_height,
1407        draw_transform,
1408        has_non_default_blend: metrics.has_non_default_blend,
1409        font_info,
1410    }))
1411}
1412
1413trait FontDataExt {
1414    fn as_skrifa(&self) -> FontRef<'_>;
1415}
1416
1417impl FontDataExt for FontData {
1418    fn as_skrifa(&self) -> FontRef<'_> {
1419        FontRef::from_index(self.data.data(), self.index).unwrap()
1420    }
1421}
1422
1423/// Rendering style for glyphs.
1424#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1425pub(crate) enum Style {
1426    /// Fill the glyph.
1427    Fill,
1428    /// Stroke the glyph.
1429    Stroke,
1430}
1431
1432/// A sequence of glyphs with shared rendering properties.
1433#[derive(Clone, Debug)]
1434pub struct GlyphRun<'a> {
1435    /// Font for all glyphs in the run.
1436    font: FontData,
1437    /// Size of the font in pixels per em.
1438    font_size: f32,
1439    /// Synthetic embolden settings.
1440    font_embolden: FontEmbolden,
1441    /// Global transform.
1442    transform: Affine,
1443    /// Paint transform for the glyph run in scene space.
1444    scene_paint_transform: Affine,
1445    /// Per-glyph transform. Use [`Affine::skew`] with horizontal-skew only to simulate italic
1446    /// text.
1447    glyph_transform: Option<Affine>,
1448    /// Normalized variation coordinates for variable fonts.
1449    normalized_coords: &'a [skrifa::instance::NormalizedCoord],
1450    /// Controls whether font hinting is enabled.
1451    hint: bool,
1452}
1453
1454struct PreparedGlyphRun<'a> {
1455    /// The underlying font data.
1456    font: FontData,
1457    /// Basic metadata about the underlying font.
1458    font_info: FontInfo,
1459    // The fact that we store `run_size` and `glyph_transform` here, as well
1460    // as having more transforms and an effective font size inside of the `draw_props` field is pretty
1461    // confusing, so here is a brief explanation:
1462    // Basically, the reason why we need both `run_size` and `glyph_transform` here is that
1463    // we need to store some of the original metadata in scene space for certain functionality
1464    // (for example handling of underlines).
1465    /// The original run size supplied by the caller.
1466    run_size: f32,
1467    /// Synthetic embolden settings.
1468    font_embolden: FontEmbolden,
1469    /// The original per-glyph transform supplied by the caller.
1470    glyph_transform: Option<Affine>,
1471    // Continuing the above comment, the problem is that we also need to precalculate data
1472    // that is needed specifically for glyph rendering. This includes:
1473    // 1) We need to concatenate run transform and glyph transform to compute the final transform
1474    // for the glyph outline.
1475    // 2) Whenever possible, we need to try to _absorb_ the font size into the draw transform,
1476    // such that we can just use the font size to uniquely identify a glyph cache hit (for example,
1477    // if we draw a glyph at font size 12 with scale 2, it's the same as drawing the glyph at font size 24).
1478    // While it would make things easier to just use the cache key in the transform and accept less
1479    // caching potential for easier code, we would still need scaling absorption to implement proper
1480    // hinting. Hence, it makes sense to just generalize the whole absorption procedure.
1481    // In any case, since we do scaling absorption, we cannot use `run_size`, `GlyphRun::transform` and
1482    // `glyph_transform` for glyph drawing purposes anymore. In particular, it can easily happen
1483    // that
1484    // 1) `run_size` != `draw_props.font_size`
1485    // 2) `run_transform` * `glyph_transform` != `draw_props.effective_transform`.
1486    // Therefore, we need to track a separate set of fields for glyph-drawing operations.
1487    /// Properties for turning glyph-local positions into final draw transforms.
1488    draw_props: DrawProps,
1489    /// The original transform for the paint in scene space.
1490    scene_paint_transform: Affine,
1491    normalized_coords: &'a [skrifa::instance::NormalizedCoord],
1492    hinting_instance: Option<&'a HintingInstance>,
1493}
1494
1495/// Properties for easily calculating the transform of a positioned glyph.
1496#[derive(Clone, Copy, Debug)]
1497struct DrawProps {
1498    // Why do we need two separate transforms? Fundamentally, the problem is that the order
1499    // of application should be:
1500    // `run_transform` * `glyph_position` * `font_size` * `glyph_transform`.
1501    // As part of absorption, we are only left with a potentially new `font_size` and a merged
1502    // `effective_transform`. However, the translation that results form `glyph_position` logically
1503    // needs to be applied after `run_transform` but before `glyph_transform`.
1504    // Therefore, we need to store two separate transforms: One that is used only to transform
1505    // the original glyph position, and another one that is used to actually transform the glyph
1506    // outlines.
1507    /// A positioning transform for the glyph.
1508    positioning_transform: Affine,
1509    /// A transform to apply to the glyph after positioning.
1510    effective_transform: Affine,
1511    /// The actual font size that should be assumed for drawing and caching
1512    /// purposes.
1513    font_size: f32,
1514}
1515
1516impl DrawProps {
1517    #[inline]
1518    fn positioned_transform(self, glyph: Glyph) -> Affine {
1519        // First, determine the "coarse" location of the glyph by applying the scaling/skewing
1520        // of the original run transform to the glyph position. Note that `positioning_transform`
1521        // has a translation factor of zero (since it has been absorbed into `effective_transform`), so
1522        // only the skewing and scaling factors are relevant.
1523        let translation = self.positioning_transform * Point::new(glyph.x as f64, glyph.y as f64);
1524
1525        // Now, apply the final draw transform on top of that, which will also consider
1526        // the original glyph transform.
1527        Affine::translate(translation.to_vec2()) * self.effective_transform
1528    }
1529}
1530
1531impl Debug for PreparedGlyphRun<'_> {
1532    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
1533        // HintingInstance doesn't implement Debug so we have to do this manually :(
1534        f.debug_struct("PreparedGlyphRun")
1535            .field("font", &self.font)
1536            .field("font_info", &self.font_info)
1537            .field("run_size", &self.run_size)
1538            .field("font_embolden", &self.font_embolden)
1539            .field("glyph_transform", &self.glyph_transform)
1540            .field("transforms", &self.draw_props)
1541            .field("normalized_coords", &self.normalized_coords)
1542            .finish()
1543    }
1544}
1545
1546/// Prepare a glyph run for rendering.
1547fn prepare_glyph_run<'a>(run: GlyphRun<'a>, hint_cache: &'a mut HintCache) -> PreparedGlyphRun<'a> {
1548    let full_transform = run.transform * run.glyph_transform.unwrap_or(Affine::IDENTITY);
1549    let [_, _, t_c, t_d, t_e, t_f] = full_transform.as_coeffs();
1550
1551    /// The mode that should be used to handle transforms.
1552    #[derive(Clone, Copy, Debug)]
1553    enum PreparedGlyphRunMode {
1554        /// No absorption has happened, the font size stays the same and the effective transform
1555        /// is simply the concatenation of run transform and glyph transform.
1556        ///
1557        /// No hinting should be applied.
1558        Direct,
1559        /// The scaling factor has been absorbed, and hinting should be applied.
1560        AbsorbScaleUnhinted,
1561        /// The scaling factor has been absorbed, but not hinting should be applied.
1562        AbsorbScaleHinted,
1563    }
1564
1565    let mode = if !run.hint {
1566        // TODO: We could explore generalizing this by decomposing the transform, such that
1567        // we always absorb it, even if there is a skewing factor in the transform. This won't
1568        // automatically make them eligible for caching because any skewing factor is currently
1569        // rejected for caching, but it might make the code a bit more consistent.
1570        if full_transform.is_positive_uniform_scale_without_skew() {
1571            PreparedGlyphRunMode::AbsorbScaleUnhinted
1572        } else {
1573            PreparedGlyphRunMode::Direct
1574        }
1575    } else {
1576        // We perform vertical-only hinting.
1577        //
1578        // Hinting doesn't make sense if we later scale the glyphs via some transform. So, similarly to
1579        // normal glyph runs, we try to extract the scale. As is currently done for unhinted glyph runs, we
1580        // also expect the scale to be uniform: Simply using the vertical scale as font
1581        // size and then transforming by the relative horizontal scale can cause, e.g., overlapping
1582        // glyphs. Note that this extracted scale should be later applied to the glyph's position.
1583        //
1584        // As the hinting is vertical-only, we can handle horizontal skew, but not vertical skew or
1585        // rotations.
1586        if full_transform.is_positive_uniform_scale_without_vertical_skew() {
1587            PreparedGlyphRunMode::AbsorbScaleHinted
1588        } else {
1589            PreparedGlyphRunMode::Direct
1590        }
1591    };
1592
1593    let (effective_transform, draw_font_size, hinting_instance) = match mode {
1594        PreparedGlyphRunMode::Direct => (full_transform, run.font_size, None),
1595        PreparedGlyphRunMode::AbsorbScaleUnhinted => (
1596            Affine::new([1., 0., 0., 1., t_e, t_f]),
1597            run.font_size * t_d as f32,
1598            None,
1599        ),
1600        PreparedGlyphRunMode::AbsorbScaleHinted => {
1601            let vertical_font_size = run.font_size * t_d as f32;
1602            let font_ref = run.font.as_skrifa();
1603            let outlines = font_ref.outline_glyphs();
1604            let hinting_instance = hint_cache.get(&HintKey {
1605                font_id: run.font.data.id(),
1606                font_index: run.font.index,
1607                outlines: &outlines,
1608                size: vertical_font_size,
1609                coords: run.normalized_coords,
1610            });
1611
1612            (
1613                // The scale has been absorbed into the font size, so we need to remove it from the skew
1614                // coefficient (t_c) as well. Otherwise the skew would be applied twice: once via the
1615                // larger outline, once via the transform. The translation (t_e, t_f) stays as-is since
1616                // it positions the run in scene coordinates.
1617                Affine::new([1., 0., t_c / t_d, 1., t_e, t_f]),
1618                vertical_font_size,
1619                hinting_instance,
1620            )
1621        }
1622    };
1623
1624    let upem = run
1625        .font
1626        .as_skrifa()
1627        .head()
1628        .map(|h| h.units_per_em())
1629        .unwrap()
1630        .into();
1631    let font_info = FontInfo {
1632        id: run.font.data.id(),
1633        index: run.font.index,
1634        upem,
1635    };
1636
1637    PreparedGlyphRun {
1638        font: run.font,
1639        font_info,
1640        run_size: run.font_size,
1641        font_embolden: run.font_embolden,
1642        glyph_transform: run.glyph_transform,
1643        draw_props: DrawProps {
1644            positioning_transform: run
1645                .transform
1646                // Translation factor is already considered in `effective_transform`, so we need to remove
1647                // it here.
1648                .with_translation(Vec2::ZERO),
1649            effective_transform,
1650            font_size: draw_font_size,
1651        },
1652        scene_paint_transform: run.scene_paint_transform,
1653        normalized_coords: run.normalized_coords,
1654        hinting_instance,
1655    }
1656}
1657
1658// TODO: Although these are sane defaults, we might want to make them
1659// configurable.
1660const HINTING_OPTIONS: HintingOptions = HintingOptions {
1661    engine: skrifa::outline::Engine::AutoFallback,
1662    target: skrifa::outline::Target::Smooth {
1663        mode: skrifa::outline::SmoothMode::Lcd,
1664        symmetric_rendering: false,
1665        preserve_linear_metrics: true,
1666    },
1667};
1668
1669#[derive(Clone, Default)]
1670pub(crate) struct OutlinePath {
1671    pub(crate) path: BezPath,
1672}
1673
1674impl OutlinePath {
1675    pub(crate) fn new() -> Self {
1676        Self {
1677            path: BezPath::new(),
1678        }
1679    }
1680
1681    pub(crate) fn reuse(&mut self) {
1682        self.path.truncate(0);
1683    }
1684}
1685
1686// Note that we flip the y-axis to match our coordinate system.
1687impl OutlinePen for OutlinePath {
1688    #[inline]
1689    fn move_to(&mut self, x: f32, y: f32) {
1690        self.path.move_to((x, y));
1691    }
1692
1693    #[inline]
1694    fn line_to(&mut self, x: f32, y: f32) {
1695        self.path.line_to((x, y));
1696    }
1697
1698    #[inline]
1699    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
1700        self.path.curve_to((cx0, cy0), (cx1, cy1), (x, y));
1701    }
1702
1703    #[inline]
1704    fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
1705        self.path.quad_to((cx, cy), (x, y));
1706    }
1707
1708    #[inline]
1709    fn close(&mut self) {
1710        self.path.close_path();
1711    }
1712}
1713
1714/// A normalized variation coordinate (for variable fonts) in 2.14 fixed point format.
1715///
1716/// In most cases, this can be [cast](bytemuck::cast_slice) from the
1717/// normalised coords provided by your text layout library.
1718///
1719/// Equivalent to [`skrifa::instance::NormalizedCoord`], but defined
1720/// in Glifo so that Skrifa is not part of Glifo's public API.
1721/// This allows Glifo to update its Skrifa in a patch release, and limits
1722/// the need for updates only to align Skrifa versions.
1723pub type NormalizedCoord = i16;
1724
1725/// Caches used for glyph rendering.
1726///
1727/// Contains renderer-agnostic caches (outline paths, hinting instances)
1728/// alongside the glyph atlas bitmap cache.
1729// TODO: Consider capturing cache performance metrics like hit rate, etc.
1730#[derive(Debug, Default)]
1731pub struct GlyphCaches {
1732    /// Caches glyph outlines (paths) for reuse.
1733    pub(crate) outline_cache: OutlineCache,
1734    /// Caches hinting instances for reuse.
1735    pub(crate) hinting_cache: HintCache,
1736    /// Horizontal spans excluded from "ink-skipping" underlines. Cached to reuse one allocation.
1737    pub(crate) underline_exclusions: Vec<(f64, f64)>,
1738    /// Caches rasterized glyph bitmaps in atlas pages.
1739    pub(crate) glyph_atlas: GlyphAtlas,
1740}
1741
1742impl GlyphCaches {
1743    /// Clears the glyph caches.
1744    pub fn clear(&mut self) {
1745        self.outline_cache.clear();
1746        self.hinting_cache.clear();
1747        self.underline_exclusions.clear();
1748        self.glyph_atlas.clear();
1749    }
1750
1751    /// Maintains the glyph caches by evicting unused cache entries.
1752    ///
1753    /// The `image_cache` must be the same allocator passed to
1754    /// `GlyphRunBuilder::build` so that evicted entries are deallocated from
1755    /// the correct allocator.
1756    ///
1757    /// Should be called once per scene rendering.
1758    pub fn maintain(&mut self, image_cache: &mut ImageCache) {
1759        self.outline_cache.maintain();
1760        self.glyph_atlas.maintain(image_cache);
1761    }
1762}
1763
1764#[derive(Copy, Clone, PartialEq, Eq, Hash, Default, Debug)]
1765struct OutlineKey {
1766    font_id: u64,
1767    font_index: u32,
1768    glyph_id: u32,
1769    size_bits: u32,
1770    embolden_x_bits: u32,
1771    embolden_y_bits: u32,
1772    embolden_join_bits: u8,
1773    embolden_miter_limit_bits: u32,
1774    embolden_tolerance_bits: u32,
1775    hint: bool,
1776}
1777
1778#[inline(always)]
1779fn join_bits(join: Join) -> u8 {
1780    match join {
1781        Join::Bevel => 0,
1782        Join::Miter => 1,
1783        Join::Round => 2,
1784    }
1785}
1786
1787#[expect(
1788    clippy::cast_possible_truncation,
1789    reason = "Cache keys intentionally store embolden parameters at f32 precision."
1790)]
1791#[inline(always)]
1792fn f32_bits(value: f64) -> u32 {
1793    (value as f32).to_bits()
1794}
1795
1796struct OutlineEntry {
1797    path: Arc<BezPath>,
1798    bbox: Rect,
1799    serial: u32,
1800}
1801
1802impl OutlineEntry {
1803    fn new(path: Arc<BezPath>, bbox: Rect, serial: u32) -> Self {
1804        Self { path, bbox, serial }
1805    }
1806
1807    /// Takes the inner `BezPath` out of this entry if the `Arc` is uniquely owned.
1808    fn take_path(&mut self) -> Option<OutlinePath> {
1809        let arc = core::mem::replace(&mut self.path, Arc::new(BezPath::new()));
1810        Arc::try_unwrap(arc).ok().map(|path| OutlinePath { path })
1811    }
1812}
1813
1814/// A cached outline glyph path with its precise bounding box.
1815pub(crate) struct CachedOutline<'a> {
1816    pub(crate) path: &'a Arc<BezPath>,
1817    pub(crate) bbox: Rect,
1818}
1819
1820/// Caches glyph outlines for reuse.
1821/// Heavily inspired by `vello_encoding::glyph_cache`.
1822#[derive(Default)]
1823pub struct OutlineCache {
1824    free_list: Vec<OutlinePath>,
1825    static_map: HashMap<OutlineKey, OutlineEntry>,
1826    variable_map: HashMap<VarKey, HashMap<OutlineKey, OutlineEntry>>,
1827    cached_count: usize,
1828    serial: u32,
1829    last_prune_serial: u32,
1830}
1831
1832impl Debug for OutlineCache {
1833    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
1834        f.debug_struct("OutlineCache")
1835            .field("free_list", &self.free_list.len())
1836            .field("static_map", &self.static_map.len())
1837            .field("variable_map", &self.variable_map.len())
1838            .field("cached_count", &self.cached_count)
1839            .field("serial", &self.serial)
1840            .field("last_prune_serial", &self.last_prune_serial)
1841            .finish()
1842    }
1843}
1844
1845impl OutlineCache {
1846    /// Maintains the outline cache by evicting unused cache entries.
1847    ///
1848    /// Should be called once per scene rendering.
1849    pub fn maintain(&mut self) {
1850        // Maximum number of full renders where we'll retain an unused glyph
1851        const MAX_ENTRY_AGE: u32 = 64;
1852        // Maximum number of full renders before we force a prune
1853        const PRUNE_FREQUENCY: u32 = 64;
1854        // Always prune if the cached count is greater than this value
1855        const CACHED_COUNT_THRESHOLD: usize = 256;
1856        // Number of encoding buffers we'll keep on the free list
1857        const MAX_FREE_LIST_SIZE: usize = 128;
1858
1859        let free_list = &mut self.free_list;
1860        let serial = self.serial;
1861        self.serial += 1;
1862        // Don't iterate over the whole cache every frame
1863        if serial - self.last_prune_serial < PRUNE_FREQUENCY
1864            && self.cached_count < CACHED_COUNT_THRESHOLD
1865        {
1866            return;
1867        }
1868        self.last_prune_serial = serial;
1869        self.static_map.retain(|_, entry| {
1870            if serial - entry.serial > MAX_ENTRY_AGE {
1871                if free_list.len() < MAX_FREE_LIST_SIZE {
1872                    // Try to recover the inner BezPath for reuse as a drawing buffer.
1873                    // This succeeds when the Arc has no other owners (refcount == 1).
1874                    if let Some(path) = entry.take_path() {
1875                        free_list.push(path);
1876                    }
1877                }
1878                self.cached_count -= 1;
1879                false
1880            } else {
1881                true
1882            }
1883        });
1884        self.variable_map.retain(|_, map| {
1885            map.retain(|_, entry| {
1886                if serial - entry.serial > MAX_ENTRY_AGE {
1887                    if free_list.len() < MAX_FREE_LIST_SIZE
1888                        && let Some(path) = entry.take_path()
1889                    {
1890                        free_list.push(path);
1891                    }
1892                    self.cached_count -= 1;
1893                    false
1894                } else {
1895                    true
1896                }
1897            });
1898            !map.is_empty()
1899        });
1900    }
1901
1902    /// Clears the outline cache.
1903    pub fn clear(&mut self) {
1904        self.free_list.clear();
1905        self.static_map.clear();
1906        self.variable_map.clear();
1907        self.cached_count = 0;
1908        self.serial = 0;
1909        self.last_prune_serial = 0;
1910    }
1911}
1912
1913pub(crate) struct OutlineCacheSession<'a> {
1914    map: &'a mut HashMap<OutlineKey, OutlineEntry>,
1915    free_list: &'a mut Vec<OutlinePath>,
1916    serial: u32,
1917    cached_count: &'a mut usize,
1918}
1919
1920impl<'a> OutlineCacheSession<'a> {
1921    fn new(outline_cache: &'a mut OutlineCache, var_key: VarLookupKey<'_>) -> Self {
1922        let map = if var_key.coords().is_empty() {
1923            &mut outline_cache.static_map
1924        } else {
1925            match outline_cache
1926                .variable_map
1927                .raw_entry_mut()
1928                .from_key(&var_key)
1929            {
1930                RawEntryMut::Occupied(entry) => entry.into_mut(),
1931                RawEntryMut::Vacant(entry) => entry.insert(var_key.into(), HashMap::new()).1,
1932            }
1933        };
1934        Self {
1935            map,
1936            free_list: &mut outline_cache.free_list,
1937            serial: outline_cache.serial,
1938            cached_count: &mut outline_cache.cached_count,
1939        }
1940    }
1941
1942    pub(crate) fn get_or_insert(
1943        &mut self,
1944        glyph_id: u32,
1945        font_info: FontInfo,
1946        size: f32,
1947        embolden: FontEmbolden,
1948        var_key: VarLookupKey<'_>,
1949        outline_glyph: &skrifa::outline::OutlineGlyph<'_>,
1950        hinting_instance: Option<&HintingInstance>,
1951    ) -> CachedOutline<'_> {
1952        let key = OutlineKey {
1953            glyph_id,
1954            font_id: font_info.id,
1955            font_index: font_info.index,
1956            size_bits: size.to_bits(),
1957            embolden_x_bits: f32_bits(embolden.amount.xx),
1958            embolden_y_bits: f32_bits(embolden.amount.yy),
1959            embolden_join_bits: join_bits(embolden.join),
1960            embolden_miter_limit_bits: f32_bits(embolden.miter_limit),
1961            embolden_tolerance_bits: f32_bits(embolden.tolerance),
1962            hint: hinting_instance.is_some(),
1963        };
1964
1965        match self.map.entry(key) {
1966            Entry::Occupied(mut entry) => {
1967                entry.get_mut().serial = self.serial;
1968                let entry = entry.into_mut();
1969                CachedOutline {
1970                    path: &entry.path,
1971                    bbox: entry.bbox,
1972                }
1973            }
1974            Entry::Vacant(entry) => {
1975                // Pop a drawing buffer from the free list (or create a new one).
1976                let mut drawing_buf = self.free_list.pop().unwrap_or_default();
1977
1978                let draw_settings = if let Some(hinting_instance) = hinting_instance {
1979                    DrawSettings::hinted(hinting_instance, false)
1980                } else {
1981                    DrawSettings::unhinted(Size::new(size), var_key.coords())
1982                };
1983
1984                drawing_buf.reuse();
1985                outline_glyph.draw(draw_settings, &mut drawing_buf).unwrap();
1986                if embolden.amount != Diagonal2::new(0.0, 0.0) {
1987                    drawing_buf.path = kurbo::expand_path(
1988                        &drawing_buf.path,
1989                        embolden.amount,
1990                        embolden.join,
1991                        embolden.miter_limit,
1992                        embolden.tolerance,
1993                    );
1994                }
1995
1996                let bbox = drawing_buf.path.bounding_box();
1997                let entry = entry.insert(OutlineEntry::new(
1998                    Arc::new(drawing_buf.path),
1999                    bbox,
2000                    self.serial,
2001                ));
2002                *self.cached_count += 1;
2003                CachedOutline {
2004                    path: &entry.path,
2005                    bbox: entry.bbox,
2006                }
2007            }
2008        }
2009    }
2010}
2011
2012/// Key for variable font caches.
2013type VarKey = SmallVec<[skrifa::instance::NormalizedCoord; 4]>;
2014
2015/// Lookup key for variable font caches.
2016#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
2017pub(crate) struct VarLookupKey<'a>(&'a [skrifa::instance::NormalizedCoord]);
2018
2019impl<'a> VarLookupKey<'a> {
2020    pub(crate) fn new(coords: &'a [skrifa::instance::NormalizedCoord]) -> Self {
2021        Self(coords)
2022    }
2023
2024    fn coords(self) -> &'a [skrifa::instance::NormalizedCoord] {
2025        self.0
2026    }
2027}
2028
2029impl Equivalent<VarKey> for VarLookupKey<'_> {
2030    fn equivalent(&self, other: &VarKey) -> bool {
2031        self.0 == other.as_slice()
2032    }
2033}
2034
2035impl From<VarLookupKey<'_>> for VarKey {
2036    fn from(key: VarLookupKey<'_>) -> Self {
2037        Self::from_slice(key.0)
2038    }
2039}
2040
2041/// We keep this small to enable a simple LRU cache with a linear
2042/// search. Regenerating hinting data is low to medium cost so it's fine
2043/// to redo it occasionally.
2044const MAX_CACHED_HINT_INSTANCES: usize = 16;
2045
2046/// Hint key for hinting instances.
2047#[derive(Debug)]
2048pub struct HintKey<'a> {
2049    font_id: u64,
2050    font_index: u32,
2051    outlines: &'a OutlineGlyphCollection<'a>,
2052    size: f32,
2053    coords: &'a [skrifa::instance::NormalizedCoord],
2054}
2055
2056impl HintKey<'_> {
2057    fn instance(&self) -> Option<HintingInstance> {
2058        HintingInstance::new(
2059            self.outlines,
2060            Size::new(self.size),
2061            self.coords,
2062            HINTING_OPTIONS,
2063        )
2064        .ok()
2065    }
2066}
2067
2068/// LRU cache for hinting instances.
2069///
2070/// Heavily inspired by `vello_encoding::glyph_cache`.
2071#[derive(Default)]
2072pub struct HintCache {
2073    // Split caches for glyf/cff because the instance type can reuse
2074    // internal memory when reconfigured for the same format.
2075    glyf_entries: Vec<HintEntry>,
2076    cff_entries: Vec<HintEntry>,
2077    varc_entries: Vec<HintEntry>,
2078    serial: u64,
2079}
2080
2081impl Debug for HintCache {
2082    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
2083        f.debug_struct("HintCache")
2084            .field("glyf_entries", &self.glyf_entries.len())
2085            .field("cff_entries", &self.cff_entries.len())
2086            .field("varc_entries", &self.varc_entries.len())
2087            .field("serial", &self.serial)
2088            .finish()
2089    }
2090}
2091
2092impl HintCache {
2093    /// Gets a hinting instance for the given key.
2094    pub fn get(&mut self, key: &HintKey<'_>) -> Option<&HintingInstance> {
2095        let entries = match key.outlines.format()? {
2096            OutlineGlyphFormat::Glyf => &mut self.glyf_entries,
2097            OutlineGlyphFormat::Cff | OutlineGlyphFormat::Cff2 => &mut self.cff_entries,
2098            OutlineGlyphFormat::Varc => &mut self.varc_entries,
2099        };
2100        let (entry_ix, is_current) = find_hint_entry(entries, key)?;
2101        let entry = entries.get_mut(entry_ix)?;
2102        self.serial += 1;
2103        entry.serial = self.serial;
2104        if !is_current {
2105            entry.font_id = key.font_id;
2106            entry.font_index = key.font_index;
2107            entry
2108                .instance
2109                .reconfigure(
2110                    key.outlines,
2111                    Size::new(key.size),
2112                    key.coords,
2113                    HINTING_OPTIONS,
2114                )
2115                .ok()?;
2116        }
2117        Some(&entry.instance)
2118    }
2119
2120    /// Clears the hint cache.
2121    pub fn clear(&mut self) {
2122        self.glyf_entries.clear();
2123        self.cff_entries.clear();
2124        self.varc_entries.clear();
2125        self.serial = 0;
2126    }
2127}
2128
2129struct HintEntry {
2130    font_id: u64,
2131    font_index: u32,
2132    instance: HintingInstance,
2133    serial: u64,
2134}
2135
2136fn find_hint_entry(entries: &mut Vec<HintEntry>, key: &HintKey<'_>) -> Option<(usize, bool)> {
2137    let mut found_serial = u64::MAX;
2138    let mut found_index = 0;
2139    for (ix, entry) in entries.iter().enumerate() {
2140        if entry.font_id == key.font_id
2141            && entry.font_index == key.font_index
2142            && entry.instance.size() == Size::new(key.size)
2143            && entry.instance.location().coords() == key.coords
2144        {
2145            return Some((ix, true));
2146        }
2147        if entry.serial < found_serial {
2148            found_serial = entry.serial;
2149            found_index = ix;
2150        }
2151    }
2152    if entries.len() < MAX_CACHED_HINT_INSTANCES {
2153        let instance = key.instance()?;
2154        let ix = entries.len();
2155        entries.push(HintEntry {
2156            font_id: key.font_id,
2157            font_index: key.font_index,
2158            instance,
2159            // This should be updated by the caller.
2160            serial: 0,
2161        });
2162        Some((ix, true))
2163    } else {
2164        Some((found_index, false))
2165    }
2166}
2167
2168fn x_y_advances(transform: &Affine) -> (Vec2, Vec2) {
2169    let scale_skew_transform = {
2170        let c = transform.as_coeffs();
2171        Affine::new([c[0], c[1], c[2], c[3], 0.0, 0.0])
2172    };
2173
2174    let x_advance = scale_skew_transform * Point::new(1.0, 0.0);
2175    let y_advance = scale_skew_transform * Point::new(0.0, 1.0);
2176
2177    (
2178        Vec2::new(x_advance.x, x_advance.y),
2179        Vec2::new(y_advance.x, y_advance.y),
2180    )
2181}
2182
2183#[cfg(test)]
2184mod tests {
2185    use super::*;
2186    use crate::atlas::{AtlasConfig, AtlasPaint};
2187    use crate::interface::{DrawSink, GlyphRenderer};
2188    use crate::peniko::BlendMode;
2189    use crate::peniko::Blob;
2190    use crate::peniko::color::{AlphaColor, Srgb};
2191    use alloc::sync::Arc;
2192    use vello_common::paint::{Image, ImageId, ImageSource, PaintType, Tint};
2193
2194    const _NORMALISED_COORD_SIZE_MATCHES: () =
2195        assert!(size_of::<skrifa::instance::NormalizedCoord>() == size_of::<NormalizedCoord>());
2196
2197    const ROBOTO_FONT: &[u8] = include_bytes!("../../examples/assets/roboto/Roboto-Regular.ttf");
2198    const NOTO_COLR_FONT: &[u8] =
2199        include_bytes!("../../examples/assets/noto_color_emoji/NotoColorEmoji-Subset.ttf");
2200    #[cfg(feature = "png")]
2201    const NOTO_CBTF_FONT: &[u8] =
2202        include_bytes!("../../examples/assets/noto_color_emoji/NotoColorEmoji-CBTF-Subset.ttf");
2203
2204    #[derive(Clone, Copy)]
2205    enum TestGlyphKind {
2206        Outline,
2207        Colr,
2208        #[cfg(feature = "png")]
2209        Bitmap,
2210    }
2211
2212    #[derive(Default)]
2213    struct NoopRenderer;
2214
2215    static BLACK_PAINT: PaintType = PaintType::Solid(BLACK);
2216
2217    struct TestResources {
2218        renderer: NoopRenderer,
2219        prep_cache: GlyphPrepCache,
2220        glyph_atlas: GlyphAtlas,
2221        image_cache: ImageCache,
2222    }
2223
2224    impl Default for TestResources {
2225        fn default() -> Self {
2226            Self {
2227                renderer: NoopRenderer,
2228                prep_cache: GlyphPrepCache::default(),
2229                glyph_atlas: GlyphAtlas::default(),
2230                image_cache: ImageCache::new_with_config(AtlasConfig {
2231                    atlas_size: (512, 512),
2232                    ..AtlasConfig::default()
2233                }),
2234            }
2235        }
2236    }
2237
2238    impl DrawSink for NoopRenderer {
2239        fn set_transform(&mut self, _t: Affine) {}
2240
2241        fn set_paint(&mut self, _paint: AtlasPaint) {}
2242
2243        fn set_paint_transform(&mut self, _t: Affine) {}
2244
2245        fn fill_path(&mut self, _path: &BezPath) {}
2246
2247        fn fill_rect(&mut self, _rect: &Rect) {}
2248
2249        fn push_clip_layer(&mut self, _clip: &BezPath) {}
2250
2251        fn push_blend_layer(&mut self, _blend_mode: BlendMode) {}
2252
2253        fn pop_layer(&mut self) {}
2254
2255        fn width(&self) -> u16 {
2256            512
2257        }
2258
2259        fn height(&self) -> u16 {
2260            512
2261        }
2262    }
2263
2264    impl GlyphRenderer for NoopRenderer {
2265        type SavedState = ();
2266
2267        fn save_state(&mut self) -> Self::SavedState {}
2268
2269        fn restore_state(&mut self, _state: Self::SavedState) {}
2270
2271        fn stroke_path(&mut self, _path: &BezPath) {}
2272
2273        fn set_paint_image(&mut self, _image: Image) {}
2274
2275        fn set_tint(&mut self, _tint: Option<Tint>) {}
2276
2277        fn get_context_color(&self) -> AlphaColor<Srgb> {
2278            BLACK
2279        }
2280
2281        fn current_paint(&self) -> &PaintType {
2282            &BLACK_PAINT
2283        }
2284
2285        fn atlas_image_source(&self, atlas_slot: &AtlasSlot) -> ImageSource {
2286            ImageSource::opaque_id(ImageId::new(atlas_slot.page_index))
2287        }
2288
2289        fn atlas_paint_transform(&self, atlas_slot: &AtlasSlot) -> Affine {
2290            Affine::translate((-(atlas_slot.x as f64), -(atlas_slot.y as f64)))
2291        }
2292    }
2293
2294    fn test_font(kind: TestGlyphKind) -> FontData {
2295        let bytes = match kind {
2296            TestGlyphKind::Outline => ROBOTO_FONT,
2297            TestGlyphKind::Colr => NOTO_COLR_FONT,
2298            #[cfg(feature = "png")]
2299            TestGlyphKind::Bitmap => NOTO_CBTF_FONT,
2300        };
2301        FontData::new(Blob::new(Arc::new(bytes)), 0)
2302    }
2303
2304    fn test_glyph(font: &FontData, kind: TestGlyphKind) -> Glyph {
2305        let ch = match kind {
2306            TestGlyphKind::Outline => 'H',
2307            TestGlyphKind::Colr => '✅',
2308            #[cfg(feature = "png")]
2309            TestGlyphKind::Bitmap => '✅',
2310        };
2311        let glyph_id = font.as_skrifa().charmap().map(ch).unwrap();
2312        Glyph {
2313            id: glyph_id.to_u32(),
2314            x: 0.0,
2315            y: 0.0,
2316        }
2317    }
2318
2319    fn draw_test_glyph(
2320        font: &FontData,
2321        glyph: Glyph,
2322        atlas_cache_enabled: bool,
2323        style: Style,
2324        resources: &mut TestResources,
2325    ) {
2326        let atlas_cacher = if atlas_cache_enabled {
2327            AtlasCacher::Enabled(&mut resources.glyph_atlas, &mut resources.image_cache)
2328        } else {
2329            AtlasCacher::Disabled
2330        };
2331
2332        let transform = Affine::translate((0.0, 20.0));
2333        let mut run = GlyphRun {
2334            font: font.clone(),
2335            font_size: 20.0,
2336            font_embolden: FontEmbolden::default(),
2337            transform,
2338            scene_paint_transform: transform,
2339            glyph_transform: None,
2340            normalized_coords: &[],
2341            hint: false,
2342        }
2343        .build(
2344            core::iter::once(glyph),
2345            resources.prep_cache.as_mut(),
2346            atlas_cacher,
2347        );
2348
2349        match style {
2350            Style::Fill => run.fill_glyphs(&mut resources.renderer),
2351            Style::Stroke => run.stroke_glyphs(&mut resources.renderer),
2352        }
2353    }
2354
2355    fn ensure_cache(kind: TestGlyphKind, style: Style) {
2356        let font = test_font(kind);
2357        let glyph = test_glyph(&font, kind);
2358        let mut resources = TestResources::default();
2359
2360        draw_test_glyph(&font, glyph, true, style, &mut resources);
2361
2362        assert_eq!(resources.glyph_atlas.len(), 1);
2363        assert_eq!(resources.glyph_atlas.cache_hits(), 0);
2364        // Note that we are checking > 0 instead of == 1 here because
2365        // COLR actually has slightly different cache access behavior than
2366        // normal outlines (we first perform a speculative check for normal outlines
2367        // and then get a second cache miss for the actual COLR glyph).
2368        assert!(resources.glyph_atlas.cache_misses() > 0);
2369
2370        draw_test_glyph(&font, glyph, true, style, &mut resources);
2371
2372        assert_eq!(resources.glyph_atlas.len(), 1);
2373        assert_eq!(resources.glyph_atlas.cache_hits(), 1);
2374        assert!(resources.glyph_atlas.cache_misses() > 0);
2375    }
2376
2377    fn ensure_no_cache(kind: TestGlyphKind, style: Style, atlas_cache_enabled: bool) {
2378        let font = test_font(kind);
2379        let glyph = test_glyph(&font, kind);
2380        let mut resources = TestResources::default();
2381
2382        draw_test_glyph(&font, glyph, atlas_cache_enabled, style, &mut resources);
2383
2384        assert_eq!(resources.glyph_atlas.len(), 0);
2385        assert_eq!(resources.glyph_atlas.cache_hits(), 0);
2386        assert_eq!(resources.glyph_atlas.cache_misses(), 0);
2387
2388        draw_test_glyph(&font, glyph, atlas_cache_enabled, style, &mut resources);
2389
2390        assert_eq!(resources.glyph_atlas.len(), 0);
2391        assert_eq!(resources.glyph_atlas.cache_hits(), 0);
2392        assert_eq!(resources.glyph_atlas.cache_misses(), 0);
2393    }
2394
2395    #[test]
2396    fn outline_glyph_is_cached_when_atlas_cache_is_enabled() {
2397        ensure_cache(TestGlyphKind::Outline, Style::Fill);
2398    }
2399
2400    #[test]
2401    fn outline_glyph_is_not_cached_when_atlas_cache_is_disabled() {
2402        ensure_no_cache(TestGlyphKind::Outline, Style::Fill, false);
2403    }
2404
2405    // This might change in the future, but for now they are not cached.
2406    #[test]
2407    fn stroked_outline_glyph_is_not_cached_when_atlas_cache_is_enabled() {
2408        ensure_no_cache(TestGlyphKind::Outline, Style::Stroke, true);
2409    }
2410
2411    #[test]
2412    fn stroked_outline_glyph_is_not_cached_when_atlas_cache_is_disabled() {
2413        ensure_no_cache(TestGlyphKind::Outline, Style::Stroke, false);
2414    }
2415
2416    #[test]
2417    fn colr_glyph_is_cached_when_atlas_cache_is_enabled() {
2418        ensure_cache(TestGlyphKind::Colr, Style::Fill);
2419    }
2420
2421    #[test]
2422    fn colr_glyph_is_not_cached_when_atlas_cache_is_disabled() {
2423        ensure_no_cache(TestGlyphKind::Colr, Style::Fill, false);
2424    }
2425
2426    #[cfg(feature = "png")]
2427    #[test]
2428    fn bitmap_glyph_is_cached_when_atlas_cache_is_enabled() {
2429        ensure_cache(TestGlyphKind::Bitmap, Style::Fill);
2430    }
2431
2432    #[cfg(feature = "png")]
2433    #[test]
2434    fn bitmap_glyph_is_not_cached_when_atlas_cache_is_disabled() {
2435        ensure_no_cache(TestGlyphKind::Bitmap, Style::Fill, false);
2436    }
2437}