use super::*;
fn pixel_is_dark(image: &Image, x: u32, y: u32) -> bool {
let idx = ((y * image.width + x) * 4) as usize;
image.pixels[idx..idx + 3]
.iter()
.all(|channel| *channel < 220)
}
fn pixel_is_bright_rgba(image: &image::RgbaImage, x: u32, y: u32) -> bool {
let pixel = image.get_pixel(x, y).0;
pixel[0] > 200 && pixel[1] > 200 && pixel[2] > 200 && pixel[3] > 0
}
fn image_pixel_rgba(image: &Image, x: u32, y: u32) -> [u8; 4] {
let idx = ((y * image.width + x) * 4) as usize;
[
image.pixels[idx],
image.pixels[idx + 1],
image.pixels[idx + 2],
image.pixels[idx + 3],
]
}
fn count_red_pixels_outside_rect(image: &Image, rect: Rect) -> usize {
let left = rect.left().floor() as i32;
let right = rect.right().ceil() as i32;
let top = rect.top().floor() as i32;
let bottom = rect.bottom().ceil() as i32;
let mut count = 0usize;
for y in 0..image.height as i32 {
for x in 0..image.width as i32 {
if x >= left && x < right && y >= top && y < bottom {
continue;
}
let idx = ((y as u32 * image.width + x as u32) * 4) as usize;
let pixel = &image.pixels[idx..idx + 4];
if pixel[3] > 0 && pixel[0] > 160 && pixel[1] < 80 && pixel[2] < 80 {
count += 1;
}
}
}
count
}
#[test]
fn test_renderer_creation() {
let theme = Theme::default();
let renderer = SkiaRenderer::new(800, 600, theme);
assert!(renderer.is_ok());
let renderer = renderer.unwrap();
assert_eq!(renderer.width, 800);
assert_eq!(renderer.height, 600);
}
#[test]
fn test_set_dpi_scale_sanitizes_invalid_values() {
let theme = Theme::default();
let mut renderer = SkiaRenderer::new(100, 100, theme).unwrap();
renderer.set_dpi_scale(2.5);
assert!((renderer.dpi_scale() - 2.5).abs() < f32::EPSILON);
renderer.set_dpi_scale(0.0);
assert!((renderer.dpi_scale() - 1.0).abs() < f32::EPSILON);
renderer.set_dpi_scale(-3.0);
assert!((renderer.dpi_scale() - 1.0).abs() < f32::EPSILON);
renderer.set_dpi_scale(f32::NAN);
assert!((renderer.dpi_scale() - 1.0).abs() < f32::EPSILON);
renderer.set_dpi_scale(f32::INFINITY);
assert!((renderer.dpi_scale() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_plot_area_calculation() {
let area = calculate_plot_area(800, 600, 0.1);
assert_eq!(area.left(), 80.0);
assert_eq!(area.top(), 60.0);
assert_eq!(area.width(), 640.0);
assert_eq!(area.height(), 480.0);
}
#[test]
fn test_data_to_pixel_mapping() {
let plot_area = Rect::from_xywh(100.0, 100.0, 600.0, 400.0).unwrap();
let (px, py) = map_data_to_pixels(
1.5, 2.5, 1.0, 2.0, 2.0, 3.0, plot_area,
);
assert_eq!(px, 400.0); assert_eq!(py, 300.0); }
#[test]
fn test_tick_generation() {
let ticks = generate_ticks(0.0, 10.0, 5);
assert!(!ticks.is_empty());
assert!(ticks[0] >= 0.0);
assert!(ticks.last().unwrap() <= &10.0);
let ticks = generate_ticks(5.0, 5.0, 3);
assert_eq!(ticks, vec![5.0, 5.0]);
}
#[test]
fn test_compute_colorbar_ticks_formats_log_decades_and_minor_ticks() {
let ticks = compute_colorbar_ticks(1e-5, 1e3, &crate::axes::AxisScale::Log, true);
assert_eq!(ticks.major_labels.first().map(String::as_str), Some("10⁻⁵"));
assert_eq!(ticks.major_labels.last().map(String::as_str), Some("10³"));
assert!(ticks.minor_values.contains(&2e-5));
assert!(ticks.minor_values.contains(&900.0));
}
#[test]
fn test_colorbar_layout_metrics_keep_rotated_label_after_tick_labels() {
let metrics = super::compute_colorbar_layout_metrics(20.0, 12.0, 36.0, Some(14.0));
assert!((metrics.major_tick_width - 6.0).abs() < 1e-6);
assert!((metrics.minor_tick_width - 3.6).abs() < 1e-6);
assert!(metrics.tick_label_x_offset > 20.0);
assert_eq!(metrics.rotated_label_center_x_offset, Some(78.0));
assert!((metrics.total_extent - 85.0).abs() < 1e-6);
}
#[test]
fn test_colorbar_major_label_top_centers_label_on_tick() {
let tick_y = 48.0;
let label_center_from_top = 9.5;
let label_top = super::colorbar_major_label_top(tick_y, label_center_from_top);
assert!(((label_top + label_center_from_top) - tick_y).abs() < f32::EPSILON);
}
#[test]
fn test_colorbar_major_label_anchor_center_uses_base_center_for_log_decades() {
let anchor_center = super::colorbar_major_label_anchor_center_from_top(
&crate::axes::AxisScale::Log,
"10²",
8.0,
Some(6.0),
);
assert!((anchor_center - 6.0).abs() < f32::EPSILON);
}
#[test]
fn test_colorbar_major_label_anchor_center_keeps_rendered_center_for_non_log_labels() {
let anchor_center = super::colorbar_major_label_anchor_center_from_top(
&crate::axes::AxisScale::Linear,
"6",
8.0,
Some(6.0),
);
assert!((anchor_center - 8.0).abs() < f32::EPSILON);
}
#[test]
fn test_draw_pixel_aligned_solid_rectangle_blends_transparent_fill() {
let theme = Theme::default();
let mut renderer = SkiaRenderer::new(6, 6, theme).unwrap();
renderer
.pixmap
.fill(crate::render::Color::new(0, 0, 255).to_tiny_skia_color());
renderer
.draw_pixel_aligned_solid_rectangle(
1.0,
1.0,
4.0,
4.0,
crate::render::Color::new_rgba(255, 0, 0, 128),
)
.expect("transparent pixel-aligned fill should composite successfully");
let image = renderer.into_image();
let pixel = image_pixel_rgba(&image, 2, 2);
assert!(
pixel[0] > 0,
"composited fill should retain red contribution"
);
assert!(
pixel[2] > 0,
"composited fill should retain background contribution instead of overwriting it"
);
assert_eq!(pixel[3], 255);
}
#[test]
fn test_draw_pixel_aligned_solid_rectangle_preserves_subpixel_tiles() {
let theme = Theme::default();
let mut renderer = SkiaRenderer::new(8, 8, theme).unwrap();
renderer.pixmap.fill(tiny_skia::Color::TRANSPARENT);
renderer
.draw_pixel_aligned_solid_rectangle(3.2, 1.0, 0.25, 6.0, crate::render::Color::RED)
.expect("subpixel-aligned fill should render via composited fallback");
let image = renderer.into_image();
assert!(
image.pixels.chunks_exact(4).any(|pixel| pixel[3] > 0),
"thin pixel-aligned tiles should still contribute visible coverage"
);
}
#[test]
fn test_draw_axes_with_config_draws_top_and_right_ticks() {
let theme = Theme::default();
let mut renderer = SkiaRenderer::new(120, 100, theme).unwrap();
let plot_area = Rect::from_xywh(20.0, 20.0, 80.0, 60.0).unwrap();
renderer
.draw_axes_with_config(
plot_area,
&[60.0],
&[50.0],
&[],
&[],
&TickDirection::Inside,
&TickSides::all(),
Color::BLACK,
1.0,
)
.unwrap();
let image = renderer.into_image();
assert!(pixel_is_dark(&image, 60, 20));
assert!(pixel_is_dark(&image, 100, 50));
assert!(pixel_is_dark(&image, 60, 24));
assert!(pixel_is_dark(&image, 96, 50));
}
#[test]
fn test_draw_axes_with_config_respects_bottom_left_ticks() {
let theme = Theme::default();
let mut renderer = SkiaRenderer::new(120, 100, theme).unwrap();
let plot_area = Rect::from_xywh(20.0, 20.0, 80.0, 60.0).unwrap();
renderer
.draw_axes_with_config(
plot_area,
&[60.0],
&[50.0],
&[],
&[],
&TickDirection::Inside,
&TickSides::bottom_left(),
Color::BLACK,
1.0,
)
.unwrap();
let image = renderer.into_image();
assert!(pixel_is_dark(&image, 60, 20));
assert!(pixel_is_dark(&image, 100, 50));
assert!(!pixel_is_dark(&image, 60, 24));
assert!(!pixel_is_dark(&image, 96, 50));
assert!(pixel_is_dark(&image, 60, 76));
assert!(pixel_is_dark(&image, 24, 50));
}
#[test]
fn test_draw_axis_labels_at_handles_collapsed_ranges() {
let theme = Theme::default();
let mut renderer = SkiaRenderer::new(120, 100, theme).unwrap();
let plot_area = LayoutRect {
left: 20.0,
top: 20.0,
right: 100.0,
bottom: 80.0,
};
renderer
.draw_axis_labels_at(
&plot_area,
1.0,
1.0,
2.0,
2.0,
&[1.0],
&[2.0],
88.0,
18.0,
10.0,
Color::BLACK,
100.0,
true,
false,
)
.expect("collapsed ranges should use centered label placement");
}
#[test]
fn test_draw_polyline_clipped_keeps_pixels_inside_clip_rect() {
let theme = Theme::default();
let mut renderer = SkiaRenderer::new(120, 120, theme).unwrap();
let clip_rect = Rect::from_xywh(20.0, 20.0, 80.0, 80.0).unwrap();
renderer
.draw_polyline_clipped(
&[(20.0, 20.0), (100.0, 100.0)],
Color::new(220, 20, 20),
18.0,
LineStyle::Solid,
(
clip_rect.x(),
clip_rect.y(),
clip_rect.width(),
clip_rect.height(),
),
)
.unwrap();
let image = renderer.into_image();
assert_eq!(count_red_pixels_outside_rect(&image, clip_rect), 0);
}
#[test]
fn test_draw_datashader_image_scales_into_plot_area() {
let theme = Theme::dark();
let mut renderer = SkiaRenderer::new(80, 60, theme).unwrap();
renderer.clear();
let image = crate::data::DataShaderImage::new(1, 1, vec![255, 255, 255, 255]);
let plot_area = Rect::from_xywh(10.0, 15.0, 30.0, 20.0).unwrap();
renderer.draw_datashader_image(&image, plot_area).unwrap();
let png = renderer.encode_png_bytes().unwrap();
let rendered = image::load_from_memory(&png).unwrap().to_rgba8();
assert!(pixel_is_bright_rgba(&rendered, 15, 20));
assert!(pixel_is_bright_rgba(&rendered, 35, 30));
assert!(!pixel_is_bright_rgba(&rendered, 5, 5));
assert!(!pixel_is_bright_rgba(&rendered, 50, 45));
}
#[cfg(feature = "typst-math")]
#[test]
fn test_typst_raster_uses_native_1x_scale() {
let theme = Theme::default();
let mut renderer = SkiaRenderer::new(400, 300, theme).unwrap();
renderer.set_dpi_scale(1.0);
renderer.set_text_engine_mode(TextEngineMode::Typst);
let rendered_native = typst_text::render_raster(
"scale-check",
12.0,
Color::BLACK,
0.0,
"typst native scale test",
)
.unwrap();
let rendered_second = typst_text::render_raster(
"scale-check",
12.0,
Color::BLACK,
0.0,
"typst native scale test",
)
.unwrap();
assert_eq!(
rendered_native.pixmap.width(),
rendered_second.pixmap.width()
);
assert_eq!(
rendered_native.pixmap.height(),
rendered_second.pixmap.height()
);
assert!(
(rendered_native.pixmap.width() as f32 - rendered_native.width).abs() <= 1.0,
"native raster width should align with logical width: pixel={} logical={}",
rendered_native.pixmap.width(),
rendered_native.width
);
assert!(
(rendered_native.pixmap.height() as f32 - rendered_native.height).abs() <= 1.0,
"native raster height should align with logical height: pixel={} logical={}",
rendered_native.pixmap.height(),
rendered_native.height
);
}
#[test]
fn test_renderer_dimensions() {
let theme = Theme::default();
let renderer = SkiaRenderer::new(800, 600, theme).unwrap();
assert_eq!(renderer.width(), 800);
assert_eq!(renderer.height(), 600);
}
#[test]
fn test_draw_subplot() {
use crate::core::plot::Image;
let theme = Theme::default();
let mut main_renderer = SkiaRenderer::new(800, 600, theme.clone()).unwrap();
let subplot_renderer = SkiaRenderer::new(200, 150, theme).unwrap();
let subplot_image = subplot_renderer.into_image();
let result = main_renderer.draw_subplot(subplot_image, 10, 20);
assert!(result.is_ok());
}
#[test]
fn test_draw_subplot_bounds_checking() {
use crate::core::plot::Image;
let theme = Theme::default();
let mut main_renderer = SkiaRenderer::new(400, 300, theme.clone()).unwrap();
let subplot_renderer = SkiaRenderer::new(200, 150, theme).unwrap();
let subplot_image = subplot_renderer.into_image();
assert!(
main_renderer
.draw_subplot(subplot_image.clone(), 0, 0)
.is_ok()
);
assert!(
main_renderer
.draw_subplot(subplot_image.clone(), 100, 50)
.is_ok()
);
assert!(main_renderer.draw_subplot(subplot_image, 200, 150).is_ok());
}
#[test]
fn test_to_image_conversion() {
let theme = Theme::default();
let renderer = SkiaRenderer::new(400, 300, theme).unwrap();
let image = renderer.into_image();
assert_eq!(image.width, 400);
assert_eq!(image.height, 300);
assert_eq!(image.pixels.len(), 400 * 300 * 4); }