blob: d3a7b86feeb4e63d5d4116451f4b2b5bd6d89296 [file]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Tests for glyph rendering.
use crate::renderer::Renderer;
#[cfg(target_os = "macos")]
use crate::util::layout_glyphs_apple_color_emoji;
use crate::util::{
layout_glyphs_noto_cbtf, layout_glyphs_noto_colr, layout_glyphs_roboto,
stops_blue_green_red_yellow,
};
use glifo::{FontEmbolden, Glyph};
use std::f64::consts::FRAC_PI_4;
use std::iter;
use std::sync::Arc;
use vello_common::color::Srgb;
use vello_common::color::palette::css::{BLACK, BLUE, GREEN, REBECCA_PURPLE};
use vello_common::kurbo::{Affine, Diagonal2, Point, Stroke};
use vello_common::paint::{Image, PaintType, PremulColor};
use vello_common::peniko::{
Blob, Extend, FontData, Gradient, ImageQuality, ImageSampler, LinearGradientPosition,
};
use vello_common::pixmap::Pixmap;
use vello_dev_macros::vello_test;
fn render_transform_composition_rows(
ctx: &mut impl Renderer,
enable_caching: bool,
hint: bool,
reverse_x_shift: f64,
paint: impl Into<PaintType>,
layout: impl Fn(f32) -> (FontData, Vec<Glyph>),
) {
let rows = [
(Affine::IDENTITY, 20.0_f32, Affine::IDENTITY),
(Affine::scale(20.0), 1.0_f32, Affine::IDENTITY),
(Affine::IDENTITY, 10.0_f32, Affine::scale(2.0)),
(Affine::scale(2.0), 5.0_f32, Affine::scale(2.0)),
(
Affine::translate((-4.0, 0.0)),
20.0_f32,
Affine::translate((4.0, 0.0)),
),
(
Affine::translate((-4.0, 0.0)) * Affine::scale(4.0),
5.0_f32,
Affine::translate((1.0, 0.0)),
),
(
Affine::translate((-1.0, 0.0)),
40.0_f32,
Affine::scale(0.5) * Affine::translate((2.0, 0.0)),
),
(
Affine::IDENTITY,
20.0_f32,
Affine::translate((10.0, -10.0))
* Affine::rotate(FRAC_PI_4)
* Affine::translate((-10.0, 10.0)),
),
(
Affine::IDENTITY,
20.0_f32,
Affine::translate((10.0, -10.0))
* Affine::skew(0.35, 0.0)
* Affine::translate((-10.0, 10.0)),
),
(
Affine::IDENTITY,
20.0_f32,
Affine::translate((10.0, -10.0))
* Affine::skew(0.0, 0.2)
* Affine::translate((-10.0, 10.0)),
),
(
Affine::scale_non_uniform(1.0, -1.0) * Affine::translate((0.0, 20.0)),
20.0_f32,
Affine::IDENTITY,
),
(
Affine::scale_non_uniform(-1.0, 1.0) * Affine::translate((-reverse_x_shift, 0.0)),
20.0_f32,
Affine::IDENTITY,
),
(
Affine::scale_non_uniform(-1.0, -1.0) * Affine::translate((-reverse_x_shift, 20.0)),
20.0_f32,
Affine::IDENTITY,
),
];
ctx.set_paint(paint);
let mut y = 28.35;
for (run_scale_transform, font_size, glyph_transform) in rows {
let (font, glyphs) = layout(font_size);
ctx.set_transform(Affine::translate((16.0, y)) * run_scale_transform);
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.glyph_transform(glyph_transform)
.hint(hint)
.fill_glyphs(glyphs.into_iter());
y += 30.0;
}
}
#[vello_test(width = 300, height = 70, glyph)]
fn glyphs_filled(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 50_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello, world!", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size))));
ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5));
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.hint(true)
.fill_glyphs(glyphs.into_iter());
}
#[vello_test(width = 300, height = 70, glyph)]
fn glyphs_filled_unhinted(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 50_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello, world!", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size))));
ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5));
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.hint(false)
.fill_glyphs(glyphs.into_iter());
}
#[vello_test(width = 760, height = 140)]
fn glyphs_emboldened(ctx: &mut impl Renderer) {
let font_size: f32 = 44_f32;
let (font, glyphs) = layout_glyphs_roboto("this is regular and emboldened text", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size))));
ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5));
ctx.glyph_run(&font)
.font_size(font_size)
.hint(true)
.fill_glyphs(glyphs.into_iter());
let (font, glyphs) = layout_glyphs_roboto("this is regular and emboldened text", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size) + 58.0)));
ctx.glyph_run(&font)
.font_size(font_size)
.font_embolden(FontEmbolden::new(Diagonal2::new(1.0, 1.0)))
.hint(true)
.fill_glyphs(glyphs.into_iter());
}
#[vello_test(width = 300, height = 70, glyph)]
fn glyphs_stroked(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 50_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello, world!", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size))));
ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5));
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.hint(true)
.stroke_glyphs(glyphs.into_iter());
}
#[vello_test(width = 300, height = 70, glyph)]
fn glyphs_stroked_unhinted(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 50_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello, world!", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size))));
ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5));
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.hint(false)
.stroke_glyphs(glyphs.into_iter());
}
#[vello_test(width = 300, height = 70, glyph)]
fn glyphs_stroked_scaled_up(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 5_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello, world!", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size))).then_scale(10.0));
ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5));
ctx.set_stroke(Stroke {
width: 0.3,
..Stroke::default()
});
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.hint(false)
.stroke_glyphs(glyphs.into_iter());
}
#[vello_test(width = 300, height = 70, glyph)]
fn glyphs_large_stroke_width(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 50_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello, world!", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size))));
ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5));
ctx.set_stroke(Stroke {
width: 3.0,
..Stroke::default()
});
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.stroke_glyphs(glyphs.into_iter());
}
#[vello_test(width = 300, height = 120)]
fn glyphs_stroked_then_filled(ctx: &mut impl Renderer) {
let font_size: f32 = 50_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello, world!", font_size);
render_roboto_with_mode(
ctx,
&font,
font_size,
glyphs.iter().copied(),
Affine::translate((0., f64::from(font_size))),
DrawMode::Stroke,
);
render_roboto_with_mode(
ctx,
&font,
font_size,
glyphs.into_iter(),
Affine::translate((0., f64::from(font_size * 2.0))),
DrawMode::Fill,
);
}
#[vello_test(width = 300, height = 120)]
fn glyphs_filled_then_stroked(ctx: &mut impl Renderer) {
let font_size: f32 = 50_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello, world!", font_size);
render_roboto_with_mode(
ctx,
&font,
font_size,
glyphs.iter().copied(),
Affine::translate((0., f64::from(font_size))),
DrawMode::Fill,
);
render_roboto_with_mode(
ctx,
&font,
font_size,
glyphs.into_iter(),
Affine::translate((0., f64::from(font_size * 2.0))),
DrawMode::Stroke,
);
}
#[vello_test(width = 300, height = 70, glyph)]
fn glyphs_skewed(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 50_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello, world!", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size))));
ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5));
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.glyph_transform(Affine::skew(-20_f64.to_radians().tan(), 0.))
.hint(true)
.fill_glyphs(glyphs.into_iter());
}
#[vello_test(width = 300, height = 70, glyph)]
fn glyphs_skewed_unhinted(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 50_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello, world!", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size))));
ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5));
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.glyph_transform(Affine::skew(-20_f64.to_radians().tan(), 0.))
.hint(false)
.fill_glyphs(glyphs.into_iter());
}
#[vello_test(width = 250, height = 75, glyph)]
fn glyphs_skewed_long(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 20_f32;
let (font, glyphs) = layout_glyphs_roboto(
"Lorem ipsum dolor sit amet,\nconsectetur adipiscing elit.\nSed ornare arcu lectus.",
font_size,
);
ctx.set_transform(Affine::translate((0., f64::from(font_size))));
ctx.set_paint(REBECCA_PURPLE);
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.glyph_transform(Affine::skew(-10_f64.to_radians().tan(), 0.))
.hint(true)
.fill_glyphs(glyphs.into_iter());
}
#[vello_test(width = 250, height = 75, glyph)]
fn glyphs_skewed_long_unhinted(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 20_f32;
let (font, glyphs) = layout_glyphs_roboto(
"Lorem ipsum dolor sit amet,\nconsectetur adipiscing elit.\nSed ornare arcu lectus.",
font_size,
);
ctx.set_transform(Affine::translate((0., f64::from(font_size))));
ctx.set_paint(REBECCA_PURPLE);
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.glyph_transform(Affine::skew(-10_f64.to_radians().tan(), 0.))
.hint(false)
.fill_glyphs(glyphs.into_iter());
}
#[vello_test(width = 150, height = 125, glyph)]
fn glyphs_skewed_unskewed(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 50_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello,\nworld!", font_size);
ctx.set_transform(
Affine::translate((0., f64::from(font_size)))
* Affine::skew(-20_f64.to_radians().tan(), 0.),
);
ctx.set_paint(REBECCA_PURPLE);
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.glyph_transform(Affine::skew(20_f64.to_radians().tan(), 0.))
.hint(true)
.fill_glyphs(glyphs.into_iter());
}
#[vello_test(width = 150, height = 125, glyph)]
fn glyphs_skewed_unskewed_unhinted(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 50_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello,\nworld!", font_size);
ctx.set_transform(
Affine::translate((0., f64::from(font_size)))
* Affine::skew(-20_f64.to_radians().tan(), 0.),
);
ctx.set_paint(REBECCA_PURPLE);
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.glyph_transform(Affine::skew(20_f64.to_radians().tan(), 0.))
.hint(false)
.fill_glyphs(glyphs.into_iter());
}
#[vello_test(width = 150, height = 125, glyph)]
fn glyphs_scaled(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 25_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello,\nworld!", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size))).then_scale(2.0));
ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5));
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.hint(true)
.fill_glyphs(glyphs.into_iter());
}
#[vello_test(width = 150, height = 125, glyph)]
fn glyphs_scaled_unhinted(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 25_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello,\nworld!", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size))).then_scale(2.0));
ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5));
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.hint(false)
.fill_glyphs(glyphs.into_iter());
}
#[vello_test(width = 150, height = 125, glyph)]
fn glyphs_glyph_transform(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 25_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello,\nworld!", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size))).then_scale(2.0));
ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5));
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.glyph_transform(Affine::translate((10., 10.)))
.hint(true)
.fill_glyphs(glyphs.into_iter());
}
#[vello_test(width = 150, height = 125, glyph)]
fn glyphs_glyph_transform_unhinted(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 25_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello,\nworld!", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size))).then_scale(2.0));
ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5));
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.glyph_transform(Affine::translate((10., 10.)))
.hint(false)
.fill_glyphs(glyphs.into_iter());
}
enum GlyphPaint {
Gradient,
Image,
}
fn glyphs_with_transformed_paint_inner(
ctx: &mut impl Renderer,
enable_caching: bool,
mode: DrawMode,
glyph_paint: GlyphPaint,
) {
const SCENE_WIDTH: f64 = 300.0;
const GRADIENT_WIDTH: f64 = SCENE_WIDTH / 2.0;
const IMAGE_WIDTH: f64 = 4.0;
const BASELINE_DELTA: f64 = 40.0;
let font_size = 36.0_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello World", font_size);
let text_width = glyphs
.last()
.map_or(0.0, |glyph| f64::from(glyph.x) + f64::from(font_size) * 0.6);
let centered_x = ((SCENE_WIDTH - text_width) / 2.0) as f32;
let paint = match glyph_paint {
GlyphPaint::Gradient => PaintType::from(Gradient {
kind: LinearGradientPosition {
start: Point::new(0.0, 0.0),
end: Point::new(GRADIENT_WIDTH, 0.0),
}
.into(),
stops: stops_blue_green_red_yellow(),
..Default::default()
}),
GlyphPaint::Image => {
let color_strip = stops_blue_green_red_yellow();
let pixels = color_strip
.0
.iter()
.map(|stop| {
PremulColor::from_alpha_color(stop.color.to_alpha_color::<Srgb>())
.as_premul_rgba8()
})
.collect();
let image = ctx.get_image_source(Arc::new(Pixmap::from_parts(pixels, 4, 1)));
PaintType::from(Image {
image,
sampler: ImageSampler {
x_extend: Extend::Pad,
y_extend: Extend::Pad,
// TODO: There seems to be a mismatch when using `Medium` here between Hybrid and CPU,
// so let's use `Low` for now.
quality: ImageQuality::Low,
alpha: 1.0,
},
})
}
};
ctx.set_paint(paint);
ctx.set_stroke(Stroke {
width: 1.5,
..Stroke::default()
});
let mut baseline_y = 42.0;
// 1) Visible gradient starts at green since we only translate the
// glyph X, which shouldn't affect the paint transform.
// 2) Visible gradient starts at 2, since paint is transformed along with
// the path transform.
// 3) Same as 2), just that the source is the paint transform instead of the
// scene transform.
// 4) Gradient should be shifted to the right, since it's affected by
// scene transform + paint transform.
for (use_render_transform, shift_gradient) in
[(false, false), (true, false), (false, true), (true, true)]
{
let (transform, glyph_x) = if use_render_transform {
(Affine::translate((centered_x as f64, baseline_y)), 0.0)
} else {
(Affine::translate((0.0, baseline_y)), centered_x)
};
let paint_transform = match glyph_paint {
GlyphPaint::Gradient => {
if shift_gradient {
Affine::translate((centered_x as f64, 0.0))
} else {
Affine::IDENTITY
}
}
GlyphPaint::Image => {
let image_transform = Affine::scale_non_uniform(GRADIENT_WIDTH / IMAGE_WIDTH, 1.0);
if shift_gradient {
Affine::translate((centered_x as f64, 0.0)) * image_transform
} else {
image_transform
}
}
};
let row_glyphs = glyphs.iter().map(|glyph| Glyph {
id: glyph.id,
x: glyph.x + glyph_x,
y: glyph.y,
});
ctx.set_transform(transform);
ctx.set_paint_transform(paint_transform);
let builder = ctx
.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.hint(false);
match mode {
DrawMode::Fill => builder.fill_glyphs(row_glyphs),
DrawMode::Stroke => builder.stroke_glyphs(row_glyphs),
}
baseline_y += BASELINE_DELTA;
}
}
#[vello_test(width = 300, height = 180, glyph)]
fn glyphs_with_gradient(ctx: &mut impl Renderer, enable_caching: bool) {
glyphs_with_transformed_paint_inner(ctx, enable_caching, DrawMode::Fill, GlyphPaint::Gradient);
}
#[vello_test(width = 300, height = 180, glyph)]
fn glyphs_with_gradient_stroked(ctx: &mut impl Renderer, enable_caching: bool) {
glyphs_with_transformed_paint_inner(
ctx,
enable_caching,
DrawMode::Stroke,
GlyphPaint::Gradient,
);
}
#[vello_test(width = 300, height = 180, glyph)]
fn glyphs_with_image(ctx: &mut impl Renderer, enable_caching: bool) {
glyphs_with_transformed_paint_inner(ctx, enable_caching, DrawMode::Fill, GlyphPaint::Image);
}
#[vello_test(width = 110, height = 410, glyph, hybrid_tolerance = 1)]
fn glyphs_transform_composition_rows_outline(ctx: &mut impl Renderer, enable_caching: bool) {
render_transform_composition_rows(
ctx,
enable_caching,
false,
47.0,
REBECCA_PURPLE.with_alpha(0.5),
|font_size| layout_glyphs_roboto("Hello", font_size),
);
}
#[vello_test(width = 110, height = 410, glyph, hybrid_tolerance = 1)]
fn glyphs_transform_composition_rows_outline_hinted(ctx: &mut impl Renderer, enable_caching: bool) {
render_transform_composition_rows(
ctx,
enable_caching,
true,
47.0,
REBECCA_PURPLE.with_alpha(0.5),
|font_size| layout_glyphs_roboto("Hello", font_size),
);
}
// Next two tests require high tolerance on CPU likely due to having to use bicubic interpolation, since
// we downscale a lot.
#[vello_test(width = 210, height = 410, skip_hybrid, glyph, cpu_u8_tolerance = 3)]
fn glyphs_transform_composition_rows_bitmap(ctx: &mut impl Renderer, enable_caching: bool) {
render_transform_composition_rows(ctx, enable_caching, false, 100.0, BLACK, |font_size| {
layout_glyphs_noto_cbtf("✅👀🎉🤠", font_size)
});
}
#[vello_test(width = 210, height = 410, skip_hybrid, glyph, cpu_u8_tolerance = 3)]
fn glyphs_transform_composition_rows_bitmap_hinted(ctx: &mut impl Renderer, enable_caching: bool) {
render_transform_composition_rows(ctx, enable_caching, true, 100.0, BLACK, |font_size| {
layout_glyphs_noto_cbtf("✅👀🎉🤠", font_size)
});
}
// TODO: The cached versions of COLR glyphs seem to have a slight shift, investigate.
#[vello_test(
width = 210,
height = 410,
cpu_u8_tolerance = 3,
hybrid_tolerance = 3,
glyph
)]
fn glyphs_transform_composition_rows_colr(ctx: &mut impl Renderer, enable_caching: bool) {
render_transform_composition_rows(ctx, enable_caching, false, 100.0, BLACK, |font_size| {
layout_glyphs_noto_colr("✅👀🎉🤠", font_size)
});
}
#[vello_test(
width = 210,
height = 410,
cpu_u8_tolerance = 3,
hybrid_tolerance = 3,
glyph
)]
fn glyphs_transform_composition_rows_colr_hinted(ctx: &mut impl Renderer, enable_caching: bool) {
render_transform_composition_rows(ctx, enable_caching, true, 100.0, BLACK, |font_size| {
layout_glyphs_noto_colr("✅👀🎉🤠", font_size)
});
}
#[vello_test(width = 60, height = 12, glyph)]
fn glyphs_small(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 10_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello, world!", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size))));
ctx.set_paint(REBECCA_PURPLE);
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.hint(true)
.fill_glyphs(glyphs.into_iter());
}
#[vello_test(width = 60, height = 12, glyph)]
fn glyphs_small_unhinted(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 10_f32;
let (font, glyphs) = layout_glyphs_roboto("Hello, world!", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size))));
ctx.set_paint(REBECCA_PURPLE);
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.hint(false)
.fill_glyphs(glyphs.into_iter());
}
#[vello_test(width = 250, height = 70, skip_hybrid, glyph)]
fn glyphs_bitmap_noto(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 50_f32;
let (font, glyphs) = layout_glyphs_noto_cbtf("✅👀🎉🤠", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size))));
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.fill_glyphs(glyphs.into_iter());
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DrawMode {
Fill,
Stroke,
}
fn render_roboto_with_mode(
ctx: &mut impl Renderer,
font: &FontData,
font_size: f32,
glyphs: impl Iterator<Item = Glyph> + Clone,
transform: Affine,
mode: DrawMode,
) {
ctx.set_transform(transform);
ctx.set_paint(REBECCA_PURPLE);
ctx.set_stroke(Stroke {
width: 3.0,
..Stroke::default()
});
let builder = ctx.glyph_run(font).font_size(font_size);
match mode {
DrawMode::Fill => {
builder.fill_glyphs(glyphs);
}
DrawMode::Stroke => {
builder.stroke_glyphs(glyphs);
}
}
}
#[vello_test(
width = 250,
height = 70,
cpu_u8_tolerance = 1,
hybrid_tolerance = 2,
glyph
)]
fn glyphs_colr_noto(ctx: &mut impl Renderer, enable_caching: bool) {
render_colr_noto_with_transform(
ctx,
Affine::translate((0., 50.)),
enable_caching,
DrawMode::Fill,
);
}
#[vello_test(
width = 250,
height = 70,
cpu_u8_tolerance = 1,
hybrid_tolerance = 2,
glyph
)]
fn glyphs_colr_noto_stroked(ctx: &mut impl Renderer, enable_caching: bool) {
render_colr_noto_with_transform(
ctx,
Affine::translate((0., 50.)),
enable_caching,
DrawMode::Fill,
);
}
#[vello_test(width = 100, height = 100, glyph)]
fn glyphs_colr_noto_overflow_centered(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size = 150.0;
let (font, mut glyphs) = layout_glyphs_noto_colr("✅", font_size);
let glyph = glyphs.pop().unwrap();
let centered_glyph = Glyph {
id: glyph.id,
x: -25.0,
y: 125.0,
};
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.hint(false)
.fill_glyphs(iter::once(centered_glyph));
}
#[vello_test(
width = 500,
height = 140,
cpu_u8_tolerance = 1,
hybrid_tolerance = 2,
glyph
)]
fn glyphs_colr_noto_scaled_2x(ctx: &mut impl Renderer, enable_caching: bool) {
render_colr_noto_with_transform(
ctx,
Affine::translate((0., 50.)).then_scale(2.0),
enable_caching,
DrawMode::Fill,
);
}
#[vello_test(
width = 125,
height = 35,
cpu_u8_tolerance = 1,
hybrid_tolerance = 2,
glyph
)]
fn glyphs_colr_noto_scaled_half(ctx: &mut impl Renderer, enable_caching: bool) {
render_colr_noto_with_transform(
ctx,
Affine::translate((0., 50.)).then_scale(0.5),
enable_caching,
DrawMode::Fill,
);
}
#[vello_test(
width = 350,
height = 350,
cpu_u8_tolerance = 3,
hybrid_tolerance = 2,
glyph
)]
fn glyphs_colr_noto_rotated(ctx: &mut impl Renderer, enable_caching: bool) {
render_colr_noto_with_transform(
ctx,
Affine::translate((175., 100.)) * Affine::rotate(FRAC_PI_4),
enable_caching,
DrawMode::Fill,
);
}
#[vello_test(
width = 600,
height = 600,
cpu_u8_tolerance = 2,
hybrid_tolerance = 2,
glyph
)]
fn glyphs_colr_noto_rotated_scaled(ctx: &mut impl Renderer, enable_caching: bool) {
render_colr_noto_with_transform(
ctx,
Affine::translate((300., 150.)) * Affine::rotate(FRAC_PI_4) * Affine::scale(2.0),
enable_caching,
DrawMode::Fill,
);
}
#[vello_test(
width = 250,
height = 140,
cpu_u8_tolerance = 1,
hybrid_tolerance = 2,
glyph
)]
fn glyphs_colr_noto_scaled_non_uniform(ctx: &mut impl Renderer, enable_caching: bool) {
render_colr_noto_with_transform(
ctx,
Affine::translate((0., 50.)) * Affine::scale_non_uniform(1.0, 2.0),
enable_caching,
DrawMode::Fill,
);
}
#[vello_test(
width = 300,
height = 300,
cpu_u8_tolerance = 2,
hybrid_tolerance = 1,
glyph
)]
fn glyphs_colr_noto_rotated_scaled_non_uniform(ctx: &mut impl Renderer, enable_caching: bool) {
render_colr_noto_with_transform(
ctx,
Affine::translate((150., 150.))
* Affine::rotate(FRAC_PI_4)
* Affine::scale_non_uniform(1.0, 2.0),
enable_caching,
DrawMode::Fill,
);
}
#[vello_test(width = 250, height = 70, skip_hybrid)]
fn glyphs_bitmap_noto_stroked(ctx: &mut impl Renderer) {
let font_size: f32 = 50_f32;
let (font, glyphs) = layout_glyphs_noto_cbtf("✅👀🎉🤠", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size))));
ctx.glyph_run(&font)
.font_size(font_size)
.stroke_glyphs(glyphs.into_iter());
}
#[cfg(target_os = "macos")]
#[vello_test(width = 200, height = 70, skip_hybrid, cpu_u8_tolerance = 2, glyph)]
fn glyphs_bitmap_apple(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size: f32 = 50_f32;
let (font, glyphs) = layout_glyphs_apple_color_emoji("✅👀🎉🤠", font_size);
ctx.set_transform(Affine::translate((0., f64::from(font_size))));
ctx.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.fill_glyphs(glyphs.into_iter());
}
// In case anything changes here, compare to https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/web_tests/platform/linux/virtual/text-antialias/colrv1-expected.png
#[vello_test(
width = 400,
height = 960,
hybrid_tolerance = 1,
diff_pixels = 55,
glyph
)]
fn glyphs_colr_test_glyphs(ctx: &mut impl Renderer, enable_caching: bool) {
const TEST_FONT: &[u8] =
include_bytes!("../../../examples/assets/colr_test_glyphs/test_glyphs-glyf_colr_1.ttf");
let font = FontData::new(Blob::new(Arc::new(TEST_FONT)), 0);
let num_glyphs = 221;
let font_size = 40_f64;
let cols = 10.0;
let width = (font_size * cols) as u16;
ctx.set_paint(BLACK);
let mut cur_x = 0.0;
let mut cur_y = font_size;
let draw_glyphs = (0..=num_glyphs).filter(|n| match n {
// Those are not COLR glyphs.
0..8 => false,
161..=165 => false,
170..=176 => false,
_ => true,
});
for id in draw_glyphs {
if cur_x >= width as f64 {
cur_x = 0.0;
cur_y += font_size;
}
let glyph_iter = iter::once(Glyph { id, x: 0.0, y: 0.0 });
ctx.set_transform(Affine::translate((cur_x, cur_y)));
ctx.glyph_run(&font)
.font_size(font_size as f32)
.atlas_cache(enable_caching)
.fill_glyphs(glyph_iter);
cur_x += font_size;
}
cur_y += font_size;
for color in [BLUE, GREEN] {
cur_x = 0.0;
cur_y += font_size;
ctx.set_paint(color);
for g in 148..=153 {
let glyph_iter = iter::once(Glyph {
id: g,
x: 0.0,
y: 0.0,
});
ctx.set_transform(Affine::translate((cur_x, cur_y)));
ctx.glyph_run(&font)
.font_size(font_size as f32)
.atlas_cache(enable_caching)
.fill_glyphs(glyph_iter);
cur_x += font_size;
}
}
}
/// Hinting is disabled to preserve transforms passed to `prepare_colr_glyph`.
fn render_colr_noto_with_transform(
ctx: &mut impl Renderer,
transform: Affine,
enable_caching: bool,
mode: DrawMode,
) {
let font_size: f32 = 50_f32;
let (font, glyphs) = layout_glyphs_noto_colr("✅👀🎉🤠", font_size);
ctx.set_transform(transform);
let run = ctx
.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.hint(false);
if mode == DrawMode::Stroke {
run.stroke_glyphs(glyphs.into_iter());
} else {
run.fill_glyphs(glyphs.into_iter());
}
}
fn render_decorated_text(
ctx: &mut impl Renderer,
text: &str,
font_size: f32,
enable_caching: bool,
glyph_transform: Option<Affine>,
offset: f32,
size: f32,
buffer: f32,
) {
let (font, glyphs) = layout_glyphs_roboto(text, font_size);
ctx.set_paint(REBECCA_PURPLE);
let mut fill_builder = ctx
.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.hint(false);
if let Some(gt) = glyph_transform {
fill_builder = fill_builder.glyph_transform(gt);
}
fill_builder.fill_glyphs(glyphs.iter().copied());
let x_end = glyphs.last().map_or(0.0, |g| g.x + font_size * 0.6);
let mut deco_builder = ctx
.glyph_run(&font)
.font_size(font_size)
.atlas_cache(enable_caching)
.hint(glyph_transform.is_none());
if let Some(gt) = glyph_transform {
deco_builder = deco_builder.glyph_transform(gt);
}
deco_builder.render_decoration(glyphs.into_iter(), 0.0..=x_end, 0.0, offset, size, buffer);
}
#[vello_test(width = 300, height = 180, glyph)]
fn glyphs_decoration_offset_values(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size = 30.0_f32;
for (i, offset) in [-6.0_f32, -2.0, 0.0, 8.0, 15.0].iter().enumerate() {
let y = 30.0 + (i as f64) * 32.0;
ctx.set_transform(Affine::translate((0., y)));
render_decorated_text(
ctx,
"Happy joyful",
font_size,
enable_caching,
None,
*offset,
1.5,
1.5,
);
}
}
#[vello_test(width = 180, height = 180, glyph)]
fn glyphs_decoration_size_values(ctx: &mut impl Renderer, enable_caching: bool) {
let font_size = 30.0_f32;
for (i, size) in [0.5_f32, 1.0, 2.0, 4.0].iter().enumerate() {
let y = 30.0 + (i as f64) * 38.0;
ctx.set_transform(Affine::translate((0., y)));
render_decorated_text(
ctx,
"Happy joyful",
font_size,
enable_caching,
None,
-2.0,
*size,
1.5,
);
}
}
#[vello_test(width = 180, height = 70, glyph)]
fn glyphs_decoration_no_descenders(ctx: &mut impl Renderer, enable_caching: bool) {
ctx.set_transform(Affine::translate((0., 50.)));
render_decorated_text(ctx, "HELLO", 50.0, enable_caching, None, -2.0, 2.0, 1.5);
}
#[vello_test(width = 100, height = 150, glyph, hybrid_tolerance = 1)]
fn glyphs_decoration_transformed(ctx: &mut impl Renderer, enable_caching: bool) {
let text = "Happy";
let rows: [(Affine, f32, Option<Affine>, f64); 4] = [
// Run-level scale absorbs into font_size and underline scaled.
(Affine::scale(2.0), 12.0, None, 30.0),
// Glyph-level scale (affects skip-ink outline transform)
(Affine::IDENTITY, 10.0, Some(Affine::scale(1.2)), 40.0),
// Y-flip (decoration should appear above the flipped text)
(
Affine::scale_non_uniform(1.0, -1.0) * Affine::translate((0.0, 20.0)),
20.0,
None,
10.0,
),
// Rotated text
(Affine::rotate(FRAC_PI_4), 12.0, None, 40.0),
];
let mut y = 30.0;
for (run_transform, font_size, glyph_transform, buffer) in rows {
ctx.set_transform(Affine::translate((16.0, y)) * run_transform);
render_decorated_text(
ctx,
text,
font_size,
enable_caching,
glyph_transform,
-1.0,
1.0,
1.0,
);
y += buffer;
}
}