diff --git a/src/imageops/fast_blur.rs b/src/imageops/fast_blur.rs index a1b9978a92..3b2c387509 100644 --- a/src/imageops/fast_blur.rs +++ b/src/imageops/fast_blur.rs @@ -1,6 +1,7 @@ use num_traits::Bounded; use crate::imageops::filter_1d::{SafeAdd, SafeMul}; +use crate::primitive_sealed::NearestFrom; use crate::{ImageBuffer, Pixel, Primitive}; /// Approximation of Gaussian blur. @@ -136,26 +137,8 @@ fn ceil_to_odd(x: usize) -> usize { } #[inline] -#[allow(clippy::manual_clamp)] fn rounding_saturating_mul(v: f32, w: f32) -> T { - // T::DEFAULT_MAX_VALUE is equal to 1.0 only in cases where storage type if `f32/f64`, - // that means it should be safe to round here. - if T::DEFAULT_MAX_VALUE.to_f32().unwrap() != 1.0 { - T::from( - (v * w) - .round() - .min(T::DEFAULT_MAX_VALUE.to_f32().unwrap()) - .max(T::DEFAULT_MIN_VALUE.to_f32().unwrap()), - ) - .unwrap() - } else { - T::from( - (v * w) - .min(T::DEFAULT_MAX_VALUE.to_f32().unwrap()) - .max(T::DEFAULT_MIN_VALUE.to_f32().unwrap()), - ) - .unwrap() - } + NearestFrom::nearest_from(v * w) } fn box_blur_horizontal_pass_strategy( diff --git a/src/lib.rs b/src/lib.rs index 4cd7e0c37c..96d2df97df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -276,6 +276,7 @@ mod images; /// This is going to be internal. pub mod io; pub mod metadata; +mod primitive_sealed; //TODO delete this module after a few releases mod traits; mod utils; diff --git a/src/primitive_sealed.rs b/src/primitive_sealed.rs new file mode 100644 index 0000000000..e89ba6a148 --- /dev/null +++ b/src/primitive_sealed.rs @@ -0,0 +1,74 @@ +//! Module for crate-private traits implemented for all primitive types. + +/// Crate-private trait to seal the [`Primitive`](crate::Primitive) trait. +/// +/// This trait is `pub` but not exported, so it cannot be implemented outside +/// this crate. +#[allow(private_bounds)] +pub trait PrimitiveSealed: Sized + NearestFrom {} + +impl PrimitiveSealed for usize {} +impl PrimitiveSealed for u8 {} +impl PrimitiveSealed for u16 {} +impl PrimitiveSealed for u32 {} +impl PrimitiveSealed for u64 {} +impl PrimitiveSealed for isize {} +impl PrimitiveSealed for i8 {} +impl PrimitiveSealed for i16 {} +impl PrimitiveSealed for i32 {} +impl PrimitiveSealed for i64 {} +impl PrimitiveSealed for f32 {} +impl PrimitiveSealed for f64 {} + +/// Returns the nearest value of `Self` to a given value. +/// +/// Properties: +/// - For a float -> int conversion: +/// - The float is rounded to the nearest integer. +/// (An implementation may use a fast approximation instead of a precise rounding method.) +/// - NaN is mapped to 0. +/// - Values outside the range of the integer type are clamped to the min or max value. +/// - For a float -> float conversion: +/// - The float is clamped to the range `[0.0, 1.0]`. +/// - NaN is mapped to 0.0. +pub(crate) trait NearestFrom { + fn nearest_from(value: T) -> Self; +} + +impl NearestFrom for u8 { + fn nearest_from(value: f32) -> Self { + // Approximate rounding using the well-known + 0.5 trick. + // This does not handle certain cases correctly. E.g. `0.5_f32.nextdown()` + // is incorrectly rounded to 1 instead of 0. However, this isn't typically + // an issue in practice. + (value + 0.5) as u8 + } +} +impl NearestFrom for u16 { + fn nearest_from(value: f32) -> Self { + (value + 0.5) as u16 + } +} +impl NearestFrom for f32 { + #[allow(clippy::manual_clamp)] // to map NaN to 0.0 + fn nearest_from(value: f32) -> Self { + value.max(0.0).min(1.0) + } +} +impl NearestFrom for f64 { + #[allow(clippy::manual_clamp)] // to map NaN to 0.0 + fn nearest_from(value: f32) -> Self { + value.max(0.0).min(1.0) as f64 + } +} + +macro_rules! impl_nearest_from_f32_for_ints { + ($($t:ty),+) => { $( + impl NearestFrom for $t { + fn nearest_from(value: f32) -> Self { + value.round() as $t + } + } + )+ }; + } +impl_nearest_from_f32_for_ints!(u32, u64, usize, i8, i16, i32, i64, isize); diff --git a/src/traits.rs b/src/traits.rs index 2e6ce7ae0f..2e245156b8 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -6,6 +6,7 @@ use num_traits::{Bounded, Num, NumCast}; use std::ops::AddAssign; use crate::color::{Luma, LumaA, Rgb, Rgba}; +use crate::primitive_sealed::PrimitiveSealed; use crate::ExtendedColorType; /// Types which are safe to treat as an immutable byte slice in a pixel layout @@ -33,27 +34,10 @@ impl EncodableLayout for [f32] { } } -mod sealed { - pub trait PrimitiveSealed: Sized {} -} - -impl sealed::PrimitiveSealed for usize {} -impl sealed::PrimitiveSealed for u8 {} -impl sealed::PrimitiveSealed for u16 {} -impl sealed::PrimitiveSealed for u32 {} -impl sealed::PrimitiveSealed for u64 {} -impl sealed::PrimitiveSealed for isize {} -impl sealed::PrimitiveSealed for i8 {} -impl sealed::PrimitiveSealed for i16 {} -impl sealed::PrimitiveSealed for i32 {} -impl sealed::PrimitiveSealed for i64 {} -impl sealed::PrimitiveSealed for f32 {} -impl sealed::PrimitiveSealed for f64 {} - /// The type of each channel in a pixel. For example, this can be `u8`, `u16`, `f32`. // TODO rename to `PixelComponent`? Split up into separate traits? Seal? pub trait Primitive: - Copy + NumCast + Num + PartialOrd + Clone + Bounded + sealed::PrimitiveSealed + Copy + NumCast + Num + PartialOrd + Clone + Bounded + PrimitiveSealed { /// The maximum value for this type of primitive within the context of color. /// For floats, the maximum is `1.0`, whereas the integer types inherit their usual maximum values.