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