Skip to main content

glifo/
renderer.rs

1// Copyright 2026 the Vello Authors and the Parley Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Shared glyph rendering logic for rendering backends.
5
6use crate::atlas::commands::{AtlasCommand, AtlasCommandRecorder};
7use crate::atlas::key::subpixel_offset;
8use crate::atlas::{AtlasSlot, GlyphAtlas, GlyphCacheKey, ImageCache, RasterMetrics};
9use crate::colr::ColrPainter;
10use crate::glyph::{
11    AtlasCacher, CachedGlyphType, GlyphBitmap, GlyphColr, GlyphOutline, GlyphType,
12    OutlineCacheSession, PreparedGlyph,
13};
14use crate::interface::{DrawSink, GlyphRenderer};
15use crate::util::AffineExt;
16use crate::{kurbo, peniko};
17use alloc::sync::Arc;
18use alloc::vec::Vec;
19#[cfg(not(feature = "std"))]
20use core_maths::CoreFloat as _;
21use kurbo::{Affine, BezPath, Rect, Shape};
22use peniko::color::palette::css::BLACK;
23use peniko::color::{AlphaColor, Srgb};
24use peniko::{Extend, ImageQuality, ImageSampler};
25use vello_common::paint::{Image, ImageSource, Tint, TintMode};
26
27/// Outcome of a cache-first render attempt.
28///
29/// Callers use the variant to decide whether to fall back to direct rendering.
30enum CacheResult {
31    /// Glyph was rasterised, stored in the atlas, and drawn. No fallback needed.
32    CachedAndRendered,
33    /// Transform contains rotation or skew — cannot be cached at a single
34    /// raster resolution, so the caller must render directly.
35    UnsupportedTransform,
36    /// Atlas allocator could not fit the glyph (page full, eviction didn't
37    /// free enough space, or the glyph exceeds the page dimensions).
38    AtlasFull,
39}
40
41/// Fill a prepared glyph, using the glyph atlas when possible and falling
42/// back to direct rendering otherwise.
43pub(crate) fn fill_glyph(
44    renderer: &mut impl GlyphRenderer,
45    prepared_glyph: PreparedGlyph<'_>,
46    atlas_cacher: &mut AtlasCacher<'_>,
47    outline_cache: &mut OutlineCacheSession<'_>,
48) {
49    let AtlasCacher::Enabled(glyph_atlas, image_cache) = atlas_cacher else {
50        let transform = prepared_glyph.outline_transform;
51        let paint_transform = prepared_glyph.relative_paint_transform;
52
53        return match prepared_glyph.glyph_type {
54            GlyphType::Outline(glyph) => {
55                fill_uncached_outline_glyph(
56                    renderer,
57                    &glyph.path,
58                    glyph.scale,
59                    transform,
60                    paint_transform,
61                );
62            }
63            GlyphType::Bitmap(glyph) => render_uncached_bitmap_glyph(renderer, glyph, transform),
64            GlyphType::Colr(glyph) => {
65                let context_color = renderer.get_context_color();
66                render_uncached_colr_glyph(
67                    renderer,
68                    &glyph,
69                    transform,
70                    context_color,
71                    outline_cache,
72                );
73            }
74        };
75    };
76
77    let mut cache_key = prepared_glyph.cache_key;
78    let transform = prepared_glyph.outline_transform;
79    let paint_transform = prepared_glyph.relative_paint_transform;
80
81    match prepared_glyph.glyph_type {
82        GlyphType::Outline(glyph) => {
83            let tint_color = renderer.get_context_color();
84            if let Some(key) = cache_key.take()
85                && let CacheResult::CachedAndRendered = insert_and_render_outline(
86                    renderer,
87                    &glyph,
88                    transform,
89                    key,
90                    glyph_atlas,
91                    image_cache,
92                    tint_color,
93                )
94            {
95                return;
96            }
97
98            fill_uncached_outline_glyph(
99                renderer,
100                &glyph.path,
101                glyph.scale,
102                transform,
103                paint_transform,
104            );
105        }
106        GlyphType::Bitmap(glyph) => {
107            if let Some(key) = cache_key.take()
108                && let CacheResult::CachedAndRendered = insert_and_render_bitmap(
109                    renderer,
110                    &glyph,
111                    transform,
112                    key,
113                    glyph_atlas,
114                    image_cache,
115                )
116            {
117                return;
118            }
119
120            render_uncached_bitmap_glyph(renderer, glyph, transform);
121        }
122        GlyphType::Colr(glyph) => {
123            if let Some(key) = cache_key.take()
124                && let CacheResult::CachedAndRendered = insert_and_render_colr(
125                    renderer,
126                    &glyph,
127                    transform,
128                    key,
129                    glyph_atlas,
130                    image_cache,
131                    outline_cache,
132                )
133            {
134                return;
135            }
136
137            let context_color = renderer.get_context_color();
138            render_uncached_colr_glyph(renderer, &glyph, transform, context_color, outline_cache);
139        }
140    }
141}
142
143/// Stroke a prepared glyph, using the glyph atlas when possible and falling
144/// back to direct rendering otherwise.
145pub(crate) fn stroke_glyph(
146    renderer: &mut impl GlyphRenderer,
147    prepared_glyph: PreparedGlyph<'_>,
148    atlas_cacher: &mut AtlasCacher<'_>,
149    outline_cache: &mut OutlineCacheSession<'_>,
150) {
151    let AtlasCacher::Enabled(glyph_atlas, image_cache) = atlas_cacher else {
152        let outline_transform = prepared_glyph.outline_transform;
153        let paint_transform = prepared_glyph.relative_paint_transform;
154        return match prepared_glyph.glyph_type {
155            GlyphType::Outline(glyph) => {
156                stroke_uncached_outline_glyph(
157                    renderer,
158                    &glyph.path,
159                    glyph.scale,
160                    outline_transform,
161                    paint_transform,
162                );
163            }
164            GlyphType::Bitmap(_) | GlyphType::Colr(_) => {
165                fill_glyph(renderer, prepared_glyph, atlas_cacher, outline_cache);
166            }
167        };
168    };
169
170    match prepared_glyph.glyph_type {
171        GlyphType::Outline(glyph) => {
172            let mut cache_key = prepared_glyph.cache_key;
173            let outline_transform = prepared_glyph.outline_transform;
174            let paint_transform = prepared_glyph.relative_paint_transform;
175            let tint_color = renderer.get_context_color();
176
177            if let Some(key) = cache_key.take()
178                && let CacheResult::CachedAndRendered = insert_and_render_outline(
179                    renderer,
180                    &glyph,
181                    outline_transform,
182                    key,
183                    glyph_atlas,
184                    image_cache,
185                    tint_color,
186                )
187            {
188                return;
189            }
190
191            stroke_uncached_outline_glyph(
192                renderer,
193                &glyph.path,
194                glyph.scale,
195                outline_transform,
196                paint_transform,
197            );
198        }
199        GlyphType::Bitmap(_) | GlyphType::Colr(_) => {
200            fill_glyph(renderer, prepared_glyph, atlas_cacher, outline_cache);
201        }
202    }
203}
204
205fn fill_uncached_outline_glyph(
206    renderer: &mut impl GlyphRenderer,
207    path: &BezPath,
208    scale: f64,
209    outline_transform: Affine,
210    paint_transform: Affine,
211) {
212    let state = renderer.save_state();
213    renderer.set_transform(outline_transform.pre_scale(scale));
214    renderer.set_paint_transform(paint_transform);
215    renderer.fill_path(path);
216    renderer.restore_state(state);
217}
218
219fn stroke_uncached_outline_glyph(
220    renderer: &mut impl GlyphRenderer,
221    path: &BezPath,
222    scale: f64,
223    outline_transform: Affine,
224    paint_transform: Affine,
225) {
226    let state = renderer.save_state();
227    renderer.set_transform(outline_transform.pre_scale(scale));
228    renderer.set_paint_transform(paint_transform);
229    renderer.stroke_path(path);
230    renderer.restore_state(state);
231}
232
233fn render_uncached_bitmap_glyph(
234    renderer: &mut impl GlyphRenderer,
235    glyph: GlyphBitmap,
236    outline_transform: Affine,
237) {
238    let image = Image {
239        image: ImageSource::Pixmap(glyph.pixmap),
240        sampler: ImageSampler {
241            x_extend: Extend::Pad,
242            y_extend: Extend::Pad,
243            quality: quality_for_scale(&outline_transform),
244            alpha: 1.0,
245        },
246    };
247
248    let state = renderer.save_state();
249    renderer.set_transform(outline_transform);
250    renderer.set_paint_image(image);
251    renderer.fill_rect(&glyph.area);
252    renderer.restore_state(state);
253}
254
255fn render_uncached_colr_glyph(
256    renderer: &mut impl GlyphRenderer,
257    glyph: &GlyphColr<'_>,
258    outline_transform: Affine,
259    context_color: AlphaColor<Srgb>,
260    outline_cache: &mut OutlineCacheSession<'_>,
261) {
262    let state = renderer.save_state();
263    renderer.set_transform(outline_transform);
264    // Two reasons why we wrap COLR glyphs in a clip layer:
265    // 1) We need a layer to make sure they are isolated and don't blend into the main surface (unless
266    // the glyph is guaranteed to only use default blending, in which case we don't need this).
267    // Otherwise, blend modes that are part of the glyph could affect already drawn contents.
268    // 2) We do the clipping as a temporary measure to allow the Vello renderers to get a bounding box
269    // of the glyph, necessary to keep the cost of blending operations with
270    // destructive blend modes to a minimum.
271    if glyph.has_non_default_blend {
272        renderer.push_clip_layer(&glyph.area.to_path(0.1));
273    } else {
274        renderer.push_clip_path(&glyph.area.to_path(0.1));
275    }
276
277    // TODO: Maybe ColrPainter can be reused across glyphs?
278    let mut colr_painter = ColrPainter::new(glyph, context_color, renderer, outline_cache);
279    colr_painter.paint();
280    if glyph.has_non_default_blend {
281        renderer.pop_layer();
282    } else {
283        renderer.pop_clip_path();
284    }
285
286    renderer.restore_state(state);
287}
288
289/// Render a cached glyph from the atlas.
290pub(crate) fn render_cached_glyph(
291    renderer: &mut impl GlyphRenderer,
292    cached_slot: AtlasSlot,
293    transform: Affine,
294    glyph_type: CachedGlyphType,
295) {
296    match glyph_type {
297        CachedGlyphType::Outline => {
298            let tint = renderer.get_context_color();
299            render_outline_glyph_from_atlas(renderer, cached_slot, transform, tint);
300        }
301        CachedGlyphType::Bitmap => {
302            render_bitmap_glyph_from_atlas(renderer, cached_slot, transform);
303        }
304        CachedGlyphType::Colr(area) => {
305            render_colr_glyph_from_atlas(renderer, cached_slot, transform, area);
306        }
307    }
308}
309
310/// Render from the atlas, constructing the appropriate image from the slot.
311fn render_from_atlas(
312    renderer: &mut impl GlyphRenderer,
313    atlas_slot: AtlasSlot,
314    rect_transform: Affine,
315    area: Rect,
316    quality: ImageQuality,
317    tint: Option<Tint>,
318) {
319    let paint_transform = renderer.atlas_paint_transform(&atlas_slot);
320    let image_source = renderer.atlas_image_source(&atlas_slot);
321    let image = Image {
322        image: image_source,
323        sampler: ImageSampler {
324            x_extend: Extend::Pad,
325            y_extend: Extend::Pad,
326            quality,
327            alpha: 1.0,
328        },
329    };
330
331    let state = renderer.save_state();
332    renderer.set_tint(tint);
333    renderer.set_transform(rect_transform);
334    renderer.set_paint_image(image);
335    renderer.set_paint_transform(paint_transform);
336    renderer.fill_rect(&area);
337    renderer.set_tint(None);
338    renderer.restore_state(state);
339}
340
341/// Record outline glyph draw commands into the atlas command recorder.
342fn render_outline_to_atlas(
343    path: &Arc<BezPath>,
344    scale: f64,
345    subpixel_offset: f32,
346    recorder: &mut AtlasCommandRecorder,
347    atlas_slot: AtlasSlot,
348    raster_metrics: RasterMetrics,
349) {
350    let outline_transform =
351        Affine::scale_non_uniform(scale, -scale).then_translate(kurbo::Vec2::new(
352            atlas_slot.x as f64 - raster_metrics.bearing_x as f64 + subpixel_offset as f64,
353            atlas_slot.y as f64 - raster_metrics.bearing_y as f64,
354        ));
355    recorder.set_transform(outline_transform);
356    recorder.set_paint(BLACK.into());
357    recorder.fill_path(path);
358}
359
360/// Record COLR glyph draw commands into the atlas command recorder.
361fn render_colr_to_atlas(
362    glyph: &GlyphColr<'_>,
363    context_color: AlphaColor<Srgb>,
364    recorder: &mut AtlasCommandRecorder,
365    atlas_slot: AtlasSlot,
366    outline_cache: &mut OutlineCacheSession<'_>,
367) {
368    recorder.set_transform(Affine::translate((
369        atlas_slot.x as f64,
370        atlas_slot.y as f64,
371    )));
372    // See the comment in `render_uncached_colr_glyph` for why we wrap COLR glyphs
373    // in a clip layer.
374    if glyph.has_non_default_blend {
375        recorder.push_clip_layer(&glyph.area.to_path(0.1));
376    } else {
377        recorder.push_clip_path(&glyph.area.to_path(0.1));
378    }
379
380    // TODO: Maybe ColrPainter can be reused across glyphs?
381    let mut colr_painter = ColrPainter::new(glyph, context_color, recorder, outline_cache);
382    colr_painter.paint();
383
384    if glyph.has_non_default_blend {
385        recorder.pop_layer();
386    } else {
387        recorder.pop_clip_path();
388    }
389}
390
391/// Insert an outline glyph into the atlas and render it from there.
392///
393/// Allocates atlas space (the insert returns the per-page command recorder)
394/// and records rasterisation commands. The upstream caller is responsible for
395/// checking the cache first and only calling this on a miss.
396fn insert_and_render_outline(
397    renderer: &mut impl GlyphRenderer,
398    glyph: &GlyphOutline,
399    outline_transform: Affine,
400    cache_key: GlyphCacheKey,
401    glyph_atlas: &mut GlyphAtlas,
402    image_cache: &mut ImageCache,
403    tint_color: AlphaColor<Srgb>,
404) -> CacheResult {
405    if !supports_atlas_caching(&outline_transform, CachedGlyphType::Outline) {
406        return CacheResult::UnsupportedTransform;
407    }
408
409    let bounds = glyph.bbox.scale_from_origin(glyph.scale);
410    let raster_metrics = calculate_raster_metrics(&bounds);
411
412    let subpixel_offset = subpixel_offset(cache_key.subpixel_x);
413
414    let Some((atlas_slot, recorder)) = glyph_atlas.insert(image_cache, cache_key, raster_metrics)
415    else {
416        return CacheResult::AtlasFull;
417    };
418
419    render_outline_to_atlas(
420        &glyph.path,
421        glyph.scale,
422        subpixel_offset,
423        recorder,
424        atlas_slot,
425        raster_metrics,
426    );
427
428    render_outline_glyph_from_atlas(renderer, atlas_slot, outline_transform, tint_color);
429    CacheResult::CachedAndRendered
430}
431
432fn insert_and_render_bitmap(
433    renderer: &mut impl GlyphRenderer,
434    glyph: &GlyphBitmap,
435    transform: Affine,
436    cache_key: GlyphCacheKey,
437    glyph_atlas: &mut GlyphAtlas,
438    image_cache: &mut ImageCache,
439) -> CacheResult {
440    if !supports_atlas_caching(&transform, CachedGlyphType::Bitmap) {
441        return CacheResult::UnsupportedTransform;
442    }
443
444    let width = glyph.pixmap.width();
445    let height = glyph.pixmap.height();
446
447    let raster_metrics = RasterMetrics {
448        width,
449        height,
450        bearing_x: 0,
451        bearing_y: 0,
452    };
453
454    // Bitmap glyphs already have pixel data — no draw commands to record,
455    // so we discard the returned recorder.
456    let Some((atlas_slot, _)) = glyph_atlas.insert(image_cache, cache_key, raster_metrics) else {
457        return CacheResult::AtlasFull;
458    };
459
460    // Both backends defer the actual pixel copy/upload; it completes before
461    // the render pass that resolves image references.
462    glyph_atlas.push_pending_upload(atlas_slot.image_id, Arc::clone(&glyph.pixmap), atlas_slot);
463
464    render_from_atlas(
465        renderer,
466        atlas_slot,
467        transform,
468        glyph.area,
469        quality_for_scale(&transform),
470        None,
471    );
472    CacheResult::CachedAndRendered
473}
474
475fn insert_and_render_colr(
476    renderer: &mut impl GlyphRenderer,
477    glyph: &GlyphColr<'_>,
478    outline_transform: Affine,
479    cache_key: GlyphCacheKey,
480    glyph_atlas: &mut GlyphAtlas,
481    image_cache: &mut ImageCache,
482    outline_cache: &mut OutlineCacheSession<'_>,
483) -> CacheResult {
484    if !supports_atlas_caching(&outline_transform, CachedGlyphType::Colr(Rect::ZERO)) {
485        return CacheResult::UnsupportedTransform;
486    }
487
488    let width = glyph.pix_width;
489    let height = glyph.pix_height;
490
491    let raster_metrics = RasterMetrics {
492        width,
493        height,
494        bearing_x: 0,
495        bearing_y: 0,
496    };
497
498    let area = glyph.area;
499
500    let context_color = cache_key.context_color;
501    let Some((atlas_slot, recorder)) = glyph_atlas.insert(image_cache, cache_key, raster_metrics)
502    else {
503        return CacheResult::AtlasFull;
504    };
505
506    render_colr_to_atlas(glyph, context_color, recorder, atlas_slot, outline_cache);
507
508    render_from_atlas(
509        renderer,
510        atlas_slot,
511        outline_transform,
512        area,
513        quality_for_skew(&outline_transform),
514        None,
515    );
516    CacheResult::CachedAndRendered
517}
518
519/// Render an outline glyph from the atlas using bearing-based positioning.
520#[inline]
521fn render_outline_glyph_from_atlas(
522    renderer: &mut impl GlyphRenderer,
523    atlas_slot: AtlasSlot,
524    outline_transform: Affine,
525    tint_color: AlphaColor<Srgb>,
526) {
527    let [_, _, _, _, tx, ty] = outline_transform.as_coeffs();
528    let rect_transform = Affine::translate((
529        tx.floor() + atlas_slot.bearing_x as f64,
530        ty.floor() + atlas_slot.bearing_y as f64,
531    ));
532    let area = Rect::new(0.0, 0.0, atlas_slot.width as f64, atlas_slot.height as f64);
533    render_from_atlas(
534        renderer,
535        atlas_slot,
536        rect_transform,
537        area,
538        ImageQuality::Low,
539        Some(Tint {
540            color: tint_color,
541            mode: TintMode::AlphaMask,
542        }),
543    );
544}
545
546/// Render a bitmap glyph from the atlas cache.
547#[inline]
548fn render_bitmap_glyph_from_atlas(
549    renderer: &mut impl GlyphRenderer,
550    atlas_slot: AtlasSlot,
551    transform: Affine,
552) {
553    let area = Rect::new(0.0, 0.0, atlas_slot.width as f64, atlas_slot.height as f64);
554    render_from_atlas(
555        renderer,
556        atlas_slot,
557        transform,
558        area,
559        quality_for_scale(&transform),
560        None,
561    );
562}
563
564/// Render a COLR glyph from the atlas cache.
565///
566/// This version accepts a pre-calculated fractional area to preserve
567/// sub-pixel accuracy during rendering, avoiding scaling artifacts.
568#[inline]
569fn render_colr_glyph_from_atlas(
570    renderer: &mut impl GlyphRenderer,
571    atlas_slot: AtlasSlot,
572    transform: Affine,
573    area: Rect,
574) {
575    render_from_atlas(
576        renderer,
577        atlas_slot,
578        transform,
579        area,
580        quality_for_skew(&transform),
581        None,
582    );
583}
584
585/// Calculate raster metrics (pixel bounds, bearings) from a glyph's bounding box.
586#[expect(
587    clippy::cast_possible_truncation,
588    reason = "glyph bounds fit in i32/u16/i16 at reasonable ppem values"
589)]
590#[inline]
591pub(crate) fn calculate_raster_metrics(bounds: &Rect) -> RasterMetrics {
592    // Floor/ceil round outward from the fractional bounding box. Width gets an
593    // extra pixel to accommodate the horizontal subpixel offset (up to 0.75 px)
594    // applied when rasterising into the atlas; the Y axis has no subpixel shift
595    // so floor/ceil alone is sufficient. GLYPH_PADDING in the atlas allocator
596    // provides the guard band needed by the hybrid renderer's Extend::Pad sampling.
597    let min_x = bounds.x0.floor() as i32;
598    let max_x = bounds.x1.ceil() as i32 + 1;
599
600    // For Y, we flip the coordinate system: font Y up -> screen Y down
601    // After flipping Y, min_y becomes -max_y and max_y becomes -min_y
602    let flipped_min_y = (-bounds.y1).floor() as i32;
603    let flipped_max_y = (-bounds.y0).ceil() as i32;
604
605    let width = (max_x - min_x) as u16;
606    let height = (flipped_max_y - flipped_min_y) as u16;
607
608    RasterMetrics {
609        width,
610        height,
611        bearing_x: min_x as i16,
612        bearing_y: flipped_min_y as i16,
613    }
614}
615
616/// Choose image sampling quality based on downscale factor.
617///
618/// Returns `High` when the transform scales below 50% (where aliasing is
619/// visible), `Medium` otherwise.
620#[inline]
621pub fn quality_for_scale(transform: &Affine) -> ImageQuality {
622    let [a, _, _, d, _, _] = transform.as_coeffs();
623    if a < 0.5 || d < 0.5 {
624        ImageQuality::High
625    } else {
626        ImageQuality::Medium
627    }
628}
629
630/// Choose image sampling quality based on skew presence.
631///
632/// Skewed transforms need `Medium` quality to avoid aliasing; axis-aligned
633/// transforms use `Low` (nearest-neighbour) since the content was already
634/// rasterized at pixel boundaries.
635#[inline]
636pub(crate) fn quality_for_skew(transform: &Affine) -> ImageQuality {
637    if transform.has_skew() {
638        ImageQuality::Medium
639    } else {
640        ImageQuality::Low
641    }
642}
643
644/// Replay recorded atlas commands into a [`DrawSink`].
645///
646/// The commands `Vec` is drained, freeing memory as each command is consumed.
647pub fn replay_atlas_commands(commands: &mut Vec<AtlasCommand>, target: &mut impl DrawSink) {
648    for cmd in commands.drain(..) {
649        match cmd {
650            AtlasCommand::SetTransform(t) => target.set_transform(t),
651            AtlasCommand::SetPaint(p) => target.set_paint(p),
652            AtlasCommand::SetPaintTransform(t) => target.set_paint_transform(t),
653            AtlasCommand::FillPath(p) => target.fill_path(&p),
654            AtlasCommand::FillRect(r) => target.fill_rect(&r),
655            AtlasCommand::PushClipLayer(c) => target.push_clip_layer(&c),
656            AtlasCommand::PushClipPath(c) => target.push_clip_path(&c),
657            AtlasCommand::PushBlendLayer(m) => target.push_blend_layer(m),
658            AtlasCommand::PopLayer => target.pop_layer(),
659            AtlasCommand::PopClipPath => target.pop_clip_path(),
660        }
661    }
662}
663
664/// Returns `true` if the transform is safe for atlas-cached glyph rendering.
665#[inline]
666pub(crate) fn supports_atlas_caching(transform: &Affine, glyph_type: CachedGlyphType) -> bool {
667    // TODO: Investigate whether we can support arbitrary mirroring. From some
668    // initial experiments, allowing x-mirroring leads to slightly shifted glyphs, so
669    // we don't support this now. Y-mirroring also needs more consideration.
670
671    let [a, _, _, d, _, _] = transform.as_coeffs();
672
673    match glyph_type {
674        // For those glyphs, we expect any scaling factor to have been completely absorbed. Due to the fact
675        // that we had to apply a flip transform for outlines, the y-scaling factor is expected to be negative.
676        CachedGlyphType::Outline | CachedGlyphType::Colr(_) => {
677            !transform.has_non_unit_skew_or_scale() && a.is_sign_positive() && d.is_sign_negative()
678        }
679        // For bitmap glyphs, we need to relax the condition a bit, since bitmap glyphs already have a fixed
680        // size and thus might not correspond 100% to the font size. Therefore, they likely don't have a unit
681        // transform.
682        CachedGlyphType::Bitmap => {
683            !transform.has_skew() && a.is_sign_positive() && d.is_sign_positive()
684        }
685    }
686}
687
688#[cfg(test)]
689mod tests {
690    use super::supports_atlas_caching;
691    use crate::glyph::CachedGlyphType;
692    use peniko::kurbo::Affine;
693    use peniko::kurbo::Rect;
694
695    #[test]
696    fn supports_bitmap_caching_for_identity_and_translation() {
697        assert!(supports_atlas_caching(
698            &Affine::IDENTITY,
699            CachedGlyphType::Bitmap
700        ));
701        assert!(supports_atlas_caching(
702            &Affine::translate((12.0, -3.5)),
703            CachedGlyphType::Bitmap
704        ));
705    }
706
707    #[test]
708    fn rejects_skewed_transforms() {
709        assert!(!supports_atlas_caching(
710            &Affine::new([1.0, 0.1, 0.0, 1.0, 0.0, 0.0]),
711            CachedGlyphType::Bitmap
712        ));
713        assert!(!supports_atlas_caching(
714            &Affine::skew(0.2, 0.0),
715            CachedGlyphType::Outline
716        ));
717        assert!(!supports_atlas_caching(
718            &Affine::skew(0.2, 0.0),
719            CachedGlyphType::Colr(Rect::ZERO)
720        ));
721    }
722
723    #[test]
724    fn outline_and_colr_reject_non_unit_scales() {
725        assert!(!supports_atlas_caching(
726            &Affine::scale(2.0),
727            CachedGlyphType::Outline
728        ));
729        assert!(!supports_atlas_caching(
730            &Affine::scale_non_uniform(1.0, -0.5),
731            CachedGlyphType::Outline
732        ));
733        assert!(!supports_atlas_caching(
734            &Affine::scale(2.0),
735            CachedGlyphType::Colr(Rect::ZERO)
736        ));
737    }
738
739    #[test]
740    fn outline_and_colr_requires_negative_y_and_positive_x() {
741        assert!(supports_atlas_caching(
742            &Affine::scale_non_uniform(1.0, -1.0),
743            CachedGlyphType::Outline
744        ));
745        assert!(supports_atlas_caching(
746            &Affine::scale_non_uniform(1.0, -1.0),
747            CachedGlyphType::Colr(Rect::ZERO)
748        ));
749        assert!(!supports_atlas_caching(
750            &Affine::scale_non_uniform(-1.0, -1.0),
751            CachedGlyphType::Outline
752        ));
753        assert!(!supports_atlas_caching(
754            &Affine::scale_non_uniform(1.0, 1.0),
755            CachedGlyphType::Outline
756        ));
757    }
758
759    #[test]
760    fn bitmap_allows_positive_scales_only() {
761        assert!(supports_atlas_caching(
762            &Affine::scale_non_uniform(1.0, 1.0),
763            CachedGlyphType::Bitmap
764        ));
765        assert!(supports_atlas_caching(
766            &Affine::scale_non_uniform(2.0, 3.0),
767            CachedGlyphType::Bitmap
768        ));
769        assert!(!supports_atlas_caching(
770            &Affine::scale_non_uniform(1.0, -1.0),
771            CachedGlyphType::Bitmap
772        ));
773        assert!(!supports_atlas_caching(
774            &Affine::scale_non_uniform(-1.0, 1.0),
775            CachedGlyphType::Bitmap
776        ));
777    }
778}