Skip to content

Commit 398328e

Browse files
committed
feat: validate HSLColor construction
1 parent ea990af commit 398328e

1 file changed

Lines changed: 87 additions & 11 deletions

File tree

plotters/src/style/color.rs

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use plotters_backend::{BackendColor, BackendStyle};
66
#[cfg(feature = "serialization")]
77
use serde::{Deserialize, Serialize};
88

9+
use std::fmt;
910
use std::marker::PhantomData;
1011

1112
/// Any color representation
@@ -139,12 +140,53 @@ impl BackendStyle for RGBColor {
139140
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
140141
pub struct HSLColor(pub f64, pub f64, pub f64);
141142

143+
/// Errors that can occur when constructing an `HSLColor`.
144+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145+
pub enum HSLColorError {
146+
/// Hue (or degrees input) must be finite.
147+
NonFiniteHue,
148+
/// Saturation must be in the closed interval `[0, 1]`.
149+
SaturationOutOfRange,
150+
/// Lightness must be in the closed interval `[0, 1]`.
151+
LightnessOutOfRange,
152+
}
153+
154+
impl fmt::Display for HSLColorError {
155+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156+
match self {
157+
HSLColorError::NonFiniteHue => f.write_str("hue must be finite"),
158+
HSLColorError::SaturationOutOfRange => f.write_str("saturation must be in [0, 1]"),
159+
HSLColorError::LightnessOutOfRange => f.write_str("lightness must be in [0, 1]"),
160+
}
161+
}
162+
}
163+
164+
impl std::error::Error for HSLColorError {}
165+
142166
impl HSLColor {
167+
/// Creates an `HSLColor` from normalized components, returning an error if any are out of range.
168+
pub fn try_new(h: f64, s: f64, l: f64) -> Result<Self, HSLColorError> {
169+
if !h.is_finite() {
170+
return Err(HSLColorError::NonFiniteHue);
171+
}
172+
if !s.is_finite() || s < 0.0 || s > 1.0 {
173+
return Err(HSLColorError::SaturationOutOfRange);
174+
}
175+
if !l.is_finite() || l < 0.0 || l > 1.0 {
176+
return Err(HSLColorError::LightnessOutOfRange);
177+
}
178+
Ok(Self(h, s, l))
179+
}
180+
143181
/// Creates an `HSLColor` from degrees, wrapping into `[0, 360)` before normalizing.
144-
/// Prefer this helper when specifying hue in degrees.
182+
/// Prefer this helper when specifying hue in degrees. Returns an error if saturation
183+
/// or lightness fall outside `[0, 1]` or if the input hue is non-finite.
145184
#[inline]
146-
pub fn from_degrees(h_deg: f64, s: f64, l: f64) -> Self {
147-
Self(h_deg.rem_euclid(360.0) / 360.0, s, l)
185+
pub fn from_degrees(h_deg: f64, s: f64, l: f64) -> Result<Self, HSLColorError> {
186+
if !h_deg.is_finite() {
187+
return Err(HSLColorError::NonFiniteHue);
188+
}
189+
Self::try_new(h_deg.rem_euclid(360.0) / 360.0, s, l)
148190
}
149191
}
150192

@@ -207,30 +249,64 @@ mod hue_robustness_tests {
207249

208250
#[test]
209251
fn degrees_passed_via_helper_should_work_for_common_cases() {
210-
let red = HSLColor::from_degrees(0.0, 1.0, 0.5).to_backend_color().rgb;
252+
let red = HSLColor::from_degrees(0.0, 1.0, 0.5)
253+
.unwrap()
254+
.to_backend_color()
255+
.rgb;
211256
assert_eq!(red, (255, 0, 0));
212257

213-
let green = HSLColor::from_degrees(120.0, 1.0, 0.5).to_backend_color().rgb;
258+
let green = HSLColor::from_degrees(120.0, 1.0, 0.5)
259+
.unwrap()
260+
.to_backend_color()
261+
.rgb;
214262
assert_eq!(green, (0, 255, 0));
215263

216-
let blue = HSLColor::from_degrees(240.0, 1.0, 0.5).to_backend_color().rgb;
264+
let blue = HSLColor::from_degrees(240.0, 1.0, 0.5)
265+
.unwrap()
266+
.to_backend_color()
267+
.rgb;
217268
assert_eq!(blue, (0, 0, 255));
218269
}
219270

220271
#[test]
221272
fn from_degrees_wraps_and_matches_normalized() {
222273
let normalized = HSLColor(120.0 / 360.0, 1.0, 0.5).to_backend_color().rgb;
223-
let via_helper = HSLColor::from_degrees(120.0, 1.0, 0.5).to_backend_color().rgb;
274+
let via_helper = HSLColor::from_degrees(120.0, 1.0, 0.5)
275+
.unwrap()
276+
.to_backend_color()
277+
.rgb;
224278
assert_eq!(normalized, via_helper);
225279

226280
let wrap_positive =
227-
HSLColor::from_degrees(720.0, 1.0, 0.5).to_backend_color().rgb;
281+
HSLColor::from_degrees(720.0, 1.0, 0.5).unwrap().to_backend_color().rgb;
228282
let wrap_negative =
229-
HSLColor::from_degrees(-120.0, 1.0, 0.5).to_backend_color().rgb;
283+
HSLColor::from_degrees(-120.0, 1.0, 0.5).unwrap().to_backend_color().rgb;
230284
let canonical =
231-
HSLColor::from_degrees(0.0, 1.0, 0.5).to_backend_color().rgb;
285+
HSLColor::from_degrees(0.0, 1.0, 0.5).unwrap().to_backend_color().rgb;
232286

233287
assert_eq!(wrap_positive, canonical);
234-
assert_eq!(wrap_negative, HSLColor::from_degrees(240.0, 1.0, 0.5).to_backend_color().rgb);
288+
assert_eq!(
289+
wrap_negative,
290+
HSLColor::from_degrees(240.0, 1.0, 0.5)
291+
.unwrap()
292+
.to_backend_color()
293+
.rgb
294+
);
295+
}
296+
297+
#[test]
298+
fn from_degrees_rejects_out_of_range_components() {
299+
assert!(matches!(
300+
HSLColor::from_degrees(0.0, -0.1, 0.5),
301+
Err(HSLColorError::SaturationOutOfRange)
302+
));
303+
assert!(matches!(
304+
HSLColor::from_degrees(0.0, 0.5, 1.1),
305+
Err(HSLColorError::LightnessOutOfRange)
306+
));
307+
assert!(matches!(
308+
HSLColor::from_degrees(f64::INFINITY, 0.5, 0.5),
309+
Err(HSLColorError::NonFiniteHue)
310+
));
235311
}
236312
}

0 commit comments

Comments
 (0)