diff --git a/plotters-backend/Cargo.toml b/plotters-backend/Cargo.toml index aad934c2..10444dd0 100644 --- a/plotters-backend/Cargo.toml +++ b/plotters-backend/Cargo.toml @@ -12,3 +12,4 @@ readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +num-traits = "0.2.19" diff --git a/plotters-backend/src/error.rs b/plotters-backend/src/error.rs new file mode 100644 index 00000000..2160a6ef --- /dev/null +++ b/plotters-backend/src/error.rs @@ -0,0 +1,136 @@ +use crate::MathError; +use std::error::Error; +/// The error produced by a drawing backend. +#[derive(Debug)] +pub enum DrawingErrorKind { + /// A drawing backend error + DrawingError(E), + /// A font rendering error + FontError(Box), + /// A mathematical operation has failed + MathError(MathError), +} + +impl From for DrawingErrorKind { + fn from(err: MathError) -> Self { + DrawingErrorKind::MathError(err) + } +} + +impl std::fmt::Display for DrawingErrorKind { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + match self { + DrawingErrorKind::DrawingError(e) => write!(fmt, "Drawing backend error: {}", e), + DrawingErrorKind::FontError(e) => write!(fmt, "Font loading error: {}", e), + DrawingErrorKind::MathError(e) => write!(fmt, "Math error: {}", e), + } + } +} + +impl Error for DrawingErrorKind {} + +#[cfg(test)] +mod tests { + use super::*; + use std::fmt; + + #[derive(Debug)] + struct TestBackendError; + + impl fmt::Display for TestBackendError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "backend exploded") + } + } + + impl std::error::Error for TestBackendError {} + + #[derive(Debug)] + struct TestFontError; + + impl fmt::Display for TestFontError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "font exploded") + } + } + + impl std::error::Error for TestFontError {} + + #[test] + fn from_math_error_creates_math_error_variant() { + let err: DrawingErrorKind = MathError::ValueOutOfRange.into(); + + assert!(matches!( + err, + DrawingErrorKind::MathError(MathError::ValueOutOfRange) + )); + } + + #[test] + fn display_formats_drawing_backend_error() { + let err: DrawingErrorKind = + DrawingErrorKind::DrawingError(TestBackendError); + + assert_eq!(err.to_string(), "Drawing backend error: backend exploded"); + } + + #[test] + fn display_formats_font_error() { + let err: DrawingErrorKind = + DrawingErrorKind::FontError(Box::new(TestFontError)); + + assert_eq!(err.to_string(), "Font loading error: font exploded"); + } + + #[test] + fn display_formats_math_error() { + let math_error = MathError::ValueOutOfRange; + let err: DrawingErrorKind = DrawingErrorKind::MathError(math_error); + + assert_eq!(err.to_string(), format!("Math error: {}", math_error)); + } + + #[test] + fn drawing_error_kind_implements_error() { + fn assert_error() {} + + assert_error::>(); + } + + #[test] + fn drawing_error_variant_can_be_matched() { + let err: DrawingErrorKind = + DrawingErrorKind::DrawingError(TestBackendError); + + match err { + DrawingErrorKind::DrawingError(e) => { + assert_eq!(e.to_string(), "backend exploded"); + } + _ => panic!("expected DrawingError variant"), + } + } + + #[test] + fn font_error_variant_can_be_matched() { + let err: DrawingErrorKind = + DrawingErrorKind::FontError(Box::new(TestFontError)); + + match err { + DrawingErrorKind::FontError(e) => { + assert_eq!(e.to_string(), "font exploded"); + } + _ => panic!("expected FontError variant"), + } + } + + #[test] + fn math_error_variant_can_be_matched() { + let err: DrawingErrorKind = + DrawingErrorKind::MathError(MathError::ZeroDivision); + + match err { + DrawingErrorKind::MathError(MathError::ZeroDivision) => {} + _ => panic!("expected MathError::ZeroDivision variant"), + } + } +} diff --git a/plotters-backend/src/lib.rs b/plotters-backend/src/lib.rs index 90066147..e28a6157 100644 --- a/plotters-backend/src/lib.rs +++ b/plotters-backend/src/lib.rs @@ -61,41 +61,35 @@ All the plotters main crate and second-party backends with version "x.y.*" should be compatible, and they should depens on the latest version of `plotters-backend x.y.*` */ +#![warn(clippy::arithmetic_side_effects)] use std::error::Error; +pub mod error; pub mod rasterizer; +pub use error::DrawingErrorKind; +pub mod math_errors; +pub use math_errors::MathError; + mod style; mod text; +mod math_guard; + pub use style::{BackendColor, BackendStyle}; pub use text::{text_anchor, BackendTextStyle, FontFamily, FontStyle, FontTransform}; use text_anchor::{HPos, VPos}; +use crate::math_guard::{ + checked_add_i32, checked_add_u32, checked_add_usize, checked_div_i32, checked_mul_u32, + checked_mul_usize, checked_neg_i32, checked_sub_i32, i32_to_u32_checked, u32_to_i32_checked, + u32_to_usize_checked, +}; + /// A coordinate in the pixel-based backend. The coordinate follows the framebuffer's convention, /// which defines the top-left point as (0, 0). pub type BackendCoord = (i32, i32); -/// The error produced by a drawing backend. -#[derive(Debug)] -pub enum DrawingErrorKind { - /// A drawing backend error - DrawingError(E), - /// A font rendering error - FontError(Box), -} - -impl std::fmt::Display for DrawingErrorKind { - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { - match self { - DrawingErrorKind::DrawingError(e) => write!(fmt, "Drawing backend error: {}", e), - DrawingErrorKind::FontError(e) => write!(fmt, "Font loading error: {}", e), - } - } -} - -impl Error for DrawingErrorKind {} - /// The drawing backend trait, which implements the low-level drawing APIs. /// This trait has a set of default implementation. And the minimal requirement of /// implementing a drawing backend is implementing the `draw_pixel` function. @@ -183,7 +177,7 @@ pub trait DrawingBackend: Sized { } } else { let p: Vec<_> = path.into_iter().collect(); - let v = rasterizer::polygonize(&p[..], style.stroke_width()); + let v = rasterizer::polygonize(&p[..], style.stroke_width())?; return self.fill_polygon(v, &style.color()); } Ok(()) @@ -233,23 +227,26 @@ pub trait DrawingBackend: Sized { .layout_box(text) .map_err(|e| DrawingErrorKind::FontError(Box::new(e)))?; let ((min_x, min_y), (max_x, max_y)) = layout; - let width = max_x - min_x; - let height = max_y - min_y; + let width = checked_sub_i32(max_x, min_x)?; + let height = checked_sub_i32(max_y, min_y)?; let dx = match style.anchor().h_pos { - HPos::Left => 0, - HPos::Right => -width, - HPos::Center => -width / 2, - }; + HPos::Left => Ok(0), + HPos::Right => checked_neg_i32(width), + HPos::Center => checked_div_i32(checked_neg_i32(width)?, 2), + }?; let dy = match style.anchor().v_pos { - VPos::Top => 0, - VPos::Center => -height / 2, - VPos::Bottom => -height, - }; + VPos::Top => Ok(0), + VPos::Center => checked_div_i32(checked_neg_i32(height)?, 2), + VPos::Bottom => checked_neg_i32(height), + }?; let trans = style.transform(); let (w, h) = self.get_size(); let drawing_result = style.draw(text, (0, 0), |x, y, color| { - let (x, y) = trans.transform(x + dx - min_x, y + dy - min_y); - let (x, y) = (pos.0 + x, pos.1 + y); + let (x, y) = trans.transform( + checked_sub_i32(checked_add_i32(x, dx)?, min_x)?, + checked_sub_i32(checked_add_i32(y, dy)?, min_y)?, + )?; + let (x, y) = (checked_add_i32(pos.0, x)?, checked_add_i32(pos.1, y)?); if x >= 0 && x < w as i32 && y >= 0 && y < h as i32 { self.draw_pixel((x, y), color) } else { @@ -278,10 +275,10 @@ pub trait DrawingBackend: Sized { let layout = style .layout_box(text) .map_err(|e| DrawingErrorKind::FontError(Box::new(e)))?; - Ok(( - ((layout.1).0 - (layout.0).0) as u32, - ((layout.1).1 - (layout.0).1) as u32, - )) + let width = checked_sub_i32((layout.1).0, (layout.0).0)?; + let height = checked_sub_i32((layout.1).1, (layout.0).1)?; + + Ok((i32_to_u32_checked(width)?, i32_to_u32_checked(height)?)) } /// Blit a bitmap on to the backend. @@ -301,22 +298,33 @@ pub trait DrawingBackend: Sized { let (w, h) = self.get_size(); for dx in 0..iw { - if pos.0 + dx as i32 >= w as i32 { + if checked_add_i32(pos.0, u32_to_i32_checked(dx)?)? >= u32_to_i32_checked(w)? { break; } for dy in 0..ih { - if pos.1 + dy as i32 >= h as i32 { + if checked_add_i32(pos.1, u32_to_i32_checked(dy)?)? >= u32_to_i32_checked(h)? { break; } // FIXME: This assume we have RGB image buffer - let r = src[(dx + dy * iw) as usize * 3]; - let g = src[(dx + dy * iw) as usize * 3 + 1]; - let b = src[(dx + dy * iw) as usize * 3 + 2]; + let src_calc = + u32_to_usize_checked(checked_add_u32(dx, checked_mul_u32(dy, iw)?)?)?; + let r = checked_mul_usize(src_calc, 3)?; + let g = checked_add_usize(r, 1)?; + let b = checked_add_usize(r, 2)?; + let r = src[r]; + let g = src[g]; + let b = src[b]; let color = BackendColor { alpha: 1.0, rgb: (r, g, b), }; - let result = self.draw_pixel((pos.0 + dx as i32, pos.1 + dy as i32), color); + let result = self.draw_pixel( + ( + checked_add_i32(pos.0, u32_to_i32_checked(dx)?)?, + checked_add_i32(pos.1, u32_to_i32_checked(dy)?)?, + ), + color, + ); #[allow(clippy::question_mark)] if result.is_err() { return result; @@ -327,3 +335,357 @@ pub trait DrawingBackend: Sized { Ok(()) } } + +#[cfg(test)] +#[allow(clippy::arithmetic_side_effects)] +mod tests { + use super::*; + use std::fmt; + + #[derive(Debug)] + struct TestBackendError; + + impl fmt::Display for TestBackendError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "test backend error") + } + } + + impl Error for TestBackendError {} + + #[derive(Debug, Clone)] + enum TestOp { + Pixel { + point: BackendCoord, + rgb: (u8, u8, u8), + alpha: f64, + }, + Line { + from: BackendCoord, + to: BackendCoord, + }, + FillPolygon { + len: usize, + }, + } + + struct TestBackend { + size: (u32, u32), + ops: Vec, + ensure_prepared_count: usize, + present_count: usize, + } + + impl TestBackend { + fn new(size: (u32, u32)) -> Self { + Self { + size, + ops: Vec::new(), + ensure_prepared_count: 0, + present_count: 0, + } + } + } + + impl DrawingBackend for TestBackend { + type ErrorType = TestBackendError; + + fn get_size(&self) -> (u32, u32) { + self.size + } + + fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind> { + self.ensure_prepared_count += 1; + Ok(()) + } + + fn present(&mut self) -> Result<(), DrawingErrorKind> { + self.present_count += 1; + Ok(()) + } + + fn draw_pixel( + &mut self, + point: BackendCoord, + color: BackendColor, + ) -> Result<(), DrawingErrorKind> { + self.ops.push(TestOp::Pixel { + point, + rgb: color.rgb, + alpha: color.alpha, + }); + Ok(()) + } + + fn draw_line( + &mut self, + from: BackendCoord, + to: BackendCoord, + _style: &S, + ) -> Result<(), DrawingErrorKind> { + self.ops.push(TestOp::Line { from, to }); + Ok(()) + } + + fn fill_polygon>( + &mut self, + vert: I, + _style: &S, + ) -> Result<(), DrawingErrorKind> { + let len = vert.into_iter().count(); + self.ops.push(TestOp::FillPolygon { len }); + Ok(()) + } + } + + #[derive(Clone, Copy)] + struct TestStyle { + color: BackendColor, + stroke_width: u32, + } + + impl TestStyle { + fn new(stroke_width: u32, alpha: f64) -> Self { + Self { + color: BackendColor { + rgb: (1, 2, 3), + alpha, + }, + stroke_width, + } + } + } + + impl BackendStyle for TestStyle { + fn color(&self) -> BackendColor { + self.color + } + + fn stroke_width(&self) -> u32 { + self.stroke_width + } + } + + #[test] + fn backend_coord_is_i32_pair() { + let coord: BackendCoord = (-1, 2); + + assert_eq!(coord, (-1_i32, 2_i32)); + } + + #[test] + fn ensure_prepared_and_present_can_be_called() { + let mut backend = TestBackend::new((10, 10)); + + backend.ensure_prepared().unwrap(); + backend.present().unwrap(); + + assert_eq!(backend.ensure_prepared_count, 1); + assert_eq!(backend.present_count, 1); + } + + #[test] + fn draw_path_with_transparent_style_draws_nothing() { + let mut backend = TestBackend::new((100, 100)); + let style = TestStyle::new(1, 0.0); + + backend + .draw_path(vec![(0, 0), (10, 10), (20, 0)], &style) + .unwrap(); + + assert!(backend.ops.is_empty()); + } + + #[test] + fn draw_path_with_single_point_draws_nothing() { + let mut backend = TestBackend::new((100, 100)); + let style = TestStyle::new(1, 1.0); + + backend.draw_path(vec![(0, 0)], &style).unwrap(); + + assert!(backend.ops.is_empty()); + } + + #[test] + fn draw_path_with_stroke_width_one_draws_consecutive_lines() { + let mut backend = TestBackend::new((100, 100)); + let style = TestStyle::new(1, 1.0); + + backend + .draw_path(vec![(0, 0), (10, 10), (20, 0)], &style) + .unwrap(); + + assert_eq!(backend.ops.len(), 2); + + match &backend.ops[0] { + TestOp::Line { from, to } => { + assert_eq!((*from, *to), ((0, 0), (10, 10))); + } + other => panic!("expected first op to be line, got {:?}", other), + } + + match &backend.ops[1] { + TestOp::Line { from, to } => { + assert_eq!((*from, *to), ((10, 10), (20, 0))); + } + other => panic!("expected second op to be line, got {:?}", other), + } + } + + #[test] + fn draw_path_with_wide_stroke_uses_fill_polygon() { + let mut backend = TestBackend::new((100, 100)); + let style = TestStyle::new(3, 1.0); + + backend + .draw_path(vec![(0, 0), (10, 10), (20, 0)], &style) + .unwrap(); + + assert_eq!(backend.ops.len(), 1); + + match &backend.ops[0] { + TestOp::FillPolygon { len } => { + assert!(*len > 0); + } + other => panic!("expected fill polygon op, got {:?}", other), + } + } + + #[test] + fn blit_bitmap_draws_rgb_pixels() { + let mut backend = TestBackend::new((2, 2)); + + let src = [ + 255, 0, 0, // pixel (0, 0) + 0, 255, 0, // pixel (1, 0) + 0, 0, 255, // pixel (0, 1) + 255, 255, 0, // pixel (1, 1) + ]; + + backend.blit_bitmap((0, 0), (2, 2), &src).unwrap(); + + assert_eq!(backend.ops.len(), 4); + + match &backend.ops[0] { + TestOp::Pixel { point, rgb, alpha } => { + assert_eq!(*point, (0, 0)); + assert_eq!(*rgb, (255, 0, 0)); + assert_eq!(*alpha, 1.0); + } + other => panic!("expected pixel op, got {:?}", other), + } + + match &backend.ops[1] { + TestOp::Pixel { point, rgb, alpha } => { + assert_eq!(*point, (0, 1)); + assert_eq!(*rgb, (0, 0, 255)); + assert_eq!(*alpha, 1.0); + } + other => panic!("expected pixel op, got {:?}", other), + } + + match &backend.ops[2] { + TestOp::Pixel { point, rgb, alpha } => { + assert_eq!(*point, (1, 0)); + assert_eq!(*rgb, (0, 255, 0)); + assert_eq!(*alpha, 1.0); + } + other => panic!("expected pixel op, got {:?}", other), + } + + match &backend.ops[3] { + TestOp::Pixel { point, rgb, alpha } => { + assert_eq!(*point, (1, 1)); + assert_eq!(*rgb, (255, 255, 0)); + assert_eq!(*alpha, 1.0); + } + other => panic!("expected pixel op, got {:?}", other), + } + } + + #[test] + fn blit_bitmap_clips_at_right_edge() { + let mut backend = TestBackend::new((2, 2)); + + let src = [ + 255, 0, 0, // pixel (0, 0) + 0, 255, 0, // pixel (1, 0) + 0, 0, 255, // pixel (0, 1) + 255, 255, 0, // pixel (1, 1) + ]; + + backend.blit_bitmap((1, 0), (2, 2), &src).unwrap(); + + assert_eq!(backend.ops.len(), 2); + + match &backend.ops[0] { + TestOp::Pixel { point, rgb, .. } => { + assert_eq!(*point, (1, 0)); + assert_eq!(*rgb, (255, 0, 0)); + } + other => panic!("expected pixel op, got {:?}", other), + } + + match &backend.ops[1] { + TestOp::Pixel { point, rgb, .. } => { + assert_eq!(*point, (1, 1)); + assert_eq!(*rgb, (0, 0, 255)); + } + other => panic!("expected pixel op, got {:?}", other), + } + } + + #[test] + fn blit_bitmap_clips_at_bottom_edge() { + let mut backend = TestBackend::new((2, 1)); + + let src = [ + 255, 0, 0, // pixel (0, 0) + 0, 255, 0, // pixel (1, 0) + 0, 0, 255, // pixel (0, 1) + 255, 255, 0, // pixel (1, 1) + ]; + + backend.blit_bitmap((0, 0), (2, 2), &src).unwrap(); + + assert_eq!(backend.ops.len(), 2); + + match &backend.ops[0] { + TestOp::Pixel { point, rgb, .. } => { + assert_eq!(*point, (0, 0)); + assert_eq!(*rgb, (255, 0, 0)); + } + other => panic!("expected pixel op, got {:?}", other), + } + + match &backend.ops[1] { + TestOp::Pixel { point, rgb, .. } => { + assert_eq!(*point, (1, 0)); + assert_eq!(*rgb, (0, 255, 0)); + } + other => panic!("expected pixel op, got {:?}", other), + } + } + + #[test] + fn blit_bitmap_outside_right_edge_draws_nothing() { + let mut backend = TestBackend::new((2, 2)); + + let src = [255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0]; + + backend.blit_bitmap((2, 0), (2, 2), &src).unwrap(); + + assert!(backend.ops.is_empty()); + } + + #[test] + fn blit_bitmap_outside_bottom_edge_draws_nothing() { + let mut backend = TestBackend::new((2, 2)); + + let src = [255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0]; + + backend.blit_bitmap((0, 2), (2, 2), &src).unwrap(); + + assert!(backend.ops.is_empty()); + } +} diff --git a/plotters-backend/src/math_errors.rs b/plotters-backend/src/math_errors.rs new file mode 100644 index 00000000..61387550 --- /dev/null +++ b/plotters-backend/src/math_errors.rs @@ -0,0 +1,97 @@ +use core::fmt; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum MathError { + ValueOverflow, + ValueUnderflow, + NonFiniteCalculation, + ValueOutOfRange, + ZeroDivision, +} + +impl fmt::Display for MathError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MathError::ValueOverflow => { + write!(f, "value exceeds the target type's maximum") + } + MathError::ValueUnderflow => { + write!(f, "value is below the target type's minimum") + } + MathError::NonFiniteCalculation => { + write!(f, "calculation produced a non-finite value") + } + MathError::ValueOutOfRange => { + write!(f, "value is out of range for the target type") + } + MathError::ZeroDivision => { + write!(f, "attempted to divide by zero") + } + } + } +} + +impl std::error::Error for MathError {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn math_error_is_copy_clone_debug_partial_eq_and_eq() { + let err = MathError::ValueOutOfRange; + let copied = err; + let cloned = err; + + assert_eq!(err, copied); + assert_eq!(err, cloned); + assert_eq!(format!("{:?}", err), "ValueOutOfRange"); + } + + #[test] + fn display_formats_value_overflow() { + assert_eq!( + MathError::ValueOverflow.to_string(), + "value exceeds the target type's maximum" + ); + } + + #[test] + fn display_formats_value_underflow() { + assert_eq!( + MathError::ValueUnderflow.to_string(), + "value is below the target type's minimum" + ); + } + + #[test] + fn display_formats_non_finite_calculation() { + assert_eq!( + MathError::NonFiniteCalculation.to_string(), + "calculation produced a non-finite value" + ); + } + + #[test] + fn display_formats_value_out_of_range() { + assert_eq!( + MathError::ValueOutOfRange.to_string(), + "value is out of range for the target type" + ); + } + + #[test] + fn display_formats_zero_division() { + assert_eq!( + MathError::ZeroDivision.to_string(), + "attempted to divide by zero" + ); + } + + #[test] + fn math_error_implements_std_error() { + fn assert_error() {} + + assert_error::(); + } +} diff --git a/plotters-backend/src/math_guard.rs b/plotters-backend/src/math_guard.rs new file mode 100644 index 00000000..ceab732f --- /dev/null +++ b/plotters-backend/src/math_guard.rs @@ -0,0 +1,797 @@ +use crate::math_errors::MathError; +use std::convert::TryFrom; +pub(crate) fn f64_to_i32_checked(v: f64) -> Result { + if !v.is_finite() { + return Err(MathError::NonFiniteCalculation); + } + + if v < f64::from(i32::MIN) || v > f64::from(i32::MAX) { + return Err(MathError::ValueOutOfRange); + } + + Ok(v as i32) +} + +pub(crate) fn ceil_f64_to_i32(v: f64) -> Result { + f64_to_i32_checked(v.ceil()) +} + +pub(crate) fn floor_f64_to_i32(v: f64) -> Result { + f64_to_i32_checked(v.floor()) +} + +pub(crate) fn f64_to_f32_checked(v: f64) -> Result { + if !v.is_finite() { + return Err(MathError::NonFiniteCalculation); + } + + if v < f64::from(f32::MIN) || v > f64::from(f32::MAX) { + return Err(MathError::ValueOutOfRange); + } + + let out = v as f32; + + if !out.is_finite() { + return Err(MathError::NonFiniteCalculation); + } + + Ok(out) +} + +pub(crate) fn non_zero_i32(v: i32) -> Result { + if v == 0 { + Err(MathError::ZeroDivision) + } else { + Ok(v) + } +} + +pub(crate) fn non_zero_u32(v: u32) -> Result { + if v == 0 { + Err(MathError::ZeroDivision) + } else { + Ok(v) + } +} + +pub(crate) fn non_zero_f64(v: f64) -> Result { + if !v.is_finite() { + return Err(MathError::NonFiniteCalculation); + } + + if v == 0.0 { + Err(MathError::ZeroDivision) + } else { + Ok(v) + } +} + +pub(crate) fn checked_add_i32(lhs: i32, rhs: i32) -> Result { + lhs.checked_add(rhs).ok_or(MathError::ValueOutOfRange) +} + +pub(crate) fn checked_sub_i32(lhs: i32, rhs: i32) -> Result { + lhs.checked_sub(rhs).ok_or(MathError::ValueOutOfRange) +} + +pub(crate) fn checked_mul_i32(lhs: i32, rhs: i32) -> Result { + lhs.checked_mul(rhs).ok_or(MathError::ValueOutOfRange) +} + +pub(crate) fn checked_div_i32(lhs: i32, rhs: i32) -> Result { + lhs.checked_div(rhs).ok_or(MathError::ValueOutOfRange) +} + +pub(crate) fn checked_neg_i32(v: i32) -> Result { + v.checked_neg().ok_or(MathError::ValueOutOfRange) +} + +pub(crate) fn checked_add_i64(lhs: i64, rhs: i64) -> Result { + lhs.checked_add(rhs).ok_or(MathError::ValueOutOfRange) +} + +pub(crate) fn checked_sub_i64(lhs: i64, rhs: i64) -> Result { + lhs.checked_sub(rhs).ok_or(MathError::ValueOutOfRange) +} + +pub(crate) fn checked_mul_i64(lhs: i64, rhs: i64) -> Result { + lhs.checked_mul(rhs).ok_or(MathError::ValueOutOfRange) +} + +pub(crate) fn checked_div_i64(lhs: i64, rhs: i64) -> Result { + lhs.checked_div(rhs).ok_or(MathError::ValueOutOfRange) +} + +pub(crate) fn checked_neg_i64(v: i64) -> Result { + v.checked_neg().ok_or(MathError::ValueOutOfRange) +} + +pub(crate) fn checked_add_u32(lhs: u32, rhs: u32) -> Result { + lhs.checked_add(rhs).ok_or(MathError::ValueOutOfRange) +} + +pub(crate) fn checked_sub_u32(lhs: u32, rhs: u32) -> Result { + lhs.checked_sub(rhs).ok_or(MathError::ValueOutOfRange) +} + +pub(crate) fn checked_mul_u32(lhs: u32, rhs: u32) -> Result { + lhs.checked_mul(rhs).ok_or(MathError::ValueOutOfRange) +} + +pub(crate) fn checked_div_u32(lhs: u32, rhs: u32) -> Result { + lhs.checked_div(rhs).ok_or(MathError::ValueOutOfRange) +} + +pub(crate) fn u32_to_i32_checked(v: u32) -> Result { + i32::try_from(v).map_err(|_| MathError::ValueOutOfRange) +} + +pub(crate) fn u32_to_usize_checked(v: u32) -> Result { + if u64::from(v) > usize::MAX as u64 { + return Err(MathError::ValueOutOfRange); + } + + Ok(v as usize) +} + +pub(crate) fn i32_to_u32_checked(v: i32) -> Result { + u32::try_from(v).map_err(|_| MathError::ValueOutOfRange) +} +pub(crate) fn sqrt_f64_checked(v: f64) -> Result { + if !v.is_finite() || v < 0.0 { + return Err(MathError::NonFiniteCalculation); + } + + Ok(v.sqrt()) +} + +pub(crate) fn checked_div_f64(lhs: f64, rhs: f64) -> Result { + if !lhs.is_finite() || !rhs.is_finite() { + return Err(MathError::NonFiniteCalculation); + } + + if rhs == 0.0 { + return Err(MathError::ZeroDivision); + } + + let out = lhs / rhs; + + if !out.is_finite() { + return Err(MathError::NonFiniteCalculation); + } + + Ok(out) +} + +pub(crate) fn checked_add_usize(lhs: usize, rhs: usize) -> Result { + lhs.checked_add(rhs).ok_or(MathError::ValueOutOfRange) +} + +pub(crate) fn checked_sub_usize(lhs: usize, rhs: usize) -> Result { + lhs.checked_sub(rhs).ok_or(MathError::ValueOutOfRange) +} + +pub(crate) fn checked_mul_usize(lhs: usize, rhs: usize) -> Result { + lhs.checked_mul(rhs).ok_or(MathError::ValueOutOfRange) +} + +pub(crate) fn checked_div_usize(lhs: usize, rhs: usize) -> Result { + lhs.checked_div(rhs).ok_or(MathError::ValueOutOfRange) +} + +pub(crate) fn i32_to_usize_checked(v: i32) -> Result { + if v < 0 { + return Err(MathError::ValueOutOfRange); + } + + Ok(v as usize) +} + +pub(crate) fn i64_to_usize_checked(v: i64) -> Result { + if v < 0 { + return Err(MathError::ValueOutOfRange); + } + + if v as u64 > usize::MAX as u64 { + return Err(MathError::ValueOutOfRange); + } + + Ok(v as usize) +} + +pub(crate) fn usize_to_i32_checked(v: usize) -> Result { + if v > i32::MAX as usize { + return Err(MathError::ValueOutOfRange); + } + + Ok(v as i32) +} + +pub(crate) fn usize_to_u32_checked(v: usize) -> Result { + if v > u32::MAX as usize { + return Err(MathError::ValueOutOfRange); + } + + Ok(v as u32) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn float_to_i32_checked_accepts_valid_value() { + assert_eq!(f64_to_i32_checked(42.0), Ok(42)); + } + + #[test] + fn float_to_i32_checked_rejects_non_finite_values() { + assert_eq!( + f64_to_i32_checked(f64::NAN), + Err(MathError::NonFiniteCalculation) + ); + assert_eq!( + f64_to_i32_checked(f64::INFINITY), + Err(MathError::NonFiniteCalculation) + ); + assert_eq!( + f64_to_i32_checked(f64::NEG_INFINITY), + Err(MathError::NonFiniteCalculation) + ); + } + + #[test] + fn float_to_i32_checked_rejects_out_of_range_values() { + assert_eq!( + f64_to_i32_checked(f64::from(i32::MAX) + 1.0), + Err(MathError::ValueOutOfRange) + ); + + assert_eq!( + f64_to_i32_checked(f64::from(i32::MIN) - 1.0), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn ceil_f64_to_i32_rounds_up_before_conversion() { + assert_eq!(ceil_f64_to_i32(1.2), Ok(2)); + } + + #[test] + fn floor_f64_to_i32_rounds_down_before_conversion() { + assert_eq!(floor_f64_to_i32(1.8), Ok(1)); + } + + #[test] + fn f64_to_f32_checked_accepts_finite_value() { + assert_eq!(f64_to_f32_checked(1.5), Ok(1.5_f32)); + } + + #[test] + fn f64_to_f32_checked_rejects_non_finite_values() { + assert_eq!( + f64_to_f32_checked(f64::NAN), + Err(MathError::NonFiniteCalculation) + ); + assert_eq!( + f64_to_f32_checked(f64::INFINITY), + Err(MathError::NonFiniteCalculation) + ); + assert_eq!( + f64_to_f32_checked(f64::NEG_INFINITY), + Err(MathError::NonFiniteCalculation) + ); + } + + #[test] + fn f64_to_f32_checked_rejects_out_of_range_values() { + assert_eq!( + f64_to_f32_checked(f64::from(f32::MAX) * 2.0), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn non_zero_i32_accepts_non_zero_value() { + assert_eq!(non_zero_i32(7), Ok(7)); + } + + #[test] + fn non_zero_i32_rejects_zero() { + assert_eq!(non_zero_i32(0), Err(MathError::ZeroDivision)); + } + + #[test] + fn non_zero_u32_accepts_non_zero_value() { + assert_eq!(non_zero_u32(7), Ok(7)); + } + + #[test] + fn non_zero_u32_rejects_zero() { + assert_eq!(non_zero_u32(0), Err(MathError::ZeroDivision)); + } + + #[test] + fn non_zero_f64_accepts_non_zero_value() { + assert_eq!(non_zero_f64(7.0), Ok(7.0)); + } + + #[test] + fn non_zero_f64_rejects_zero() { + assert_eq!(non_zero_f64(0.0), Err(MathError::ZeroDivision)); + } + + #[test] + fn non_zero_f64_rejects_non_finite_value() { + assert_eq!(non_zero_f64(f64::NAN), Err(MathError::NonFiniteCalculation)); + } + + #[test] + fn checked_add_i32_accepts_valid_sum() { + assert_eq!(checked_add_i32(2, 3), Ok(5)); + } + + #[test] + fn checked_add_i32_rejects_overflow() { + assert_eq!( + checked_add_i32(i32::MAX, 1), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn checked_sub_i32_accepts_valid_difference() { + assert_eq!(checked_sub_i32(5, 3), Ok(2)); + } + + #[test] + fn checked_sub_i32_rejects_overflow() { + assert_eq!( + checked_sub_i32(i32::MIN, 1), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn checked_mul_i32_accepts_valid_product() { + assert_eq!(checked_mul_i32(6, 7), Ok(42)); + } + + #[test] + fn checked_mul_i32_rejects_overflow() { + assert_eq!( + checked_mul_i32(i32::MAX, 2), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn checked_div_i32_accepts_valid_quotient() { + assert_eq!(checked_div_i32(8, 2), Ok(4)); + } + + #[test] + fn checked_div_i32_rejects_division_by_zero() { + assert_eq!(checked_div_i32(8, 0), Err(MathError::ValueOutOfRange)); + } + + #[test] + fn checked_div_i32_rejects_min_divided_by_negative_one() { + assert_eq!( + checked_div_i32(i32::MIN, -1), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn checked_neg_i32_accepts_valid_negation() { + assert_eq!(checked_neg_i32(7), Ok(-7)); + } + + #[test] + fn checked_neg_i32_rejects_min_value() { + assert_eq!(checked_neg_i32(i32::MIN), Err(MathError::ValueOutOfRange)); + } + + #[test] + fn checked_add_u32_accepts_valid_sum() { + assert_eq!(checked_add_u32(2, 3), Ok(5)); + } + + #[test] + fn checked_add_u32_rejects_overflow() { + assert_eq!( + checked_add_u32(u32::MAX, 1), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn checked_sub_u32_accepts_valid_difference() { + assert_eq!(checked_sub_u32(5, 3), Ok(2)); + } + + #[test] + fn checked_sub_u32_rejects_underflow() { + assert_eq!(checked_sub_u32(0, 1), Err(MathError::ValueOutOfRange)); + } + + #[test] + fn checked_mul_u32_accepts_valid_product() { + assert_eq!(checked_mul_u32(6, 7), Ok(42)); + } + + #[test] + fn checked_mul_u32_rejects_overflow() { + assert_eq!( + checked_mul_u32(u32::MAX, 2), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn checked_div_u32_accepts_valid_quotient() { + assert_eq!(checked_div_u32(8, 2), Ok(4)); + } + + #[test] + fn checked_div_u32_rejects_division_by_zero() { + assert_eq!(checked_div_u32(8, 0), Err(MathError::ValueOutOfRange)); + } + + #[test] + fn u32_to_i32_checked_accepts_in_range_value() { + assert_eq!(u32_to_i32_checked(42), Ok(42)); + } + + #[test] + fn u32_to_i32_checked_rejects_out_of_range_value() { + assert_eq!( + u32_to_i32_checked(i32::MAX as u32 + 1), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn i32_to_u32_checked_accepts_in_range_value() { + assert_eq!(i32_to_u32_checked(42), Ok(42)); + } + + #[test] + fn i32_to_u32_checked_rejects_out_of_range_value() { + assert_eq!( + i32_to_u32_checked(i32::MIN), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn i32_to_u32_checked_rejects_negative_one() { + assert_eq!(i32_to_u32_checked(-1), Err(MathError::ValueOutOfRange)); + } + + #[test] + fn i32_to_u32_checked_accepts_i32_max() { + assert_eq!(i32_to_u32_checked(i32::MAX), Ok(i32::MAX as u32)); + } + + #[test] + fn sqrt_f64_checked_accepts_valid_value() { + assert_eq!(sqrt_f64_checked(9.0), Ok(3.0)); + } + + #[test] + fn sqrt_f64_checked_rejects_negative_value() { + assert_eq!(sqrt_f64_checked(-1.0), Err(MathError::NonFiniteCalculation)); + } + + #[test] + fn sqrt_f64_checked_rejects_non_finite_value() { + assert_eq!( + sqrt_f64_checked(f64::NAN), + Err(MathError::NonFiniteCalculation) + ); + } + + #[test] + fn checked_add_i64_accepts_valid_sum() { + assert_eq!(checked_add_i64(2, 3), Ok(5)); + } + + #[test] + fn checked_add_i64_rejects_overflow() { + assert_eq!( + checked_add_i64(i64::MAX, 1), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn checked_add_i64_rejects_underflow() { + assert_eq!( + checked_add_i64(i64::MIN, -1), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn checked_sub_i64_accepts_valid_difference() { + assert_eq!(checked_sub_i64(5, 3), Ok(2)); + } + + #[test] + fn checked_sub_i64_rejects_overflow() { + assert_eq!( + checked_sub_i64(i64::MAX, -1), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn checked_sub_i64_rejects_underflow() { + assert_eq!( + checked_sub_i64(i64::MIN, 1), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn checked_mul_i64_accepts_valid_product() { + assert_eq!(checked_mul_i64(6, 7), Ok(42)); + } + + #[test] + fn checked_mul_i64_rejects_overflow() { + assert_eq!( + checked_mul_i64(i64::MAX, 2), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn checked_mul_i64_rejects_underflow() { + assert_eq!( + checked_mul_i64(i64::MIN, 2), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn checked_div_i64_accepts_valid_quotient() { + assert_eq!(checked_div_i64(8, 2), Ok(4)); + } + + #[test] + fn checked_div_i64_rejects_division_by_zero() { + assert_eq!(checked_div_i64(8, 0), Err(MathError::ValueOutOfRange)); + } + + #[test] + fn checked_div_i64_rejects_min_divided_by_negative_one() { + assert_eq!( + checked_div_i64(i64::MIN, -1), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn checked_neg_i64_accepts_valid_negation() { + assert_eq!(checked_neg_i64(7), Ok(-7)); + } + + #[test] + fn checked_neg_i64_accepts_zero() { + assert_eq!(checked_neg_i64(0), Ok(0)); + } + + #[test] + fn checked_neg_i64_rejects_min_value() { + assert_eq!(checked_neg_i64(i64::MIN), Err(MathError::ValueOutOfRange)); + } + + #[test] + fn checked_neg_i64_accepts_max_value() { + assert_eq!(checked_neg_i64(i64::MAX), Ok(-i64::MAX)); + } + + #[test] + fn checked_div_f64_accepts_valid_quotient() { + assert_eq!(checked_div_f64(8.0, 2.0), Ok(4.0)); + } + + #[test] + fn checked_div_f64_accepts_fractional_quotient() { + assert_eq!(checked_div_f64(1.0, 4.0), Ok(0.25)); + } + + #[test] + fn checked_div_f64_accepts_negative_quotient() { + assert_eq!(checked_div_f64(-8.0, 2.0), Ok(-4.0)); + } + + #[test] + fn checked_div_f64_rejects_division_by_positive_zero() { + assert_eq!(checked_div_f64(8.0, 0.0), Err(MathError::ZeroDivision)); + } + + #[test] + fn checked_div_f64_rejects_division_by_negative_zero() { + assert_eq!(checked_div_f64(8.0, -0.0), Err(MathError::ZeroDivision)); + } + + #[test] + fn checked_div_f64_rejects_nan_lhs() { + assert_eq!( + checked_div_f64(f64::NAN, 2.0), + Err(MathError::NonFiniteCalculation) + ); + } + + #[test] + fn checked_div_f64_rejects_nan_rhs() { + assert_eq!( + checked_div_f64(8.0, f64::NAN), + Err(MathError::NonFiniteCalculation) + ); + } + + #[test] + fn checked_div_f64_rejects_infinite_lhs() { + assert_eq!( + checked_div_f64(f64::INFINITY, 2.0), + Err(MathError::NonFiniteCalculation) + ); + } + + #[test] + fn checked_div_f64_rejects_infinite_rhs() { + assert_eq!( + checked_div_f64(8.0, f64::INFINITY), + Err(MathError::NonFiniteCalculation) + ); + } + + #[test] + fn checked_div_f64_rejects_non_finite_output() { + assert_eq!( + checked_div_f64(f64::MAX, f64::MIN_POSITIVE), + Err(MathError::NonFiniteCalculation) + ); + } + + #[test] + fn checked_add_usize_accepts_valid_sum() { + assert_eq!(checked_add_usize(2, 3), Ok(5)); + } + + #[test] + fn checked_add_usize_rejects_overflow() { + assert_eq!( + checked_add_usize(usize::MAX, 1), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn checked_sub_usize_accepts_valid_difference() { + assert_eq!(checked_sub_usize(5, 3), Ok(2)); + } + + #[test] + fn checked_sub_usize_rejects_underflow() { + assert_eq!(checked_sub_usize(0, 1), Err(MathError::ValueOutOfRange)); + } + + #[test] + fn checked_mul_usize_accepts_valid_product() { + assert_eq!(checked_mul_usize(6, 7), Ok(42)); + } + + #[test] + fn checked_mul_usize_rejects_overflow() { + assert_eq!( + checked_mul_usize(usize::MAX, 2), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn checked_div_usize_accepts_valid_quotient() { + assert_eq!(checked_div_usize(8, 2), Ok(4)); + } + + #[test] + fn checked_div_usize_rejects_division_by_zero() { + assert_eq!(checked_div_usize(8, 0), Err(MathError::ValueOutOfRange)); + } + + #[test] + fn i32_to_usize_checked_accepts_non_negative_value() { + assert_eq!(i32_to_usize_checked(42), Ok(42)); + } + + #[test] + fn i32_to_usize_checked_accepts_zero() { + assert_eq!(i32_to_usize_checked(0), Ok(0)); + } + + #[test] + fn i32_to_usize_checked_rejects_negative_value() { + assert_eq!(i32_to_usize_checked(-1), Err(MathError::ValueOutOfRange)); + } + + #[test] + fn i64_to_usize_checked_accepts_non_negative_value() { + assert_eq!(i64_to_usize_checked(42), Ok(42)); + } + + #[test] + fn i64_to_usize_checked_rejects_negative_value() { + assert_eq!(i64_to_usize_checked(-1), Err(MathError::ValueOutOfRange)); + } + + #[test] + fn i64_to_usize_checked_rejects_out_of_range_value() { + if usize::BITS < 64 { + assert_eq!( + i64_to_usize_checked(i64::MAX), + Err(MathError::ValueOutOfRange) + ); + } + } + + #[test] + fn usize_to_i32_checked_accepts_in_range_value() { + assert_eq!(usize_to_i32_checked(42), Ok(42)); + } + + #[test] + fn usize_to_i32_checked_accepts_i32_max() { + assert_eq!(usize_to_i32_checked(i32::MAX as usize), Ok(i32::MAX)); + } + + #[test] + fn usize_to_i32_checked_rejects_out_of_range_value() { + assert_eq!( + usize_to_i32_checked(i32::MAX as usize + 1), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn usize_to_u32_checked_accepts_in_range_value() { + assert_eq!(usize_to_u32_checked(42), Ok(42)); + } + + #[test] + fn usize_to_u32_checked_accepts_u32_max() { + assert_eq!(usize_to_u32_checked(u32::MAX as usize), Ok(u32::MAX)); + } + + #[test] + fn usize_to_u32_checked_rejects_out_of_range_value() { + if usize::BITS > 32 { + assert_eq!( + usize_to_u32_checked(u32::MAX as usize + 1), + Err(MathError::ValueOutOfRange) + ); + } + } + #[test] + fn u32_to_usize_checked_accepts_zero() { + assert_eq!(u32_to_usize_checked(0), Ok(0)); + } + + #[test] + fn u32_to_usize_checked_accepts_in_range_value() { + assert_eq!(u32_to_usize_checked(42), Ok(42)); + } + + #[test] + fn u32_to_usize_checked_accepts_u32_max_on_supported_platforms() { + if usize::BITS >= 32 { + assert_eq!(u32_to_usize_checked(u32::MAX), Ok(u32::MAX as usize)); + } + } +} diff --git a/plotters-backend/src/rasterizer/circle.rs b/plotters-backend/src/rasterizer/circle.rs index fa7fc50d..449b3480 100644 --- a/plotters-backend/src/rasterizer/circle.rs +++ b/plotters-backend/src/rasterizer/circle.rs @@ -1,5 +1,10 @@ +use crate::math_errors::MathError; +use crate::math_guard::{ + ceil_f64_to_i32, checked_add_i32, checked_add_u32, checked_div_u32, checked_neg_i32, + checked_sub_i32, checked_sub_u32, f64_to_i32_checked, floor_f64_to_i32, sqrt_f64_checked, + u32_to_i32_checked, +}; use crate::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind}; - fn draw_part_a< B: DrawingBackend, Draw: FnMut(i32, (f64, f64)) -> Result<(), DrawingErrorKind>, @@ -12,8 +17,8 @@ fn draw_part_a< - (radius as f64 - height) * (radius as f64 - height)) .sqrt(); - let x0 = (-half_width).ceil() as i32; - let x1 = half_width.floor() as i32; + let x0 = ceil_f64_to_i32(half_width)?; + let x1 = floor_f64_to_i32(half_width)?; let y0 = (radius as f64 - height).ceil(); @@ -33,9 +38,11 @@ fn draw_part_b< size: f64, mut draw: Draw, ) -> Result<(), DrawingErrorKind> { - let from = from.floor(); - for x in (from - size).floor() as i32..=from as i32 { - check_result!(draw(x, (-x as f64, x as f64))); + let len = floor_f64_to_i32(from - size)?; + let from = floor_f64_to_i32(from)?; + for x in len..=from { + let neg_x = checked_neg_i32(x)?; + check_result!(draw(x, (f64::from(neg_x), f64::from(x)))); } Ok(()) } @@ -48,35 +55,49 @@ fn draw_part_c< r_limit: i32, mut draw: Draw, ) -> Result<(), DrawingErrorKind> { - let half_size = r as f64 / (2f64).sqrt(); + if r < 0 || r_limit < 0 || r > r_limit { + return Err(MathError::ValueOutOfRange.into()); + } + let r_f = f64::from(r); + let r_limit_f = f64::from(r_limit); + let half_size = r_f / (2f64).sqrt(); - let (x0, x1) = ((-half_size).ceil() as i32, half_size.floor() as i32); + let x0 = ceil_f64_to_i32(-half_size)?; + let x1 = floor_f64_to_i32(half_size)?; for x in x0..x1 { - let outer_y0 = ((r_limit as f64) * (r_limit as f64) - x as f64 * x as f64).sqrt(); - let inner_y0 = r as f64 - 1.0; + let x_f = f64::from(x); + let outer_y0 = sqrt_f64_checked(r_limit_f * r_limit_f - x_f * x_f)?; + let inner_y0 = r_f - 1.0; let mut y1 = outer_y0.min(inner_y0); - let y0 = ((r as f64) * (r as f64) - x as f64 * x as f64).sqrt(); + let y0 = sqrt_f64_checked(r_f * r_f - x_f * x_f)?; if y0 > y1 { y1 = y0.ceil(); - if y1 >= r as f64 { + if y1 >= r_f { continue; } } check_result!(draw(x, (y0, y1))); } - - for x in x1 + 1..r { - let outer_y0 = ((r_limit as f64) * (r_limit as f64) - x as f64 * x as f64).sqrt(); - let inner_y0 = r as f64 - 1.0; + let start = checked_add_i32(x1, 1)?; + let end = checked_add_i32(x1, r)?; + for x in start..end { + let x_f = f64::from(x); + let outer_radicand = r_limit_f * r_limit_f - x_f * x_f; + if outer_radicand < 0.0 { + continue; + } + let outer_y0 = sqrt_f64_checked(outer_radicand)?; + let inner_y0 = r_f - 1.0; let y0 = outer_y0.min(inner_y0); - let y1 = x as f64; + let y1 = x_f; if y1 < y0 { check_result!(draw(x, (y0, y1 + 1.0))); - check_result!(draw(-x, (y0, y1 + 1.0))); + let neg_x = checked_neg_i32(x)?; + check_result!(draw(neg_x, (y0, y1 + 1.0))); } } @@ -93,29 +114,43 @@ fn draw_sweep_line( ) -> Result<(), DrawingErrorKind> { let mut s = if dx < 0 || dy < 0 { -s } else { s }; let mut e = if dx < 0 || dy < 0 { -e } else { e }; + if !s.is_finite() || !e.is_finite() { + return Err(MathError::NonFiniteCalculation.into()); + } if s > e { std::mem::swap(&mut s, &mut e); } + let s_ceil = ceil_f64_to_i32(s)?; + let e_floor = floor_f64_to_i32(e)?; + let vs = s.ceil() - s; let ve = e - e.floor(); if dx == 0 { - check_result!(b.draw_line( - (p0 + x0, s.ceil() as i32 + y0), - (p0 + x0, e.floor() as i32 + y0), - &style.color() - )); - check_result!(b.draw_pixel((p0 + x0, s.ceil() as i32 + y0 - 1), style.color().mix(vs))); - check_result!(b.draw_pixel((p0 + x0, e.floor() as i32 + y0 + 1), style.color().mix(ve))); + let px0 = checked_add_i32(p0, x0)?; + let sy0 = checked_add_i32(s_ceil, y0)?; + let ey0 = checked_add_i32(e_floor, y0)?; + + check_result!(b.draw_line((px0, sy0), (px0, ey0), &style.color())); + + let sy0_sub_1 = checked_sub_i32(sy0, 1)?; + let ey0_add_1 = checked_add_i32(ey0, 1)?; + + check_result!(b.draw_pixel((px0, sy0_sub_1), style.color().mix(vs))); + check_result!(b.draw_pixel((px0, ey0_add_1), style.color().mix(ve))); } else { - check_result!(b.draw_line( - (s.ceil() as i32 + x0, p0 + y0), - (e.floor() as i32 + x0, p0 + y0), - &style.color() - )); - check_result!(b.draw_pixel((s.ceil() as i32 + x0 - 1, p0 + y0), style.color().mix(vs))); - check_result!(b.draw_pixel((e.floor() as i32 + x0 + 1, p0 + y0), style.color().mix(ve))); + let sx0 = checked_add_i32(s_ceil, x0)?; + let py0 = checked_add_i32(p0, y0)?; + let ex0 = checked_add_i32(e_floor, x0)?; + + check_result!(b.draw_line((sx0, py0), (ex0, py0), &style.color())); + + let sx0_sub_1 = checked_sub_i32(sx0, 1)?; + let ex0_add_1 = checked_add_i32(ex0, 1)?; + + check_result!(b.draw_pixel((sx0_sub_1, py0), style.color().mix(vs))); + check_result!(b.draw_pixel((ex0_add_1, py0), style.color().mix(ve))); } Ok(()) @@ -127,68 +162,75 @@ fn draw_annulus( radius: (u32, u32), style: &S, ) -> Result<(), DrawingErrorKind> { - let a0 = ((radius.0 - radius.1) as f64).min(radius.0 as f64 * (1.0 - 1.0 / (2f64).sqrt())); - let a1 = (radius.0 as f64 - a0 - radius.1 as f64).max(0.0); - - check_result!(draw_part_a::(a0, radius.0, |p, r| draw_sweep_line( - b, - style, - center, - (0, 1), - p, - r - ))); - check_result!(draw_part_a::(a0, radius.0, |p, r| draw_sweep_line( - b, - style, - center, - (0, -1), - p, - r - ))); - check_result!(draw_part_a::(a0, radius.0, |p, r| draw_sweep_line( - b, - style, - center, - (1, 0), - p, - r - ))); - check_result!(draw_part_a::(a0, radius.0, |p, r| draw_sweep_line( - b, - style, - center, - (-1, 0), - p, - r - ))); + let radius0_f = f64::from(radius.0); + let radius1_f = f64::from(radius.1); + + let radius0_i32 = u32_to_i32_checked(radius.0)?; + let radius1_i32 = u32_to_i32_checked(radius.1)?; + + let rad_sub = f64::from(checked_sub_u32(radius.0, radius.1)?); + let a0 = rad_sub.min(radius0_f * (1.0 - 1.0 / (2f64).sqrt())); + let a1 = (radius0_f - a0 - radius1_f).max(0.0); + + check_result!(draw_part_a::(a0, radius.0, |p, r| { + draw_sweep_line(b, style, center, (0, 1), p, r) + })); + + check_result!(draw_part_a::(a0, radius.0, |p, r| { + draw_sweep_line(b, style, center, (0, -1), p, r) + })); + + check_result!(draw_part_a::(a0, radius.0, |p, r| { + draw_sweep_line(b, style, center, (1, 0), p, r) + })); + + check_result!(draw_part_a::(a0, radius.0, |p, r| { + draw_sweep_line(b, style, center, (-1, 0), p, r) + })); if a1 > 0.0 { check_result!(draw_part_b::( - radius.0 as f64 - a0, + radius0_f - a0, a1.floor(), |h, (f, t)| { - let f = f as i32; - let t = t as i32; + let f = f64_to_i32_checked(f)?; + let t = f64_to_i32_checked(t)?; + + let center_h = checked_add_i32(center.0, h)?; + let center_f = checked_add_i32(center.1, f)?; + let center_t = checked_add_i32(center.1, t)?; + check_result!(b.draw_line( - (center.0 + h, center.1 + f), - (center.0 + h, center.1 + t), + (center_h, center_f), + (center_h, center_t), &style.color() )); + + let center_sub_h = checked_sub_i32(center.0, h)?; + check_result!(b.draw_line( - (center.0 - h, center.1 + f), - (center.0 - h, center.1 + t), + (center_sub_h, center_f), + (center_sub_h, center_t), &style.color() )); + let center0_f = checked_add_i32(center.0, f)?; + let center0_f1 = checked_add_i32(center0_f, 1)?; + let center0_t = checked_add_i32(center.0, t)?; + let center0_tsub1 = checked_sub_i32(center0_t, 1)?; + let center1_h = checked_add_i32(center.1, h)?; + check_result!(b.draw_line( - (center.0 + f + 1, center.1 + h), - (center.0 + t - 1, center.1 + h), + (center0_f1, center1_h), + (center0_tsub1, center1_h), &style.color() )); + + let center1_sub_h = checked_sub_i32(center.1, h)?; + check_result!(b.draw_line( - (center.0 + f + 1, center.1 - h), - (center.0 + t - 1, center.1 - h), + (center0_f1, center1_sub_h), + (center0_tsub1, center1_sub_h), &style.color() )); @@ -197,74 +239,91 @@ fn draw_annulus( )); } - check_result!(draw_part_c::( - radius.1 as i32, - radius.0 as i32, - |p, r| draw_sweep_line(b, style, center, (0, 1), p, r) - )); - check_result!(draw_part_c::( - radius.1 as i32, - radius.0 as i32, - |p, r| draw_sweep_line(b, style, center, (0, -1), p, r) - )); - check_result!(draw_part_c::( - radius.1 as i32, - radius.0 as i32, - |p, r| draw_sweep_line(b, style, center, (1, 0), p, r) - )); - check_result!(draw_part_c::( - radius.1 as i32, - radius.0 as i32, - |p, r| draw_sweep_line(b, style, center, (-1, 0), p, r) - )); + check_result!(draw_part_c::(radius1_i32, radius0_i32, |p, r| { + draw_sweep_line(b, style, center, (0, 1), p, r) + })); + + check_result!(draw_part_c::(radius1_i32, radius0_i32, |p, r| { + draw_sweep_line(b, style, center, (0, -1), p, r) + })); + + check_result!(draw_part_c::(radius1_i32, radius0_i32, |p, r| { + draw_sweep_line(b, style, center, (1, 0), p, r) + })); + + check_result!(draw_part_c::(radius1_i32, radius0_i32, |p, r| { + draw_sweep_line(b, style, center, (-1, 0), p, r) + })); + + let d_inner = floor_f64_to_i32(radius1_f / (2f64).sqrt())?; - let d_inner = ((radius.1 as f64) / (2f64).sqrt()) as i32; - let d_outer = (((radius.0 as f64) / (2f64).sqrt()) as i32).min(radius.1 as i32 - 1); - let d_outer_actually = (radius.1 as i32).min( - (radius.0 as f64 * radius.0 as f64 - radius.1 as f64 * radius.1 as f64 / 2.0) - .sqrt() - .ceil() as i32, - ); + let d_outer_limit = checked_sub_i32(radius1_i32, 1)?; + let d_outer = floor_f64_to_i32(radius0_f / (2f64).sqrt())?.min(d_outer_limit); + + let d_outer_actual_value = + sqrt_f64_checked(radius0_f * radius0_f - radius1_f * radius1_f / 2.0)?; + let d_outer_actually = radius1_i32.min(ceil_f64_to_i32(d_outer_actual_value)?); + + let cx_sub_d_inner = checked_sub_i32(center.0, d_inner)?; + let cx_add_d_inner = checked_add_i32(center.0, d_inner)?; + let cy_sub_d_inner = checked_sub_i32(center.1, d_inner)?; + let cy_add_d_inner = checked_add_i32(center.1, d_inner)?; + + let cx_sub_d_outer = checked_sub_i32(center.0, d_outer)?; + let cx_add_d_outer = checked_add_i32(center.0, d_outer)?; + let cy_sub_d_outer = checked_sub_i32(center.1, d_outer)?; + let cy_add_d_outer = checked_add_i32(center.1, d_outer)?; + + let cx_sub_d_outer_actually = checked_sub_i32(center.0, d_outer_actually)?; + let cx_add_d_outer_actually = checked_add_i32(center.0, d_outer_actually)?; + let cy_sub_d_outer_actually = checked_sub_i32(center.1, d_outer_actually)?; + let cy_add_d_outer_actually = checked_add_i32(center.1, d_outer_actually)?; check_result!(b.draw_line( - (center.0 - d_inner, center.1 - d_inner), - (center.0 - d_outer, center.1 - d_outer), + (cx_sub_d_inner, cy_sub_d_inner), + (cx_sub_d_outer, cy_sub_d_outer), &style.color() )); + check_result!(b.draw_line( - (center.0 + d_inner, center.1 - d_inner), - (center.0 + d_outer, center.1 - d_outer), + (cx_add_d_inner, cy_sub_d_inner), + (cx_add_d_outer, cy_sub_d_outer), &style.color() )); + check_result!(b.draw_line( - (center.0 - d_inner, center.1 + d_inner), - (center.0 - d_outer, center.1 + d_outer), + (cx_sub_d_inner, cy_add_d_inner), + (cx_sub_d_outer, cy_add_d_outer), &style.color() )); + check_result!(b.draw_line( - (center.0 + d_inner, center.1 + d_inner), - (center.0 + d_outer, center.1 + d_outer), + (cx_add_d_inner, cy_add_d_inner), + (cx_add_d_outer, cy_add_d_outer), &style.color() )); check_result!(b.draw_line( - (center.0 - d_inner, center.1 + d_inner), - (center.0 - d_outer_actually, center.1 + d_inner), + (cx_sub_d_inner, cy_add_d_inner), + (cx_sub_d_outer_actually, cy_add_d_inner), &style.color() )); + check_result!(b.draw_line( - (center.0 + d_inner, center.1 - d_inner), - (center.0 + d_inner, center.1 - d_outer_actually), + (cx_add_d_inner, cy_sub_d_inner), + (cx_add_d_inner, cy_sub_d_outer_actually), &style.color() )); + check_result!(b.draw_line( - (center.0 + d_inner, center.1 + d_inner), - (center.0 + d_inner, center.1 + d_outer_actually), + (cx_add_d_inner, cy_add_d_inner), + (cx_add_d_inner, cy_add_d_outer_actually), &style.color() )); + check_result!(b.draw_line( - (center.0 + d_inner, center.1 + d_inner), - (center.0 + d_outer_actually, center.1 + d_inner), + (cx_add_d_inner, cy_add_d_inner), + (cx_add_d_outer_actually, cy_add_d_inner), &style.color() )); @@ -283,59 +342,343 @@ pub fn draw_circle( } if !fill && style.stroke_width() != 1 { - let inner_radius = radius - (style.stroke_width() / 2).min(radius); - radius += style.stroke_width() / 2; + let half_stroke = checked_div_u32(style.stroke_width(), 2)?; + let inner_delta = half_stroke.min(radius); + let inner_radius = checked_sub_u32(radius, inner_delta)?; + radius = checked_add_u32(radius, half_stroke)?; if inner_radius > 0 { return draw_annulus(b, center, (radius, inner_radius), style); } else { fill = true; } } - - let min = (f64::from(radius) * (1.0 - (2f64).sqrt() / 2.0)).ceil() as i32; - let max = (f64::from(radius) * (1.0 + (2f64).sqrt() / 2.0)).floor() as i32; + let radius_f = f64::from(radius); + let radius_i32 = u32_to_i32_checked(radius)?; + let sqrt_2 = (2f64).sqrt(); + let min = ceil_f64_to_i32(radius_f * (1.0 - sqrt_2 / 2.0))?; + let max = floor_f64_to_i32(radius_f * (1.0 + sqrt_2 / 2.0))?; + let up = checked_sub_i32(checked_add_i32(min, center.1)?, radius_i32)?; + let down = checked_sub_i32(checked_add_i32(max, center.1)?, radius_i32)?; let range = min..=max; - let (up, down) = ( - range.start() + center.1 - radius as i32, - range.end() + center.1 - radius as i32, - ); - for dy in range { - let dy = dy - radius as i32; - let y = center.1 + dy; + let dy = checked_sub_i32(dy, radius_i32)?; + let dy_f = f64::from(dy); - let lx = (f64::from(radius) * f64::from(radius) - - (f64::from(dy) * f64::from(dy)).max(1e-5)) - .sqrt(); + let y = checked_add_i32(center.1, dy)?; + + let lx = sqrt_f64_checked(radius_f * radius_f - (dy_f * dy_f).max(1e-5))?; + let lx_floor = floor_f64_to_i32(lx)?; - let left = center.0 - lx.floor() as i32; - let right = center.0 + lx.floor() as i32; + let left = checked_sub_i32(center.0, lx_floor)?; + let right = checked_add_i32(center.0, lx_floor)?; let v = lx - lx.floor(); - let x = center.0 + dy; - let top = center.1 - lx.floor() as i32; - let bottom = center.1 + lx.floor() as i32; + let x = checked_add_i32(center.0, dy)?; + let top = checked_sub_i32(center.1, lx_floor)?; + let bottom = checked_add_i32(center.1, lx_floor)?; if fill { + let up_minus_one = checked_sub_i32(up, 1)?; + let down_plus_one = checked_add_i32(down, 1)?; + check_result!(b.draw_line((left, y), (right, y), &style.color())); - check_result!(b.draw_line((x, top), (x, up - 1), &style.color())); - check_result!(b.draw_line((x, down + 1), (x, bottom), &style.color())); + check_result!(b.draw_line((x, top), (x, up_minus_one), &style.color())); + check_result!(b.draw_line((x, down_plus_one), (x, bottom), &style.color())); } else { - check_result!(b.draw_pixel((left, y), style.color().mix(1.0 - v))); - check_result!(b.draw_pixel((right, y), style.color().mix(1.0 - v))); + let inverse_v = 1.0 - v; + + check_result!(b.draw_pixel((left, y), style.color().mix(inverse_v))); + check_result!(b.draw_pixel((right, y), style.color().mix(inverse_v))); - check_result!(b.draw_pixel((x, top), style.color().mix(1.0 - v))); - check_result!(b.draw_pixel((x, bottom), style.color().mix(1.0 - v))); + check_result!(b.draw_pixel((x, top), style.color().mix(inverse_v))); + check_result!(b.draw_pixel((x, bottom), style.color().mix(inverse_v))); } - check_result!(b.draw_pixel((left - 1, y), style.color().mix(v))); - check_result!(b.draw_pixel((right + 1, y), style.color().mix(v))); - check_result!(b.draw_pixel((x, top - 1), style.color().mix(v))); - check_result!(b.draw_pixel((x, bottom + 1), style.color().mix(v))); + let left_minus_one = checked_sub_i32(left, 1)?; + let right_plus_one = checked_add_i32(right, 1)?; + let top_minus_one = checked_sub_i32(top, 1)?; + let bottom_plus_one = checked_add_i32(bottom, 1)?; + + check_result!(b.draw_pixel((left_minus_one, y), style.color().mix(v))); + check_result!(b.draw_pixel((right_plus_one, y), style.color().mix(v))); + check_result!(b.draw_pixel((x, top_minus_one), style.color().mix(v))); + check_result!(b.draw_pixel((x, bottom_plus_one), style.color().mix(v))); } Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::BackendColor; + use std::error::Error; + use std::fmt; + + #[derive(Debug)] + struct TestError; + + impl fmt::Display for TestError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "test backend error") + } + } + + impl Error for TestError {} + + #[derive(Debug, Clone, PartialEq, Eq)] + enum TestOp { + Pixel(BackendCoord), + Line(BackendCoord, BackendCoord), + } + + #[derive(Default)] + struct TestBackend { + ops: Vec, + } + + impl DrawingBackend for TestBackend { + type ErrorType = TestError; + fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind> { + Ok(()) + } + fn get_size(&self) -> (u32, u32) { + (100, 100) + } + + fn present(&mut self) -> Result<(), DrawingErrorKind> { + Ok(()) + } + + fn draw_pixel( + &mut self, + point: BackendCoord, + _color: BackendColor, + ) -> Result<(), DrawingErrorKind> { + self.ops.push(TestOp::Pixel(point)); + Ok(()) + } + + fn draw_line( + &mut self, + from: BackendCoord, + to: BackendCoord, + _style: &S, + ) -> Result<(), DrawingErrorKind> { + self.ops.push(TestOp::Line(from, to)); + Ok(()) + } + } + + #[derive(Clone, Copy)] + struct TestStyle { + color: BackendColor, + stroke_width: u32, + } + + impl TestStyle { + fn new(stroke_width: u32, alpha: f64) -> Self { + Self { + color: BackendColor { + rgb: (0, 0, 0), + alpha, + }, + stroke_width, + } + } + } + + impl BackendStyle for TestStyle { + fn color(&self) -> BackendColor { + self.color + } + + fn stroke_width(&self) -> u32 { + self.stroke_width + } + } + + #[test] + fn draw_part_a_draws_expected_integer_width_slice() { + let mut points = Vec::new(); + + draw_part_a::(5.0, 5, |x, range| { + points.push((x, range)); + Ok(()) + }) + .unwrap(); + + assert_eq!(points, vec![(5, (0.0, 0.0))]); + } + + #[test] + fn draw_part_a_rejects_non_finite_height() { + let result = draw_part_a::(f64::NAN, 5, |_x, _range| Ok(())); + + assert!(result.is_err()); + } + + #[test] + fn draw_part_b_draws_expected_reflected_ranges() { + let mut points = Vec::new(); + + draw_part_b::(3.0, 2.0, |x, range| { + points.push((x, range)); + Ok(()) + }) + .unwrap(); + + assert_eq!( + points, + vec![(1, (-1.0, 1.0)), (2, (-2.0, 2.0)), (3, (-3.0, 3.0)),] + ); + } + + #[test] + fn draw_part_b_rejects_negation_overflow() { + let result = draw_part_b::(f64::from(i32::MIN), 0.0, |_x, _range| Ok(())); + + assert!(result.is_err()); + } + + #[test] + fn draw_part_c_draws_points_for_valid_annulus_segment() { + let mut points = Vec::new(); + + draw_part_c::(7, 8, |x, range| { + points.push((x, range)); + Ok(()) + }) + .unwrap(); + + assert!(!points.is_empty()); + + for (_x, (y0, y1)) in points { + assert!(y0.is_finite()); + assert!(y1.is_finite()); + } + } + + #[test] + fn draw_part_c_rejects_invalid_radius_geometry() { + let result = draw_part_c::(5, 3, |_x, _range| Ok(())); + + assert!(result.is_err()); + } + + #[test] + fn draw_sweep_line_draws_vertical_line_and_edge_pixels() { + let mut backend = TestBackend::default(); + let style = TestStyle::new(1, 1.0); + + draw_sweep_line(&mut backend, &style, (10, 20), (0, 1), 2, (1.2, 3.7)).unwrap(); + + assert_eq!( + backend.ops, + vec![ + TestOp::Line((12, 22), (12, 23)), + TestOp::Pixel((12, 21)), + TestOp::Pixel((12, 24)), + ] + ); + } + + #[test] + fn draw_sweep_line_draws_horizontal_line_and_edge_pixels() { + let mut backend = TestBackend::default(); + let style = TestStyle::new(1, 1.0); + + draw_sweep_line(&mut backend, &style, (10, 20), (1, 0), 2, (1.2, 3.7)).unwrap(); + + assert_eq!( + backend.ops, + vec![ + TestOp::Line((12, 22), (13, 22)), + TestOp::Pixel((11, 22)), + TestOp::Pixel((14, 22)), + ] + ); + } + + #[test] + fn draw_sweep_line_rejects_non_finite_range() { + let mut backend = TestBackend::default(); + let style = TestStyle::new(1, 1.0); + + let result = draw_sweep_line(&mut backend, &style, (10, 20), (0, 1), 2, (f64::NAN, 3.7)); + + assert!(result.is_err()); + assert!(backend.ops.is_empty()); + } + + #[test] + fn draw_annulus_rejects_inner_radius_larger_than_outer_radius() { + let mut backend = TestBackend::default(); + let style = TestStyle::new(1, 1.0); + + let result = draw_annulus(&mut backend, (20, 20), (2, 3), &style); + + assert!(result.is_err()); + assert!(backend.ops.is_empty()); + } + + #[test] + fn draw_circle_with_transparent_style_draws_nothing() { + let mut backend = TestBackend::default(); + let style = TestStyle::new(1, 0.0); + + draw_circle(&mut backend, (20, 20), 5, &style, false).unwrap(); + + assert!(backend.ops.is_empty()); + } + + #[test] + fn draw_circle_outline_draws_pixels() { + let mut backend = TestBackend::default(); + let style = TestStyle::new(1, 1.0); + + draw_circle(&mut backend, (20, 20), 5, &style, false).unwrap(); + + assert!(!backend.ops.is_empty()); + assert!(backend.ops.iter().all(|op| matches!(op, TestOp::Pixel(_)))); + } + + #[test] + fn draw_circle_fill_draws_lines_and_pixels() { + let mut backend = TestBackend::default(); + let style = TestStyle::new(1, 1.0); + + draw_circle(&mut backend, (20, 20), 5, &style, true).unwrap(); + + assert!(backend + .ops + .iter() + .any(|op| matches!(op, TestOp::Line(_, _)))); + + assert!(backend.ops.iter().any(|op| matches!(op, TestOp::Pixel(_)))); + } + + #[test] + fn draw_circle_rejects_coordinate_overflow() { + let mut backend = TestBackend::default(); + let style = TestStyle::new(1, 1.0); + + let result = draw_circle(&mut backend, (i32::MAX, 20), 5, &style, false); + + assert!(result.is_err()); + } + + #[test] + fn draw_circle_rejects_radius_expansion_overflow() { + let mut backend = TestBackend::default(); + let style = TestStyle::new(2, 1.0); + + let result = draw_circle(&mut backend, (20, 20), u32::MAX, &style, false); + + assert!(result.is_err()); + assert!(backend.ops.is_empty()); + } +} diff --git a/plotters-backend/src/rasterizer/line.rs b/plotters-backend/src/rasterizer/line.rs index ae1ddd4c..dfba885d 100644 --- a/plotters-backend/src/rasterizer/line.rs +++ b/plotters-backend/src/rasterizer/line.rs @@ -1,4 +1,10 @@ -use crate::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind}; +use crate::{ + math_guard::{ + checked_add_i32, checked_add_i64, checked_div_f64, checked_mul_i64, checked_sub_i32, + checked_sub_i64, f64_to_i32_checked, non_zero_f64, non_zero_i32, u32_to_i32_checked, + }, + BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind, +}; pub fn draw_line( back: &mut DB, @@ -12,14 +18,24 @@ pub fn draw_line( if style.stroke_width() != 1 { // If the line is wider than 1px, then we need to make it a polygon - let v = (i64::from(to.0 - from.0), i64::from(to.1 - from.1)); - let l = ((v.0 * v.0 + v.1 * v.1) as f64).sqrt(); + let dx = i64::from(checked_sub_i32(to.0, from.0)?); + let dy = i64::from(checked_sub_i32(to.1, from.1)?); - if l < 1e-5 { + let x2 = checked_mul_i64(dx, dx)?; + let y2 = checked_mul_i64(dy, dy)?; + let sum = checked_add_i64(x2, y2)? as f64; + + let len = sum.sqrt(); + + if len < 1e-5 { return Ok(()); } - let v = (v.0 as f64 / l, v.1 as f64 / l); + let len = non_zero_f64(len)?; + let v = ( + checked_div_f64(dx as f64, len)?, + checked_div_f64(dy as f64, len)?, + ); let r = f64::from(style.stroke_width()) / 2.0; let mut trans = [(v.1 * r, -v.0 * r), (-v.1 * r, v.0 * r)]; @@ -58,8 +74,9 @@ pub fn draw_line( } return Ok(()); } - - let steep = (from.0 - to.0).abs() < (from.1 - to.1).abs(); + let dx = checked_sub_i64(i64::from(to.0), i64::from(from.0))?; + let dy = checked_sub_i64(i64::from(to.1), i64::from(from.1))?; + let steep = dx.abs() < dy.abs(); if steep { from = (from.1, from.0); @@ -77,8 +94,8 @@ pub fn draw_line( if steep { size_limit = (size_limit.1, size_limit.0); } - - let grad = f64::from(to.1 - from.1) / f64::from(to.0 - from.0); + let grad = f64::from(checked_sub_i32(to.1, from.1)?) + / f64::from(non_zero_i32(checked_sub_i32(to.0, from.0)?)?); let mut put_pixel = |(x, y): BackendCoord, b: f64| { if steep { @@ -87,37 +104,289 @@ pub fn draw_line( back.draw_pixel((x, y), style.color().mix(b)) } }; + let y_max = checked_sub_i32(u32_to_i32_checked(size_limit.1)?, 1)?; + + let y_clamped = to.1.min(y_max).max(0); + + let y_delta = checked_sub_i32(y_clamped, from.1)?; + + let y_step_limit = f64_to_i32_checked((f64::from(y_delta) / non_zero_f64(grad)?).floor())?; - let y_step_limit = - (f64::from(to.1.min(size_limit.1 as i32 - 1).max(0) - from.1) / grad).floor() as i32; + let y_max = checked_sub_i32(u32_to_i32_checked(size_limit.1)?, 2)?; - let batch_start = (f64::from(from.1.min(size_limit.1 as i32 - 2).max(0) - from.1) / grad) - .abs() - .ceil() as i32 - + from.0; + let y_clamped = from.1.min(y_max).max(0); - let batch_limit = - to.0.min(size_limit.0 as i32 - 2) - .min(from.0 + y_step_limit - 1); + let y_delta = checked_sub_i32(y_clamped, from.1)?; - let mut y = f64::from(from.1) + f64::from(batch_start - from.0) * grad; + let x_offset = (f64::from(y_delta) / grad).abs().ceil() as i32; + + let batch_start = checked_add_i32(x_offset, from.0)?; + + let x_max = checked_sub_i32(u32_to_i32_checked(size_limit.0)?, 2)?; + + let stepped_x = checked_sub_i32(checked_add_i32(from.0, y_step_limit)?, 1)?; + + let batch_limit = to.0.min(x_max).min(stepped_x); + + let batch_delta = checked_sub_i32(batch_start, from.0)?; + + let mut y = f64::from(from.1) + f64::from(batch_delta) * grad; for x in batch_start..=batch_limit { - check_result!(put_pixel((x, y as i32), 1.0 + y.floor() - y)); - check_result!(put_pixel((x, y as i32 + 1), y - y.floor())); + let y_i = f64_to_i32_checked(y)?; + + let y_next = checked_add_i32(y_i, 1)?; + + let y_floor = y.floor(); + + check_result!(put_pixel((x, y_i), 1.0 + y_floor - y)); + check_result!(put_pixel((x, y_next), y - y_floor)); y += grad; } if to.0 > batch_limit && y < f64::from(to.1) { - let x = batch_limit + 1; - if 1.0 + y.floor() - y > 1e-5 { - check_result!(put_pixel((x, y as i32), 1.0 + y.floor() - y)); + let x = checked_add_i32(batch_limit, 1)?; + let y_floor = y.floor(); + + let y_i = f64_to_i32_checked(y)?; + + let lower_alpha = 1.0 + y_floor - y; + if lower_alpha > 1e-5 { + check_result!(put_pixel((x, y_i), lower_alpha)); } - if y - y.floor() > 1e-5 && y + 1.0 < f64::from(to.1) { - check_result!(put_pixel((x, y as i32 + 1), y - y.floor())); + + let upper_alpha = y - y_floor; + let y_next = checked_add_i32(y_i, 1)?; + + if upper_alpha > 1e-5 && y + 1.0 < f64::from(to.1) { + check_result!(put_pixel((x, y_next), upper_alpha)); } } Ok(()) } + +#[cfg(test)] +mod tests { + // tried keep this unit test inside this file as much as possible. + use super::*; + use crate::{BackendColor, BackendStyle, MathError}; + + // a simple backend error for testing in this module + #[derive(Debug)] + struct TestBackendError; + + impl std::fmt::Display for TestBackendError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "test backend error") + } + } + + impl std::error::Error for TestBackendError {} + + #[derive(Clone, Copy)] + struct TestStyle { + color: BackendColor, + stroke_width: u32, + } + + impl BackendStyle for TestStyle { + fn color(&self) -> BackendColor { + self.color + } + + fn stroke_width(&self) -> u32 { + self.stroke_width + } + } + + #[derive(Default)] + struct TestBackend { + size: (u32, u32), + pixels: Vec<(BackendCoord, BackendColor)>, + polygons: Vec>, + } + + impl DrawingBackend for TestBackend { + type ErrorType = TestBackendError; + + fn get_size(&self) -> (u32, u32) { + self.size + } + + fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind> { + Ok(()) + } + + fn present(&mut self) -> Result<(), DrawingErrorKind> { + Ok(()) + } + + fn draw_pixel( + &mut self, + point: BackendCoord, + color: BackendColor, + ) -> Result<(), DrawingErrorKind> { + self.pixels.push((point, color)); + Ok(()) + } + + fn fill_polygon>( + &mut self, + vertices: I, + _style: &S, + ) -> Result<(), DrawingErrorKind> { + self.polygons.push(vertices.into_iter().collect()); + Ok(()) + } + } + + fn style(stroke_width: u32, alpha: f64) -> TestStyle { + TestStyle { + color: BackendColor { + rgb: (0, 0, 0), + alpha, + }, + stroke_width, + } + } + + fn backend() -> TestBackend { + TestBackend { + size: (100, 100), + ..Default::default() + } + } + + #[test] + fn transparent_line_draws_nothing() { + let mut backend = backend(); + + assert!(draw_line(&mut backend, (0, 0), (10, 10), &style(1, 0.0)).is_ok()); + + assert!(backend.pixels.is_empty()); + assert!(backend.polygons.is_empty()); + } + + #[test] + fn zero_width_line_draws_nothing() { + let mut backend = backend(); + + assert!(draw_line(&mut backend, (0, 0), (10, 10), &style(0, 1.0)).is_ok()); + + assert!(backend.pixels.is_empty()); + assert!(backend.polygons.is_empty()); + } + + #[test] + fn vertical_line_draws_pixels_in_order() { + let mut backend = backend(); + + assert!(draw_line(&mut backend, (2, 1), (2, 3), &style(1, 1.0)).is_ok()); + + let points: Vec<_> = backend.pixels.iter().map(|(point, _)| *point).collect(); + + assert_eq!(points, vec![(2, 1), (2, 2), (2, 3)]); + } + + #[test] + fn reversed_vertical_line_draws_pixels_in_order() { + let mut backend = backend(); + + assert!(draw_line(&mut backend, (2, 3), (2, 1), &style(1, 1.0)).is_ok()); + + let points: Vec<_> = backend.pixels.iter().map(|(point, _)| *point).collect(); + + assert_eq!(points, vec![(2, 1), (2, 2), (2, 3)]); + } + + #[test] + fn horizontal_line_draws_pixels_in_order() { + let mut backend = backend(); + + assert!(draw_line(&mut backend, (1, 2), (3, 2), &style(1, 1.0)).is_ok()); + + let points: Vec<_> = backend.pixels.iter().map(|(point, _)| *point).collect(); + + assert_eq!(points, vec![(1, 2), (2, 2), (3, 2)]); + } + + #[test] + fn reversed_horizontal_line_draws_pixels_in_order() { + let mut backend = backend(); + + assert!(draw_line(&mut backend, (3, 2), (1, 2), &style(1, 1.0)).is_ok()); + + let points: Vec<_> = backend.pixels.iter().map(|(point, _)| *point).collect(); + + assert_eq!(points, vec![(1, 2), (2, 2), (3, 2)]); + } + + #[test] + fn diagonal_line_draws_some_pixels() { + let mut backend = backend(); + + assert!(draw_line(&mut backend, (1, 1), (5, 3), &style(1, 1.0)).is_ok()); + + assert!(!backend.pixels.is_empty()); + } + + #[test] + fn wide_zero_length_line_is_noop() { + let mut backend = backend(); + + assert!(draw_line(&mut backend, (5, 5), (5, 5), &style(3, 1.0)).is_ok()); + + assert!(backend.pixels.is_empty()); + assert!(backend.polygons.is_empty()); + } + + #[test] + fn wide_line_is_converted_to_polygon() { + let mut backend = backend(); + + assert!(draw_line(&mut backend, (10, 10), (20, 10), &style(4, 1.0)).is_ok()); + + assert_eq!(backend.polygons.len(), 1); + assert_eq!(backend.polygons[0].len(), 4); + } + + #[test] + fn wide_line_reports_math_error_for_out_of_range_delta() { + let mut backend = backend(); + + let err = + draw_line(&mut backend, (i32::MIN, 0), (i32::MAX, 0), &style(4, 1.0)).unwrap_err(); + + assert!( + matches!(err, DrawingErrorKind::MathError(MathError::ValueOutOfRange)), + "unexpected error: {:?}", + err + ); + } + + #[test] + fn diagonal_line_reports_math_error_for_out_of_range_x_span() { + let mut backend = backend(); + + let err = + draw_line(&mut backend, (i32::MIN, 0), (i32::MAX, 1), &style(1, 1.0)).unwrap_err(); + + assert!( + matches!(err, DrawingErrorKind::MathError(MathError::ValueOutOfRange)), + "unexpected error: {:?}", + err + ); + } + + #[test] + fn diagonal_line_with_tiny_backend_does_not_panic() { + let mut backend = TestBackend { + size: (1, 1), + ..Default::default() + }; + + assert!(draw_line(&mut backend, (0, 0), (2, 1), &style(1, 1.0)).is_ok()); + } +} diff --git a/plotters-backend/src/rasterizer/path.rs b/plotters-backend/src/rasterizer/path.rs index 004461c2..69599e46 100644 --- a/plotters-backend/src/rasterizer/path.rs +++ b/plotters-backend/src/rasterizer/path.rs @@ -1,28 +1,46 @@ -use crate::BackendCoord; +use crate::{ + math_guard::{checked_add_i64, checked_mul_i64, checked_sub_i32, non_zero_f64}, + BackendCoord, MathError, +}; +use std::convert::From; // Compute the tanginal and normal vectors of the given straight line. -fn get_dir_vector(from: BackendCoord, to: BackendCoord, flag: bool) -> ((f64, f64), (f64, f64)) { - let v = (i64::from(to.0 - from.0), i64::from(to.1 - from.1)); - let l = ((v.0 * v.0 + v.1 * v.1) as f64).sqrt(); +fn get_dir_vector( + from: BackendCoord, + to: BackendCoord, + flag: bool, +) -> Result<((f64, f64), (f64, f64)), MathError> { + let dx = i64::from(checked_sub_i32(to.0, from.0)?); + let dy = i64::from(checked_sub_i32(to.1, from.1)?); - let v = (v.0 as f64 / l, v.1 as f64 / l); + let x2 = checked_mul_i64(dx, dx)?; + let y2 = checked_mul_i64(dy, dy)?; + let sum = checked_add_i64(x2, y2)? as f64; - if flag { + let len = non_zero_f64(sum.sqrt())?; + + let v = (dx as f64 / len, dy as f64 / len); + + Ok(if flag { (v, (v.1, -v.0)) } else { (v, (-v.1, v.0)) - } + }) } // Compute the polygonized vertex of the given angle // d is the distance between the polygon edge and the actual line. // d can be negative, this will emit a vertex on the other side of the line. -fn compute_polygon_vertex(triple: &[BackendCoord; 3], d: f64, buf: &mut Vec) { +fn compute_polygon_vertex( + triple: &[BackendCoord; 3], + d: f64, + buf: &mut Vec, +) -> Result<(), MathError> { buf.clear(); // Compute the tanginal and normal vectors of the given straight line. - let (a_t, a_n) = get_dir_vector(triple[0], triple[1], false); - let (b_t, b_n) = get_dir_vector(triple[2], triple[1], true); + let (a_t, a_n) = get_dir_vector(triple[0], triple[1], false)?; + let (b_t, b_n) = get_dir_vector(triple[2], triple[1], true)?; // Compute a point that is d away from the line for line a and line b. let a_p = ( @@ -37,7 +55,7 @@ fn compute_polygon_vertex(triple: &[BackendCoord; 3], d: f64, buf: &mut Vec d * d * 16.0 { buf.push((a_p.0.round() as i32, a_p.1.round() as i32)); buf.push((b_p.0.round() as i32, b_p.1.round() as i32)); - return; + return Ok(()); } } buf.push((x.round() as i32, y.round() as i32)); + Ok(()) } fn traverse_vertices<'a>( mut vertices: impl Iterator, width: u32, mut op: impl FnMut(BackendCoord), -) { +) -> Result<(), MathError> { let mut a = vertices.next().unwrap(); let mut b = vertices.next().unwrap(); @@ -94,11 +113,11 @@ fn traverse_vertices<'a>( if let Some(new_b) = vertices.next() { b = new_b; } else { - return; + return Ok(()); } } - let (_, n) = get_dir_vector(*a, *b, false); + let (_, n) = get_dir_vector(*a, *b, false)?; op(( (f64::from(a.0) + n.0 * f64::from(width) / 2.0).round() as i32, @@ -115,33 +134,37 @@ fn traverse_vertices<'a>( recent.swap(0, 1); recent.swap(1, 2); recent[2] = *p; - compute_polygon_vertex(&recent, f64::from(width) / 2.0, &mut vertex_buf); + compute_polygon_vertex(&recent, f64::from(width) / 2.0, &mut vertex_buf)?; vertex_buf.iter().cloned().for_each(&mut op); } let b = recent[1]; let a = recent[2]; - let (_, n) = get_dir_vector(a, b, true); + let (_, n) = get_dir_vector(a, b, true)?; op(( (f64::from(a.0) + n.0 * f64::from(width) / 2.0).round() as i32, (f64::from(a.1) + n.1 * f64::from(width) / 2.0).round() as i32, )); + Ok(()) } /// Covert a path with >1px stroke width into polygon. -pub fn polygonize(vertices: &[BackendCoord], stroke_width: u32) -> Vec { +pub fn polygonize( + vertices: &[BackendCoord], + stroke_width: u32, +) -> Result, MathError> { if vertices.len() < 2 { - return vec![]; + return Ok(vec![]); } let mut ret = vec![]; - traverse_vertices(vertices.iter(), stroke_width, |v| ret.push(v)); - traverse_vertices(vertices.iter().rev(), stroke_width, |v| ret.push(v)); + traverse_vertices(vertices.iter(), stroke_width, |v| ret.push(v))?; + traverse_vertices(vertices.iter().rev(), stroke_width, |v| ret.push(v))?; - ret + Ok(ret) } #[cfg(test)] @@ -153,7 +176,7 @@ mod test { fn test_no_inf_in_compute_polygon_vertex() { let path = [(335, 386), (338, 326), (340, 286)]; let mut buf = Vec::new(); - compute_polygon_vertex(&path, 2.0, buf.as_mut()); + compute_polygon_vertex(&path, 2.0, buf.as_mut()).unwrap(); assert!(!buf.is_empty()); let nani32 = f64::INFINITY as i32; assert!(!buf.iter().any(|&v| v.0 == nani32 || v.1 == nani32)); @@ -164,9 +187,93 @@ mod test { fn standard_corner() { let path = [(10, 10), (20, 10), (20, 20)]; let mut buf = Vec::new(); - compute_polygon_vertex(&path, 2.0, buf.as_mut()); + compute_polygon_vertex(&path, 2.0, buf.as_mut()).unwrap(); assert!(!buf.is_empty()); let buf2 = vec![(18, 12)]; assert_eq!(buf, buf2); } + #[test] + fn get_dir_vector_rejects_zero_length_line() { + assert_eq!( + get_dir_vector((10, 10), (10, 10), false), + Err(MathError::ZeroDivision) + ); + } + + #[test] + fn get_dir_vector_rejects_extreme_delta_out_of_range() { + assert_eq!( + get_dir_vector((i32::MIN, 0), (i32::MAX, 0), false), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn get_dir_vector_returns_unit_tangent_for_horizontal_line() { + let result = get_dir_vector((0, 0), (10, 0), false); + + assert_eq!(result, Ok(((1.0, 0.0), (-0.0, 1.0)))); + } + + #[test] + fn get_dir_vector_returns_unit_tangent_for_vertical_line() { + let result = get_dir_vector((0, 0), (0, 10), false); + + assert_eq!(result, Ok(((0.0, 1.0), (-1.0, 0.0)))); + } + + #[test] + fn compute_polygon_vertex_handles_colinear_points() { + let path = [(0, 0), (10, 0), (20, 0)]; + let mut buf = Vec::new(); + + assert_eq!(compute_polygon_vertex(&path, 2.0, &mut buf), Ok(())); + assert_eq!(buf.len(), 1); + } + + #[test] + fn compute_polygon_vertex_rejects_repeated_adjacent_point() { + let path = [(10, 10), (10, 10), (20, 20)]; + let mut buf = Vec::new(); + + assert_eq!( + compute_polygon_vertex(&path, 2.0, &mut buf), + Err(MathError::ZeroDivision) + ); + } + + #[test] + fn traverse_vertices_with_only_repeated_points_returns_ok() { + let vertices = [(1, 1), (1, 1), (1, 1)]; + let mut out = Vec::new(); + + assert_eq!( + traverse_vertices(vertices.iter(), 2, |v| out.push(v)), + Ok(()) + ); + + assert!(out.is_empty()); + } + + #[test] + fn polygonize_returns_empty_for_less_than_two_vertices() { + assert_eq!(polygonize(&[], 2), Ok(vec![])); + assert_eq!(polygonize(&[(1, 1)], 2), Ok(vec![])); + } + + #[test] + fn polygonize_returns_vertices_for_simple_line() { + let out = polygonize(&[(10, 10), (20, 10)], 2) + .expect("polygonize should succeed for a simple line"); + + assert!(!out.is_empty()); + } + + #[test] + fn polygonize_rejects_extreme_coordinate_span() { + assert_eq!( + polygonize(&[(i32::MIN, 0), (i32::MAX, 0)], 2), + Err(MathError::ValueOutOfRange) + ); + } } diff --git a/plotters-backend/src/rasterizer/polygon.rs b/plotters-backend/src/rasterizer/polygon.rs index a91baf53..8dcc5b70 100644 --- a/plotters-backend/src/rasterizer/polygon.rs +++ b/plotters-backend/src/rasterizer/polygon.rs @@ -1,6 +1,15 @@ -use crate::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind}; - -use std::cmp::{Ord, Ordering, PartialOrd}; +use crate::{ + math_guard::{ + checked_add_u32, checked_add_usize, checked_mul_i64, checked_sub_i32, checked_sub_i64, + checked_sub_u32, checked_sub_usize, i32_to_u32_checked, non_zero_f64, + }, + BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind, MathError, +}; + +use std::{ + cmp::{Ord, Ordering, PartialOrd}, + convert::TryFrom, +}; #[derive(Clone, Debug)] struct Edge { @@ -11,39 +20,52 @@ struct Edge { } impl Edge { - fn horizontal_sweep(mut from: BackendCoord, mut to: BackendCoord) -> Option { + fn horizontal_sweep( + mut from: BackendCoord, + mut to: BackendCoord, + ) -> Result, MathError> { if from.0 == to.0 { - return None; + return Ok(None); } if from.0 > to.0 { std::mem::swap(&mut from, &mut to); } - Some(Edge { + let total_epoch = i32_to_u32_checked(checked_sub_i32(to.0, from.0)?)?; + Ok(Some(Edge { epoch: 0, - total_epoch: (to.0 - from.0) as u32, + total_epoch, slave_begin: from.1, slave_end: to.1, - }) + })) } - fn vertical_sweep(from: BackendCoord, to: BackendCoord) -> Option { + fn vertical_sweep(from: BackendCoord, to: BackendCoord) -> Result, MathError> { Edge::horizontal_sweep((from.1, from.0), (to.1, to.0)) } - fn get_master_pos(&self) -> i32 { - (self.total_epoch - self.epoch) as i32 + fn get_master_pos(&self) -> Result { + let epoch_diff = checked_sub_u32(self.total_epoch, self.epoch)?; + i32::try_from(epoch_diff).map_err(|_| MathError::ValueOutOfRange) } - fn inc_epoch(&mut self) { - self.epoch += 1; + fn inc_epoch(&mut self) -> Result<(), MathError> { + self.epoch = checked_add_u32(self.epoch, 1)?; + Ok(()) } - fn get_slave_pos(&self) -> f64 { - f64::from(self.slave_begin) - + (i64::from(self.slave_end - self.slave_begin) * i64::from(self.epoch)) as f64 - / f64::from(self.total_epoch) + fn get_slave_pos(&self) -> Result { + let slave_diff = checked_sub_i64(i64::from(self.slave_end), i64::from(self.slave_begin))?; + let product = checked_mul_i64(slave_diff, i64::from(self.epoch))? as f64; + let total_epoch = non_zero_f64(f64::from(self.total_epoch))?; + + Ok(f64::from(self.slave_begin) + product / total_epoch) + } + /// Helper method to avoid returning a `Result`, necessary for ordering and equality where `Result` is not permissable + fn get_slave_pos_unchecked_for_sort(&self) -> f64 { + self.get_slave_pos() + .expect("edge slave position calculation failed during sort") } } @@ -55,7 +77,7 @@ impl PartialOrd for Edge { impl PartialEq for Edge { fn eq(&self, other: &Self) -> bool { - self.get_slave_pos() == other.get_slave_pos() + self.get_slave_pos_unchecked_for_sort() == other.get_slave_pos_unchecked_for_sort() } } @@ -63,9 +85,8 @@ impl Eq for Edge {} impl Ord for Edge { fn cmp(&self, other: &Self) -> Ordering { - self.get_slave_pos() - .partial_cmp(&other.get_slave_pos()) - .unwrap() + self.get_slave_pos_unchecked_for_sort() + .total_cmp(&other.get_slave_pos_unchecked_for_sort()) } } @@ -94,15 +115,16 @@ pub fn fill_polygon( if x_span.0 == x_span.1 || y_span.0 == y_span.1 { return back.draw_line((x_span.0, y_span.0), (x_span.1, y_span.1), style); } - - let horizontal_sweep = x_span.1 - x_span.0 > y_span.1 - y_span.0; - + let x_diff = checked_sub_i32(x_span.1, x_span.0)?; + let y_diff = checked_sub_i32(y_span.1, y_span.0)?; + let horizontal_sweep = x_diff > y_diff; + let last_idx = checked_sub_usize(vertices.len(), 1)?; let mut edges: Vec<_> = vertices .iter() .zip(vertices.iter().skip(1)) .map(|(a, b)| (*a, *b)) .collect(); - edges.push((vertices[vertices.len() - 1], vertices[0])); + edges.push((vertices[last_idx], vertices[0])); edges.sort_by_key(|((x1, y1), (x2, y2))| { if horizontal_sweep { *x1.min(x2) @@ -131,8 +153,8 @@ pub fn fill_polygon( let mut new_vec = vec![]; for mut e in active_edge { - if e.get_master_pos() > 0 { - e.inc_epoch(); + if e.get_master_pos()? > 0 { + e.inc_epoch()?; new_vec.push(e); } } @@ -156,13 +178,13 @@ pub fn fill_polygon( Edge::horizontal_sweep(edges[idx].0, edges[idx].1) } else { Edge::vertical_sweep(edges[idx].0, edges[idx].1) - }; + }?; if let Some(edge_obj) = edge_obj { active_edge.push(edge_obj); } - idx += 1; + idx = checked_add_usize(idx, 1)?; } active_edge.sort(); @@ -179,22 +201,22 @@ pub fn fill_polygon( if let Some(a) = first.clone() { if let Some(b) = second.clone() { - if a.get_master_pos() == 0 && b.get_master_pos() != 0 { + if a.get_master_pos()? == 0 && b.get_master_pos()? != 0 { first = Some(b); second = None; continue; } - if a.get_master_pos() != 0 && b.get_master_pos() == 0 { + if a.get_master_pos()? != 0 && b.get_master_pos()? == 0 { first = Some(a); second = None; continue; } - let from = a.get_slave_pos(); - let to = b.get_slave_pos(); + let from = a.get_slave_pos()?; + let to = b.get_slave_pos()?; - if a.get_master_pos() == 0 && b.get_master_pos() == 0 && to - from > 1.0 { + if a.get_master_pos()? == 0 && b.get_master_pos()? == 0 && to - from > 1.0 { first = None; second = None; continue; @@ -240,3 +262,348 @@ pub fn fill_polygon( Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{BackendColor, BackendStyle}; + + #[derive(Debug)] + struct TestBackendError; + + impl std::fmt::Display for TestBackendError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "test backend error") + } + } + + impl std::error::Error for TestBackendError {} + + #[derive(Clone, Copy)] + struct TestStyle { + color: BackendColor, + } + + impl BackendStyle for TestStyle { + fn color(&self) -> BackendColor { + self.color + } + + fn stroke_width(&self) -> u32 { + 1 + } + } + + #[derive(Default)] + struct TestBackend { + lines: Vec<(BackendCoord, BackendCoord)>, + pixels: Vec<(BackendCoord, BackendColor)>, + } + + impl DrawingBackend for TestBackend { + type ErrorType = TestBackendError; + + fn get_size(&self) -> (u32, u32) { + (100, 100) + } + + fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind> { + Ok(()) + } + + fn present(&mut self) -> Result<(), DrawingErrorKind> { + Ok(()) + } + + fn draw_pixel( + &mut self, + point: BackendCoord, + color: BackendColor, + ) -> Result<(), DrawingErrorKind> { + self.pixels.push((point, color)); + Ok(()) + } + + fn draw_line( + &mut self, + from: BackendCoord, + to: BackendCoord, + _style: &S, + ) -> Result<(), DrawingErrorKind> { + self.lines.push((from, to)); + Ok(()) + } + } + + fn visible_style() -> TestStyle { + TestStyle { + color: BackendColor { + rgb: (0, 0, 0), + alpha: 1.0, + }, + } + } + + #[test] + fn horizontal_sweep_returns_none_for_vertical_edge() { + assert!(Edge::horizontal_sweep((1, 2), (1, 5)).unwrap().is_none()); + } + + #[test] + fn horizontal_sweep_normalizes_direction() { + let edge = Edge::horizontal_sweep((5, 10), (2, 20)).unwrap().unwrap(); + + assert_eq!(edge.epoch, 0); + assert_eq!(edge.total_epoch, 3); + assert_eq!(edge.slave_begin, 20); + assert_eq!(edge.slave_end, 10); + } + + #[test] + fn horizontal_sweep_reports_overflow_for_extreme_span() { + let err = Edge::horizontal_sweep((i32::MIN, 0), (i32::MAX, 0)).unwrap_err(); + + assert_eq!(err, MathError::ValueOutOfRange); + } + + #[test] + fn vertical_sweep_uses_y_axis_as_master_axis() { + let edge = Edge::vertical_sweep((10, 2), (20, 5)).unwrap().unwrap(); + + assert_eq!(edge.epoch, 0); + assert_eq!(edge.total_epoch, 3); + assert_eq!(edge.slave_begin, 10); + assert_eq!(edge.slave_end, 20); + } + + #[test] + fn get_master_pos_returns_remaining_epoch_distance() { + let edge = Edge { + epoch: 2, + total_epoch: 5, + slave_begin: 0, + slave_end: 10, + }; + + assert_eq!(edge.get_master_pos(), Ok(3)); + } + + #[test] + fn get_master_pos_reports_underflow_when_epoch_exceeds_total_epoch() { + let edge = Edge { + epoch: 6, + total_epoch: 5, + slave_begin: 0, + slave_end: 10, + }; + + assert_eq!(edge.get_master_pos(), Err(MathError::ValueOutOfRange)); + } + + #[test] + fn get_master_pos_reports_out_of_range_for_large_u32_result() { + let edge = Edge { + epoch: 0, + total_epoch: u32::MAX, + slave_begin: 0, + slave_end: 10, + }; + + assert_eq!(edge.get_master_pos(), Err(MathError::ValueOutOfRange)); + } + + #[test] + fn inc_epoch_advances_epoch() { + let mut edge = Edge { + epoch: 0, + total_epoch: 5, + slave_begin: 0, + slave_end: 10, + }; + + assert_eq!(edge.inc_epoch(), Ok(())); + assert_eq!(edge.epoch, 1); + } + + #[test] + fn inc_epoch_reports_overflow_at_max_epoch() { + let mut edge = Edge { + epoch: u32::MAX, + total_epoch: u32::MAX, + slave_begin: 0, + slave_end: 10, + }; + + assert_eq!(edge.inc_epoch(), Err(MathError::ValueOutOfRange)); + assert_eq!(edge.epoch, u32::MAX); + } + + #[test] + fn get_slave_pos_interpolates_between_slave_points() { + let edge = Edge { + epoch: 5, + total_epoch: 10, + slave_begin: 0, + slave_end: 20, + }; + + assert_eq!(edge.get_slave_pos(), Ok(10.0)); + } + + #[test] + fn get_slave_pos_includes_slave_begin_offset() { + let edge = Edge { + epoch: 5, + total_epoch: 10, + slave_begin: 10, + slave_end: 30, + }; + + assert_eq!(edge.get_slave_pos(), Ok(20.0)); + } + + #[test] + fn get_slave_pos_rejects_zero_total_epoch() { + let edge = Edge { + epoch: 5, + total_epoch: 0, + slave_begin: 0, + slave_end: 20, + }; + + assert_eq!(edge.get_slave_pos(), Err(MathError::ZeroDivision)); + } + + #[test] + fn edge_ordering_sorts_by_slave_position() { + let low = Edge { + epoch: 5, + total_epoch: 10, + slave_begin: 0, + slave_end: 10, + }; + + let high = Edge { + epoch: 5, + total_epoch: 10, + slave_begin: 0, + slave_end: 20, + }; + + assert!(low < high); + } + + #[test] + fn edge_ordering_treats_equal_slave_positions_as_equal() { + let a = Edge { + epoch: 5, + total_epoch: 10, + slave_begin: 0, + slave_end: 20, + }; + + let b = Edge { + epoch: 10, + total_epoch: 20, + slave_begin: 0, + slave_end: 20, + }; + + assert_eq!(a.cmp(&b), Ordering::Equal); + assert_eq!(a, b); + } + + #[test] + fn edge_ordering_sorts_vec_by_slave_position() { + let mut edges = [ + Edge { + epoch: 5, + total_epoch: 10, + slave_begin: 0, + slave_end: 30, + }, + Edge { + epoch: 5, + total_epoch: 10, + slave_begin: 0, + slave_end: 10, + }, + Edge { + epoch: 5, + total_epoch: 10, + slave_begin: 0, + slave_end: 20, + }, + ]; + + edges.sort(); + + let positions: Vec = edges + .iter() + .map(|edge| edge.get_slave_pos().expect("test edge should be valid")) + .collect(); + + assert_eq!(positions, vec![5.0, 10.0, 15.0]); + } + + #[test] + fn fill_polygon_with_empty_vertices_draws_nothing() { + let mut backend = TestBackend::default(); + + fill_polygon(&mut backend, &[], &visible_style()).unwrap(); + + assert!(backend.lines.is_empty()); + assert!(backend.pixels.is_empty()); + } + + #[test] + fn fill_polygon_with_horizontal_line_draws_single_line() { + let mut backend = TestBackend::default(); + + fill_polygon(&mut backend, &[(1, 2), (4, 2), (7, 2)], &visible_style()).unwrap(); + + assert_eq!(backend.lines, vec![((1, 2), (7, 2))]); + assert!(backend.pixels.is_empty()); + } + + #[test] + fn fill_polygon_with_vertical_line_draws_single_line() { + let mut backend = TestBackend::default(); + + fill_polygon(&mut backend, &[(3, 1), (3, 4), (3, 7)], &visible_style()).unwrap(); + + assert_eq!(backend.lines, vec![((3, 1), (3, 7))]); + assert!(backend.pixels.is_empty()); + } + + #[test] + fn fill_polygon_fills_simple_rectangle() { + let mut backend = TestBackend::default(); + + fill_polygon( + &mut backend, + &[(1, 1), (4, 1), (4, 3), (1, 3)], + &visible_style(), + ) + .unwrap(); + + assert!(!backend.lines.is_empty()); + } + + #[test] + fn fill_polygon_propagates_math_error_from_extreme_horizontal_span() { + let mut backend = TestBackend::default(); + + let err = fill_polygon( + &mut backend, + &[(i32::MIN, 0), (i32::MAX, 1), (i32::MAX, 2), (i32::MIN, 2)], + &visible_style(), + ) + .unwrap_err(); + dbg!(&err); + assert!(matches!( + err, + DrawingErrorKind::MathError(MathError::ValueOutOfRange) + )); + } +} diff --git a/plotters-backend/src/rasterizer/rect.rs b/plotters-backend/src/rasterizer/rect.rs index cd6c7740..427d7d3d 100644 --- a/plotters-backend/src/rasterizer/rect.rs +++ b/plotters-backend/src/rasterizer/rect.rs @@ -1,4 +1,6 @@ -use crate::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind}; +use crate::{ + math_guard::checked_sub_i32, BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind, +}; pub fn draw_rect( b: &mut B, @@ -10,48 +12,202 @@ pub fn draw_rect( if style.color().alpha == 0.0 { return Ok(()); } - let (upper_left, bottom_right) = ( - ( - upper_left.0.min(bottom_right.0), - upper_left.1.min(bottom_right.1), - ), - ( - upper_left.0.max(bottom_right.0), - upper_left.1.max(bottom_right.1), - ), - ); + + let x0 = upper_left.0.min(bottom_right.0); + let y0 = upper_left.1.min(bottom_right.1); + let x1 = upper_left.0.max(bottom_right.0); + let y1 = upper_left.1.max(bottom_right.1); + + let width = checked_sub_i32(x1, x0)?; + let height = checked_sub_i32(y1, y0)?; if fill { - if bottom_right.0 - upper_left.0 < bottom_right.1 - upper_left.1 { - for x in upper_left.0..=bottom_right.0 { - check_result!(b.draw_line((x, upper_left.1), (x, bottom_right.1), style)); + if width < height { + for x in x0..=x1 { + check_result!(b.draw_line((x, y0), (x, y1), style)); } } else { - for y in upper_left.1..=bottom_right.1 { - check_result!(b.draw_line((upper_left.0, y), (bottom_right.0, y), style)); + for y in y0..=y1 { + check_result!(b.draw_line((x0, y), (x1, y), style)); } } } else { - b.draw_line( - (upper_left.0, upper_left.1), - (upper_left.0, bottom_right.1), - style, - )?; - b.draw_line( - (upper_left.0, upper_left.1), - (bottom_right.0, upper_left.1), - style, - )?; - b.draw_line( - (bottom_right.0, bottom_right.1), - (upper_left.0, bottom_right.1), - style, - )?; - b.draw_line( - (bottom_right.0, bottom_right.1), - (bottom_right.0, upper_left.1), - style, - )?; + b.draw_line((x0, y0), (x0, y1), style)?; + b.draw_line((x0, y0), (x1, y0), style)?; + b.draw_line((x1, y1), (x0, y1), style)?; + b.draw_line((x1, y1), (x1, y0), style)?; } + Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{BackendColor, BackendStyle, MathError}; + + #[derive(Debug)] + struct TestBackendError; + + impl std::fmt::Display for TestBackendError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "test backend error") + } + } + + impl std::error::Error for TestBackendError {} + + #[derive(Clone, Copy)] + struct TestStyle { + color: BackendColor, + } + + impl BackendStyle for TestStyle { + fn color(&self) -> BackendColor { + self.color + } + + fn stroke_width(&self) -> u32 { + 1 + } + } + + #[derive(Default)] + struct TestBackend { + lines: Vec<(BackendCoord, BackendCoord)>, + } + + impl DrawingBackend for TestBackend { + type ErrorType = TestBackendError; + + fn get_size(&self) -> (u32, u32) { + (100, 100) + } + + fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind> { + Ok(()) + } + + fn present(&mut self) -> Result<(), DrawingErrorKind> { + Ok(()) + } + + fn draw_pixel( + &mut self, + _point: BackendCoord, + _color: BackendColor, + ) -> Result<(), DrawingErrorKind> { + Ok(()) + } + + fn draw_line( + &mut self, + from: BackendCoord, + to: BackendCoord, + _style: &S, + ) -> Result<(), DrawingErrorKind> { + self.lines.push((from, to)); + Ok(()) + } + } + + fn visible_style() -> TestStyle { + TestStyle { + color: BackendColor { + rgb: (0, 0, 0), + alpha: 1.0, + }, + } + } + + fn transparent_style() -> TestStyle { + TestStyle { + color: BackendColor { + rgb: (0, 0, 0), + alpha: 0.0, + }, + } + } + + #[test] + fn transparent_rect_draws_nothing() { + let mut backend = TestBackend::default(); + + draw_rect(&mut backend, (0, 0), (10, 10), &transparent_style(), false).unwrap(); + + assert!(backend.lines.is_empty()); + } + + #[test] + fn unfilled_rect_draws_four_edges() { + let mut backend = TestBackend::default(); + + draw_rect(&mut backend, (1, 2), (4, 5), &visible_style(), false).unwrap(); + + assert_eq!( + backend.lines, + vec![ + ((1, 2), (1, 5)), + ((1, 2), (4, 2)), + ((4, 5), (1, 5)), + ((4, 5), (4, 2)), + ] + ); + } + + #[test] + fn unfilled_rect_normalizes_reversed_coordinates() { + let mut backend = TestBackend::default(); + + draw_rect(&mut backend, (4, 5), (1, 2), &visible_style(), false).unwrap(); + + assert_eq!( + backend.lines, + vec![ + ((1, 2), (1, 5)), + ((1, 2), (4, 2)), + ((4, 5), (1, 5)), + ((4, 5), (4, 2)), + ] + ); + } + + #[test] + fn filled_wide_rect_draws_horizontal_lines() { + let mut backend = TestBackend::default(); + + draw_rect(&mut backend, (1, 1), (4, 2), &visible_style(), true).unwrap(); + + assert_eq!(backend.lines, vec![((1, 1), (4, 1)), ((1, 2), (4, 2)),]); + } + + #[test] + fn filled_tall_rect_draws_vertical_lines() { + let mut backend = TestBackend::default(); + + draw_rect(&mut backend, (1, 1), (2, 4), &visible_style(), true).unwrap(); + + assert_eq!(backend.lines, vec![((1, 1), (1, 4)), ((2, 1), (2, 4)),]); + } + + #[test] + fn rect_with_extreme_coordinates_returns_out_of_range_math_error() { + let mut backend = TestBackend::default(); + + let err = draw_rect( + &mut backend, + (i32::MIN, 0), + (i32::MAX, 1), + &visible_style(), + true, + ) + .unwrap_err(); + + assert!(matches!( + err, + DrawingErrorKind::MathError(MathError::ValueOutOfRange) + )); + + assert!(backend.lines.is_empty()); + } +} diff --git a/plotters-backend/src/style.rs b/plotters-backend/src/style.rs index 028a06bc..28deb1e0 100644 --- a/plotters-backend/src/style.rs +++ b/plotters-backend/src/style.rs @@ -31,3 +31,126 @@ impl BackendStyle for BackendColor { *self } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn backend_color_mix_multiplies_alpha() { + let color = BackendColor { + alpha: 0.8, + rgb: (10, 20, 30), + }; + + let mixed = color.mix(0.5); + + assert!((mixed.alpha - 0.4).abs() < f64::EPSILON); + assert_eq!(mixed.rgb, (10, 20, 30)); + } + + #[test] + fn backend_color_mix_preserves_rgb() { + let color = BackendColor { + alpha: 1.0, + rgb: (255, 128, 64), + }; + + let mixed = color.mix(0.25); + + assert_eq!(mixed.rgb, color.rgb); + } + + #[test] + fn backend_color_mix_with_zero_alpha_makes_fully_transparent() { + let color = BackendColor { + alpha: 1.0, + rgb: (1, 2, 3), + }; + + let mixed = color.mix(0.0); + + assert_eq!(mixed.alpha, 0.0); + assert_eq!(mixed.rgb, (1, 2, 3)); + } + + #[test] + fn backend_color_mix_combines_with_existing_alpha() { + let color = BackendColor { + alpha: 0.5, + rgb: (1, 2, 3), + }; + + let mixed = color.mix(0.5); + + assert_eq!(mixed.alpha, 0.25); + assert_eq!(mixed.rgb, (1, 2, 3)); + } + + #[test] + fn backend_color_as_style_returns_itself_as_color() { + let color = BackendColor { + alpha: 0.75, + rgb: (4, 5, 6), + }; + + let style_color = color.color(); + + assert_eq!(style_color.alpha, 0.75); + assert_eq!(style_color.rgb, (4, 5, 6)); + } + + #[test] + fn backend_color_as_style_uses_default_stroke_width() { + let color = BackendColor { + alpha: 1.0, + rgb: (4, 5, 6), + }; + + assert_eq!(color.stroke_width(), 1); + } + + struct CustomStyle { + color: BackendColor, + stroke_width: u32, + } + + impl BackendStyle for CustomStyle { + fn color(&self) -> BackendColor { + self.color + } + + fn stroke_width(&self) -> u32 { + self.stroke_width + } + } + + #[test] + fn custom_backend_style_can_override_stroke_width() { + let style = CustomStyle { + color: BackendColor { + alpha: 1.0, + rgb: (7, 8, 9), + }, + stroke_width: 5, + }; + + assert_eq!(style.stroke_width(), 5); + } + + #[test] + fn custom_backend_style_returns_its_color() { + let style = CustomStyle { + color: BackendColor { + alpha: 0.6, + rgb: (7, 8, 9), + }, + stroke_width: 5, + }; + + let color = style.color(); + + assert_eq!(color.alpha, 0.6); + assert_eq!(color.rgb, (7, 8, 9)); + } +} diff --git a/plotters-backend/src/text.rs b/plotters-backend/src/text.rs index 1d26005e..f68b89fa 100644 --- a/plotters-backend/src/text.rs +++ b/plotters-backend/src/text.rs @@ -1,4 +1,5 @@ use super::{BackendColor, BackendCoord}; +use crate::{math_guard::checked_neg_i32, MathError}; use std::error::Error; /// Describes font family. @@ -129,16 +130,15 @@ impl FontTransform { /// - `x`: The x coordinate in pixels before transform /// - `y`: The y coordinate in pixels before transform /// - **returns**: The coordinate after transform - pub fn transform(&self, x: i32, y: i32) -> (i32, i32) { - match self { + pub fn transform(&self, x: i32, y: i32) -> Result<(i32, i32), MathError> { + Ok(match self { FontTransform::None => (x, y), - FontTransform::Rotate90 => (-y, x), - FontTransform::Rotate180 => (-x, -y), - FontTransform::Rotate270 => (y, -x), - } + FontTransform::Rotate90 => (checked_neg_i32(y)?, x), + FontTransform::Rotate180 => (checked_neg_i32(x)?, checked_neg_i32(y)?), + FontTransform::Rotate270 => (y, checked_neg_i32(x)?), + }) } } - /// Describes the font style. Such as Italic, Oblique, etc. #[derive(Clone, Copy)] pub enum FontStyle { @@ -229,3 +229,284 @@ pub trait BackendTextStyle { draw: DrawFunc, ) -> Result, Self::FontError>; } + +#[cfg(test)] +mod tests { + use super::*; + use std::fmt; + + #[derive(Debug)] + struct TestFontError; + + impl fmt::Display for TestFontError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "test font error") + } + } + + impl Error for TestFontError {} + + struct TestTextStyle { + family: FontFamily<'static>, + } + + impl BackendTextStyle for TestTextStyle { + type FontError = TestFontError; + + fn family(&self) -> FontFamily<'_> { + self.family + } + + fn layout_box(&self, text: &str) -> Result<((i32, i32), (i32, i32)), Self::FontError> { + Ok(((0, 0), (text.len() as i32, 10))) + } + + fn draw Result<(), E>>( + &self, + _text: &str, + pos: BackendCoord, + mut draw: DrawFunc, + ) -> Result, Self::FontError> { + Ok(draw(pos.0, pos.1, self.color())) + } + } + + #[test] + fn font_family_as_str_returns_css_family_names() { + assert_eq!(FontFamily::Serif.as_str(), "serif"); + assert_eq!(FontFamily::SansSerif.as_str(), "sans-serif"); + assert_eq!(FontFamily::Monospace.as_str(), "monospace"); + assert_eq!(FontFamily::Name("Fira Sans").as_str(), "Fira Sans"); + } + + #[test] + fn font_family_from_recognizes_builtin_names_case_insensitively() { + assert!(matches!(FontFamily::from("serif"), FontFamily::Serif)); + assert!(matches!(FontFamily::from("SERIF"), FontFamily::Serif)); + + assert!(matches!( + FontFamily::from("sans-serif"), + FontFamily::SansSerif + )); + assert!(matches!( + FontFamily::from("SANS-SERIF"), + FontFamily::SansSerif + )); + + assert!(matches!( + FontFamily::from("monospace"), + FontFamily::Monospace + )); + assert!(matches!( + FontFamily::from("MONOSPACE"), + FontFamily::Monospace + )); + } + + #[test] + fn font_family_from_preserves_unknown_family_name() { + match FontFamily::from("Fira Sans") { + FontFamily::Name(name) => assert_eq!(name, "Fira Sans"), + _ => panic!("expected custom font family name"), + } + } + + #[test] + fn text_anchor_pos_default_is_left_top() { + let pos = text_anchor::Pos::default(); + + assert!(matches!(pos.h_pos, text_anchor::HPos::Left)); + assert!(matches!(pos.v_pos, text_anchor::VPos::Top)); + } + + #[test] + fn text_anchor_pos_new_uses_given_positions() { + let pos = text_anchor::Pos::new(text_anchor::HPos::Right, text_anchor::VPos::Bottom); + + assert!(matches!(pos.h_pos, text_anchor::HPos::Right)); + assert!(matches!(pos.v_pos, text_anchor::VPos::Bottom)); + } + + #[test] + fn font_transform_none_keeps_coordinates() { + assert_eq!(FontTransform::None.transform(2, 3), Ok((2, 3))); + } + + #[test] + fn font_transform_rotate90_rotates_coordinates() { + assert_eq!(FontTransform::Rotate90.transform(2, 3), Ok((-3, 2))); + } + + #[test] + fn font_transform_rotate180_rotates_coordinates() { + assert_eq!(FontTransform::Rotate180.transform(2, 3), Ok((-2, -3))); + } + + #[test] + fn font_transform_rotate270_rotates_coordinates() { + assert_eq!(FontTransform::Rotate270.transform(2, 3), Ok((3, -2))); + } + + #[test] + fn font_transform_rotate90_rejects_y_min_value() { + assert_eq!( + FontTransform::Rotate90.transform(2, i32::MIN), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn font_transform_rotate180_rejects_x_min_value() { + assert_eq!( + FontTransform::Rotate180.transform(i32::MIN, 3), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn font_transform_rotate180_rejects_y_min_value() { + assert_eq!( + FontTransform::Rotate180.transform(2, i32::MIN), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn font_transform_rotate270_rejects_x_min_value() { + assert_eq!( + FontTransform::Rotate270.transform(i32::MIN, 3), + Err(MathError::ValueOutOfRange) + ); + } + + #[test] + fn font_style_as_str_returns_css_style_names() { + assert_eq!(FontStyle::Normal.as_str(), "normal"); + assert_eq!(FontStyle::Italic.as_str(), "italic"); + assert_eq!(FontStyle::Oblique.as_str(), "oblique"); + assert_eq!(FontStyle::Bold.as_str(), "bold"); + } + + #[test] + fn font_style_from_recognizes_known_styles_case_insensitively() { + assert!(matches!(FontStyle::from("normal"), FontStyle::Normal)); + assert!(matches!(FontStyle::from("NORMAL"), FontStyle::Normal)); + + assert!(matches!(FontStyle::from("italic"), FontStyle::Italic)); + assert!(matches!(FontStyle::from("ITALIC"), FontStyle::Italic)); + + assert!(matches!(FontStyle::from("oblique"), FontStyle::Oblique)); + assert!(matches!(FontStyle::from("OBLIQUE"), FontStyle::Oblique)); + + assert!(matches!(FontStyle::from("bold"), FontStyle::Bold)); + assert!(matches!(FontStyle::from("BOLD"), FontStyle::Bold)); + } + + #[test] + fn font_style_from_unknown_value_defaults_to_normal() { + assert!(matches!(FontStyle::from("weird-style"), FontStyle::Normal)); + } + + #[test] + fn backend_text_style_default_color_is_opaque_black() { + let style = TestTextStyle { + family: FontFamily::Serif, + }; + + let color = style.color(); + + assert_eq!(color.alpha, 1.0); + assert_eq!(color.rgb, (0, 0, 0)); + } + + #[test] + fn backend_text_style_default_size_is_one() { + let style = TestTextStyle { + family: FontFamily::Serif, + }; + + assert_eq!(style.size(), 1.0); + } + + #[test] + fn backend_text_style_default_transform_is_none() { + let style = TestTextStyle { + family: FontFamily::Serif, + }; + + assert_eq!(style.transform().transform(4, 5), Ok((4, 5))); + } + + #[test] + fn backend_text_style_default_style_is_normal() { + let style = TestTextStyle { + family: FontFamily::Serif, + }; + + assert!(matches!(style.style(), FontStyle::Normal)); + } + + #[test] + fn backend_text_style_default_anchor_is_left_top() { + let style = TestTextStyle { + family: FontFamily::Serif, + }; + + let anchor = style.anchor(); + + assert!(matches!(anchor.h_pos, text_anchor::HPos::Left)); + assert!(matches!(anchor.v_pos, text_anchor::VPos::Top)); + } + + #[test] + fn backend_text_style_returns_family() { + let style = TestTextStyle { + family: FontFamily::Monospace, + }; + + assert!(matches!(style.family(), FontFamily::Monospace)); + } + + #[test] + fn backend_text_style_layout_box_uses_text_length() { + let style = TestTextStyle { + family: FontFamily::Serif, + }; + + assert_eq!(style.layout_box("hello").unwrap(), ((0, 0), (5, 10))); + } + + #[test] + fn backend_text_style_draw_invokes_draw_callback() { + let style = TestTextStyle { + family: FontFamily::Serif, + }; + + let mut drawn = Vec::new(); + + let result = style + .draw("hello", (3, 4), |x, y, color| { + drawn.push((x, y, color.rgb, color.alpha)); + Ok::<(), TestFontError>(()) + }) + .unwrap(); + + assert!(result.is_ok()); + assert_eq!(drawn, vec![(3, 4, (0, 0, 0), 1.0)]); + } + + #[test] + fn backend_text_style_draw_propagates_callback_error_inside_ok() { + let style = TestTextStyle { + family: FontFamily::Serif, + }; + + let result = style + .draw("hello", (3, 4), |_x, _y, _color| { + Err::<(), TestFontError>(TestFontError) + }) + .unwrap(); + + assert!(result.is_err()); + } +} diff --git a/plotters-svg/src/svg.rs b/plotters-svg/src/svg.rs index e24623d7..51484f0a 100644 --- a/plotters-svg/src/svg.rs +++ b/plotters-svg/src/svg.rs @@ -602,11 +602,8 @@ impl<'a> DrawingBackend for SVGBackend<'a> { let color = image::ColorType::Rgb8; - encoder.write_image(src, w, h, color).map_err(|e| { - DrawingErrorKind::DrawingError(Error::new( - std::io::ErrorKind::Other, - format!("Image error: {}", e), - )) + encoder.write_image(src, w, h, color.into()).map_err(|e| { + DrawingErrorKind::DrawingError(Error::other(format!("Image error: {}", e))) })?; } diff --git a/plotters/src/chart/context/cartesian2d/draw_impl.rs b/plotters/src/chart/context/cartesian2d/draw_impl.rs index 616d7d8e..2ad64f26 100644 --- a/plotters/src/chart/context/cartesian2d/draw_impl.rs +++ b/plotters/src/chart/context/cartesian2d/draw_impl.rs @@ -185,7 +185,7 @@ impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesia let right_align_width = (min_width * 2).min(max_width); /* Then we need to draw the tick mark and the label */ - for ((p, t), w) in labels.iter().zip(label_width.into_iter()) { + for ((p, t), w) in labels.iter().zip(label_width) { /* Make sure we are actually in the visible range */ let rp = if orientation.0 == 0 { *p - x0 } else { *p - y0 }; @@ -240,13 +240,9 @@ impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesia let ymax = th as i32 - 1; let (kx0, ky0, kx1, ky1) = match orientation { (dx, dy) if dx > 0 && dy == 0 => (0, *p - y0, tick_size, *p - y0), - (dx, dy) if dx < 0 && dy == 0 => { - (xmax - tick_size, *p - y0, xmax, *p - y0) - } + (dx, dy) if dx < 0 && dy == 0 => (xmax - tick_size, *p - y0, xmax, *p - y0), (dx, dy) if dx == 0 && dy > 0 => (*p - x0, 0, *p - x0, tick_size), - (dx, dy) if dx == 0 && dy < 0 => { - (*p - x0, ymax - tick_size, *p - x0, ymax) - } + (dx, dy) if dx == 0 && dy < 0 => (*p - x0, ymax - tick_size, *p - x0, ymax), _ => panic!("Bug: Invalid orientation specification"), }; let line = PathElement::new(vec![(kx0, ky0), (kx1, ky1)], *style); @@ -348,10 +344,7 @@ impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesia for (px, _) in &x_labels { let x = *px - x0; if x >= 0 && x < dw { - let line = PathElement::new( - vec![(x, 0), (x, abs_tick)], - *axis_style, - ); + let line = PathElement::new(vec![(x, 0), (x, abs_tick)], *axis_style); plot_area.draw(&line)?; } } @@ -380,10 +373,7 @@ impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesia for (py, _) in &y_labels { let y = *py - y0; if y >= 0 && y < dh { - let line = PathElement::new( - vec![(0, y), (abs_tick, y)], - *axis_style, - ); + let line = PathElement::new(vec![(0, y), (abs_tick, y)], *axis_style); plot_area.draw(&line)?; } } @@ -407,4 +397,4 @@ impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesia Ok(()) } -} \ No newline at end of file +} diff --git a/plotters/src/chart/series.rs b/plotters/src/chart/series.rs index 997f30d0..e0539e8b 100644 --- a/plotters/src/chart/series.rs +++ b/plotters/src/chart/series.rs @@ -285,7 +285,7 @@ impl<'a, 'b, DB: DrawingBackend + 'a, CT: CoordTranslate> SeriesLabelStyle<'a, ' DrawingAreaErrorKind::BackendError(DrawingErrorKind::FontError(Box::new(e))) })? .into_iter() - .zip(funcs.into_iter()) + .zip(funcs) { let legend_element = make_elem((label_x + margin, (y0 + y1) / 2)); drawing_area.draw(&legend_element)?; diff --git a/plotters/src/lib.rs b/plotters/src/lib.rs index b3731333..0b87ed41 100644 --- a/plotters/src/lib.rs +++ b/plotters/src/lib.rs @@ -1,4 +1,6 @@ #![warn(missing_docs)] +// this will warn against the math that can cause bugs +//#![warn(clippy::arithmetic_side_effects)] #![allow(clippy::type_complexity)] #![allow(clippy::doc_overindented_list_items)] #![cfg_attr(doc_cfg, feature(doc_cfg))] diff --git a/plotters/src/style/color.rs b/plotters/src/style/color.rs index 056755c9..7f02722c 100644 --- a/plotters/src/style/color.rs +++ b/plotters/src/style/color.rs @@ -169,10 +169,10 @@ impl HSLColor { if !h.is_finite() { return Err(HSLColorError::NonFiniteHue); } - if !s.is_finite() || s < 0.0 || s > 1.0 { + if !s.is_finite() || !(0.0..=1.0).contains(&s) { return Err(HSLColorError::SaturationOutOfRange); } - if !l.is_finite() || l < 0.0 || l > 1.0 { + if !l.is_finite() || !(0.0..=1.0).contains(&l) { return Err(HSLColorError::LightnessOutOfRange); } Ok(Self(h, s, l)) @@ -277,12 +277,18 @@ mod hue_robustness_tests { .rgb; assert_eq!(normalized, via_helper); - let wrap_positive = - HSLColor::from_degrees(720.0, 1.0, 0.5).unwrap().to_backend_color().rgb; - let wrap_negative = - HSLColor::from_degrees(-120.0, 1.0, 0.5).unwrap().to_backend_color().rgb; - let canonical = - HSLColor::from_degrees(0.0, 1.0, 0.5).unwrap().to_backend_color().rgb; + let wrap_positive = HSLColor::from_degrees(720.0, 1.0, 0.5) + .unwrap() + .to_backend_color() + .rgb; + let wrap_negative = HSLColor::from_degrees(-120.0, 1.0, 0.5) + .unwrap() + .to_backend_color() + .rgb; + let canonical = HSLColor::from_degrees(0.0, 1.0, 0.5) + .unwrap() + .to_backend_color() + .rgb; assert_eq!(wrap_positive, canonical); assert_eq!( diff --git a/plotters/src/style/font/ab_glyph.rs b/plotters/src/style/font/ab_glyph.rs index 42b43344..59eae04f 100644 --- a/plotters/src/style/font/ab_glyph.rs +++ b/plotters/src/style/font/ab_glyph.rs @@ -1,9 +1,7 @@ -use super::{FontData, FontFamily, FontStyle, LayoutBox}; +use super::{FontData, FontFamily, FontStyle, LayoutBox, FontError}; use ab_glyph::{Font, FontRef, ScaleFont}; -use core::fmt::{self, Display}; use once_cell::sync::Lazy; use std::collections::HashMap; -use std::error::Error; use std::sync::RwLock; struct FontMap { @@ -68,22 +66,6 @@ pub struct FontDataInternal { font_ref: FontRef<'static>, } -#[derive(Debug, Clone)] -pub enum FontError { - /// No idea what the problem is - Unknown, - /// No font data available for the requested family and style. - FontUnavailable, -} -impl Display for FontError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Since it makes literally no difference to how we'd format - // this, just delegate to the derived Debug formatter. - write!(f, "{:?}", self) - } -} -impl Error for FontError {} - impl FontData for FontDataInternal { // TODO: can we rename this to `Error`? type ErrorType = FontError; diff --git a/plotters/src/style/font/error.rs b/plotters/src/style/font/error.rs new file mode 100644 index 00000000..9591e4f8 --- /dev/null +++ b/plotters/src/style/font/error.rs @@ -0,0 +1,110 @@ +use plotters_backend::MathError; +use std::sync::Arc; +use font_kit::error::FontLoadingError; +use font_kit::error::GlyphLoadingError; + +/// Unified error type for font operations. +#[derive(Debug, Clone)] +pub enum FontError { + /// Failed to lock shared font state. + LockError, + /// Requested font family/style was not found. + NoSuchFont(String, String), + /// Failed to load font data. + FontLoadError(Arc), + /// Failed to load or render a glyph. + GlyphError(Arc), + /// Font handle was unavailable. + FontHandleUnavailable, + /// Failed to parse font face data. + FaceParseError(String), + /// Unknown font error. + Unknown, + /// Requested font is unavailable. + FontUnavailable, + /// Arithmetic failed during font layout. + MathError(MathError), +} + +impl std::fmt::Display for FontError { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FontError::LockError => write!(fmt, "Could not lock mutex"), + FontError::NoSuchFont(family, style) => { + write!(fmt, "No such font: {} {}", family, style) + } + FontError::FontLoadError(e) => write!(fmt, "Font loading error {}", e), + FontError::GlyphError(e) => write!(fmt, "Glyph error {}", e), + FontError::FontHandleUnavailable => write!(fmt, "Font handle is not available"), + FontError::FaceParseError(e) => write!(fmt, "Font face parse error {}", e), + FontError::Unknown => write!(fmt, "Unknown font error"), + FontError::FontUnavailable => write!(fmt, "Font unavailable"), + FontError::MathError(e) => write!(fmt, "Math error: {}", e), + } + } +} + +impl From for FontError { + fn from(err: MathError) -> Self { + FontError::MathError(err) + } +} + +impl std::error::Error for FontError {} + +#[cfg(test)] +mod tests { + use super::*; + use std::error::Error; + + #[test] + fn displays_simple_font_errors() { + assert_eq!(FontError::LockError.to_string(), "Could not lock mutex"); + assert_eq!( + FontError::NoSuchFont("Arial".into(), "Bold".into()).to_string(), + "No such font: Arial Bold" + ); + assert_eq!( + FontError::FontHandleUnavailable.to_string(), + "Font handle is not available" + ); + assert_eq!( + FontError::FaceParseError("bad face".into()).to_string(), + "Font face parse error bad face" + ); + assert_eq!(FontError::Unknown.to_string(), "Unknown font error"); + assert_eq!(FontError::FontUnavailable.to_string(), "Font unavailable"); + } + + #[test] + fn converts_math_error_into_font_error() { + let err: FontError = MathError::ValueOverflow.into(); + + assert!(matches!( + err, + FontError::MathError(MathError::ValueOverflow) + )); + } + + #[test] + fn displays_math_error() { + let err = FontError::MathError(MathError::ZeroDivision); + + assert_eq!(err.to_string(), "Math error: attempted to divide by zero"); + } + + #[test] + fn implements_std_error() { + fn assert_error() {} + + assert_error::(); + } + + #[test] + fn clones_font_error() { + let err = FontError::NoSuchFont("serif".into(), "italic".into()); + let cloned = err.clone(); + + assert_eq!(err.to_string(), cloned.to_string()); + } +} diff --git a/plotters/src/style/font/font_desc.rs b/plotters/src/style/font/font_desc.rs index 42f45079..4650ccf7 100644 --- a/plotters/src/style/font/font_desc.rs +++ b/plotters/src/style/font/font_desc.rs @@ -1,4 +1,4 @@ -use super::{FontData, FontDataInternal}; +use super::{FontData, FontDataInternal, FontError}; use crate::style::text_anchor::Pos; use crate::style::{Color, TextStyle}; @@ -6,10 +6,7 @@ use std::convert::From; pub use plotters_backend::{FontFamily, FontStyle, FontTransform}; -/// The error type for the font implementation -pub type FontError = ::ErrorType; - -/// The type we used to represent a result of any font operations +/// The type used to represent a result of any font operation. pub type FontResult = Result; /// Describes a font @@ -153,7 +150,7 @@ impl<'a> FontDesc<'a> { /// and estimate the overall size of the font pub fn box_size(&self, text: &str) -> FontResult<(u32, u32)> { let ((min_x, min_y), (max_x, max_y)) = self.layout_box(text)?; - let (w, h) = self.get_transform().transform(max_x - min_x, max_y - min_y); + let (w, h) = self.get_transform().transform(max_x - min_x, max_y - min_y)?; Ok((w.unsigned_abs(), h.unsigned_abs())) } diff --git a/plotters/src/style/font/mod.rs b/plotters/src/style/font/mod.rs index 84f09af8..ff0d94f0 100644 --- a/plotters/src/style/font/mod.rs +++ b/plotters/src/style/font/mod.rs @@ -5,6 +5,8 @@ //! to handle all the font issue. //! //! Thus we need different mechanism for the font implementation +pub mod error; +pub use error::FontError; #[cfg(all( not(all(target_arch = "wasm32", not(target_os = "wasi"))), diff --git a/plotters/src/style/font/naive.rs b/plotters/src/style/font/naive.rs index 99530401..a1f590c3 100644 --- a/plotters/src/style/font/naive.rs +++ b/plotters/src/style/font/naive.rs @@ -1,16 +1,5 @@ use super::{FontData, FontFamily, FontStyle, LayoutBox}; -#[derive(Debug, Clone)] -pub struct FontError; - -impl std::fmt::Display for FontError { - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { - write!(fmt, "General Error")?; - Ok(()) - } -} - -impl std::error::Error for FontError {} #[derive(Clone)] pub struct FontDataInternal(String, String); diff --git a/plotters/src/style/font/ttf.rs b/plotters/src/style/font/ttf.rs index 1f7b5037..addc0fa7 100644 --- a/plotters/src/style/font/ttf.rs +++ b/plotters/src/style/font/ttf.rs @@ -7,7 +7,6 @@ use lazy_static::lazy_static; use font_kit::{ canvas::{Canvas, Format, RasterizationOptions}, - error::{FontLoadingError, GlyphLoadingError}, family_name::FamilyName, font::Font, handle::Handle, @@ -21,36 +20,10 @@ use ttf_parser::{Face, GlyphId}; use pathfinder_geometry::transform2d::Transform2F; use pathfinder_geometry::vector::{Vector2F, Vector2I}; -use super::{FontData, FontFamily, FontStyle, LayoutBox}; - +use super::{FontData, FontFamily, FontStyle, LayoutBox, FontError}; type FontResult = Result; -#[derive(Debug, Clone)] -pub enum FontError { - LockError, - NoSuchFont(String, String), - FontLoadError(Arc), - GlyphError(Arc), - FontHandleUnavailable, - FaceParseError(String), -} - -impl std::fmt::Display for FontError { - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { - match self { - FontError::LockError => write!(fmt, "Could not lock mutex"), - FontError::NoSuchFont(family, style) => { - write!(fmt, "No such font: {} {}", family, style) - } - FontError::FontLoadError(e) => write!(fmt, "Font loading error {}", e), - FontError::GlyphError(e) => write!(fmt, "Glyph error {}", e), - FontError::FontHandleUnavailable => write!(fmt, "Font handle is not available"), - FontError::FaceParseError(e) => write!(fmt, "Font face parse error {}", e), - } - } -} -impl std::error::Error for FontError {} lazy_static! { static ref DATA_CACHE: RwLock>> = @@ -79,9 +52,7 @@ impl Drop for FontExt { impl FontExt { fn new(font: Font) -> FontResult { - let handle = font - .handle() - .ok_or(FontError::FontHandleUnavailable)?; + let handle = font.handle().ok_or(FontError::FontHandleUnavailable)?; let face = match handle { Handle::Memory { bytes, font_index } => { let face = ttf_parser::Face::parse(bytes.as_slice(), font_index)