fast image operations
-rw-r--r--Cargo.toml13
-rw-r--r--README.md2
-rw-r--r--benches/scaling.rs21
-rw-r--r--src/lib.rs3
-rw-r--r--src/scale.rs42
-rw-r--r--src/scale/algorithms.rs140
-rw-r--r--src/scale/mod.rs79
-rw-r--r--src/scale/traits.rs91
8 files changed, 343 insertions, 48 deletions
diff --git a/Cargo.toml b/Cargo.toml
index add8cb9..dfa3cdf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "fimg"
-version = "0.4.19"
+version = "0.4.20"
authors = ["bend-n <[email protected]>"]
license = "MIT"
edition = "2021"
@@ -14,9 +14,10 @@ png = { version = "0.17", features = ["unstable"], optional = true }
fontdue = { version = "0.7.3", optional = true }
vecto = "0.1.0"
umath = "0.0.7"
+fr = { version = "0.1.1", package = "fer", optional = true }
[dev-dependencies]
-iai = { path = "../iai" }
+iai = { git = "https://github.com/bend-n/iai.git" }
[[bench]]
name = "overlays"
@@ -34,14 +35,20 @@ path = "benches/affine_transformations.rs"
harness = false
[[bench]]
+name = "scaling"
+path = "benches/scaling.rs"
+harness = false
+
+[[bench]]
name = "tile"
path = "benches/tile.rs"
harness = false
[features]
+scale = ["fr"]
save = ["png"]
text = ["fontdue"]
-default = ["save"]
+default = ["save", "scale"]
[profile.release]
debug = 2
diff --git a/README.md b/README.md
index bf212c0..bc312b3 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ quick simple image operations
- [x] rotation
- [x] flipping
- [x] image tiling
-- [x] nearest neighbor scaling
+- [x] image scaling
- [x] triangle drawing
- [x] simple line drawing
- [x] box drawing
diff --git a/benches/scaling.rs b/benches/scaling.rs
new file mode 100644
index 0000000..389998f
--- /dev/null
+++ b/benches/scaling.rs
@@ -0,0 +1,21 @@
+use fimg::{scale::*, Image};
+
+macro_rules! bench {
+ ($([$a: ident, $alg:ident]),+ $(,)?) => {
+ $(fn $a() {
+ let img: Image<_, 3> = Image::open("tdata/cat.png");
+ iai::black_box(img.scale::<$alg>(267, 178));
+ })+
+
+ iai::main!($($a,)+);
+ };
+}
+bench![
+ [nearest, Nearest],
+ [bilinear, Bilinear],
+ [boxs, Box],
+ [lanczos3, Lanczos3],
+ [catmull, CatmullRom],
+ [mitchell, Mitchell],
+ [hamming, Hamming],
+];
diff --git a/src/lib.rs b/src/lib.rs
index fac8791..4489c17 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -14,8 +14,6 @@
array_chunks
)]
#![warn(
- clippy::missing_docs_in_private_items,
- clippy::multiple_unsafe_ops_per_block,
clippy::undocumented_unsafe_blocks,
clippy::missing_const_for_fn,
clippy::missing_safety_doc,
@@ -34,6 +32,7 @@ pub mod cloner;
mod drawing;
pub(crate) mod math;
mod overlay;
+#[cfg(feature = "scale")]
pub mod scale;
use cloner::ImageCloner;
pub use overlay::{ClonerOverlay, ClonerOverlayAt, Overlay, OverlayAt};
diff --git a/src/scale.rs b/src/scale.rs
deleted file mode 100644
index 36165f7..0000000
--- a/src/scale.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-//! holds scaling operations, at current only the Nearest Neighbor
-use crate::Image;
-
-/// [Nearest Neighbor](https://en.wikipedia.org/wiki/Nearest-neighbor_interpolation) image scaling algorithm implementation.
-/// Use [`Nearest::scale`].
-pub struct Nearest;
-impl Nearest {
- /// Resize a image.
- /// # Safety
- ///
- /// `image` must be as big or bigger than `width`, `height.
- #[must_use = "function does not modify the original image"]
- pub unsafe fn scale<const N: usize>(
- image: Image<&[u8], N>,
- width: u32,
- height: u32,
- ) -> Image<Vec<u8>, N> {
- let x_scale = image.width() as f32 / width as f32;
- let y_scale = image.height() as f32 / height as f32;
- let mut out = Image::alloc(width, height);
- for y in 0..height {
- for x in 0..width {
- let x1 = ((x as f32 + 0.5) * x_scale).floor() as u32;
- let y1 = ((y as f32 + 0.5) * y_scale).floor() as u32;
- // SAFETY: i asked the caller to make sure its ok
- let px = unsafe { image.pixel(x1, y1) };
- // SAFETY: were looping over the width and height of out. its ok.
- unsafe { out.set_pixel(x, y, px) };
- }
- }
- out
- }
-}
-
-#[test]
-fn test_nearest() {
- let i = Image::<_, 3>::open("tdata/cat.png");
- assert_eq!(
- unsafe { Nearest::scale(i.as_ref(), 268, 178) }.buffer,
- Image::<_, 3>::open("tdata/small_cat.png").buffer
- );
-}
diff --git a/src/scale/algorithms.rs b/src/scale/algorithms.rs
new file mode 100644
index 0000000..d1dd6cf
--- /dev/null
+++ b/src/scale/algorithms.rs
@@ -0,0 +1,140 @@
+use super::{traits::*, *};
+use std::num::NonZeroU32;
+
+/// [Nearest Neighbor](https://en.wikipedia.org/wiki/Nearest-neighbor_interpolation) image scaling algorithm.
+pub struct Nearest;
+
+impl ScalingAlgorithm for Nearest {
+ /// Can be used on non opaque too! (Nearest is special like that).
+ fn scale_opaque<const N: usize>(
+ i: Image<&[u8], N>,
+ w: NonZeroU32,
+ h: NonZeroU32,
+ ) -> Image<std::boxed::Box<[u8]>, N>
+ where
+ ChannelCount<N>: ToImageView<N>,
+ {
+ let mut dst = fr::Image::new(w, h);
+ // SAFETY: swear, the pixel types are the same
+ unsafe {
+ fr::Resizer::new(fr::ResizeAlg::Nearest)
+ .resize(&ChannelCount::<N>::wrap(i), &mut dst.view_mut())
+ };
+
+ // SAFETY: ctor
+ unsafe { Image::new(dst.width(), dst.height(), dst.into_vec().into()) }
+ }
+
+ #[inline]
+ fn scale_transparent<const N: usize>(
+ i: Image<&mut [u8], N>,
+ w: NonZeroU32,
+ h: NonZeroU32,
+ ) -> Image<std::boxed::Box<[u8]>, N>
+ where
+ ChannelCount<N>: AlphaDiv<N>,
+ {
+ Self::scale_opaque(i.as_ref(), w, h)
+ }
+}
+
+macro_rules! alg {
+ ($for:ident) => {
+ impl ScalingAlgorithm for $for {
+ fn scale_opaque<const N: usize>(
+ i: Image<&[u8], N>,
+ w: NonZeroU32,
+ h: NonZeroU32,
+ ) -> Image<std::boxed::Box<[u8]>, N>
+ where
+ ChannelCount<N>: ToImageView<N>,
+ {
+ let mut dst = fr::Image::new(w, h);
+ // SAFETY: swear, the pixel types are the same
+ unsafe {
+ fr::Resizer::new(fr::ResizeAlg::Convolution(fr::FilterType::$for))
+ .resize(&ChannelCount::<N>::wrap(i), &mut dst.view_mut())
+ };
+
+ // SAFETY: ctor
+ unsafe { Image::new(dst.width(), dst.height(), dst.into_vec().into()) }
+ }
+
+ fn scale_transparent<const N: usize>(
+ i: Image<&mut [u8], N>,
+ w: NonZeroU32,
+ h: NonZeroU32,
+ ) -> Image<std::boxed::Box<[u8]>, N>
+ where
+ ChannelCount<N>: AlphaDiv<N>,
+ {
+ let mut dst = fr::Image::new(w, h);
+ // SAFETY: yes
+ unsafe {
+ fr::Resizer::new(fr::ResizeAlg::Convolution(fr::FilterType::$for))
+ .resize(&ChannelCount::<N>::handle(i).view(), &mut dst.view_mut())
+ }
+
+ // SAFETY: ctor
+ unsafe { Image::new(dst.width(), dst.height(), dst.into_vec().into()) }
+ }
+ }
+ };
+}
+
+/// [Lanczos](https://en.wikipedia.org/wiki/Lanczos_resampling) scaling with a filter size (*a*) of 3.
+pub struct Lanczos3 {}
+alg!(Lanczos3);
+
+/// [Catmull-Rom](https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline) bicubic filtering.
+pub struct CatmullRom {}
+alg!(CatmullRom);
+
+/// Linear interpolation.
+pub struct Bilinear {}
+alg!(Bilinear);
+
+/// The opposite of [`Nearest`].
+pub struct Box {}
+alg!(Box);
+
+/// Hamming filtering has the same performance as a [`Bilinear`] filter, while
+/// providing image (downscaling) quality comparable to bicubic filters like
+/// [`CatmullRom`] or [`Mitchell`]. Creates a sharper image than [`Bilinear`] filtering,
+/// and doesn't have dislocations on local level like [`Box`] suffers from.
+/// Not recommended for upscaling.
+pub struct Hamming {}
+alg!(Hamming);
+
+/// [Mitchell–Netravali](https://en.wikipedia.org/wiki/Mitchell%E2%80%93Netravali_filters) bicubic filtering.
+pub struct Mitchell {}
+alg!(Mitchell);
+
+impl Nearest {
+ /// Resize a image.
+ /// # Safety
+ ///
+ /// `image` must be as big or bigger than `width`, `height.
+ #[must_use = "function does not modify the original image"]
+ #[deprecated = "use Image::scale instead (note that Image::scale does not support any N. if there is a N you would like to see supported, please open a issue)"]
+ pub unsafe fn scale<const N: usize>(
+ image: Image<&[u8], N>,
+ width: u32,
+ height: u32,
+ ) -> Image<Vec<u8>, N> {
+ let x_scale = image.width() as f32 / width as f32;
+ let y_scale = image.height() as f32 / height as f32;
+ let mut out = Image::alloc(width, height);
+ for y in 0..height {
+ for x in 0..width {
+ let x1 = ((x as f32 + 0.5) * x_scale).floor() as u32;
+ let y1 = ((y as f32 + 0.5) * y_scale).floor() as u32;
+ // SAFETY: i asked the caller to make sure its ok
+ let px = unsafe { image.pixel(x1, y1) };
+ // SAFETY: were looping over the width and height of out. its ok.
+ unsafe { out.set_pixel(x, y, px) };
+ }
+ }
+ out
+ }
+}
diff --git a/src/scale/mod.rs b/src/scale/mod.rs
new file mode 100644
index 0000000..ca68970
--- /dev/null
+++ b/src/scale/mod.rs
@@ -0,0 +1,79 @@
+//! holds scaling operations.
+//!
+//! choose from the wide expanse of options (ordered fastest to slowest):
+//!
+//! - [`Nearest`]: quickest, dumbest, jaggedest, scaling algorithm
+//! - [`Box`]: you want slightly less pixels than nearest? here you go! kinda blurry though.
+//! - [`Bilinear`]: _smooth_ scaling algorithm. rather fuzzy.
+//! - [`Hamming`]: solves the [`Box`] problems. clearer image.
+//! - [`CatmullRom`]: about the same as [`Hamming`], just a little slower.
+//! - [`Mitchell`]: honestly, cant see the difference from [`CatmullRom`].
+//! - [`Lanczos3`]: prettiest scaling algorithm. highly recommend.
+//!
+//! usage:
+//! ```
+//! # use fimg::{Image, scale::Lanczos3};
+//! let i = Image::<_, 3>::open("tdata/small_cat.png");
+//! let scaled = i.scale::<Lanczos3>(2144, 1424);
+//! ```
+use crate::Image;
+
+mod algorithms;
+pub mod traits;
+pub use algorithms::*;
+
+macro_rules! transparent {
+ ($n: literal, $name: ident) => {
+ impl<T: AsMut<[u8]> + AsRef<[u8]>> Image<T, $n> {
+ /// Scale a
+ #[doc = stringify!($name)]
+ /// image with a given scaling algorithm.
+ pub fn scale<A: traits::ScalingAlgorithm>(
+ &mut self,
+ width: u32,
+ height: u32,
+ ) -> Image<std::boxed::Box<[u8]>, $n> {
+ A::scale_transparent(
+ self.as_mut(),
+ width.try_into().unwrap(),
+ height.try_into().unwrap(),
+ )
+ }
+ }
+ };
+}
+
+macro_rules! opaque {
+ ($n: literal, $name: ident) => {
+ impl<T: AsMut<[u8]> + AsRef<[u8]>> Image<T, $n> {
+ /// Scale a
+ #[doc = stringify!($name)]
+ /// image with a given scaling algorithm.
+ pub fn scale<A: traits::ScalingAlgorithm>(
+ &self,
+ width: u32,
+ height: u32,
+ ) -> Image<std::boxed::Box<[u8]>, $n> {
+ A::scale_opaque(
+ self.as_ref(),
+ width.try_into().unwrap(),
+ height.try_into().unwrap(),
+ )
+ }
+ }
+ };
+}
+
+opaque!(1, Y);
+transparent!(2, YA);
+opaque!(3, RGB);
+transparent!(4, RGBA);
+
+#[test]
+fn test_nearest() {
+ let i = Image::<_, 3>::open("tdata/cat.png");
+ assert_eq!(
+ &*i.scale::<Nearest>(268, 178).buffer,
+ &*Image::<_, 3>::open("tdata/small_cat.png").buffer
+ );
+}
diff --git a/src/scale/traits.rs b/src/scale/traits.rs
new file mode 100644
index 0000000..66d1ddb
--- /dev/null
+++ b/src/scale/traits.rs
@@ -0,0 +1,91 @@
+//! implementation detail for scaling. look into if you want to add a algorithm
+use std::num::NonZeroU32;
+
+#[doc(hidden)]
+mod seal {
+ #[doc(hidden)]
+ pub trait Sealed {}
+}
+
+use seal::Sealed;
+
+use crate::Image;
+impl Sealed for ChannelCount<1> {}
+impl Sealed for ChannelCount<2> {}
+impl Sealed for ChannelCount<3> {}
+impl Sealed for ChannelCount<4> {}
+
+/// How to scale a image
+pub trait ScalingAlgorithm {
+ /// Y/Rgb scale
+ fn scale_opaque<const N: usize>(
+ i: Image<&[u8], N>,
+ w: NonZeroU32,
+ h: NonZeroU32,
+ ) -> Image<Box<[u8]>, N>
+ where
+ ChannelCount<N>: ToImageView<N>;
+ /// Ya/Rgba scale
+ fn scale_transparent<const N: usize>(
+ i: Image<&mut [u8], N>,
+ w: NonZeroU32,
+ h: NonZeroU32,
+ ) -> Image<Box<[u8]>, N>
+ where
+ ChannelCount<N>: AlphaDiv<N>;
+}
+
+/// helper
+pub trait ToImageView<const N: usize>: Sealed {
+ #[doc(hidden)]
+ type P: fr::PixelExt + fr::Convolution;
+ #[doc(hidden)]
+ fn wrap(i: Image<&[u8], N>) -> fr::ImageView<Self::P>;
+}
+
+/// helper
+pub trait AlphaDiv<const N: usize>: Sealed + ToImageView<N> {
+ #[doc(hidden)]
+ type P: fr::PixelExt + fr::Convolution + fr::AlphaMulDiv;
+ #[doc(hidden)]
+ fn handle(i: Image<&mut [u8], N>) -> fr::Image<'_, <Self as AlphaDiv<N>>::P>;
+}
+
+/// Generic helper for [`Image`] and [`fr::Image`] transfers.
+pub struct ChannelCount<const N: usize> {}
+
+macro_rules! tiv {
+ ($n:literal, $which:ident) => {
+ impl ToImageView<$n> for ChannelCount<$n> {
+ type P = fr::$which;
+ fn wrap(i: Image<&[u8], $n>) -> fr::ImageView<Self::P> {
+ // SAFETY: same conds
+ unsafe { fr::ImageView::new(i.width, i.height, i.buffer()) }
+ }
+ }
+ };
+}
+
+tiv!(1, U8);
+tiv!(2, U8x2);
+tiv!(3, U8x3);
+tiv!(4, U8x4);
+
+macro_rules! adiv {
+ ($n:literal, $which:ident) => {
+ impl AlphaDiv<$n> for ChannelCount<$n> {
+ type P = fr::$which;
+ fn handle(i: Image<&mut [u8], $n>) -> fr::Image<<Self as AlphaDiv<$n>>::P> {
+ // SAFETY: we kinda have the same conditions
+ let mut i = unsafe { fr::Image::from_slice_u8(i.width, i.height, i.take_buffer()) };
+ // SAFETY: mhm
+ unsafe { fr::MulDiv::default().multiply_alpha_inplace(&mut i.view_mut()) };
+
+ i
+ }
+ }
+ };
+}
+
+adiv!(2, U8x2);
+adiv!(4, U8x4);