use anyhow::{Context, Result};
use skia_canvas::native::{
EngineKind, FontAxisTag, FontVariation, LinearColorSpace, NativeBackend, NativeError,
NativeFontManager, NativeImage, NativePaint, NativeRecorder, NativeTextEngine, PixelFormat,
RawFrameOptions, Rect, RenderEngine, RgbaLinear, SurfaceOptions, TextBoxOptions, TextStyle,
};
#[test]
fn native_facade_renders_tight_rgba8_without_importing_skia_safe() -> Result<()> {
let mut recorder = NativeRecorder::new(Rect::from_xywh(0.0, 0.0, 8.0, 8.0))?;
recorder.record(|canvas| {
canvas.clear(RgbaLinear::opaque(0.0, 0.0, 0.0));
canvas.draw_rect(
Rect::from_xywh(2.0, 2.0, 4.0, 4.0),
&NativePaint::fill(RgbaLinear::opaque(1.0, 0.0, 0.0)),
);
});
let frame = recorder.render_raw(
SurfaceOptions {
color_space: LinearColorSpace::Srgb,
..SurfaceOptions::default()
},
RawFrameOptions {
pixel_format: PixelFormat::Rgba8UnormUnpremul,
..RawFrameOptions::default()
},
)?;
assert_eq!(frame.width(), 8);
assert_eq!(frame.height(), 8);
assert_eq!(frame.stride(), 32);
assert_eq!(frame.pixels().len(), 8 * 32);
assert!(frame.pixels().iter().any(|channel| *channel != 0));
Ok(())
}
#[test]
fn native_facade_constructs_required_linear_working_spaces() -> Result<()> {
for color_space in [
LinearColorSpace::Srgb,
LinearColorSpace::DisplayP3,
LinearColorSpace::Rec2020,
] {
let mut recorder = NativeRecorder::new(Rect::from_xywh(0.0, 0.0, 4.0, 4.0))?;
recorder.record(|canvas| canvas.clear(RgbaLinear::opaque(0.25, 0.5, 1.5)));
let frame = recorder.render_raw(
SurfaceOptions {
color_space,
..SurfaceOptions::default()
},
RawFrameOptions::default(),
)?;
assert_eq!(frame.width(), 4);
assert_eq!(frame.height(), 4);
}
Ok(())
}
#[test]
fn native_facade_draws_shapes() -> Result<()> {
let mut recorder = NativeRecorder::new(Rect::from_xywh(0.0, 0.0, 64.0, 64.0))?;
recorder.record(|canvas| {
canvas.clear(RgbaLinear::opaque(0.0, 0.0, 0.0));
canvas.draw_rect(
Rect::from_xywh(4.0, 4.0, 16.0, 16.0),
&NativePaint::fill(RgbaLinear::opaque(1.0, 0.0, 0.0)),
);
canvas.draw_rounded_rect(
Rect::from_xywh(24.0, 4.0, 16.0, 16.0),
4.0,
&NativePaint::stroke(RgbaLinear::opaque(0.0, 1.0, 0.0), 2.0),
);
canvas.draw_oval(
Rect::from_xywh(44.0, 4.0, 16.0, 16.0),
&NativePaint::fill(RgbaLinear::opaque(0.0, 0.0, 1.0)),
);
});
let frame = recorder.render_raw(SurfaceOptions::default(), RawFrameOptions::default())?;
let pixels = frame.pixels();
let stride = frame.stride();
let pixel_at = |x: usize, y: usize| -> &[u8] {
let off = y * stride + x * 4;
&pixels[off..off + 4]
};
assert!(
pixel_at(12, 12)[0] > 64,
"expected red center to be visible"
);
assert!(
pixel_at(52, 12)[2] > 64,
"expected blue center to be visible"
);
let stroke_pixel = pixel_at(24, 12);
assert!(
stroke_pixel[1] > 32 || stroke_pixel[3] > 32,
"expected stroked rounded rect to leave green/alpha pixels"
);
Ok(())
}
#[test]
fn native_facade_decodes_and_draws_encoded_image() -> Result<()> {
let bytes = std::fs::read("tests/assets/pentagon.png").context("read fixture")?;
let image = NativeImage::from_encoded(&bytes).context("decode fixture")?;
assert!(image.width() > 0);
assert!(image.height() > 0);
let mut recorder = NativeRecorder::new(Rect::from_xywh(0.0, 0.0, 32.0, 32.0))?;
recorder.record(|canvas| {
canvas.clear(RgbaLinear::opaque(0.0, 0.0, 0.0));
canvas.draw_image_rect(&image, Rect::from_xywh(0.0, 0.0, 32.0, 32.0), 1.0);
});
let frame = recorder.render_raw(SurfaceOptions::default(), RawFrameOptions::default())?;
assert!(frame.pixels().iter().any(|channel| *channel != 0));
Ok(())
}
#[test]
fn engine_auto_resolves_and_draws() -> Result<()> {
let backend = NativeBackend::new();
let mut surface = backend.create_surface(
16,
16,
SurfaceOptions {
engine: RenderEngine::Auto,
..SurfaceOptions::default()
},
)?;
let kind = surface.engine();
assert!(matches!(kind, EngineKind::Cpu | EngineKind::Gpu));
surface.with_canvas(|canvas| {
canvas.clear(RgbaLinear::opaque(0.0, 0.0, 0.0));
canvas.draw_rect(
Rect::from_xywh(2.0, 2.0, 10.0, 10.0),
&NativePaint::fill(RgbaLinear::opaque(1.0, 0.0, 0.0)),
);
});
surface.flush();
let frame = surface.read_pixels()?;
assert!(frame.pixels().iter().any(|channel| *channel != 0));
Ok(())
}
#[test]
fn engine_cpu_is_always_available() -> Result<()> {
let backend = NativeBackend::new();
let mut surface = backend.create_surface(
8,
8,
SurfaceOptions {
engine: RenderEngine::Cpu,
..SurfaceOptions::default()
},
)?;
assert_eq!(surface.engine(), EngineKind::Cpu);
surface.with_canvas(|canvas| {
canvas.clear(RgbaLinear::opaque(0.5, 0.5, 0.5));
});
let frame = surface.read_pixels()?;
assert!(frame.pixels().iter().any(|channel| *channel != 0));
Ok(())
}
#[test]
fn engine_gpu_either_works_or_returns_engine_unavailable() {
let backend = NativeBackend::new();
let result = backend.create_surface(
8,
8,
SurfaceOptions {
engine: RenderEngine::Gpu,
..SurfaceOptions::default()
},
);
match result {
Ok(s) => assert_eq!(s.engine(), EngineKind::Gpu),
Err(NativeError::EngineUnavailable {
engine: RenderEngine::Gpu,
..
}) => {}
Err(other) => panic!("unexpected error from Gpu request: {other}"),
}
}
#[test]
fn engine_status_reports_typed_fields() {
let backend = NativeBackend::new();
let auto = backend.engine_status(RenderEngine::Auto);
let cpu = backend.engine_status(RenderEngine::Cpu);
assert_eq!(cpu.renderer, EngineKind::Cpu);
assert!(cpu.api.is_none(), "CPU pin should not advertise a GPU API");
assert!(cpu.threads >= 1);
assert_eq!(
auto.is_gpu_available,
matches!(auto.renderer, EngineKind::Gpu),
"Auto-resolved kind should match is_gpu_available",
);
let gpu = backend.engine_status(RenderEngine::Gpu);
if gpu.is_gpu_available {
assert_eq!(gpu.renderer, EngineKind::Gpu);
}
}
#[test]
fn native_facade_draws_visible_text_pixels() -> Result<()> {
let mut recorder = NativeRecorder::new(Rect::from_xywh(0.0, 0.0, 128.0, 64.0))?;
recorder.record(|canvas| {
canvas.clear(RgbaLinear::opaque(0.0, 0.0, 0.0));
canvas.draw_text_box(
"Studio",
Rect::from_xywh(4.0, 4.0, 120.0, 56.0),
&TextBoxOptions {
color: RgbaLinear::opaque(1.0, 1.0, 1.0),
font_size: 32.0,
..TextBoxOptions::default()
},
);
});
let frame = recorder.render_raw(SurfaceOptions::default(), RawFrameOptions::default())?;
assert!(frame.pixels().iter().any(|channel| *channel > 32));
Ok(())
}
#[test]
fn font_axis_tag_parsing() {
assert_eq!("wght".parse::<FontAxisTag>(), Ok(FontAxisTag::WGHT));
assert_eq!("wdth".parse::<FontAxisTag>(), Ok(FontAxisTag::WDTH));
assert!("wgh".parse::<FontAxisTag>().is_err());
assert!("wghts".parse::<FontAxisTag>().is_err());
assert!("wgh❤".parse::<FontAxisTag>().is_err());
assert_eq!(FontAxisTag::WGHT.as_bytes(), b"wght");
}
#[test]
fn text_layout_honors_font_variations_wght_axis() -> Result<()> {
let font_bytes =
std::fs::read("tests/assets/Oswald/Oswald-VariableFont_wght.ttf").context("oswald-vf")?;
let fm = NativeFontManager::new();
fm.register_font_from_data("Oswald", &font_bytes)?;
let engine = NativeTextEngine::new(&fm);
let backend = NativeBackend::new();
let ink_at = |wght: f32| -> Result<usize> {
let mut surface = backend.create_surface(
220,
60,
SurfaceOptions {
engine: RenderEngine::Cpu,
..SurfaceOptions::default()
},
)?;
let style = TextStyle {
font_families: vec!["Oswald".to_string()],
color: RgbaLinear::opaque(1.0, 1.0, 1.0),
font_size: 36.0,
font_variations: vec![FontVariation::new(FontAxisTag::WGHT, wght)],
..TextStyle::default()
};
let layout = engine.layout_text("Studio", &style, 200.0);
surface.with_canvas(|canvas| {
canvas.clear(RgbaLinear::opaque(0.0, 0.0, 0.0));
canvas.draw_text_layout(&layout, 4.0, 4.0);
});
let frame = surface.read_pixels()?;
Ok(frame.pixels().chunks_exact(4).filter(|p| p[0] > 64).count())
};
let thin = ink_at(200.0)?;
let bold = ink_at(700.0)?;
assert!(thin > 0, "thin variant rendered no text");
assert!(
bold > thin,
"wght=700 should produce more ink than wght=200; got thin={thin} bold={bold}",
);
Ok(())
}