From ab867761914e1b0ccae39de4d3f206ee7f82534c Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Thu, 23 Apr 2026 13:10:45 +0800 Subject: [PATCH 01/25] began updating math for safety concerns --- plotters-svg/src/svg.rs | 5 ++--- .../src/chart/context/cartesian2d/draw_impl.rs | 2 +- plotters/src/chart/series.rs | 2 +- plotters/src/element/text.rs | 2 +- plotters/src/evcxr.rs | 15 ++++++++------- plotters/src/lib.rs | 1 + plotters/src/style/color.rs | 4 ++-- plotters/src/style/font/ab_glyph.rs | 12 ++++++++++++ plotters/src/style/font/font_desc.rs | 6 +++++- plotters/src/style/font/ttf.rs | 2 ++ 10 files changed, 35 insertions(+), 16 deletions(-) diff --git a/plotters-svg/src/svg.rs b/plotters-svg/src/svg.rs index e24623d7..a6de5973 100644 --- a/plotters-svg/src/svg.rs +++ b/plotters-svg/src/svg.rs @@ -602,9 +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, + 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..d38f921a 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 }; 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/element/text.rs b/plotters/src/element/text.rs index ecfc150a..3e480210 100644 --- a/plotters/src/element/text.rs +++ b/plotters/src/element/text.rs @@ -170,7 +170,7 @@ fn test_multi_layout() { let font = FontDesc::new(FontFamily::SansSerif, 20 as f64, FontStyle::Bold); - layout_multiline_text("öäabcde", 40, font, |txt| { + layout_multiline_text("öäabc", 40, font, |txt| { println!("Got: {}", txt); assert!(txt == "öäabc" || txt == "de"); }); diff --git a/plotters/src/evcxr.rs b/plotters/src/evcxr.rs index 9d7b9666..84396601 100644 --- a/plotters/src/evcxr.rs +++ b/plotters/src/evcxr.rs @@ -74,19 +74,20 @@ pub fn evcxr_bitmap_figure< >( size: (u32, u32), draw: Draw, -) -> SVGWrapper { +) -> Result> { const PIXEL_SIZE: usize = 3; - let mut buf = vec![0; (size.0 as usize) * (size.1 as usize) * PIXEL_SIZE]; + let mut buf_len = (size.0 as usize).checked_mul(size.1 as usize).and_then(|n| n.checked_mul(PIXEL_SIZE)).ok_or("image buffer size overflow")?; + + let mut buf = vec![0; buf_len]; let root = BitMapBackend::with_buffer(&mut buf, size).into_drawing_area(); - draw(root).expect("Drawing failure"); - let mut buffer = "".to_string(); + draw(root)?; + let mut buffer = String::new(); { let mut svg_root = SVGBackend::with_string(&mut buffer, size); svg_root - .blit_bitmap((0, 0), size, &buf) - .expect("Failure converting to SVG"); + .blit_bitmap((0, 0), size, &buf)?; } - SVGWrapper(buffer, "".to_string()) + Ok(SVGWrapper(buffer, String::new())) } diff --git a/plotters/src/lib.rs b/plotters/src/lib.rs index b3731333..d7299c43 100644 --- a/plotters/src/lib.rs +++ b/plotters/src/lib.rs @@ -1,4 +1,5 @@ #![warn(missing_docs)] +#![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..5bcdd2b9 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)) diff --git a/plotters/src/style/font/ab_glyph.rs b/plotters/src/style/font/ab_glyph.rs index 42b43344..2c7bc02d 100644 --- a/plotters/src/style/font/ab_glyph.rs +++ b/plotters/src/style/font/ab_glyph.rs @@ -74,6 +74,8 @@ pub enum FontError { Unknown, /// No font data available for the requested family and style. FontUnavailable, + /// Metrics passed are out of a valid and safe range + InvalidMetrics, } impl Display for FontError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -158,3 +160,13 @@ impl FontData for FontDataInternal { Ok(Ok(())) } } + + +/// Small helper to assist with `f32` to `i32` casting safety +fn f32_to_i32_checked(v: f32) -> Result { + if v.is_finite() && v >= i32::MIN as f32 && v <= i32::MAX as f32 { + Ok(v as i32) + } else { + Err(FontError::InvalidMetrics) + } +} \ No newline at end of file diff --git a/plotters/src/style/font/font_desc.rs b/plotters/src/style/font/font_desc.rs index 42f45079..27087f93 100644 --- a/plotters/src/style/font/font_desc.rs +++ b/plotters/src/style/font/font_desc.rs @@ -153,7 +153,11 @@ 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 dx = max_x.checked_sub(min_x).ok_or(FontError::InvalidFontBox)?; + let dy = max_y.checked_sub(min_y).ok_or(FontError::InvalidFontBox)?; + + let (w, h) = self.get_transform().transform(dx, dy); Ok((w.unsigned_abs(), h.unsigned_abs())) } diff --git a/plotters/src/style/font/ttf.rs b/plotters/src/style/font/ttf.rs index 1f7b5037..52bfe2c4 100644 --- a/plotters/src/style/font/ttf.rs +++ b/plotters/src/style/font/ttf.rs @@ -33,6 +33,7 @@ pub enum FontError { GlyphError(Arc), FontHandleUnavailable, FaceParseError(String), + InvalidFontBox, } impl std::fmt::Display for FontError { @@ -46,6 +47,7 @@ impl std::fmt::Display for FontError { 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::InvalidFontBox => write!(fmt, "Font Box is Invalid") } } } From 8fc1b2110bc002784bc89ef603ce2f2e2f781ffa Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Thu, 23 Apr 2026 13:11:01 +0800 Subject: [PATCH 02/25] began updating math for safety concerns --- plotters/src/coord/ranged1d/types/datetime.rs | 2 +- plotters/src/style/font/ab_glyph.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plotters/src/coord/ranged1d/types/datetime.rs b/plotters/src/coord/ranged1d/types/datetime.rs index 27f604ea..e9d58e24 100644 --- a/plotters/src/coord/ranged1d/types/datetime.rs +++ b/plotters/src/coord/ranged1d/types/datetime.rs @@ -249,7 +249,7 @@ where } let week_per_point = ((total_weeks as f64) / (max_points as f64)).ceil() as usize; - + for idx in 0..=(total_weeks as usize / week_per_point) { ret.push(self.0.clone() + Duration::weeks((idx * week_per_point) as i64)); } diff --git a/plotters/src/style/font/ab_glyph.rs b/plotters/src/style/font/ab_glyph.rs index 2c7bc02d..af24bb30 100644 --- a/plotters/src/style/font/ab_glyph.rs +++ b/plotters/src/style/font/ab_glyph.rs @@ -169,4 +169,4 @@ fn f32_to_i32_checked(v: f32) -> Result { } else { Err(FontError::InvalidMetrics) } -} \ No newline at end of file +} From ef39b56c375013fde105540455cf203a75f4101f Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Thu, 23 Apr 2026 17:19:43 +0800 Subject: [PATCH 03/25] created module for doing safe math --- plotters/src/lib.rs | 3 +++ plotters/src/math_guard.rs | 36 +++++++++++++++++++++++++++++ plotters/src/style/font/ab_glyph.rs | 9 -------- 3 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 plotters/src/math_guard.rs diff --git a/plotters/src/lib.rs b/plotters/src/lib.rs index d7299c43..a751f957 100644 --- a/plotters/src/lib.rs +++ b/plotters/src/lib.rs @@ -798,6 +798,9 @@ pub mod element; pub mod series; pub mod style; +// used withing crate for ensuring math done doesn't cause unexpected behavior (overflow/underflow etc) +pub(crate) mod math_guard; + /// Evaluation Context for Rust. See [the evcxr crate](https://crates.io/crates/evcxr) for more information. #[cfg(feature = "evcxr")] #[cfg_attr(doc_cfg, doc(cfg(feature = "evcxr")))] diff --git a/plotters/src/math_guard.rs b/plotters/src/math_guard.rs new file mode 100644 index 00000000..16f52ac3 --- /dev/null +++ b/plotters/src/math_guard.rs @@ -0,0 +1,36 @@ +use std::convert::TryFrom; + +use num_traits::{CheckedAdd, Float, NumCast, PrimInt}; + +fn float_to_integer_checked(v: F, err: E) -> Result +where + F: Float + NumCast, + I: PrimInt + NumCast, +{ + if !v.is_finite() { + return Err(err); + } + + let min: F = NumCast::from(I::min_value()).ok_or(err)?; + let max: F = NumCast::from(I::max_value()).ok_or(err)?; + + if v < min || v > max { + return Err(err); + } + + NumCast::from(v).ok_or(err) +} + +fn add_integer_checked(a: N, b: N, err: E) -> Result +where + N: CheckedAdd, +{ + a.checked_add(&b).ok_or(err) +} + +fn try_convert_checked(v: T, err: E) -> Result +where + U: TryFrom, +{ + U::try_from(v).map_err(|_| err) +} \ No newline at end of file diff --git a/plotters/src/style/font/ab_glyph.rs b/plotters/src/style/font/ab_glyph.rs index af24bb30..7f84b3ab 100644 --- a/plotters/src/style/font/ab_glyph.rs +++ b/plotters/src/style/font/ab_glyph.rs @@ -161,12 +161,3 @@ impl FontData for FontDataInternal { } } - -/// Small helper to assist with `f32` to `i32` casting safety -fn f32_to_i32_checked(v: f32) -> Result { - if v.is_finite() && v >= i32::MIN as f32 && v <= i32::MAX as f32 { - Ok(v as i32) - } else { - Err(FontError::InvalidMetrics) - } -} From 716e644bbc32fd3a1187469a32f1357b7bf9e4ba Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Thu, 23 Apr 2026 21:51:28 +0800 Subject: [PATCH 04/25] working on ab_glyph draw method --- plotters/src/evcxr.rs | 2 +- plotters/src/math_guard.rs | 14 +++++++++++--- plotters/src/style/font/ab_glyph.rs | 6 +++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/plotters/src/evcxr.rs b/plotters/src/evcxr.rs index 84396601..aa42c801 100644 --- a/plotters/src/evcxr.rs +++ b/plotters/src/evcxr.rs @@ -77,7 +77,7 @@ pub fn evcxr_bitmap_figure< ) -> Result> { const PIXEL_SIZE: usize = 3; - let mut buf_len = (size.0 as usize).checked_mul(size.1 as usize).and_then(|n| n.checked_mul(PIXEL_SIZE)).ok_or("image buffer size overflow")?; + let buf_len = (size.0 as usize).checked_mul(size.1 as usize).and_then(|n| n.checked_mul(PIXEL_SIZE)).ok_or("image buffer size overflow")?; let mut buf = vec![0; buf_len]; diff --git a/plotters/src/math_guard.rs b/plotters/src/math_guard.rs index 16f52ac3..715301db 100644 --- a/plotters/src/math_guard.rs +++ b/plotters/src/math_guard.rs @@ -2,7 +2,7 @@ use std::convert::TryFrom; use num_traits::{CheckedAdd, Float, NumCast, PrimInt}; -fn float_to_integer_checked(v: F, err: E) -> Result +pub(crate) fn float_to_integer_checked(v: F, err: E) -> Result where F: Float + NumCast, I: PrimInt + NumCast, @@ -21,16 +21,24 @@ where NumCast::from(v).ok_or(err) } -fn add_integer_checked(a: N, b: N, err: E) -> Result +pub(crate) fn add_integer_checked(a: N, b: N, err: E) -> Result where N: CheckedAdd, { a.checked_add(&b).ok_or(err) } -fn try_convert_checked(v: T, err: E) -> Result +pub(crate) fn try_convert_checked(v: T, err: E) -> Result where U: TryFrom, { U::try_from(v).map_err(|_| err) +} + +pub(crate) fn try_convert_float(v: FB, err: E) -> Result where FB: Float + NumCast, FS: Float + NumCast { + if !v.is_finite() { + return Err(err); + } + let out: FS = NumCast::from(v).ok_or(err)?; + Ok(out) } \ No newline at end of file diff --git a/plotters/src/style/font/ab_glyph.rs b/plotters/src/style/font/ab_glyph.rs index 7f84b3ab..4e88403e 100644 --- a/plotters/src/style/font/ab_glyph.rs +++ b/plotters/src/style/font/ab_glyph.rs @@ -6,6 +6,8 @@ use std::collections::HashMap; use std::error::Error; use std::sync::RwLock; +use crate::math_guard::try_convert_float; + struct FontMap { map: HashMap>, } @@ -128,7 +130,9 @@ impl FontData for FontDataInternal { text: &str, mut draw: DrawFunc, ) -> Result, Self::ErrorType> { - let font = self.font_ref.as_scaled(size as f32); + let size = try_convert_float::(size, FontError::InvalidMetrics)?; + todo!(); + let font = self.font_ref.as_scaled(size); let mut draw = |x: i32, y: i32, c| { let (base_x, base_y) = pos; draw(base_x + x, base_y + y, c) From 4d1eb138a861e6a3ed7585d7782d258e638d44f6 Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Fri, 24 Apr 2026 13:08:53 +0800 Subject: [PATCH 05/25] Implemented unified `Ranged1DError` for refactoring for errors in ranged1d --- clippy.txt | 114 ++++++++++++++++++ .../src/coord/ranged1d/combinators/ckps.rs | 10 +- .../coord/ranged1d/combinators/group_by.rs | 3 +- .../coord/ranged1d/combinators/linspace.rs | 4 +- .../coord/ranged1d/combinators/logarithmic.rs | 15 ++- .../src/coord/ranged1d/combinators/nested.rs | 6 +- .../ranged1d/combinators/partial_axis.rs | 5 +- plotters/src/coord/ranged1d/discrete.rs | 7 +- plotters/src/coord/ranged1d/errors.rs | 25 ++++ plotters/src/coord/ranged1d/mod.rs | 10 +- plotters/src/coord/ranged1d/types/datetime.rs | 34 +++--- plotters/src/coord/ranged1d/types/numeric.rs | 25 ++-- plotters/src/coord/ranged1d/types/slice.rs | 6 +- plotters/src/math_guard.rs | 17 +-- plotters/src/style/font/ab_glyph.rs | 24 ++-- 15 files changed, 232 insertions(+), 73 deletions(-) create mode 100644 clippy.txt create mode 100644 plotters/src/coord/ranged1d/errors.rs diff --git a/clippy.txt b/clippy.txt new file mode 100644 index 00000000..bcfef20f --- /dev/null +++ b/clippy.txt @@ -0,0 +1,114 @@ +warning: /home/alex/Projects/plotters/plotters/Cargo.toml: unused manifest key: package.msrv + Checking plotters v0.3.7 (/home/alex/Projects/plotters/plotters) +error: unused import: `core::fmt` + --> plotters/src/coord/ranged1d/types/datetime.rs:3:5 + | +3 | use core::fmt; + | ^^^^^^^^^ + | + = note: `-D unused-imports` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(unused_imports)]` + +error: unused import: `fmt::Display` + --> plotters/src/coord/ranged1d/types/datetime.rs:4:11 + | +4 | use std::{fmt::Display, ops::{Add, Range, Sub}}; + | ^^^^^^^^^^^^ + +error[E0277]: `coord::ranged1d::types::datetime::TimeError` doesn't implement `std::fmt::Debug` + --> plotters/src/coord/ranged1d/types/datetime.rs:30:28 + | +30 | impl std::error::Error for TimeError {} + | ^^^^^^^^^ the trait `std::fmt::Debug` is not implemented for `coord::ranged1d::types::datetime::TimeError` + | + = note: add `#[derive(Debug)]` to `coord::ranged1d::types::datetime::TimeError` or manually `impl std::fmt::Debug for coord::ranged1d::types::datetime::TimeError` +note: required by a bound in `std::error::Error` + --> /home/alex/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/error.rs:59:18 + | +59 | pub trait Error: Debug + Display { + | ^^^^^ required by this bound in `Error` +help: consider annotating `coord::ranged1d::types::datetime::TimeError` with `#[derive(Debug)]` + | +12 + #[derive(Debug)] +13 | enum TimeError { + | + +error[E0308]: mismatched types + --> plotters/src/coord/ranged1d/types/datetime.rs:72:9 + | +53 | fn map_coord(value: &Self, begin: &Self, end: &Self, limit: (i32, i32)) -> Result{ + | ---------------------- expected `std::result::Result` because of return type +... +72 | (f64::from(limit.1 - limit.0) * value_days / total_days) as i32 + limit.0 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Result`, found `i32` + | + = note: expected enum `std::result::Result` + found type `i32` +help: try wrapping the expression in `Ok` + | +72 | Ok((f64::from(limit.1 - limit.0) * value_days / total_days) as i32 + limit.0) + | +++ + + +error[E0308]: mismatched types + --> plotters/src/coord/ranged1d/types/datetime.rs:244:9 + | +243 | fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { + | --- expected `i32` because of return type +244 | TimeValue::map_coord(value, &self.0, &self.1, limit) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `i32`, found `Result` + | + = note: expected type `i32` + found enum `std::result::Result` +help: consider using `Result::expect` to unwrap the `std::result::Result` value, panicking if the value is a `Result::Err` + | +244 | TimeValue::map_coord(value, &self.0, &self.1, limit).expect("REASON") + | +++++++++++++++++ + +error[E0308]: mismatched types + --> plotters/src/coord/ranged1d/types/datetime.rs:434:9 + | +433 | fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { + | --- expected `i32` because of return type +434 | T::map_coord(value, &self.0.start, &self.0.end, limit) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `i32`, found `Result` + | + = note: expected type `i32` + found enum `std::result::Result` +help: consider using `Result::expect` to unwrap the `std::result::Result` value, panicking if the value is a `Result::Err` + | +434 | T::map_coord(value, &self.0.start, &self.0.end, limit).expect("REASON") + | +++++++++++++++++ + +error[E0308]: mismatched types + --> plotters/src/coord/ranged1d/types/datetime.rs:561:9 + | +560 | fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { + | --- expected `i32` because of return type +561 | T::map_coord(value, &self.0.start, &self.0.end, limit) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `i32`, found `Result` + | + = note: expected type `i32` + found enum `std::result::Result` +help: consider using `Result::expect` to unwrap the `std::result::Result` value, panicking if the value is a `Result::Err` + | +561 | T::map_coord(value, &self.0.start, &self.0.end, limit).expect("REASON") + | +++++++++++++++++ + +error[E0308]: mismatched types + --> plotters/src/coord/ranged1d/types/datetime.rs:688:9 + | +687 | fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { + | --- expected `i32` because of return type +688 | TimeValue::map_coord(value, &self.0, &self.1, limit) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `i32`, found `Result` + | + = note: expected type `i32` + found enum `std::result::Result` +help: consider using `Result::expect` to unwrap the `std::result::Result` value, panicking if the value is a `Result::Err` + | +688 | TimeValue::map_coord(value, &self.0, &self.1, limit).expect("REASON") + | +++++++++++++++++ + +Some errors have detailed explanations: E0277, E0308. +For more information about an error, try `rustc --explain E0277`. +error: could not compile `plotters` (lib) due to 8 previous errors diff --git a/plotters/src/coord/ranged1d/combinators/ckps.rs b/plotters/src/coord/ranged1d/combinators/ckps.rs index cdaaf922..0935e676 100644 --- a/plotters/src/coord/ranged1d/combinators/ckps.rs +++ b/plotters/src/coord/ranged1d/combinators/ckps.rs @@ -55,12 +55,13 @@ where { type ValueType = R::ValueType; type FormatOption = R::FormatOption; + type ErrorType = R::ErrorType; fn range(&self) -> Range { self.inner.range() } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { self.inner.map(value, limit) } @@ -180,12 +181,13 @@ impl WithKeyPointMethod { impl Ranged for WithKeyPointMethod { type ValueType = R::ValueType; type FormatOption = R::FormatOption; + type ErrorType = R::ErrorType; fn range(&self) -> Range { self.inner.range() } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { self.inner.map(value, limit) } @@ -221,7 +223,7 @@ mod test { #[test] fn test_with_key_points() { let range = (0..100).with_key_points(vec![1, 2, 3]); - assert_eq!(range.map(&3, (0, 1000)), 30); + assert_eq!(range.map(&3, (0, 1000))?, 30); assert_eq!(range.range(), 0..100); assert_eq!(range.key_points(BoldPoints(100)), vec![1, 2, 3]); assert_eq!(range.key_points(LightPoints::new(100, 100)), vec![]); @@ -249,7 +251,7 @@ mod test { #[test] fn test_with_key_point_method() { let range = (0..100).with_key_point_func(|_| vec![1, 2, 3]); - assert_eq!(range.map(&3, (0, 1000)), 30); + assert_eq!(range.map(&3, (0, 1000))?, 30); assert_eq!(range.range(), 0..100); assert_eq!(range.key_points(BoldPoints(100)), vec![1, 2, 3]); assert_eq!(range.key_points(LightPoints::new(100, 100)), vec![]); diff --git a/plotters/src/coord/ranged1d/combinators/group_by.rs b/plotters/src/coord/ranged1d/combinators/group_by.rs index 875b02d4..3e6959af 100644 --- a/plotters/src/coord/ranged1d/combinators/group_by.rs +++ b/plotters/src/coord/ranged1d/combinators/group_by.rs @@ -64,7 +64,8 @@ impl + ValueFormatter> ValueFormatter impl Ranged for GroupBy { type FormatOption = NoDefaultFormatting; type ValueType = T::ValueType; - fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> i32 { + type ErrorType = T::ErrorType; + fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> Result { self.0.map(value, limit) } fn range(&self) -> Range { diff --git a/plotters/src/coord/ranged1d/combinators/linspace.rs b/plotters/src/coord/ranged1d/combinators/linspace.rs index 14b5ebaa..097d3a27 100644 --- a/plotters/src/coord/ranged1d/combinators/linspace.rs +++ b/plotters/src/coord/ranged1d/combinators/linspace.rs @@ -278,12 +278,12 @@ where { type FormatOption = NoDefaultFormatting; type ValueType = T::ValueType; - + type ErrorType = T::ErrorType; fn range(&self) -> Range { self.inner.range() } - fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> i32 { + fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> Result { self.inner.map(value, limit) } diff --git a/plotters/src/coord/ranged1d/combinators/logarithmic.rs b/plotters/src/coord/ranged1d/combinators/logarithmic.rs index 56f54f05..cb804486 100644 --- a/plotters/src/coord/ranged1d/combinators/logarithmic.rs +++ b/plotters/src/coord/ranged1d/combinators/logarithmic.rs @@ -4,6 +4,7 @@ use crate::coord::ranged1d::{ }; use std::marker::PhantomData; use std::ops::Range; +use crate::coord::ranged1d::errors::Ranged1DError; /// The trait for the type that is able to be presented in the log scale. /// This trait is primarily used by [LogRangeExt](struct.LogRangeExt.html). @@ -81,14 +82,12 @@ impl IntoLogRange for Range { } impl ReversibleRanged for LogCoord { - fn unmap(&self, input: i32, limit: (i32, i32)) -> Option { - self.linear.unmap(input, limit).map(|value_ln| { - let fv = value_ln.exp(); - self.f64_to_value(fv) - }) + fn unmap(&self, input: i32, limit: (i32, i32)) -> Result, Self::ErrorType> { + self.linear + .unmap(input, limit) + .map(|value_ln| value_ln.map(|value_ln| self.f64_to_value(value_ln.exp()))) } } - /// The logarithmic coordinate decorator. /// This decorator is used to make the axis rendered as logarithmically. #[derive(Clone)] @@ -200,8 +199,8 @@ impl LogCoord { impl Ranged for LogCoord { type FormatOption = DefaultFormatting; type ValueType = V; - - fn map(&self, value: &V, limit: (i32, i32)) -> i32 { + type ErrorType = Ranged1DError; + fn map(&self, value: &V, limit: (i32, i32)) -> Result { let fv = self.value_to_f64(value); let value_ln = fv.ln(); self.linear.map(&value_ln, limit) diff --git a/plotters/src/coord/ranged1d/combinators/nested.rs b/plotters/src/coord/ranged1d/combinators/nested.rs index 379f2d4f..e67700fe 100644 --- a/plotters/src/coord/ranged1d/combinators/nested.rs +++ b/plotters/src/coord/ranged1d/combinators/nested.rs @@ -1,5 +1,5 @@ use crate::coord::ranged1d::{ - AsRangedCoord, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, ValueFormatter, + AsRangedCoord, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, Ranged1DError, ValueFormatter }; use std::ops::Range; @@ -69,7 +69,7 @@ where impl Ranged for NestedRange { type FormatOption = NoDefaultFormatting; type ValueType = NestedValue; - + type ErrorType = Ranged1DError; fn range(&self) -> Range { let primary_range = self.primary.range(); @@ -80,7 +80,7 @@ impl Ranged for NestedRange { ..NestedValue::Value(primary_range.end, secondary_right) } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { let idx = self.primary.index_of(value.category()).unwrap_or(0); let total = self.primary.size(); diff --git a/plotters/src/coord/ranged1d/combinators/partial_axis.rs b/plotters/src/coord/ranged1d/combinators/partial_axis.rs index b778ee2c..c0a53289 100644 --- a/plotters/src/coord/ranged1d/combinators/partial_axis.rs +++ b/plotters/src/coord/ranged1d/combinators/partial_axis.rs @@ -1,5 +1,5 @@ use crate::coord::ranged1d::{ - AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, Ranged, + AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, Ranged, Ranged1DError, }; use std::ops::Range; @@ -31,8 +31,9 @@ where { type FormatOption = DefaultFormatting; type ValueType = R::ValueType; + type ErrorType = Ranged1DError; - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { self.0.map(value, limit) } diff --git a/plotters/src/coord/ranged1d/discrete.rs b/plotters/src/coord/ranged1d/discrete.rs index 8b5da4b6..82200199 100644 --- a/plotters/src/coord/ranged1d/discrete.rs +++ b/plotters/src/coord/ranged1d/discrete.rs @@ -1,5 +1,5 @@ use crate::coord::ranged1d::{ - AsRangedCoord, KeyPointHint, NoDefaultFormatting, Ranged, ReversibleRanged, ValueFormatter, + AsRangedCoord, KeyPointHint, NoDefaultFormatting, Ranged, Ranged1DError, ReversibleRanged, ValueFormatter }; use std::ops::Range; @@ -139,8 +139,9 @@ where impl Ranged for SegmentedCoord { type FormatOption = NoDefaultFormatting; type ValueType = SegmentValue; + type ErrorType = Ranged1DError; - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { let margin = ((limit.1 - limit.0) as f32 / self.0.size() as f32).round() as i32; match value { @@ -205,7 +206,7 @@ impl From for SegmentValue { } impl ReversibleRanged for DC { - fn unmap(&self, input: i32, limit: (i32, i32)) -> Option { + fn unmap(&self, input: i32, limit: (i32, i32)) -> Result, Self::ErrorType> { let idx = (f64::from(input - limit.0) * (self.size() as f64) / f64::from(limit.1 - limit.0)) .floor() as usize; self.from_index(idx) diff --git a/plotters/src/coord/ranged1d/errors.rs b/plotters/src/coord/ranged1d/errors.rs new file mode 100644 index 00000000..4b775fcf --- /dev/null +++ b/plotters/src/coord/ranged1d/errors.rs @@ -0,0 +1,25 @@ + +use core::fmt; + +#[derive(Debug)] +pub enum Ranged1DError { + PixelRangeOverflow, + ZeroSpan, + NonFiniteProjection, + CoordOutOfRange, +} + +impl fmt::Display for Ranged1DError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Ranged1DError::PixelRangeOverflow => write!(f, "pixel range overflow"), + Ranged1DError::ZeroSpan => write!(f, "time span is zero"), + Ranged1DError::NonFiniteProjection => { + write!(f, "projection produced a non-finite value") + } + Ranged1DError::CoordOutOfRange => write!(f, "projected coordinate is out of range"), + } + } +} + +impl std::error::Error for Ranged1DError {} \ No newline at end of file diff --git a/plotters/src/coord/ranged1d/mod.rs b/plotters/src/coord/ranged1d/mod.rs index 1b2072c2..04963075 100644 --- a/plotters/src/coord/ranged1d/mod.rs +++ b/plotters/src/coord/ranged1d/mod.rs @@ -56,6 +56,9 @@ use std::ops::Range; pub(super) mod combinators; pub(super) mod types; +pub(crate) mod errors; +pub use errors::Ranged1DError; + mod discrete; pub use discrete::{DiscreteRanged, IntoSegmentedCoord, SegmentValue, SegmentedCoord}; @@ -197,9 +200,12 @@ pub trait Ranged { /// The type of this value in this range specification type ValueType; + + /// The error type to return in a result. + type ErrorType; /// This function maps the value to i32, which is the drawing coordinate - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32; + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result; /// This function gives the key points that we can draw a grid based on this fn key_points(&self, hint: Hint) -> Vec; @@ -223,7 +229,7 @@ pub trait Ranged { /// logic value. pub trait ReversibleRanged: Ranged { /// Perform the reverse mapping - fn unmap(&self, input: i32, limit: (i32, i32)) -> Option; + fn unmap(&self, input: i32, limit: (i32, i32)) -> Result, Self::ErrorType>; } /// The trait for the type that can be converted into a ranged coordinate axis diff --git a/plotters/src/coord/ranged1d/types/datetime.rs b/plotters/src/coord/ranged1d/types/datetime.rs index e9d58e24..1187096e 100644 --- a/plotters/src/coord/ranged1d/types/datetime.rs +++ b/plotters/src/coord/ranged1d/types/datetime.rs @@ -1,10 +1,9 @@ /// The datetime coordinates use chrono::{Date, DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, TimeZone, Timelike}; -use std::ops::{Add, Range, Sub}; +use std::{ops::{Add, Range, Sub}}; use crate::coord::ranged1d::{ - AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, - ReversibleRanged, ValueFormatter, + AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, Ranged1DError, ReversibleRanged, ValueFormatter }; /// The trait that describe some time value. This is the uniformed abstraction that works @@ -28,15 +27,16 @@ pub trait TimeValue: Eq + Sized { fn from_date(date: Self::DateType) -> Self; /// Map the coord spec - fn map_coord(value: &Self, begin: &Self, end: &Self, limit: (i32, i32)) -> i32 { + fn map_coord(value: &Self, begin: &Self, end: &Self, limit: (i32, i32)) -> Result{ let total_span = end.subtract(begin); let value_span = value.subtract(begin); // First, lets try the nanoseconds precision if let Some(total_ns) = total_span.num_nanoseconds() { if let Some(value_ns) = value_span.num_nanoseconds() { - return (f64::from(limit.1 - limit.0) * value_ns as f64 / total_ns as f64) as i32 - + limit.0; + let limit_sub = limit.1.checked_sub(limit.0).ok_or(TimeError::CoordOutOfRange)?; + return Ok((f64::from(limit_sub) * value_ns as f64 / total_ns as f64) as i32 + + limit.0); } } @@ -46,7 +46,7 @@ pub trait TimeValue: Eq + Sized { let total_days = total_span.num_days() as f64; let value_days = value_span.num_days() as f64; - (f64::from(limit.1 - limit.0) * value_days / total_days) as i32 + limit.0 + Ok((f64::from(limit.1 - limit.0) * value_days / total_days) as i32 + limit.0) } /// Map pixel to coord spec @@ -212,12 +212,13 @@ where { type FormatOption = DefaultFormatting; type ValueType = D; + type ErrorType = Ranged1DError; fn range(&self) -> Range { self.0.clone()..self.1.clone() } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { TimeValue::map_coord(value, &self.0, &self.1, limit) } @@ -402,12 +403,13 @@ where { type FormatOption = NoDefaultFormatting; type ValueType = T; + type ErrorType = Ranged1DError; fn range(&self) -> Range { self.0.start.clone()..self.0.end.clone() } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { T::map_coord(value, &self.0.start, &self.0.end, limit) } @@ -529,12 +531,13 @@ where { type FormatOption = NoDefaultFormatting; type ValueType = T; + type ErrorType = Ranged1DError; fn range(&self) -> Range { self.0.start.clone()..self.0.end.clone() } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { T::map_coord(value, &self.0.start, &self.0.end, limit) } @@ -656,12 +659,13 @@ where { type FormatOption = DefaultFormatting; type ValueType = DT; + type ErrorType = Ranged1DError; fn range(&self) -> Range
{ self.0.clone()..self.1.clone() } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { TimeValue::map_coord(value, &self.0, &self.1, limit) } @@ -713,8 +717,9 @@ where RangedDate: Ranged, { /// Perform the reverse mapping - fn unmap(&self, input: i32, limit: (i32, i32)) -> Option { - Some(TimeValue::unmap_coord(input, &self.0, &self.1, limit)) + fn unmap(&self, input: i32, limit: (i32, i32)) -> Result, Self::ErrorType> { + let value = Some(TimeValue::unmap_coord(input, &self.0, &self.1, limit)); + Ok(value) } } @@ -736,12 +741,13 @@ impl From> for RangedDuration { impl Ranged for RangedDuration { type FormatOption = DefaultFormatting; type ValueType = Duration; + type ErrorType = Ranged1DError; fn range(&self) -> Range { self.0..self.1 } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { let total_span = self.1 - self.0; let value_span = *value - self.0; diff --git a/plotters/src/coord/ranged1d/types/numeric.rs b/plotters/src/coord/ranged1d/types/numeric.rs index 6b40d4c5..ef5c78f5 100644 --- a/plotters/src/coord/ranged1d/types/numeric.rs +++ b/plotters/src/coord/ranged1d/types/numeric.rs @@ -5,7 +5,7 @@ use crate::coord::{ combinators::WithKeyPoints, ranged1d::{ AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, - Ranged, ReversibleRanged, ValueFormatter, + Ranged, ReversibleRanged, ValueFormatter, errors::Ranged1DError }, }; @@ -48,7 +48,7 @@ macro_rules! impl_ranged_type_trait { macro_rules! impl_reverse_mapping_trait { ($type:ty, $name: ident) => { impl ReversibleRanged for $name { - fn unmap(&self, p: i32, (min, max): (i32, i32)) -> Option<$type> { + fn unmap(&self, p: i32, (min, max): (i32, i32)) -> Result, Ranged1DError> { if p < min.min(max) || p > max.max(min) || min == max { return None; } @@ -61,7 +61,7 @@ macro_rules! impl_reverse_mapping_trait { }; } macro_rules! make_numeric_coord { - ($type:ty, $name:ident, $key_points:ident, $doc: expr, $fmt: ident) => { + ($type:ty, $error:ty, $name:ident, $key_points:ident, $doc: expr, $fmt: ident) => { #[doc = $doc] #[derive(Clone)] pub struct $name($type, $type); @@ -73,8 +73,9 @@ macro_rules! make_numeric_coord { impl Ranged for $name { type FormatOption = $fmt; type ValueType = $type; + type ErrorType = $error; #[allow(clippy::float_cmp)] - fn map(&self, v: &$type, limit: (i32, i32)) -> i32 { + fn map(&self, v: &$type, limit: (i32, i32)) -> Result { // Corner case: If we have a range that have only one value, // then we just assign everything to the only point if self.1 == self.0 { @@ -111,8 +112,8 @@ macro_rules! make_numeric_coord { } } }; - ($type:ty, $name:ident, $key_points:ident, $doc: expr) => { - make_numeric_coord!($type, $name, $key_points, $doc, DefaultFormatting); + ($type:ty, $error:ty, $name:ident, $key_points:ident, $doc: expr) => { + make_numeric_coord!($type, $error, $name, $key_points, $doc, DefaultFormatting); }; } @@ -254,6 +255,7 @@ gen_key_points_comp!(integer, compute_usize_key_points, usize); make_numeric_coord!( f32, + Ranged1DError, RangedCoordf32, compute_f32_key_points, "The ranged coordinate for type f32", @@ -283,6 +285,7 @@ impl ValueFormatter for WithKeyPoints { make_numeric_coord!( f64, + Ranged1DError, RangedCoordf64, compute_f64_key_points, "The ranged coordinate for type f64", @@ -311,48 +314,56 @@ impl ValueFormatter for WithKeyPoints { } make_numeric_coord!( u32, + Ranged1DError, RangedCoordu32, compute_u32_key_points, "The ranged coordinate for type u32" ); make_numeric_coord!( i32, + Ranged1DError, RangedCoordi32, compute_i32_key_points, "The ranged coordinate for type i32" ); make_numeric_coord!( u64, + Ranged1DError, RangedCoordu64, compute_u64_key_points, "The ranged coordinate for type u64" ); make_numeric_coord!( i64, + Ranged1DError, RangedCoordi64, compute_i64_key_points, "The ranged coordinate for type i64" ); make_numeric_coord!( u128, + Ranged1DError, RangedCoordu128, compute_u128_key_points, "The ranged coordinate for type u128" ); make_numeric_coord!( i128, + Ranged1DError, RangedCoordi128, compute_i128_key_points, "The ranged coordinate for type i128" ); make_numeric_coord!( usize, + Ranged1DError, RangedCoordusize, compute_usize_key_points, "The ranged coordinate for type usize" ); make_numeric_coord!( isize, + Ranged1DError, RangedCoordisize, compute_isize_key_points, "The ranged coordinate for type isize" @@ -421,7 +432,7 @@ mod test { fn test_coord_unmap() { let coord: RangedCoordu32 = (0..20).into(); let pos = coord.map(&5, (1000, 2000)); - let value = coord.unmap(pos, (1000, 2000)); + let value = coord.unmap(pos?, (1000, 2000)); assert_eq!(value, Some(5)); } diff --git a/plotters/src/coord/ranged1d/types/slice.rs b/plotters/src/coord/ranged1d/types/slice.rs index 13be3d7f..e4bc5bba 100644 --- a/plotters/src/coord/ranged1d/types/slice.rs +++ b/plotters/src/coord/ranged1d/types/slice.rs @@ -1,5 +1,5 @@ use crate::coord::ranged1d::{ - AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, Ranged, + AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, Ranged, Ranged1DError, }; use std::ops::Range; @@ -12,13 +12,13 @@ pub struct RangedSlice<'a, T: PartialEq>(&'a [T]); impl<'a, T: PartialEq> Ranged for RangedSlice<'a, T> { type FormatOption = DefaultFormatting; type ValueType = &'a T; - + type ErrorType = Ranged1DError; fn range(&self) -> Range<&'a T> { // If inner slice is empty, we should always panic &self.0[0]..&self.0[self.0.len() - 1] } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { match self.0.iter().position(|x| &x == value) { Some(pos) => { let pixel_span = limit.1 - limit.0; diff --git a/plotters/src/math_guard.rs b/plotters/src/math_guard.rs index 715301db..3223c716 100644 --- a/plotters/src/math_guard.rs +++ b/plotters/src/math_guard.rs @@ -1,6 +1,4 @@ -use std::convert::TryFrom; - -use num_traits::{CheckedAdd, Float, NumCast, PrimInt}; +use num_traits::{Float, NumCast, PrimInt}; pub(crate) fn float_to_integer_checked(v: F, err: E) -> Result where @@ -21,19 +19,6 @@ where NumCast::from(v).ok_or(err) } -pub(crate) fn add_integer_checked(a: N, b: N, err: E) -> Result -where - N: CheckedAdd, -{ - a.checked_add(&b).ok_or(err) -} - -pub(crate) fn try_convert_checked(v: T, err: E) -> Result -where - U: TryFrom, -{ - U::try_from(v).map_err(|_| err) -} pub(crate) fn try_convert_float(v: FB, err: E) -> Result where FB: Float + NumCast, FS: Float + NumCast { if !v.is_finite() { diff --git a/plotters/src/style/font/ab_glyph.rs b/plotters/src/style/font/ab_glyph.rs index 4e88403e..28fc1ac2 100644 --- a/plotters/src/style/font/ab_glyph.rs +++ b/plotters/src/style/font/ab_glyph.rs @@ -2,11 +2,11 @@ use super::{FontData, FontFamily, FontStyle, LayoutBox}; use ab_glyph::{Font, FontRef, ScaleFont}; use core::fmt::{self, Display}; use once_cell::sync::Lazy; -use std::collections::HashMap; +use std::{collections::HashMap, convert::TryFrom}; use std::error::Error; use std::sync::RwLock; -use crate::math_guard::try_convert_float; +use crate::math_guard::{try_convert_float, float_to_integer_checked}; struct FontMap { map: HashMap>, @@ -70,7 +70,7 @@ pub struct FontDataInternal { font_ref: FontRef<'static>, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub enum FontError { /// No idea what the problem is Unknown, @@ -130,12 +130,15 @@ impl FontData for FontDataInternal { text: &str, mut draw: DrawFunc, ) -> Result, Self::ErrorType> { - let size = try_convert_float::(size, FontError::InvalidMetrics)?; - todo!(); + let metric_error = FontError::InvalidMetrics; + let size = try_convert_float::(size, metric_error)?; let font = self.font_ref.as_scaled(size); + let mut draw = |x: i32, y: i32, c| { let (base_x, base_y) = pos; - draw(base_x + x, base_y + y, c) + let add_x = x.checked_add(base_x).ok_or(metric_error)?; + let add_y = y.checked_add(base_y).ok_or(metric_error)?; + draw(add_x, add_y, c).map_err(|_| FontError::Unknown) }; let mut x_shift = 0f32; let mut prev = None; @@ -147,12 +150,17 @@ impl FontData for FontDataInternal { let glyph = font.scaled_glyph(c); if let Some(q) = font.outline_glyph(glyph) { let rect = q.px_bounds(); - let y_shift = ((size as f32) / 2.0 + rect.min.y) as i32; + let y_div = size / 2.0 + rect.min.y; + let y_shift = float_to_integer_checked::(y_div, metric_error)?; let x_shift = x_shift as i32; let mut buf = vec![]; q.draw(|x, y, c| buf.push((x, y, c))); for (x, y, c) in buf { - draw(x as i32 + x_shift, y as i32 + y_shift, c).map_err(|_e| { + let x = i32::try_from(x).map_err(|_| metric_error)?; + let x_val = x.checked_add(x_shift).ok_or( metric_error)?; + let y= i32::try_from(y).map_err(|_| metric_error)?; + let y_val = y.checked_add(y_shift).ok_or(metric_error)?; + draw(x_val, y_val, c).map_err(|_e| { // Note: If ever `plotters` adds a tracing or logging crate, // this would be a good place to use it. FontError::Unknown From ac5b8ac6fc57fed2b77e214a383cdcd7bd12cd25 Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Fri, 24 Apr 2026 22:48:02 +0800 Subject: [PATCH 06/25] Continued method updates --- plotters/src/coord/ranged1d/combinators/nested.rs | 8 +++++--- .../src/coord/ranged1d/combinators/partial_axis.rs | 12 ++++++------ plotters/src/coord/ranged1d/errors.rs | 8 ++++---- plotters/src/coord/ranged1d/mod.rs | 6 +++--- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/plotters/src/coord/ranged1d/combinators/nested.rs b/plotters/src/coord/ranged1d/combinators/nested.rs index e67700fe..c5a1fb8b 100644 --- a/plotters/src/coord/ranged1d/combinators/nested.rs +++ b/plotters/src/coord/ranged1d/combinators/nested.rs @@ -66,7 +66,7 @@ where } } -impl Ranged for NestedRange { +impl> Ranged for NestedRange { type FormatOption = NoDefaultFormatting; type ValueType = NestedValue; type ErrorType = Ranged1DError; @@ -97,7 +97,9 @@ impl Ranged for NestedRange { if let Some(secondary_value) = value.nested_value() { self.secondary[idx].map(secondary_value, (s_left, s_right)) } else { - (s_left + s_right) / 2 + let sum = s_left.checked_add(s_right).ok_or(Ranged1DError::RangeOverflow)?; + let result = sum.checked_div(2).ok_or(Ranged1DError::RangeUnderFlow)?; + Ok(result) } } @@ -129,7 +131,7 @@ impl Ranged for NestedRange { } } -impl DiscreteRanged for NestedRange { +impl> DiscreteRanged for NestedRange { fn size(&self) -> usize { self.secondary.iter().map(|x| x.size()).sum::() } diff --git a/plotters/src/coord/ranged1d/combinators/partial_axis.rs b/plotters/src/coord/ranged1d/combinators/partial_axis.rs index c0a53289..2a985fba 100644 --- a/plotters/src/coord/ranged1d/combinators/partial_axis.rs +++ b/plotters/src/coord/ranged1d/combinators/partial_axis.rs @@ -25,7 +25,7 @@ pub trait IntoPartialAxis: AsRangedCoord { impl IntoPartialAxis for R {} -impl Ranged for PartialAxis +impl> Ranged for PartialAxis where R::ValueType: Clone, { @@ -45,17 +45,17 @@ where self.0.range() } - fn axis_pixel_range(&self, limit: (i32, i32)) -> Range { - let left = self.map(&self.1.start, limit); - let right = self.map(&self.1.end, limit); + fn axis_pixel_range(&self, limit: (i32, i32)) -> Result, Self::ErrorType> { + let left = self.map(&self.1.start, limit)?; + let right = self.map(&self.1.end, limit)?; - left.min(right)..left.max(right) + Ok(left.min(right)..left.max(right)) } } impl DiscreteRanged for PartialAxis where - R: Ranged, + R: Ranged, ::ValueType: Eq + Clone, { fn size(&self) -> usize { diff --git a/plotters/src/coord/ranged1d/errors.rs b/plotters/src/coord/ranged1d/errors.rs index 4b775fcf..c992ec1b 100644 --- a/plotters/src/coord/ranged1d/errors.rs +++ b/plotters/src/coord/ranged1d/errors.rs @@ -3,8 +3,8 @@ use core::fmt; #[derive(Debug)] pub enum Ranged1DError { - PixelRangeOverflow, - ZeroSpan, + RangeOverflow, + RangeUnderFlow, NonFiniteProjection, CoordOutOfRange, } @@ -12,8 +12,8 @@ pub enum Ranged1DError { impl fmt::Display for Ranged1DError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Ranged1DError::PixelRangeOverflow => write!(f, "pixel range overflow"), - Ranged1DError::ZeroSpan => write!(f, "time span is zero"), + Ranged1DError::RangeOverflow => write!(f, "value range overflow"), + Ranged1DError::RangeUnderFlow => write!(f, "value range underflow"), Ranged1DError::NonFiniteProjection => { write!(f, "projection produced a non-finite value") } diff --git a/plotters/src/coord/ranged1d/mod.rs b/plotters/src/coord/ranged1d/mod.rs index 04963075..9e75c385 100644 --- a/plotters/src/coord/ranged1d/mod.rs +++ b/plotters/src/coord/ranged1d/mod.rs @@ -215,11 +215,11 @@ pub trait Ranged { /// This function provides the on-axis part of its range #[allow(clippy::range_plus_one)] - fn axis_pixel_range(&self, limit: (i32, i32)) -> Range { + fn axis_pixel_range(&self, limit: (i32, i32)) -> Result, Self::ErrorType> { if limit.0 < limit.1 { - limit.0..limit.1 + Ok(limit.0..limit.1) } else { - limit.1..limit.0 + Ok(limit.1..limit.0) } } } From 94f078d2a586ed61dbf4a15423cc10980a802076 Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Sun, 26 Apr 2026 21:24:24 +0800 Subject: [PATCH 07/25] Made error enum top level as it will apply broadly --- .../src/coord/ranged1d/combinators/ckps.rs | 8 ++--- .../coord/ranged1d/combinators/logarithmic.rs | 4 +-- .../src/coord/ranged1d/combinators/nested.rs | 13 ++++---- .../ranged1d/combinators/partial_axis.rs | 9 +++--- plotters/src/coord/ranged1d/discrete.rs | 5 +-- plotters/src/coord/ranged1d/errors.rs | 25 --------------- plotters/src/coord/ranged1d/mod.rs | 3 -- plotters/src/coord/ranged1d/types/datetime.rs | 19 ++++++------ plotters/src/coord/ranged1d/types/numeric.rs | 31 ++++++++++--------- plotters/src/errors.rs | 25 +++++++++++++++ plotters/src/lib.rs | 4 ++- 11 files changed, 75 insertions(+), 71 deletions(-) delete mode 100644 plotters/src/coord/ranged1d/errors.rs create mode 100644 plotters/src/errors.rs diff --git a/plotters/src/coord/ranged1d/combinators/ckps.rs b/plotters/src/coord/ranged1d/combinators/ckps.rs index 0935e676..68b81b54 100644 --- a/plotters/src/coord/ranged1d/combinators/ckps.rs +++ b/plotters/src/coord/ranged1d/combinators/ckps.rs @@ -73,7 +73,7 @@ where } } - fn axis_pixel_range(&self, limit: (i32, i32)) -> Range { + fn axis_pixel_range(&self, limit: (i32, i32)) -> Result, Self::ErrorType> { self.inner.axis_pixel_range(limit) } } @@ -199,7 +199,7 @@ impl Ranged for WithKeyPointMethod { } } - fn axis_pixel_range(&self, limit: (i32, i32)) -> Range { + fn axis_pixel_range(&self, limit: (i32, i32)) -> Result, Self::ErrorType> { self.inner.axis_pixel_range(limit) } } @@ -238,7 +238,7 @@ mod test { assert_eq!(range.index_of(&10), Some(10)); assert_eq!(range.from_index(10), Some(10)); - assert_eq!(range.axis_pixel_range((0, 1000)), 0..1000); + assert_eq!(range.axis_pixel_range((0, 1000))?, 0..1000); let mut range = range; @@ -266,6 +266,6 @@ mod test { assert_eq!(range.index_of(&10), Some(10)); assert_eq!(range.from_index(10), Some(10)); - assert_eq!(range.axis_pixel_range((0, 1000)), 0..1000); + assert_eq!(range.axis_pixel_range((0, 1000))?, 0..1000); } } diff --git a/plotters/src/coord/ranged1d/combinators/logarithmic.rs b/plotters/src/coord/ranged1d/combinators/logarithmic.rs index cb804486..ef8d6aa6 100644 --- a/plotters/src/coord/ranged1d/combinators/logarithmic.rs +++ b/plotters/src/coord/ranged1d/combinators/logarithmic.rs @@ -4,7 +4,7 @@ use crate::coord::ranged1d::{ }; use std::marker::PhantomData; use std::ops::Range; -use crate::coord::ranged1d::errors::Ranged1DError; +use crate::errors::PlotError; /// The trait for the type that is able to be presented in the log scale. /// This trait is primarily used by [LogRangeExt](struct.LogRangeExt.html). @@ -199,7 +199,7 @@ impl LogCoord { impl Ranged for LogCoord { type FormatOption = DefaultFormatting; type ValueType = V; - type ErrorType = Ranged1DError; + type ErrorType = PlotError; fn map(&self, value: &V, limit: (i32, i32)) -> Result { let fv = self.value_to_f64(value); let value_ln = fv.ln(); diff --git a/plotters/src/coord/ranged1d/combinators/nested.rs b/plotters/src/coord/ranged1d/combinators/nested.rs index c5a1fb8b..a925bab6 100644 --- a/plotters/src/coord/ranged1d/combinators/nested.rs +++ b/plotters/src/coord/ranged1d/combinators/nested.rs @@ -1,6 +1,7 @@ use crate::coord::ranged1d::{ - AsRangedCoord, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, Ranged1DError, ValueFormatter + AsRangedCoord, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, ValueFormatter }; +use crate::errors::PlotError; use std::ops::Range; /// Describe a value for a nested coordinate @@ -66,10 +67,10 @@ where } } -impl> Ranged for NestedRange { +impl> Ranged for NestedRange { type FormatOption = NoDefaultFormatting; type ValueType = NestedValue; - type ErrorType = Ranged1DError; + type ErrorType = PlotError; fn range(&self) -> Range { let primary_range = self.primary.range(); @@ -97,8 +98,8 @@ impl> Ranged for NestedR if let Some(secondary_value) = value.nested_value() { self.secondary[idx].map(secondary_value, (s_left, s_right)) } else { - let sum = s_left.checked_add(s_right).ok_or(Ranged1DError::RangeOverflow)?; - let result = sum.checked_div(2).ok_or(Ranged1DError::RangeUnderFlow)?; + let sum = s_left.checked_add(s_right).ok_or(PlotError::RangeOverflow)?; + let result = sum.checked_div(2).ok_or(PlotError::RangeUnderFlow)?; Ok(result) } } @@ -131,7 +132,7 @@ impl> Ranged for NestedR } } -impl> DiscreteRanged for NestedRange { +impl> DiscreteRanged for NestedRange { fn size(&self) -> usize { self.secondary.iter().map(|x| x.size()).sum::() } diff --git a/plotters/src/coord/ranged1d/combinators/partial_axis.rs b/plotters/src/coord/ranged1d/combinators/partial_axis.rs index 2a985fba..febfcb38 100644 --- a/plotters/src/coord/ranged1d/combinators/partial_axis.rs +++ b/plotters/src/coord/ranged1d/combinators/partial_axis.rs @@ -1,6 +1,7 @@ use crate::coord::ranged1d::{ - AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, Ranged, Ranged1DError, + AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, Ranged, }; +use crate::errors::PlotError; use std::ops::Range; /// This axis decorator will make the axis partially display on the axis. @@ -25,13 +26,13 @@ pub trait IntoPartialAxis: AsRangedCoord { impl IntoPartialAxis for R {} -impl> Ranged for PartialAxis +impl> Ranged for PartialAxis where R::ValueType: Clone, { type FormatOption = DefaultFormatting; type ValueType = R::ValueType; - type ErrorType = Ranged1DError; + type ErrorType = PlotError; fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { self.0.map(value, limit) @@ -55,7 +56,7 @@ where impl DiscreteRanged for PartialAxis where - R: Ranged, + R: Ranged, ::ValueType: Eq + Clone, { fn size(&self) -> usize { diff --git a/plotters/src/coord/ranged1d/discrete.rs b/plotters/src/coord/ranged1d/discrete.rs index 82200199..f62bff44 100644 --- a/plotters/src/coord/ranged1d/discrete.rs +++ b/plotters/src/coord/ranged1d/discrete.rs @@ -1,6 +1,7 @@ use crate::coord::ranged1d::{ - AsRangedCoord, KeyPointHint, NoDefaultFormatting, Ranged, Ranged1DError, ReversibleRanged, ValueFormatter + AsRangedCoord, KeyPointHint, NoDefaultFormatting, Ranged, ReversibleRanged, ValueFormatter }; +use crate::errors::PlotError; use std::ops::Range; /// The trait indicates the coordinate is discrete @@ -139,7 +140,7 @@ where impl Ranged for SegmentedCoord { type FormatOption = NoDefaultFormatting; type ValueType = SegmentValue; - type ErrorType = Ranged1DError; + type ErrorType = PlotError; fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { let margin = ((limit.1 - limit.0) as f32 / self.0.size() as f32).round() as i32; diff --git a/plotters/src/coord/ranged1d/errors.rs b/plotters/src/coord/ranged1d/errors.rs deleted file mode 100644 index c992ec1b..00000000 --- a/plotters/src/coord/ranged1d/errors.rs +++ /dev/null @@ -1,25 +0,0 @@ - -use core::fmt; - -#[derive(Debug)] -pub enum Ranged1DError { - RangeOverflow, - RangeUnderFlow, - NonFiniteProjection, - CoordOutOfRange, -} - -impl fmt::Display for Ranged1DError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Ranged1DError::RangeOverflow => write!(f, "value range overflow"), - Ranged1DError::RangeUnderFlow => write!(f, "value range underflow"), - Ranged1DError::NonFiniteProjection => { - write!(f, "projection produced a non-finite value") - } - Ranged1DError::CoordOutOfRange => write!(f, "projected coordinate is out of range"), - } - } -} - -impl std::error::Error for Ranged1DError {} \ No newline at end of file diff --git a/plotters/src/coord/ranged1d/mod.rs b/plotters/src/coord/ranged1d/mod.rs index 9e75c385..00b6ee86 100644 --- a/plotters/src/coord/ranged1d/mod.rs +++ b/plotters/src/coord/ranged1d/mod.rs @@ -56,9 +56,6 @@ use std::ops::Range; pub(super) mod combinators; pub(super) mod types; -pub(crate) mod errors; -pub use errors::Ranged1DError; - mod discrete; pub use discrete::{DiscreteRanged, IntoSegmentedCoord, SegmentValue, SegmentedCoord}; diff --git a/plotters/src/coord/ranged1d/types/datetime.rs b/plotters/src/coord/ranged1d/types/datetime.rs index 1187096e..32aee3a5 100644 --- a/plotters/src/coord/ranged1d/types/datetime.rs +++ b/plotters/src/coord/ranged1d/types/datetime.rs @@ -3,8 +3,9 @@ use chrono::{Date, DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, TimeZ use std::{ops::{Add, Range, Sub}}; use crate::coord::ranged1d::{ - AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, Ranged1DError, ReversibleRanged, ValueFormatter + AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, ReversibleRanged, ValueFormatter }; +use crate::errors::PlotError; /// The trait that describe some time value. This is the uniformed abstraction that works /// for both Date, DateTime and Duration, etc. @@ -27,14 +28,14 @@ pub trait TimeValue: Eq + Sized { fn from_date(date: Self::DateType) -> Self; /// Map the coord spec - fn map_coord(value: &Self, begin: &Self, end: &Self, limit: (i32, i32)) -> Result{ + fn map_coord(value: &Self, begin: &Self, end: &Self, limit: (i32, i32)) -> Result{ let total_span = end.subtract(begin); let value_span = value.subtract(begin); // First, lets try the nanoseconds precision if let Some(total_ns) = total_span.num_nanoseconds() { if let Some(value_ns) = value_span.num_nanoseconds() { - let limit_sub = limit.1.checked_sub(limit.0).ok_or(TimeError::CoordOutOfRange)?; + let limit_sub = limit.1.checked_sub(limit.0).ok_or(PlotError::CoordOutOfRange)?; return Ok((f64::from(limit_sub) * value_ns as f64 / total_ns as f64) as i32 + limit.0); } @@ -212,13 +213,13 @@ where { type FormatOption = DefaultFormatting; type ValueType = D; - type ErrorType = Ranged1DError; + type ErrorType = PlotError; fn range(&self) -> Range { self.0.clone()..self.1.clone() } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { TimeValue::map_coord(value, &self.0, &self.1, limit) } @@ -403,7 +404,7 @@ where { type FormatOption = NoDefaultFormatting; type ValueType = T; - type ErrorType = Ranged1DError; + type ErrorType = PlotError; fn range(&self) -> Range { self.0.start.clone()..self.0.end.clone() @@ -531,7 +532,7 @@ where { type FormatOption = NoDefaultFormatting; type ValueType = T; - type ErrorType = Ranged1DError; + type ErrorType = PlotError; fn range(&self) -> Range { self.0.start.clone()..self.0.end.clone() @@ -659,7 +660,7 @@ where { type FormatOption = DefaultFormatting; type ValueType = DT; - type ErrorType = Ranged1DError; + type ErrorType = PlotError; fn range(&self) -> Range
{ self.0.clone()..self.1.clone() @@ -741,7 +742,7 @@ impl From> for RangedDuration { impl Ranged for RangedDuration { type FormatOption = DefaultFormatting; type ValueType = Duration; - type ErrorType = Ranged1DError; + type ErrorType = PlotError; fn range(&self) -> Range { self.0..self.1 diff --git a/plotters/src/coord/ranged1d/types/numeric.rs b/plotters/src/coord/ranged1d/types/numeric.rs index ef5c78f5..a666072f 100644 --- a/plotters/src/coord/ranged1d/types/numeric.rs +++ b/plotters/src/coord/ranged1d/types/numeric.rs @@ -5,9 +5,10 @@ use crate::coord::{ combinators::WithKeyPoints, ranged1d::{ AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, - Ranged, ReversibleRanged, ValueFormatter, errors::Ranged1DError + Ranged, ReversibleRanged, ValueFormatter }, }; +use crate::errors::PlotError; macro_rules! impl_discrete_trait { ($name:ident) => { @@ -48,14 +49,14 @@ macro_rules! impl_ranged_type_trait { macro_rules! impl_reverse_mapping_trait { ($type:ty, $name: ident) => { impl ReversibleRanged for $name { - fn unmap(&self, p: i32, (min, max): (i32, i32)) -> Result, Ranged1DError> { + fn unmap(&self, p: i32, (min, max): (i32, i32)) -> Result, PlotError> { if p < min.min(max) || p > max.max(min) || min == max { - return None; + return Ok(None); } let logical_offset = f64::from(p - min) / f64::from(max - min); - return Some(((self.1 - self.0) as f64 * logical_offset + self.0 as f64) as $type); + return Ok(Some(((self.1 - self.0) as f64 * logical_offset + self.0 as f64) as $type)); } } }; @@ -75,7 +76,7 @@ macro_rules! make_numeric_coord { type ValueType = $type; type ErrorType = $error; #[allow(clippy::float_cmp)] - fn map(&self, v: &$type, limit: (i32, i32)) -> Result { + fn map(&self, v: &$type, limit: (i32, i32)) -> Result { // Corner case: If we have a range that have only one value, // then we just assign everything to the only point if self.1 == self.0 { @@ -255,7 +256,7 @@ gen_key_points_comp!(integer, compute_usize_key_points, usize); make_numeric_coord!( f32, - Ranged1DError, + PlotError, RangedCoordf32, compute_f32_key_points, "The ranged coordinate for type f32", @@ -285,7 +286,7 @@ impl ValueFormatter for WithKeyPoints { make_numeric_coord!( f64, - Ranged1DError, + PlotError, RangedCoordf64, compute_f64_key_points, "The ranged coordinate for type f64", @@ -314,56 +315,56 @@ impl ValueFormatter for WithKeyPoints { } make_numeric_coord!( u32, - Ranged1DError, + PlotError, RangedCoordu32, compute_u32_key_points, "The ranged coordinate for type u32" ); make_numeric_coord!( i32, - Ranged1DError, + PlotError, RangedCoordi32, compute_i32_key_points, "The ranged coordinate for type i32" ); make_numeric_coord!( u64, - Ranged1DError, + PlotError, RangedCoordu64, compute_u64_key_points, "The ranged coordinate for type u64" ); make_numeric_coord!( i64, - Ranged1DError, + PlotError, RangedCoordi64, compute_i64_key_points, "The ranged coordinate for type i64" ); make_numeric_coord!( u128, - Ranged1DError, + PlotError, RangedCoordu128, compute_u128_key_points, "The ranged coordinate for type u128" ); make_numeric_coord!( i128, - Ranged1DError, + PlotError, RangedCoordi128, compute_i128_key_points, "The ranged coordinate for type i128" ); make_numeric_coord!( usize, - Ranged1DError, + PlotError, RangedCoordusize, compute_usize_key_points, "The ranged coordinate for type usize" ); make_numeric_coord!( isize, - Ranged1DError, + PlotError, RangedCoordisize, compute_isize_key_points, "The ranged coordinate for type isize" diff --git a/plotters/src/errors.rs b/plotters/src/errors.rs new file mode 100644 index 00000000..c44263be --- /dev/null +++ b/plotters/src/errors.rs @@ -0,0 +1,25 @@ + +use core::fmt; + +#[derive(Debug)] +pub enum PlotError { + RangeOverflow, + RangeUnderFlow, + NonFiniteProjection, + CoordOutOfRange, +} + +impl fmt::Display for PlotError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PlotError::RangeOverflow => write!(f, "value range overflow"), + PlotError::RangeUnderFlow => write!(f, "value range underflow"), + PlotError::NonFiniteProjection => { + write!(f, "projection produced a non-finite value") + } + PlotError::CoordOutOfRange => write!(f, "projected coordinate is out of range"), + } + } +} + +impl std::error::Error for PlotError {} \ No newline at end of file diff --git a/plotters/src/lib.rs b/plotters/src/lib.rs index a751f957..45ba936f 100644 --- a/plotters/src/lib.rs +++ b/plotters/src/lib.rs @@ -798,8 +798,10 @@ pub mod element; pub mod series; pub mod style; -// used withing crate for ensuring math done doesn't cause unexpected behavior (overflow/underflow etc) +/// used within crate for ensuring math done doesn't cause unexpected behavior (overflow/underflow etc) pub(crate) mod math_guard; +/// Error handling for crate +pub(crate) mod errors; /// Evaluation Context for Rust. See [the evcxr crate](https://crates.io/crates/evcxr) for more information. #[cfg(feature = "evcxr")] From 9f18660a10b1f396dd344022e95bb54fee848bf8 Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Mon, 27 Apr 2026 13:08:02 +0800 Subject: [PATCH 08/25] Reworking map implementations throughout `ranged1d` --- plotters-svg/src/svg.rs | 4 +- .../chart/context/cartesian2d/draw_impl.rs | 20 +-- .../coord/ranged1d/combinators/logarithmic.rs | 2 +- .../src/coord/ranged1d/combinators/nested.rs | 18 +- plotters/src/coord/ranged1d/discrete.rs | 8 +- plotters/src/coord/ranged1d/mod.rs | 10 +- plotters/src/coord/ranged1d/types/datetime.rs | 154 ++++++++++++------ plotters/src/coord/ranged1d/types/numeric.rs | 56 ++++--- plotters/src/coord/ranged1d/types/slice.rs | 34 ++-- plotters/src/errors.rs | 33 ++-- plotters/src/evcxr.rs | 10 +- plotters/src/lib.rs | 4 +- plotters/src/math_guard.rs | 22 ++- plotters/src/style/color.rs | 18 +- plotters/src/style/font/ab_glyph.rs | 16 +- plotters/src/style/font/ttf.rs | 6 +- 16 files changed, 260 insertions(+), 155 deletions(-) diff --git a/plotters-svg/src/svg.rs b/plotters-svg/src/svg.rs index a6de5973..51484f0a 100644 --- a/plotters-svg/src/svg.rs +++ b/plotters-svg/src/svg.rs @@ -603,9 +603,7 @@ impl<'a> DrawingBackend for SVGBackend<'a> { let color = image::ColorType::Rgb8; encoder.write_image(src, w, h, color.into()).map_err(|e| { - DrawingErrorKind::DrawingError(Error::other( - format!("Image error: {}", 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 d38f921a..2ad64f26 100644 --- a/plotters/src/chart/context/cartesian2d/draw_impl.rs +++ b/plotters/src/chart/context/cartesian2d/draw_impl.rs @@ -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/coord/ranged1d/combinators/logarithmic.rs b/plotters/src/coord/ranged1d/combinators/logarithmic.rs index ef8d6aa6..14ac5f82 100644 --- a/plotters/src/coord/ranged1d/combinators/logarithmic.rs +++ b/plotters/src/coord/ranged1d/combinators/logarithmic.rs @@ -2,9 +2,9 @@ use crate::coord::ranged1d::types::RangedCoordf64; use crate::coord::ranged1d::{ AsRangedCoord, DefaultFormatting, KeyPointHint, Ranged, ReversibleRanged, }; +use crate::errors::PlotError; use std::marker::PhantomData; use std::ops::Range; -use crate::errors::PlotError; /// The trait for the type that is able to be presented in the log scale. /// This trait is primarily used by [LogRangeExt](struct.LogRangeExt.html). diff --git a/plotters/src/coord/ranged1d/combinators/nested.rs b/plotters/src/coord/ranged1d/combinators/nested.rs index a925bab6..fec68dfc 100644 --- a/plotters/src/coord/ranged1d/combinators/nested.rs +++ b/plotters/src/coord/ranged1d/combinators/nested.rs @@ -1,5 +1,5 @@ use crate::coord::ranged1d::{ - AsRangedCoord, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, ValueFormatter + AsRangedCoord, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, ValueFormatter, }; use crate::errors::PlotError; use std::ops::Range; @@ -98,8 +98,10 @@ impl> Ranged for NestedRange if let Some(secondary_value) = value.nested_value() { self.secondary[idx].map(secondary_value, (s_left, s_right)) } else { - let sum = s_left.checked_add(s_right).ok_or(PlotError::RangeOverflow)?; - let result = sum.checked_div(2).ok_or(PlotError::RangeUnderFlow)?; + let sum = s_left + .checked_add(s_right) + .ok_or(PlotError::ValueOverflow)?; + let result = sum.checked_div(2).ok_or(PlotError::ValueUnderflow)?; Ok(result) } } @@ -132,7 +134,9 @@ impl> Ranged for NestedRange } } -impl> DiscreteRanged for NestedRange { +impl> DiscreteRanged + for NestedRange +{ fn size(&self) -> usize { self.secondary.iter().map(|x| x.size()).sum::() } @@ -198,9 +202,9 @@ mod test { let range = coord.range(); assert_eq!(NestedValue::Value(0, 0)..NestedValue::Value(10, 11), range); - assert_eq!(coord.map(&NestedValue::Category(0), (0, 1100)), 50); - assert_eq!(coord.map(&NestedValue::Value(0, 0), (0, 1100)), 0); - assert_eq!(coord.map(&NestedValue::Value(5, 4), (0, 1100)), 567); + assert_eq!(coord.map(&NestedValue::Category(0), (0, 1100))?, 50); + assert_eq!(coord.map(&NestedValue::Value(0, 0), (0, 1100))?, 0); + assert_eq!(coord.map(&NestedValue::Value(5, 4), (0, 1100))?, 567); assert_eq!(coord.size(), (2 + 12) * 11 / 2); assert_eq!(coord.index_of(&NestedValue::Value(5, 4)), Some(24)); diff --git a/plotters/src/coord/ranged1d/discrete.rs b/plotters/src/coord/ranged1d/discrete.rs index f62bff44..15fcbece 100644 --- a/plotters/src/coord/ranged1d/discrete.rs +++ b/plotters/src/coord/ranged1d/discrete.rs @@ -1,5 +1,5 @@ use crate::coord::ranged1d::{ - AsRangedCoord, KeyPointHint, NoDefaultFormatting, Ranged, ReversibleRanged, ValueFormatter + AsRangedCoord, KeyPointHint, NoDefaultFormatting, Ranged, ReversibleRanged, ValueFormatter, }; use crate::errors::PlotError; use std::ops::Range; @@ -207,7 +207,11 @@ impl From for SegmentValue { } impl ReversibleRanged for DC { - fn unmap(&self, input: i32, limit: (i32, i32)) -> Result, Self::ErrorType> { + fn unmap( + &self, + input: i32, + limit: (i32, i32), + ) -> Result, Self::ErrorType> { let idx = (f64::from(input - limit.0) * (self.size() as f64) / f64::from(limit.1 - limit.0)) .floor() as usize; self.from_index(idx) diff --git a/plotters/src/coord/ranged1d/mod.rs b/plotters/src/coord/ranged1d/mod.rs index 00b6ee86..4dc0d593 100644 --- a/plotters/src/coord/ranged1d/mod.rs +++ b/plotters/src/coord/ranged1d/mod.rs @@ -197,8 +197,8 @@ pub trait Ranged { /// The type of this value in this range specification type ValueType; - - /// The error type to return in a result. + + /// The error type to return in a result. type ErrorType; /// This function maps the value to i32, which is the drawing coordinate @@ -226,7 +226,11 @@ pub trait Ranged { /// logic value. pub trait ReversibleRanged: Ranged { /// Perform the reverse mapping - fn unmap(&self, input: i32, limit: (i32, i32)) -> Result, Self::ErrorType>; + fn unmap( + &self, + input: i32, + limit: (i32, i32), + ) -> Result, Self::ErrorType>; } /// The trait for the type that can be converted into a ranged coordinate axis diff --git a/plotters/src/coord/ranged1d/types/datetime.rs b/plotters/src/coord/ranged1d/types/datetime.rs index 32aee3a5..d5294719 100644 --- a/plotters/src/coord/ranged1d/types/datetime.rs +++ b/plotters/src/coord/ranged1d/types/datetime.rs @@ -1,11 +1,15 @@ /// The datetime coordinates use chrono::{Date, DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, TimeZone, Timelike}; -use std::{ops::{Add, Range, Sub}}; +use std::ops::{Add, Range, Sub}; -use crate::coord::ranged1d::{ - AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, ReversibleRanged, ValueFormatter -}; use crate::errors::PlotError; +use crate::{ + coord::ranged1d::{ + AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, + Ranged, ReversibleRanged, ValueFormatter, + }, + math_guard::{float_to_integer_checked, non_zero_checked}, +}; /// The trait that describe some time value. This is the uniformed abstraction that works /// for both Date, DateTime and Duration, etc. @@ -28,26 +32,46 @@ pub trait TimeValue: Eq + Sized { fn from_date(date: Self::DateType) -> Self; /// Map the coord spec - fn map_coord(value: &Self, begin: &Self, end: &Self, limit: (i32, i32)) -> Result{ + fn map_coord( + value: &Self, + begin: &Self, + end: &Self, + limit: (i32, i32), + ) -> Result { let total_span = end.subtract(begin); let value_span = value.subtract(begin); - + // Calculate the pixel span cast into i64 to avoid under flowing. + let pixel_span = (limit.1 as i64 - limit.0 as i64) as f64; // First, lets try the nanoseconds precision if let Some(total_ns) = total_span.num_nanoseconds() { if let Some(value_ns) = value_span.num_nanoseconds() { - let limit_sub = limit.1.checked_sub(limit.0).ok_or(PlotError::CoordOutOfRange)?; - return Ok((f64::from(limit_sub) * value_ns as f64 / total_ns as f64) as i32 - + limit.0); + let value_ns = value_ns as f64; + let total_ns = non_zero_checked::( + total_ns, + PlotError::ZeroDivision, + )? as f64; + + let result = float_to_integer_checked::( + pixel_span * value_ns / total_ns, + PlotError::ValueOutOfRange, + )?; + return result.checked_add(limit.0).ok_or(PlotError::ValueOverflow); } } // Yes, converting them to floating point may lose precision, but this is Ok. // If it overflows, it means we have a time span nearly 300 years, we are safe to ignore the // portion less than 1 day. - let total_days = total_span.num_days() as f64; + let total_days = non_zero_checked::( + total_span.num_days(), + PlotError::ZeroDivision, + )? as f64; let value_days = value_span.num_days() as f64; - - Ok((f64::from(limit.1 - limit.0) * value_days / total_days) as i32 + limit.0) + let result = float_to_integer_checked::( + pixel_span * value_days / total_days, + PlotError::ValueOutOfRange, + )?; + result.checked_add(limit.0).ok_or(PlotError::ValueOverflow) } /// Map pixel to coord spec @@ -251,7 +275,7 @@ where } let week_per_point = ((total_weeks as f64) / (max_points as f64)).ceil() as usize; - + for idx in 0..=(total_weeks as usize / week_per_point) { ret.push(self.0.clone() + Duration::weeks((idx * week_per_point) as i64)); } @@ -718,7 +742,11 @@ where RangedDate: Ranged, { /// Perform the reverse mapping - fn unmap(&self, input: i32, limit: (i32, i32)) -> Result, Self::ErrorType> { + fn unmap( + &self, + input: i32, + limit: (i32, i32), + ) -> Result, Self::ErrorType> { let value = Some(TimeValue::unmap_coord(input, &self.0, &self.1, limit)); Ok(value) } @@ -749,23 +777,50 @@ impl Ranged for RangedDuration { } fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { - let total_span = self.1 - self.0; - let value_span = *value - self.0; + let total_span = self + .1 + .checked_sub(&self.0) + .ok_or(PlotError::ValueUnderflow)?; + let value_span = value + .checked_sub(&self.0) + .ok_or(PlotError::ValueUnderflow)?; + + let limit_difference = (i64::from(limit.1) - i64::from(limit.0)) as f64; + let offset = 1e-10; if let Some(total_ns) = total_span.num_nanoseconds() { if let Some(value_ns) = value_span.num_nanoseconds() { - return limit.0 - + (f64::from(limit.1 - limit.0) * value_ns as f64 / total_ns as f64 + 1e-10) - as i32; + let value_ns = value_ns as f64; + let total_ns = non_zero_checked::( + total_ns, + PlotError::ZeroDivision, + )? as f64; + let result = float_to_integer_checked::( + limit_difference * value_ns / total_ns + offset, + PlotError::ValueOutOfRange, + )?; + return limit + .0 + .checked_add(result) + .ok_or(PlotError::ValueOverflow); } - return limit.1; + return Ok(limit.1); } - let total_days = total_span.num_days(); - let value_days = value_span.num_days(); + let value_days = value_span.num_days() as f64; + let total_days = non_zero_checked::( + total_span.num_days(), + PlotError::ZeroDivision, + )? as f64; - limit.0 - + (f64::from(limit.1 - limit.0) * value_days as f64 / total_days as f64 + 1e-10) as i32 + let result = float_to_integer_checked::( + limit_difference * value_days / total_days + offset, + PlotError::ValueOutOfRange, + )?; + limit + .0 + .checked_add(result) + .ok_or(PlotError::ValueOverflow) } fn key_points(&self, hint: HintType) -> Vec { @@ -916,8 +971,8 @@ mod test { let ranged_coord = Into::>::into(range); - assert_eq!(ranged_coord.map(&Utc.ymd(1000, 8, 10), (0, 100)), 0); - assert_eq!(ranged_coord.map(&Utc.ymd(2999, 8, 10), (0, 100)), 100); + assert_eq!(ranged_coord.map(&Utc.ymd(1000, 8, 10), (0, 100)), Ok(0)); + assert_eq!(ranged_coord.map(&Utc.ymd(2999, 8, 10), (0, 100)), Ok(100)); let kps = ranged_coord.key_points(23); @@ -986,8 +1041,8 @@ mod test { let range = Utc.ymd(1000, 8, 5)..Utc.ymd(2999, 1, 1); let ranged_coord = range.yearly(); - assert_eq!(ranged_coord.map(&Utc.ymd(1000, 8, 10), (0, 100)), 0); - assert_eq!(ranged_coord.map(&Utc.ymd(2999, 8, 10), (0, 100)), 100); + assert_eq!(ranged_coord.map(&Utc.ymd(1000, 8, 10), (0, 100)), Ok(0)); + assert_eq!(ranged_coord.map(&Utc.ymd(2999, 8, 10), (0, 100)), Ok(100)); let kps = ranged_coord.key_points(23); @@ -1048,11 +1103,11 @@ mod test { assert_eq!( coord.map(&Utc.ymd(1000, 1, 1).and_hms(0, 0, 0), (0, 100)), - 0 + Ok(0) ); assert_eq!( coord.map(&Utc.ymd(3000, 1, 1).and_hms(0, 0, 0), (0, 100)), - 100 + Ok(100) ); let kps = coord.key_points(23); @@ -1151,8 +1206,8 @@ mod test { fn test_duration_long_range() { let coord: RangedDuration = (Duration::days(-1000000)..Duration::days(1000000)).into(); - assert_eq!(coord.map(&Duration::days(-1000000), (0, 100)), 0); - assert_eq!(coord.map(&Duration::days(1000000), (0, 100)), 100); + assert_eq!(coord.map(&Duration::days(-1000000), (0, 100)), Ok(0)); + assert_eq!(coord.map(&Duration::days(1000000), (0, 100)), Ok(100)); let kps = coord.key_points(23); @@ -1238,9 +1293,10 @@ mod test { let mid = Utc.ymd(2022, 1, 1).and_hms(8, 0, 0); let coord: RangedDateTime<_> = (start_time..end_time).into(); let pos = coord.map(&mid, (1000, 2000)); - assert_eq!(pos, 1500); - let value = coord.unmap(pos, (1000, 2000)); - assert_eq!(value, Some(mid)); + assert_eq!(pos, Ok(1500)); + let value: Result>, PlotError> = + coord.unmap(pos.unwrap(), (1000, 2000)); + assert_eq!(value, Ok(Some(mid))); } #[test] @@ -1250,9 +1306,9 @@ mod test { let mid = NaiveDate::from_ymd(2022, 1, 1).and_hms_milli(8, 0, 0, 0); let coord: RangedDateTime<_> = (start_time..end_time).into(); let pos = coord.map(&mid, (1000, 2000)); - assert_eq!(pos, 1500); - let value = coord.unmap(pos, (1000, 2000)); - assert_eq!(value, Some(mid)); + assert_eq!(pos, Ok(1500)); + let value = coord.unmap(pos.unwrap(), (1000, 2000)); + assert_eq!(value, Ok(Some(mid))); } #[test] @@ -1262,9 +1318,9 @@ mod test { let mid = Utc.ymd(2022, 1, 1); let coord: RangedDate> = (start_date..end_date).into(); let pos = coord.map(&mid, (1000, 2000)); - assert_eq!(pos, 1500); - let value = coord.unmap(pos, (1000, 2000)); - assert_eq!(value, Some(mid)); + assert_eq!(pos, Ok(1500)); + let value = coord.unmap(pos.unwrap(), (1000, 2000)); + assert_eq!(value, Ok(Some(mid))); } #[test] @@ -1274,9 +1330,9 @@ mod test { let mid = NaiveDate::from_ymd(2022, 1, 1); let coord: RangedDate = (start_date..end_date).into(); let pos = coord.map(&mid, (1000, 2000)); - assert_eq!(pos, 1500); - let value = coord.unmap(pos, (1000, 2000)); - assert_eq!(value, Some(mid)); + assert_eq!(pos, Ok(1500)); + let value = coord.unmap(pos.unwrap(), (1000, 2000)); + assert_eq!(value, Ok(Some(mid))); } #[test] @@ -1286,9 +1342,9 @@ mod test { let mid = start_time + Duration::nanoseconds(950); let coord: RangedDateTime<_> = (start_time..end_time).into(); let pos = coord.map(&mid, (1000, 2000)); - assert_eq!(pos, 1500); - let value = coord.unmap(pos, (1000, 2000)); - assert_eq!(value, Some(mid)); + assert_eq!(pos, Ok(1500)); + let value = coord.unmap(pos.unwrap(), (1000, 2000)); + assert_eq!(value, Ok(Some(mid))); } #[test] @@ -1297,9 +1353,9 @@ mod test { let end_time = start_time + Duration::nanoseconds(400); let coord: RangedDateTime<_> = (start_time..end_time).into(); let value = coord.unmap(2000, (1000, 2000)); - assert_eq!(value, Some(end_time)); + assert_eq!(value, Ok(Some(end_time))); let mid = start_time + Duration::nanoseconds(200); let value = coord.unmap(500, (0, 1000)); - assert_eq!(value, Some(mid)); + assert_eq!(value, Ok(Some(mid))); } } diff --git a/plotters/src/coord/ranged1d/types/numeric.rs b/plotters/src/coord/ranged1d/types/numeric.rs index a666072f..5e36e24b 100644 --- a/plotters/src/coord/ranged1d/types/numeric.rs +++ b/plotters/src/coord/ranged1d/types/numeric.rs @@ -1,13 +1,13 @@ use std::convert::TryFrom; use std::ops::Range; -use crate::coord::{ +use crate::{coord::{ combinators::WithKeyPoints, ranged1d::{ AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, - Ranged, ReversibleRanged, ValueFormatter + Ranged, ReversibleRanged, ValueFormatter, }, -}; +}, math_guard::float_to_integer_checked}; use crate::errors::PlotError; macro_rules! impl_discrete_trait { @@ -56,7 +56,9 @@ macro_rules! impl_reverse_mapping_trait { let logical_offset = f64::from(p - min) / f64::from(max - min); - return Ok(Some(((self.1 - self.0) as f64 * logical_offset + self.0 as f64) as $type)); + return Ok(Some( + ((self.1 - self.0) as f64 * logical_offset + self.0 as f64) as $type, + )); } } }; @@ -76,34 +78,40 @@ macro_rules! make_numeric_coord { type ValueType = $type; type ErrorType = $error; #[allow(clippy::float_cmp)] - fn map(&self, v: &$type, limit: (i32, i32)) -> Result { + fn map(&self, v: &$type, limit: (i32, i32)) -> Result { // Corner case: If we have a range that have only one value, // then we just assign everything to the only point if self.1 == self.0 { - return (limit.1 - limit.0) / 2; + let midpoint = (i64::from(limit.0) + i64::from(limit.1)) / 2; + return Ok(midpoint as i32); } let logic_length = (*v as f64 - self.0 as f64) / (self.1 as f64 - self.0 as f64); - - let actual_length = limit.1 - limit.0; - - if actual_length == 0 { - return limit.1; + if !logic_length.is_finite() { + return Err(PlotError::NonFiniteCalculation); } - if logic_length.is_infinite() { - if logic_length.is_sign_positive() { - return limit.1; - } else { - return limit.0; - } + let actual_length = (i64::from(limit.1) - i64::from(limit.0)) as f64; + + if actual_length == 0.0 { + return Ok(limit.1); } - if actual_length > 0 { - return limit.0 + (actual_length as f64 * logic_length + 1e-3).floor() as i32; + let projected = if actual_length > 0.0 { + (actual_length * logic_length + 1e-3).floor() } else { - return limit.0 + (actual_length as f64 * logic_length - 1e-3).ceil() as i32; - } + (actual_length * logic_length - 1e-3).ceil() + }; + + let offset = float_to_integer_checked::( + projected, + PlotError::ValueOutOfRange, + )?; + + limit + .0 + .checked_add(offset) + .ok_or(PlotError::ValueOverflow) } fn key_points(&self, hint: Hint) -> Vec<$type> { $key_points((self.0, self.1), hint.max_num_points()) @@ -413,10 +421,10 @@ mod test { assert_eq!(coord.key_points(11).len(), 11); assert_eq!(coord.key_points(11)[0], 0); assert_eq!(coord.key_points(11)[10], 20); - assert_eq!(coord.map(&5, (0, 100)), 25); + assert_eq!(coord.map(&5, (0, 100)), Ok(25)); let coord: RangedCoordf32 = (0f32..20f32).into(); - assert_eq!(coord.map(&5.0, (0, 100)), 25); + assert_eq!(coord.map(&5.0, (0, 100)), Ok(25)); } #[test] @@ -434,7 +442,7 @@ mod test { let coord: RangedCoordu32 = (0..20).into(); let pos = coord.map(&5, (1000, 2000)); let value = coord.unmap(pos?, (1000, 2000)); - assert_eq!(value, Some(5)); + assert_eq!(value, Ok(Some(5))); } #[test] diff --git a/plotters/src/coord/ranged1d/types/slice.rs b/plotters/src/coord/ranged1d/types/slice.rs index e4bc5bba..90134790 100644 --- a/plotters/src/coord/ranged1d/types/slice.rs +++ b/plotters/src/coord/ranged1d/types/slice.rs @@ -1,6 +1,8 @@ use crate::coord::ranged1d::{ - AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, Ranged, Ranged1DError, + AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, Ranged, }; +use crate::errors::PlotError; +use crate::math_guard::{float_to_integer_checked, non_zero_checked}; use std::ops::Range; /// A range that is defined by a slice of values. @@ -12,23 +14,33 @@ pub struct RangedSlice<'a, T: PartialEq>(&'a [T]); impl<'a, T: PartialEq> Ranged for RangedSlice<'a, T> { type FormatOption = DefaultFormatting; type ValueType = &'a T; - type ErrorType = Ranged1DError; + type ErrorType = PlotError; fn range(&self) -> Range<&'a T> { // If inner slice is empty, we should always panic &self.0[0]..&self.0[self.0.len() - 1] } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { match self.0.iter().position(|x| &x == value) { Some(pos) => { - let pixel_span = limit.1 - limit.0; - let value_span = self.0.len() - 1; - (f64::from(limit.0) - + f64::from(pixel_span) - * (f64::from(pos as u32) / f64::from(value_span as u32))) - .round() as i32 + let pixel_span = (i64::from(limit.1) - i64::from(limit.0)) as f64; + let value_span = self + .0 + .len() + .checked_sub(1) + .ok_or(PlotError::ValueUnderflow)?; + + let value_span = + non_zero_checked::(value_span, PlotError::ZeroDivision)? as f64; + + let offset = float_to_integer_checked::( + pixel_span * (pos as f64 / value_span), + PlotError::ValueOutOfRange, + )?; + + limit.0.checked_add(offset).ok_or(PlotError::ValueOverflow) } - None => limit.0, + None => Ok(limit.0), } } @@ -85,7 +97,7 @@ mod test { slice_range.key_points(6), my_slice.iter().collect::>() ); - assert_eq!(slice_range.map(&&0, (0, 50)), 30); + assert_eq!(slice_range.map(&&0, (0, 50)), Ok(30)); } #[test] diff --git a/plotters/src/errors.rs b/plotters/src/errors.rs index c44263be..59f269e0 100644 --- a/plotters/src/errors.rs +++ b/plotters/src/errors.rs @@ -1,25 +1,34 @@ - use core::fmt; -#[derive(Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum PlotError { - RangeOverflow, - RangeUnderFlow, - NonFiniteProjection, - CoordOutOfRange, + ValueOverflow, + ValueUnderflow, + NonFiniteCalculation, + ValueOutOfRange, + ZeroDivision, } impl fmt::Display for PlotError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - PlotError::RangeOverflow => write!(f, "value range overflow"), - PlotError::RangeUnderFlow => write!(f, "value range underflow"), - PlotError::NonFiniteProjection => { - write!(f, "projection produced a non-finite value") + PlotError::ValueOverflow => { + write!(f, "value exceeds the target type's maximum") + } + PlotError::ValueUnderflow => { + write!(f, "value is below the target type's minimum") + } + PlotError::NonFiniteCalculation => { + write!(f, "calculation produced a non-finite value") + } + PlotError::ValueOutOfRange => { + write!(f, "value is out of range for the target type") + } + PlotError::ZeroDivision => { + write!(f, "attempted to divide by zero") } - PlotError::CoordOutOfRange => write!(f, "projected coordinate is out of range"), } } } -impl std::error::Error for PlotError {} \ No newline at end of file +impl std::error::Error for PlotError {} diff --git a/plotters/src/evcxr.rs b/plotters/src/evcxr.rs index aa42c801..bd8f15fd 100644 --- a/plotters/src/evcxr.rs +++ b/plotters/src/evcxr.rs @@ -77,8 +77,11 @@ pub fn evcxr_bitmap_figure< ) -> Result> { const PIXEL_SIZE: usize = 3; - let buf_len = (size.0 as usize).checked_mul(size.1 as usize).and_then(|n| n.checked_mul(PIXEL_SIZE)).ok_or("image buffer size overflow")?; - + let buf_len = (size.0 as usize) + .checked_mul(size.1 as usize) + .and_then(|n| n.checked_mul(PIXEL_SIZE)) + .ok_or("image buffer size overflow")?; + let mut buf = vec![0; buf_len]; let root = BitMapBackend::with_buffer(&mut buf, size).into_drawing_area(); @@ -86,8 +89,7 @@ pub fn evcxr_bitmap_figure< let mut buffer = String::new(); { let mut svg_root = SVGBackend::with_string(&mut buffer, size); - svg_root - .blit_bitmap((0, 0), size, &buf)?; + svg_root.blit_bitmap((0, 0), size, &buf)?; } Ok(SVGWrapper(buffer, String::new())) } diff --git a/plotters/src/lib.rs b/plotters/src/lib.rs index 45ba936f..1cf0006f 100644 --- a/plotters/src/lib.rs +++ b/plotters/src/lib.rs @@ -798,10 +798,10 @@ pub mod element; pub mod series; pub mod style; -/// used within crate for ensuring math done doesn't cause unexpected behavior (overflow/underflow etc) -pub(crate) mod math_guard; /// Error handling for crate pub(crate) mod errors; +/// used within crate for ensuring math done doesn't cause unexpected behavior (overflow/underflow etc) +pub(crate) mod math_guard; /// Evaluation Context for Rust. See [the evcxr crate](https://crates.io/crates/evcxr) for more information. #[cfg(feature = "evcxr")] diff --git a/plotters/src/math_guard.rs b/plotters/src/math_guard.rs index 3223c716..43a22434 100644 --- a/plotters/src/math_guard.rs +++ b/plotters/src/math_guard.rs @@ -1,4 +1,4 @@ -use num_traits::{Float, NumCast, PrimInt}; +use num_traits::{Float, NumCast, PrimInt, Zero}; pub(crate) fn float_to_integer_checked(v: F, err: E) -> Result where @@ -19,11 +19,25 @@ where NumCast::from(v).ok_or(err) } - -pub(crate) fn try_convert_float(v: FB, err: E) -> Result where FB: Float + NumCast, FS: Float + NumCast { +pub(crate) fn try_convert_float(v: FB, err: E) -> Result +where + FB: Float + NumCast, + FS: Float + NumCast, +{ if !v.is_finite() { return Err(err); } let out: FS = NumCast::from(v).ok_or(err)?; Ok(out) -} \ No newline at end of file +} + +pub(crate) fn non_zero_checked(v: T, err: E) -> Result +where + T: Zero, +{ + if v.is_zero() { + Err(err) + } else { + Ok(v) + } +} diff --git a/plotters/src/style/color.rs b/plotters/src/style/color.rs index 5bcdd2b9..7f02722c 100644 --- a/plotters/src/style/color.rs +++ b/plotters/src/style/color.rs @@ -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 28fc1ac2..9b02ae79 100644 --- a/plotters/src/style/font/ab_glyph.rs +++ b/plotters/src/style/font/ab_glyph.rs @@ -2,11 +2,11 @@ use super::{FontData, FontFamily, FontStyle, LayoutBox}; use ab_glyph::{Font, FontRef, ScaleFont}; use core::fmt::{self, Display}; use once_cell::sync::Lazy; -use std::{collections::HashMap, convert::TryFrom}; use std::error::Error; use std::sync::RwLock; +use std::{collections::HashMap, convert::TryFrom}; -use crate::math_guard::{try_convert_float, float_to_integer_checked}; +use crate::math_guard::{float_to_integer_checked, try_convert_float}; struct FontMap { map: HashMap>, @@ -131,9 +131,9 @@ impl FontData for FontDataInternal { mut draw: DrawFunc, ) -> Result, Self::ErrorType> { let metric_error = FontError::InvalidMetrics; - let size = try_convert_float::(size, metric_error)?; + let size = try_convert_float::(size, metric_error)?; let font = self.font_ref.as_scaled(size); - + let mut draw = |x: i32, y: i32, c| { let (base_x, base_y) = pos; let add_x = x.checked_add(base_x).ok_or(metric_error)?; @@ -151,14 +151,15 @@ impl FontData for FontDataInternal { if let Some(q) = font.outline_glyph(glyph) { let rect = q.px_bounds(); let y_div = size / 2.0 + rect.min.y; - let y_shift = float_to_integer_checked::(y_div, metric_error)?; + let y_shift = + float_to_integer_checked::(y_div, metric_error)?; let x_shift = x_shift as i32; let mut buf = vec![]; q.draw(|x, y, c| buf.push((x, y, c))); for (x, y, c) in buf { let x = i32::try_from(x).map_err(|_| metric_error)?; - let x_val = x.checked_add(x_shift).ok_or( metric_error)?; - let y= i32::try_from(y).map_err(|_| metric_error)?; + let x_val = x.checked_add(x_shift).ok_or(metric_error)?; + let y = i32::try_from(y).map_err(|_| metric_error)?; let y_val = y.checked_add(y_shift).ok_or(metric_error)?; draw(x_val, y_val, c).map_err(|_e| { // Note: If ever `plotters` adds a tracing or logging crate, @@ -172,4 +173,3 @@ impl FontData for FontDataInternal { Ok(Ok(())) } } - diff --git a/plotters/src/style/font/ttf.rs b/plotters/src/style/font/ttf.rs index 52bfe2c4..2b9fda2d 100644 --- a/plotters/src/style/font/ttf.rs +++ b/plotters/src/style/font/ttf.rs @@ -47,7 +47,7 @@ impl std::fmt::Display for FontError { 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::InvalidFontBox => write!(fmt, "Font Box is Invalid") + FontError::InvalidFontBox => write!(fmt, "Font Box is Invalid"), } } } @@ -81,9 +81,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) From 91626fb11b96b07febc5bf6bb43c0c282091676a Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Mon, 27 Apr 2026 16:46:39 +0800 Subject: [PATCH 09/25] updated `discrete` mod's `map` function --- plotters/src/coord/ranged1d/discrete.rs | 67 ++++++++++++++----- plotters/src/coord/ranged1d/types/datetime.rs | 38 ++++------- plotters/src/coord/ranged1d/types/numeric.rs | 22 +++--- plotters/src/coord/ranged1d/types/slice.rs | 5 +- 4 files changed, 77 insertions(+), 55 deletions(-) diff --git a/plotters/src/coord/ranged1d/discrete.rs b/plotters/src/coord/ranged1d/discrete.rs index 15fcbece..daf06f51 100644 --- a/plotters/src/coord/ranged1d/discrete.rs +++ b/plotters/src/coord/ranged1d/discrete.rs @@ -2,6 +2,7 @@ use crate::coord::ranged1d::{ AsRangedCoord, KeyPointHint, NoDefaultFormatting, Ranged, ReversibleRanged, ValueFormatter, }; use crate::errors::PlotError; +use crate::math_guard::{float_to_integer_checked, non_zero_checked}; use std::ops::Range; /// The trait indicates the coordinate is discrete @@ -137,30 +138,59 @@ where } } -impl Ranged for SegmentedCoord { +impl Ranged for SegmentedCoord +where + PlotError: From<::ErrorType>, +{ type FormatOption = NoDefaultFormatting; type ValueType = SegmentValue; type ErrorType = PlotError; fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { - let margin = ((limit.1 - limit.0) as f32 / self.0.size() as f32).round() as i32; - + let pixel_span = (i64::from(limit.1) - i64::from(limit.0)) as f64; + let size = + non_zero_checked::(self.0.size(), PlotError::ZeroDivision)? as f64; + let margin = float_to_integer_checked::( + (pixel_span / size).round(), + PlotError::ValueOutOfRange, + )?; + let upper = limit.1.checked_sub(margin).ok_or(PlotError::ValueUnderflow)?; match value { - SegmentValue::Exact(coord) => self.0.map(coord, (limit.0, limit.1 - margin)), + SegmentValue::Exact(coord) => Ok(self.0.map( + coord, + ( + limit.0, + upper, + ), + )?), SegmentValue::CenterOf(coord) => { - let left = self.0.map(coord, (limit.0, limit.1 - margin)); + let left = self.0.map( + coord, + ( + limit.0, + upper, + ), + )?; if let Some(idx) = self.0.index_of(coord) { - if idx + 1 < self.0.size() { + if idx.checked_add(1).ok_or(PlotError::ValueOverflow)? < self.0.size() { let right = self.0.map( - &self.0.from_index(idx + 1).unwrap(), - (limit.0, limit.1 - margin), - ); - return (left + right) / 2; + &self + .0 + .from_index(idx.checked_add(1).ok_or(PlotError::ValueOverflow)?) + .ok_or(PlotError::ValueOutOfRange)?, + ( + limit.0, + upper, + ), + )?; + let mid = (i64::from(left) + i64::from(right)) / 2; + return Ok(mid as i32); } } - left + margin / 2 + let pos = (i64::from(left) + i64::from(margin)) / 2; + Ok(pos as i32) } - SegmentValue::Last => limit.1, + SegmentValue::Last => Ok(limit.1), } } @@ -178,7 +208,10 @@ impl Ranged for SegmentedCoord { } } -impl DiscreteRanged for SegmentedCoord { +impl DiscreteRanged for SegmentedCoord +where + PlotError: From<::ErrorType>, +{ fn size(&self) -> usize { self.0.size() + 1 } @@ -214,7 +247,7 @@ impl ReversibleRanged for DC { ) -> Result, Self::ErrorType> { let idx = (f64::from(input - limit.0) * (self.size() as f64) / f64::from(limit.1 - limit.0)) .floor() as usize; - self.from_index(idx) + Ok(self.from_index(idx)) } } @@ -273,8 +306,8 @@ mod test { } } - assert_eq!(coord.map(&SegmentValue::CenterOf(0), (0, 24)), 1); - assert_eq!(coord.map(&SegmentValue::Exact(0), (0, 24)), 0); - assert_eq!(coord.map(&SegmentValue::Exact(1), (0, 24)), 2); + assert_eq!(coord.map(&SegmentValue::CenterOf(0), (0, 24)), Ok(1)); + assert_eq!(coord.map(&SegmentValue::Exact(0), (0, 24)), Ok(0)); + assert_eq!(coord.map(&SegmentValue::Exact(1), (0, 24)), Ok(2)); } } diff --git a/plotters/src/coord/ranged1d/types/datetime.rs b/plotters/src/coord/ranged1d/types/datetime.rs index d5294719..b220b937 100644 --- a/plotters/src/coord/ranged1d/types/datetime.rs +++ b/plotters/src/coord/ranged1d/types/datetime.rs @@ -8,7 +8,7 @@ use crate::{ AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, ReversibleRanged, ValueFormatter, }, - math_guard::{float_to_integer_checked, non_zero_checked}, + math_guard::{float_to_integer_checked, non_zero_checked}, }; /// The trait that describe some time value. This is the uniformed abstraction that works @@ -46,10 +46,8 @@ pub trait TimeValue: Eq + Sized { if let Some(total_ns) = total_span.num_nanoseconds() { if let Some(value_ns) = value_span.num_nanoseconds() { let value_ns = value_ns as f64; - let total_ns = non_zero_checked::( - total_ns, - PlotError::ZeroDivision, - )? as f64; + let total_ns = + non_zero_checked::(total_ns, PlotError::ZeroDivision)? as f64; let result = float_to_integer_checked::( pixel_span * value_ns / total_ns, @@ -62,10 +60,9 @@ pub trait TimeValue: Eq + Sized { // Yes, converting them to floating point may lose precision, but this is Ok. // If it overflows, it means we have a time span nearly 300 years, we are safe to ignore the // portion less than 1 day. - let total_days = non_zero_checked::( - total_span.num_days(), - PlotError::ZeroDivision, - )? as f64; + let total_days = + non_zero_checked::(total_span.num_days(), PlotError::ZeroDivision)? + as f64; let value_days = value_span.num_days() as f64; let result = float_to_integer_checked::( pixel_span * value_days / total_days, @@ -791,36 +788,27 @@ impl Ranged for RangedDuration { if let Some(total_ns) = total_span.num_nanoseconds() { if let Some(value_ns) = value_span.num_nanoseconds() { let value_ns = value_ns as f64; - let total_ns = non_zero_checked::( - total_ns, - PlotError::ZeroDivision, - )? as f64; + let total_ns = + non_zero_checked::(total_ns, PlotError::ZeroDivision)? as f64; let result = float_to_integer_checked::( limit_difference * value_ns / total_ns + offset, PlotError::ValueOutOfRange, )?; - return limit - .0 - .checked_add(result) - .ok_or(PlotError::ValueOverflow); + return limit.0.checked_add(result).ok_or(PlotError::ValueOverflow); } return Ok(limit.1); } let value_days = value_span.num_days() as f64; - let total_days = non_zero_checked::( - total_span.num_days(), - PlotError::ZeroDivision, - )? as f64; + let total_days = + non_zero_checked::(total_span.num_days(), PlotError::ZeroDivision)? + as f64; let result = float_to_integer_checked::( limit_difference * value_days / total_days + offset, PlotError::ValueOutOfRange, )?; - limit - .0 - .checked_add(result) - .ok_or(PlotError::ValueOverflow) + limit.0.checked_add(result).ok_or(PlotError::ValueOverflow) } fn key_points(&self, hint: HintType) -> Vec { diff --git a/plotters/src/coord/ranged1d/types/numeric.rs b/plotters/src/coord/ranged1d/types/numeric.rs index 5e36e24b..1665cdb4 100644 --- a/plotters/src/coord/ranged1d/types/numeric.rs +++ b/plotters/src/coord/ranged1d/types/numeric.rs @@ -1,14 +1,17 @@ use std::convert::TryFrom; use std::ops::Range; -use crate::{coord::{ - combinators::WithKeyPoints, - ranged1d::{ - AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, - Ranged, ReversibleRanged, ValueFormatter, - }, -}, math_guard::float_to_integer_checked}; use crate::errors::PlotError; +use crate::{ + coord::{ + combinators::WithKeyPoints, + ranged1d::{ + AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, + Ranged, ReversibleRanged, ValueFormatter, + }, + }, + math_guard::float_to_integer_checked, +}; macro_rules! impl_discrete_trait { ($name:ident) => { @@ -108,10 +111,7 @@ macro_rules! make_numeric_coord { PlotError::ValueOutOfRange, )?; - limit - .0 - .checked_add(offset) - .ok_or(PlotError::ValueOverflow) + limit.0.checked_add(offset).ok_or(PlotError::ValueOverflow) } fn key_points(&self, hint: Hint) -> Vec<$type> { $key_points((self.0, self.1), hint.max_num_points()) diff --git a/plotters/src/coord/ranged1d/types/slice.rs b/plotters/src/coord/ranged1d/types/slice.rs index 90134790..7bdf6294 100644 --- a/plotters/src/coord/ranged1d/types/slice.rs +++ b/plotters/src/coord/ranged1d/types/slice.rs @@ -30,8 +30,9 @@ impl<'a, T: PartialEq> Ranged for RangedSlice<'a, T> { .checked_sub(1) .ok_or(PlotError::ValueUnderflow)?; - let value_span = - non_zero_checked::(value_span, PlotError::ZeroDivision)? as f64; + let value_span = + non_zero_checked::(value_span, PlotError::ZeroDivision)? + as f64; let offset = float_to_integer_checked::( pixel_span * (pos as f64 / value_span), From 1c531c07fd8f63c26b28aa34eda3840e9f6d8d00 Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Mon, 27 Apr 2026 21:28:08 +0800 Subject: [PATCH 10/25] `PlotError` is now `MathError` to better fit error enum role --- plotters/src/chart/context.rs | 2 +- .../chart/context/cartesian3d/draw_impl.rs | 2 +- plotters/src/chart/dual_coord.rs | 6 +-- plotters/src/coord/mod.rs | 15 ++++-- .../coord/ranged1d/combinators/logarithmic.rs | 4 +- .../src/coord/ranged1d/combinators/nested.rs | 12 ++--- .../ranged1d/combinators/partial_axis.rs | 10 ++-- plotters/src/coord/ranged1d/discrete.rs | 22 ++++---- plotters/src/coord/ranged1d/types/datetime.rs | 54 +++++++++---------- plotters/src/coord/ranged1d/types/numeric.rs | 32 +++++------ plotters/src/coord/ranged1d/types/slice.rs | 16 +++--- plotters/src/coord/ranged2d/cartesian.rs | 30 +++++------ plotters/src/coord/ranged3d/cartesian3d.rs | 6 ++- plotters/src/coord/translate.rs | 9 ++-- plotters/src/drawing/area.rs | 10 ++++ plotters/src/lib.rs | 2 +- plotters/src/{errors.rs => math_errors.rs} | 16 +++--- 17 files changed, 135 insertions(+), 113 deletions(-) rename plotters/src/{errors.rs => math_errors.rs} (68%) diff --git a/plotters/src/chart/context.rs b/plotters/src/chart/context.rs index c63ee3b0..4c366938 100644 --- a/plotters/src/chart/context.rs +++ b/plotters/src/chart/context.rs @@ -32,7 +32,7 @@ pub struct ChartContext<'a, DB: DrawingBackend, CT: CoordTranslate> { impl<'a, DB: DrawingBackend, CT: ReverseCoordTranslate> ChartContext<'a, DB, CT> { /// Convert the chart context into an closure that can be used for coordinate translation - pub fn into_coord_trans(self) -> impl Fn(BackendCoord) -> Option { + pub fn into_coord_trans(self) -> impl Fn(BackendCoord) -> Result, CT::ErrorType>{ let coord_spec = self.drawing_area.into_coord_spec(); move |coord| coord_spec.reverse_translate(coord) } diff --git a/plotters/src/chart/context/cartesian3d/draw_impl.rs b/plotters/src/chart/context/cartesian3d/draw_impl.rs index 81b1ad2a..bbb837eb 100644 --- a/plotters/src/chart/context/cartesian3d/draw_impl.rs +++ b/plotters/src/chart/context/cartesian3d/draw_impl.rs @@ -69,7 +69,7 @@ where &axis[1][1], &axis[1][2], ])); - let axis_dir = (end.0 - begin.0, end.1 - begin.1); + let axis_dir = (end?.0 - begin?.0, end?.1 - begin?.1); let (x_range, y_range) = self.plotting_area().get_pixel_range(); let x_mid = (x_range.start + x_range.end) / 2; let y_mid = (y_range.start + y_range.end) / 2; diff --git a/plotters/src/chart/dual_coord.rs b/plotters/src/chart/dual_coord.rs index 048bea02..491d9dc5 100644 --- a/plotters/src/chart/dual_coord.rs +++ b/plotters/src/chart/dual_coord.rs @@ -142,7 +142,7 @@ impl DualCoordChartContext<'_, DB, CT1, CT2> { /// Convert the chart context into the secondary coordinate translation function - pub fn into_secondary_coord_trans(self) -> impl Fn(BackendCoord) -> Option { + pub fn into_secondary_coord_trans(self) -> impl Fn(BackendCoord) -> Result, CT2::ErrorType> { let coord_spec = self.secondary.drawing_area.into_coord_spec(); move |coord| coord_spec.reverse_translate(coord) } @@ -156,8 +156,8 @@ impl pub fn into_coord_trans_pair( self, ) -> ( - impl Fn(BackendCoord) -> Option, - impl Fn(BackendCoord) -> Option, + impl Fn(BackendCoord) -> Result, CT1::ErrorType>, + impl Fn(BackendCoord) -> Result, CT2::ErrorType>, ) { let coord_spec_1 = self.primary.drawing_area.into_coord_spec(); let coord_spec_2 = self.secondary.drawing_area.into_coord_spec(); diff --git a/plotters/src/coord/mod.rs b/plotters/src/coord/mod.rs index b01fc697..ea5e09e8 100644 --- a/plotters/src/coord/mod.rs +++ b/plotters/src/coord/mod.rs @@ -55,19 +55,26 @@ pub mod cartesian { mod translate; pub use translate::{CoordTranslate, ReverseCoordTranslate}; +use crate::math_errors::MathError; + /// The coordinate translation that only impose shift #[derive(Debug, Clone)] pub struct Shift(pub BackendCoord); impl CoordTranslate for Shift { type From = BackendCoord; - fn translate(&self, from: &Self::From) -> BackendCoord { - (from.0 + (self.0).0, from.1 + (self.0).1) + type ErrorType = MathError; + fn translate(&self, from: &Self::From) -> Result { + let x = from.0.checked_add((self.0).0).ok_or(MathError::ValueOverflow)?; + let y = from.1.checked_add((self.0).1).ok_or(MathError::ValueOverflow)?; + Ok((x,y)) } } impl ReverseCoordTranslate for Shift { - fn reverse_translate(&self, input: BackendCoord) -> Option { - Some((input.0 - (self.0).0, input.1 - (self.0).1)) + fn reverse_translate(&self, input: BackendCoord) -> Result, Self::ErrorType> { + let x = input.0.checked_sub((self.0).0).ok_or(MathError::ValueUnderflow)?; + let y = input.1.checked_sub((self.0).1).ok_or(MathError::ValueUnderflow)?; + Ok(Some((x,y))) } } diff --git a/plotters/src/coord/ranged1d/combinators/logarithmic.rs b/plotters/src/coord/ranged1d/combinators/logarithmic.rs index 14ac5f82..8380d8bc 100644 --- a/plotters/src/coord/ranged1d/combinators/logarithmic.rs +++ b/plotters/src/coord/ranged1d/combinators/logarithmic.rs @@ -2,7 +2,7 @@ use crate::coord::ranged1d::types::RangedCoordf64; use crate::coord::ranged1d::{ AsRangedCoord, DefaultFormatting, KeyPointHint, Ranged, ReversibleRanged, }; -use crate::errors::PlotError; +use crate::math_errors::MathError; use std::marker::PhantomData; use std::ops::Range; @@ -199,7 +199,7 @@ impl LogCoord { impl Ranged for LogCoord { type FormatOption = DefaultFormatting; type ValueType = V; - type ErrorType = PlotError; + type ErrorType = MathError; fn map(&self, value: &V, limit: (i32, i32)) -> Result { let fv = self.value_to_f64(value); let value_ln = fv.ln(); diff --git a/plotters/src/coord/ranged1d/combinators/nested.rs b/plotters/src/coord/ranged1d/combinators/nested.rs index fec68dfc..4dd170eb 100644 --- a/plotters/src/coord/ranged1d/combinators/nested.rs +++ b/plotters/src/coord/ranged1d/combinators/nested.rs @@ -1,7 +1,7 @@ use crate::coord::ranged1d::{ AsRangedCoord, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, ValueFormatter, }; -use crate::errors::PlotError; +use crate::math_errors::MathError; use std::ops::Range; /// Describe a value for a nested coordinate @@ -67,10 +67,10 @@ where } } -impl> Ranged for NestedRange { +impl> Ranged for NestedRange { type FormatOption = NoDefaultFormatting; type ValueType = NestedValue; - type ErrorType = PlotError; + type ErrorType = MathError; fn range(&self) -> Range { let primary_range = self.primary.range(); @@ -100,8 +100,8 @@ impl> Ranged for NestedRange } else { let sum = s_left .checked_add(s_right) - .ok_or(PlotError::ValueOverflow)?; - let result = sum.checked_div(2).ok_or(PlotError::ValueUnderflow)?; + .ok_or(MathError::ValueOverflow)?; + let result = sum.checked_div(2).ok_or(MathError::ValueUnderflow)?; Ok(result) } } @@ -134,7 +134,7 @@ impl> Ranged for NestedRange } } -impl> DiscreteRanged +impl> DiscreteRanged for NestedRange { fn size(&self) -> usize { diff --git a/plotters/src/coord/ranged1d/combinators/partial_axis.rs b/plotters/src/coord/ranged1d/combinators/partial_axis.rs index febfcb38..af4e546e 100644 --- a/plotters/src/coord/ranged1d/combinators/partial_axis.rs +++ b/plotters/src/coord/ranged1d/combinators/partial_axis.rs @@ -1,7 +1,7 @@ use crate::coord::ranged1d::{ AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, Ranged, }; -use crate::errors::PlotError; +use crate::math_errors::MathError; use std::ops::Range; /// This axis decorator will make the axis partially display on the axis. @@ -26,13 +26,13 @@ pub trait IntoPartialAxis: AsRangedCoord { impl IntoPartialAxis for R {} -impl> Ranged for PartialAxis +impl> Ranged for PartialAxis where R::ValueType: Clone, { type FormatOption = DefaultFormatting; type ValueType = R::ValueType; - type ErrorType = PlotError; + type ErrorType = MathError; fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { self.0.map(value, limit) @@ -56,7 +56,7 @@ where impl DiscreteRanged for PartialAxis where - R: Ranged, + R: Ranged, ::ValueType: Eq + Clone, { fn size(&self) -> usize { @@ -110,6 +110,6 @@ mod test { let r = make_partial_axis(20..80, 0.2..0.8).unwrap(); assert_eq!(r.size(), 101); assert_eq!(r.range(), 0..100); - assert_eq!(r.axis_pixel_range((0, 100)), 20..80); + assert_eq!(r.axis_pixel_range((0, 100)), Ok(20..80)); } } diff --git a/plotters/src/coord/ranged1d/discrete.rs b/plotters/src/coord/ranged1d/discrete.rs index daf06f51..66a64347 100644 --- a/plotters/src/coord/ranged1d/discrete.rs +++ b/plotters/src/coord/ranged1d/discrete.rs @@ -1,7 +1,7 @@ use crate::coord::ranged1d::{ AsRangedCoord, KeyPointHint, NoDefaultFormatting, Ranged, ReversibleRanged, ValueFormatter, }; -use crate::errors::PlotError; +use crate::math_errors::MathError; use crate::math_guard::{float_to_integer_checked, non_zero_checked}; use std::ops::Range; @@ -140,21 +140,21 @@ where impl Ranged for SegmentedCoord where - PlotError: From<::ErrorType>, + MathError: From<::ErrorType>, { type FormatOption = NoDefaultFormatting; type ValueType = SegmentValue; - type ErrorType = PlotError; + type ErrorType = MathError; fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { let pixel_span = (i64::from(limit.1) - i64::from(limit.0)) as f64; let size = - non_zero_checked::(self.0.size(), PlotError::ZeroDivision)? as f64; - let margin = float_to_integer_checked::( + non_zero_checked::(self.0.size(), MathError::ZeroDivision)? as f64; + let margin = float_to_integer_checked::( (pixel_span / size).round(), - PlotError::ValueOutOfRange, + MathError::ValueOutOfRange, )?; - let upper = limit.1.checked_sub(margin).ok_or(PlotError::ValueUnderflow)?; + let upper = limit.1.checked_sub(margin).ok_or(MathError::ValueUnderflow)?; match value { SegmentValue::Exact(coord) => Ok(self.0.map( coord, @@ -172,12 +172,12 @@ where ), )?; if let Some(idx) = self.0.index_of(coord) { - if idx.checked_add(1).ok_or(PlotError::ValueOverflow)? < self.0.size() { + if idx.checked_add(1).ok_or(MathError::ValueOverflow)? < self.0.size() { let right = self.0.map( &self .0 - .from_index(idx.checked_add(1).ok_or(PlotError::ValueOverflow)?) - .ok_or(PlotError::ValueOutOfRange)?, + .from_index(idx.checked_add(1).ok_or(MathError::ValueOverflow)?) + .ok_or(MathError::ValueOutOfRange)?, ( limit.0, upper, @@ -210,7 +210,7 @@ where impl DiscreteRanged for SegmentedCoord where - PlotError: From<::ErrorType>, + MathError: From<::ErrorType>, { fn size(&self) -> usize { self.0.size() + 1 diff --git a/plotters/src/coord/ranged1d/types/datetime.rs b/plotters/src/coord/ranged1d/types/datetime.rs index b220b937..649004f8 100644 --- a/plotters/src/coord/ranged1d/types/datetime.rs +++ b/plotters/src/coord/ranged1d/types/datetime.rs @@ -2,7 +2,7 @@ use chrono::{Date, DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, TimeZone, Timelike}; use std::ops::{Add, Range, Sub}; -use crate::errors::PlotError; +use crate::math_errors::MathError; use crate::{ coord::ranged1d::{ AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, @@ -37,7 +37,7 @@ pub trait TimeValue: Eq + Sized { begin: &Self, end: &Self, limit: (i32, i32), - ) -> Result { + ) -> Result { let total_span = end.subtract(begin); let value_span = value.subtract(begin); // Calculate the pixel span cast into i64 to avoid under flowing. @@ -47,13 +47,13 @@ pub trait TimeValue: Eq + Sized { if let Some(value_ns) = value_span.num_nanoseconds() { let value_ns = value_ns as f64; let total_ns = - non_zero_checked::(total_ns, PlotError::ZeroDivision)? as f64; + non_zero_checked::(total_ns, MathError::ZeroDivision)? as f64; - let result = float_to_integer_checked::( + let result = float_to_integer_checked::( pixel_span * value_ns / total_ns, - PlotError::ValueOutOfRange, + MathError::ValueOutOfRange, )?; - return result.checked_add(limit.0).ok_or(PlotError::ValueOverflow); + return result.checked_add(limit.0).ok_or(MathError::ValueOverflow); } } @@ -61,14 +61,14 @@ pub trait TimeValue: Eq + Sized { // If it overflows, it means we have a time span nearly 300 years, we are safe to ignore the // portion less than 1 day. let total_days = - non_zero_checked::(total_span.num_days(), PlotError::ZeroDivision)? + non_zero_checked::(total_span.num_days(), MathError::ZeroDivision)? as f64; let value_days = value_span.num_days() as f64; - let result = float_to_integer_checked::( + let result = float_to_integer_checked::( pixel_span * value_days / total_days, - PlotError::ValueOutOfRange, + MathError::ValueOutOfRange, )?; - result.checked_add(limit.0).ok_or(PlotError::ValueOverflow) + result.checked_add(limit.0).ok_or(MathError::ValueOverflow) } /// Map pixel to coord spec @@ -234,13 +234,13 @@ where { type FormatOption = DefaultFormatting; type ValueType = D; - type ErrorType = PlotError; + type ErrorType = MathError; fn range(&self) -> Range { self.0.clone()..self.1.clone() } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { TimeValue::map_coord(value, &self.0, &self.1, limit) } @@ -425,7 +425,7 @@ where { type FormatOption = NoDefaultFormatting; type ValueType = T; - type ErrorType = PlotError; + type ErrorType = MathError; fn range(&self) -> Range { self.0.start.clone()..self.0.end.clone() @@ -553,7 +553,7 @@ where { type FormatOption = NoDefaultFormatting; type ValueType = T; - type ErrorType = PlotError; + type ErrorType = MathError; fn range(&self) -> Range { self.0.start.clone()..self.0.end.clone() @@ -681,7 +681,7 @@ where { type FormatOption = DefaultFormatting; type ValueType = DT; - type ErrorType = PlotError; + type ErrorType = MathError; fn range(&self) -> Range
{ self.0.clone()..self.1.clone() @@ -767,7 +767,7 @@ impl From> for RangedDuration { impl Ranged for RangedDuration { type FormatOption = DefaultFormatting; type ValueType = Duration; - type ErrorType = PlotError; + type ErrorType = MathError; fn range(&self) -> Range { self.0..self.1 @@ -777,10 +777,10 @@ impl Ranged for RangedDuration { let total_span = self .1 .checked_sub(&self.0) - .ok_or(PlotError::ValueUnderflow)?; + .ok_or(MathError::ValueUnderflow)?; let value_span = value .checked_sub(&self.0) - .ok_or(PlotError::ValueUnderflow)?; + .ok_or(MathError::ValueUnderflow)?; let limit_difference = (i64::from(limit.1) - i64::from(limit.0)) as f64; let offset = 1e-10; @@ -789,26 +789,26 @@ impl Ranged for RangedDuration { if let Some(value_ns) = value_span.num_nanoseconds() { let value_ns = value_ns as f64; let total_ns = - non_zero_checked::(total_ns, PlotError::ZeroDivision)? as f64; - let result = float_to_integer_checked::( + non_zero_checked::(total_ns, MathError::ZeroDivision)? as f64; + let result = float_to_integer_checked::( limit_difference * value_ns / total_ns + offset, - PlotError::ValueOutOfRange, + MathError::ValueOutOfRange, )?; - return limit.0.checked_add(result).ok_or(PlotError::ValueOverflow); + return limit.0.checked_add(result).ok_or(MathError::ValueOverflow); } return Ok(limit.1); } let value_days = value_span.num_days() as f64; let total_days = - non_zero_checked::(total_span.num_days(), PlotError::ZeroDivision)? + non_zero_checked::(total_span.num_days(), MathError::ZeroDivision)? as f64; - let result = float_to_integer_checked::( + let result = float_to_integer_checked::( limit_difference * value_days / total_days + offset, - PlotError::ValueOutOfRange, + MathError::ValueOutOfRange, )?; - limit.0.checked_add(result).ok_or(PlotError::ValueOverflow) + limit.0.checked_add(result).ok_or(MathError::ValueOverflow) } fn key_points(&self, hint: HintType) -> Vec { @@ -1282,7 +1282,7 @@ mod test { let coord: RangedDateTime<_> = (start_time..end_time).into(); let pos = coord.map(&mid, (1000, 2000)); assert_eq!(pos, Ok(1500)); - let value: Result>, PlotError> = + let value: Result>, MathError> = coord.unmap(pos.unwrap(), (1000, 2000)); assert_eq!(value, Ok(Some(mid))); } diff --git a/plotters/src/coord/ranged1d/types/numeric.rs b/plotters/src/coord/ranged1d/types/numeric.rs index 1665cdb4..6b5c739f 100644 --- a/plotters/src/coord/ranged1d/types/numeric.rs +++ b/plotters/src/coord/ranged1d/types/numeric.rs @@ -1,7 +1,7 @@ use std::convert::TryFrom; use std::ops::Range; -use crate::errors::PlotError; +use crate::math_errors::MathError; use crate::{ coord::{ combinators::WithKeyPoints, @@ -52,7 +52,7 @@ macro_rules! impl_ranged_type_trait { macro_rules! impl_reverse_mapping_trait { ($type:ty, $name: ident) => { impl ReversibleRanged for $name { - fn unmap(&self, p: i32, (min, max): (i32, i32)) -> Result, PlotError> { + fn unmap(&self, p: i32, (min, max): (i32, i32)) -> Result, MathError> { if p < min.min(max) || p > max.max(min) || min == max { return Ok(None); } @@ -91,7 +91,7 @@ macro_rules! make_numeric_coord { let logic_length = (*v as f64 - self.0 as f64) / (self.1 as f64 - self.0 as f64); if !logic_length.is_finite() { - return Err(PlotError::NonFiniteCalculation); + return Err(MathError::NonFiniteCalculation); } let actual_length = (i64::from(limit.1) - i64::from(limit.0)) as f64; @@ -106,12 +106,12 @@ macro_rules! make_numeric_coord { (actual_length * logic_length - 1e-3).ceil() }; - let offset = float_to_integer_checked::( + let offset = float_to_integer_checked::( projected, - PlotError::ValueOutOfRange, + MathError::ValueOutOfRange, )?; - limit.0.checked_add(offset).ok_or(PlotError::ValueOverflow) + limit.0.checked_add(offset).ok_or(MathError::ValueOverflow) } fn key_points(&self, hint: Hint) -> Vec<$type> { $key_points((self.0, self.1), hint.max_num_points()) @@ -264,7 +264,7 @@ gen_key_points_comp!(integer, compute_usize_key_points, usize); make_numeric_coord!( f32, - PlotError, + MathError, RangedCoordf32, compute_f32_key_points, "The ranged coordinate for type f32", @@ -294,7 +294,7 @@ impl ValueFormatter for WithKeyPoints { make_numeric_coord!( f64, - PlotError, + MathError, RangedCoordf64, compute_f64_key_points, "The ranged coordinate for type f64", @@ -323,56 +323,56 @@ impl ValueFormatter for WithKeyPoints { } make_numeric_coord!( u32, - PlotError, + MathError, RangedCoordu32, compute_u32_key_points, "The ranged coordinate for type u32" ); make_numeric_coord!( i32, - PlotError, + MathError, RangedCoordi32, compute_i32_key_points, "The ranged coordinate for type i32" ); make_numeric_coord!( u64, - PlotError, + MathError, RangedCoordu64, compute_u64_key_points, "The ranged coordinate for type u64" ); make_numeric_coord!( i64, - PlotError, + MathError, RangedCoordi64, compute_i64_key_points, "The ranged coordinate for type i64" ); make_numeric_coord!( u128, - PlotError, + MathError, RangedCoordu128, compute_u128_key_points, "The ranged coordinate for type u128" ); make_numeric_coord!( i128, - PlotError, + MathError, RangedCoordi128, compute_i128_key_points, "The ranged coordinate for type i128" ); make_numeric_coord!( usize, - PlotError, + MathError, RangedCoordusize, compute_usize_key_points, "The ranged coordinate for type usize" ); make_numeric_coord!( isize, - PlotError, + MathError, RangedCoordisize, compute_isize_key_points, "The ranged coordinate for type isize" diff --git a/plotters/src/coord/ranged1d/types/slice.rs b/plotters/src/coord/ranged1d/types/slice.rs index 7bdf6294..3b854327 100644 --- a/plotters/src/coord/ranged1d/types/slice.rs +++ b/plotters/src/coord/ranged1d/types/slice.rs @@ -1,7 +1,7 @@ use crate::coord::ranged1d::{ AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, Ranged, }; -use crate::errors::PlotError; +use crate::math_errors::MathError; use crate::math_guard::{float_to_integer_checked, non_zero_checked}; use std::ops::Range; @@ -14,13 +14,13 @@ pub struct RangedSlice<'a, T: PartialEq>(&'a [T]); impl<'a, T: PartialEq> Ranged for RangedSlice<'a, T> { type FormatOption = DefaultFormatting; type ValueType = &'a T; - type ErrorType = PlotError; + type ErrorType = MathError; fn range(&self) -> Range<&'a T> { // If inner slice is empty, we should always panic &self.0[0]..&self.0[self.0.len() - 1] } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { match self.0.iter().position(|x| &x == value) { Some(pos) => { let pixel_span = (i64::from(limit.1) - i64::from(limit.0)) as f64; @@ -28,18 +28,18 @@ impl<'a, T: PartialEq> Ranged for RangedSlice<'a, T> { .0 .len() .checked_sub(1) - .ok_or(PlotError::ValueUnderflow)?; + .ok_or(MathError::ValueUnderflow)?; let value_span = - non_zero_checked::(value_span, PlotError::ZeroDivision)? + non_zero_checked::(value_span, MathError::ZeroDivision)? as f64; - let offset = float_to_integer_checked::( + let offset = float_to_integer_checked::( pixel_span * (pos as f64 / value_span), - PlotError::ValueOutOfRange, + MathError::ValueOutOfRange, )?; - limit.0.checked_add(offset).ok_or(PlotError::ValueOverflow) + limit.0.checked_add(offset).ok_or(MathError::ValueOverflow) } None => Ok(limit.0), } diff --git a/plotters/src/coord/ranged2d/cartesian.rs b/plotters/src/coord/ranged2d/cartesian.rs index 57d2240f..0378d93d 100644 --- a/plotters/src/coord/ranged2d/cartesian.rs +++ b/plotters/src/coord/ranged2d/cartesian.rs @@ -10,6 +10,7 @@ use crate::coord::ranged1d::{KeyPointHint, Ranged, ReversibleRanged}; use crate::coord::{CoordTranslate, ReverseCoordTranslate}; +use crate::math_errors::MathError; use crate::style::ShapeStyle; use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind}; @@ -61,8 +62,8 @@ impl Cartesian2d { for logic_x in xkp { let x = self.logic_x.map(&logic_x, self.back_x); draw_mesh(MeshLine::XMesh( - (x, self.back_y.0), - (x, self.back_y.1), + (x?, self.back_y.0), + (x?, self.back_y.1), &logic_x, ))?; } @@ -70,8 +71,8 @@ impl Cartesian2d { for logic_y in ykp { let y = self.logic_y.map(&logic_y, self.back_y); draw_mesh(MeshLine::YMesh( - (self.back_x.0, y), - (self.back_x.1, y), + (self.back_x.0, y?), + (self.back_x.1, y?), &logic_y, ))?; } @@ -90,12 +91,12 @@ impl Cartesian2d { } /// Get the horizental backend coordinate range where X axis should be drawn - pub fn get_x_axis_pixel_range(&self) -> Range { + pub fn get_x_axis_pixel_range(&self) -> Result, ::ErrorType> { self.logic_x.axis_pixel_range(self.back_x) } /// Get the vertical backend coordinate range where Y axis should be drawn - pub fn get_y_axis_pixel_range(&self) -> Range { + pub fn get_y_axis_pixel_range(&self) -> Result, ::ErrorType> { self.logic_y.axis_pixel_range(self.back_y) } @@ -112,20 +113,19 @@ impl Cartesian2d { impl CoordTranslate for Cartesian2d { type From = (X::ValueType, Y::ValueType); - - fn translate(&self, from: &Self::From) -> BackendCoord { - ( - self.logic_x.map(&from.0, self.back_x), - self.logic_y.map(&from.1, self.back_y), - ) + type ErrorType = MathError; + fn translate(&self, from: &Self::From) -> Result { + let x = self.logic_x.map(&from.0, self.back_x)?; + let y = self.logic_y.map(&from.1, self.back_y)?; + Ok((x,y)) } } impl ReverseCoordTranslate for Cartesian2d { - fn reverse_translate(&self, input: BackendCoord) -> Option { + fn reverse_translate(&self, input: BackendCoord) -> Result, Self::ErrorType> { Some(( - self.logic_x.unmap(input.0, self.back_x)?, - self.logic_y.unmap(input.1, self.back_y)?, + self.logic_x.unmap(input.0, self.back_x)??, + self.logic_y.unmap(input.1, self.back_y)??, )) } } diff --git a/plotters/src/coord/ranged3d/cartesian3d.rs b/plotters/src/coord/ranged3d/cartesian3d.rs index 8719680c..c4876ee6 100644 --- a/plotters/src/coord/ranged3d/cartesian3d.rs +++ b/plotters/src/coord/ranged3d/cartesian3d.rs @@ -1,6 +1,7 @@ use super::{ProjectionMatrix, ProjectionMatrixBuilder}; use crate::coord::ranged1d::Ranged; use crate::coord::CoordTranslate; +use crate::math_errors::MathError; use plotters_backend::BackendCoord; use std::ops::Range; @@ -120,9 +121,10 @@ impl Cartesian3d { impl CoordTranslate for Cartesian3d { type From = (X::ValueType, Y::ValueType, Z::ValueType); - fn translate(&self, coord: &Self::From) -> BackendCoord { + type ErrorType = MathError; + fn translate(&self, coord: &Self::From) -> Result { let pixel_coord_3d = self.map_3d(&coord.0, &coord.1, &coord.2); - self.projection * pixel_coord_3d + Ok(self.projection * pixel_coord_3d) } fn depth(&self, coord: &Self::From) -> i32 { diff --git a/plotters/src/coord/translate.rs b/plotters/src/coord/translate.rs index 222f948a..7cac7cd3 100644 --- a/plotters/src/coord/translate.rs +++ b/plotters/src/coord/translate.rs @@ -5,9 +5,11 @@ use std::ops::Deref; pub trait CoordTranslate { /// Specifies the object to be translated from type From; + /// Specifies the error type to use + type ErrorType; /// Translate the guest coordinate to the guest coordinate - fn translate(&self, from: &Self::From) -> BackendCoord; + fn translate(&self, from: &Self::From) -> Result; /// Get the Z-value of current coordinate fn depth(&self, _from: &Self::From) -> i32 { @@ -21,7 +23,8 @@ where T: Deref, { type From = C::From; - fn translate(&self, from: &Self::From) -> BackendCoord { + type ErrorType = C::ErrorType; + fn translate(&self, from: &Self::From) -> Result { self.deref().translate(from) } } @@ -34,5 +37,5 @@ pub trait ReverseCoordTranslate: CoordTranslate { /// logic coordinate. /// Note: the return value is an option, because it's possible that the drawing /// coordinate isn't able to be represented in te guest coordinate system - fn reverse_translate(&self, input: BackendCoord) -> Option; + fn reverse_translate(&self, input: BackendCoord) -> Result< Option, Self::ErrorType>; } diff --git a/plotters/src/drawing/area.rs b/plotters/src/drawing/area.rs index e8981fea..27dce6eb 100644 --- a/plotters/src/drawing/area.rs +++ b/plotters/src/drawing/area.rs @@ -2,6 +2,7 @@ use crate::coord::cartesian::{Cartesian2d, MeshLine}; use crate::coord::ranged1d::{KeyPointHint, Ranged}; use crate::coord::{CoordTranslate, Shift}; use crate::element::{CoordMapper, Drawable, PointCollection}; +use crate::math_errors::MathError; use crate::style::text_anchor::{HPos, Pos, VPos}; use crate::style::{Color, SizeDesc, TextStyle}; @@ -143,6 +144,8 @@ pub enum DrawingAreaErrorKind { SharingError, /// The error caused by invalid layout LayoutError, + /// The error is due math or an invalid value + Math(MathError) } impl std::fmt::Display for DrawingAreaErrorKind { @@ -153,10 +156,17 @@ impl std::fmt::Display for DrawingAreaErrorKind { write!(fmt, "Multiple backend operation in progress") } DrawingAreaErrorKind::LayoutError => write!(fmt, "Bad layout"), + DrawingAreaErrorKind::Math(math_error) => write!(fmt, "math error: {}", math_error), } } } +impl From for DrawingAreaErrorKind { + fn from(err: MathError) -> Self { + DrawingAreaErrorKind::Math(err) + } +} + impl Error for DrawingAreaErrorKind {} #[allow(type_alias_bounds)] diff --git a/plotters/src/lib.rs b/plotters/src/lib.rs index 1cf0006f..5c878388 100644 --- a/plotters/src/lib.rs +++ b/plotters/src/lib.rs @@ -799,7 +799,7 @@ pub mod series; pub mod style; /// Error handling for crate -pub(crate) mod errors; +pub(crate) mod math_errors; /// used within crate for ensuring math done doesn't cause unexpected behavior (overflow/underflow etc) pub(crate) mod math_guard; diff --git a/plotters/src/errors.rs b/plotters/src/math_errors.rs similarity index 68% rename from plotters/src/errors.rs rename to plotters/src/math_errors.rs index 59f269e0..0766cf2f 100644 --- a/plotters/src/errors.rs +++ b/plotters/src/math_errors.rs @@ -1,7 +1,7 @@ use core::fmt; #[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum PlotError { +pub enum MathError { ValueOverflow, ValueUnderflow, NonFiniteCalculation, @@ -9,26 +9,26 @@ pub enum PlotError { ZeroDivision, } -impl fmt::Display for PlotError { +impl fmt::Display for MathError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - PlotError::ValueOverflow => { + MathError::ValueOverflow => { write!(f, "value exceeds the target type's maximum") } - PlotError::ValueUnderflow => { + MathError::ValueUnderflow => { write!(f, "value is below the target type's minimum") } - PlotError::NonFiniteCalculation => { + MathError::NonFiniteCalculation => { write!(f, "calculation produced a non-finite value") } - PlotError::ValueOutOfRange => { + MathError::ValueOutOfRange => { write!(f, "value is out of range for the target type") } - PlotError::ZeroDivision => { + MathError::ZeroDivision => { write!(f, "attempted to divide by zero") } } } } -impl std::error::Error for PlotError {} +impl std::error::Error for MathError {} From 1912604336f625aa20275104c9818907768eb4bf Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Tue, 28 Apr 2026 10:24:52 +0800 Subject: [PATCH 11/25] updated `ranged2d/cartesian` to work with new error type --- plotters/src/chart/context.rs | 4 +- .../chart/context/cartesian3d/draw_impl.rs | 54 +++++++++++++------ plotters/src/chart/dual_coord.rs | 4 +- plotters/src/coord/mod.rs | 29 +++++++--- plotters/src/coord/ranged1d/discrete.rs | 26 +++------ plotters/src/coord/ranged2d/cartesian.rs | 48 ++++++++--------- plotters/src/coord/translate.rs | 3 +- plotters/src/drawing/area.rs | 4 +- 8 files changed, 100 insertions(+), 72 deletions(-) diff --git a/plotters/src/chart/context.rs b/plotters/src/chart/context.rs index 4c366938..0065bff6 100644 --- a/plotters/src/chart/context.rs +++ b/plotters/src/chart/context.rs @@ -32,7 +32,9 @@ pub struct ChartContext<'a, DB: DrawingBackend, CT: CoordTranslate> { impl<'a, DB: DrawingBackend, CT: ReverseCoordTranslate> ChartContext<'a, DB, CT> { /// Convert the chart context into an closure that can be used for coordinate translation - pub fn into_coord_trans(self) -> impl Fn(BackendCoord) -> Result, CT::ErrorType>{ + pub fn into_coord_trans( + self, + ) -> impl Fn(BackendCoord) -> Result, CT::ErrorType> { let coord_spec = self.drawing_area.into_coord_spec(); move |coord| coord_spec.reverse_translate(coord) } diff --git a/plotters/src/chart/context/cartesian3d/draw_impl.rs b/plotters/src/chart/context/cartesian3d/draw_impl.rs index bbb837eb..e028a809 100644 --- a/plotters/src/chart/context/cartesian3d/draw_impl.rs +++ b/plotters/src/chart/context/cartesian3d/draw_impl.rs @@ -10,6 +10,7 @@ use crate::coord::{ }; use crate::drawing::DrawingAreaErrorKind; use crate::element::{EmptyElement, PathElement, Polygon, Text}; +use crate::math_errors::MathError; use crate::style::{ text_anchor::{HPos, Pos, VPos}, ShapeStyle, TextStyle, @@ -74,13 +75,13 @@ where let x_mid = (x_range.start + x_range.end) / 2; let y_mid = (y_range.start + y_range.end) / 2; - let x_dir = if begin.0 < x_mid { + let x_dir = if begin?.0 < x_mid { (-tick_size, 0) } else { (tick_size, 0) }; - let y_dir = if begin.1 < y_mid { + let y_dir = if begin?.1 < y_mid { (0, -tick_size) } else { (0, tick_size) @@ -126,6 +127,10 @@ where [[Coord3D; 3]; 2], DrawingAreaErrorKind, > { + if idx >= 3 { + return Err(DrawingAreaErrorKind::LayoutError); + } + let coord = self.plotting_area().as_coord_spec(); let x_range = coord.logic_x.range(); let y_range = coord.logic_y.range(); @@ -141,7 +146,7 @@ where let mut start = [&ranges[0][0], &ranges[1][0], &ranges[2][0]]; let mut end = [&ranges[0][1], &ranges[1][1], &ranges[2][1]]; - let mut plan = vec![]; + let mut best = None; for i in 0..3 { if i == idx { @@ -149,33 +154,48 @@ where } start[i] = &panels[i][0][i]; end[i] = &panels[i][0][i]; + for j in 0..3 { if i != idx && i != j && j != idx { for k in 0..2 { - start[j] = &panels[i][k][j]; + start[j] = &panels[i][k][i]; end[j] = &panels[i][k][j]; - plan.push((start, end)); + + let d1 = coord.projected_depth( + start[0].get_x(), + start[1].get_y(), + start[2].get_z(), + ); + let d2 = coord.projected_depth( + end[0].get_x(), + end[1].get_y(), + end[2].get_z(), + ); + + let depth_score = d1.checked_add(d2).ok_or(MathError::ValueOverflow)?; + let (_, y1) = coord.translate(&Coord3D::build_coord(start))?; + let (_, y2) = coord.translate(&Coord3D::build_coord(end))?; + let y_score = y1.checked_add(y2).ok_or(MathError::ValueOverflow)?; + let candidate = (start, end); + let score = (depth_score, y_score); + match best { + None => best = Some((candidate, score)), + Some((_, best_score)) if score < best_score => { + best = Some((candidate, score)) + } + _ => {} + } } } } } - plan.into_iter() - .min_by_key(|&(s, e)| { - let d = coord.projected_depth(s[0].get_x(), s[1].get_y(), s[2].get_z()); - let d = d + coord.projected_depth(e[0].get_x(), e[1].get_y(), e[2].get_z()); - let (_, y1) = coord.translate(&Coord3D::build_coord(s)); - let (_, y2) = coord.translate(&Coord3D::build_coord(e)); - let y = y1 + y2; - (d, y) - }) - .unwrap() + best.map(|(candidate, _)| candidate) + .ok_or(DrawingAreaErrorKind::LayoutError)? }; - self.plotting_area().draw(&PathElement::new( vec![Coord3D::build_coord(start), Coord3D::build_coord(end)], style, ))?; - Ok([ [start[0].clone(), start[1].clone(), start[2].clone()], [end[0].clone(), end[1].clone(), end[2].clone()], diff --git a/plotters/src/chart/dual_coord.rs b/plotters/src/chart/dual_coord.rs index 491d9dc5..ac2799d2 100644 --- a/plotters/src/chart/dual_coord.rs +++ b/plotters/src/chart/dual_coord.rs @@ -142,7 +142,9 @@ impl DualCoordChartContext<'_, DB, CT1, CT2> { /// Convert the chart context into the secondary coordinate translation function - pub fn into_secondary_coord_trans(self) -> impl Fn(BackendCoord) -> Result, CT2::ErrorType> { + pub fn into_secondary_coord_trans( + self, + ) -> impl Fn(BackendCoord) -> Result, CT2::ErrorType> { let coord_spec = self.secondary.drawing_area.into_coord_spec(); move |coord| coord_spec.reverse_translate(coord) } diff --git a/plotters/src/coord/mod.rs b/plotters/src/coord/mod.rs index ea5e09e8..b89f9e96 100644 --- a/plotters/src/coord/mod.rs +++ b/plotters/src/coord/mod.rs @@ -65,16 +65,31 @@ impl CoordTranslate for Shift { type From = BackendCoord; type ErrorType = MathError; fn translate(&self, from: &Self::From) -> Result { - let x = from.0.checked_add((self.0).0).ok_or(MathError::ValueOverflow)?; - let y = from.1.checked_add((self.0).1).ok_or(MathError::ValueOverflow)?; - Ok((x,y)) + let x = from + .0 + .checked_add((self.0).0) + .ok_or(MathError::ValueOverflow)?; + let y = from + .1 + .checked_add((self.0).1) + .ok_or(MathError::ValueOverflow)?; + Ok((x, y)) } } impl ReverseCoordTranslate for Shift { - fn reverse_translate(&self, input: BackendCoord) -> Result, Self::ErrorType> { - let x = input.0.checked_sub((self.0).0).ok_or(MathError::ValueUnderflow)?; - let y = input.1.checked_sub((self.0).1).ok_or(MathError::ValueUnderflow)?; - Ok(Some((x,y))) + fn reverse_translate( + &self, + input: BackendCoord, + ) -> Result, Self::ErrorType> { + let x = input + .0 + .checked_sub((self.0).0) + .ok_or(MathError::ValueUnderflow)?; + let y = input + .1 + .checked_sub((self.0).1) + .ok_or(MathError::ValueUnderflow)?; + Ok(Some((x, y))) } } diff --git a/plotters/src/coord/ranged1d/discrete.rs b/plotters/src/coord/ranged1d/discrete.rs index 66a64347..8d4eb643 100644 --- a/plotters/src/coord/ranged1d/discrete.rs +++ b/plotters/src/coord/ranged1d/discrete.rs @@ -154,23 +154,14 @@ where (pixel_span / size).round(), MathError::ValueOutOfRange, )?; - let upper = limit.1.checked_sub(margin).ok_or(MathError::ValueUnderflow)?; + let upper = limit + .1 + .checked_sub(margin) + .ok_or(MathError::ValueUnderflow)?; match value { - SegmentValue::Exact(coord) => Ok(self.0.map( - coord, - ( - limit.0, - upper, - ), - )?), + SegmentValue::Exact(coord) => Ok(self.0.map(coord, (limit.0, upper))?), SegmentValue::CenterOf(coord) => { - let left = self.0.map( - coord, - ( - limit.0, - upper, - ), - )?; + let left = self.0.map(coord, (limit.0, upper))?; if let Some(idx) = self.0.index_of(coord) { if idx.checked_add(1).ok_or(MathError::ValueOverflow)? < self.0.size() { let right = self.0.map( @@ -178,10 +169,7 @@ where .0 .from_index(idx.checked_add(1).ok_or(MathError::ValueOverflow)?) .ok_or(MathError::ValueOutOfRange)?, - ( - limit.0, - upper, - ), + (limit.0, upper), )?; let mid = (i64::from(left) + i64::from(right)) / 2; return Ok(mid as i32); diff --git a/plotters/src/coord/ranged2d/cartesian.rs b/plotters/src/coord/ranged2d/cartesian.rs index 0378d93d..6d6e3598 100644 --- a/plotters/src/coord/ranged2d/cartesian.rs +++ b/plotters/src/coord/ranged2d/cartesian.rs @@ -53,28 +53,20 @@ impl Cartesian2d { h_limit: YH, v_limit: XH, mut draw_mesh: DrawMesh, - ) -> Result<(), E> { + ) -> Result<(), E> where E: From + From { let (xkp, ykp) = ( self.logic_x.key_points(v_limit), self.logic_y.key_points(h_limit), ); for logic_x in xkp { - let x = self.logic_x.map(&logic_x, self.back_x); - draw_mesh(MeshLine::XMesh( - (x?, self.back_y.0), - (x?, self.back_y.1), - &logic_x, - ))?; + let x = self.logic_x.map(&logic_x, self.back_x)?; + draw_mesh(MeshLine::XMesh((x, self.back_y.0),(x, self.back_x.1), &logic_x))?; } for logic_y in ykp { - let y = self.logic_y.map(&logic_y, self.back_y); - draw_mesh(MeshLine::YMesh( - (self.back_x.0, y?), - (self.back_x.1, y?), - &logic_y, - ))?; + let y = self.logic_y.map(&logic_y, self.back_y)?; + draw_mesh(MeshLine::YMesh((self.back_x.0, y), (self.back_x.1, y), &logic_y))?; } Ok(()) @@ -90,7 +82,7 @@ impl Cartesian2d { self.logic_y.range() } - /// Get the horizental backend coordinate range where X axis should be drawn + /// Get the horizontal backend coordinate range where X axis should be drawn pub fn get_x_axis_pixel_range(&self) -> Result, ::ErrorType> { self.logic_x.axis_pixel_range(self.back_x) } @@ -111,22 +103,30 @@ impl Cartesian2d { } } -impl CoordTranslate for Cartesian2d { +impl, Y: Ranged> CoordTranslate for Cartesian2d { type From = (X::ValueType, Y::ValueType); type ErrorType = MathError; fn translate(&self, from: &Self::From) -> Result { - let x = self.logic_x.map(&from.0, self.back_x)?; - let y = self.logic_y.map(&from.1, self.back_y)?; - Ok((x,y)) + let x = self.logic_x.map(&from.0, self.back_x)?; + let y = self.logic_y.map(&from.1, self.back_y)?; + Ok((x, y)) } } -impl ReverseCoordTranslate for Cartesian2d { - fn reverse_translate(&self, input: BackendCoord) -> Result, Self::ErrorType> { - Some(( - self.logic_x.unmap(input.0, self.back_x)??, - self.logic_y.unmap(input.1, self.back_y)??, - )) +impl, Y: ReversibleRanged> ReverseCoordTranslate for Cartesian2d { + fn reverse_translate( + &self, + input: BackendCoord, + ) -> Result, Self::ErrorType> { + let Some(x) = self.logic_x.unmap(input.0, self.back_x)? else { + return Ok(None); + }; + + let Some(y) = self.logic_y.unmap(input.1, self.back_y)? else { + return Ok(None); + }; + + Ok(Some((x, y))) } } diff --git a/plotters/src/coord/translate.rs b/plotters/src/coord/translate.rs index 7cac7cd3..b94d3b43 100644 --- a/plotters/src/coord/translate.rs +++ b/plotters/src/coord/translate.rs @@ -37,5 +37,6 @@ pub trait ReverseCoordTranslate: CoordTranslate { /// logic coordinate. /// Note: the return value is an option, because it's possible that the drawing /// coordinate isn't able to be represented in te guest coordinate system - fn reverse_translate(&self, input: BackendCoord) -> Result< Option, Self::ErrorType>; + fn reverse_translate(&self, input: BackendCoord) + -> Result, Self::ErrorType>; } diff --git a/plotters/src/drawing/area.rs b/plotters/src/drawing/area.rs index 27dce6eb..a70d4e10 100644 --- a/plotters/src/drawing/area.rs +++ b/plotters/src/drawing/area.rs @@ -145,7 +145,7 @@ pub enum DrawingAreaErrorKind { /// The error caused by invalid layout LayoutError, /// The error is due math or an invalid value - Math(MathError) + Math(MathError), } impl std::fmt::Display for DrawingAreaErrorKind { @@ -161,7 +161,7 @@ impl std::fmt::Display for DrawingAreaErrorKind { } } -impl From for DrawingAreaErrorKind { +impl From for DrawingAreaErrorKind { fn from(err: MathError) -> Self { DrawingAreaErrorKind::Math(err) } From 77eae2d0c40b66cccc5fd3225f0ddd761d4857e5 Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Tue, 28 Apr 2026 12:06:32 +0800 Subject: [PATCH 12/25] updated `cartesian3d` to work with new error type --- plotters/src/coord/ranged3d/cartesian3d.rs | 24 +++++++++++----------- plotters/src/drawing/area.rs | 8 ++++---- plotters/src/element/mod.rs | 11 +++++----- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/plotters/src/coord/ranged3d/cartesian3d.rs b/plotters/src/coord/ranged3d/cartesian3d.rs index c4876ee6..b4dd192b 100644 --- a/plotters/src/coord/ranged3d/cartesian3d.rs +++ b/plotters/src/coord/ranged3d/cartesian3d.rs @@ -16,7 +16,7 @@ pub struct Cartesian3d { projection: ProjectionMatrix, } -impl Cartesian3d { +impl, Y: Ranged, Z: Ranged> Cartesian3d { fn compute_default_size(actual_x: Range, actual_y: Range) -> i32 { (actual_x.end - actual_x.start).min(actual_y.end - actual_y.start) * 4 / 5 } @@ -105,17 +105,17 @@ impl Cartesian3d { } /// Do not project, only transform the guest coordinate system - pub fn map_3d(&self, x: &X::ValueType, y: &Y::ValueType, z: &Z::ValueType) -> (i32, i32, i32) { - ( - self.logic_x.map(x, (0, self.coord_size.0)), - self.logic_y.map(y, (0, self.coord_size.1)), - self.logic_z.map(z, (0, self.coord_size.2)), - ) + pub fn map_3d(&self, x: &X::ValueType, y: &Y::ValueType, z: &Z::ValueType) -> Result<(i32, i32, i32), MathError> { + Ok(( + self.logic_x.map(x, (0, self.coord_size.0))?, + self.logic_y.map(y, (0, self.coord_size.1))?, + self.logic_z.map(z, (0, self.coord_size.2))?, + )) } /// Get the depth of the projection - pub fn projected_depth(&self, x: &X::ValueType, y: &Y::ValueType, z: &Z::ValueType) -> i32 { - self.projection.projected_depth(self.map_3d(x, y, z)) + pub fn projected_depth(&self, x: &X::ValueType, y: &Y::ValueType, z: &Z::ValueType) -> Result { + Ok(self.projection.projected_depth(self.map_3d(x, y, z)?)) } } @@ -123,11 +123,11 @@ impl CoordTranslate for Cartesian3d { type From = (X::ValueType, Y::ValueType, Z::ValueType); type ErrorType = MathError; fn translate(&self, coord: &Self::From) -> Result { - let pixel_coord_3d = self.map_3d(&coord.0, &coord.1, &coord.2); - Ok(self.projection * pixel_coord_3d) + let pixel_coord_3d = self.map_3d(&coord.0, &coord.1, &coord.2)?; + Ok(&self.projection * pixel_coord_3d) } - fn depth(&self, coord: &Self::From) -> i32 { + fn depth(&self, coord: &Self::From) -> Result { self.projected_depth(&coord.0, &coord.1, &coord.2) } } diff --git a/plotters/src/drawing/area.rs b/plotters/src/drawing/area.rs index a70d4e10..25ee020d 100644 --- a/plotters/src/drawing/area.rs +++ b/plotters/src/drawing/area.rs @@ -224,12 +224,12 @@ impl DrawingArea } /// Get the range of X of the backend coordinate for current drawing area - pub fn get_x_axis_pixel_range(&self) -> Range { + pub fn get_x_axis_pixel_range(&self) -> Result, Self::ErrorType> { self.coord.get_x_axis_pixel_range() } /// Get the range of Y of the backend coordinate for current drawing area - pub fn get_y_axis_pixel_range(&self) -> Range { + pub fn get_y_axis_pixel_range(&self) -> Result, Self::ErrorType> { self.coord.get_y_axis_pixel_range() } } @@ -313,7 +313,7 @@ impl DrawingArea { pos: CT::From, color: &ColorType, ) -> Result<(), DrawingAreaError> { - let pos = self.coord.translate(&pos); + let pos = self.coord.translate(&pos)?; self.backend_ops(|b| b.draw_pixel(pos, color.to_backend_color())) } @@ -337,7 +337,7 @@ impl DrawingArea { } /// Map coordinate to the backend coordinate - pub fn map_coordinate(&self, coord: &CT::From) -> BackendCoord { + pub fn map_coordinate(&self, coord: &CT::From) -> Result { self.coord.translate(coord) } diff --git a/plotters/src/element/mod.rs b/plotters/src/element/mod.rs index 921b2c0f..90debb53 100644 --- a/plotters/src/element/mod.rs +++ b/plotters/src/element/mod.rs @@ -155,6 +155,7 @@ ![](https://plotters-rs.github.io/plotters-doc-data/element-3.png) */ use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind}; +use serde::de::Error; use std::borrow::Borrow; mod basic_shapes; @@ -268,8 +269,8 @@ pub struct BackendCoordOnly; impl CoordMapper for BackendCoordOnly { type Output = BackendCoord; - fn map(coord_trans: &CT, from: &CT::From, rect: &Rect) -> BackendCoord { - rect.truncate(coord_trans.translate(from)) + fn map(coord_trans: &CT, from: &CT::From, rect: &Rect) -> Result { + rect.truncate(coord_trans.translate(from)?)? } } @@ -286,9 +287,9 @@ impl CoordMapper for BackendCoordAndZ { coord_trans: &CT, from: &CT::From, rect: &Rect, - ) -> (BackendCoord, i32) { - let coord = rect.truncate(coord_trans.translate(from)); + ) -> Result<(BackendCoord, i32), Self:ErrorType> { + let coord = rect.truncate(coord_trans.translate(from)?); let z = coord_trans.depth(from); - (coord, z) + Ok((coord, z)) } } From 2f67a853a933a97383ddfd5aaee25a950e6b3312 Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Tue, 28 Apr 2026 14:15:31 +0800 Subject: [PATCH 13/25] continued updating generics for new error type --- plotters/src/chart/context/cartesian2d/mod.rs | 32 +++++++++++++++---- plotters/src/coord/ranged2d/cartesian.rs | 27 ++++++++++++---- plotters/src/coord/ranged3d/cartesian3d.rs | 21 ++++++++++-- plotters/src/element/mod.rs | 12 +++++-- plotters/src/series/histogram.rs | 7 ++-- 5 files changed, 79 insertions(+), 20 deletions(-) diff --git a/plotters/src/chart/context/cartesian2d/mod.rs b/plotters/src/chart/context/cartesian2d/mod.rs index fd1aef27..c59be660 100644 --- a/plotters/src/chart/context/cartesian2d/mod.rs +++ b/plotters/src/chart/context/cartesian2d/mod.rs @@ -9,14 +9,15 @@ use crate::coord::{ Shift, }; use crate::drawing::DrawingArea; +use crate::math_errors::MathError; mod draw_impl; impl<'a, DB, XT, YT, X, Y> ChartContext<'a, DB, Cartesian2d> where DB: DrawingBackend, - X: Ranged + ValueFormatter, - Y: Ranged + ValueFormatter, + X: Ranged + ValueFormatter, + Y: Ranged + ValueFormatter, { pub(crate) fn is_overlapping_drawing_area( &self, @@ -46,7 +47,13 @@ where } } -impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesian2d> { +impl< + 'a, + DB: DrawingBackend, + X: Ranged, + Y: Ranged, + > ChartContext<'a, DB, Cartesian2d> +{ /// Get the range of X axis pub fn x_range(&self) -> Range { self.drawing_area.get_x_range() @@ -59,12 +66,21 @@ impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesia /// Maps the coordinate to the backend coordinate. This is typically used /// with an interactive chart. - pub fn backend_coord(&self, coord: &(X::ValueType, Y::ValueType)) -> BackendCoord { + pub fn backend_coord( + &self, + coord: &(X::ValueType, Y::ValueType), + ) -> Result { self.drawing_area.map_coordinate(coord) } } -impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesian2d> { +impl< + 'a, + DB: DrawingBackend, + X: Ranged, + Y: Ranged, + > ChartContext<'a, DB, Cartesian2d> +{ /// Convert this chart context into a dual axis chart context and attach a second coordinate spec /// on the chart context. For more detailed information, see documentation for [struct DualCoordChartContext](struct.DualCoordChartContext.html) /// @@ -81,7 +97,11 @@ impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesia DB, Cartesian2d, Cartesian2d, - > { + > + where + SX::CoordDescType: Ranged, + SY::CoordDescType: Ranged, + { let mut pixel_range = self.drawing_area.get_pixel_range(); pixel_range.1 = pixel_range.1.end..pixel_range.1.start; diff --git a/plotters/src/coord/ranged2d/cartesian.rs b/plotters/src/coord/ranged2d/cartesian.rs index 6d6e3598..35778837 100644 --- a/plotters/src/coord/ranged2d/cartesian.rs +++ b/plotters/src/coord/ranged2d/cartesian.rs @@ -53,7 +53,10 @@ impl Cartesian2d { h_limit: YH, v_limit: XH, mut draw_mesh: DrawMesh, - ) -> Result<(), E> where E: From + From { + ) -> Result<(), E> + where + E: From + From, + { let (xkp, ykp) = ( self.logic_x.key_points(v_limit), self.logic_y.key_points(h_limit), @@ -61,12 +64,20 @@ impl Cartesian2d { for logic_x in xkp { let x = self.logic_x.map(&logic_x, self.back_x)?; - draw_mesh(MeshLine::XMesh((x, self.back_y.0),(x, self.back_x.1), &logic_x))?; + draw_mesh(MeshLine::XMesh( + (x, self.back_y.0), + (x, self.back_x.1), + &logic_x, + ))?; } for logic_y in ykp { - let y = self.logic_y.map(&logic_y, self.back_y)?; - draw_mesh(MeshLine::YMesh((self.back_x.0, y), (self.back_x.1, y), &logic_y))?; + let y = self.logic_y.map(&logic_y, self.back_y)?; + draw_mesh(MeshLine::YMesh( + (self.back_x.0, y), + (self.back_x.1, y), + &logic_y, + ))?; } Ok(()) @@ -103,7 +114,9 @@ impl Cartesian2d { } } -impl, Y: Ranged> CoordTranslate for Cartesian2d { +impl, Y: Ranged> CoordTranslate + for Cartesian2d +{ type From = (X::ValueType, Y::ValueType); type ErrorType = MathError; fn translate(&self, from: &Self::From) -> Result { @@ -113,7 +126,9 @@ impl, Y: Ranged> CoordTr } } -impl, Y: ReversibleRanged> ReverseCoordTranslate for Cartesian2d { +impl, Y: ReversibleRanged> + ReverseCoordTranslate for Cartesian2d +{ fn reverse_translate( &self, input: BackendCoord, diff --git a/plotters/src/coord/ranged3d/cartesian3d.rs b/plotters/src/coord/ranged3d/cartesian3d.rs index b4dd192b..3724b927 100644 --- a/plotters/src/coord/ranged3d/cartesian3d.rs +++ b/plotters/src/coord/ranged3d/cartesian3d.rs @@ -16,7 +16,12 @@ pub struct Cartesian3d { projection: ProjectionMatrix, } -impl, Y: Ranged, Z: Ranged> Cartesian3d { +impl< + X: Ranged, + Y: Ranged, + Z: Ranged, + > Cartesian3d +{ fn compute_default_size(actual_x: Range, actual_y: Range) -> i32 { (actual_x.end - actual_x.start).min(actual_y.end - actual_y.start) * 4 / 5 } @@ -105,7 +110,12 @@ impl, Y: Ranged, Z: Rang } /// Do not project, only transform the guest coordinate system - pub fn map_3d(&self, x: &X::ValueType, y: &Y::ValueType, z: &Z::ValueType) -> Result<(i32, i32, i32), MathError> { + pub fn map_3d( + &self, + x: &X::ValueType, + y: &Y::ValueType, + z: &Z::ValueType, + ) -> Result<(i32, i32, i32), MathError> { Ok(( self.logic_x.map(x, (0, self.coord_size.0))?, self.logic_y.map(y, (0, self.coord_size.1))?, @@ -114,7 +124,12 @@ impl, Y: Ranged, Z: Rang } /// Get the depth of the projection - pub fn projected_depth(&self, x: &X::ValueType, y: &Y::ValueType, z: &Z::ValueType) -> Result { + pub fn projected_depth( + &self, + x: &X::ValueType, + y: &Y::ValueType, + z: &Z::ValueType, + ) -> Result { Ok(self.projection.projected_depth(self.map_3d(x, y, z)?)) } } diff --git a/plotters/src/element/mod.rs b/plotters/src/element/mod.rs index 90debb53..63759c9a 100644 --- a/plotters/src/element/mod.rs +++ b/plotters/src/element/mod.rs @@ -205,6 +205,7 @@ pub use pie::Pie; use crate::coord::CoordTranslate; use crate::drawing::Rect; +use crate::math_errors::MathError; /// A type which is logically a collection of points, under any given coordinate system. /// Note: Ideally, a point collection trait should be any type of which coordinate elements can be @@ -269,8 +270,13 @@ pub struct BackendCoordOnly; impl CoordMapper for BackendCoordOnly { type Output = BackendCoord; - fn map(coord_trans: &CT, from: &CT::From, rect: &Rect) -> Result { - rect.truncate(coord_trans.translate(from)?)? + + fn map( + coord_trans: &CT, + from: &CT::From, + rect: &Rect, + ) -> Result { + rect.truncate(coord_trans.translate(from)?) } } @@ -287,7 +293,7 @@ impl CoordMapper for BackendCoordAndZ { coord_trans: &CT, from: &CT::From, rect: &Rect, - ) -> Result<(BackendCoord, i32), Self:ErrorType> { + ) -> Result<(BackendCoord, i32), Self: ErrorType> { let coord = rect.truncate(coord_trans.translate(from)?); let z = coord_trans.depth(from); Ok((coord, z)) diff --git a/plotters/src/series/histogram.rs b/plotters/src/series/histogram.rs index 2574727f..730f6e17 100644 --- a/plotters/src/series/histogram.rs +++ b/plotters/src/series/histogram.rs @@ -6,6 +6,7 @@ use crate::chart::ChartContext; use crate::coord::cartesian::Cartesian2d; use crate::coord::ranged1d::{DiscreteRanged, Ranged}; use crate::element::Rectangle; +use crate::math_errors::MathError; use crate::style::{Color, ShapeStyle, GREEN}; use plotters_backend::DrawingBackend; @@ -202,7 +203,8 @@ where parent: &ChartContext>, ) -> Self where - ACoord: Ranged, + BR: Ranged, + ACoord: Ranged, { let dp = parent.as_coord_spec().x_spec(); @@ -224,7 +226,8 @@ where parent: &ChartContext>, ) -> Self where - ACoord: Ranged, + BR: Ranged, + ACoord: Ranged, { let dp = parent.as_coord_spec().y_spec(); Self::empty(dp) From 91e558189287d0646cfa7e8fb6d901ae8c4ff18b Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Wed, 29 Apr 2026 12:06:13 +0800 Subject: [PATCH 14/25] Keeping math_errors and math_guard but reverting all implementation --- clippy.txt | 114 ------------- plotters-svg/src/svg.rs | 7 +- plotters/src/chart/context.rs | 4 +- .../chart/context/cartesian2d/draw_impl.rs | 22 ++- plotters/src/chart/context/cartesian2d/mod.rs | 32 +--- .../chart/context/cartesian3d/draw_impl.rs | 56 ++----- plotters/src/chart/dual_coord.rs | 8 +- plotters/src/chart/series.rs | 2 +- plotters/src/coord/mod.rs | 30 +--- .../src/coord/ranged1d/combinators/ckps.rs | 18 +- .../coord/ranged1d/combinators/group_by.rs | 3 +- .../coord/ranged1d/combinators/linspace.rs | 4 +- .../coord/ranged1d/combinators/logarithmic.rs | 15 +- .../src/coord/ranged1d/combinators/nested.rs | 23 +-- .../ranged1d/combinators/partial_axis.rs | 18 +- plotters/src/coord/ranged1d/discrete.rs | 65 +++----- plotters/src/coord/ranged1d/mod.rs | 17 +- plotters/src/coord/ranged1d/types/datetime.rs | 155 ++++++------------ plotters/src/coord/ranged1d/types/numeric.rs | 84 ++++------ plotters/src/coord/ranged1d/types/slice.rs | 33 ++-- plotters/src/coord/ranged2d/cartesian.rs | 55 +++---- plotters/src/coord/ranged3d/cartesian3d.rs | 43 ++--- plotters/src/coord/translate.rs | 10 +- plotters/src/drawing/area.rs | 18 +- plotters/src/element/mod.rs | 17 +- plotters/src/element/text.rs | 2 +- plotters/src/evcxr.rs | 19 +-- plotters/src/lib.rs | 6 - plotters/src/series/histogram.rs | 7 +- plotters/src/style/color.rs | 22 +-- plotters/src/style/font/ab_glyph.rs | 27 +-- plotters/src/style/font/font_desc.rs | 6 +- plotters/src/style/font/ttf.rs | 6 +- 33 files changed, 281 insertions(+), 667 deletions(-) delete mode 100644 clippy.txt diff --git a/clippy.txt b/clippy.txt deleted file mode 100644 index bcfef20f..00000000 --- a/clippy.txt +++ /dev/null @@ -1,114 +0,0 @@ -warning: /home/alex/Projects/plotters/plotters/Cargo.toml: unused manifest key: package.msrv - Checking plotters v0.3.7 (/home/alex/Projects/plotters/plotters) -error: unused import: `core::fmt` - --> plotters/src/coord/ranged1d/types/datetime.rs:3:5 - | -3 | use core::fmt; - | ^^^^^^^^^ - | - = note: `-D unused-imports` implied by `-D warnings` - = help: to override `-D warnings` add `#[allow(unused_imports)]` - -error: unused import: `fmt::Display` - --> plotters/src/coord/ranged1d/types/datetime.rs:4:11 - | -4 | use std::{fmt::Display, ops::{Add, Range, Sub}}; - | ^^^^^^^^^^^^ - -error[E0277]: `coord::ranged1d::types::datetime::TimeError` doesn't implement `std::fmt::Debug` - --> plotters/src/coord/ranged1d/types/datetime.rs:30:28 - | -30 | impl std::error::Error for TimeError {} - | ^^^^^^^^^ the trait `std::fmt::Debug` is not implemented for `coord::ranged1d::types::datetime::TimeError` - | - = note: add `#[derive(Debug)]` to `coord::ranged1d::types::datetime::TimeError` or manually `impl std::fmt::Debug for coord::ranged1d::types::datetime::TimeError` -note: required by a bound in `std::error::Error` - --> /home/alex/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/error.rs:59:18 - | -59 | pub trait Error: Debug + Display { - | ^^^^^ required by this bound in `Error` -help: consider annotating `coord::ranged1d::types::datetime::TimeError` with `#[derive(Debug)]` - | -12 + #[derive(Debug)] -13 | enum TimeError { - | - -error[E0308]: mismatched types - --> plotters/src/coord/ranged1d/types/datetime.rs:72:9 - | -53 | fn map_coord(value: &Self, begin: &Self, end: &Self, limit: (i32, i32)) -> Result{ - | ---------------------- expected `std::result::Result` because of return type -... -72 | (f64::from(limit.1 - limit.0) * value_days / total_days) as i32 + limit.0 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Result`, found `i32` - | - = note: expected enum `std::result::Result` - found type `i32` -help: try wrapping the expression in `Ok` - | -72 | Ok((f64::from(limit.1 - limit.0) * value_days / total_days) as i32 + limit.0) - | +++ + - -error[E0308]: mismatched types - --> plotters/src/coord/ranged1d/types/datetime.rs:244:9 - | -243 | fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { - | --- expected `i32` because of return type -244 | TimeValue::map_coord(value, &self.0, &self.1, limit) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `i32`, found `Result` - | - = note: expected type `i32` - found enum `std::result::Result` -help: consider using `Result::expect` to unwrap the `std::result::Result` value, panicking if the value is a `Result::Err` - | -244 | TimeValue::map_coord(value, &self.0, &self.1, limit).expect("REASON") - | +++++++++++++++++ - -error[E0308]: mismatched types - --> plotters/src/coord/ranged1d/types/datetime.rs:434:9 - | -433 | fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { - | --- expected `i32` because of return type -434 | T::map_coord(value, &self.0.start, &self.0.end, limit) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `i32`, found `Result` - | - = note: expected type `i32` - found enum `std::result::Result` -help: consider using `Result::expect` to unwrap the `std::result::Result` value, panicking if the value is a `Result::Err` - | -434 | T::map_coord(value, &self.0.start, &self.0.end, limit).expect("REASON") - | +++++++++++++++++ - -error[E0308]: mismatched types - --> plotters/src/coord/ranged1d/types/datetime.rs:561:9 - | -560 | fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { - | --- expected `i32` because of return type -561 | T::map_coord(value, &self.0.start, &self.0.end, limit) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `i32`, found `Result` - | - = note: expected type `i32` - found enum `std::result::Result` -help: consider using `Result::expect` to unwrap the `std::result::Result` value, panicking if the value is a `Result::Err` - | -561 | T::map_coord(value, &self.0.start, &self.0.end, limit).expect("REASON") - | +++++++++++++++++ - -error[E0308]: mismatched types - --> plotters/src/coord/ranged1d/types/datetime.rs:688:9 - | -687 | fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { - | --- expected `i32` because of return type -688 | TimeValue::map_coord(value, &self.0, &self.1, limit) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `i32`, found `Result` - | - = note: expected type `i32` - found enum `std::result::Result` -help: consider using `Result::expect` to unwrap the `std::result::Result` value, panicking if the value is a `Result::Err` - | -688 | TimeValue::map_coord(value, &self.0, &self.1, limit).expect("REASON") - | +++++++++++++++++ - -Some errors have detailed explanations: E0277, E0308. -For more information about an error, try `rustc --explain E0277`. -error: could not compile `plotters` (lib) due to 8 previous errors diff --git a/plotters-svg/src/svg.rs b/plotters-svg/src/svg.rs index 51484f0a..e24623d7 100644 --- a/plotters-svg/src/svg.rs +++ b/plotters-svg/src/svg.rs @@ -602,8 +602,11 @@ impl<'a> DrawingBackend for SVGBackend<'a> { let color = image::ColorType::Rgb8; - encoder.write_image(src, w, h, color.into()).map_err(|e| { - DrawingErrorKind::DrawingError(Error::other(format!("Image error: {}", e))) + encoder.write_image(src, w, h, color).map_err(|e| { + DrawingErrorKind::DrawingError(Error::new( + std::io::ErrorKind::Other, + format!("Image error: {}", e), + )) })?; } diff --git a/plotters/src/chart/context.rs b/plotters/src/chart/context.rs index 0065bff6..c63ee3b0 100644 --- a/plotters/src/chart/context.rs +++ b/plotters/src/chart/context.rs @@ -32,9 +32,7 @@ pub struct ChartContext<'a, DB: DrawingBackend, CT: CoordTranslate> { impl<'a, DB: DrawingBackend, CT: ReverseCoordTranslate> ChartContext<'a, DB, CT> { /// Convert the chart context into an closure that can be used for coordinate translation - pub fn into_coord_trans( - self, - ) -> impl Fn(BackendCoord) -> Result, CT::ErrorType> { + pub fn into_coord_trans(self) -> impl Fn(BackendCoord) -> Option { let coord_spec = self.drawing_area.into_coord_spec(); move |coord| coord_spec.reverse_translate(coord) } diff --git a/plotters/src/chart/context/cartesian2d/draw_impl.rs b/plotters/src/chart/context/cartesian2d/draw_impl.rs index 2ad64f26..616d7d8e 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) { + for ((p, t), w) in labels.iter().zip(label_width.into_iter()) { /* Make sure we are actually in the visible range */ let rp = if orientation.0 == 0 { *p - x0 } else { *p - y0 }; @@ -240,9 +240,13 @@ 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); @@ -344,7 +348,10 @@ 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)?; } } @@ -373,7 +380,10 @@ 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)?; } } @@ -397,4 +407,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/context/cartesian2d/mod.rs b/plotters/src/chart/context/cartesian2d/mod.rs index c59be660..fd1aef27 100644 --- a/plotters/src/chart/context/cartesian2d/mod.rs +++ b/plotters/src/chart/context/cartesian2d/mod.rs @@ -9,15 +9,14 @@ use crate::coord::{ Shift, }; use crate::drawing::DrawingArea; -use crate::math_errors::MathError; mod draw_impl; impl<'a, DB, XT, YT, X, Y> ChartContext<'a, DB, Cartesian2d> where DB: DrawingBackend, - X: Ranged + ValueFormatter, - Y: Ranged + ValueFormatter, + X: Ranged + ValueFormatter, + Y: Ranged + ValueFormatter, { pub(crate) fn is_overlapping_drawing_area( &self, @@ -47,13 +46,7 @@ where } } -impl< - 'a, - DB: DrawingBackend, - X: Ranged, - Y: Ranged, - > ChartContext<'a, DB, Cartesian2d> -{ +impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesian2d> { /// Get the range of X axis pub fn x_range(&self) -> Range { self.drawing_area.get_x_range() @@ -66,21 +59,12 @@ impl< /// Maps the coordinate to the backend coordinate. This is typically used /// with an interactive chart. - pub fn backend_coord( - &self, - coord: &(X::ValueType, Y::ValueType), - ) -> Result { + pub fn backend_coord(&self, coord: &(X::ValueType, Y::ValueType)) -> BackendCoord { self.drawing_area.map_coordinate(coord) } } -impl< - 'a, - DB: DrawingBackend, - X: Ranged, - Y: Ranged, - > ChartContext<'a, DB, Cartesian2d> -{ +impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesian2d> { /// Convert this chart context into a dual axis chart context and attach a second coordinate spec /// on the chart context. For more detailed information, see documentation for [struct DualCoordChartContext](struct.DualCoordChartContext.html) /// @@ -97,11 +81,7 @@ impl< DB, Cartesian2d, Cartesian2d, - > - where - SX::CoordDescType: Ranged, - SY::CoordDescType: Ranged, - { + > { let mut pixel_range = self.drawing_area.get_pixel_range(); pixel_range.1 = pixel_range.1.end..pixel_range.1.start; diff --git a/plotters/src/chart/context/cartesian3d/draw_impl.rs b/plotters/src/chart/context/cartesian3d/draw_impl.rs index e028a809..81b1ad2a 100644 --- a/plotters/src/chart/context/cartesian3d/draw_impl.rs +++ b/plotters/src/chart/context/cartesian3d/draw_impl.rs @@ -10,7 +10,6 @@ use crate::coord::{ }; use crate::drawing::DrawingAreaErrorKind; use crate::element::{EmptyElement, PathElement, Polygon, Text}; -use crate::math_errors::MathError; use crate::style::{ text_anchor::{HPos, Pos, VPos}, ShapeStyle, TextStyle, @@ -70,18 +69,18 @@ where &axis[1][1], &axis[1][2], ])); - let axis_dir = (end?.0 - begin?.0, end?.1 - begin?.1); + let axis_dir = (end.0 - begin.0, end.1 - begin.1); let (x_range, y_range) = self.plotting_area().get_pixel_range(); let x_mid = (x_range.start + x_range.end) / 2; let y_mid = (y_range.start + y_range.end) / 2; - let x_dir = if begin?.0 < x_mid { + let x_dir = if begin.0 < x_mid { (-tick_size, 0) } else { (tick_size, 0) }; - let y_dir = if begin?.1 < y_mid { + let y_dir = if begin.1 < y_mid { (0, -tick_size) } else { (0, tick_size) @@ -127,10 +126,6 @@ where [[Coord3D; 3]; 2], DrawingAreaErrorKind, > { - if idx >= 3 { - return Err(DrawingAreaErrorKind::LayoutError); - } - let coord = self.plotting_area().as_coord_spec(); let x_range = coord.logic_x.range(); let y_range = coord.logic_y.range(); @@ -146,7 +141,7 @@ where let mut start = [&ranges[0][0], &ranges[1][0], &ranges[2][0]]; let mut end = [&ranges[0][1], &ranges[1][1], &ranges[2][1]]; - let mut best = None; + let mut plan = vec![]; for i in 0..3 { if i == idx { @@ -154,48 +149,33 @@ where } start[i] = &panels[i][0][i]; end[i] = &panels[i][0][i]; - for j in 0..3 { if i != idx && i != j && j != idx { for k in 0..2 { - start[j] = &panels[i][k][i]; + start[j] = &panels[i][k][j]; end[j] = &panels[i][k][j]; - - let d1 = coord.projected_depth( - start[0].get_x(), - start[1].get_y(), - start[2].get_z(), - ); - let d2 = coord.projected_depth( - end[0].get_x(), - end[1].get_y(), - end[2].get_z(), - ); - - let depth_score = d1.checked_add(d2).ok_or(MathError::ValueOverflow)?; - let (_, y1) = coord.translate(&Coord3D::build_coord(start))?; - let (_, y2) = coord.translate(&Coord3D::build_coord(end))?; - let y_score = y1.checked_add(y2).ok_or(MathError::ValueOverflow)?; - let candidate = (start, end); - let score = (depth_score, y_score); - match best { - None => best = Some((candidate, score)), - Some((_, best_score)) if score < best_score => { - best = Some((candidate, score)) - } - _ => {} - } + plan.push((start, end)); } } } } - best.map(|(candidate, _)| candidate) - .ok_or(DrawingAreaErrorKind::LayoutError)? + plan.into_iter() + .min_by_key(|&(s, e)| { + let d = coord.projected_depth(s[0].get_x(), s[1].get_y(), s[2].get_z()); + let d = d + coord.projected_depth(e[0].get_x(), e[1].get_y(), e[2].get_z()); + let (_, y1) = coord.translate(&Coord3D::build_coord(s)); + let (_, y2) = coord.translate(&Coord3D::build_coord(e)); + let y = y1 + y2; + (d, y) + }) + .unwrap() }; + self.plotting_area().draw(&PathElement::new( vec![Coord3D::build_coord(start), Coord3D::build_coord(end)], style, ))?; + Ok([ [start[0].clone(), start[1].clone(), start[2].clone()], [end[0].clone(), end[1].clone(), end[2].clone()], diff --git a/plotters/src/chart/dual_coord.rs b/plotters/src/chart/dual_coord.rs index ac2799d2..048bea02 100644 --- a/plotters/src/chart/dual_coord.rs +++ b/plotters/src/chart/dual_coord.rs @@ -142,9 +142,7 @@ impl DualCoordChartContext<'_, DB, CT1, CT2> { /// Convert the chart context into the secondary coordinate translation function - pub fn into_secondary_coord_trans( - self, - ) -> impl Fn(BackendCoord) -> Result, CT2::ErrorType> { + pub fn into_secondary_coord_trans(self) -> impl Fn(BackendCoord) -> Option { let coord_spec = self.secondary.drawing_area.into_coord_spec(); move |coord| coord_spec.reverse_translate(coord) } @@ -158,8 +156,8 @@ impl pub fn into_coord_trans_pair( self, ) -> ( - impl Fn(BackendCoord) -> Result, CT1::ErrorType>, - impl Fn(BackendCoord) -> Result, CT2::ErrorType>, + impl Fn(BackendCoord) -> Option, + impl Fn(BackendCoord) -> Option, ) { let coord_spec_1 = self.primary.drawing_area.into_coord_spec(); let coord_spec_2 = self.secondary.drawing_area.into_coord_spec(); diff --git a/plotters/src/chart/series.rs b/plotters/src/chart/series.rs index e0539e8b..997f30d0 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) + .zip(funcs.into_iter()) { let legend_element = make_elem((label_x + margin, (y0 + y1) / 2)); drawing_area.draw(&legend_element)?; diff --git a/plotters/src/coord/mod.rs b/plotters/src/coord/mod.rs index b89f9e96..b01fc697 100644 --- a/plotters/src/coord/mod.rs +++ b/plotters/src/coord/mod.rs @@ -55,41 +55,19 @@ pub mod cartesian { mod translate; pub use translate::{CoordTranslate, ReverseCoordTranslate}; -use crate::math_errors::MathError; - /// The coordinate translation that only impose shift #[derive(Debug, Clone)] pub struct Shift(pub BackendCoord); impl CoordTranslate for Shift { type From = BackendCoord; - type ErrorType = MathError; - fn translate(&self, from: &Self::From) -> Result { - let x = from - .0 - .checked_add((self.0).0) - .ok_or(MathError::ValueOverflow)?; - let y = from - .1 - .checked_add((self.0).1) - .ok_or(MathError::ValueOverflow)?; - Ok((x, y)) + fn translate(&self, from: &Self::From) -> BackendCoord { + (from.0 + (self.0).0, from.1 + (self.0).1) } } impl ReverseCoordTranslate for Shift { - fn reverse_translate( - &self, - input: BackendCoord, - ) -> Result, Self::ErrorType> { - let x = input - .0 - .checked_sub((self.0).0) - .ok_or(MathError::ValueUnderflow)?; - let y = input - .1 - .checked_sub((self.0).1) - .ok_or(MathError::ValueUnderflow)?; - Ok(Some((x, y))) + fn reverse_translate(&self, input: BackendCoord) -> Option { + Some((input.0 - (self.0).0, input.1 - (self.0).1)) } } diff --git a/plotters/src/coord/ranged1d/combinators/ckps.rs b/plotters/src/coord/ranged1d/combinators/ckps.rs index 68b81b54..cdaaf922 100644 --- a/plotters/src/coord/ranged1d/combinators/ckps.rs +++ b/plotters/src/coord/ranged1d/combinators/ckps.rs @@ -55,13 +55,12 @@ where { type ValueType = R::ValueType; type FormatOption = R::FormatOption; - type ErrorType = R::ErrorType; fn range(&self) -> Range { self.inner.range() } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { self.inner.map(value, limit) } @@ -73,7 +72,7 @@ where } } - fn axis_pixel_range(&self, limit: (i32, i32)) -> Result, Self::ErrorType> { + fn axis_pixel_range(&self, limit: (i32, i32)) -> Range { self.inner.axis_pixel_range(limit) } } @@ -181,13 +180,12 @@ impl WithKeyPointMethod { impl Ranged for WithKeyPointMethod { type ValueType = R::ValueType; type FormatOption = R::FormatOption; - type ErrorType = R::ErrorType; fn range(&self) -> Range { self.inner.range() } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { self.inner.map(value, limit) } @@ -199,7 +197,7 @@ impl Ranged for WithKeyPointMethod { } } - fn axis_pixel_range(&self, limit: (i32, i32)) -> Result, Self::ErrorType> { + fn axis_pixel_range(&self, limit: (i32, i32)) -> Range { self.inner.axis_pixel_range(limit) } } @@ -223,7 +221,7 @@ mod test { #[test] fn test_with_key_points() { let range = (0..100).with_key_points(vec![1, 2, 3]); - assert_eq!(range.map(&3, (0, 1000))?, 30); + assert_eq!(range.map(&3, (0, 1000)), 30); assert_eq!(range.range(), 0..100); assert_eq!(range.key_points(BoldPoints(100)), vec![1, 2, 3]); assert_eq!(range.key_points(LightPoints::new(100, 100)), vec![]); @@ -238,7 +236,7 @@ mod test { assert_eq!(range.index_of(&10), Some(10)); assert_eq!(range.from_index(10), Some(10)); - assert_eq!(range.axis_pixel_range((0, 1000))?, 0..1000); + assert_eq!(range.axis_pixel_range((0, 1000)), 0..1000); let mut range = range; @@ -251,7 +249,7 @@ mod test { #[test] fn test_with_key_point_method() { let range = (0..100).with_key_point_func(|_| vec![1, 2, 3]); - assert_eq!(range.map(&3, (0, 1000))?, 30); + assert_eq!(range.map(&3, (0, 1000)), 30); assert_eq!(range.range(), 0..100); assert_eq!(range.key_points(BoldPoints(100)), vec![1, 2, 3]); assert_eq!(range.key_points(LightPoints::new(100, 100)), vec![]); @@ -266,6 +264,6 @@ mod test { assert_eq!(range.index_of(&10), Some(10)); assert_eq!(range.from_index(10), Some(10)); - assert_eq!(range.axis_pixel_range((0, 1000))?, 0..1000); + assert_eq!(range.axis_pixel_range((0, 1000)), 0..1000); } } diff --git a/plotters/src/coord/ranged1d/combinators/group_by.rs b/plotters/src/coord/ranged1d/combinators/group_by.rs index 3e6959af..875b02d4 100644 --- a/plotters/src/coord/ranged1d/combinators/group_by.rs +++ b/plotters/src/coord/ranged1d/combinators/group_by.rs @@ -64,8 +64,7 @@ impl + ValueFormatter> ValueFormatter impl Ranged for GroupBy { type FormatOption = NoDefaultFormatting; type ValueType = T::ValueType; - type ErrorType = T::ErrorType; - fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> Result { + fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> i32 { self.0.map(value, limit) } fn range(&self) -> Range { diff --git a/plotters/src/coord/ranged1d/combinators/linspace.rs b/plotters/src/coord/ranged1d/combinators/linspace.rs index 097d3a27..14b5ebaa 100644 --- a/plotters/src/coord/ranged1d/combinators/linspace.rs +++ b/plotters/src/coord/ranged1d/combinators/linspace.rs @@ -278,12 +278,12 @@ where { type FormatOption = NoDefaultFormatting; type ValueType = T::ValueType; - type ErrorType = T::ErrorType; + fn range(&self) -> Range { self.inner.range() } - fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> Result { + fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> i32 { self.inner.map(value, limit) } diff --git a/plotters/src/coord/ranged1d/combinators/logarithmic.rs b/plotters/src/coord/ranged1d/combinators/logarithmic.rs index 8380d8bc..56f54f05 100644 --- a/plotters/src/coord/ranged1d/combinators/logarithmic.rs +++ b/plotters/src/coord/ranged1d/combinators/logarithmic.rs @@ -2,7 +2,6 @@ use crate::coord::ranged1d::types::RangedCoordf64; use crate::coord::ranged1d::{ AsRangedCoord, DefaultFormatting, KeyPointHint, Ranged, ReversibleRanged, }; -use crate::math_errors::MathError; use std::marker::PhantomData; use std::ops::Range; @@ -82,12 +81,14 @@ impl IntoLogRange for Range { } impl ReversibleRanged for LogCoord { - fn unmap(&self, input: i32, limit: (i32, i32)) -> Result, Self::ErrorType> { - self.linear - .unmap(input, limit) - .map(|value_ln| value_ln.map(|value_ln| self.f64_to_value(value_ln.exp()))) + fn unmap(&self, input: i32, limit: (i32, i32)) -> Option { + self.linear.unmap(input, limit).map(|value_ln| { + let fv = value_ln.exp(); + self.f64_to_value(fv) + }) } } + /// The logarithmic coordinate decorator. /// This decorator is used to make the axis rendered as logarithmically. #[derive(Clone)] @@ -199,8 +200,8 @@ impl LogCoord { impl Ranged for LogCoord { type FormatOption = DefaultFormatting; type ValueType = V; - type ErrorType = MathError; - fn map(&self, value: &V, limit: (i32, i32)) -> Result { + + fn map(&self, value: &V, limit: (i32, i32)) -> i32 { let fv = self.value_to_f64(value); let value_ln = fv.ln(); self.linear.map(&value_ln, limit) diff --git a/plotters/src/coord/ranged1d/combinators/nested.rs b/plotters/src/coord/ranged1d/combinators/nested.rs index 4dd170eb..379f2d4f 100644 --- a/plotters/src/coord/ranged1d/combinators/nested.rs +++ b/plotters/src/coord/ranged1d/combinators/nested.rs @@ -1,7 +1,6 @@ use crate::coord::ranged1d::{ AsRangedCoord, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, ValueFormatter, }; -use crate::math_errors::MathError; use std::ops::Range; /// Describe a value for a nested coordinate @@ -67,10 +66,10 @@ where } } -impl> Ranged for NestedRange { +impl Ranged for NestedRange { type FormatOption = NoDefaultFormatting; type ValueType = NestedValue; - type ErrorType = MathError; + fn range(&self) -> Range { let primary_range = self.primary.range(); @@ -81,7 +80,7 @@ impl> Ranged for NestedRange ..NestedValue::Value(primary_range.end, secondary_right) } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { let idx = self.primary.index_of(value.category()).unwrap_or(0); let total = self.primary.size(); @@ -98,11 +97,7 @@ impl> Ranged for NestedRange if let Some(secondary_value) = value.nested_value() { self.secondary[idx].map(secondary_value, (s_left, s_right)) } else { - let sum = s_left - .checked_add(s_right) - .ok_or(MathError::ValueOverflow)?; - let result = sum.checked_div(2).ok_or(MathError::ValueUnderflow)?; - Ok(result) + (s_left + s_right) / 2 } } @@ -134,9 +129,7 @@ impl> Ranged for NestedRange } } -impl> DiscreteRanged - for NestedRange -{ +impl DiscreteRanged for NestedRange { fn size(&self) -> usize { self.secondary.iter().map(|x| x.size()).sum::() } @@ -202,9 +195,9 @@ mod test { let range = coord.range(); assert_eq!(NestedValue::Value(0, 0)..NestedValue::Value(10, 11), range); - assert_eq!(coord.map(&NestedValue::Category(0), (0, 1100))?, 50); - assert_eq!(coord.map(&NestedValue::Value(0, 0), (0, 1100))?, 0); - assert_eq!(coord.map(&NestedValue::Value(5, 4), (0, 1100))?, 567); + assert_eq!(coord.map(&NestedValue::Category(0), (0, 1100)), 50); + assert_eq!(coord.map(&NestedValue::Value(0, 0), (0, 1100)), 0); + assert_eq!(coord.map(&NestedValue::Value(5, 4), (0, 1100)), 567); assert_eq!(coord.size(), (2 + 12) * 11 / 2); assert_eq!(coord.index_of(&NestedValue::Value(5, 4)), Some(24)); diff --git a/plotters/src/coord/ranged1d/combinators/partial_axis.rs b/plotters/src/coord/ranged1d/combinators/partial_axis.rs index af4e546e..b778ee2c 100644 --- a/plotters/src/coord/ranged1d/combinators/partial_axis.rs +++ b/plotters/src/coord/ranged1d/combinators/partial_axis.rs @@ -1,7 +1,6 @@ use crate::coord::ranged1d::{ AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, Ranged, }; -use crate::math_errors::MathError; use std::ops::Range; /// This axis decorator will make the axis partially display on the axis. @@ -26,15 +25,14 @@ pub trait IntoPartialAxis: AsRangedCoord { impl IntoPartialAxis for R {} -impl> Ranged for PartialAxis +impl Ranged for PartialAxis where R::ValueType: Clone, { type FormatOption = DefaultFormatting; type ValueType = R::ValueType; - type ErrorType = MathError; - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { self.0.map(value, limit) } @@ -46,17 +44,17 @@ where self.0.range() } - fn axis_pixel_range(&self, limit: (i32, i32)) -> Result, Self::ErrorType> { - let left = self.map(&self.1.start, limit)?; - let right = self.map(&self.1.end, limit)?; + fn axis_pixel_range(&self, limit: (i32, i32)) -> Range { + let left = self.map(&self.1.start, limit); + let right = self.map(&self.1.end, limit); - Ok(left.min(right)..left.max(right)) + left.min(right)..left.max(right) } } impl DiscreteRanged for PartialAxis where - R: Ranged, + R: Ranged, ::ValueType: Eq + Clone, { fn size(&self) -> usize { @@ -110,6 +108,6 @@ mod test { let r = make_partial_axis(20..80, 0.2..0.8).unwrap(); assert_eq!(r.size(), 101); assert_eq!(r.range(), 0..100); - assert_eq!(r.axis_pixel_range((0, 100)), Ok(20..80)); + assert_eq!(r.axis_pixel_range((0, 100)), 20..80); } } diff --git a/plotters/src/coord/ranged1d/discrete.rs b/plotters/src/coord/ranged1d/discrete.rs index 8d4eb643..8b5da4b6 100644 --- a/plotters/src/coord/ranged1d/discrete.rs +++ b/plotters/src/coord/ranged1d/discrete.rs @@ -1,8 +1,6 @@ use crate::coord::ranged1d::{ AsRangedCoord, KeyPointHint, NoDefaultFormatting, Ranged, ReversibleRanged, ValueFormatter, }; -use crate::math_errors::MathError; -use crate::math_guard::{float_to_integer_checked, non_zero_checked}; use std::ops::Range; /// The trait indicates the coordinate is discrete @@ -138,47 +136,29 @@ where } } -impl Ranged for SegmentedCoord -where - MathError: From<::ErrorType>, -{ +impl Ranged for SegmentedCoord { type FormatOption = NoDefaultFormatting; type ValueType = SegmentValue; - type ErrorType = MathError; - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { - let pixel_span = (i64::from(limit.1) - i64::from(limit.0)) as f64; - let size = - non_zero_checked::(self.0.size(), MathError::ZeroDivision)? as f64; - let margin = float_to_integer_checked::( - (pixel_span / size).round(), - MathError::ValueOutOfRange, - )?; - let upper = limit - .1 - .checked_sub(margin) - .ok_or(MathError::ValueUnderflow)?; + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { + let margin = ((limit.1 - limit.0) as f32 / self.0.size() as f32).round() as i32; + match value { - SegmentValue::Exact(coord) => Ok(self.0.map(coord, (limit.0, upper))?), + SegmentValue::Exact(coord) => self.0.map(coord, (limit.0, limit.1 - margin)), SegmentValue::CenterOf(coord) => { - let left = self.0.map(coord, (limit.0, upper))?; + let left = self.0.map(coord, (limit.0, limit.1 - margin)); if let Some(idx) = self.0.index_of(coord) { - if idx.checked_add(1).ok_or(MathError::ValueOverflow)? < self.0.size() { + if idx + 1 < self.0.size() { let right = self.0.map( - &self - .0 - .from_index(idx.checked_add(1).ok_or(MathError::ValueOverflow)?) - .ok_or(MathError::ValueOutOfRange)?, - (limit.0, upper), - )?; - let mid = (i64::from(left) + i64::from(right)) / 2; - return Ok(mid as i32); + &self.0.from_index(idx + 1).unwrap(), + (limit.0, limit.1 - margin), + ); + return (left + right) / 2; } } - let pos = (i64::from(left) + i64::from(margin)) / 2; - Ok(pos as i32) + left + margin / 2 } - SegmentValue::Last => Ok(limit.1), + SegmentValue::Last => limit.1, } } @@ -196,10 +176,7 @@ where } } -impl DiscreteRanged for SegmentedCoord -where - MathError: From<::ErrorType>, -{ +impl DiscreteRanged for SegmentedCoord { fn size(&self) -> usize { self.0.size() + 1 } @@ -228,14 +205,10 @@ impl From for SegmentValue { } impl ReversibleRanged for DC { - fn unmap( - &self, - input: i32, - limit: (i32, i32), - ) -> Result, Self::ErrorType> { + fn unmap(&self, input: i32, limit: (i32, i32)) -> Option { let idx = (f64::from(input - limit.0) * (self.size() as f64) / f64::from(limit.1 - limit.0)) .floor() as usize; - Ok(self.from_index(idx)) + self.from_index(idx) } } @@ -294,8 +267,8 @@ mod test { } } - assert_eq!(coord.map(&SegmentValue::CenterOf(0), (0, 24)), Ok(1)); - assert_eq!(coord.map(&SegmentValue::Exact(0), (0, 24)), Ok(0)); - assert_eq!(coord.map(&SegmentValue::Exact(1), (0, 24)), Ok(2)); + assert_eq!(coord.map(&SegmentValue::CenterOf(0), (0, 24)), 1); + assert_eq!(coord.map(&SegmentValue::Exact(0), (0, 24)), 0); + assert_eq!(coord.map(&SegmentValue::Exact(1), (0, 24)), 2); } } diff --git a/plotters/src/coord/ranged1d/mod.rs b/plotters/src/coord/ranged1d/mod.rs index 4dc0d593..1b2072c2 100644 --- a/plotters/src/coord/ranged1d/mod.rs +++ b/plotters/src/coord/ranged1d/mod.rs @@ -198,11 +198,8 @@ pub trait Ranged { /// The type of this value in this range specification type ValueType; - /// The error type to return in a result. - type ErrorType; - /// This function maps the value to i32, which is the drawing coordinate - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result; + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32; /// This function gives the key points that we can draw a grid based on this fn key_points(&self, hint: Hint) -> Vec; @@ -212,11 +209,11 @@ pub trait Ranged { /// This function provides the on-axis part of its range #[allow(clippy::range_plus_one)] - fn axis_pixel_range(&self, limit: (i32, i32)) -> Result, Self::ErrorType> { + fn axis_pixel_range(&self, limit: (i32, i32)) -> Range { if limit.0 < limit.1 { - Ok(limit.0..limit.1) + limit.0..limit.1 } else { - Ok(limit.1..limit.0) + limit.1..limit.0 } } } @@ -226,11 +223,7 @@ pub trait Ranged { /// logic value. pub trait ReversibleRanged: Ranged { /// Perform the reverse mapping - fn unmap( - &self, - input: i32, - limit: (i32, i32), - ) -> Result, Self::ErrorType>; + fn unmap(&self, input: i32, limit: (i32, i32)) -> Option; } /// The trait for the type that can be converted into a ranged coordinate axis diff --git a/plotters/src/coord/ranged1d/types/datetime.rs b/plotters/src/coord/ranged1d/types/datetime.rs index 649004f8..27f604ea 100644 --- a/plotters/src/coord/ranged1d/types/datetime.rs +++ b/plotters/src/coord/ranged1d/types/datetime.rs @@ -2,13 +2,9 @@ use chrono::{Date, DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, TimeZone, Timelike}; use std::ops::{Add, Range, Sub}; -use crate::math_errors::MathError; -use crate::{ - coord::ranged1d::{ - AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, - Ranged, ReversibleRanged, ValueFormatter, - }, - math_guard::{float_to_integer_checked, non_zero_checked}, +use crate::coord::ranged1d::{ + AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, Ranged, + ReversibleRanged, ValueFormatter, }; /// The trait that describe some time value. This is the uniformed abstraction that works @@ -32,43 +28,25 @@ pub trait TimeValue: Eq + Sized { fn from_date(date: Self::DateType) -> Self; /// Map the coord spec - fn map_coord( - value: &Self, - begin: &Self, - end: &Self, - limit: (i32, i32), - ) -> Result { + fn map_coord(value: &Self, begin: &Self, end: &Self, limit: (i32, i32)) -> i32 { let total_span = end.subtract(begin); let value_span = value.subtract(begin); - // Calculate the pixel span cast into i64 to avoid under flowing. - let pixel_span = (limit.1 as i64 - limit.0 as i64) as f64; + // First, lets try the nanoseconds precision if let Some(total_ns) = total_span.num_nanoseconds() { if let Some(value_ns) = value_span.num_nanoseconds() { - let value_ns = value_ns as f64; - let total_ns = - non_zero_checked::(total_ns, MathError::ZeroDivision)? as f64; - - let result = float_to_integer_checked::( - pixel_span * value_ns / total_ns, - MathError::ValueOutOfRange, - )?; - return result.checked_add(limit.0).ok_or(MathError::ValueOverflow); + return (f64::from(limit.1 - limit.0) * value_ns as f64 / total_ns as f64) as i32 + + limit.0; } } // Yes, converting them to floating point may lose precision, but this is Ok. // If it overflows, it means we have a time span nearly 300 years, we are safe to ignore the // portion less than 1 day. - let total_days = - non_zero_checked::(total_span.num_days(), MathError::ZeroDivision)? - as f64; + let total_days = total_span.num_days() as f64; let value_days = value_span.num_days() as f64; - let result = float_to_integer_checked::( - pixel_span * value_days / total_days, - MathError::ValueOutOfRange, - )?; - result.checked_add(limit.0).ok_or(MathError::ValueOverflow) + + (f64::from(limit.1 - limit.0) * value_days / total_days) as i32 + limit.0 } /// Map pixel to coord spec @@ -234,13 +212,12 @@ where { type FormatOption = DefaultFormatting; type ValueType = D; - type ErrorType = MathError; fn range(&self) -> Range { self.0.clone()..self.1.clone() } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { TimeValue::map_coord(value, &self.0, &self.1, limit) } @@ -425,13 +402,12 @@ where { type FormatOption = NoDefaultFormatting; type ValueType = T; - type ErrorType = MathError; fn range(&self) -> Range { self.0.start.clone()..self.0.end.clone() } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { T::map_coord(value, &self.0.start, &self.0.end, limit) } @@ -553,13 +529,12 @@ where { type FormatOption = NoDefaultFormatting; type ValueType = T; - type ErrorType = MathError; fn range(&self) -> Range { self.0.start.clone()..self.0.end.clone() } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { T::map_coord(value, &self.0.start, &self.0.end, limit) } @@ -681,13 +656,12 @@ where { type FormatOption = DefaultFormatting; type ValueType = DT; - type ErrorType = MathError; fn range(&self) -> Range
{ self.0.clone()..self.1.clone() } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { TimeValue::map_coord(value, &self.0, &self.1, limit) } @@ -739,13 +713,8 @@ where RangedDate: Ranged, { /// Perform the reverse mapping - fn unmap( - &self, - input: i32, - limit: (i32, i32), - ) -> Result, Self::ErrorType> { - let value = Some(TimeValue::unmap_coord(input, &self.0, &self.1, limit)); - Ok(value) + fn unmap(&self, input: i32, limit: (i32, i32)) -> Option { + Some(TimeValue::unmap_coord(input, &self.0, &self.1, limit)) } } @@ -767,48 +736,29 @@ impl From> for RangedDuration { impl Ranged for RangedDuration { type FormatOption = DefaultFormatting; type ValueType = Duration; - type ErrorType = MathError; fn range(&self) -> Range { self.0..self.1 } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { - let total_span = self - .1 - .checked_sub(&self.0) - .ok_or(MathError::ValueUnderflow)?; - let value_span = value - .checked_sub(&self.0) - .ok_or(MathError::ValueUnderflow)?; - - let limit_difference = (i64::from(limit.1) - i64::from(limit.0)) as f64; - let offset = 1e-10; + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { + let total_span = self.1 - self.0; + let value_span = *value - self.0; if let Some(total_ns) = total_span.num_nanoseconds() { if let Some(value_ns) = value_span.num_nanoseconds() { - let value_ns = value_ns as f64; - let total_ns = - non_zero_checked::(total_ns, MathError::ZeroDivision)? as f64; - let result = float_to_integer_checked::( - limit_difference * value_ns / total_ns + offset, - MathError::ValueOutOfRange, - )?; - return limit.0.checked_add(result).ok_or(MathError::ValueOverflow); + return limit.0 + + (f64::from(limit.1 - limit.0) * value_ns as f64 / total_ns as f64 + 1e-10) + as i32; } - return Ok(limit.1); + return limit.1; } - let value_days = value_span.num_days() as f64; - let total_days = - non_zero_checked::(total_span.num_days(), MathError::ZeroDivision)? - as f64; + let total_days = total_span.num_days(); + let value_days = value_span.num_days(); - let result = float_to_integer_checked::( - limit_difference * value_days / total_days + offset, - MathError::ValueOutOfRange, - )?; - limit.0.checked_add(result).ok_or(MathError::ValueOverflow) + limit.0 + + (f64::from(limit.1 - limit.0) * value_days as f64 / total_days as f64 + 1e-10) as i32 } fn key_points(&self, hint: HintType) -> Vec { @@ -959,8 +909,8 @@ mod test { let ranged_coord = Into::>::into(range); - assert_eq!(ranged_coord.map(&Utc.ymd(1000, 8, 10), (0, 100)), Ok(0)); - assert_eq!(ranged_coord.map(&Utc.ymd(2999, 8, 10), (0, 100)), Ok(100)); + assert_eq!(ranged_coord.map(&Utc.ymd(1000, 8, 10), (0, 100)), 0); + assert_eq!(ranged_coord.map(&Utc.ymd(2999, 8, 10), (0, 100)), 100); let kps = ranged_coord.key_points(23); @@ -1029,8 +979,8 @@ mod test { let range = Utc.ymd(1000, 8, 5)..Utc.ymd(2999, 1, 1); let ranged_coord = range.yearly(); - assert_eq!(ranged_coord.map(&Utc.ymd(1000, 8, 10), (0, 100)), Ok(0)); - assert_eq!(ranged_coord.map(&Utc.ymd(2999, 8, 10), (0, 100)), Ok(100)); + assert_eq!(ranged_coord.map(&Utc.ymd(1000, 8, 10), (0, 100)), 0); + assert_eq!(ranged_coord.map(&Utc.ymd(2999, 8, 10), (0, 100)), 100); let kps = ranged_coord.key_points(23); @@ -1091,11 +1041,11 @@ mod test { assert_eq!( coord.map(&Utc.ymd(1000, 1, 1).and_hms(0, 0, 0), (0, 100)), - Ok(0) + 0 ); assert_eq!( coord.map(&Utc.ymd(3000, 1, 1).and_hms(0, 0, 0), (0, 100)), - Ok(100) + 100 ); let kps = coord.key_points(23); @@ -1194,8 +1144,8 @@ mod test { fn test_duration_long_range() { let coord: RangedDuration = (Duration::days(-1000000)..Duration::days(1000000)).into(); - assert_eq!(coord.map(&Duration::days(-1000000), (0, 100)), Ok(0)); - assert_eq!(coord.map(&Duration::days(1000000), (0, 100)), Ok(100)); + assert_eq!(coord.map(&Duration::days(-1000000), (0, 100)), 0); + assert_eq!(coord.map(&Duration::days(1000000), (0, 100)), 100); let kps = coord.key_points(23); @@ -1281,10 +1231,9 @@ mod test { let mid = Utc.ymd(2022, 1, 1).and_hms(8, 0, 0); let coord: RangedDateTime<_> = (start_time..end_time).into(); let pos = coord.map(&mid, (1000, 2000)); - assert_eq!(pos, Ok(1500)); - let value: Result>, MathError> = - coord.unmap(pos.unwrap(), (1000, 2000)); - assert_eq!(value, Ok(Some(mid))); + assert_eq!(pos, 1500); + let value = coord.unmap(pos, (1000, 2000)); + assert_eq!(value, Some(mid)); } #[test] @@ -1294,9 +1243,9 @@ mod test { let mid = NaiveDate::from_ymd(2022, 1, 1).and_hms_milli(8, 0, 0, 0); let coord: RangedDateTime<_> = (start_time..end_time).into(); let pos = coord.map(&mid, (1000, 2000)); - assert_eq!(pos, Ok(1500)); - let value = coord.unmap(pos.unwrap(), (1000, 2000)); - assert_eq!(value, Ok(Some(mid))); + assert_eq!(pos, 1500); + let value = coord.unmap(pos, (1000, 2000)); + assert_eq!(value, Some(mid)); } #[test] @@ -1306,9 +1255,9 @@ mod test { let mid = Utc.ymd(2022, 1, 1); let coord: RangedDate> = (start_date..end_date).into(); let pos = coord.map(&mid, (1000, 2000)); - assert_eq!(pos, Ok(1500)); - let value = coord.unmap(pos.unwrap(), (1000, 2000)); - assert_eq!(value, Ok(Some(mid))); + assert_eq!(pos, 1500); + let value = coord.unmap(pos, (1000, 2000)); + assert_eq!(value, Some(mid)); } #[test] @@ -1318,9 +1267,9 @@ mod test { let mid = NaiveDate::from_ymd(2022, 1, 1); let coord: RangedDate = (start_date..end_date).into(); let pos = coord.map(&mid, (1000, 2000)); - assert_eq!(pos, Ok(1500)); - let value = coord.unmap(pos.unwrap(), (1000, 2000)); - assert_eq!(value, Ok(Some(mid))); + assert_eq!(pos, 1500); + let value = coord.unmap(pos, (1000, 2000)); + assert_eq!(value, Some(mid)); } #[test] @@ -1330,9 +1279,9 @@ mod test { let mid = start_time + Duration::nanoseconds(950); let coord: RangedDateTime<_> = (start_time..end_time).into(); let pos = coord.map(&mid, (1000, 2000)); - assert_eq!(pos, Ok(1500)); - let value = coord.unmap(pos.unwrap(), (1000, 2000)); - assert_eq!(value, Ok(Some(mid))); + assert_eq!(pos, 1500); + let value = coord.unmap(pos, (1000, 2000)); + assert_eq!(value, Some(mid)); } #[test] @@ -1341,9 +1290,9 @@ mod test { let end_time = start_time + Duration::nanoseconds(400); let coord: RangedDateTime<_> = (start_time..end_time).into(); let value = coord.unmap(2000, (1000, 2000)); - assert_eq!(value, Ok(Some(end_time))); + assert_eq!(value, Some(end_time)); let mid = start_time + Duration::nanoseconds(200); let value = coord.unmap(500, (0, 1000)); - assert_eq!(value, Ok(Some(mid))); + assert_eq!(value, Some(mid)); } } diff --git a/plotters/src/coord/ranged1d/types/numeric.rs b/plotters/src/coord/ranged1d/types/numeric.rs index 6b5c739f..6b40d4c5 100644 --- a/plotters/src/coord/ranged1d/types/numeric.rs +++ b/plotters/src/coord/ranged1d/types/numeric.rs @@ -1,16 +1,12 @@ use std::convert::TryFrom; use std::ops::Range; -use crate::math_errors::MathError; -use crate::{ - coord::{ - combinators::WithKeyPoints, - ranged1d::{ - AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, - Ranged, ReversibleRanged, ValueFormatter, - }, +use crate::coord::{ + combinators::WithKeyPoints, + ranged1d::{ + AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, NoDefaultFormatting, + Ranged, ReversibleRanged, ValueFormatter, }, - math_guard::float_to_integer_checked, }; macro_rules! impl_discrete_trait { @@ -52,22 +48,20 @@ macro_rules! impl_ranged_type_trait { macro_rules! impl_reverse_mapping_trait { ($type:ty, $name: ident) => { impl ReversibleRanged for $name { - fn unmap(&self, p: i32, (min, max): (i32, i32)) -> Result, MathError> { + fn unmap(&self, p: i32, (min, max): (i32, i32)) -> Option<$type> { if p < min.min(max) || p > max.max(min) || min == max { - return Ok(None); + return None; } let logical_offset = f64::from(p - min) / f64::from(max - min); - return Ok(Some( - ((self.1 - self.0) as f64 * logical_offset + self.0 as f64) as $type, - )); + return Some(((self.1 - self.0) as f64 * logical_offset + self.0 as f64) as $type); } } }; } macro_rules! make_numeric_coord { - ($type:ty, $error:ty, $name:ident, $key_points:ident, $doc: expr, $fmt: ident) => { + ($type:ty, $name:ident, $key_points:ident, $doc: expr, $fmt: ident) => { #[doc = $doc] #[derive(Clone)] pub struct $name($type, $type); @@ -79,39 +73,35 @@ macro_rules! make_numeric_coord { impl Ranged for $name { type FormatOption = $fmt; type ValueType = $type; - type ErrorType = $error; #[allow(clippy::float_cmp)] - fn map(&self, v: &$type, limit: (i32, i32)) -> Result { + fn map(&self, v: &$type, limit: (i32, i32)) -> i32 { // Corner case: If we have a range that have only one value, // then we just assign everything to the only point if self.1 == self.0 { - let midpoint = (i64::from(limit.0) + i64::from(limit.1)) / 2; - return Ok(midpoint as i32); + return (limit.1 - limit.0) / 2; } let logic_length = (*v as f64 - self.0 as f64) / (self.1 as f64 - self.0 as f64); - if !logic_length.is_finite() { - return Err(MathError::NonFiniteCalculation); - } - let actual_length = (i64::from(limit.1) - i64::from(limit.0)) as f64; + let actual_length = limit.1 - limit.0; - if actual_length == 0.0 { - return Ok(limit.1); + if actual_length == 0 { + return limit.1; } - let projected = if actual_length > 0.0 { - (actual_length * logic_length + 1e-3).floor() - } else { - (actual_length * logic_length - 1e-3).ceil() - }; - - let offset = float_to_integer_checked::( - projected, - MathError::ValueOutOfRange, - )?; + if logic_length.is_infinite() { + if logic_length.is_sign_positive() { + return limit.1; + } else { + return limit.0; + } + } - limit.0.checked_add(offset).ok_or(MathError::ValueOverflow) + if actual_length > 0 { + return limit.0 + (actual_length as f64 * logic_length + 1e-3).floor() as i32; + } else { + return limit.0 + (actual_length as f64 * logic_length - 1e-3).ceil() as i32; + } } fn key_points(&self, hint: Hint) -> Vec<$type> { $key_points((self.0, self.1), hint.max_num_points()) @@ -121,8 +111,8 @@ macro_rules! make_numeric_coord { } } }; - ($type:ty, $error:ty, $name:ident, $key_points:ident, $doc: expr) => { - make_numeric_coord!($type, $error, $name, $key_points, $doc, DefaultFormatting); + ($type:ty, $name:ident, $key_points:ident, $doc: expr) => { + make_numeric_coord!($type, $name, $key_points, $doc, DefaultFormatting); }; } @@ -264,7 +254,6 @@ gen_key_points_comp!(integer, compute_usize_key_points, usize); make_numeric_coord!( f32, - MathError, RangedCoordf32, compute_f32_key_points, "The ranged coordinate for type f32", @@ -294,7 +283,6 @@ impl ValueFormatter for WithKeyPoints { make_numeric_coord!( f64, - MathError, RangedCoordf64, compute_f64_key_points, "The ranged coordinate for type f64", @@ -323,56 +311,48 @@ impl ValueFormatter for WithKeyPoints { } make_numeric_coord!( u32, - MathError, RangedCoordu32, compute_u32_key_points, "The ranged coordinate for type u32" ); make_numeric_coord!( i32, - MathError, RangedCoordi32, compute_i32_key_points, "The ranged coordinate for type i32" ); make_numeric_coord!( u64, - MathError, RangedCoordu64, compute_u64_key_points, "The ranged coordinate for type u64" ); make_numeric_coord!( i64, - MathError, RangedCoordi64, compute_i64_key_points, "The ranged coordinate for type i64" ); make_numeric_coord!( u128, - MathError, RangedCoordu128, compute_u128_key_points, "The ranged coordinate for type u128" ); make_numeric_coord!( i128, - MathError, RangedCoordi128, compute_i128_key_points, "The ranged coordinate for type i128" ); make_numeric_coord!( usize, - MathError, RangedCoordusize, compute_usize_key_points, "The ranged coordinate for type usize" ); make_numeric_coord!( isize, - MathError, RangedCoordisize, compute_isize_key_points, "The ranged coordinate for type isize" @@ -421,10 +401,10 @@ mod test { assert_eq!(coord.key_points(11).len(), 11); assert_eq!(coord.key_points(11)[0], 0); assert_eq!(coord.key_points(11)[10], 20); - assert_eq!(coord.map(&5, (0, 100)), Ok(25)); + assert_eq!(coord.map(&5, (0, 100)), 25); let coord: RangedCoordf32 = (0f32..20f32).into(); - assert_eq!(coord.map(&5.0, (0, 100)), Ok(25)); + assert_eq!(coord.map(&5.0, (0, 100)), 25); } #[test] @@ -441,8 +421,8 @@ mod test { fn test_coord_unmap() { let coord: RangedCoordu32 = (0..20).into(); let pos = coord.map(&5, (1000, 2000)); - let value = coord.unmap(pos?, (1000, 2000)); - assert_eq!(value, Ok(Some(5))); + let value = coord.unmap(pos, (1000, 2000)); + assert_eq!(value, Some(5)); } #[test] diff --git a/plotters/src/coord/ranged1d/types/slice.rs b/plotters/src/coord/ranged1d/types/slice.rs index 3b854327..13be3d7f 100644 --- a/plotters/src/coord/ranged1d/types/slice.rs +++ b/plotters/src/coord/ranged1d/types/slice.rs @@ -1,8 +1,6 @@ use crate::coord::ranged1d::{ AsRangedCoord, DefaultFormatting, DiscreteRanged, KeyPointHint, Ranged, }; -use crate::math_errors::MathError; -use crate::math_guard::{float_to_integer_checked, non_zero_checked}; use std::ops::Range; /// A range that is defined by a slice of values. @@ -14,34 +12,23 @@ pub struct RangedSlice<'a, T: PartialEq>(&'a [T]); impl<'a, T: PartialEq> Ranged for RangedSlice<'a, T> { type FormatOption = DefaultFormatting; type ValueType = &'a T; - type ErrorType = MathError; + fn range(&self) -> Range<&'a T> { // If inner slice is empty, we should always panic &self.0[0]..&self.0[self.0.len() - 1] } - fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> Result { + fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { match self.0.iter().position(|x| &x == value) { Some(pos) => { - let pixel_span = (i64::from(limit.1) - i64::from(limit.0)) as f64; - let value_span = self - .0 - .len() - .checked_sub(1) - .ok_or(MathError::ValueUnderflow)?; - - let value_span = - non_zero_checked::(value_span, MathError::ZeroDivision)? - as f64; - - let offset = float_to_integer_checked::( - pixel_span * (pos as f64 / value_span), - MathError::ValueOutOfRange, - )?; - - limit.0.checked_add(offset).ok_or(MathError::ValueOverflow) + let pixel_span = limit.1 - limit.0; + let value_span = self.0.len() - 1; + (f64::from(limit.0) + + f64::from(pixel_span) + * (f64::from(pos as u32) / f64::from(value_span as u32))) + .round() as i32 } - None => Ok(limit.0), + None => limit.0, } } @@ -98,7 +85,7 @@ mod test { slice_range.key_points(6), my_slice.iter().collect::>() ); - assert_eq!(slice_range.map(&&0, (0, 50)), Ok(30)); + assert_eq!(slice_range.map(&&0, (0, 50)), 30); } #[test] diff --git a/plotters/src/coord/ranged2d/cartesian.rs b/plotters/src/coord/ranged2d/cartesian.rs index 35778837..57d2240f 100644 --- a/plotters/src/coord/ranged2d/cartesian.rs +++ b/plotters/src/coord/ranged2d/cartesian.rs @@ -10,7 +10,6 @@ use crate::coord::ranged1d::{KeyPointHint, Ranged, ReversibleRanged}; use crate::coord::{CoordTranslate, ReverseCoordTranslate}; -use crate::math_errors::MathError; use crate::style::ShapeStyle; use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind}; @@ -53,26 +52,23 @@ impl Cartesian2d { h_limit: YH, v_limit: XH, mut draw_mesh: DrawMesh, - ) -> Result<(), E> - where - E: From + From, - { + ) -> Result<(), E> { let (xkp, ykp) = ( self.logic_x.key_points(v_limit), self.logic_y.key_points(h_limit), ); for logic_x in xkp { - let x = self.logic_x.map(&logic_x, self.back_x)?; + let x = self.logic_x.map(&logic_x, self.back_x); draw_mesh(MeshLine::XMesh( (x, self.back_y.0), - (x, self.back_x.1), + (x, self.back_y.1), &logic_x, ))?; } for logic_y in ykp { - let y = self.logic_y.map(&logic_y, self.back_y)?; + let y = self.logic_y.map(&logic_y, self.back_y); draw_mesh(MeshLine::YMesh( (self.back_x.0, y), (self.back_x.1, y), @@ -93,13 +89,13 @@ impl Cartesian2d { self.logic_y.range() } - /// Get the horizontal backend coordinate range where X axis should be drawn - pub fn get_x_axis_pixel_range(&self) -> Result, ::ErrorType> { + /// Get the horizental backend coordinate range where X axis should be drawn + pub fn get_x_axis_pixel_range(&self) -> Range { self.logic_x.axis_pixel_range(self.back_x) } /// Get the vertical backend coordinate range where Y axis should be drawn - pub fn get_y_axis_pixel_range(&self) -> Result, ::ErrorType> { + pub fn get_y_axis_pixel_range(&self) -> Range { self.logic_y.axis_pixel_range(self.back_y) } @@ -114,34 +110,23 @@ impl Cartesian2d { } } -impl, Y: Ranged> CoordTranslate - for Cartesian2d -{ +impl CoordTranslate for Cartesian2d { type From = (X::ValueType, Y::ValueType); - type ErrorType = MathError; - fn translate(&self, from: &Self::From) -> Result { - let x = self.logic_x.map(&from.0, self.back_x)?; - let y = self.logic_y.map(&from.1, self.back_y)?; - Ok((x, y)) + + fn translate(&self, from: &Self::From) -> BackendCoord { + ( + self.logic_x.map(&from.0, self.back_x), + self.logic_y.map(&from.1, self.back_y), + ) } } -impl, Y: ReversibleRanged> - ReverseCoordTranslate for Cartesian2d -{ - fn reverse_translate( - &self, - input: BackendCoord, - ) -> Result, Self::ErrorType> { - let Some(x) = self.logic_x.unmap(input.0, self.back_x)? else { - return Ok(None); - }; - - let Some(y) = self.logic_y.unmap(input.1, self.back_y)? else { - return Ok(None); - }; - - Ok(Some((x, y))) +impl ReverseCoordTranslate for Cartesian2d { + fn reverse_translate(&self, input: BackendCoord) -> Option { + Some(( + self.logic_x.unmap(input.0, self.back_x)?, + self.logic_y.unmap(input.1, self.back_y)?, + )) } } diff --git a/plotters/src/coord/ranged3d/cartesian3d.rs b/plotters/src/coord/ranged3d/cartesian3d.rs index 3724b927..8719680c 100644 --- a/plotters/src/coord/ranged3d/cartesian3d.rs +++ b/plotters/src/coord/ranged3d/cartesian3d.rs @@ -1,7 +1,6 @@ use super::{ProjectionMatrix, ProjectionMatrixBuilder}; use crate::coord::ranged1d::Ranged; use crate::coord::CoordTranslate; -use crate::math_errors::MathError; use plotters_backend::BackendCoord; use std::ops::Range; @@ -16,12 +15,7 @@ pub struct Cartesian3d { projection: ProjectionMatrix, } -impl< - X: Ranged, - Y: Ranged, - Z: Ranged, - > Cartesian3d -{ +impl Cartesian3d { fn compute_default_size(actual_x: Range, actual_y: Range) -> i32 { (actual_x.end - actual_x.start).min(actual_y.end - actual_y.start) * 4 / 5 } @@ -110,39 +104,28 @@ impl< } /// Do not project, only transform the guest coordinate system - pub fn map_3d( - &self, - x: &X::ValueType, - y: &Y::ValueType, - z: &Z::ValueType, - ) -> Result<(i32, i32, i32), MathError> { - Ok(( - self.logic_x.map(x, (0, self.coord_size.0))?, - self.logic_y.map(y, (0, self.coord_size.1))?, - self.logic_z.map(z, (0, self.coord_size.2))?, - )) + pub fn map_3d(&self, x: &X::ValueType, y: &Y::ValueType, z: &Z::ValueType) -> (i32, i32, i32) { + ( + self.logic_x.map(x, (0, self.coord_size.0)), + self.logic_y.map(y, (0, self.coord_size.1)), + self.logic_z.map(z, (0, self.coord_size.2)), + ) } /// Get the depth of the projection - pub fn projected_depth( - &self, - x: &X::ValueType, - y: &Y::ValueType, - z: &Z::ValueType, - ) -> Result { - Ok(self.projection.projected_depth(self.map_3d(x, y, z)?)) + pub fn projected_depth(&self, x: &X::ValueType, y: &Y::ValueType, z: &Z::ValueType) -> i32 { + self.projection.projected_depth(self.map_3d(x, y, z)) } } impl CoordTranslate for Cartesian3d { type From = (X::ValueType, Y::ValueType, Z::ValueType); - type ErrorType = MathError; - fn translate(&self, coord: &Self::From) -> Result { - let pixel_coord_3d = self.map_3d(&coord.0, &coord.1, &coord.2)?; - Ok(&self.projection * pixel_coord_3d) + fn translate(&self, coord: &Self::From) -> BackendCoord { + let pixel_coord_3d = self.map_3d(&coord.0, &coord.1, &coord.2); + self.projection * pixel_coord_3d } - fn depth(&self, coord: &Self::From) -> Result { + fn depth(&self, coord: &Self::From) -> i32 { self.projected_depth(&coord.0, &coord.1, &coord.2) } } diff --git a/plotters/src/coord/translate.rs b/plotters/src/coord/translate.rs index b94d3b43..222f948a 100644 --- a/plotters/src/coord/translate.rs +++ b/plotters/src/coord/translate.rs @@ -5,11 +5,9 @@ use std::ops::Deref; pub trait CoordTranslate { /// Specifies the object to be translated from type From; - /// Specifies the error type to use - type ErrorType; /// Translate the guest coordinate to the guest coordinate - fn translate(&self, from: &Self::From) -> Result; + fn translate(&self, from: &Self::From) -> BackendCoord; /// Get the Z-value of current coordinate fn depth(&self, _from: &Self::From) -> i32 { @@ -23,8 +21,7 @@ where T: Deref, { type From = C::From; - type ErrorType = C::ErrorType; - fn translate(&self, from: &Self::From) -> Result { + fn translate(&self, from: &Self::From) -> BackendCoord { self.deref().translate(from) } } @@ -37,6 +34,5 @@ pub trait ReverseCoordTranslate: CoordTranslate { /// logic coordinate. /// Note: the return value is an option, because it's possible that the drawing /// coordinate isn't able to be represented in te guest coordinate system - fn reverse_translate(&self, input: BackendCoord) - -> Result, Self::ErrorType>; + fn reverse_translate(&self, input: BackendCoord) -> Option; } diff --git a/plotters/src/drawing/area.rs b/plotters/src/drawing/area.rs index 25ee020d..e8981fea 100644 --- a/plotters/src/drawing/area.rs +++ b/plotters/src/drawing/area.rs @@ -2,7 +2,6 @@ use crate::coord::cartesian::{Cartesian2d, MeshLine}; use crate::coord::ranged1d::{KeyPointHint, Ranged}; use crate::coord::{CoordTranslate, Shift}; use crate::element::{CoordMapper, Drawable, PointCollection}; -use crate::math_errors::MathError; use crate::style::text_anchor::{HPos, Pos, VPos}; use crate::style::{Color, SizeDesc, TextStyle}; @@ -144,8 +143,6 @@ pub enum DrawingAreaErrorKind { SharingError, /// The error caused by invalid layout LayoutError, - /// The error is due math or an invalid value - Math(MathError), } impl std::fmt::Display for DrawingAreaErrorKind { @@ -156,17 +153,10 @@ impl std::fmt::Display for DrawingAreaErrorKind { write!(fmt, "Multiple backend operation in progress") } DrawingAreaErrorKind::LayoutError => write!(fmt, "Bad layout"), - DrawingAreaErrorKind::Math(math_error) => write!(fmt, "math error: {}", math_error), } } } -impl From for DrawingAreaErrorKind { - fn from(err: MathError) -> Self { - DrawingAreaErrorKind::Math(err) - } -} - impl Error for DrawingAreaErrorKind {} #[allow(type_alias_bounds)] @@ -224,12 +214,12 @@ impl DrawingArea } /// Get the range of X of the backend coordinate for current drawing area - pub fn get_x_axis_pixel_range(&self) -> Result, Self::ErrorType> { + pub fn get_x_axis_pixel_range(&self) -> Range { self.coord.get_x_axis_pixel_range() } /// Get the range of Y of the backend coordinate for current drawing area - pub fn get_y_axis_pixel_range(&self) -> Result, Self::ErrorType> { + pub fn get_y_axis_pixel_range(&self) -> Range { self.coord.get_y_axis_pixel_range() } } @@ -313,7 +303,7 @@ impl DrawingArea { pos: CT::From, color: &ColorType, ) -> Result<(), DrawingAreaError> { - let pos = self.coord.translate(&pos)?; + let pos = self.coord.translate(&pos); self.backend_ops(|b| b.draw_pixel(pos, color.to_backend_color())) } @@ -337,7 +327,7 @@ impl DrawingArea { } /// Map coordinate to the backend coordinate - pub fn map_coordinate(&self, coord: &CT::From) -> Result { + pub fn map_coordinate(&self, coord: &CT::From) -> BackendCoord { self.coord.translate(coord) } diff --git a/plotters/src/element/mod.rs b/plotters/src/element/mod.rs index 63759c9a..921b2c0f 100644 --- a/plotters/src/element/mod.rs +++ b/plotters/src/element/mod.rs @@ -155,7 +155,6 @@ ![](https://plotters-rs.github.io/plotters-doc-data/element-3.png) */ use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind}; -use serde::de::Error; use std::borrow::Borrow; mod basic_shapes; @@ -205,7 +204,6 @@ pub use pie::Pie; use crate::coord::CoordTranslate; use crate::drawing::Rect; -use crate::math_errors::MathError; /// A type which is logically a collection of points, under any given coordinate system. /// Note: Ideally, a point collection trait should be any type of which coordinate elements can be @@ -270,13 +268,8 @@ pub struct BackendCoordOnly; impl CoordMapper for BackendCoordOnly { type Output = BackendCoord; - - fn map( - coord_trans: &CT, - from: &CT::From, - rect: &Rect, - ) -> Result { - rect.truncate(coord_trans.translate(from)?) + fn map(coord_trans: &CT, from: &CT::From, rect: &Rect) -> BackendCoord { + rect.truncate(coord_trans.translate(from)) } } @@ -293,9 +286,9 @@ impl CoordMapper for BackendCoordAndZ { coord_trans: &CT, from: &CT::From, rect: &Rect, - ) -> Result<(BackendCoord, i32), Self: ErrorType> { - let coord = rect.truncate(coord_trans.translate(from)?); + ) -> (BackendCoord, i32) { + let coord = rect.truncate(coord_trans.translate(from)); let z = coord_trans.depth(from); - Ok((coord, z)) + (coord, z) } } diff --git a/plotters/src/element/text.rs b/plotters/src/element/text.rs index 3e480210..ecfc150a 100644 --- a/plotters/src/element/text.rs +++ b/plotters/src/element/text.rs @@ -170,7 +170,7 @@ fn test_multi_layout() { let font = FontDesc::new(FontFamily::SansSerif, 20 as f64, FontStyle::Bold); - layout_multiline_text("öäabc", 40, font, |txt| { + layout_multiline_text("öäabcde", 40, font, |txt| { println!("Got: {}", txt); assert!(txt == "öäabc" || txt == "de"); }); diff --git a/plotters/src/evcxr.rs b/plotters/src/evcxr.rs index bd8f15fd..9d7b9666 100644 --- a/plotters/src/evcxr.rs +++ b/plotters/src/evcxr.rs @@ -74,22 +74,19 @@ pub fn evcxr_bitmap_figure< >( size: (u32, u32), draw: Draw, -) -> Result> { +) -> SVGWrapper { const PIXEL_SIZE: usize = 3; - let buf_len = (size.0 as usize) - .checked_mul(size.1 as usize) - .and_then(|n| n.checked_mul(PIXEL_SIZE)) - .ok_or("image buffer size overflow")?; - - let mut buf = vec![0; buf_len]; + let mut buf = vec![0; (size.0 as usize) * (size.1 as usize) * PIXEL_SIZE]; let root = BitMapBackend::with_buffer(&mut buf, size).into_drawing_area(); - draw(root)?; - let mut buffer = String::new(); + draw(root).expect("Drawing failure"); + let mut buffer = "".to_string(); { let mut svg_root = SVGBackend::with_string(&mut buffer, size); - svg_root.blit_bitmap((0, 0), size, &buf)?; + svg_root + .blit_bitmap((0, 0), size, &buf) + .expect("Failure converting to SVG"); } - Ok(SVGWrapper(buffer, String::new())) + SVGWrapper(buffer, "".to_string()) } diff --git a/plotters/src/lib.rs b/plotters/src/lib.rs index 5c878388..b3731333 100644 --- a/plotters/src/lib.rs +++ b/plotters/src/lib.rs @@ -1,5 +1,4 @@ #![warn(missing_docs)] -#![warn(clippy::arithmetic_side_effects)] #![allow(clippy::type_complexity)] #![allow(clippy::doc_overindented_list_items)] #![cfg_attr(doc_cfg, feature(doc_cfg))] @@ -798,11 +797,6 @@ pub mod element; pub mod series; pub mod style; -/// Error handling for crate -pub(crate) mod math_errors; -/// used within crate for ensuring math done doesn't cause unexpected behavior (overflow/underflow etc) -pub(crate) mod math_guard; - /// Evaluation Context for Rust. See [the evcxr crate](https://crates.io/crates/evcxr) for more information. #[cfg(feature = "evcxr")] #[cfg_attr(doc_cfg, doc(cfg(feature = "evcxr")))] diff --git a/plotters/src/series/histogram.rs b/plotters/src/series/histogram.rs index 730f6e17..2574727f 100644 --- a/plotters/src/series/histogram.rs +++ b/plotters/src/series/histogram.rs @@ -6,7 +6,6 @@ use crate::chart::ChartContext; use crate::coord::cartesian::Cartesian2d; use crate::coord::ranged1d::{DiscreteRanged, Ranged}; use crate::element::Rectangle; -use crate::math_errors::MathError; use crate::style::{Color, ShapeStyle, GREEN}; use plotters_backend::DrawingBackend; @@ -203,8 +202,7 @@ where parent: &ChartContext>, ) -> Self where - BR: Ranged, - ACoord: Ranged, + ACoord: Ranged, { let dp = parent.as_coord_spec().x_spec(); @@ -226,8 +224,7 @@ where parent: &ChartContext>, ) -> Self where - BR: Ranged, - ACoord: Ranged, + ACoord: Ranged, { let dp = parent.as_coord_spec().y_spec(); Self::empty(dp) diff --git a/plotters/src/style/color.rs b/plotters/src/style/color.rs index 7f02722c..056755c9 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() || !(0.0..=1.0).contains(&s) { + if !s.is_finite() || s < 0.0 || s > 1.0 { return Err(HSLColorError::SaturationOutOfRange); } - if !l.is_finite() || !(0.0..=1.0).contains(&l) { + if !l.is_finite() || l < 0.0 || l > 1.0 { return Err(HSLColorError::LightnessOutOfRange); } Ok(Self(h, s, l)) @@ -277,18 +277,12 @@ 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 9b02ae79..42b43344 100644 --- a/plotters/src/style/font/ab_glyph.rs +++ b/plotters/src/style/font/ab_glyph.rs @@ -2,11 +2,9 @@ use super::{FontData, FontFamily, FontStyle, LayoutBox}; 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; -use std::{collections::HashMap, convert::TryFrom}; - -use crate::math_guard::{float_to_integer_checked, try_convert_float}; struct FontMap { map: HashMap>, @@ -70,14 +68,12 @@ pub struct FontDataInternal { font_ref: FontRef<'static>, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub enum FontError { /// No idea what the problem is Unknown, /// No font data available for the requested family and style. FontUnavailable, - /// Metrics passed are out of a valid and safe range - InvalidMetrics, } impl Display for FontError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -130,15 +126,10 @@ impl FontData for FontDataInternal { text: &str, mut draw: DrawFunc, ) -> Result, Self::ErrorType> { - let metric_error = FontError::InvalidMetrics; - let size = try_convert_float::(size, metric_error)?; - let font = self.font_ref.as_scaled(size); - + let font = self.font_ref.as_scaled(size as f32); let mut draw = |x: i32, y: i32, c| { let (base_x, base_y) = pos; - let add_x = x.checked_add(base_x).ok_or(metric_error)?; - let add_y = y.checked_add(base_y).ok_or(metric_error)?; - draw(add_x, add_y, c).map_err(|_| FontError::Unknown) + draw(base_x + x, base_y + y, c) }; let mut x_shift = 0f32; let mut prev = None; @@ -150,18 +141,12 @@ impl FontData for FontDataInternal { let glyph = font.scaled_glyph(c); if let Some(q) = font.outline_glyph(glyph) { let rect = q.px_bounds(); - let y_div = size / 2.0 + rect.min.y; - let y_shift = - float_to_integer_checked::(y_div, metric_error)?; + let y_shift = ((size as f32) / 2.0 + rect.min.y) as i32; let x_shift = x_shift as i32; let mut buf = vec![]; q.draw(|x, y, c| buf.push((x, y, c))); for (x, y, c) in buf { - let x = i32::try_from(x).map_err(|_| metric_error)?; - let x_val = x.checked_add(x_shift).ok_or(metric_error)?; - let y = i32::try_from(y).map_err(|_| metric_error)?; - let y_val = y.checked_add(y_shift).ok_or(metric_error)?; - draw(x_val, y_val, c).map_err(|_e| { + draw(x as i32 + x_shift, y as i32 + y_shift, c).map_err(|_e| { // Note: If ever `plotters` adds a tracing or logging crate, // this would be a good place to use it. FontError::Unknown diff --git a/plotters/src/style/font/font_desc.rs b/plotters/src/style/font/font_desc.rs index 27087f93..42f45079 100644 --- a/plotters/src/style/font/font_desc.rs +++ b/plotters/src/style/font/font_desc.rs @@ -153,11 +153,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 dx = max_x.checked_sub(min_x).ok_or(FontError::InvalidFontBox)?; - let dy = max_y.checked_sub(min_y).ok_or(FontError::InvalidFontBox)?; - - let (w, h) = self.get_transform().transform(dx, dy); + 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/ttf.rs b/plotters/src/style/font/ttf.rs index 2b9fda2d..1f7b5037 100644 --- a/plotters/src/style/font/ttf.rs +++ b/plotters/src/style/font/ttf.rs @@ -33,7 +33,6 @@ pub enum FontError { GlyphError(Arc), FontHandleUnavailable, FaceParseError(String), - InvalidFontBox, } impl std::fmt::Display for FontError { @@ -47,7 +46,6 @@ impl std::fmt::Display for FontError { 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::InvalidFontBox => write!(fmt, "Font Box is Invalid"), } } } @@ -81,7 +79,9 @@ 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) From b76c46f3f8b0ac590416e7f20dd120c57c097948 Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Wed, 29 Apr 2026 14:10:54 +0800 Subject: [PATCH 15/25] Starting from plotters-backend, reworking math safety --- plotters-backend/Cargo.toml | 1 + plotters-backend/src/error.rs | 30 +++ plotters-backend/src/lib.rs | 29 +-- .../src/math_errors.rs | 0 plotters-backend/src/math_guard.rs | 201 ++++++++++++++++ plotters-backend/src/rasterizer/polygon.rs | 18 +- plotters-backend/src/rasterizer/rect.rs | 227 +++++++++++++++--- plotters-backend/src/text.rs | 17 +- plotters-svg/src/svg.rs | 7 +- .../chart/context/cartesian2d/draw_impl.rs | 22 +- plotters/src/chart/series.rs | 2 +- plotters/src/lib.rs | 2 + plotters/src/math_guard.rs | 43 ---- plotters/src/style/color.rs | 22 +- plotters/src/style/font/ttf.rs | 4 +- 15 files changed, 478 insertions(+), 147 deletions(-) create mode 100644 plotters-backend/src/error.rs rename {plotters => plotters-backend}/src/math_errors.rs (100%) create mode 100644 plotters-backend/src/math_guard.rs delete mode 100644 plotters/src/math_guard.rs 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..d15f4ad4 --- /dev/null +++ b/plotters-backend/src/error.rs @@ -0,0 +1,30 @@ +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 {} diff --git a/plotters-backend/src/lib.rs b/plotters-backend/src/lib.rs index 90066147..d9d4420a 100644 --- a/plotters-backend/src/lib.rs +++ b/plotters-backend/src/lib.rs @@ -63,10 +63,17 @@ */ 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}; @@ -76,26 +83,6 @@ use text_anchor::{HPos, VPos}; /// 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. @@ -248,7 +235,7 @@ pub trait DrawingBackend: Sized { 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) = trans.transform(x + dx - min_x, y + dy - min_y)?; let (x, y) = (pos.0 + x, pos.1 + y); if x >= 0 && x < w as i32 && y >= 0 && y < h as i32 { self.draw_pixel((x, y), color) diff --git a/plotters/src/math_errors.rs b/plotters-backend/src/math_errors.rs similarity index 100% rename from plotters/src/math_errors.rs rename to plotters-backend/src/math_errors.rs diff --git a/plotters-backend/src/math_guard.rs b/plotters-backend/src/math_guard.rs new file mode 100644 index 00000000..4da5defa --- /dev/null +++ b/plotters-backend/src/math_guard.rs @@ -0,0 +1,201 @@ +use num_traits::{ + CheckedAdd, CheckedDiv, CheckedMul, CheckedNeg, CheckedSub, Float, NumCast, PrimInt, Zero, +}; + +pub(crate) fn float_to_integer_checked(v: F, err: E) -> Result +where + F: Float + NumCast, + I: PrimInt + NumCast, +{ + if !v.is_finite() { + return Err(err); + } + + let min: F = NumCast::from(I::min_value()).ok_or(err)?; + let max: F = NumCast::from(I::max_value()).ok_or(err)?; + + if v < min || v > max { + return Err(err); + } + + NumCast::from(v).ok_or(err) +} + +pub(crate) fn try_convert_float(v: FB, err: E) -> Result +where + FB: Float + NumCast, + FS: Float + NumCast, +{ + if !v.is_finite() { + return Err(err); + } + let out: FS = NumCast::from(v).ok_or(err)?; + Ok(out) +} + +pub(crate) fn non_zero_checked(v: T, err: E) -> Result +where + T: Zero, +{ + if v.is_zero() { + Err(err) + } else { + Ok(v) + } +} + +pub(crate) fn checked_add(lhs: T, rhs: T, err: E) -> Result +where + T: CheckedAdd, +{ + lhs.checked_add(&rhs).ok_or(err) +} + +pub(crate) fn checked_mul(lhs: T, rhs: T, err: E) -> Result +where + T: CheckedMul, +{ + lhs.checked_mul(&rhs).ok_or(err) +} + +pub(crate) fn checked_div(lhs: T, rhs: T, err: E) -> Result +where + T: CheckedDiv, +{ + lhs.checked_div(&rhs).ok_or(err) +} + +pub(crate) fn checked_sub(lhs: T, rhs: T, err: E) -> Result +where + T: CheckedSub, +{ + lhs.checked_sub(&rhs).ok_or(err) +} + +pub(crate) fn checked_neg(v: T, err: E) -> Result +where + T: CheckedNeg, +{ + v.checked_neg().ok_or(err) +} + +#[cfg(test)] +mod tests { + use super::*; + + const ERR: &str = "math error"; + + #[test] + fn float_to_integer_checked_accepts_valid_value() { + assert_eq!(float_to_integer_checked::(42.0, ERR), Ok(42)); + } + + #[test] + fn float_to_integer_checked_rejects_non_finite_values() { + assert_eq!( + float_to_integer_checked::(f64::NAN, ERR), + Err(ERR) + ); + assert_eq!( + float_to_integer_checked::(f64::INFINITY, ERR), + Err(ERR) + ); + assert_eq!( + float_to_integer_checked::(f64::NEG_INFINITY, ERR), + Err(ERR) + ); + } + + #[test] + fn float_to_integer_checked_rejects_out_of_range_values() { + assert_eq!(float_to_integer_checked::(128.0, ERR), Err(ERR)); + + assert_eq!( + float_to_integer_checked::(-129.0, ERR), + Err(ERR) + ); + } + + #[test] + fn try_convert_float_accepts_finite_value() { + assert_eq!(try_convert_float::(1.5, ERR), Ok(1.5_f32)); + } + + #[test] + fn try_convert_float_rejects_non_finite_values() { + assert_eq!(try_convert_float::(f64::NAN, ERR), Err(ERR)); + assert_eq!( + try_convert_float::(f64::INFINITY, ERR), + Err(ERR) + ); + assert_eq!( + try_convert_float::(f64::NEG_INFINITY, ERR), + Err(ERR) + ); + } + + #[test] + fn non_zero_checked_accepts_non_zero_value() { + assert_eq!(non_zero_checked(7_i32, ERR), Ok(7)); + } + + #[test] + fn non_zero_checked_rejects_zero() { + assert_eq!(non_zero_checked(0_i32, ERR), Err(ERR)); + } + + #[test] + fn checked_add_accepts_valid_sum() { + assert_eq!(checked_add(2_i32, 3_i32, ERR), Ok(5)); + } + + #[test] + fn checked_add_rejects_overflow() { + assert_eq!(checked_add(i32::MAX, 1, ERR), Err(ERR)); + } + + #[test] + fn checked_sub_accepts_valid_difference() { + assert_eq!(checked_sub(5_i32, 3_i32, ERR), Ok(2)); + } + + #[test] + fn checked_sub_rejects_overflow() { + assert_eq!(checked_sub(i32::MIN, 1, ERR), Err(ERR)); + } + + #[test] + fn checked_mul_accepts_valid_product() { + assert_eq!(checked_mul(6_i32, 7_i32, ERR), Ok(42)); + } + + #[test] + fn checked_mul_rejects_overflow() { + assert_eq!(checked_mul(i32::MAX, 2, ERR), Err(ERR)); + } + + #[test] + fn checked_div_accepts_valid_quotient() { + assert_eq!(checked_div(8_i32, 2_i32, ERR), Ok(4)); + } + + #[test] + fn checked_div_rejects_division_by_zero() { + assert_eq!(checked_div(8_i32, 0_i32, ERR), Err(ERR)); + } + + #[test] + fn checked_div_rejects_min_divided_by_negative_one() { + assert_eq!(checked_div(i32::MIN, -1, ERR), Err(ERR)); + } + + #[test] + fn checked_neg_accepts_valid_negation() { + assert_eq!(checked_neg(7_i32, ERR), Ok(-7)); + } + + #[test] + fn checked_neg_rejects_min_value() { + assert_eq!(checked_neg(i32::MIN, ERR), Err(ERR)); + } +} diff --git a/plotters-backend/src/rasterizer/polygon.rs b/plotters-backend/src/rasterizer/polygon.rs index a91baf53..53e51a0a 100644 --- a/plotters-backend/src/rasterizer/polygon.rs +++ b/plotters-backend/src/rasterizer/polygon.rs @@ -1,4 +1,5 @@ -use crate::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind}; +#![warn(clippy::arithmetic_side_effects)] +use crate::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind, MathError, math_guard::checked_sub}; use std::cmp::{Ord, Ordering, PartialOrd}; @@ -11,25 +12,26 @@ 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 = checked_sub::(to.0, from.0, MathError::ValueOverflow)? as u32; + 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 { - Edge::horizontal_sweep((from.1, from.0), (to.1, to.0)) + fn vertical_sweep(from: BackendCoord, to: BackendCoord) -> Result, MathError> { + Ok(Edge::horizontal_sweep((from.1, from.0), (to.1, to.0))?) } fn get_master_pos(&self) -> i32 { diff --git a/plotters-backend/src/rasterizer/rect.rs b/plotters-backend/src/rasterizer/rect.rs index cd6c7740..2af2da71 100644 --- a/plotters-backend/src/rasterizer/rect.rs +++ b/plotters-backend/src/rasterizer/rect.rs @@ -1,4 +1,7 @@ -use crate::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind}; +use crate::{ + math_guard::checked_sub, BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind, + MathError, +}; pub fn draw_rect( b: &mut B, @@ -10,48 +13,200 @@ 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(x1, x0, MathError::ValueOverflow)?; + let height = checked_sub(y1, y0, MathError::ValueOverflow)?; 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}; + + #[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_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::ValueOverflow) + )); + } +} diff --git a/plotters-backend/src/text.rs b/plotters-backend/src/text.rs index 1d26005e..ac90c0bc 100644 --- a/plotters-backend/src/text.rs +++ b/plotters-backend/src/text.rs @@ -1,4 +1,6 @@ use super::{BackendColor, BackendCoord}; +use crate::math_guard::checked_neg; +use crate::MathError; use std::error::Error; /// Describes font family. @@ -129,13 +131,16 @@ 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(y, MathError::ValueOverflow)?, x), + FontTransform::Rotate180 => ( + checked_neg(x, MathError::ValueOverflow)?, + checked_neg(y, MathError::ValueOverflow)?, + ), + FontTransform::Rotate270 => (y, checked_neg(x, MathError::ValueOverflow)?), + }) } } 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/math_guard.rs b/plotters/src/math_guard.rs deleted file mode 100644 index 43a22434..00000000 --- a/plotters/src/math_guard.rs +++ /dev/null @@ -1,43 +0,0 @@ -use num_traits::{Float, NumCast, PrimInt, Zero}; - -pub(crate) fn float_to_integer_checked(v: F, err: E) -> Result -where - F: Float + NumCast, - I: PrimInt + NumCast, -{ - if !v.is_finite() { - return Err(err); - } - - let min: F = NumCast::from(I::min_value()).ok_or(err)?; - let max: F = NumCast::from(I::max_value()).ok_or(err)?; - - if v < min || v > max { - return Err(err); - } - - NumCast::from(v).ok_or(err) -} - -pub(crate) fn try_convert_float(v: FB, err: E) -> Result -where - FB: Float + NumCast, - FS: Float + NumCast, -{ - if !v.is_finite() { - return Err(err); - } - let out: FS = NumCast::from(v).ok_or(err)?; - Ok(out) -} - -pub(crate) fn non_zero_checked(v: T, err: E) -> Result -where - T: Zero, -{ - if v.is_zero() { - Err(err) - } else { - Ok(v) - } -} 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/ttf.rs b/plotters/src/style/font/ttf.rs index 1f7b5037..e46f3b5f 100644 --- a/plotters/src/style/font/ttf.rs +++ b/plotters/src/style/font/ttf.rs @@ -79,9 +79,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) From 1973dc9c825460f7a49d44af964034e1cc893af4 Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Wed, 29 Apr 2026 21:41:25 +0800 Subject: [PATCH 16/25] Rewrote the FontError to be unified enum for all fonts --- plotters-backend/src/rasterizer/polygon.rs | 4 +- plotters/src/style/font/ab_glyph.rs | 20 +------- plotters/src/style/font/error.rs | 53 ++++++++++++++++++++++ plotters/src/style/font/font_desc.rs | 9 ++-- plotters/src/style/font/mod.rs | 2 + plotters/src/style/font/naive.rs | 11 ----- plotters/src/style/font/ttf.rs | 29 +----------- 7 files changed, 62 insertions(+), 66 deletions(-) create mode 100644 plotters/src/style/font/error.rs diff --git a/plotters-backend/src/rasterizer/polygon.rs b/plotters-backend/src/rasterizer/polygon.rs index 53e51a0a..913cf82c 100644 --- a/plotters-backend/src/rasterizer/polygon.rs +++ b/plotters-backend/src/rasterizer/polygon.rs @@ -31,7 +31,7 @@ impl Edge { } fn vertical_sweep(from: BackendCoord, to: BackendCoord) -> Result, MathError> { - Ok(Edge::horizontal_sweep((from.1, from.0), (to.1, to.0))?) + Edge::horizontal_sweep((from.1, from.0), (to.1, to.0)) } fn get_master_pos(&self) -> i32 { @@ -158,7 +158,7 @@ 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); 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..90a99b1c --- /dev/null +++ b/plotters/src/style/font/error.rs @@ -0,0 +1,53 @@ +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 {} \ No newline at end of file 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 e46f3b5f..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>> = From 3cbef2f7e7a44c880a6496474bdd643d106de0e6 Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Wed, 29 Apr 2026 21:43:00 +0800 Subject: [PATCH 17/25] Added unit tests for the FontError --- plotters/src/style/font/error.rs | 59 +++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/plotters/src/style/font/error.rs b/plotters/src/style/font/error.rs index 90a99b1c..9591e4f8 100644 --- a/plotters/src/style/font/error.rs +++ b/plotters/src/style/font/error.rs @@ -50,4 +50,61 @@ impl From for FontError { } } -impl std::error::Error for FontError {} \ No newline at end of file +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()); + } +} From 06abce901b89c455690ff35e3fadda1137bc9383 Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Thu, 30 Apr 2026 08:39:17 +0800 Subject: [PATCH 18/25] backend `polygon` module is now math safe --- plotters-backend/src/rasterizer/polygon.rs | 425 +++++++++++++++++++-- 1 file changed, 397 insertions(+), 28 deletions(-) diff --git a/plotters-backend/src/rasterizer/polygon.rs b/plotters-backend/src/rasterizer/polygon.rs index 913cf82c..3f8471ba 100644 --- a/plotters-backend/src/rasterizer/polygon.rs +++ b/plotters-backend/src/rasterizer/polygon.rs @@ -1,7 +1,13 @@ #![warn(clippy::arithmetic_side_effects)] -use crate::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind, MathError, math_guard::checked_sub}; +use crate::{ + math_guard::{checked_add, checked_mul, checked_sub, non_zero_checked}, + BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind, MathError, +}; -use std::cmp::{Ord, Ordering, PartialOrd}; +use std::{ + cmp::{Ord, Ordering, PartialOrd}, + convert::TryFrom, +}; #[derive(Clone, Debug)] struct Edge { @@ -12,7 +18,10 @@ struct Edge { } impl Edge { - fn horizontal_sweep(mut from: BackendCoord, mut to: BackendCoord) -> Result, MathError> { + fn horizontal_sweep( + mut from: BackendCoord, + mut to: BackendCoord, + ) -> Result, MathError> { if from.0 == to.0 { return Ok(None); } @@ -21,7 +30,8 @@ impl Edge { std::mem::swap(&mut from, &mut to); } - let total_epoch = checked_sub::(to.0, from.0, MathError::ValueOverflow)? as u32; + let total_epoch = + checked_sub::(to.0, from.0, MathError::ValueOverflow)? as u32; Ok(Some(Edge { epoch: 0, total_epoch, @@ -34,18 +44,32 @@ impl Edge { 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(self.total_epoch, self.epoch, MathError::ValueUnderflow)?; + 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::(self.epoch, 1, MathError::ValueOverflow)?; + 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::from(self.slave_end), + i64::from(self.slave_begin), + MathError::ValueOverflow, + )?; + let product = + checked_mul(slave_diff, i64::from(self.epoch), MathError::ValueOverflow)? as f64; + let total_epoch = non_zero_checked(f64::from(self.total_epoch), MathError::ZeroDivision)?; + + 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") } } @@ -57,7 +81,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() } } @@ -65,9 +89,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()) } } @@ -96,15 +119,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::(x_span.1, x_span.0, MathError::ValueOverflow)?; + let y_diff = checked_sub::(y_span.1, y_span.0, MathError::ValueOverflow)?; + let horizontal_sweep = x_diff > y_diff; + let last_idx = checked_sub(vertices.len(), 1, MathError::ValueUnderflow)?; 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) @@ -133,8 +157,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); } } @@ -164,7 +188,7 @@ pub fn fill_polygon( active_edge.push(edge_obj); } - idx += 1; + idx = checked_add::(idx, 1, MathError::ValueOverflow)?; } active_edge.sort(); @@ -181,22 +205,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; @@ -242,3 +266,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::ValueOverflow); + } + + #[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::ValueUnderflow)); + } + + #[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::ValueOverflow)); + 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::ValueOverflow) + )); + } +} From f53b764bb9473f74544db3c344950b8674ac1f47 Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Thu, 30 Apr 2026 10:25:19 +0800 Subject: [PATCH 19/25] backend `path` module is now math safe --- plotters-backend/src/lib.rs | 2 +- plotters-backend/src/rasterizer/path.rs | 156 +++++++++++++++++---- plotters-backend/src/rasterizer/polygon.rs | 1 - 3 files changed, 133 insertions(+), 26 deletions(-) diff --git a/plotters-backend/src/lib.rs b/plotters-backend/src/lib.rs index d9d4420a..6dbc70bd 100644 --- a/plotters-backend/src/lib.rs +++ b/plotters-backend/src/lib.rs @@ -170,7 +170,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(()) diff --git a/plotters-backend/src/rasterizer/path.rs b/plotters-backend/src/rasterizer/path.rs index 004461c2..d2863669 100644 --- a/plotters-backend/src/rasterizer/path.rs +++ b/plotters-backend/src/rasterizer/path.rs @@ -1,28 +1,47 @@ -use crate::BackendCoord; +#![warn(clippy::arithmetic_side_effects)] +use crate::{ + math_guard::{checked_add, checked_mul, checked_sub, non_zero_checked}, + 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(to.0, from.0, MathError::ValueOverflow)?); + let dy = i64::from(checked_sub(to.1, from.1, MathError::ValueOverflow)?); - let v = (v.0 as f64 / l, v.1 as f64 / l); + let x2 = checked_mul(dx, dx, MathError::ValueOverflow)?; + let y2 = checked_mul(dy, dy, MathError::ValueOverflow)?; + let sum = checked_add(x2, y2, MathError::ValueOverflow)? as f64; - if flag { + let len = non_zero_checked(sum.sqrt(), MathError::ZeroDivision)?; + + 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 +56,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 +114,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 +135,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 +177,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 +188,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_overflow() { + assert_eq!( + get_dir_vector((i32::MIN, 0), (i32::MAX, 0), false), + Err(MathError::ValueOverflow) + ); + } + + #[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::ValueOverflow) + ); + } } diff --git a/plotters-backend/src/rasterizer/polygon.rs b/plotters-backend/src/rasterizer/polygon.rs index 3f8471ba..3a04c30b 100644 --- a/plotters-backend/src/rasterizer/polygon.rs +++ b/plotters-backend/src/rasterizer/polygon.rs @@ -1,4 +1,3 @@ -#![warn(clippy::arithmetic_side_effects)] use crate::{ math_guard::{checked_add, checked_mul, checked_sub, non_zero_checked}, BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind, MathError, From 30fc80c74f8289cda8d6f2230daf0f90df03b286 Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Thu, 30 Apr 2026 13:04:01 +0800 Subject: [PATCH 20/25] backend `line` module is now math safe --- plotters-backend/src/rasterizer/circle.rs | 1 + plotters-backend/src/rasterizer/line.rs | 343 ++++++++++++++++++++-- plotters-backend/src/rasterizer/path.rs | 1 - 3 files changed, 318 insertions(+), 27 deletions(-) diff --git a/plotters-backend/src/rasterizer/circle.rs b/plotters-backend/src/rasterizer/circle.rs index fa7fc50d..1891c35a 100644 --- a/plotters-backend/src/rasterizer/circle.rs +++ b/plotters-backend/src/rasterizer/circle.rs @@ -1,3 +1,4 @@ +#![warn(clippy::arithmetic_side_effects)] use crate::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind}; fn draw_part_a< diff --git a/plotters-backend/src/rasterizer/line.rs b/plotters-backend/src/rasterizer/line.rs index ae1ddd4c..c0f1ebb1 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, checked_mul, checked_sub, float_to_integer_checked, non_zero_checked, + }, + BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind, MathError, +}; +use std::convert::TryFrom; pub fn draw_line( back: &mut DB, @@ -12,14 +18,21 @@ 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(to.0, from.0, MathError::ValueOverflow)?); + let dy = i64::from(checked_sub(to.1, from.1, MathError::ValueOverflow)?); - if l < 1e-5 { + let x2 = checked_mul(dx, dx, MathError::ValueOverflow)?; + let y2 = checked_mul(dy, dy, MathError::ValueOverflow)?; + let sum = checked_add(x2, y2, MathError::ValueOverflow)? 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_checked(len, MathError::ZeroDivision)?; + let v = (dx as f64 / len, 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 +71,17 @@ pub fn draw_line( } return Ok(()); } - - let steep = (from.0 - to.0).abs() < (from.1 - to.1).abs(); + let dx = checked_sub::( + i64::from(to.0), + i64::from(from.0), + MathError::ValueUnderflow, + )?; + let dy = checked_sub::( + i64::from(to.1), + i64::from(from.1), + MathError::ValueUnderflow, + )?; + let steep = dx.abs() < dy.abs(); if steep { from = (from.1, from.0); @@ -77,8 +99,11 @@ 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(to.1, from.1, MathError::ValueOverflow)?) + / f64::from(non_zero_checked( + checked_sub(to.0, from.0, MathError::ValueOverflow)?, + MathError::ZeroDivision, + )?); let mut put_pixel = |(x, y): BackendCoord, b: f64| { if steep { @@ -87,37 +112,303 @@ pub fn draw_line( back.draw_pixel((x, y), style.color().mix(b)) } }; + let y_max = checked_sub( + i32::try_from(size_limit.1).map_err(|_| MathError::ValueOutOfRange)?, + 1, + MathError::ValueUnderflow, + )?; + + let y_clamped = to.1.min(y_max).max(0); + + let y_delta = checked_sub(y_clamped, from.1, MathError::ValueOverflow)?; + + let y_step_limit = (f64::from(y_delta) / grad).floor() as i32; - 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::try_from(size_limit.1).map_err(|_| MathError::ValueOutOfRange)?, + 2, + MathError::ValueUnderflow, + )?; - 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(y_clamped, from.1, MathError::ValueOverflow)?; - 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(x_offset, from.0, MathError::ValueOverflow)?; + + let x_max = checked_sub( + i32::try_from(size_limit.0).map_err(|_| MathError::ValueOutOfRange)?, + 2, + MathError::ValueUnderflow, + )?; + + let stepped_x = checked_sub( + checked_add(from.0, y_step_limit, MathError::ValueOverflow)?, + 1, + MathError::ValueUnderflow, + )?; + + let batch_limit = to.0.min(x_max).min(stepped_x); + + let batch_delta = checked_sub(batch_start, from.0, MathError::ValueOverflow)?; + + 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 = float_to_integer_checked::(y, MathError::ValueOutOfRange)?; + + let y_next = checked_add(y_i, 1, MathError::ValueOverflow)?; + + 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(batch_limit, 1, MathError::ValueOverflow)?; + let y_floor = y.floor(); + + let y_i = float_to_integer_checked::(y, MathError::ValueOutOfRange)?; + + 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(y_i, 1, MathError::ValueOverflow)?; + + 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}; + + // 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_extreme_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::ValueOverflow) + )); + } + + #[test] + fn diagonal_line_reports_math_error_for_extreme_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::ValueOverflow) + )); + } + + #[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 d2863669..cc2d17fc 100644 --- a/plotters-backend/src/rasterizer/path.rs +++ b/plotters-backend/src/rasterizer/path.rs @@ -1,4 +1,3 @@ -#![warn(clippy::arithmetic_side_effects)] use crate::{ math_guard::{checked_add, checked_mul, checked_sub, non_zero_checked}, BackendCoord, MathError, From 80e3a6a5086683171172d7fa083ffa4c36c981bf Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Thu, 30 Apr 2026 15:22:02 +0800 Subject: [PATCH 21/25] backend began working on `circle` --- plotters-backend/src/rasterizer/circle.rs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/plotters-backend/src/rasterizer/circle.rs b/plotters-backend/src/rasterizer/circle.rs index 1891c35a..fbd7f6af 100644 --- a/plotters-backend/src/rasterizer/circle.rs +++ b/plotters-backend/src/rasterizer/circle.rs @@ -1,6 +1,7 @@ #![warn(clippy::arithmetic_side_effects)] +use crate::math_guard::{checked_add, checked_neg, float_to_integer_checked}; use crate::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind}; - +use crate::math_errors::MathError; fn draw_part_a< B: DrawingBackend, Draw: FnMut(i32, (f64, f64)) -> Result<(), DrawingErrorKind>, @@ -13,9 +14,9 @@ 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 = float_to_integer_checked::((half_width).ceil(), MathError::NonFiniteCalculation)?; + let x1 = float_to_integer_checked::(half_width.floor(), MathError::NonFiniteCalculation)?; + let y0 = (radius as f64 - height).ceil(); for x in x0..=x1 { @@ -34,9 +35,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 = float_to_integer_checked::((from-size).floor(), MathError::NonFiniteCalculation)?; + let from = float_to_integer_checked::(from.floor(), MathError::NonFiniteCalculation)?; + for x in len..=from { + let neg_x = checked_neg::(x, MathError::ValueOutOfRange)?; + check_result!(draw(x, (f64::from(neg_x), f64::from(x)))); } Ok(()) } @@ -68,8 +71,9 @@ fn draw_part_c< check_result!(draw(x, (y0, y1))); } - - for x in x1 + 1..r { + let start = checked_add(x1, 1,MathError::ValueUnderflow)?; + let end = checked_add(x1, r, MathError::ValueUnderflow)?; + for x in start..end { 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 y0 = outer_y0.min(inner_y0); From 9c9911b6e7e4ea381692272c2bd6f1e651f8a25b Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 1 May 2026 21:23:58 +0800 Subject: [PATCH 22/25] backend `cicle` maths safety progress --- plotters-backend/src/rasterizer/circle.rs | 25 +++++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/plotters-backend/src/rasterizer/circle.rs b/plotters-backend/src/rasterizer/circle.rs index fbd7f6af..3733e4c0 100644 --- a/plotters-backend/src/rasterizer/circle.rs +++ b/plotters-backend/src/rasterizer/circle.rs @@ -1,5 +1,5 @@ #![warn(clippy::arithmetic_side_effects)] -use crate::math_guard::{checked_add, checked_neg, float_to_integer_checked}; +use crate::math_guard::{checked_add, checked_neg, checked_sub, float_to_integer_checked}; use crate::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind}; use crate::math_errors::MathError; fn draw_part_a< @@ -14,8 +14,8 @@ fn draw_part_a< - (radius as f64 - height) * (radius as f64 - height)) .sqrt(); - let x0 = float_to_integer_checked::((half_width).ceil(), MathError::NonFiniteCalculation)?; - let x1 = float_to_integer_checked::(half_width.floor(), MathError::NonFiniteCalculation)?; + let x0: i32 = float_to_integer_checked::((half_width).ceil(), MathError::NonFiniteCalculation)?; + let x1: i32 = float_to_integer_checked::(half_width.floor(), MathError::NonFiniteCalculation)?; let y0 = (radius as f64 - height).ceil(); @@ -71,8 +71,8 @@ fn draw_part_c< check_result!(draw(x, (y0, y1))); } - let start = checked_add(x1, 1,MathError::ValueUnderflow)?; - let end = checked_add(x1, r, MathError::ValueUnderflow)?; + let start: i32 = checked_add::(x1, 1,MathError::ValueUnderflow)?; + let end: i32 = checked_add::(x1, r, MathError::ValueUnderflow)?; for x in start..end { 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; @@ -106,13 +106,20 @@ fn draw_sweep_line( let ve = e - e.floor(); if dx == 0 { + let px0 = checked_add::(p0, x0, MathError::ValueUnderflow)?; + let s_ceil = float_to_integer_checked::(s.ceil(), MathError::NonFiniteCalculation)?; + let sy0 = checked_add::(s_ceil, y0, MathError::ValueUnderflow)?; + let e_floor = float_to_integer_checked::(e.floor(), MathError::NonFiniteCalculation)?; + let ey0 = checked_add::(e_floor, y0, MathError::ValueUnderflow)?; check_result!(b.draw_line( - (p0 + x0, s.ceil() as i32 + y0), - (p0 + x0, e.floor() as i32 + y0), + (px0, sy0), + (px0, ey0), &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 sy0sub1 = checked_sub::(sy0, 1, MathError::ValueOverflow)?; + let ey0add1 = checked_add::(ey0, 1, MathError::ValueUnderflow)?; + check_result!(b.draw_pixel((px0, sy0sub1), style.color().mix(vs))); + check_result!(b.draw_pixel((px0, ey0add1), style.color().mix(ve))); } else { check_result!(b.draw_line( (s.ceil() as i32 + x0, p0 + y0), From e06330374e1800d21e42eff0af12548adee62b48 Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Mon, 4 May 2026 21:43:50 +0800 Subject: [PATCH 23/25] Made mathguard concrete, no more generics. --- plotters-backend/src/math_guard.rs | 395 ++++++++++++++++------ plotters-backend/src/rasterizer/circle.rs | 107 +++--- 2 files changed, 355 insertions(+), 147 deletions(-) diff --git a/plotters-backend/src/math_guard.rs b/plotters-backend/src/math_guard.rs index 4da5defa..47540731 100644 --- a/plotters-backend/src/math_guard.rs +++ b/plotters-backend/src/math_guard.rs @@ -1,201 +1,390 @@ -use num_traits::{ - CheckedAdd, CheckedDiv, CheckedMul, CheckedNeg, CheckedSub, Float, NumCast, PrimInt, Zero, -}; - -pub(crate) fn float_to_integer_checked(v: F, err: E) -> Result -where - F: Float + NumCast, - I: PrimInt + NumCast, -{ +use crate::math_errors::MathError; + +pub(crate) fn float_to_i32_checked(v: f64) -> Result { if !v.is_finite() { - return Err(err); + return Err(MathError::NonFiniteCalculation); } - let min: F = NumCast::from(I::min_value()).ok_or(err)?; - let max: F = NumCast::from(I::max_value()).ok_or(err)?; - - if v < min || v > max { - return Err(err); + if v < f64::from(i32::MIN) || v > f64::from(i32::MAX) { + return Err(MathError::ValueOutOfRange); } - NumCast::from(v).ok_or(err) + Ok(v as i32) } -pub(crate) fn try_convert_float(v: FB, err: E) -> Result -where - FB: Float + NumCast, - FS: Float + NumCast, -{ +pub(crate) fn ceil_f64_to_i32(v: f64) -> Result { + float_to_i32_checked(v.ceil()) +} + +pub(crate) fn floor_f64_to_i32(v: f64) -> Result { + float_to_i32_checked(v.floor()) +} + +pub(crate) fn f64_to_f32_checked(v: f64) -> Result { if !v.is_finite() { - return Err(err); + return Err(MathError::NonFiniteCalculation); + } + + if v < f64::from(f32::MIN) || v > f64::from(f32::MAX) { + return Err(MathError::ValueOutOfRange); } - let out: FS = NumCast::from(v).ok_or(err)?; + + let out = v as f32; + + if !out.is_finite() { + return Err(MathError::NonFiniteCalculation); + } + Ok(out) } -pub(crate) fn non_zero_checked(v: T, err: E) -> Result -where - T: Zero, -{ - if v.is_zero() { - Err(err) +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 checked_add(lhs: T, rhs: T, err: E) -> Result -where - T: CheckedAdd, -{ - lhs.checked_add(&rhs).ok_or(err) +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_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(lhs: T, rhs: T, err: E) -> Result -where - T: CheckedMul, -{ - lhs.checked_mul(&rhs).ok_or(err) +pub(crate) fn checked_mul_u32(lhs: u32, rhs: u32) -> Result { + lhs.checked_mul(rhs).ok_or(MathError::ValueOutOfRange) } -pub(crate) fn checked_div(lhs: T, rhs: T, err: E) -> Result -where - T: CheckedDiv, -{ - lhs.checked_div(&rhs).ok_or(err) +pub(crate) fn checked_div_u32(lhs: u32, rhs: u32) -> Result { + lhs.checked_div(rhs).ok_or(MathError::ValueOutOfRange) } -pub(crate) fn checked_sub(lhs: T, rhs: T, err: E) -> Result -where - T: CheckedSub, -{ - lhs.checked_sub(&rhs).ok_or(err) +pub(crate) fn u32_to_i32_checked(v: u32) -> Result { + i32::try_from(v).map_err(|_| MathError::ValueOutOfRange) } -pub(crate) fn checked_neg(v: T, err: E) -> Result -where - T: CheckedNeg, -{ - v.checked_neg().ok_or(err) +pub(crate) fn sqrt_f64_checked(v: f64) -> Result { + if !v.is_finite() || v < 0.0 { + return Err(MathError::NonFiniteCalculation); + } + + Ok(v.sqrt()) } #[cfg(test)] mod tests { use super::*; - const ERR: &str = "math error"; + #[test] + fn float_to_i32_checked_accepts_valid_value() { + assert_eq!(float_to_i32_checked(42.0), Ok(42)); + } + + #[test] + fn float_to_i32_checked_rejects_non_finite_values() { + assert_eq!( + float_to_i32_checked(f64::NAN), + Err(MathError::NonFiniteCalculation) + ); + assert_eq!( + float_to_i32_checked(f64::INFINITY), + Err(MathError::NonFiniteCalculation) + ); + assert_eq!( + float_to_i32_checked(f64::NEG_INFINITY), + Err(MathError::NonFiniteCalculation) + ); + } + + #[test] + fn float_to_i32_checked_rejects_out_of_range_values() { + assert_eq!( + float_to_i32_checked(f64::from(i32::MAX) + 1.0), + Err(MathError::ValueOutOfRange) + ); + + assert_eq!( + float_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 float_to_integer_checked_accepts_valid_value() { - assert_eq!(float_to_integer_checked::(42.0, ERR), Ok(42)); + fn non_zero_u32_rejects_zero() { + assert_eq!(non_zero_u32(0), Err(MathError::ZeroDivision)); } #[test] - fn float_to_integer_checked_rejects_non_finite_values() { + 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!( - float_to_integer_checked::(f64::NAN, ERR), - Err(ERR) + 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!( - float_to_integer_checked::(f64::INFINITY, ERR), - Err(ERR) + 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!( - float_to_integer_checked::(f64::NEG_INFINITY, ERR), - Err(ERR) + checked_sub_i32(i32::MIN, 1), + Err(MathError::ValueOutOfRange) ); } #[test] - fn float_to_integer_checked_rejects_out_of_range_values() { - assert_eq!(float_to_integer_checked::(128.0, ERR), Err(ERR)); + 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!( - float_to_integer_checked::(-129.0, ERR), - Err(ERR) + checked_mul_i32(i32::MAX, 2), + Err(MathError::ValueOutOfRange) ); } #[test] - fn try_convert_float_accepts_finite_value() { - assert_eq!(try_convert_float::(1.5, ERR), Ok(1.5_f32)); + fn checked_div_i32_accepts_valid_quotient() { + assert_eq!(checked_div_i32(8, 2), Ok(4)); } #[test] - fn try_convert_float_rejects_non_finite_values() { - assert_eq!(try_convert_float::(f64::NAN, ERR), Err(ERR)); + fn checked_div_i32_rejects_division_by_zero() { assert_eq!( - try_convert_float::(f64::INFINITY, ERR), - Err(ERR) + 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!( - try_convert_float::(f64::NEG_INFINITY, ERR), - Err(ERR) + checked_neg_i32(i32::MIN), + Err(MathError::ValueOutOfRange) ); } #[test] - fn non_zero_checked_accepts_non_zero_value() { - assert_eq!(non_zero_checked(7_i32, ERR), Ok(7)); + fn checked_add_u32_accepts_valid_sum() { + assert_eq!(checked_add_u32(2, 3), Ok(5)); } #[test] - fn non_zero_checked_rejects_zero() { - assert_eq!(non_zero_checked(0_i32, ERR), Err(ERR)); + fn checked_add_u32_rejects_overflow() { + assert_eq!( + checked_add_u32(u32::MAX, 1), + Err(MathError::ValueOutOfRange) + ); } #[test] - fn checked_add_accepts_valid_sum() { - assert_eq!(checked_add(2_i32, 3_i32, ERR), Ok(5)); + fn checked_sub_u32_accepts_valid_difference() { + assert_eq!(checked_sub_u32(5, 3), Ok(2)); } #[test] - fn checked_add_rejects_overflow() { - assert_eq!(checked_add(i32::MAX, 1, ERR), Err(ERR)); + fn checked_sub_u32_rejects_underflow() { + assert_eq!( + checked_sub_u32(0, 1), + Err(MathError::ValueOutOfRange) + ); } #[test] - fn checked_sub_accepts_valid_difference() { - assert_eq!(checked_sub(5_i32, 3_i32, ERR), Ok(2)); + fn checked_mul_u32_accepts_valid_product() { + assert_eq!(checked_mul_u32(6, 7), Ok(42)); } #[test] - fn checked_sub_rejects_overflow() { - assert_eq!(checked_sub(i32::MIN, 1, ERR), Err(ERR)); + fn checked_mul_u32_rejects_overflow() { + assert_eq!( + checked_mul_u32(u32::MAX, 2), + Err(MathError::ValueOutOfRange) + ); } #[test] - fn checked_mul_accepts_valid_product() { - assert_eq!(checked_mul(6_i32, 7_i32, ERR), Ok(42)); + fn checked_div_u32_accepts_valid_quotient() { + assert_eq!(checked_div_u32(8, 2), Ok(4)); } #[test] - fn checked_mul_rejects_overflow() { - assert_eq!(checked_mul(i32::MAX, 2, ERR), Err(ERR)); + fn checked_div_u32_rejects_division_by_zero() { + assert_eq!( + checked_div_u32(8, 0), + Err(MathError::ValueOutOfRange) + ); } #[test] - fn checked_div_accepts_valid_quotient() { - assert_eq!(checked_div(8_i32, 2_i32, ERR), Ok(4)); + fn u32_to_i32_checked_accepts_in_range_value() { + assert_eq!(u32_to_i32_checked(42), Ok(42)); } #[test] - fn checked_div_rejects_division_by_zero() { - assert_eq!(checked_div(8_i32, 0_i32, ERR), Err(ERR)); + 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 checked_div_rejects_min_divided_by_negative_one() { - assert_eq!(checked_div(i32::MIN, -1, ERR), Err(ERR)); + fn sqrt_f64_checked_accepts_valid_value() { + assert_eq!(sqrt_f64_checked(9.0), Ok(3.0)); } #[test] - fn checked_neg_accepts_valid_negation() { - assert_eq!(checked_neg(7_i32, ERR), Ok(-7)); + fn sqrt_f64_checked_rejects_negative_value() { + assert_eq!( + sqrt_f64_checked(-1.0), + Err(MathError::NonFiniteCalculation) + ); } #[test] - fn checked_neg_rejects_min_value() { - assert_eq!(checked_neg(i32::MIN, ERR), Err(ERR)); + fn sqrt_f64_checked_rejects_non_finite_value() { + assert_eq!( + sqrt_f64_checked(f64::NAN), + Err(MathError::NonFiniteCalculation) + ); } -} +} \ No newline at end of file diff --git a/plotters-backend/src/rasterizer/circle.rs b/plotters-backend/src/rasterizer/circle.rs index 3733e4c0..80d56940 100644 --- a/plotters-backend/src/rasterizer/circle.rs +++ b/plotters-backend/src/rasterizer/circle.rs @@ -1,5 +1,5 @@ #![warn(clippy::arithmetic_side_effects)] -use crate::math_guard::{checked_add, checked_neg, checked_sub, float_to_integer_checked}; +use crate::math_guard::{ceil_f64_to_i32, floor_f64_to_i32, sqrt_f64_checked, checked_neg_i32}; use crate::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind}; use crate::math_errors::MathError; fn draw_part_a< @@ -14,8 +14,8 @@ fn draw_part_a< - (radius as f64 - height) * (radius as f64 - height)) .sqrt(); - let x0: i32 = float_to_integer_checked::((half_width).ceil(), MathError::NonFiniteCalculation)?; - let x1: i32 = float_to_integer_checked::(half_width.floor(), MathError::NonFiniteCalculation)?; + let x0 = ceil_f64_to_i32(half_width)?; + let x1 = floor_f64_to_i32(half_width)?; let y0 = (radius as f64 - height).ceil(); @@ -35,10 +35,10 @@ fn draw_part_b< size: f64, mut draw: Draw, ) -> Result<(), DrawingErrorKind> { - let len = float_to_integer_checked::((from-size).floor(), MathError::NonFiniteCalculation)?; - let from = float_to_integer_checked::(from.floor(), MathError::NonFiniteCalculation)?; + 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::(x, MathError::ValueOutOfRange)?; + let neg_x = checked_neg_i32(x)?; check_result!(draw(x, (f64::from(neg_x), f64::from(x)))); } Ok(()) @@ -54,13 +54,14 @@ fn draw_part_c< ) -> Result<(), DrawingErrorKind> { let half_size = r as f64 / (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 outer_y0 = sqrt_f64_checked(r_limit_f * r_limit_f - x_f * x_f)?; let inner_y0 = r as f64 - 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(); @@ -71,17 +72,18 @@ fn draw_part_c< check_result!(draw(x, (y0, y1))); } - let start: i32 = checked_add::(x1, 1,MathError::ValueUnderflow)?; - let end: i32 = checked_add::(x1, r, MathError::ValueUnderflow)?; + let start = checked_add_i32(x1, 1)?; + let end = checked_add_i32(x1, r)?; for x in start..end { - let outer_y0 = ((r_limit as f64) * (r_limit as f64) - x as f64 * x as f64).sqrt(); + let outer_y0 = sqrt_f64_checked(r_limit_f * r_limit_f - x_f * x_f)?; let inner_y0 = r as f64 - 1.0; let y0 = outer_y0.min(inner_y0); let y1 = x as f64; 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))); } } @@ -98,36 +100,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 { - let px0 = checked_add::(p0, x0, MathError::ValueUnderflow)?; - let s_ceil = float_to_integer_checked::(s.ceil(), MathError::NonFiniteCalculation)?; - let sy0 = checked_add::(s_ceil, y0, MathError::ValueUnderflow)?; - let e_floor = float_to_integer_checked::(e.floor(), MathError::NonFiniteCalculation)?; - let ey0 = checked_add::(e_floor, y0, MathError::ValueUnderflow)?; - check_result!(b.draw_line( - (px0, sy0), - (px0, ey0), - &style.color() - )); - let sy0sub1 = checked_sub::(sy0, 1, MathError::ValueOverflow)?; - let ey0add1 = checked_add::(ey0, 1, MathError::ValueUnderflow)?; - check_result!(b.draw_pixel((px0, sy0sub1), style.color().mix(vs))); - check_result!(b.draw_pixel((px0, ey0add1), 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(()) @@ -139,7 +148,8 @@ 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 rad_sub = checked_sub::(radius.0,radius.1, MathError::ValueOverflow)? as f64; + let a0 = (rad_sub).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( @@ -182,25 +192,34 @@ fn draw_annulus( |h, (f, t)| { let f = f as i32; let t = t as i32; + let center_h = checked_add::(center.0, h, MathError::ValueUnderflow)?; + let center_f = checked_add::(center.1, f, MathError::ValueUnderflow)?; + let center_t = checked_add::(center.1, t, MathError::ValueUnderflow)?; 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::(center.0, h, MathError::ValueOverflow)?; 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::(center.0, f, MathError::ValueUnderflow)?; + let center0_f1 = checked_add::(center0_f, 1, MathError::ValueUnderflow)?; + let center1_h = checked_add::(center.1, h, MathError::ValueUnderflow)?; check_result!(b.draw_line( - (center.0 + f + 1, center.1 + h), - (center.0 + t - 1, center.1 + h), + (center0_f1, center1_h), + (center.0 + t - 1, center1_h), &style.color() )); + let center1subh = checked_sub::(center.1, h, MathError::ValueOverflow)?; + let center0_t = checked_add::(center.0, t, MathError::ValueUnderflow)?; + let center0_tsub1 = checked_sub::(center0_t, 1, MathError::ValueOverflow)?; check_result!(b.draw_line( - (center.0 + f + 1, center.1 - h), - (center.0 + t - 1, center.1 - h), + (center0_f1, center1subh), + (center0_tsub1, center1subh), &style.color() )); From 79cd3fb027bf80a5f4b8875d8ee2c4072b336e9e Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Tue, 5 May 2026 12:15:27 +0800 Subject: [PATCH 24/25] unit test coverage increased for backend --- plotters-backend/src/error.rs | 106 ++++ plotters-backend/src/lib.rs | 355 +++++++++++++ plotters-backend/src/math_errors.rs | 63 +++ plotters-backend/src/math_guard.rs | 451 ++++++++++++++-- plotters-backend/src/rasterizer/circle.rs | 582 ++++++++++++++++----- plotters-backend/src/rasterizer/line.rs | 112 ++-- plotters-backend/src/rasterizer/path.rs | 20 +- plotters-backend/src/rasterizer/polygon.rs | 39 +- plotters-backend/src/rasterizer/rect.rs | 15 +- plotters-backend/src/style.rs | 123 +++++ plotters-backend/src/text.rs | 294 ++++++++++- 11 files changed, 1877 insertions(+), 283 deletions(-) diff --git a/plotters-backend/src/error.rs b/plotters-backend/src/error.rs index d15f4ad4..2160a6ef 100644 --- a/plotters-backend/src/error.rs +++ b/plotters-backend/src/error.rs @@ -28,3 +28,109 @@ impl std::fmt::Display for DrawingErrorKind { } 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 6dbc70bd..45be794e 100644 --- a/plotters-backend/src/lib.rs +++ b/plotters-backend/src/lib.rs @@ -61,6 +61,7 @@ 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; @@ -314,3 +315,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 index 0766cf2f..637b3285 100644 --- a/plotters-backend/src/math_errors.rs +++ b/plotters-backend/src/math_errors.rs @@ -32,3 +32,66 @@ impl fmt::Display for MathError { } 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.clone(); + + 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 index 47540731..d0954047 100644 --- a/plotters-backend/src/math_guard.rs +++ b/plotters-backend/src/math_guard.rs @@ -1,6 +1,6 @@ use crate::math_errors::MathError; - -pub(crate) fn float_to_i32_checked(v: f64) -> Result { +use std::convert::TryFrom; +pub(crate) fn f64_to_i32_checked(v: f64) -> Result { if !v.is_finite() { return Err(MathError::NonFiniteCalculation); } @@ -13,11 +13,11 @@ pub(crate) fn float_to_i32_checked(v: f64) -> Result { } pub(crate) fn ceil_f64_to_i32(v: f64) -> Result { - float_to_i32_checked(v.ceil()) + f64_to_i32_checked(v.ceil()) } pub(crate) fn floor_f64_to_i32(v: f64) -> Result { - float_to_i32_checked(v.floor()) + f64_to_i32_checked(v.floor()) } pub(crate) fn f64_to_f32_checked(v: f64) -> Result { @@ -86,6 +86,26 @@ 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) } @@ -106,6 +126,9 @@ pub(crate) fn u32_to_i32_checked(v: u32) -> Result { i32::try_from(v).map_err(|_| MathError::ValueOutOfRange) } +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); @@ -114,27 +137,97 @@ pub(crate) fn sqrt_f64_checked(v: f64) -> Result { 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!(float_to_i32_checked(42.0), Ok(42)); + assert_eq!(f64_to_i32_checked(42.0), Ok(42)); } #[test] fn float_to_i32_checked_rejects_non_finite_values() { assert_eq!( - float_to_i32_checked(f64::NAN), + f64_to_i32_checked(f64::NAN), Err(MathError::NonFiniteCalculation) ); assert_eq!( - float_to_i32_checked(f64::INFINITY), + f64_to_i32_checked(f64::INFINITY), Err(MathError::NonFiniteCalculation) ); assert_eq!( - float_to_i32_checked(f64::NEG_INFINITY), + f64_to_i32_checked(f64::NEG_INFINITY), Err(MathError::NonFiniteCalculation) ); } @@ -142,12 +235,12 @@ mod tests { #[test] fn float_to_i32_checked_rejects_out_of_range_values() { assert_eq!( - float_to_i32_checked(f64::from(i32::MAX) + 1.0), + f64_to_i32_checked(f64::from(i32::MAX) + 1.0), Err(MathError::ValueOutOfRange) ); assert_eq!( - float_to_i32_checked(f64::from(i32::MIN) - 1.0), + f64_to_i32_checked(f64::from(i32::MIN) - 1.0), Err(MathError::ValueOutOfRange) ); } @@ -223,10 +316,7 @@ mod tests { #[test] fn non_zero_f64_rejects_non_finite_value() { - assert_eq!( - non_zero_f64(f64::NAN), - Err(MathError::NonFiniteCalculation) - ); + assert_eq!(non_zero_f64(f64::NAN), Err(MathError::NonFiniteCalculation)); } #[test] @@ -275,10 +365,7 @@ mod tests { #[test] fn checked_div_i32_rejects_division_by_zero() { - assert_eq!( - checked_div_i32(8, 0), - Err(MathError::ValueOutOfRange) - ); + assert_eq!(checked_div_i32(8, 0), Err(MathError::ValueOutOfRange)); } #[test] @@ -296,10 +383,7 @@ mod tests { #[test] fn checked_neg_i32_rejects_min_value() { - assert_eq!( - checked_neg_i32(i32::MIN), - Err(MathError::ValueOutOfRange) - ); + assert_eq!(checked_neg_i32(i32::MIN), Err(MathError::ValueOutOfRange)); } #[test] @@ -322,10 +406,7 @@ mod tests { #[test] fn checked_sub_u32_rejects_underflow() { - assert_eq!( - checked_sub_u32(0, 1), - Err(MathError::ValueOutOfRange) - ); + assert_eq!(checked_sub_u32(0, 1), Err(MathError::ValueOutOfRange)); } #[test] @@ -348,10 +429,7 @@ mod tests { #[test] fn checked_div_u32_rejects_division_by_zero() { - assert_eq!( - checked_div_u32(8, 0), - Err(MathError::ValueOutOfRange) - ); + assert_eq!(checked_div_u32(8, 0), Err(MathError::ValueOutOfRange)); } #[test] @@ -367,6 +445,29 @@ mod tests { ); } + #[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)); @@ -374,17 +475,299 @@ mod tests { #[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(-1.0), + sqrt_f64_checked(f64::NAN), Err(MathError::NonFiniteCalculation) ); } #[test] - fn sqrt_f64_checked_rejects_non_finite_value() { + 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!( - sqrt_f64_checked(f64::NAN), + 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) ); } -} \ No newline at end of file + + #[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) + ); + } + } +} diff --git a/plotters-backend/src/rasterizer/circle.rs b/plotters-backend/src/rasterizer/circle.rs index 80d56940..449b3480 100644 --- a/plotters-backend/src/rasterizer/circle.rs +++ b/plotters-backend/src/rasterizer/circle.rs @@ -1,7 +1,10 @@ -#![warn(clippy::arithmetic_side_effects)] -use crate::math_guard::{ceil_f64_to_i32, floor_f64_to_i32, sqrt_f64_checked, checked_neg_i32}; -use crate::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind}; 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>, @@ -16,7 +19,7 @@ fn draw_part_a< let x0 = ceil_f64_to_i32(half_width)?; let x1 = floor_f64_to_i32(half_width)?; - + let y0 = (radius as f64 - height).ceil(); for x in x0..=x1 { @@ -52,20 +55,26 @@ 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 = ceil_f64_to_i32(-half_size)?; let x1 = floor_f64_to_i32(half_size)?; for x in x0..x1 { + 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 as f64 - 1.0; + let inner_y0 = r_f - 1.0; let mut y1 = outer_y0.min(inner_y0); 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; } } @@ -75,10 +84,15 @@ fn draw_part_c< let start = checked_add_i32(x1, 1)?; let end = checked_add_i32(x1, r)?; for x in start..end { - let outer_y0 = sqrt_f64_checked(r_limit_f * r_limit_f - x_f * x_f)?; - let inner_y0 = r as f64 - 1.0; + 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))); @@ -148,78 +162,75 @@ fn draw_annulus( radius: (u32, u32), style: &S, ) -> Result<(), DrawingErrorKind> { - let rad_sub = checked_sub::(radius.0,radius.1, MathError::ValueOverflow)? as f64; - let a0 = (rad_sub).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 center_h = checked_add::(center.0, h, MathError::ValueUnderflow)?; - let center_f = checked_add::(center.1, f, MathError::ValueUnderflow)?; - let center_t = checked_add::(center.1, t, MathError::ValueUnderflow)?; + 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_h, center_f), (center_h, center_t), &style.color() )); - let center_sub_h = checked_sub::(center.0, h, MathError::ValueOverflow)?; + + let center_sub_h = checked_sub_i32(center.0, h)?; + check_result!(b.draw_line( (center_sub_h, center_f), (center_sub_h, center_t), &style.color() )); - let center0_f = checked_add::(center.0, f, MathError::ValueUnderflow)?; - let center0_f1 = checked_add::(center0_f, 1, MathError::ValueUnderflow)?; - let center1_h = checked_add::(center.1, h, MathError::ValueUnderflow)?; + + 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( (center0_f1, center1_h), - (center.0 + t - 1, center1_h), + (center0_tsub1, center1_h), &style.color() )); - let center1subh = checked_sub::(center.1, h, MathError::ValueOverflow)?; - let center0_t = checked_add::(center.0, t, MathError::ValueUnderflow)?; - let center0_tsub1 = checked_sub::(center0_t, 1, MathError::ValueOverflow)?; + + let center1_sub_h = checked_sub_i32(center.1, h)?; + check_result!(b.draw_line( - (center0_f1, center1subh), - (center0_tsub1, center1subh), + (center0_f1, center1_sub_h), + (center0_tsub1, center1_sub_h), &style.color() )); @@ -228,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) + })); - 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, - ); + 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_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() )); @@ -314,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 left = center.0 - lx.floor() as i32; - let right = center.0 + lx.floor() as i32; + 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 = 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((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((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(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 c0f1ebb1..dfba885d 100644 --- a/plotters-backend/src/rasterizer/line.rs +++ b/plotters-backend/src/rasterizer/line.rs @@ -1,10 +1,10 @@ use crate::{ math_guard::{ - checked_add, checked_mul, checked_sub, float_to_integer_checked, non_zero_checked, + 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, MathError, + BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind, }; -use std::convert::TryFrom; pub fn draw_line( back: &mut DB, @@ -18,12 +18,12 @@ 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 dx = i64::from(checked_sub(to.0, from.0, MathError::ValueOverflow)?); - let dy = i64::from(checked_sub(to.1, from.1, MathError::ValueOverflow)?); + let dx = i64::from(checked_sub_i32(to.0, from.0)?); + let dy = i64::from(checked_sub_i32(to.1, from.1)?); - let x2 = checked_mul(dx, dx, MathError::ValueOverflow)?; - let y2 = checked_mul(dy, dy, MathError::ValueOverflow)?; - let sum = checked_add(x2, y2, MathError::ValueOverflow)? as f64; + 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(); @@ -31,8 +31,11 @@ pub fn draw_line( return Ok(()); } - let len = non_zero_checked(len, MathError::ZeroDivision)?; - let v = (dx as f64 / len, dy as f64 / len); + 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)]; @@ -71,16 +74,8 @@ pub fn draw_line( } return Ok(()); } - let dx = checked_sub::( - i64::from(to.0), - i64::from(from.0), - MathError::ValueUnderflow, - )?; - let dy = checked_sub::( - i64::from(to.1), - i64::from(from.1), - MathError::ValueUnderflow, - )?; + 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 { @@ -99,11 +94,8 @@ pub fn draw_line( if steep { size_limit = (size_limit.1, size_limit.0); } - let grad = f64::from(checked_sub(to.1, from.1, MathError::ValueOverflow)?) - / f64::from(non_zero_checked( - checked_sub(to.0, from.0, MathError::ValueOverflow)?, - MathError::ZeroDivision, - )?); + 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 { @@ -112,54 +104,38 @@ pub fn draw_line( back.draw_pixel((x, y), style.color().mix(b)) } }; - let y_max = checked_sub( - i32::try_from(size_limit.1).map_err(|_| MathError::ValueOutOfRange)?, - 1, - MathError::ValueUnderflow, - )?; + 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(y_clamped, from.1, MathError::ValueOverflow)?; + let y_delta = checked_sub_i32(y_clamped, from.1)?; - let y_step_limit = (f64::from(y_delta) / grad).floor() as i32; + let y_step_limit = f64_to_i32_checked((f64::from(y_delta) / non_zero_f64(grad)?).floor())?; - let y_max = checked_sub( - i32::try_from(size_limit.1).map_err(|_| MathError::ValueOutOfRange)?, - 2, - MathError::ValueUnderflow, - )?; + let y_max = checked_sub_i32(u32_to_i32_checked(size_limit.1)?, 2)?; let y_clamped = from.1.min(y_max).max(0); - let y_delta = checked_sub(y_clamped, from.1, MathError::ValueOverflow)?; + let y_delta = checked_sub_i32(y_clamped, from.1)?; let x_offset = (f64::from(y_delta) / grad).abs().ceil() as i32; - let batch_start = checked_add(x_offset, from.0, MathError::ValueOverflow)?; + let batch_start = checked_add_i32(x_offset, from.0)?; - let x_max = checked_sub( - i32::try_from(size_limit.0).map_err(|_| MathError::ValueOutOfRange)?, - 2, - MathError::ValueUnderflow, - )?; + let x_max = checked_sub_i32(u32_to_i32_checked(size_limit.0)?, 2)?; - let stepped_x = checked_sub( - checked_add(from.0, y_step_limit, MathError::ValueOverflow)?, - 1, - MathError::ValueUnderflow, - )?; + 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(batch_start, from.0, MathError::ValueOverflow)?; + 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 { - let y_i = float_to_integer_checked::(y, MathError::ValueOutOfRange)?; + let y_i = f64_to_i32_checked(y)?; - let y_next = checked_add(y_i, 1, MathError::ValueOverflow)?; + let y_next = checked_add_i32(y_i, 1)?; let y_floor = y.floor(); @@ -170,10 +146,10 @@ pub fn draw_line( } if to.0 > batch_limit && y < f64::from(to.1) { - let x = checked_add(batch_limit, 1, MathError::ValueOverflow)?; + let x = checked_add_i32(batch_limit, 1)?; let y_floor = y.floor(); - let y_i = float_to_integer_checked::(y, MathError::ValueOutOfRange)?; + let y_i = f64_to_i32_checked(y)?; let lower_alpha = 1.0 + y_floor - y; if lower_alpha > 1e-5 { @@ -181,7 +157,7 @@ pub fn draw_line( } let upper_alpha = y - y_floor; - let y_next = checked_add(y_i, 1, MathError::ValueOverflow)?; + 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)); @@ -193,9 +169,9 @@ pub fn draw_line( #[cfg(test)] mod tests { - // tried keep this unit test inside this file as much as possible. + // tried keep this unit test inside this file as much as possible. use super::*; - use crate::{BackendColor, BackendStyle}; + use crate::{BackendColor, BackendStyle, MathError}; // a simple backend error for testing in this module #[derive(Debug)] @@ -377,29 +353,31 @@ mod tests { } #[test] - fn wide_line_reports_math_error_for_extreme_delta() { + 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::ValueOverflow) - )); + assert!( + matches!(err, DrawingErrorKind::MathError(MathError::ValueOutOfRange)), + "unexpected error: {:?}", + err + ); } #[test] - fn diagonal_line_reports_math_error_for_extreme_x_span() { + 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::ValueOverflow) - )); + assert!( + matches!(err, DrawingErrorKind::MathError(MathError::ValueOutOfRange)), + "unexpected error: {:?}", + err + ); } #[test] diff --git a/plotters-backend/src/rasterizer/path.rs b/plotters-backend/src/rasterizer/path.rs index cc2d17fc..69599e46 100644 --- a/plotters-backend/src/rasterizer/path.rs +++ b/plotters-backend/src/rasterizer/path.rs @@ -1,5 +1,5 @@ use crate::{ - math_guard::{checked_add, checked_mul, checked_sub, non_zero_checked}, + math_guard::{checked_add_i64, checked_mul_i64, checked_sub_i32, non_zero_f64}, BackendCoord, MathError, }; use std::convert::From; @@ -10,14 +10,14 @@ fn get_dir_vector( to: BackendCoord, flag: bool, ) -> Result<((f64, f64), (f64, f64)), MathError> { - let dx = i64::from(checked_sub(to.0, from.0, MathError::ValueOverflow)?); - let dy = i64::from(checked_sub(to.1, from.1, MathError::ValueOverflow)?); + let dx = i64::from(checked_sub_i32(to.0, from.0)?); + let dy = i64::from(checked_sub_i32(to.1, from.1)?); - let x2 = checked_mul(dx, dx, MathError::ValueOverflow)?; - let y2 = checked_mul(dy, dy, MathError::ValueOverflow)?; - let sum = checked_add(x2, y2, MathError::ValueOverflow)? as f64; + 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 = non_zero_checked(sum.sqrt(), MathError::ZeroDivision)?; + let len = non_zero_f64(sum.sqrt())?; let v = (dx as f64 / len, dy as f64 / len); @@ -201,10 +201,10 @@ mod test { } #[test] - fn get_dir_vector_rejects_extreme_delta_overflow() { + fn get_dir_vector_rejects_extreme_delta_out_of_range() { assert_eq!( get_dir_vector((i32::MIN, 0), (i32::MAX, 0), false), - Err(MathError::ValueOverflow) + Err(MathError::ValueOutOfRange) ); } @@ -273,7 +273,7 @@ mod test { fn polygonize_rejects_extreme_coordinate_span() { assert_eq!( polygonize(&[(i32::MIN, 0), (i32::MAX, 0)], 2), - Err(MathError::ValueOverflow) + Err(MathError::ValueOutOfRange) ); } } diff --git a/plotters-backend/src/rasterizer/polygon.rs b/plotters-backend/src/rasterizer/polygon.rs index 3a04c30b..8dcc5b70 100644 --- a/plotters-backend/src/rasterizer/polygon.rs +++ b/plotters-backend/src/rasterizer/polygon.rs @@ -1,5 +1,8 @@ use crate::{ - math_guard::{checked_add, checked_mul, checked_sub, non_zero_checked}, + 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, }; @@ -29,8 +32,7 @@ impl Edge { std::mem::swap(&mut from, &mut to); } - let total_epoch = - checked_sub::(to.0, from.0, MathError::ValueOverflow)? as u32; + let total_epoch = i32_to_u32_checked(checked_sub_i32(to.0, from.0)?)?; Ok(Some(Edge { epoch: 0, total_epoch, @@ -44,24 +46,19 @@ impl Edge { } fn get_master_pos(&self) -> Result { - let epoch_diff = checked_sub(self.total_epoch, self.epoch, MathError::ValueUnderflow)?; + 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) -> Result<(), MathError> { - self.epoch = checked_add::(self.epoch, 1, MathError::ValueOverflow)?; + self.epoch = checked_add_u32(self.epoch, 1)?; Ok(()) } fn get_slave_pos(&self) -> Result { - let slave_diff = checked_sub( - i64::from(self.slave_end), - i64::from(self.slave_begin), - MathError::ValueOverflow, - )?; - let product = - checked_mul(slave_diff, i64::from(self.epoch), MathError::ValueOverflow)? as f64; - let total_epoch = non_zero_checked(f64::from(self.total_epoch), MathError::ZeroDivision)?; + 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) } @@ -118,10 +115,10 @@ 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 x_diff = checked_sub::(x_span.1, x_span.0, MathError::ValueOverflow)?; - let y_diff = checked_sub::(y_span.1, y_span.0, MathError::ValueOverflow)?; + 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(vertices.len(), 1, MathError::ValueUnderflow)?; + let last_idx = checked_sub_usize(vertices.len(), 1)?; let mut edges: Vec<_> = vertices .iter() .zip(vertices.iter().skip(1)) @@ -187,7 +184,7 @@ pub fn fill_polygon( active_edge.push(edge_obj); } - idx = checked_add::(idx, 1, MathError::ValueOverflow)?; + idx = checked_add_usize(idx, 1)?; } active_edge.sort(); @@ -366,7 +363,7 @@ mod tests { 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::ValueOverflow); + assert_eq!(err, MathError::ValueOutOfRange); } #[test] @@ -400,7 +397,7 @@ mod tests { slave_end: 10, }; - assert_eq!(edge.get_master_pos(), Err(MathError::ValueUnderflow)); + assert_eq!(edge.get_master_pos(), Err(MathError::ValueOutOfRange)); } #[test] @@ -437,7 +434,7 @@ mod tests { slave_end: 10, }; - assert_eq!(edge.inc_epoch(), Err(MathError::ValueOverflow)); + assert_eq!(edge.inc_epoch(), Err(MathError::ValueOutOfRange)); assert_eq!(edge.epoch, u32::MAX); } @@ -606,7 +603,7 @@ mod tests { dbg!(&err); assert!(matches!( err, - DrawingErrorKind::MathError(MathError::ValueOverflow) + DrawingErrorKind::MathError(MathError::ValueOutOfRange) )); } } diff --git a/plotters-backend/src/rasterizer/rect.rs b/plotters-backend/src/rasterizer/rect.rs index 2af2da71..427d7d3d 100644 --- a/plotters-backend/src/rasterizer/rect.rs +++ b/plotters-backend/src/rasterizer/rect.rs @@ -1,6 +1,5 @@ use crate::{ - math_guard::checked_sub, BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind, - MathError, + math_guard::checked_sub_i32, BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind, }; pub fn draw_rect( @@ -19,8 +18,8 @@ pub fn draw_rect( let x1 = upper_left.0.max(bottom_right.0); let y1 = upper_left.1.max(bottom_right.1); - let width = checked_sub(x1, x0, MathError::ValueOverflow)?; - let height = checked_sub(y1, y0, MathError::ValueOverflow)?; + let width = checked_sub_i32(x1, x0)?; + let height = checked_sub_i32(y1, y0)?; if fill { if width < height { @@ -45,7 +44,7 @@ pub fn draw_rect( #[cfg(test)] mod tests { use super::*; - use crate::{BackendColor, BackendStyle}; + use crate::{BackendColor, BackendStyle, MathError}; #[derive(Debug)] struct TestBackendError; @@ -192,7 +191,7 @@ mod tests { } #[test] - fn rect_with_extreme_coordinates_returns_math_error() { + fn rect_with_extreme_coordinates_returns_out_of_range_math_error() { let mut backend = TestBackend::default(); let err = draw_rect( @@ -206,7 +205,9 @@ mod tests { assert!(matches!( err, - DrawingErrorKind::MathError(MathError::ValueOverflow) + 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 ac90c0bc..f68b89fa 100644 --- a/plotters-backend/src/text.rs +++ b/plotters-backend/src/text.rs @@ -1,6 +1,5 @@ use super::{BackendColor, BackendCoord}; -use crate::math_guard::checked_neg; -use crate::MathError; +use crate::{math_guard::checked_neg_i32, MathError}; use std::error::Error; /// Describes font family. @@ -134,16 +133,12 @@ impl FontTransform { pub fn transform(&self, x: i32, y: i32) -> Result<(i32, i32), MathError> { Ok(match self { FontTransform::None => (x, y), - FontTransform::Rotate90 => (checked_neg(y, MathError::ValueOverflow)?, x), - FontTransform::Rotate180 => ( - checked_neg(x, MathError::ValueOverflow)?, - checked_neg(y, MathError::ValueOverflow)?, - ), - FontTransform::Rotate270 => (y, checked_neg(x, MathError::ValueOverflow)?), + 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 { @@ -234,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()); + } +} From b8df46d0e18f5cdef359c2f4b780e9bf18a3f8a8 Mon Sep 17 00:00:00 2001 From: RPG-Alex Date: Tue, 5 May 2026 21:17:05 +0800 Subject: [PATCH 25/25] `plotters-backend` is now passing for arithmetic --- plotters-backend/src/lib.rs | 64 +++++++++++++++++++---------- plotters-backend/src/math_errors.rs | 2 +- plotters-backend/src/math_guard.rs | 24 +++++++++++ 3 files changed, 67 insertions(+), 23 deletions(-) diff --git a/plotters-backend/src/lib.rs b/plotters-backend/src/lib.rs index 45be794e..e28a6157 100644 --- a/plotters-backend/src/lib.rs +++ b/plotters-backend/src/lib.rs @@ -80,6 +80,12 @@ pub use text::{text_anchor, BackendTextStyle, FontFamily, FontStyle, FontTransfo 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); @@ -221,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 { @@ -266,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. @@ -289,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; diff --git a/plotters-backend/src/math_errors.rs b/plotters-backend/src/math_errors.rs index 637b3285..61387550 100644 --- a/plotters-backend/src/math_errors.rs +++ b/plotters-backend/src/math_errors.rs @@ -41,7 +41,7 @@ mod tests { fn math_error_is_copy_clone_debug_partial_eq_and_eq() { let err = MathError::ValueOutOfRange; let copied = err; - let cloned = err.clone(); + let cloned = err; assert_eq!(err, copied); assert_eq!(err, cloned); diff --git a/plotters-backend/src/math_guard.rs b/plotters-backend/src/math_guard.rs index d0954047..ceab732f 100644 --- a/plotters-backend/src/math_guard.rs +++ b/plotters-backend/src/math_guard.rs @@ -126,6 +126,14 @@ 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) } @@ -770,4 +778,20 @@ mod tests { ); } } + #[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)); + } + } }