Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 7 additions & 13 deletions src/imageops/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Image Processing Functions
use crate::math::Rect;
use crate::traits::{Lerp, Pixel, Primitive};
use crate::traits::{Pixel, Primitive};
use crate::{GenericImage, GenericImageView, SubImage};

/// Affine transformations
Expand Down Expand Up @@ -257,14 +257,11 @@ pub fn vertical_gradient<S, P, I>(img: &mut I, start: &P, stop: &P)
where
I: GenericImage<Pixel = P>,
P: Pixel<Subpixel = S>,
S: Primitive + Lerp,
S: Primitive,
{
for y in 0..img.height() {
let pixel = start.map2(stop, |a, b| {
let y = <S::Ratio as num_traits::NumCast>::from(y).unwrap();
let height = <S::Ratio as num_traits::NumCast>::from(img.height() - 1).unwrap();
S::lerp(a, b, y / height)
});
let ratio = y as f32 / (img.height() - 1) as f32;
let pixel = start.map2(stop, |a, b| S::lerp(a, b, ratio));

for x in 0..img.width() {
img.put_pixel(x, y, pixel);
Expand All @@ -290,14 +287,11 @@ pub fn horizontal_gradient<S, P, I>(img: &mut I, start: &P, stop: &P)
where
I: GenericImage<Pixel = P>,
P: Pixel<Subpixel = S>,
S: Primitive + Lerp,
S: Primitive,
{
for x in 0..img.width() {
let pixel = start.map2(stop, |a, b| {
let x = <S::Ratio as num_traits::NumCast>::from(x).unwrap();
let width = <S::Ratio as num_traits::NumCast>::from(img.width() - 1).unwrap();
S::lerp(a, b, x / width)
});
let ratio = x as f32 / (img.width() - 1) as f32;
let pixel = start.map2(stop, |a, b| S::lerp(a, b, ratio));

for y in 0..img.height() {
img.put_pixel(x, y, pixel);
Expand Down
41 changes: 40 additions & 1 deletion src/primitive_sealed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::imageops::fast_blur::BlurAccumulator;
/// This trait is `pub` but not exported, so it cannot be implemented outside
/// this crate.
#[allow(private_bounds)]
pub trait PrimitiveSealed: Sized + NearestFrom<f32> + WithBlurAcc + BgraSwizzle {}
pub trait PrimitiveSealed: Sized + NearestFrom<f32> + WithBlurAcc + BgraSwizzle + Lerp {}

impl PrimitiveSealed for usize {}
impl PrimitiveSealed for u8 {}
Expand Down Expand Up @@ -179,3 +179,42 @@ macro_rules! impl_with_blur_acc_f32 {
}

impl_with_blur_acc_f32!(u16, u32, u64, usize, i8, i16, i32, i64, isize, f32, f64);

/// Linear interpolation without loss of precision due to `f32`.
pub(crate) trait Lerp: Sized {
fn lerp(a: Self, b: Self, ratio: f32) -> Self;
}

macro_rules! impl_lerp_with_f32 {
($($t:ty),+) => { $(
impl Lerp for $t {
fn lerp(a: Self, b: Self, ratio: f32) -> Self {
let a = a as f32;
let b = b as f32;
let res = a + (b - a) * ratio;
NearestFrom::nearest_from(res)
}
}
)+ };
}
impl_lerp_with_f32!(u8, u16, i8, i16, f32);

macro_rules! impl_lerp_with_f64_int {
($($t:ty),+) => { $(
impl Lerp for $t {
fn lerp(a: Self, b: Self, ratio: f32) -> Self {
let a = a as f64;
let b = b as f64;
let res = a + (b - a) * ratio as f64;
res.round() as Self
}
}
)+ };
}
impl_lerp_with_f64_int!(u32, u64, usize, i32, i64, isize);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't really lerp 64-bit integer types, including usize, with f64. You'll already have a loss of precision in the initial cast as the mantissa is too small, and unlike for blurring this is highly unexpected. (E.g. it breaks the proposition that the result is contained in the interval between the two endpoints for ratio in [0.0, 1.0]). I think the PR should stick to what was there.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll already have a loss of precision in the initial cast as the mantissa is too small

Sure, but I don't think that's a huge issue. Only two functions use lerp anyway.

I think the PR should stick to what was there.

What do you mean here? "What was there" was that these types didn't implement lerp. That's not an option for a sealed trait. So do you want to keep Lerp public and just fix the clamping?

Copy link
Copy Markdown
Member

@197g 197g May 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, right, we can't provide those traits for only some of the internal types. That's rather ugly, it should not just panic either as it is probably accessible publicly through some paths. Ugh. Note sure then, I'll ponder it a bit.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want an idea for something that doesn't lose precision, I have one.

Ratio is defined as, well, a ratio. In particular, it's the ratio of 2 u32s. So we could change the API to take the ratio as 2 u32s. I.e. fn lerp(a: Self, b: Self, ratio: (u32, u32)) -> Self. Then the implementation can decide what to do with it. In particular, higher-bit integer types can use integer division. E.g. here's u64:

fn lerp(a: u64, b: u64, ratio: (u32, u32)) -> u64 {
    let a: i128 = a as i128;
    let b: i128 = b as i128;
    let res = a + ((b - a) * ratio.0 as i128 + (ratio.1 / 2 as i128)) / ratio.1 as i128;
    res as u64
}

Kind of ugly, but it works.


impl Lerp for f64 {
fn lerp(a: Self, b: Self, ratio: f32) -> Self {
a + (b - a) * ratio as f64
}
}
40 changes: 0 additions & 40 deletions src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,46 +130,6 @@ impl Enlargeable for f64 {
type Larger = f64;
}

/// Linear interpolation without involving floating numbers.
pub trait Lerp: Bounded + NumCast {
type Ratio: Primitive;

fn lerp(a: Self, b: Self, ratio: Self::Ratio) -> Self {
let a = <Self::Ratio as NumCast>::from(a).unwrap();
let b = <Self::Ratio as NumCast>::from(b).unwrap();

let res = a + (b - a) * ratio;

if res > NumCast::from(Self::max_value()).unwrap() {
Self::max_value()
} else if res < NumCast::from(0).unwrap() {
NumCast::from(0).unwrap()
} else {
NumCast::from(res).unwrap()
}
}
}

impl Lerp for u8 {
type Ratio = f32;
}

impl Lerp for u16 {
type Ratio = f32;
}

impl Lerp for u32 {
type Ratio = f64;
}

impl Lerp for f32 {
type Ratio = f32;

fn lerp(a: Self, b: Self, ratio: Self::Ratio) -> Self {
a + (b - a) * ratio
}
}

/// The pixel with an associated `ColorType`.
/// Not all possible pixels represent one of the predefined `ColorType`s.
pub trait PixelWithColorType:
Expand Down
Loading