mod common;
use common::{
artist, axes, data, has_error_at, image, image_figure, indexed_image, is_3d, mapped_image,
parent,
};
use ironlab::ir::{
Artist, ImagePlacement, IrError, IssueKind, NdArray, NdArrayElement, PixelRange, Projection,
View3d,
};
use ironlab::prelude::*;
const RGB: [u8; 18] = [
255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 128, 128, 128, 255, 255, 255,
];
const RGBA: [u8; 16] = [255, 0, 0, 255, 0, 255, 0, 128, 0, 0, 255, 0, 10, 20, 30, 40];
fn sample_pixels() -> Pixels {
Pixels::from_rgb8(2, 3, &RGB).expect("18 bytes make 2 by 3 RGB pixels")
}
fn sample_bytes() -> ByteMatrix {
ByteMatrix::from_fn(2, 3, |row, col| (row * 3 + col) as u8)
}
fn sample_floats() -> Matrix {
Matrix::from_fn(2, 3, |row, col| (row * 3 + col) as f64 / 5.0)
}
fn placement(fig: &Figure, id: NodeId) -> ImagePlacement {
match artist(fig, id) {
Artist::Image(a) => a.placement,
Artist::IndexedImage(a) => a.placement,
Artist::MappedImage(a) => a.placement,
other => panic!("expected an image, found {other:?}"),
}
}
#[test]
fn image_stores_the_pixels_as_bytes_with_shape_rows_by_cols_by_channels() {
let mut fig = Figure::new();
let mut ax = fig.axes(0, 0);
let rgb = ax.image(&sample_pixels()).id();
let rgba = ax.image(&Pixels::from_rgba8(2, 2, &RGBA).unwrap()).id();
let stored = data(&fig, image(&fig, rgb).pixels);
assert_eq!(stored.shape, vec![2, 3, 3]);
assert_eq!(stored.element(), NdArrayElement::U8);
assert_eq!(stored.as_u8(), Some(&RGB[..]));
let stored = data(&fig, image(&fig, rgba).pixels);
assert_eq!(stored.shape, vec![2, 2, 4]);
assert_eq!(stored.element(), NdArrayElement::U8);
assert_eq!(stored.as_u8(), Some(&RGBA[..]));
assert!(fig.validate().is_valid());
}
#[test]
fn every_image_kind_defaults_to_the_floor_plane_with_no_ranges_and_lenient_policies() {
let mut fig = Figure::new();
let mut ax = fig.axes(0, 0);
let plain = ax.image(&sample_pixels()).id();
let indexed = ax.indexed_image(sample_bytes()).id();
let mapped = ax.mapped_image(sample_floats()).id();
for id in [plain, indexed, mapped] {
let p = placement(&fig, id);
assert_eq!(p.plane, ImagePlane::Xy { z: None }, "{id}");
assert_eq!(p.columns, None, "{id}");
assert_eq!(p.rows, None, "{id}");
assert_eq!(artist(&fig, id).display_name(), None, "{id}");
assert!(artist(&fig, id).visible(), "{id}");
assert!(!is_3d(parent(&fig, id)), "{id}");
}
let transparent = [OutOfRange::Transparent; 3];
let i = indexed_image(&fig, indexed);
assert_eq!([i.below, i.above, i.non_finite], transparent);
let m = mapped_image(&fig, mapped);
assert_eq!([m.below, m.above, m.non_finite], transparent);
assert!(fig.validate().is_valid());
}
#[test]
fn indexed_image_stores_bytes_from_a_byte_matrix_and_floats_from_a_matrix() {
let bytes = sample_bytes();
let floats = sample_floats();
let mut fig = Figure::new();
let mut ax = fig.axes(0, 0);
let bytes_by_ref = ax.indexed_image(&bytes).id();
let floats_by_ref = ax.indexed_image(&floats).id();
let bytes_by_value = ax.indexed_image(sample_bytes()).id();
let floats_by_value = ax.indexed_image(sample_floats()).id();
let expected_bytes = NdArray::from_shape_u8(vec![2, 3], bytes.values().to_vec()).unwrap();
let expected_floats = NdArray::from_shape(vec![2, 3], floats.values().to_vec()).unwrap();
for id in [bytes_by_ref, bytes_by_value] {
let stored = data(&fig, indexed_image(&fig, id).indices);
assert_eq!(stored.element(), NdArrayElement::U8, "{id}");
assert_eq!(stored, &expected_bytes, "{id}");
}
for id in [floats_by_ref, floats_by_value] {
let stored = data(&fig, indexed_image(&fig, id).indices);
assert_eq!(stored.element(), NdArrayElement::F64, "{id}");
assert_eq!(stored, &expected_floats, "{id}");
}
assert!(fig.validate().is_valid());
}
#[test]
fn mapped_image_stores_bytes_from_a_byte_matrix_and_floats_from_a_matrix() {
let bytes = sample_bytes();
let floats = Matrix::from_rows(&[[0.0, 0.5, f64::NAN], [1.0, -0.5, 1.5]]);
let mut fig = Figure::new();
let mut ax = fig.axes(0, 0);
let bytes_by_ref = ax.mapped_image(&bytes).id();
let floats_by_ref = ax.mapped_image(&floats).id();
let bytes_by_value = ax.mapped_image(sample_bytes()).id();
let floats_by_value = ax.mapped_image(floats.clone()).id();
let expected_bytes = NdArray::from_shape_u8(vec![2, 3], bytes.values().to_vec()).unwrap();
let expected_floats = NdArray::from_shape(vec![2, 3], floats.values().to_vec()).unwrap();
for id in [bytes_by_ref, bytes_by_value] {
let stored = data(&fig, mapped_image(&fig, id).values);
assert_eq!(stored.element(), NdArrayElement::U8, "{id}");
assert_eq!(stored, &expected_bytes, "{id}");
}
for id in [floats_by_ref, floats_by_value] {
let stored = data(&fig, mapped_image(&fig, id).values);
assert_eq!(stored.element(), NdArrayElement::F64, "{id}");
assert_eq!(stored, &expected_floats, "{id}");
}
assert!(fig.validate().is_valid());
}
#[test]
fn pixels_from_bytes_keep_the_bytes_and_report_their_shape() {
let rgb = Pixels::from_rgb8(2, 3, &RGB).unwrap();
assert_eq!((rgb.rows(), rgb.cols(), rgb.channels()), (2, 3, 3));
assert_eq!(rgb.bytes(), &RGB[..]);
let rgba = Pixels::from_rgba8(2, 2, &RGBA).unwrap();
assert_eq!((rgba.rows(), rgba.cols(), rgba.channels()), (2, 2, 4));
assert_eq!(rgba.bytes(), &RGBA[..]);
}
#[test]
fn pixels_from_bytes_refuse_a_byte_count_that_does_not_match_the_shape() {
let invalid_shape = |result: Result<Pixels, Error>| {
matches!(result, Err(Error::Ir(IrError::InvalidShape { .. })))
};
assert!(invalid_shape(Pixels::from_rgb8(2, 3, &RGB[..17])));
assert!(invalid_shape(Pixels::from_rgb8(2, 3, &[0; 19])));
assert!(invalid_shape(Pixels::from_rgba8(2, 2, &[0; 12])));
assert!(invalid_shape(Pixels::from_rgb8(2, 2, &[0; 16])));
assert!(invalid_shape(Pixels::from_rgb8(usize::MAX, 2, &[0; 6])));
assert!(invalid_shape(Pixels::from_rgba8(usize::MAX, 2, &[0; 8])));
assert!(invalid_shape(Pixels::from_rgb8(usize::MAX / 2, 1, &[0; 3])));
assert!(invalid_shape(Pixels::from_rgba8(
usize::MAX / 3,
1,
&[0; 4]
)));
assert!(Pixels::from_rgb8(0, 3, &[]).is_ok());
}
#[test]
fn rgb_from_fn_quantises_components_in_row_major_order_and_drops_alpha() {
let pixels = Pixels::rgb_from_fn(2, 2, |row, col| match (row, col) {
(0, 0) => Color::rgb(0.5, 0.0, 1.0),
(0, 1) => Color::rgb(1.2, -0.3, 0.25),
(1, 0) => Color::rgba(1.0, 1.0, 1.0, 0.5),
_ => Color::rgb(f32::NAN, 0.5, 0.5),
});
assert_eq!((pixels.rows(), pixels.cols(), pixels.channels()), (2, 2, 3));
assert_eq!(
pixels.bytes(),
&[128, 0, 255, 255, 0, 64, 255, 255, 255, 0, 128, 128]
);
}
#[test]
fn rgba_from_fn_keeps_alpha_as_a_fourth_channel() {
let pixels = Pixels::rgba_from_fn(1, 3, |_, col| match col {
0 => Color::rgba(1.0, 0.0, 0.0, 0.5),
1 => Color::rgb(0.0, 1.0, 0.0),
_ => Color::rgba(0.0, 0.0, 1.0, 0.0),
});
assert_eq!((pixels.rows(), pixels.cols(), pixels.channels()), (1, 3, 4));
assert_eq!(
pixels.bytes(),
&[255, 0, 0, 128, 0, 255, 0, 255, 0, 0, 255, 0]
);
}
#[test]
fn pixels_from_planes_quantise_and_clamp_each_component() {
let r = Matrix::from_rows(&[[0.0, 0.5], [1.0, 1.2]]);
let g = Matrix::from_rows(&[[1.0, 0.0], [-0.3, 0.5]]);
let b = Matrix::from_rows(&[[0.5, 1.0], [0.0, 0.0]]);
let pixels = Pixels::from_planes(&r, &g, &b).unwrap();
assert_eq!((pixels.rows(), pixels.cols(), pixels.channels()), (2, 2, 3));
assert_eq!(
pixels.bytes(),
&[0, 255, 128, 128, 0, 255, 255, 0, 0, 255, 128, 0]
);
}
#[test]
fn pixels_from_planes_refuse_planes_of_different_shapes() {
let wide = Matrix::zeros(2, 3);
let tall = Matrix::zeros(3, 2);
let shape_mismatch =
|result: Result<Pixels, Error>| matches!(result, Err(Error::PlaneShapeMismatch { .. }));
assert!(shape_mismatch(Pixels::from_planes(&tall, &wide, &wide)));
assert!(shape_mismatch(Pixels::from_planes(&wide, &tall, &wide)));
assert!(shape_mismatch(Pixels::from_planes(&wide, &wide, &tall)));
assert!(Pixels::from_planes(&wide, &wide, &wide).is_ok());
}
#[test]
fn pixels_from_planes_refuse_a_non_finite_component() {
let good = Matrix::from_rows(&[[0.5, 0.5]]);
for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let plane = Matrix::from_rows(&[[0.5, bad]]);
let planes = [
(Pixels::from_planes(&plane, &good, &good), 0),
(Pixels::from_planes(&good, &plane, &good), 1),
(Pixels::from_planes(&good, &good, &plane), 2),
];
for (result, channel) in planes {
assert!(
matches!(
result,
Err(Error::NonFiniteComponent { row: 0, col: 1, channel: c }) if c == channel
),
"{bad} in channel {channel}"
);
}
}
}
#[test]
fn with_alpha_adds_a_fourth_channel_to_three_channel_pixels() {
let alpha = Matrix::from_rows(&[[1.0, 0.5, 0.0], [0.0, 0.5, 1.0]]);
let pixels = sample_pixels().with_alpha(&alpha).unwrap();
assert_eq!((pixels.rows(), pixels.cols(), pixels.channels()), (2, 3, 4));
assert_eq!(
pixels.bytes(),
&[
255, 0, 0, 255, 0, 255, 0, 128, 0, 0, 255, 0, 0, 0, 0, 0, 128, 128, 128, 128, 255, 255,
255, 255
]
);
}
#[test]
fn with_alpha_replaces_an_existing_alpha_channel() {
let alpha = Matrix::from_rows(&[[1.0, 1.5], [0.0, 0.5]]);
let pixels = Pixels::from_rgba8(2, 2, &RGBA)
.unwrap()
.with_alpha(&alpha)
.unwrap();
assert_eq!((pixels.rows(), pixels.cols(), pixels.channels()), (2, 2, 4));
assert_eq!(
pixels.bytes(),
&[
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 0, 10, 20, 30, 128
]
);
}
#[test]
fn with_alpha_refuses_a_shape_mismatch_and_a_non_finite_alpha() {
assert!(matches!(
sample_pixels().with_alpha(&Matrix::zeros(3, 2)),
Err(Error::PlaneShapeMismatch {
expected: [2, 3],
found: [3, 2]
})
));
assert!(matches!(
sample_pixels().with_alpha(&Matrix::zeros(2, 2)),
Err(Error::PlaneShapeMismatch {
expected: [2, 3],
found: [2, 2]
})
));
let nan = Matrix::from_fn(
2,
3,
|row, col| {
if (row, col) == (1, 2) { f64::NAN } else { 1.0 }
},
);
assert!(matches!(
sample_pixels().with_alpha(&nan),
Err(Error::NonFiniteComponent {
row: 1,
col: 2,
channel: 3
})
));
assert!(sample_pixels().with_alpha(&Matrix::zeros(2, 3)).is_ok());
}
#[test]
fn byte_matrix_from_fn_fills_row_major() {
let m = ByteMatrix::from_fn(2, 3, |row, col| (10 * row + col) as u8);
assert_eq!((m.rows(), m.cols()), (2, 3));
assert_eq!(m.values(), &[0, 1, 2, 10, 11, 12]);
assert_eq!(m[(0, 2)], 2);
assert_eq!(m[(1, 0)], 10);
}
#[test]
fn byte_matrix_from_rows_and_zeros_agree_with_from_fn() {
let m = ByteMatrix::from_rows(&[[0u8, 1, 2], [10, 11, 12]]);
assert_eq!((m.rows(), m.cols()), (2, 3));
assert_eq!(
m,
ByteMatrix::from_fn(2, 3, |row, col| (10 * row + col) as u8)
);
assert_eq!(m.into_values(), vec![0, 1, 2, 10, 11, 12]);
let mut z = ByteMatrix::zeros(2, 2);
assert_eq!((z.rows(), z.cols()), (2, 2));
assert_eq!(z.values(), &[0; 4]);
z[(0, 1)] = 5;
assert_eq!(z.values(), &[0, 5, 0, 0]);
}
#[test]
#[should_panic(expected = "matrix rows have different lengths")]
fn byte_matrix_from_rows_panics_on_ragged_rows() {
let _ = ByteMatrix::from_rows(&[vec![0u8, 1], vec![2]]);
}
#[test]
#[should_panic(expected = "out of range")]
fn byte_matrix_index_past_the_last_column_panics() {
let m = ByteMatrix::zeros(2, 2);
let _ = m[(0, 2)];
}
#[test]
#[should_panic(expected = "out of range")]
fn byte_matrix_index_mut_past_the_last_column_panics() {
let mut m = ByteMatrix::zeros(2, 2);
m[(0, 2)] = 1;
}
#[test]
fn image_containers_are_clonable_comparable_and_thread_safe() {
fn value_type<T: Clone + std::fmt::Debug + PartialEq + Send + Sync + 'static>() {}
value_type::<Pixels>();
value_type::<ByteMatrix>();
value_type::<ImageValues>();
}
mod prelude_only {
use ironlab::prelude::*;
#[test]
fn the_prelude_exports_the_image_types_and_handles() {
fn place(image: &mut ImageMut<'_>) {
image.pixel_columns(0.0, 1.0);
}
fn classify(image: &mut IndexedImageMut<'_>) {
image.above(OutOfRange::Clamp);
}
fn map(image: &mut MappedImageMut<'_>) {
image.plane(ImagePlane::Xy { z: None });
}
let bytes = ByteMatrix::zeros(1, 1);
let indices: ImageValues = ImageValues::from(&bytes);
let pixels: Pixels = Pixels::rgb_from_fn(1, 1, |_, _| Color::WHITE);
let mut fig = Figure::new();
let mut ax = fig.axes(0, 0);
place(&mut ax.image(&pixels));
classify(&mut ax.indexed_image(indices));
map(&mut ax.mapped_image(Matrix::zeros(1, 1)));
assert!(fig.validate().is_valid());
}
}
#[test]
fn pixel_columns_and_pixel_rows_set_the_pixel_centres_on_every_kind() {
let mut fig = Figure::new();
let mut ax = fig.axes(0, 0);
let plain = ax
.image(&sample_pixels())
.pixel_columns(-1.5, 1.5)
.pixel_rows(2.0, 0.0)
.display_name("photo")
.id();
let indexed = ax
.indexed_image(sample_bytes())
.pixel_columns(-1.5, 1.5)
.pixel_rows(2.0, 0.0)
.id();
let mapped = ax
.mapped_image(sample_floats())
.pixel_columns(-1.5, 1.5)
.pixel_rows(2.0, 0.0)
.id();
let columns_only = ax.image(&sample_pixels()).pixel_columns(0.0, 10.0).id();
let rows_only = ax.mapped_image(sample_floats()).pixel_rows(-2.0, -1.0).id();
let range = |first, last| Some(PixelRange { first, last });
for id in [plain, indexed, mapped] {
let p = placement(&fig, id);
assert_eq!(p.columns, range(-1.5, 1.5), "{id}");
assert_eq!(p.rows, range(2.0, 0.0), "{id}");
assert_eq!(p.plane, ImagePlane::Xy { z: None }, "{id}");
}
assert_eq!(image(&fig, plain).display_name, Some(Text::new("photo")));
let p = placement(&fig, columns_only);
assert_eq!((p.columns, p.rows), (range(0.0, 10.0), None));
let p = placement(&fig, rows_only);
assert_eq!((p.columns, p.rows), (None, range(-2.0, -1.0)));
assert!(fig.validate().is_valid());
}
#[test]
fn wall_planes_promote_the_axes_to_3d_with_the_default_view_on_every_kind() {
let xz = ImagePlane::Xz { y: Some(0.5) };
let yz = ImagePlane::Yz { x: None };
let mut fig = Figure::new().tiles(2, 3);
let image_xz = fig.axes(0, 0).image(&sample_pixels()).plane(xz).id();
let indexed_xz = fig.axes(0, 1).indexed_image(sample_bytes()).plane(xz).id();
let mapped_xz = fig.axes(0, 2).mapped_image(sample_floats()).plane(xz).id();
let image_yz = fig.axes(1, 0).image(&sample_pixels()).plane(yz).id();
let indexed_yz = fig.axes(1, 1).indexed_image(sample_bytes()).plane(yz).id();
let mapped_yz = fig.axes(1, 2).mapped_image(sample_floats()).plane(yz).id();
let placed = [
(image_xz, xz),
(indexed_xz, xz),
(mapped_xz, xz),
(image_yz, yz),
(indexed_yz, yz),
(mapped_yz, yz),
];
for (id, plane) in placed {
assert_eq!(placement(&fig, id).plane, plane, "{id}");
assert_eq!(
parent(&fig, id).projection,
Projection::ThreeD {
view3d: View3d::default()
},
"{id}"
);
}
let report = fig.validate();
assert!(report.is_valid(), "{report:?}");
}
#[test]
fn the_floor_plane_with_an_offset_leaves_a_2d_axes_two_dimensional() {
let floor = ImagePlane::Xy { z: Some(1.0) };
let mut fig = Figure::new().tiles(1, 2);
let mut ax = fig.axes(0, 0);
let plain = ax.image(&sample_pixels()).plane(floor).id();
let indexed = ax.indexed_image(sample_bytes()).plane(floor).id();
let mapped = ax.mapped_image(sample_floats()).plane(floor).id();
let on_the_floor_of_3d = fig
.axes3(0, 1)
.image(&sample_pixels())
.plane(ImagePlane::Xy { z: None })
.id();
for id in [plain, indexed, mapped] {
assert_eq!(placement(&fig, id).plane, floor, "{id}");
assert!(!is_3d(parent(&fig, id)), "{id}");
}
assert!(is_3d(parent(&fig, on_the_floor_of_3d)));
let report = fig.validate();
assert!(report.is_valid(), "{report:?}");
assert!(report.warnings.is_empty(), "{report:?}");
}
#[test]
fn a_wall_plane_keeps_the_view_of_an_axes_that_is_already_3d() {
let mut fig = Figure::new();
let axes_id = fig.axes3(0, 0).view(45.0, 10.0).id();
let before = axes(&fig, axes_id).projection;
assert_eq!(
before,
Projection::ThreeD {
view3d: View3d {
azimuth_deg: 45.0,
elevation_deg: 10.0,
..View3d::default()
}
}
);
let id = fig
.axes(0, 0)
.mapped_image(sample_floats())
.plane(ImagePlane::Yz { x: Some(-1.0) })
.id();
assert_eq!(parent(&fig, id).projection, before);
}
#[test]
fn out_of_range_policies_map_to_their_ir_fields_on_both_mapped_kinds() {
let red = Color::rgb(1.0, 0.0, 0.0);
let mut fig = Figure::new();
let mut ax = fig.axes(0, 0);
let indexed = ax
.indexed_image(sample_bytes())
.below(red)
.above(OutOfRange::Clamp)
.non_finite(OutOfRange::Strict)
.display_name("classes")
.id();
let mapped = ax
.mapped_image(sample_floats())
.below(OutOfRange::Strict)
.above(red)
.non_finite(OutOfRange::Clamp)
.display_name("$\\phi$")
.id();
let only_below = ax.mapped_image(sample_floats()).below(red).id();
let only_above = ax.indexed_image(sample_bytes()).above(red).id();
let fixed = OutOfRange::Rgba { color: red };
let i = indexed_image(&fig, indexed);
assert_eq!(
[i.below, i.above, i.non_finite],
[fixed, OutOfRange::Clamp, OutOfRange::Strict]
);
assert_eq!(i.display_name, Some(Text::new("classes")));
let m = mapped_image(&fig, mapped);
assert_eq!(
[m.below, m.above, m.non_finite],
[OutOfRange::Strict, fixed, OutOfRange::Clamp]
);
assert_eq!(m.display_name, Some(Text::new("$\\phi$")));
let m = mapped_image(&fig, only_below);
assert_eq!(
[m.below, m.above, m.non_finite],
[fixed, OutOfRange::Transparent, OutOfRange::Transparent]
);
let i = indexed_image(&fig, only_above);
assert_eq!(
[i.below, i.above, i.non_finite],
[OutOfRange::Transparent, fixed, OutOfRange::Transparent]
);
}
#[test]
fn a_strict_policy_is_reported_at_the_handles_id_and_the_default_policy_is_lenient() {
let with_nan = Matrix::from_fn(
2,
3,
|row, col| {
if (row, col) == (0, 1) { f64::NAN } else { 0.5 }
},
);
let mut fig = Figure::new();
let mut ax = fig.axes(0, 0);
let strict = ax
.mapped_image(&with_nan)
.non_finite(OutOfRange::Strict)
.id();
let lenient = ax.mapped_image(&with_nan).id();
let report = fig.validate();
assert!(
has_error_at(&report, IssueKind::PixelOutOfRange, strict),
"{report:?}"
);
assert!(
!report
.errors
.iter()
.any(|issue| issue.node == Some(lenient)),
"{report:?}"
);
}
#[test]
fn non_finite_pixel_centres_and_offsets_are_reported_by_validate_not_a_panic() {
let mut fig = Figure::new();
let mut ax = fig.axes(0, 0);
let nan_centre = ax.image(&sample_pixels()).pixel_columns(f64::NAN, 1.0).id();
let infinite_offset = ax
.mapped_image(sample_floats())
.plane(ImagePlane::Xy {
z: Some(f64::INFINITY),
})
.id();
let report = fig.validate();
for id in [nan_centre, infinite_offset] {
assert!(
has_error_at(&report, IssueKind::InvalidImagePlacement, id),
"{id}: {report:?}"
);
}
}
#[test]
fn one_image_of_each_kind_built_through_the_facade_validates_with_no_issues() {
let report = image_figure().validate();
assert!(report.is_valid(), "{report:?}");
assert!(report.warnings.is_empty(), "{report:?}");
}