blob: be7374a315f93afb8250351e4c0d333d4f292bd0 [file] [log] [blame] [edit]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Utility functions shared across different tests.
use crate::renderer::Renderer;
use image::codecs::png::PngEncoder;
use image::{ExtendedColorType, ImageEncoder, Rgba, RgbaImage, load_from_memory};
use skrifa::MetadataProvider;
use skrifa::raw::FileRef;
use smallvec::smallvec;
use std::cmp::max;
use std::io::Cursor;
use std::sync::Arc;
use vello_common::color::DynamicColor;
use vello_common::color::palette::css::{BLUE, GREEN, RED, WHITE, YELLOW};
use vello_common::glyph::Glyph;
use vello_common::kurbo::{BezPath, Join, Point, Rect, Shape, Stroke, Vec2};
use vello_common::peniko::{Blob, ColorStop, ColorStops, Font};
use vello_common::pixmap::Pixmap;
use vello_cpu::RenderMode;
#[cfg(not(target_arch = "wasm32"))]
use std::path::PathBuf;
#[cfg(not(target_arch = "wasm32"))]
static REFS_PATH: std::sync::LazyLock<PathBuf> = std::sync::LazyLock::new(|| {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../vello_sparse_tests/snapshots")
});
#[cfg(not(target_arch = "wasm32"))]
static DIFFS_PATH: std::sync::LazyLock<PathBuf> = std::sync::LazyLock::new(|| {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../vello_sparse_tests/diffs")
});
pub(crate) fn get_ctx<T: Renderer>(width: u16, height: u16, transparent: bool) -> T {
let mut ctx = T::new(width, height);
if !transparent {
let path = Rect::new(0.0, 0.0, width as f64, height as f64).to_path(0.1);
ctx.set_paint(WHITE);
ctx.fill_path(&path);
}
ctx
}
pub(crate) fn render_pixmap(ctx: &impl Renderer, render_mode: RenderMode) -> Pixmap {
let mut pixmap = Pixmap::new(ctx.width(), ctx.height());
ctx.render_to_pixmap(&mut pixmap, render_mode);
pixmap
}
pub(crate) fn miter_stroke_2() -> Stroke {
Stroke {
width: 2.0,
join: Join::Miter,
..Default::default()
}
}
pub(crate) fn crossed_line_star() -> BezPath {
let mut path = BezPath::new();
path.move_to((50.0, 10.0));
path.line_to((75.0, 90.0));
path.line_to((10.0, 40.0));
path.line_to((90.0, 40.0));
path.line_to((25.0, 90.0));
path.line_to((50.0, 10.0));
path
}
pub(crate) fn circular_star(center: Point, n: usize, inner: f64, outer: f64) -> BezPath {
let mut path = BezPath::new();
let start_angle = -std::f64::consts::FRAC_PI_2;
path.move_to(center + outer * Vec2::from_angle(start_angle));
for i in 1..n * 2 {
let th = start_angle + i as f64 * std::f64::consts::PI / n as f64;
let r = if i % 2 == 0 { outer } else { inner };
path.line_to(center + r * Vec2::from_angle(th));
}
path.close_path();
path
}
pub(crate) fn layout_glyphs_roboto(text: &str, font_size: f32) -> (Font, Vec<Glyph>) {
const ROBOTO_FONT: &[u8] = include_bytes!("../../../examples/assets/roboto/Roboto-Regular.ttf");
let font = Font::new(Blob::new(Arc::new(ROBOTO_FONT)), 0);
layout_glyphs(text, font_size, font)
}
pub(crate) fn layout_glyphs_noto_cbtf(text: &str, font_size: f32) -> (Font, Vec<Glyph>) {
const NOTO_FONT: &[u8] =
include_bytes!("../../../examples/assets/noto_color_emoji/NotoColorEmoji-CBTF-Subset.ttf");
let font = Font::new(Blob::new(Arc::new(NOTO_FONT)), 0);
layout_glyphs(text, font_size, font)
}
pub(crate) fn layout_glyphs_noto_colr(text: &str, font_size: f32) -> (Font, Vec<Glyph>) {
const NOTO_FONT: &[u8] =
include_bytes!("../../../examples/assets/noto_color_emoji/NotoColorEmoji-Subset.ttf");
let font = Font::new(Blob::new(Arc::new(NOTO_FONT)), 0);
layout_glyphs(text, font_size, font)
}
#[cfg(target_os = "macos")]
pub(crate) fn layout_glyphs_apple_color_emoji(text: &str, font_size: f32) -> (Font, Vec<Glyph>) {
let apple_font: Vec<u8> = std::fs::read("/System/Library/Fonts/Apple Color Emoji.ttc").unwrap();
let font = Font::new(Blob::new(Arc::new(apple_font)), 0);
layout_glyphs(text, font_size, font)
}
/// ***DO NOT USE THIS OUTSIDE OF THESE TESTS***
///
/// This function is used for _TESTING PURPOSES ONLY_. If you need to layout and shape
/// text for your application, use a proper text shaping library like `Parley`.
///
/// We use this function as a convenience for testing; to get some glyphs shaped and laid
/// out in a small amount of code without having to go through the trouble of setting up a
/// full text layout pipeline, which you absolutely should do in application code.
fn layout_glyphs(text: &str, font_size: f32, font: Font) -> (Font, Vec<Glyph>) {
let font_ref = {
let file_ref = FileRef::new(font.data.as_ref()).unwrap();
match file_ref {
FileRef::Font(f) => f,
FileRef::Collection(collection) => collection.get(font.index).unwrap(),
}
};
let font_size = skrifa::instance::Size::new(font_size);
let axes = font_ref.axes();
let variations: Vec<(&str, f32)> = vec![];
let var_loc = axes.location(variations.as_slice());
let charmap = font_ref.charmap();
let metrics = font_ref.metrics(font_size, &var_loc);
let line_height = metrics.ascent - metrics.descent + metrics.leading;
let glyph_metrics = font_ref.glyph_metrics(font_size, &var_loc);
let mut pen_x = 0_f32;
let mut pen_y = 0_f32;
let glyphs = text
.chars()
.filter_map(|ch| {
if ch == '\n' {
pen_y += line_height;
pen_x = 0.0;
return None;
}
let gid = charmap.map(ch).unwrap_or_default();
let advance = glyph_metrics.advance_width(gid).unwrap_or_default();
let x = pen_x;
pen_x += advance;
Some(Glyph {
id: gid.to_u32(),
x,
y: pen_y,
})
})
.collect::<Vec<_>>();
(font, glyphs)
}
pub(crate) fn stops_green_blue() -> ColorStops {
ColorStops(smallvec![
ColorStop {
offset: 0.0,
color: DynamicColor::from_alpha_color(GREEN),
},
ColorStop {
offset: 1.0,
color: DynamicColor::from_alpha_color(BLUE),
},
])
}
pub(crate) fn stops_green_blue_with_alpha() -> ColorStops {
ColorStops(smallvec![
ColorStop {
offset: 0.0,
color: DynamicColor::from_alpha_color(GREEN.with_alpha(0.25)),
},
ColorStop {
offset: 1.0,
color: DynamicColor::from_alpha_color(BLUE.with_alpha(0.75)),
},
])
}
pub(crate) fn stops_blue_green_red_yellow() -> ColorStops {
ColorStops(smallvec![
ColorStop {
offset: 0.0,
color: DynamicColor::from_alpha_color(BLUE),
},
ColorStop {
offset: 0.33,
color: DynamicColor::from_alpha_color(GREEN),
},
ColorStop {
offset: 0.66,
color: DynamicColor::from_alpha_color(RED),
},
ColorStop {
offset: 1.0,
color: DynamicColor::from_alpha_color(YELLOW),
},
])
}
pub(crate) fn pixmap_to_png(pixmap: Pixmap, width: u32, height: u32) -> Vec<u8> {
let img_buf = pixmap.take_unpremultiplied();
let mut png_data = Vec::new();
let cursor = Cursor::new(&mut png_data);
let encoder = PngEncoder::new(cursor);
encoder
.write_image(
bytemuck::cast_slice(&img_buf),
width,
height,
ExtendedColorType::Rgba8,
)
.expect("Failed to encode image");
png_data
}
pub(crate) fn check_ref(
ctx: &impl Renderer,
// The name of the test.
test_name: &str,
// The name of the specific instance of the test that is being run
// (e.g. test_gpu, test_cpu_u8, etc.)
specific_name: &str,
// Tolerance for pixel differences.
threshold: u8,
// Whether the test instance is the "gold standard" and should be used
// for creating reference images.
// N.B. Must be `false` on `wasm32` as reference image cannot be written to filesystem.
is_reference: bool,
render_mode: RenderMode,
// `ref_data` is the reference image data, passed directly for wasm32.
ref_data: &[u8],
) {
let pixmap = render_pixmap(ctx, render_mode);
let encoded_image = pixmap_to_png(pixmap, ctx.width() as u32, ctx.height() as u32);
check_ref_encoded(
&encoded_image,
test_name,
specific_name,
threshold,
is_reference,
ref_data,
);
}
pub(crate) fn check_ref_encoded(
// The encoded image under test.
encoded_image: &[u8],
// The name of the test.
test_name: &str,
// The name of the specific instance of the test that is being run
// (e.g. test_gpu, test_cpu_u8, etc.)
specific_name: &str,
// Tolerance for pixel differences.
threshold: u8,
// Whether the test instance is the "gold standard" and should be used
// for creating reference images.
// N.B. Must be `false` on `wasm32` as reference image cannot be written to filesystem.
is_reference: bool,
// `ref_data` is the reference image data, passed directly for wasm32.
ref_data: &[u8],
) {
#[cfg(not(target_arch = "wasm32"))]
{
assert_eq!(ref_data.len(), 0, "ref_data is only used for wasm32 tests");
let ref_path = REFS_PATH.join(format!("{}.png", test_name));
let write_ref_image = || {
let optimized =
oxipng::optimize_from_memory(&encoded_image, &oxipng::Options::max_compression())
.unwrap();
std::fs::write(&ref_path, optimized).unwrap();
};
if !ref_path.exists() {
if is_reference {
write_ref_image();
panic!("new reference image was created");
} else {
panic!("no reference image exists");
}
}
let ref_image = load_from_memory(&std::fs::read(&ref_path).unwrap())
.unwrap()
.into_rgba8();
let actual = load_from_memory(&encoded_image).unwrap().into_rgba8();
let diff_image = get_diff(&ref_image, &actual, threshold);
if let Some(diff_image) = diff_image {
if std::env::var("REPLACE").is_ok() && is_reference {
write_ref_image();
panic!("test was replaced");
}
if !DIFFS_PATH.exists() {
let _ = std::fs::create_dir_all(DIFFS_PATH.as_path());
}
let diff_path = DIFFS_PATH.join(format!("{}.png", specific_name));
diff_image
.save_with_format(&diff_path, image::ImageFormat::Png)
.unwrap();
panic!("test didnt match reference image");
}
}
#[cfg(target_arch = "wasm32")]
{
assert!(!is_reference, "WASM cannot create new reference images");
assert_eq!(test_name.len(), 0, "test_name is not used in wasm32 tests");
let actual = load_from_memory(&encoded_image).unwrap().into_rgba8();
let ref_image = load_from_memory(ref_data).unwrap().into_rgba8();
let diff_image = get_diff(&ref_image, &actual, threshold);
if let Some(ref img) = diff_image {
append_diff_image_to_browser_document(specific_name, img);
panic!("test didn't match reference image. Scroll to bottom of browser to view diff.");
}
}
}
#[cfg(target_arch = "wasm32")]
fn append_diff_image_to_browser_document(specific_name: &str, diff_image: &RgbaImage) {
use wasm_bindgen::JsCast;
use web_sys::js_sys::{Array, Uint8Array};
use web_sys::{Blob, BlobPropertyBag, HtmlImageElement, Url, window};
let window = window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let container = document.create_element("div").unwrap();
container
.set_attribute(
"style",
"border: 2px solid red; \
margin: 20px; \
padding: 20px; \
background: #f0f0f0; \
display: inline-block;",
)
.unwrap();
let title = document.create_element("h3").unwrap();
title.set_text_content(Some(&format!("Test Failed: {}", specific_name)));
title
.set_attribute("style", "color: red; margin-top: 0;")
.unwrap();
container.append_child(&title).unwrap();
let diff_png = {
let mut png_data = Vec::new();
let cursor = std::io::Cursor::new(&mut png_data);
let encoder = image::codecs::png::PngEncoder::new(cursor);
encoder
.write_image(
diff_image.as_raw(),
diff_image.width(),
diff_image.height(),
image::ExtendedColorType::Rgba8,
)
.unwrap();
png_data
};
let uint8_array = Uint8Array::new_with_length(diff_png.len() as u32);
uint8_array.copy_from(&diff_png);
let array = Array::new();
array.push(&uint8_array.buffer());
let blob_property_bag = BlobPropertyBag::new();
blob_property_bag.set_type("image/png");
let blob = Blob::new_with_u8_array_sequence_and_options(&array, &blob_property_bag).unwrap();
let url = Url::create_object_url_with_blob(&blob).unwrap();
let img = document
.create_element("img")
.unwrap()
.dyn_into::<HtmlImageElement>()
.unwrap();
img.set_src(&url);
img.set_attribute("style", "border: 1px solid #ccc; max-width: 100%;")
.unwrap();
img.set_attribute("title", "Expected | Diff | Actual")
.unwrap();
container.append_child(&img).unwrap();
body.append_child(&container).unwrap();
}
fn get_diff(
expected_image: &RgbaImage,
actual_image: &RgbaImage,
threshold: u8,
) -> Option<RgbaImage> {
let width = max(expected_image.width(), actual_image.width());
let height = max(expected_image.height(), actual_image.height());
let mut diff_image = RgbaImage::new(width * 3, height);
let mut pixel_diff = 0;
for x in 0..width {
for y in 0..height {
let actual_pixel = actual_image.get_pixel_checked(x, y);
let expected_pixel = expected_image.get_pixel_checked(x, y);
match (actual_pixel, expected_pixel) {
(Some(actual), Some(expected)) => {
diff_image.put_pixel(x, y, *expected);
diff_image.put_pixel(x + 2 * width, y, *actual);
if is_pix_diff(expected, actual, threshold) {
pixel_diff += 1;
diff_image.put_pixel(x + width, y, Rgba([255, 0, 0, 255]));
} else {
diff_image.put_pixel(x + width, y, Rgba([0, 0, 0, 255]));
}
}
(Some(actual), None) => {
pixel_diff += 1;
diff_image.put_pixel(x + 2 * width, y, *actual);
diff_image.put_pixel(x + width, y, Rgba([255, 0, 0, 255]));
}
(None, Some(expected)) => {
pixel_diff += 1;
diff_image.put_pixel(x, y, *expected);
diff_image.put_pixel(x + width, y, Rgba([255, 0, 0, 255]));
}
_ => {
pixel_diff += 1;
diff_image.put_pixel(x, y, Rgba([255, 0, 0, 255]));
diff_image.put_pixel(x + width, y, Rgba([255, 0, 0, 255]));
}
}
}
}
if pixel_diff > 0 {
Some(diff_image)
} else {
None
}
}
fn is_pix_diff(pixel1: &Rgba<u8>, pixel2: &Rgba<u8>, threshold: u8) -> bool {
if pixel1.0[3] == 0 && pixel2.0[3] == 0 {
return false;
}
let mut different = false;
for i in 0..3 {
let difference = pixel1.0[i].abs_diff(pixel2.0[i]);
different |= difference > threshold;
}
different
}