fast image operations
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs287
1 files changed, 287 insertions, 0 deletions
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..70b91eb
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,287 @@
+#![feature(
+ slice_swap_unchecked,
+ unchecked_math,
+ portable_simd,
+ array_chunks,
+ test
+)]
+#![warn(
+ clippy::multiple_unsafe_ops_per_block,
+ clippy::missing_const_for_fn,
+ clippy::missing_safety_doc,
+ unsafe_op_in_unsafe_fn,
+ clippy::dbg_macro,
+ clippy::perf
+)]
+#![allow(clippy::zero_prefixed_literal)]
+
+use std::{num::NonZeroU32, slice::SliceIndex};
+
+mod affine;
+mod overlay;
+pub use affine::{Flips, Rotations};
+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;
+}
+
+macro_rules! assert_unchecked {
+ ($cond:expr) => {{
+ if !$cond {
+ #[cfg(debug_assertions)]
+ let _ = ::core::ptr::NonNull::<()>::dangling().as_ref(); // force unsafe wrapping block
+ #[cfg(debug_assertions)]
+ panic!("assertion failed: {} returned false", stringify!($cond));
+ #[cfg(not(debug_assertions))]
+ std::hint::unreachable_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 {
+ 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()) {
+ let a: &mut Image<&mut [u8], 3> = &mut img.as_mut();
+ // SAFETY: caller upholds
+ unsafe { a.overlay_at(self, x * self.width(), y * self.height()) };
+ }
+ }
+ img
+ }
+}
+
+#[inline]
+unsafe fn really_unsafe_index(x: u32, y: u32, w: u32) -> usize {
+ // y * w + x
+ let tmp = unsafe { (y as usize).unchecked_mul(w as usize) };
+ unsafe { tmp.unchecked_add(x as usize) }
+}
+
+#[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(),
+ }
+ }
+}
+
+impl<T, const CHANNELS: usize> Image<T, CHANNELS> {
+ #[inline]
+ pub fn height(&self) -> u32 {
+ self.height.into()
+ }
+
+ #[inline]
+ pub fn width(&self) -> u32 {
+ self.width.into()
+ }
+
+ #[inline]
+ pub const fn new(width: NonZeroU32, height: NonZeroU32, buffer: T) -> Self {
+ Image {
+ buffer,
+ width,
+ height,
+ }
+ }
+}
+
+impl<const CHANNELS: usize> Image<&[u8], CHANNELS> {
+ #[inline]
+ #[must_use]
+ pub const fn copy(&self) -> Self {
+ Self {
+ width: self.width,
+ height: self.height,
+ buffer: self.buffer,
+ }
+ }
+}
+
+impl<T: std::ops::Deref<Target = [u8]>, const CHANNELS: usize> Image<T, CHANNELS> {
+ /// # Safety
+ ///
+ /// - UB if x, y is out of bounds
+ /// - UB if buffer is too small
+ #[inline]
+ 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");
+ let index = unsafe { really_unsafe_index(x, y, self.width()) };
+ let index = unsafe { index.unchecked_mul(CHANNELS) };
+ debug_assert!(self.buffer.len() > index);
+ index..unsafe { index.unchecked_add(CHANNELS) }
+ }
+
+ #[inline]
+ /// Returns a iterator over every pixel
+ pub fn chunked(&self) -> impl Iterator<Item = &[u8; CHANNELS]> {
+ // SAFETY: 0 sized images illegal
+ unsafe { assert_unchecked!(self.buffer.len() > CHANNELS) };
+ // SAFETY: no half pixels!
+ unsafe { assert_unchecked!(self.buffer.len() % CHANNELS == 0) };
+ self.buffer.array_chunks::<CHANNELS>()
+ }
+
+ /// Return a pixel at (x, y).
+ /// # Safety
+ ///
+ /// - UB if x, y is out of bounds
+ /// - UB if buffer is too small
+ #[inline]
+ pub unsafe fn pixel(&self, x: u32, y: u32) -> [u8; CHANNELS] {
+ let idx = unsafe { self.slice(x, y) };
+ let ptr = unsafe { self.buffer.get_unchecked(idx).as_ptr().cast() };
+ unsafe { *ptr }
+ }
+}
+
+impl<T: std::ops::DerefMut<Target = [u8]>, const CHANNELS: usize> Image<T, CHANNELS> {
+ /// Return a mutable reference to a pixel at (x, y).
+ /// # Safety
+ ///
+ /// - UB if x, y is out of bounds
+ /// - UB if buffer is too small
+ #[inline]
+ pub unsafe fn pixel_mut(&mut self, x: u32, y: u32) -> &mut [u8] {
+ let idx = unsafe { self.slice(x, y) };
+ unsafe { self.buffer.get_unchecked_mut(idx) }
+ }
+
+ #[inline]
+ /// Returns a iterator over every pixel, mutably
+ pub fn chunked_mut(&mut self) -> impl Iterator<Item = &mut [u8; CHANNELS]> {
+ // SAFETY: 0 sized images are not allowed
+ unsafe { assert_unchecked!(self.buffer.len() > CHANNELS) };
+ // SAFETY: buffer cannot have half pixels
+ unsafe { assert_unchecked!(self.buffer.len() % CHANNELS == 0) };
+ self.buffer.array_chunks_mut::<CHANNELS>()
+ }
+
+ /// Set the pixel at x, y
+ ///
+ /// # Safety
+ ///
+ /// UB if x, y is out of bounds.
+ #[inline]
+ pub unsafe fn set_pixel(&mut self, x: u32, y: u32, px: [u8; CHANNELS]) {
+ // SAFETY: Caller says that x, y is in bounds
+ let out = unsafe { self.pixel_mut(x, y) };
+ // SAFETY: px must be CHANNELS long
+ unsafe { std::ptr::copy_nonoverlapping(px.as_ptr(), out.as_mut_ptr(), CHANNELS) };
+ }
+}
+
+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> FromRefMut<CHANNELS> for Image<&mut [u8], CHANNELS> {
+ fn as_mut(&mut self) -> Image<&mut [u8], CHANNELS> {
+ 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> 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> {
+ /// Allocates a new image
+ ///
+ /// # Panics
+ ///
+ /// if width || height == 0
+ #[must_use]
+ pub fn alloc(width: u32, height: u32) -> Self {
+ Image {
+ width: width.try_into().unwrap(),
+ height: height.try_into().unwrap(),
+ buffer: vec![0; CHANNELS * width as usize * height as usize],
+ }
+ }
+}
+macro_rules! save {
+ ($channels:literal == $clr:ident ($clrhuman:literal)) => {
+ impl Image<&[u8], $channels> {
+ #[cfg(feature = "save")]
+ #[doc = "Save this "]
+ #[doc = $clrhuman]
+ #[doc = " image."]
+ pub fn save(&self, f: impl AsRef<std::path::Path>) {
+ let p = std::fs::File::create(f).unwrap();
+ let w = &mut std::io::BufWriter::new(p);
+ let mut enc = png::Encoder::new(w, self.width(), self.height());
+ enc.set_color(png::ColorType::$clr);
+ enc.set_depth(png::BitDepth::Eight);
+ enc.set_source_gamma(png::ScaledFloat::new(1.0 / 2.2));
+ enc.set_source_chromaticities(png::SourceChromaticities::new(
+ (0.31270, 0.32900),
+ (0.64000, 0.33000),
+ (0.30000, 0.60000),
+ (0.15000, 0.06000),
+ ));
+ let mut writer = enc.write_header().unwrap();
+ writer.write_image_data(self.buffer).unwrap();
+ }
+ }
+ };
+}
+
+save!(3 == Rgb("RGB"));
+save!(4 == Rgba("RGBA"));
+save!(2 == GrayscaleAlpha("YA"));
+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
+ }}
+}
+#[cfg(test)]
+use img;