| // Copyright 2025 the Vello Authors and the Parley Authors |
| // SPDX-License-Identifier: Apache-2.0 OR MIT |
| |
| //! Processing and drawing glyphs. |
| |
| #![allow( |
| clippy::cast_possible_truncation, |
| reason = "We temporarily ignore these because the casts\ |
| only break in edge cases, and some of them are also only related to conversions from f64 to f32." |
| )] |
| |
| use crate::Pixmap; |
| use crate::atlas::AtlasSlot; |
| use crate::atlas::GlyphCacheKey; |
| use crate::atlas::key::{SUBPIXEL_BITMAP, SUBPIXEL_COLR, pack_color}; |
| use crate::atlas::{GlyphAtlas, ImageCache}; |
| use crate::color::PremulRgba8; |
| use crate::color::palette::css::BLACK; |
| use crate::colr::{convert_bounding_box, get_colr_info}; |
| use crate::kurbo::Point; |
| use crate::kurbo::Rect; |
| use crate::kurbo::Vec2; |
| use crate::kurbo::{self, Affine, BezPath, Diagonal2, Join, Shape}; |
| use crate::kurbo::{Line, ParamCurve as _, PathSeg}; |
| use crate::peniko::FontData; |
| use crate::renderer::{fill_glyph, render_cached_glyph, stroke_glyph}; |
| use crate::util::AffineExt; |
| use alloc::boxed::Box; |
| use alloc::sync::Arc; |
| use alloc::vec::Vec; |
| use core::fmt::{Debug, Formatter}; |
| use core::ops::RangeInclusive; |
| #[cfg(not(feature = "std"))] |
| use core_maths::CoreFloat as _; |
| use hashbrown::hash_map::{Entry, RawEntryMut}; |
| use hashbrown::{Equivalent, HashMap}; |
| use skrifa::bitmap::{BitmapData, BitmapFormat, BitmapStrikes, Origin}; |
| use skrifa::instance::{LocationRef, Size}; |
| use skrifa::outline::{DrawSettings, OutlineGlyphFormat}; |
| use skrifa::outline::{HintingInstance, HintingOptions, OutlinePen}; |
| use skrifa::raw::TableProvider; |
| use skrifa::{FontRef, OutlineGlyphCollection}; |
| use skrifa::{GlyphId, MetadataProvider}; |
| use smallvec::SmallVec; |
| |
| /// Positioned glyph. |
| #[derive(Copy, Clone, Default, Debug)] |
| pub struct Glyph { |
| /// The font-specific identifier for this glyph. |
| /// |
| /// This ID is specific to the font being used and corresponds to the |
| /// glyph index within that font. It is *not* a Unicode code point. |
| pub id: u32, |
| /// X-offset in run, relative to transform. |
| pub x: f32, |
| /// Y-offset in run, relative to transform. |
| pub y: f32, |
| } |
| |
| /// Synthetic embolden settings for a glyph run. |
| #[derive(Clone, Copy, Debug)] |
| pub struct FontEmbolden { |
| /// Synthetic embolden amount. |
| pub amount: Diagonal2, |
| /// Join style used when expanding outlines. |
| pub join: Join, |
| /// Miter limit used when expanding outlines. |
| pub miter_limit: f64, |
| /// Tolerance used when expanding outlines. |
| pub tolerance: f64, |
| } |
| |
| impl FontEmbolden { |
| /// Create synthetic embolden settings with default expansion controls. |
| pub fn new(amount: Diagonal2) -> Self { |
| Self { |
| amount, |
| ..Self::default() |
| } |
| } |
| |
| /// Set the join style used when expanding outlines. |
| pub fn with_join(mut self, join: Join) -> Self { |
| self.join = join; |
| self |
| } |
| |
| /// Set the miter limit used when expanding outlines. |
| pub fn with_miter_limit(mut self, miter_limit: f64) -> Self { |
| self.miter_limit = miter_limit; |
| self |
| } |
| |
| /// Set the tolerance used when expanding outlines. |
| pub fn with_tolerance(mut self, tolerance: f64) -> Self { |
| self.tolerance = tolerance; |
| self |
| } |
| } |
| |
| impl Default for FontEmbolden { |
| fn default() -> Self { |
| Self { |
| amount: Diagonal2::new(0.0, 0.0), |
| join: Join::Miter, |
| miter_limit: 4.0, |
| tolerance: 0.1, |
| } |
| } |
| } |
| |
| /// Pre-packed `BLACK` color as a `u32` for use in `GlyphCacheKey`. |
| const BLACK_PACKED: u32 = PremulRgba8 { |
| r: 0, |
| g: 0, |
| b: 0, |
| a: 255, |
| } |
| .to_u32(); |
| |
| /// A type of glyph. |
| #[derive(Debug)] |
| pub(crate) enum GlyphType<'a> { |
| /// An outline glyph. |
| Outline(GlyphOutline), |
| /// A bitmap glyph. |
| Bitmap(GlyphBitmap), |
| /// A COLR glyph. |
| Colr(Box<GlyphColr<'a>>), |
| } |
| |
| /// Type hint for cached glyph rendering. |
| /// |
| /// Used when rendering directly from the atlas cache to skip glyph preparation. |
| #[derive(Debug, Clone, Copy)] |
| pub(crate) enum CachedGlyphType { |
| /// An outline glyph cached in the atlas. |
| Outline, |
| /// A bitmap glyph cached in the atlas. |
| Bitmap, |
| /// A COLR glyph cached in the atlas. |
| /// The `Rect` parameter contains the fractional area dimensions |
| /// to preserve sub-pixel accuracy during rendering. |
| Colr(Rect), |
| } |
| |
| /// A simplified representation of a glyph, prepared for easy rendering. |
| #[derive(Debug)] |
| pub(crate) struct PreparedGlyph<'a> { |
| /// The type of glyph. |
| pub(crate) glyph_type: GlyphType<'a>, |
| /// The global transform of the glyph. |
| pub(crate) transform: Affine, |
| /// Cache key for renderers that implement glyph caching. |
| /// This is `Some` for glyphs that can be cached, `None` otherwise. |
| /// |
| /// For COLR glyphs, `context_color` is extracted from the renderer's |
| /// current paint during cache key creation. |
| pub(crate) cache_key: Option<GlyphCacheKey>, |
| } |
| |
| /// A glyph defined by a path (its outline) and a local transform. |
| #[derive(Debug)] |
| pub(crate) struct GlyphOutline { |
| /// The path of the glyph (shared with the outline cache via `Arc`). |
| pub(crate) path: Arc<BezPath>, |
| } |
| |
| /// A glyph defined by a bitmap. |
| #[derive(Debug)] |
| pub(crate) struct GlyphBitmap { |
| /// The pixmap of the glyph. |
| pub(crate) pixmap: Arc<Pixmap>, |
| /// The rectangular area that should be filled with the bitmap when painting. |
| pub(crate) area: Rect, |
| } |
| |
| /// A glyph defined by a COLR glyph description. |
| /// |
| /// Clients are supposed to first draw the glyph into an intermediate image texture/pixmap |
| /// and then render that into the actual scene, in a similar fashion to |
| /// bitmap glyphs. |
| pub struct GlyphColr<'a> { |
| /// The original skrifa color glyph. |
| pub skrifa_glyph: skrifa::color::ColorGlyph<'a>, |
| /// The location of the glyph. |
| pub location: LocationRef<'a>, |
| /// The font reference. |
| pub font_ref: &'a FontRef<'a>, |
| /// The transform to apply to the glyph. |
| pub draw_transform: Affine, |
| /// The rectangular area that should be filled with the rendered representation of the |
| /// COLR glyph when painting. |
| pub area: Rect, |
| /// The width of the pixmap/texture in pixels to which the glyph should be rendered to. |
| pub pix_width: u16, |
| /// The height of the pixmap/texture in pixels to which the glyph should be rendered to. |
| pub pix_height: u16, |
| /// Whether the glyph paint graph uses a non-default blend mode. |
| pub has_non_default_blend: bool, |
| } |
| |
| impl Debug for GlyphColr<'_> { |
| fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { |
| write!(f, "GlyphColr") |
| } |
| } |
| |
| /// Caches used for preparing glyph drawing. |
| #[derive(Debug, Default)] |
| pub struct GlyphPrepCache { |
| /// Caches glyph outlines. |
| pub(crate) outline_cache: OutlineCache, |
| /// Caches hinting instances. |
| pub(crate) hinting_cache: HintCache, |
| /// Horizontal spans excluded from "ink-skipping" underlines. |
| pub(crate) underline_exclusions: Vec<(f64, f64)>, |
| } |
| |
| impl GlyphPrepCache { |
| /// Borrow this cache bundle mutable for glyph run construction. |
| pub fn as_mut(&mut self) -> GlyphPrepCacheMut<'_> { |
| GlyphPrepCacheMut { |
| outline_cache: &mut self.outline_cache, |
| hinting_cache: &mut self.hinting_cache, |
| underline_exclusions: &mut self.underline_exclusions, |
| } |
| } |
| |
| /// Clear the glyph preparation caches. |
| pub fn clear(&mut self) { |
| self.outline_cache.clear(); |
| self.hinting_cache.clear(); |
| self.underline_exclusions.clear(); |
| } |
| |
| /// Maintain the glyph preparation caches. |
| pub fn maintain(&mut self) { |
| self.outline_cache.maintain(); |
| } |
| } |
| |
| /// Mutably borrowed caches used for preparing glyph drawing. |
| #[derive(Debug)] |
| pub struct GlyphPrepCacheMut<'a> { |
| /// Caches glyph outlines. |
| pub(crate) outline_cache: &'a mut OutlineCache, |
| /// Caches hinting instances . |
| pub(crate) hinting_cache: &'a mut HintCache, |
| /// Horizontal spans excluded from "ink-skipping" underlines. |
| pub(crate) underline_exclusions: &'a mut Vec<(f64, f64)>, |
| } |
| |
| /// Determines whether atlas-backed glyph caching is available for a draw. |
| #[derive(Debug)] |
| pub enum AtlasCacher<'a> { |
| /// Draw directly without using the atlas cache. |
| Disabled, |
| /// Enable atlas-backed caching using the provided glyph atlas and image |
| /// allocator. |
| Enabled(&'a mut GlyphAtlas, &'a mut ImageCache), |
| } |
| |
| impl AtlasCacher<'_> { |
| fn config(&self) -> Option<&crate::atlas::GlyphCacheConfig> { |
| match self { |
| Self::Disabled => None, |
| Self::Enabled(glyph_atlas, _) => Some(glyph_atlas.config()), |
| } |
| } |
| |
| fn get(&mut self, key: &GlyphCacheKey) -> Option<AtlasSlot> { |
| match self { |
| Self::Disabled => None, |
| Self::Enabled(glyph_atlas, _) => glyph_atlas.get(key), |
| } |
| } |
| } |
| |
| /// A backend for glyph run builders. |
| pub trait GlyphRunBackend<'a>: Sized { |
| /// Enable or disable atlas-backed glyph caching for the glyph run. |
| fn atlas_cache(self, enabled: bool) -> Self; |
| |
| /// Fill the given glyph sequence using the configured builder state. |
| fn fill_glyphs<Glyphs>(self, run: GlyphRun<'a>, glyphs: Glyphs) |
| where |
| Glyphs: Iterator<Item = Glyph> + Clone; |
| |
| /// Stroke the given glyph sequence using the configured builder state. |
| fn stroke_glyphs<Glyphs>(self, run: GlyphRun<'a>, glyphs: Glyphs) |
| where |
| Glyphs: Iterator<Item = Glyph> + Clone; |
| |
| /// Render a decoration (e.g. underline) with skip-ink behavior. |
| fn render_decoration<Glyphs>( |
| self, |
| run: GlyphRun<'a>, |
| glyphs: Glyphs, |
| x_range: RangeInclusive<f32>, |
| baseline_y: f32, |
| offset: f32, |
| size: f32, |
| buffer: f32, |
| ) where |
| Glyphs: Iterator<Item = Glyph> + Clone; |
| } |
| |
| /// Helper struct for rendering a prepared glyph run. |
| #[derive(Debug)] |
| pub struct GlyphRunRenderer<'a, 'b, Glyphs: Iterator<Item = Glyph> + Clone> { |
| prepared_run: PreparedGlyphRun<'a>, |
| outline_cache: &'b mut OutlineCache, |
| underline_span_cache: &'b mut Vec<(f64, f64)>, |
| glyph_iterator: Glyphs, |
| atlas_cacher: AtlasCacher<'b>, |
| } |
| |
| impl<'a, 'b, Glyphs: Iterator<Item = Glyph> + Clone> GlyphRunRenderer<'a, 'b, Glyphs> { |
| /// Fills the glyphs with the current configuration. |
| pub fn fill_glyphs(&mut self, renderer: &mut impl crate::GlyphRenderer) { |
| self.draw_glyphs(Style::Fill, renderer); |
| } |
| |
| /// Strokes the glyphs with the current configuration. |
| pub fn stroke_glyphs(&mut self, renderer: &mut impl crate::GlyphRenderer) { |
| self.draw_glyphs(Style::Stroke, renderer); |
| } |
| |
| /// Core rendering loop shared by [`fill_glyphs`](Self::fill_glyphs) and |
| /// [`stroke_glyphs`](Self::stroke_glyphs). |
| /// |
| /// Each glyph is resolved through a priority cascade: COLR > bitmap > outline. |
| /// The first matching representation wins. Within each branch the atlas cache |
| /// is checked before falling through to the slow path (rasterization / path |
| /// construction). |
| fn draw_glyphs(&mut self, style: Style, renderer: &mut impl crate::GlyphRenderer) { |
| let font_ref = self.prepared_run.font.as_skrifa(); |
| let upem: f32 = font_ref.head().map(|h| h.units_per_em()).unwrap().into(); |
| |
| let outlines = font_ref.outline_glyphs(); |
| let color_glyphs = font_ref.color_glyphs(); |
| let bitmaps = font_ref.bitmap_strikes(); |
| |
| let mut outline_cache_session = OutlineCacheSession::new( |
| self.outline_cache, |
| VarLookupKey(self.prepared_run.normalized_coords), |
| ); |
| let PreparedGlyphRun { |
| draw_props, |
| run_size: _, |
| font_embolden, |
| normalized_coords, |
| hinting_instance, |
| .. |
| } = self.prepared_run; |
| |
| let font_id = self.prepared_run.font.data.id(); |
| let font_index = self.prepared_run.font.index; |
| let hinted = hinting_instance.is_some(); |
| |
| let colr_bitmap_cache_enabled = self |
| .atlas_cacher |
| .config() |
| .is_some_and(|config| draw_props.font_size <= config.max_cached_font_size); |
| let outline_cache_enabled = colr_bitmap_cache_enabled |
| // Due to the various parameters that would need to be considered in the cache key, |
| // we never cache stroked outlines for now. For COLR and bitmap, this doesn't matter |
| // because they are always filled anyway. |
| && style == Style::Fill; |
| |
| let context_color = renderer.get_context_color(); |
| let context_color_packed = pack_color(context_color); |
| for glyph in self.glyph_iterator.clone() { |
| // TODO: Add a mechanism such that glyphs that are completely outside of the viewport |
| // (especially for more expensive COLR glyphs), we don't do any processing in the |
| // first place and cull them. |
| let glyph_id = GlyphId::new(glyph.id); |
| |
| // ── Speculative outline cache check ───────────────────────── |
| // ~99% of glyphs are outlines. The transform and cache key are |
| // pure arithmetic, so we probe the cache before the expensive |
| // color_glyphs.get() / bitmaps.glyph_for_size() font-table lookups. |
| // On a miss we keep both for reuse in the outline branch below. |
| let outline_transform = |
| calculate_outline_transform(glyph, draw_props, hinting_instance); |
| let outline_cache_key = outline_cache_enabled.then(|| { |
| let fractional_x = outline_transform.translation().x.fract() as f32; |
| GlyphCacheKey::new( |
| font_id, |
| font_index, |
| glyph.id, |
| draw_props.font_size, |
| hinted, |
| fractional_x, |
| BLACK, |
| BLACK_PACKED, |
| font_embolden, |
| normalized_coords, |
| ) |
| }); |
| if let Some(ref key) = outline_cache_key |
| && let Some(cached_slot) = self.atlas_cacher.get(key) |
| { |
| render_cached_glyph( |
| renderer, |
| cached_slot, |
| outline_transform, |
| CachedGlyphType::Outline, |
| ); |
| continue; |
| } |
| |
| // ── COLR Glyphs ─────────────────────────────────────────── |
| if let Some(color_glyph) = color_glyphs.get(glyph_id) { |
| let location = LocationRef::new(normalized_coords); |
| let metrics = calculate_colr_metrics( |
| draw_props.font_size, |
| upem, |
| draw_props, |
| glyph, |
| &font_ref, |
| &color_glyph, |
| location, |
| ); |
| let transform = calculate_colr_transform(&metrics); |
| |
| // COLR glyphs are never hinted and have no sub-pixel offset; |
| // context_color is part of the key because it affects painted layers. |
| let cache_key = colr_bitmap_cache_enabled.then(|| GlyphCacheKey { |
| font_id, |
| font_index, |
| glyph_id: glyph.id, |
| size_bits: draw_props.font_size.to_bits(), |
| hinted: false, |
| subpixel_x: SUBPIXEL_COLR, |
| context_color, |
| context_color_packed, |
| embolden_x_bits: 0, |
| embolden_y_bits: 0, |
| embolden_join_bits: join_bits(Join::Miter), |
| embolden_miter_limit_bits: 4.0_f32.to_bits(), |
| embolden_tolerance_bits: 0.1_f32.to_bits(), |
| var_coords: SmallVec::from_slice(normalized_coords), |
| }); |
| |
| if let Some(ref key) = cache_key |
| && let Some(cached_slot) = self.atlas_cacher.get(key) |
| { |
| // Use fractional scaled_bbox dimensions to preserve sub-pixel accuracy. |
| let area = Rect::new( |
| 0.0, |
| 0.0, |
| metrics.scaled_bbox.width(), |
| metrics.scaled_bbox.height(), |
| ); |
| render_cached_glyph( |
| renderer, |
| cached_slot, |
| transform, |
| CachedGlyphType::Colr(area), |
| ); |
| continue; |
| } |
| |
| // Cache miss — rasterize the COLR glyph from scratch. |
| let glyph_type = |
| create_colr_glyph(&font_ref, &metrics, color_glyph, normalized_coords); |
| |
| let prepared_glyph = PreparedGlyph { |
| glyph_type, |
| transform, |
| cache_key, |
| }; |
| match style { |
| Style::Fill => fill_glyph(renderer, prepared_glyph, &mut self.atlas_cacher), |
| Style::Stroke => stroke_glyph(renderer, prepared_glyph, &mut self.atlas_cacher), |
| } |
| continue; |
| } |
| |
| // ── Bitmap Glyphs ──────────────────────────────────────────── |
| let bitmap_data: Option<(skrifa::bitmap::BitmapGlyph<'_>, Pixmap)> = bitmaps |
| .glyph_for_size(Size::new(draw_props.font_size), glyph_id) |
| .and_then(|g| match g.data { |
| #[cfg(feature = "png")] |
| BitmapData::Png(data) => Pixmap::from_png(std::io::Cursor::new(data)) |
| .ok() |
| .map(|d| (g, d)), |
| #[cfg(not(feature = "png"))] |
| BitmapData::Png(_) => None, |
| // The others are not worth implementing for now (unless we can find a test case), |
| // they should be very rare. |
| BitmapData::Bgra(_) => None, |
| BitmapData::Mask(_) => None, |
| }); |
| |
| if let Some((bitmap_glyph, pixmap)) = bitmap_data { |
| // Bitmaps use the strike's own ppem, not the run's, because the |
| // image was pre-rendered at that specific size. |
| let bitmap_ppem = bitmap_glyph.ppem_x; |
| let transform = calculate_bitmap_transform( |
| glyph, |
| &pixmap, |
| draw_props, |
| draw_props.font_size, |
| upem, |
| &bitmap_glyph, |
| &bitmaps, |
| ); |
| |
| // Bitmaps are not hinted and have no sub-pixel offset or |
| // context color; variation coords are irrelevant for fixed strikes. |
| let cache_key = colr_bitmap_cache_enabled.then(|| GlyphCacheKey { |
| font_id, |
| font_index, |
| glyph_id: glyph.id, |
| size_bits: bitmap_ppem.to_bits(), |
| hinted: false, |
| subpixel_x: SUBPIXEL_BITMAP, |
| context_color: BLACK, |
| context_color_packed: BLACK_PACKED, |
| embolden_x_bits: 0, |
| embolden_y_bits: 0, |
| embolden_join_bits: join_bits(Join::Miter), |
| embolden_miter_limit_bits: 4.0_f32.to_bits(), |
| embolden_tolerance_bits: 0.1_f32.to_bits(), |
| var_coords: SmallVec::new(), |
| }); |
| |
| if let Some(ref key) = cache_key |
| && let Some(cached_slot) = self.atlas_cacher.get(key) |
| { |
| render_cached_glyph(renderer, cached_slot, transform, CachedGlyphType::Bitmap); |
| continue; |
| } |
| |
| // Cache miss — wrap the decoded pixmap for rendering. |
| let glyph_type = create_bitmap_glyph(pixmap); |
| |
| let prepared_glyph = PreparedGlyph { |
| glyph_type, |
| transform, |
| cache_key, |
| }; |
| match style { |
| Style::Fill => fill_glyph(renderer, prepared_glyph, &mut self.atlas_cacher), |
| Style::Stroke => stroke_glyph(renderer, prepared_glyph, &mut self.atlas_cacher), |
| } |
| continue; |
| } |
| |
| // ── Outline Glyphs ────────────────────────────────────────── |
| // Transform and cache key were already computed at the top of the |
| // loop for the speculative check. Reuse them here on a cache miss. |
| |
| // Cache miss — fetch the outline from skrifa (expensive: parses font |
| // tables), then build the path. Deferred to here so cache hits skip it. |
| let Some(outline) = outlines.get(glyph_id) else { |
| continue; |
| }; |
| |
| let glyph_type = create_outline_glyph( |
| glyph.id, |
| font_id, |
| font_index, |
| &mut outline_cache_session, |
| draw_props.font_size, |
| font_embolden, |
| &outline, |
| hinting_instance, |
| normalized_coords, |
| ); |
| |
| let prepared_glyph = PreparedGlyph { |
| glyph_type, |
| transform: outline_transform, |
| cache_key: outline_cache_key, |
| }; |
| match style { |
| Style::Fill => fill_glyph(renderer, prepared_glyph, &mut self.atlas_cacher), |
| Style::Stroke => stroke_glyph(renderer, prepared_glyph, &mut self.atlas_cacher), |
| } |
| } |
| } |
| |
| /// Return the scaling factor that should be applied to the stroke width when stroking this |
| /// glyph run. |
| pub fn stroke_adjustment(&self) -> f64 { |
| let run_size = self.prepared_run.run_size; |
| |
| if run_size == 0.0 { |
| 1.0 |
| } else { |
| f64::from(self.prepared_run.draw_props.font_size / run_size) |
| } |
| } |
| |
| /// Render a decoration (like an underline) that skips over glyph descenders. |
| /// |
| /// This implements `text-decoration-skip-ink`-like behavior, where the decoration line is interrupted where it |
| /// would overlap with glyph outlines. |
| /// |
| /// The `x_range` specifies the horizontal position of the decoration, and the `offset` and `size` specify its |
| /// vertical position and height (relative to the baseline). The `buffer` specifies how much horizontal space to |
| /// leave around each descender. |
| pub fn render_decoration( |
| &mut self, |
| x_range: RangeInclusive<f32>, |
| baseline_y: f32, |
| offset: f32, |
| size: f32, |
| buffer: f32, |
| renderer: &mut impl crate::DrawSink, |
| ) { |
| self.decoration_spans(x_range, baseline_y, offset, size, buffer) |
| .for_each(|rect| { |
| renderer.fill_rect(&rect); |
| }); |
| } |
| |
| fn decoration_spans<'c>( |
| &'c mut self, |
| x_range: RangeInclusive<f32>, |
| baseline_y: f32, |
| offset: f32, |
| size: f32, |
| buffer: f32, |
| ) -> impl Iterator<Item = Rect> + 'c { |
| let font_ref = self.prepared_run.font.as_skrifa(); |
| let outlines = font_ref.outline_glyphs(); |
| |
| let PreparedGlyphRun { |
| draw_props, |
| font_embolden, |
| hinting_instance, |
| .. |
| } = self.prepared_run; |
| |
| // The glyph_transform (e.g. skew for fake italics) affects where the outline points end up. We apply it along |
| // with the Y flip to transform from font space (Y up) to layout space (Y down). |
| // |
| // During the preparation of the glyph run, the transform of the run may be absorbed into |
| // `draw_props.font_size`, outlines are generated in that scaled coordinate space. We scale them back |
| // to the nominal coordinate space. The glyph-drawing path handles this by |
| // simply drawing in global space, but we need to invert it for drawing decorations. |
| let outline_to_nominal_scale = f64::from(self.prepared_run.run_size / draw_props.font_size); |
| let outline_transform = self |
| .prepared_run |
| .glyph_transform |
| .unwrap_or(Affine::IDENTITY) |
| * Affine::FLIP_Y |
| * Affine::scale(outline_to_nominal_scale); |
| |
| // Buffer to add around each exclusion zone |
| let buffer = f64::from(buffer); |
| |
| // X range for the decoration line |
| let x0 = f64::from(*x_range.start()); |
| let x1 = f64::from(*x_range.end()); |
| |
| // Convert offset/size to layout space (Y down). |
| // offset is positive above baseline, so negate for layout coordinates. |
| let layout_y0 = f64::from(-offset); |
| let layout_y1 = f64::from(-offset + size); |
| |
| // Get a cache session for this font's variation coordinates |
| let var_key = VarLookupKey(self.prepared_run.normalized_coords); |
| let mut outline_cache_session = OutlineCacheSession::new(self.outline_cache, var_key); |
| |
| // Collect and merge exclusion zones from all glyphs. |
| let exclusions = &mut self.underline_span_cache; |
| // We `drain` this when creating the iterator, but just in case... |
| exclusions.truncate(0); |
| |
| for glyph in self.glyph_iterator.clone() { |
| // TODO: skip ink for color and bitmap glyphs |
| let Some(outline) = outlines.get(GlyphId::new(glyph.id)) else { |
| continue; |
| }; |
| |
| let cached = outline_cache_session.get_or_insert( |
| glyph.id, |
| self.prepared_run.font.data.id(), |
| self.prepared_run.font.index, |
| draw_props.font_size, |
| font_embolden, |
| var_key, |
| &outline, |
| hinting_instance, |
| ); |
| |
| // If the glyph's bounding box doesn't intersect the underline at all, we don't need to calculate |
| // intersections. This saves a lot of time, since most glyphs don't have descenders. |
| // |
| // We only need the y-extent of the transformed bbox, so we compute it directly using the formula: |
| // y' = b*x + d*y + f |
| let [_, b, _, d, _, f] = outline_transform.as_coeffs(); |
| let (y_min, y_max) = { |
| let bx0 = b * cached.bbox.x0; |
| let bx1 = b * cached.bbox.x1; |
| let dy0 = d * cached.bbox.y0; |
| let dy1 = d * cached.bbox.y1; |
| ( |
| f + bx0.min(bx1) + dy0.min(dy1), |
| f + bx0.max(bx1) + dy0.max(dy1), |
| ) |
| }; |
| if y_max < layout_y0 || y_min > layout_y1 { |
| continue; |
| } |
| |
| let mut rect = Rect { |
| x0: f64::INFINITY, |
| x1: f64::NEG_INFINITY, |
| y0: layout_y0, |
| y1: layout_y1, |
| }; |
| |
| for seg in cached.path.segments() { |
| // Transform the segment to layout space |
| let seg = outline_transform * seg; |
| expand_rect_with_segment(&mut rect, seg, layout_y0..=layout_y1); |
| } |
| |
| // Add glyph position and buffer, then clip to decoration x-range |
| let excl_start = (rect.x0 + f64::from(glyph.x) - buffer).max(x0); |
| let excl_end = (rect.x1 + f64::from(glyph.x) + buffer).min(x1); |
| |
| // Skip if no valid exclusion (empty intersection or outside x-range) |
| if excl_start >= excl_end { |
| continue; |
| } |
| |
| // Insert in sorted order and merge with overlapping ranges |
| insert_and_merge_range(exclusions, excl_start, excl_end); |
| } |
| |
| // Draw decoration segments, skipping the exclusion zones |
| let y0 = f64::from(baseline_y) + layout_y0; |
| let y1 = f64::from(baseline_y) + layout_y1; |
| |
| let mut state = Some((exclusions.drain(..), x0)); |
| core::iter::from_fn(move || { |
| let (iter, current_x) = state.as_mut()?; |
| let Some((excl_start, excl_end)) = iter.next() else { |
| // Draw the trailing rectangle |
| let final_rect = Rect::new(*current_x, y0, x1, y1); |
| state = None; |
| return (final_rect.width() > 0.0).then_some(final_rect); |
| }; |
| |
| // Draw segment before this exclusion |
| let rect = Rect::new(*current_x, y0, excl_start, y1); |
| *current_x = excl_end; |
| Some(rect) |
| }) |
| } |
| } |
| |
| /// A builder for configuring and drawing glyphs. |
| #[derive(Debug)] |
| #[must_use = "Methods on the builder don't do anything until `render` is called."] |
| pub struct GlyphRunBuilder<'a, B> { |
| run: GlyphRun<'a>, |
| backend: B, |
| } |
| |
| impl<'a, B> GlyphRunBuilder<'a, B> { |
| /// Creates a new builder for drawing glyphs with a pre-bound backend. |
| pub fn new(font: FontData, transform: Affine, backend: B) -> Self { |
| Self { |
| // Note: This needs to be kept in sync with the default in vello_common! |
| run: GlyphRun { |
| font, |
| font_size: 16.0, |
| font_embolden: FontEmbolden::default(), |
| transform, |
| glyph_transform: None, |
| hint: true, |
| normalized_coords: &[], |
| }, |
| backend, |
| } |
| } |
| |
| /// Set the font size in pixels per em. |
| pub fn font_size(mut self, size: f32) -> Self { |
| self.run.font_size = size; |
| self |
| } |
| |
| /// Set synthetic embolden settings. |
| pub fn font_embolden(mut self, embolden: FontEmbolden) -> Self { |
| self.run.font_embolden = embolden; |
| self |
| } |
| |
| /// Set the per-glyph transform. Use `Affine::skew` with a horizontal-only skew to simulate |
| /// italic text. |
| pub fn glyph_transform(mut self, transform: Affine) -> Self { |
| self.run.glyph_transform = Some(transform); |
| self |
| } |
| |
| /// Set whether font hinting is enabled. |
| /// |
| /// This performs vertical hinting only. Hinting is performed only if the combined `transform` |
| /// and `glyph_transform` have a uniform scale and no vertical skew or rotation. |
| pub fn hint(mut self, hint: bool) -> Self { |
| self.run.hint = hint; |
| self |
| } |
| |
| /// Set normalized variation coordinates for variable fonts. |
| pub fn normalized_coords(mut self, coords: &'a [NormalizedCoord]) -> Self { |
| self.run.normalized_coords = bytemuck::cast_slice(coords); |
| self |
| } |
| } |
| |
| impl<'a> GlyphRun<'a> { |
| // Note: Not sure if we should just remove that method and let each backend |
| // call `prepare_glyph_run` manually, it might allow us to reduce the number of |
| // generics we need to use. But for now, it seems nice to be able to abstract away |
| // the `prepare_glyph_run` method call. |
| /// Returns a renderer that can fill, stroke, and decorate this glyph run. |
| #[doc(hidden)] |
| pub fn build<'b: 'a, Glyphs: Iterator<Item = Glyph> + Clone>( |
| self, |
| glyphs: Glyphs, |
| prep_cache: GlyphPrepCacheMut<'b>, |
| atlas_cacher: AtlasCacher<'b>, |
| ) -> GlyphRunRenderer<'a, 'b, Glyphs> { |
| let prepared_run = prepare_glyph_run(self, prep_cache.hinting_cache); |
| GlyphRunRenderer { |
| prepared_run, |
| glyph_iterator: glyphs, |
| outline_cache: prep_cache.outline_cache, |
| underline_span_cache: prep_cache.underline_exclusions, |
| atlas_cacher, |
| } |
| } |
| } |
| |
| impl<'a, B> GlyphRunBuilder<'a, B> |
| where |
| B: GlyphRunBackend<'a>, |
| { |
| /// Enable or disable the glyph atlas cache. |
| pub fn atlas_cache(self, enabled: bool) -> Self { |
| Self { |
| run: self.run, |
| backend: self.backend.atlas_cache(enabled), |
| } |
| } |
| |
| /// Fill the glyphs using the current settings. |
| pub fn fill_glyphs<Glyphs>(self, glyphs: Glyphs) |
| where |
| Glyphs: Iterator<Item = Glyph> + Clone, |
| { |
| let GlyphRunBuilder { run, backend } = self; |
| backend.fill_glyphs(run, glyphs); |
| } |
| |
| /// Stroke the glyphs using the current settings. |
| pub fn stroke_glyphs<Glyphs>(self, glyphs: Glyphs) |
| where |
| Glyphs: Iterator<Item = Glyph> + Clone, |
| { |
| let GlyphRunBuilder { run, backend } = self; |
| backend.stroke_glyphs(run, glyphs); |
| } |
| |
| /// Render a decoration (e.g. underline) with skip-ink behavior. |
| /// |
| /// See [`GlyphRunRenderer::render_decoration`]. |
| pub fn render_decoration<Glyphs>( |
| self, |
| glyphs: Glyphs, |
| x_range: RangeInclusive<f32>, |
| baseline_y: f32, |
| offset: f32, |
| size: f32, |
| buffer: f32, |
| ) where |
| Glyphs: Iterator<Item = Glyph> + Clone, |
| { |
| let GlyphRunBuilder { run, backend } = self; |
| backend.render_decoration(run, glyphs, x_range, baseline_y, offset, size, buffer); |
| } |
| } |
| |
| /// Insert a range into a sorted list, merging with any overlapping ranges. |
| fn insert_and_merge_range(ranges: &mut Vec<(f64, f64)>, start: f64, end: f64) { |
| // Search backwards from the end to find insertion point. Since glyphs come in visual (left-to-right) order, new |
| // ranges are usually at or near the end, making this O(1) in the common case. |
| let insert_pos = ranges |
| .iter() |
| .rposition(|r| r.0 <= start) |
| .map_or(0, |i| i + 1); |
| |
| // Check if we overlap with the previous range |
| let merge_start = insert_pos |
| .checked_sub(1) |
| .filter(|&i| ranges[i].1 >= start) |
| .unwrap_or(insert_pos); |
| |
| // Find all overlapping ranges and compute merged bounds |
| let new_end = ranges[merge_start..] |
| .iter() |
| .take_while(|(s, _)| *s <= end) |
| .fold(end, |acc, (_, e)| acc.max(*e)); |
| |
| let merge_end = merge_start |
| + ranges[merge_start..] |
| .iter() |
| .take_while(|(s, _)| *s <= new_end) |
| .count(); |
| |
| // Replace the overlapping ranges with the merged range |
| if merge_start < merge_end { |
| let new_start = start.min(ranges[merge_start].0); |
| ranges.splice(merge_start..merge_end, [(new_start, new_end)]); |
| } else { |
| ranges.insert(insert_pos, (start, end)); |
| } |
| } |
| |
| fn expand_rect_with_segment(rect: &mut Rect, seg: PathSeg, y_span: RangeInclusive<f64>) { |
| // Calculate the rough bounds of the segment from its control points. This is *not* the same as |
| // `kurbo::Shape::bounding_box`, which returns a precise bounding box but requires expensively calculating the curve |
| // extrema. |
| let (mut x_bounds, y_bounds) = match seg { |
| PathSeg::Line(line) => ( |
| (line.p0.x.min(line.p1.x), line.p0.x.max(line.p1.x)), |
| (line.p0.y.min(line.p1.y), line.p0.y.max(line.p1.y)), |
| ), |
| PathSeg::Quad(quad) => ( |
| ( |
| quad.p0.x.min(quad.p1.x).min(quad.p2.x), |
| quad.p0.x.max(quad.p1.x).max(quad.p2.x), |
| ), |
| ( |
| quad.p0.y.min(quad.p1.y).min(quad.p2.y), |
| quad.p0.y.max(quad.p1.y).max(quad.p2.y), |
| ), |
| ), |
| PathSeg::Cubic(cubic) => ( |
| ( |
| cubic.p0.x.min(cubic.p1.x).min(cubic.p2.x).min(cubic.p3.x), |
| cubic.p0.x.max(cubic.p1.x).max(cubic.p2.x).max(cubic.p3.x), |
| ), |
| ( |
| cubic.p0.y.min(cubic.p1.y).min(cubic.p2.y).min(cubic.p3.y), |
| cubic.p0.y.max(cubic.p1.y).max(cubic.p2.y).max(cubic.p3.y), |
| ), |
| ), |
| }; |
| // Skip segments entirely outside the y_span |
| if y_bounds.1 < *y_span.start() || y_bounds.0 > *y_span.end() { |
| return; |
| } |
| |
| // All we care about are the x-intersections. The intersection methods don't work on infinitely-long lines, so we |
| // construct a "long enough" line based on segment bounds. This expansion allows for a little bit of error. |
| x_bounds.0 -= 1.0; |
| x_bounds.1 += 1.0; |
| let top_line = Line::new((x_bounds.0, *y_span.start()), (x_bounds.1, *y_span.start())); |
| let bottom_line = Line::new((x_bounds.0, *y_span.end()), (x_bounds.1, *y_span.end())); |
| |
| for intersection in seg.intersect_line(top_line) { |
| let point = top_line.eval(intersection.line_t); |
| // There might be some slight inaccuracy calculating `point` from `line_t`, so we only adjust the x-values |
| // instead of using `union_pt`, which may also expand the y-values. |
| rect.x0 = rect.x0.min(point.x); |
| rect.x1 = rect.x1.max(point.x); |
| } |
| |
| for intersection in seg.intersect_line(bottom_line) { |
| let point = bottom_line.eval(intersection.line_t); |
| rect.x0 = rect.x0.min(point.x); |
| rect.x1 = rect.x1.max(point.x); |
| } |
| |
| // Also check segment endpoints that lie within the y-range |
| let (seg_start, seg_end) = match seg { |
| PathSeg::Line(line) => (line.p0, line.p1), |
| PathSeg::Quad(quad) => (quad.p0, quad.p2), |
| PathSeg::Cubic(cubic) => (cubic.p0, cubic.p3), |
| }; |
| |
| for point in [seg_start, seg_end] { |
| if (*y_span.start()..=*y_span.end()).contains(&point.y) { |
| rect.x0 = rect.x0.min(point.x); |
| rect.x1 = rect.x1.max(point.x); |
| } |
| } |
| } |
| |
| /// Create outline glyph data from cache. |
| /// |
| /// This extracts the glyph path from the outline cache, creating a `GlyphType::Outline` |
| /// without any positioning information. |
| fn create_outline_glyph<'a>( |
| glyph_id: u32, |
| font_id: u64, |
| font_index: u32, |
| outline_cache: &'a mut OutlineCacheSession<'_>, |
| size: f32, |
| embolden: FontEmbolden, |
| outline_glyph: &skrifa::outline::OutlineGlyph<'a>, |
| hinting_instance: Option<&HintingInstance>, |
| normalized_coords: &[skrifa::instance::NormalizedCoord], |
| ) -> GlyphType<'a> { |
| let cached = outline_cache.get_or_insert( |
| glyph_id, |
| font_id, |
| font_index, |
| size, |
| embolden, |
| VarLookupKey(normalized_coords), |
| outline_glyph, |
| hinting_instance, |
| ); |
| |
| GlyphType::Outline(GlyphOutline { |
| path: Arc::clone(cached.path), |
| }) |
| } |
| |
| /// Calculate transform for outline glyphs. |
| /// |
| /// This computes the final positioning transform for an outline glyph, taking into account: |
| /// - Glyph position within the run |
| /// - Run-space glyph positioning |
| /// - Y-axis flip (fonts use upside-down coordinate system) |
| /// - Hinting adjustments (snap y-offset to integer) |
| fn calculate_outline_transform( |
| glyph: Glyph, |
| draw_props: DrawProps, |
| hinting_instance: Option<&HintingInstance>, |
| ) -> Affine { |
| let mut final_transform = draw_props |
| .positioned_transform(glyph) |
| .pre_scale_non_uniform(1.0, -1.0) |
| .as_coeffs(); |
| |
| if hinting_instance.is_some() { |
| final_transform[5] = final_transform[5].round(); |
| } |
| |
| Affine::new(final_transform) |
| } |
| |
| /// Create bitmap glyph data. |
| /// |
| /// This wraps the pixmap in a `GlyphType::Bitmap` with its display area, |
| /// without any positioning information. |
| fn create_bitmap_glyph(pixmap: Pixmap) -> GlyphType<'static> { |
| // Scale factor already accounts for ppem, so we can just draw in the size of the |
| // actual image |
| let area = Rect::new( |
| 0.0, |
| 0.0, |
| f64::from(pixmap.width()), |
| f64::from(pixmap.height()), |
| ); |
| |
| GlyphType::Bitmap(GlyphBitmap { |
| pixmap: Arc::new(pixmap), |
| area, |
| }) |
| } |
| |
| /// Calculate transform for bitmap glyphs. |
| /// |
| /// This computes the final positioning transform for a bitmap glyph, taking into account: |
| /// - Glyph position within the run |
| /// - Bitmap scaling to match requested font size |
| /// - Bearing adjustments (outer and inner) |
| /// - Origin placement (top-left vs bottom-left) |
| /// - Special handling for Apple Color Emoji |
| fn calculate_bitmap_transform( |
| glyph: Glyph, |
| pixmap: &Pixmap, |
| draw_props: DrawProps, |
| font_size: f32, |
| upem: f32, |
| bitmap_glyph: &skrifa::bitmap::BitmapGlyph<'_>, |
| bitmaps: &BitmapStrikes<'_>, |
| ) -> Affine { |
| let x_scale_factor = font_size / bitmap_glyph.ppem_x; |
| let y_scale_factor = font_size / bitmap_glyph.ppem_y; |
| let font_units_to_size = font_size / upem; |
| |
| // CoreText appears to special case Apple Color Emoji, adding |
| // a 100 font unit vertical offset. We do the same but only |
| // when both vertical offsets are 0 to avoid incorrect |
| // rendering if Apple ever does encode the offset directly in |
| // the font. |
| let bearing_y = if bitmap_glyph.bearing_y == 0.0 && bitmaps.format() == Some(BitmapFormat::Sbix) |
| { |
| 100.0 |
| } else { |
| bitmap_glyph.bearing_y |
| }; |
| |
| let origin_shift = match bitmap_glyph.placement_origin { |
| Origin::TopLeft => Vec2::default(), |
| Origin::BottomLeft => Vec2 { |
| x: 0., |
| y: -f64::from(pixmap.height()), |
| }, |
| }; |
| |
| draw_props |
| .positioned_transform(glyph) |
| // Apply outer bearings. |
| .pre_translate(Vec2 { |
| x: (-bitmap_glyph.bearing_x * font_units_to_size).into(), |
| y: (bearing_y * font_units_to_size).into(), |
| }) |
| // Scale to pixel-space. |
| .pre_scale_non_uniform(f64::from(x_scale_factor), f64::from(y_scale_factor)) |
| // Apply inner bearings. |
| .pre_translate(Vec2 { |
| x: (-bitmap_glyph.inner_bearing_x).into(), |
| y: (-bitmap_glyph.inner_bearing_y).into(), |
| }) |
| .pre_translate(origin_shift) |
| } |
| |
| /// Helper struct containing computed COLR glyph metrics. |
| struct ColrMetrics { |
| /// Base transform with glyph position applied. |
| transform: Affine, |
| /// Scaled bounding box in device coordinates. |
| scaled_bbox: Rect, |
| /// Scale factor for x-axis. |
| scale_factor_x: f64, |
| /// Scale factor for y-axis. |
| scale_factor_y: f64, |
| /// Font size scale (`font_size` / `upem`). |
| font_size_scale: f64, |
| has_non_default_blend: bool, |
| } |
| |
| /// Calculate COLR glyph metrics (scale factors, bounding box, etc.). |
| /// |
| /// This computes the intermediate values needed for both creating the `GlyphColr` |
| /// and calculating its positioning transform. |
| fn calculate_colr_metrics( |
| font_size: f32, |
| upem: f32, |
| draw_props: DrawProps, |
| glyph: Glyph, |
| font_ref: &FontRef<'_>, |
| color_glyph: &skrifa::color::ColorGlyph<'_>, |
| location: LocationRef<'_>, |
| ) -> ColrMetrics { |
| // The scale factor we need to apply to scale from font units to our font size. |
| let font_size_scale = (font_size / upem) as f64; |
| let transform = draw_props.positioned_transform(glyph); |
| |
| // Estimate the size of the intermediate pixmap. Ideally, the intermediate bitmap should have |
| // exactly one pixel (or more) per device pixel, to ensure that no quality is lost. Therefore, |
| // we simply use the scaling/skewing factor to calculate how much to scale each axis by. |
| let (scale_factor_x, scale_factor_y) = { |
| let (x_vec, y_vec) = x_y_advances(&transform.pre_scale(font_size_scale)); |
| (x_vec.length(), y_vec.length()) |
| }; |
| |
| // TODO: Cache this across frames. |
| let colr_info = get_colr_info(font_ref, color_glyph, location); |
| let bbox = color_glyph |
| // First try to get the clip bbox from the COLR table, |
| // as this one has the highest priority. |
| .bounding_box(location, Size::unscaled()) |
| .map(convert_bounding_box) |
| // Otherwise, we use the conservative bounding box we determined before. |
| .or(colr_info.bbox) |
| .unwrap_or(Rect::ZERO); |
| |
| // Calculate the position of the rectangle that will contain the rendered pixmap in device |
| // coordinates. |
| let scaled_bbox = Rect { |
| x0: bbox.x0 * scale_factor_x, |
| y0: bbox.y0 * scale_factor_y, |
| x1: bbox.x1 * scale_factor_x, |
| y1: bbox.y1 * scale_factor_y, |
| }; |
| |
| ColrMetrics { |
| transform, |
| scaled_bbox, |
| scale_factor_x, |
| scale_factor_y, |
| font_size_scale, |
| has_non_default_blend: colr_info.has_non_default_blend, |
| } |
| } |
| |
| /// Calculate transform for COLR glyphs. |
| /// |
| /// This uses pre-calculated metrics to compute the final positioning transform for a COLR glyph, |
| /// taking into account: |
| /// - Y-axis flip (fonts use upside-down coordinate system) |
| /// - Scale compensation (to avoid double-application of run transform scale) |
| /// - Bounding box alignment |
| fn calculate_colr_transform(metrics: &ColrMetrics) -> Affine { |
| metrics.transform |
| // There are two things going on here: |
| // - On the one hand, for images, the position (0, 0) will be at the top-left, while |
| // for images, the position will be at the bottom-left. |
| // - COLR glyphs have a flipped y-axis, so in the intermediate image they will be |
| // upside down. |
| // Because of both of these, all we simply need to do is to flip the image on the y-axis. |
| // This will ensure that the glyph in the image isn't upside down anymore, and at the same |
| // time also flips from having the origin in the top-left to having the origin in the |
| // bottom-right. |
| * Affine::scale_non_uniform(1.0, -1.0) |
| // Overall, the whole pixmap is scaled by `scale_factor_x` and `scale_factor_y`. `scale_factor_x` |
| // and `scale_factor_y` are composed by the scale necessary to adjust for the glyph size, |
| // as well as the scale that has been applied to the whole glyph run. However, the scale |
| // of the whole glyph run will be applied later on in the render context. If |
| // we didn't do anything, the scales would be applied twice (see https://github.com/linebender/vello/pull/1370). |
| // Therefore, we apply another scale factor that unapplies the effect of the glyph run transform |
| // and only retains the transform necessary to account for the size of the glyph. |
| * Affine::scale_non_uniform( |
| metrics.font_size_scale / metrics.scale_factor_x, |
| metrics.font_size_scale / metrics.scale_factor_y, |
| ) |
| // Shift the pixmap back so that the bbox aligns with the original position |
| // of where the glyph should be placed. |
| * Affine::translate((metrics.scaled_bbox.x0, metrics.scaled_bbox.y0)) |
| } |
| |
| /// Create COLR glyph data with intermediate texture parameters. |
| /// |
| /// This uses pre-calculated metrics to create a `GlyphType::Colr` with all necessary |
| /// data for rendering to an intermediate texture. |
| fn create_colr_glyph<'a>( |
| font_ref: &'a FontRef<'a>, |
| metrics: &ColrMetrics, |
| color_glyph: skrifa::color::ColorGlyph<'a>, |
| normalized_coords: &'a [skrifa::instance::NormalizedCoord], |
| ) -> GlyphType<'a> { |
| let (pix_width, pix_height) = ( |
| metrics.scaled_bbox.width().ceil() as u16, |
| metrics.scaled_bbox.height().ceil() as u16, |
| ); |
| |
| let draw_transform = |
| // Shift everything so that the bbox starts at (0, 0) and the whole visible area of |
| // the glyph will be contained in the intermediate pixmap. |
| Affine::translate((-metrics.scaled_bbox.x0, -metrics.scaled_bbox.y0)) * |
| // Scale down to the actual size that the COLR glyph will have in device units. |
| Affine::scale_non_uniform(metrics.scale_factor_x, metrics.scale_factor_y); |
| |
| // The shift-back happens in `glyph_transform`, so here we can assume (0.0, 0.0) as the origin |
| // of the area we want to draw to. |
| let area = Rect::new( |
| 0.0, |
| 0.0, |
| metrics.scaled_bbox.width(), |
| metrics.scaled_bbox.height(), |
| ); |
| |
| let location = LocationRef::new(normalized_coords); |
| |
| GlyphType::Colr(Box::new(GlyphColr { |
| skrifa_glyph: color_glyph, |
| font_ref, |
| location, |
| area, |
| pix_width, |
| pix_height, |
| draw_transform, |
| has_non_default_blend: metrics.has_non_default_blend, |
| })) |
| } |
| |
| trait FontDataExt { |
| fn as_skrifa(&self) -> FontRef<'_>; |
| } |
| |
| impl FontDataExt for FontData { |
| fn as_skrifa(&self) -> FontRef<'_> { |
| FontRef::from_index(self.data.data(), self.index).unwrap() |
| } |
| } |
| |
| /// Rendering style for glyphs. |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| pub(crate) enum Style { |
| /// Fill the glyph. |
| Fill, |
| /// Stroke the glyph. |
| Stroke, |
| } |
| |
| /// A sequence of glyphs with shared rendering properties. |
| #[derive(Clone, Debug)] |
| pub struct GlyphRun<'a> { |
| /// Font for all glyphs in the run. |
| font: FontData, |
| /// Size of the font in pixels per em. |
| font_size: f32, |
| /// Synthetic embolden settings. |
| font_embolden: FontEmbolden, |
| /// Global transform. |
| transform: Affine, |
| /// Per-glyph transform. Use [`Affine::skew`] with horizontal-skew only to simulate italic |
| /// text. |
| glyph_transform: Option<Affine>, |
| /// Normalized variation coordinates for variable fonts. |
| normalized_coords: &'a [skrifa::instance::NormalizedCoord], |
| /// Controls whether font hinting is enabled. |
| hint: bool, |
| } |
| |
| struct PreparedGlyphRun<'a> { |
| /// The underlying font data. |
| font: FontData, |
| // The fact that we store `run_size` and `glyph_transform` here, as well |
| // as having more transforms and an effective font size inside of the `draw_props` field is pretty |
| // confusing, so here is a brief explanation: |
| // Basically, the reason why we need both `run_size` and `glyph_transform` here is that |
| // we need to store some of the original metadata in scene space for certain functionality |
| // (for example handling of underlines). |
| /// The original run size supplied by the caller. |
| run_size: f32, |
| /// Synthetic embolden settings. |
| font_embolden: FontEmbolden, |
| /// The original per-glyph transform supplied by the caller. |
| glyph_transform: Option<Affine>, |
| // Continuing the above comment, the problem is that we also need to precalculate data |
| // that is needed specifically for glyph rendering. This includes: |
| // 1) We need to concatenate run transform and glyph transform to compute the final transform |
| // for the glyph outline. |
| // 2) Whenever possible, we need to try to _absorb_ the font size into the draw transform, |
| // such that we can just use the font size to uniquely identify a glyph cache hit (for example, |
| // if we draw a glyph at font size 12 with scale 2, it's the same as drawing the glyph at font size 24). |
| // While it would make things easier to just use the cache key in the transform and accept less |
| // caching potential for easier code, we would still need scaling absorption to implement proper |
| // hinting. Hence, it makes sense to just generalize the whole absorption procedure. |
| // In any case, since we do scaling absorption, we cannot use `run_size`, `GlyphRun::transform` and |
| // `glyph_transform` for glyph drawing purposes anymore. In particular, it can easily happen |
| // that |
| // 1) `run_size` != `draw_props.font_size` |
| // 2) `run_transform` * `glyph_transform` != `draw_props.effective_transform`. |
| // Therefore, we need to track a separate set of fields for glyph-drawing operations. |
| /// Properties for turning glyph-local positions into final draw transforms. |
| draw_props: DrawProps, |
| normalized_coords: &'a [skrifa::instance::NormalizedCoord], |
| hinting_instance: Option<&'a HintingInstance>, |
| } |
| |
| /// Properties for easily calculating the transform of a positioned glyph. |
| #[derive(Clone, Copy, Debug)] |
| struct DrawProps { |
| // Why do we need two separate transforms? Fundamentally, the problem is that the order |
| // of application should be: |
| // `run_transform` * `glyph_position` * `font_size` * `glyph_transform`. |
| // As part of absorption, we are only left with a potentially new `font_size` and a merged |
| // `effective_transform`. However, the translation that results form `glyph_position` logically |
| // needs to be applied after `run_transform` but before `glyph_transform`. |
| // Therefore, we need to store two separate transforms: One that is used only to transform |
| // the original glyph position, and another one that is used to actually transform the glyph |
| // outlines. |
| /// A positioning transform for the glyph. |
| positioning_transform: Affine, |
| /// A transform to apply to the glyph after positioning. |
| effective_transform: Affine, |
| /// The actual font size that should be assumed for drawing and caching |
| /// purposes. |
| font_size: f32, |
| } |
| |
| impl DrawProps { |
| #[inline] |
| fn positioned_transform(self, glyph: Glyph) -> Affine { |
| // First, determine the "coarse" location of the glyph by applying the scaling/skewing |
| // of the original run transform to the glyph position. Note that `positioning_transform` |
| // has a translation factor of zero (since it has been absorbed into `effective_transform`), so |
| // only the skewing and scaling factors are relevant. |
| let translation = self.positioning_transform * Point::new(glyph.x as f64, glyph.y as f64); |
| |
| // Now, apply the final draw transform on top of that, which will also consider |
| // the original glyph transform. |
| Affine::translate(translation.to_vec2()) * self.effective_transform |
| } |
| } |
| |
| impl Debug for PreparedGlyphRun<'_> { |
| fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { |
| // HintingInstance doesn't implement Debug so we have to do this manually :( |
| f.debug_struct("PreparedGlyphRun") |
| .field("font", &self.font) |
| .field("run_size", &self.run_size) |
| .field("font_embolden", &self.font_embolden) |
| .field("glyph_transform", &self.glyph_transform) |
| .field("transforms", &self.draw_props) |
| .field("normalized_coords", &self.normalized_coords) |
| .finish() |
| } |
| } |
| |
| /// Prepare a glyph run for rendering. |
| fn prepare_glyph_run<'a>(run: GlyphRun<'a>, hint_cache: &'a mut HintCache) -> PreparedGlyphRun<'a> { |
| let full_transform = run.transform * run.glyph_transform.unwrap_or(Affine::IDENTITY); |
| let [_, _, t_c, t_d, t_e, t_f] = full_transform.as_coeffs(); |
| |
| /// The mode that should be used to handle transforms. |
| #[derive(Clone, Copy, Debug)] |
| enum PreparedGlyphRunMode { |
| /// No absorption has happened, the font size stays the same and the effective transform |
| /// is simply the concatenation of run transform and glyph transform. |
| /// |
| /// No hinting should be applied. |
| Direct, |
| /// The scaling factor has been absorbed, and hinting should be applied. |
| AbsorbScaleUnhinted, |
| /// The scaling factor has been absorbed, but not hinting should be applied. |
| AbsorbScaleHinted, |
| } |
| |
| let mode = if !run.hint { |
| // TODO: We could explore generalizing this by decomposing the transform, such that |
| // we always absorb it, even if there is a skewing factor in the transform. This won't |
| // automatically make them eligible for caching because any skewing factor is currently |
| // rejected for caching, but it might make the code a bit more consistent. |
| if full_transform.is_positive_uniform_scale_without_skew() { |
| PreparedGlyphRunMode::AbsorbScaleUnhinted |
| } else { |
| PreparedGlyphRunMode::Direct |
| } |
| } else { |
| // We perform vertical-only hinting. |
| // |
| // Hinting doesn't make sense if we later scale the glyphs via some transform. So, similarly to |
| // normal glyph runs, we try to extract the scale. As is currently done for unhinted glyph runs, we |
| // also expect the scale to be uniform: Simply using the vertical scale as font |
| // size and then transforming by the relative horizontal scale can cause, e.g., overlapping |
| // glyphs. Note that this extracted scale should be later applied to the glyph's position. |
| // |
| // As the hinting is vertical-only, we can handle horizontal skew, but not vertical skew or |
| // rotations. |
| if full_transform.is_positive_uniform_scale_without_vertical_skew() { |
| PreparedGlyphRunMode::AbsorbScaleHinted |
| } else { |
| PreparedGlyphRunMode::Direct |
| } |
| }; |
| |
| let (effective_transform, draw_font_size, hinting_instance) = match mode { |
| PreparedGlyphRunMode::Direct => (full_transform, run.font_size, None), |
| PreparedGlyphRunMode::AbsorbScaleUnhinted => ( |
| Affine::new([1., 0., 0., 1., t_e, t_f]), |
| run.font_size * t_d as f32, |
| None, |
| ), |
| PreparedGlyphRunMode::AbsorbScaleHinted => { |
| let vertical_font_size = run.font_size * t_d as f32; |
| let font_ref = run.font.as_skrifa(); |
| let outlines = font_ref.outline_glyphs(); |
| let hinting_instance = hint_cache.get(&HintKey { |
| font_id: run.font.data.id(), |
| font_index: run.font.index, |
| outlines: &outlines, |
| size: vertical_font_size, |
| coords: run.normalized_coords, |
| }); |
| |
| ( |
| // The scale has been absorbed into the font size, so we need to remove it from the skew |
| // coefficient (t_c) as well. Otherwise the skew would be applied twice: once via the |
| // larger outline, once via the transform. The translation (t_e, t_f) stays as-is since |
| // it positions the run in scene coordinates. |
| Affine::new([1., 0., t_c / t_d, 1., t_e, t_f]), |
| vertical_font_size, |
| hinting_instance, |
| ) |
| } |
| }; |
| |
| PreparedGlyphRun { |
| font: run.font, |
| run_size: run.font_size, |
| font_embolden: run.font_embolden, |
| glyph_transform: run.glyph_transform, |
| draw_props: DrawProps { |
| positioning_transform: run |
| .transform |
| // Translation factor is already considered in `effective_transform`, so we need to remove |
| // it here. |
| .with_translation(Vec2::ZERO), |
| effective_transform, |
| font_size: draw_font_size, |
| }, |
| normalized_coords: run.normalized_coords, |
| hinting_instance, |
| } |
| } |
| |
| // TODO: Although these are sane defaults, we might want to make them |
| // configurable. |
| const HINTING_OPTIONS: HintingOptions = HintingOptions { |
| engine: skrifa::outline::Engine::AutoFallback, |
| target: skrifa::outline::Target::Smooth { |
| mode: skrifa::outline::SmoothMode::Lcd, |
| symmetric_rendering: false, |
| preserve_linear_metrics: true, |
| }, |
| }; |
| |
| #[derive(Clone, Default)] |
| pub(crate) struct OutlinePath { |
| pub(crate) path: BezPath, |
| pub(crate) bbox: Rect, |
| } |
| |
| impl OutlinePath { |
| pub(crate) fn new() -> Self { |
| Self { |
| path: BezPath::new(), |
| bbox: Rect { |
| x0: f64::INFINITY, |
| y0: f64::INFINITY, |
| x1: f64::NEG_INFINITY, |
| y1: f64::NEG_INFINITY, |
| }, |
| } |
| } |
| |
| pub(crate) fn reuse(&mut self) { |
| self.path.truncate(0); |
| self.bbox = Rect { |
| x0: f64::INFINITY, |
| y0: f64::INFINITY, |
| x1: f64::NEG_INFINITY, |
| y1: f64::NEG_INFINITY, |
| }; |
| } |
| } |
| |
| // Note that we flip the y-axis to match our coordinate system. |
| impl OutlinePen for OutlinePath { |
| #[inline] |
| fn move_to(&mut self, x: f32, y: f32) { |
| self.path.move_to((x, y)); |
| self.bbox = self.bbox.union_pt((x, y)); |
| } |
| |
| #[inline] |
| fn line_to(&mut self, x: f32, y: f32) { |
| self.path.line_to((x, y)); |
| self.bbox = self.bbox.union_pt((x, y)); |
| } |
| |
| #[inline] |
| fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { |
| self.path.curve_to((cx0, cy0), (cx1, cy1), (x, y)); |
| self.bbox = self.bbox.union_pt((cx0, cy0)); |
| self.bbox = self.bbox.union_pt((cx1, cy1)); |
| self.bbox = self.bbox.union_pt((x, y)); |
| } |
| |
| #[inline] |
| fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) { |
| self.path.quad_to((cx, cy), (x, y)); |
| self.bbox = self.bbox.union_pt((cx, cy)); |
| self.bbox = self.bbox.union_pt((x, y)); |
| } |
| |
| #[inline] |
| fn close(&mut self) { |
| self.path.close_path(); |
| } |
| } |
| |
| /// A normalized variation coordinate (for variable fonts) in 2.14 fixed point format. |
| /// |
| /// In most cases, this can be [cast](bytemuck::cast_slice) from the |
| /// normalised coords provided by your text layout library. |
| /// |
| /// Equivalent to [`skrifa::instance::NormalizedCoord`], but defined |
| /// in Glifo so that Skrifa is not part of Glifo's public API. |
| /// This allows Glifo to update its Skrifa in a patch release, and limits |
| /// the need for updates only to align Skrifa versions. |
| pub type NormalizedCoord = i16; |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| const _NORMALISED_COORD_SIZE_MATCHES: () = |
| assert!(size_of::<skrifa::instance::NormalizedCoord>() == size_of::<NormalizedCoord>()); |
| } |
| |
| /// Caches used for glyph rendering. |
| /// |
| /// Contains renderer-agnostic caches (outline paths, hinting instances) |
| /// alongside the glyph atlas bitmap cache. |
| // TODO: Consider capturing cache performance metrics like hit rate, etc. |
| #[derive(Debug, Default)] |
| pub struct GlyphCaches { |
| /// Caches glyph outlines (paths) for reuse. |
| pub(crate) outline_cache: OutlineCache, |
| /// Caches hinting instances for reuse. |
| pub(crate) hinting_cache: HintCache, |
| /// Horizontal spans excluded from "ink-skipping" underlines. Cached to reuse one allocation. |
| pub(crate) underline_exclusions: Vec<(f64, f64)>, |
| /// Caches rasterized glyph bitmaps in atlas pages. |
| pub(crate) glyph_atlas: GlyphAtlas, |
| } |
| |
| impl GlyphCaches { |
| /// Clears the glyph caches. |
| pub fn clear(&mut self) { |
| self.outline_cache.clear(); |
| self.hinting_cache.clear(); |
| self.underline_exclusions.clear(); |
| self.glyph_atlas.clear(); |
| } |
| |
| /// Maintains the glyph caches by evicting unused cache entries. |
| /// |
| /// The `image_cache` must be the same allocator passed to |
| /// `GlyphRunBuilder::build` so that evicted entries are deallocated from |
| /// the correct allocator. |
| /// |
| /// Should be called once per scene rendering. |
| pub fn maintain(&mut self, image_cache: &mut ImageCache) { |
| self.outline_cache.maintain(); |
| self.glyph_atlas.maintain(image_cache); |
| } |
| } |
| |
| #[derive(Copy, Clone, PartialEq, Eq, Hash, Default, Debug)] |
| struct OutlineKey { |
| font_id: u64, |
| font_index: u32, |
| glyph_id: u32, |
| size_bits: u32, |
| embolden_x_bits: u32, |
| embolden_y_bits: u32, |
| embolden_join_bits: u8, |
| embolden_miter_limit_bits: u32, |
| embolden_tolerance_bits: u32, |
| hint: bool, |
| } |
| |
| #[inline(always)] |
| fn join_bits(join: Join) -> u8 { |
| match join { |
| Join::Bevel => 0, |
| Join::Miter => 1, |
| Join::Round => 2, |
| } |
| } |
| |
| #[expect( |
| clippy::cast_possible_truncation, |
| reason = "Cache keys intentionally store embolden parameters at f32 precision." |
| )] |
| #[inline(always)] |
| fn f32_bits(value: f64) -> u32 { |
| (value as f32).to_bits() |
| } |
| |
| struct OutlineEntry { |
| path: Arc<BezPath>, |
| bbox: Rect, |
| serial: u32, |
| } |
| |
| impl OutlineEntry { |
| fn new(path: Arc<BezPath>, bbox: Rect, serial: u32) -> Self { |
| Self { path, bbox, serial } |
| } |
| |
| /// Takes the inner `BezPath` out of this entry if the `Arc` is uniquely owned. |
| fn take_path(&mut self) -> Option<OutlinePath> { |
| let arc = core::mem::replace(&mut self.path, Arc::new(BezPath::new())); |
| Arc::try_unwrap(arc).ok().map(|path| OutlinePath { |
| path, |
| bbox: Rect::ZERO, |
| }) |
| } |
| } |
| |
| /// A cached outline glyph path with its approximate bounding box. |
| pub(crate) struct CachedOutline<'a> { |
| pub(crate) path: &'a Arc<BezPath>, |
| pub(crate) bbox: Rect, |
| } |
| |
| /// Caches glyph outlines for reuse. |
| /// Heavily inspired by `vello_encoding::glyph_cache`. |
| #[derive(Default)] |
| pub struct OutlineCache { |
| free_list: Vec<OutlinePath>, |
| static_map: HashMap<OutlineKey, OutlineEntry>, |
| variable_map: HashMap<VarKey, HashMap<OutlineKey, OutlineEntry>>, |
| cached_count: usize, |
| serial: u32, |
| last_prune_serial: u32, |
| } |
| |
| impl Debug for OutlineCache { |
| fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { |
| f.debug_struct("OutlineCache") |
| .field("free_list", &self.free_list.len()) |
| .field("static_map", &self.static_map.len()) |
| .field("variable_map", &self.variable_map.len()) |
| .field("cached_count", &self.cached_count) |
| .field("serial", &self.serial) |
| .field("last_prune_serial", &self.last_prune_serial) |
| .finish() |
| } |
| } |
| |
| impl OutlineCache { |
| /// Maintains the outline cache by evicting unused cache entries. |
| /// |
| /// Should be called once per scene rendering. |
| pub fn maintain(&mut self) { |
| // Maximum number of full renders where we'll retain an unused glyph |
| const MAX_ENTRY_AGE: u32 = 64; |
| // Maximum number of full renders before we force a prune |
| const PRUNE_FREQUENCY: u32 = 64; |
| // Always prune if the cached count is greater than this value |
| const CACHED_COUNT_THRESHOLD: usize = 256; |
| // Number of encoding buffers we'll keep on the free list |
| const MAX_FREE_LIST_SIZE: usize = 128; |
| |
| let free_list = &mut self.free_list; |
| let serial = self.serial; |
| self.serial += 1; |
| // Don't iterate over the whole cache every frame |
| if serial - self.last_prune_serial < PRUNE_FREQUENCY |
| && self.cached_count < CACHED_COUNT_THRESHOLD |
| { |
| return; |
| } |
| self.last_prune_serial = serial; |
| self.static_map.retain(|_, entry| { |
| if serial - entry.serial > MAX_ENTRY_AGE { |
| if free_list.len() < MAX_FREE_LIST_SIZE { |
| // Try to recover the inner BezPath for reuse as a drawing buffer. |
| // This succeeds when the Arc has no other owners (refcount == 1). |
| if let Some(path) = entry.take_path() { |
| free_list.push(path); |
| } |
| } |
| self.cached_count -= 1; |
| false |
| } else { |
| true |
| } |
| }); |
| self.variable_map.retain(|_, map| { |
| map.retain(|_, entry| { |
| if serial - entry.serial > MAX_ENTRY_AGE { |
| if free_list.len() < MAX_FREE_LIST_SIZE |
| && let Some(path) = entry.take_path() |
| { |
| free_list.push(path); |
| } |
| self.cached_count -= 1; |
| false |
| } else { |
| true |
| } |
| }); |
| !map.is_empty() |
| }); |
| } |
| |
| /// Clears the outline cache. |
| pub fn clear(&mut self) { |
| self.free_list.clear(); |
| self.static_map.clear(); |
| self.variable_map.clear(); |
| self.cached_count = 0; |
| self.serial = 0; |
| self.last_prune_serial = 0; |
| } |
| } |
| |
| struct OutlineCacheSession<'a> { |
| map: &'a mut HashMap<OutlineKey, OutlineEntry>, |
| free_list: &'a mut Vec<OutlinePath>, |
| serial: u32, |
| cached_count: &'a mut usize, |
| } |
| |
| impl<'a> OutlineCacheSession<'a> { |
| fn new(outline_cache: &'a mut OutlineCache, var_key: VarLookupKey<'_>) -> Self { |
| let map = if var_key.0.is_empty() { |
| &mut outline_cache.static_map |
| } else { |
| match outline_cache |
| .variable_map |
| .raw_entry_mut() |
| .from_key(&var_key) |
| { |
| RawEntryMut::Occupied(entry) => entry.into_mut(), |
| RawEntryMut::Vacant(entry) => entry.insert(var_key.into(), HashMap::new()).1, |
| } |
| }; |
| Self { |
| map, |
| free_list: &mut outline_cache.free_list, |
| serial: outline_cache.serial, |
| cached_count: &mut outline_cache.cached_count, |
| } |
| } |
| |
| fn get_or_insert( |
| &mut self, |
| glyph_id: u32, |
| font_id: u64, |
| font_index: u32, |
| size: f32, |
| embolden: FontEmbolden, |
| var_key: VarLookupKey<'_>, |
| outline_glyph: &skrifa::outline::OutlineGlyph<'_>, |
| hinting_instance: Option<&HintingInstance>, |
| ) -> CachedOutline<'_> { |
| let key = OutlineKey { |
| glyph_id, |
| font_id, |
| font_index, |
| size_bits: size.to_bits(), |
| embolden_x_bits: f32_bits(embolden.amount.xx), |
| embolden_y_bits: f32_bits(embolden.amount.yy), |
| embolden_join_bits: join_bits(embolden.join), |
| embolden_miter_limit_bits: f32_bits(embolden.miter_limit), |
| embolden_tolerance_bits: f32_bits(embolden.tolerance), |
| hint: hinting_instance.is_some(), |
| }; |
| |
| match self.map.entry(key) { |
| Entry::Occupied(mut entry) => { |
| entry.get_mut().serial = self.serial; |
| let entry = entry.into_mut(); |
| CachedOutline { |
| path: &entry.path, |
| bbox: entry.bbox, |
| } |
| } |
| Entry::Vacant(entry) => { |
| // Pop a drawing buffer from the free list (or create a new one). |
| let mut drawing_buf = self.free_list.pop().unwrap_or_default(); |
| |
| let draw_settings = if let Some(hinting_instance) = hinting_instance { |
| DrawSettings::hinted(hinting_instance, false) |
| } else { |
| DrawSettings::unhinted(Size::new(size), var_key.0) |
| }; |
| |
| drawing_buf.reuse(); |
| outline_glyph.draw(draw_settings, &mut drawing_buf).unwrap(); |
| if embolden.amount != Diagonal2::new(0.0, 0.0) { |
| drawing_buf.path = kurbo::expand_path( |
| &drawing_buf.path, |
| embolden.amount, |
| embolden.join, |
| embolden.miter_limit, |
| embolden.tolerance, |
| ); |
| drawing_buf.bbox = drawing_buf.path.bounding_box(); |
| } |
| |
| let bbox = drawing_buf.bbox; |
| let entry = entry.insert(OutlineEntry::new( |
| Arc::new(drawing_buf.path), |
| bbox, |
| self.serial, |
| )); |
| *self.cached_count += 1; |
| CachedOutline { |
| path: &entry.path, |
| bbox: entry.bbox, |
| } |
| } |
| } |
| } |
| } |
| |
| /// Key for variable font caches. |
| type VarKey = SmallVec<[skrifa::instance::NormalizedCoord; 4]>; |
| |
| /// Lookup key for variable font caches. |
| #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] |
| struct VarLookupKey<'a>(&'a [skrifa::instance::NormalizedCoord]); |
| |
| impl Equivalent<VarKey> for VarLookupKey<'_> { |
| fn equivalent(&self, other: &VarKey) -> bool { |
| self.0 == other.as_slice() |
| } |
| } |
| |
| impl From<VarLookupKey<'_>> for VarKey { |
| fn from(key: VarLookupKey<'_>) -> Self { |
| Self::from_slice(key.0) |
| } |
| } |
| |
| /// We keep this small to enable a simple LRU cache with a linear |
| /// search. Regenerating hinting data is low to medium cost so it's fine |
| /// to redo it occasionally. |
| const MAX_CACHED_HINT_INSTANCES: usize = 16; |
| |
| /// Hint key for hinting instances. |
| #[derive(Debug)] |
| pub struct HintKey<'a> { |
| font_id: u64, |
| font_index: u32, |
| outlines: &'a OutlineGlyphCollection<'a>, |
| size: f32, |
| coords: &'a [skrifa::instance::NormalizedCoord], |
| } |
| |
| impl HintKey<'_> { |
| fn instance(&self) -> Option<HintingInstance> { |
| HintingInstance::new( |
| self.outlines, |
| Size::new(self.size), |
| self.coords, |
| HINTING_OPTIONS, |
| ) |
| .ok() |
| } |
| } |
| |
| /// LRU cache for hinting instances. |
| /// |
| /// Heavily inspired by `vello_encoding::glyph_cache`. |
| #[derive(Default)] |
| pub struct HintCache { |
| // Split caches for glyf/cff because the instance type can reuse |
| // internal memory when reconfigured for the same format. |
| glyf_entries: Vec<HintEntry>, |
| cff_entries: Vec<HintEntry>, |
| varc_entries: Vec<HintEntry>, |
| serial: u64, |
| } |
| |
| impl Debug for HintCache { |
| fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { |
| f.debug_struct("HintCache") |
| .field("glyf_entries", &self.glyf_entries.len()) |
| .field("cff_entries", &self.cff_entries.len()) |
| .field("varc_entries", &self.varc_entries.len()) |
| .field("serial", &self.serial) |
| .finish() |
| } |
| } |
| |
| impl HintCache { |
| /// Gets a hinting instance for the given key. |
| pub fn get(&mut self, key: &HintKey<'_>) -> Option<&HintingInstance> { |
| let entries = match key.outlines.format()? { |
| OutlineGlyphFormat::Glyf => &mut self.glyf_entries, |
| OutlineGlyphFormat::Cff | OutlineGlyphFormat::Cff2 => &mut self.cff_entries, |
| OutlineGlyphFormat::Varc => &mut self.varc_entries, |
| }; |
| let (entry_ix, is_current) = find_hint_entry(entries, key)?; |
| let entry = entries.get_mut(entry_ix)?; |
| self.serial += 1; |
| entry.serial = self.serial; |
| if !is_current { |
| entry.font_id = key.font_id; |
| entry.font_index = key.font_index; |
| entry |
| .instance |
| .reconfigure( |
| key.outlines, |
| Size::new(key.size), |
| key.coords, |
| HINTING_OPTIONS, |
| ) |
| .ok()?; |
| } |
| Some(&entry.instance) |
| } |
| |
| /// Clears the hint cache. |
| pub fn clear(&mut self) { |
| self.glyf_entries.clear(); |
| self.cff_entries.clear(); |
| self.varc_entries.clear(); |
| self.serial = 0; |
| } |
| } |
| |
| struct HintEntry { |
| font_id: u64, |
| font_index: u32, |
| instance: HintingInstance, |
| serial: u64, |
| } |
| |
| fn find_hint_entry(entries: &mut Vec<HintEntry>, key: &HintKey<'_>) -> Option<(usize, bool)> { |
| let mut found_serial = u64::MAX; |
| let mut found_index = 0; |
| for (ix, entry) in entries.iter().enumerate() { |
| if entry.font_id == key.font_id |
| && entry.font_index == key.font_index |
| && entry.instance.size() == Size::new(key.size) |
| && entry.instance.location().coords() == key.coords |
| { |
| return Some((ix, true)); |
| } |
| if entry.serial < found_serial { |
| found_serial = entry.serial; |
| found_index = ix; |
| } |
| } |
| if entries.len() < MAX_CACHED_HINT_INSTANCES { |
| let instance = key.instance()?; |
| let ix = entries.len(); |
| entries.push(HintEntry { |
| font_id: key.font_id, |
| font_index: key.font_index, |
| instance, |
| // This should be updated by the caller. |
| serial: 0, |
| }); |
| Some((ix, true)) |
| } else { |
| Some((found_index, false)) |
| } |
| } |
| |
| fn x_y_advances(transform: &Affine) -> (Vec2, Vec2) { |
| let scale_skew_transform = { |
| let c = transform.as_coeffs(); |
| Affine::new([c[0], c[1], c[2], c[3], 0.0, 0.0]) |
| }; |
| |
| let x_advance = scale_skew_transform * Point::new(1.0, 0.0); |
| let y_advance = scale_skew_transform * Point::new(0.0, 1.0); |
| |
| ( |
| Vec2::new(x_advance.x, x_advance.y), |
| Vec2::new(y_advance.x, y_advance.y), |
| ) |
| } |