fast image operations
Diffstat (limited to 'src/lib.rs')
| -rw-r--r-- | src/lib.rs | 246 |
1 files changed, 184 insertions, 62 deletions
@@ -1,37 +1,42 @@ +//! # fimg +//! +//! Provides fast image operations, such as rotation, flipping, and overlaying. #![feature( slice_swap_unchecked, + generic_const_exprs, slice_as_chunks, unchecked_math, portable_simd, + const_option, array_chunks, test )] #![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, unsafe_op_in_unsafe_fn, clippy::dbg_macro, - clippy::perf + missing_docs )] -#![allow(clippy::zero_prefixed_literal)] +#![allow(clippy::zero_prefixed_literal, incomplete_features)] use std::{num::NonZeroU32, slice::SliceIndex}; mod affine; +pub mod builder; +mod drawing; mod overlay; -pub use affine::{Flips, Rotations}; +pub mod scale; pub use overlay::{Overlay, OverlayAt}; -pub trait RepeatNew { - type Output; - /// Repeat self till it fills a new image of size x, y - /// # Safety - /// - /// UB if self's width is not a multiple of x, or self's height is not a multiple of y - unsafe fn repeated(&self, x: u32, y: u32) -> Self::Output; -} - +/// like assert!(), but causes undefined behaviour at runtime when the condition is not met. +/// +/// # Safety +/// +/// UB if condition is false. macro_rules! assert_unchecked { ($cond:expr) => {{ if !$cond { @@ -46,9 +51,12 @@ macro_rules! assert_unchecked { } use assert_unchecked; -impl RepeatNew for Image<&[u8], 3> { - type Output = Image<Vec<u8>, 3>; - unsafe fn repeated(&self, x: u32, y: u32) -> Self::Output { +impl Image<&[u8], 3> { + /// Repeat self till it fills a new image of size x, y + /// # Safety + /// + /// UB if self's width is not a multiple of x, or self's height is not a multiple of y + pub unsafe fn repeated(&self, x: u32, y: u32) -> Image<Vec<u8>, 3> { let mut img = Image::alloc(x, y); // could probably optimize this a ton but eh for x in 0..(x / self.width()) { for y in 0..(y / self.height()) { @@ -61,54 +69,96 @@ impl RepeatNew for Image<&[u8], 3> { } } +/// calculates a column major index, with unchecked math #[inline] unsafe fn really_unsafe_index(x: u32, y: u32, w: u32) -> usize { // y * w + x + // SAFETY: FIXME make safe math let tmp = unsafe { (y as usize).unchecked_mul(w as usize) }; + // SAFETY: FIXME make safe math unsafe { tmp.unchecked_add(x as usize) } } +/// A image with a variable number of channels, and a nonzero size. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Image<T, const CHANNELS: usize> { - pub buffer: T, - pub width: NonZeroU32, - pub height: NonZeroU32, -} - -impl<const CHANNELS: usize> Default for Image<&'static [u8], CHANNELS> { - fn default() -> Self { - Self { - buffer: &[0; CHANNELS], - width: NonZeroU32::new(1).unwrap(), - height: NonZeroU32::new(1).unwrap(), - } - } + /// column order 2d slice/vec + buffer: T, + /// image horizontal size + width: NonZeroU32, + /// image vertical size + height: NonZeroU32, } impl<T, const CHANNELS: usize> Image<T, CHANNELS> { #[inline] + /// get the height as a [`u32`] pub fn height(&self) -> u32 { self.height.into() } #[inline] + /// get the width as a [`u32`] pub fn width(&self) -> u32 { self.width.into() } #[inline] - pub const fn new(width: NonZeroU32, height: NonZeroU32, buffer: T) -> Self { - Image { + /// create a new image + /// + /// # Safety + /// + /// does not check that buffer.len() == w * h * C + /// + /// using this with invalid values may result in future UB + pub const unsafe fn new(width: NonZeroU32, height: NonZeroU32, buffer: T) -> Self { + Self { buffer, width, height, } } + + /// consumes the image, returning the image buffer + pub fn take_buffer(self) -> T { + self.buffer + } + + /// returns a immutable reference to the backing buffer + pub const fn buffer(&self) -> &T { + &self.buffer + } + + /// returns a mutable(!) reference to the backing buffer + /// + /// # Safety + /// + /// please do not change buffer size. + pub unsafe fn buffer_mut(&mut self) -> &mut T { + &mut self.buffer + } +} + +impl<const CHANNELS: usize, T: Clone> Image<&[T], CHANNELS> { + /// Allocate a new `Image<Vec<T>>` from this imageref. + pub fn to_owned(&self) -> Image<Vec<T>, CHANNELS> { + // SAFETY: we have been constructed already, so must be valid + unsafe { Image::new(self.width, self.height, self.buffer.to_vec()) } + } +} + +impl<const CHANNELS: usize, T: Clone> Image<&mut [T], CHANNELS> { + /// Allocate a new `Image<Vec<T>>` from this mutable imageref. + pub fn to_owned(&self) -> Image<Vec<T>, CHANNELS> { + // SAFETY: we have been constructed already, so must be valid + unsafe { Image::new(self.width, self.height, self.buffer.to_vec()) } + } } impl<const CHANNELS: usize> Image<&[u8], CHANNELS> { #[inline] #[must_use] + /// Copy this ref image pub const fn copy(&self) -> Self { Self { width: self.width, @@ -116,6 +166,28 @@ impl<const CHANNELS: usize> Image<&[u8], CHANNELS> { buffer: self.buffer, } } + + /// Create a new immutable image of width x, y. + /// + /// # Panics + /// + /// if width || height == 0 + /// + /// ``` + /// # use fimg::Image; + /// let img = Image::make::<5, 5>(); + /// # let img: Image<_, 4> = img; + /// ``` + pub const fn make<'a, const WIDTH: u32, const HEIGHT: u32>() -> Image<&'a [u8], CHANNELS> + where + [(); CHANNELS * WIDTH as usize * HEIGHT as usize]: Sized, + { + Image { + width: NonZeroU32::new(WIDTH).expect("passed zero width to builder"), + height: NonZeroU32::new(HEIGHT).expect("passed zero height to builder"), + buffer: &[0; CHANNELS * WIDTH as usize * HEIGHT as usize], + } + } } impl<T: std::ops::Deref<Target = [u8]>, const CHANNELS: usize> Image<T, CHANNELS> { @@ -127,9 +199,12 @@ impl<T: std::ops::Deref<Target = [u8]>, const CHANNELS: usize> Image<T, CHANNELS unsafe fn slice(&self, x: u32, y: u32) -> impl SliceIndex<[u8], Output = [u8]> { debug_assert!(x < self.width(), "x out of bounds"); debug_assert!(y < self.height(), "y out of bounds"); + // SAFETY: me when uncheck math: 😧 let index = unsafe { really_unsafe_index(x, y, self.width()) }; + // SAFETY: 🧐 is unsound? 😖 let index = unsafe { index.unchecked_mul(CHANNELS) }; debug_assert!(self.buffer.len() > index); + // SAFETY: as long as the buffer isnt wrong, this is 😄 index..unsafe { index.unchecked_add(CHANNELS) } } @@ -150,8 +225,11 @@ impl<T: std::ops::Deref<Target = [u8]>, const CHANNELS: usize> Image<T, CHANNELS /// - UB if buffer is too small #[inline] pub unsafe fn pixel(&self, x: u32, y: u32) -> [u8; CHANNELS] { + // SAFETY: we have been told x, y is in bounds let idx = unsafe { self.slice(x, y) }; + // SAFETY: slice always returns a valid index let ptr = unsafe { self.buffer.get_unchecked(idx).as_ptr().cast() }; + // SAFETY: slice always returns a length of `CHANNELS`, so we `cast()` it for convenience. unsafe { *ptr } } } @@ -164,7 +242,9 @@ impl<T: std::ops::DerefMut<Target = [u8]>, const CHANNELS: usize> Image<T, CHANN /// - UB if buffer is too small #[inline] pub unsafe fn pixel_mut(&mut self, x: u32, y: u32) -> &mut [u8] { + // SAFETY: we have been told x, y is in bounds. let idx = unsafe { self.slice(x, y) }; + // SAFETY: slice should always return a valid index unsafe { self.buffer.get_unchecked_mut(idx) } } @@ -192,37 +272,39 @@ impl<T: std::ops::DerefMut<Target = [u8]>, const CHANNELS: usize> Image<T, CHANN } } -pub trait FromRef<const CHANNELS: usize> { - /// Reference the buffer - fn as_ref(&self) -> Image<&[u8], CHANNELS>; -} - -pub trait FromRefMut<const CHANNELS: usize> { - /// Reference the buffer, mutably - fn as_mut(&mut self) -> Image<&mut [u8], CHANNELS>; -} - -impl<const CHANNELS: usize> FromRef<CHANNELS> for Image<&mut [u8], CHANNELS> { - fn as_ref(&self) -> Image<&[u8], CHANNELS> { - Image::new(self.width, self.height, self.buffer) +impl<const CHANNELS: usize> Image<&mut [u8], CHANNELS> { + /// Downcast the mutable reference + pub fn as_ref(&self) -> Image<&[u8], CHANNELS> { + // SAFETY: we got constructed okay, parameters must be valid + unsafe { Image::new(self.width, self.height, self.buffer) } } -} -impl<const CHANNELS: usize> FromRefMut<CHANNELS> for Image<&mut [u8], CHANNELS> { - fn as_mut(&mut self) -> Image<&mut [u8], CHANNELS> { - Image::new(self.width, self.height, self.buffer) + /// Copy this ref image + pub fn copy(&mut self) -> Image<&mut [u8], CHANNELS> { + #[allow(clippy::undocumented_unsafe_blocks)] + unsafe { + Image::new(self.width, self.height, self.buffer) + } } } -impl<const CHANNELS: usize> FromRef<CHANNELS> for Image<Vec<u8>, CHANNELS> { - fn as_ref(&self) -> Image<&[u8], CHANNELS> { - Image::new(self.width, self.height, &self.buffer) +impl<const CHANNELS: usize> Image<Vec<u8>, CHANNELS> { + /// Create a reference to this owned image + pub fn as_ref(&self) -> Image<&[u8], CHANNELS> { + #[allow(clippy::undocumented_unsafe_blocks)] + unsafe { + Image::new(self.width, self.height, &self.buffer) + } } } -impl<const CHANNELS: usize> FromRefMut<CHANNELS> for Image<Vec<u8>, CHANNELS> { - fn as_mut(&mut self) -> Image<&mut [u8], CHANNELS> { - Image::new(self.width, self.height, &mut self.buffer) +impl<const CHANNELS: usize> Image<Vec<u8>, CHANNELS> { + /// Create a mutable reference to this owned image + pub fn as_mut(&mut self) -> Image<&mut [u8], CHANNELS> { + #[allow(clippy::undocumented_unsafe_blocks)] + unsafe { + Image::new(self.width, self.height, &mut self.buffer) + } } } @@ -234,15 +316,27 @@ impl<const CHANNELS: usize> Image<Vec<u8>, CHANNELS> { /// if width || height == 0 #[must_use] pub fn alloc(width: u32, height: u32) -> Self { - Image { + Self { width: width.try_into().unwrap(), height: height.try_into().unwrap(), buffer: vec![0; CHANNELS * width as usize * height as usize], } } } + +/// helper macro for defining the save() method. macro_rules! save { ($channels:literal == $clr:ident ($clrhuman:literal)) => { + impl Image<Vec<u8>, $channels> { + #[cfg(feature = "save")] + #[doc = "Save this "] + #[doc = $clrhuman] + #[doc = " image."] + pub fn save(&self, f: impl AsRef<std::path::Path>) { + self.as_ref().save(f) + } + } + impl Image<&[u8], $channels> { #[cfg(feature = "save")] #[doc = "Save this "] @@ -268,6 +362,29 @@ macro_rules! save { }; } +impl<const CHANNELS: usize> Image<Vec<u8>, CHANNELS> { + #[cfg(feature = "save")] + /// Open a PNG image + pub fn open(f: impl AsRef<std::path::Path>) -> Self { + let p = std::fs::File::open(f).unwrap(); + let r = std::io::BufReader::new(p); + let dec = png::Decoder::new(r); + let mut reader = dec.read_info().unwrap(); + let mut buf = vec![0; reader.output_buffer_size()]; + let info = reader.next_frame(&mut buf).unwrap(); + use png::ColorType::*; + match info.color_type { + Indexed | Grayscale => { + assert_eq!(CHANNELS, 1, "indexed | grayscale requires one channel") + } + Rgb => assert_eq!(CHANNELS, 3, "rgb requires three channels"), + Rgba => assert_eq!(CHANNELS, 4, "rgba requires four channels"), + GrayscaleAlpha => assert_eq!(CHANNELS, 2, "ya requires two channels"), + } + Self::build(info.width, info.height).buf(buf) + } +} + save!(3 == Rgb("RGB")); save!(4 == Rgba("RGBA")); save!(2 == GrayscaleAlpha("YA")); @@ -275,14 +392,19 @@ save!(1 == Grayscale("Y")); #[cfg(test)] macro_rules! img { - [[$($v:literal),+] [$($v2:literal),+]] => {{ - let from: Image<Vec<u8>, 1> = Image::new( - 2.try_into().unwrap(), - 2.try_into().unwrap(), - vec![$($v,)+ $($v2,)+] - ); - from - }} + [[$($v:literal),+] [$($v2:literal),+]] => { + Image::<Vec<u8>, 1>::build(2,2).buf(vec![$($v,)+ $($v2,)+]) + } } #[cfg(test)] use img; + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn repeat() { + let x: Image<&[u8], 3> = Image::build(8, 8).buf(include_bytes!("../benches/3_8x8.imgbuf")); + unsafe { x.repeated(128, 128) }; // repeat 16 times + } +} |