diff --git a/Cargo.toml b/Cargo.toml index 14421ed..f84495a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fimg" -version = "0.4.21" +version = "0.4.22" authors = ["bend-n "] license = "MIT" edition = "2021" diff --git a/src/convert.rs b/src/convert.rs new file mode 100644 index 0000000..5704f26 --- /dev/null +++ b/src/convert.rs @@ -0,0 +1,64 @@ +//! define From's for images. +//! these conversions are defined by [`PFrom`]. +use crate::{pixels::convert::PFrom, Image}; + +fn map(image: Image<&[u8], A>) -> Image, B> +where + [u8; B]: PFrom, +{ + let buffer = image + .chunked() + .copied() + .flat_map(<[u8; B] as PFrom>::pfrom) + .collect::>() + .into(); + // SAFETY: ctor + unsafe { Image::new(image.width, image.height, buffer) } +} + +macro_rules! convert { + ($a:literal => $b:literal) => { + impl From> for Image, $a> { + fn from(value: Image<&[u8], $b>) -> Self { + map(value) + } + } + }; +} + +macro_rules! cv { + [$($n:literal),+] => { + $(convert!($n => 1); + convert!($n => 2); + convert!($n => 3); + convert!($n => 4);)+ + }; +} + +cv![1, 2, 3, 4]; + +macro_rules! boxconv { + ($a:literal => $b: literal) => { + impl From, $b>> for Image, $a> { + fn from(value: Image, $b>) -> Self { + value.as_ref().into() + } + } + }; +} + +boxconv!(1 => 2); +boxconv!(1 => 3); +boxconv!(1 => 4); + +boxconv!(2 => 1); +boxconv!(2 => 3); +boxconv!(2 => 4); + +boxconv!(3 => 1); +boxconv!(3 => 2); +boxconv!(3 => 4); + +boxconv!(4 => 1); +boxconv!(4 => 2); +boxconv!(4 => 3); diff --git a/src/dyn/affine.rs b/src/dyn/affine.rs new file mode 100644 index 0000000..2c425c0 --- /dev/null +++ b/src/dyn/affine.rs @@ -0,0 +1,38 @@ +use super::{e, DynImage}; + +impl + AsRef<[u8]>> DynImage { + /// Rotate this image 90 degrees clockwise. + /// + /// # Safety + /// + /// UB if this image is not square + pub unsafe fn rot_90(&mut self) { + // SAFETY: caller guarantees + unsafe { e!(self, |i| i.rot_90()) } + } + + /// Rotate this image 180 degrees clockwise. + pub fn rot_180(&mut self) { + e!(self, |i| i.rot_180()) + } + + /// Rotate this image 270 degrees clockwise. + /// + /// # Safety + /// + /// UB if this image is not square + pub unsafe fn rot_270(&mut self) { + // SAFETY: caller guarantees + unsafe { e!(self, |i| i.rot_270()) } + } + + /// Flip this image horizontally. + pub fn flip_h(&mut self) { + e!(self, |i| i.flip_h()) + } + + /// Flip this image vertically. + pub fn flip_v(&mut self) { + e!(self, |i| i.flip_v()) + } +} diff --git a/src/dyn/convert.rs b/src/dyn/convert.rs new file mode 100644 index 0000000..146c73e --- /dev/null +++ b/src/dyn/convert.rs @@ -0,0 +1,110 @@ +#![allow(clippy::useless_conversion)] +use super::{e, DynImage, Image}; + +impl From>> for Image, 1> { + fn from(value: DynImage>) -> Self { + e!(value, |i| i.into()) + } +} + +impl From>> for Image, 2> { + fn from(value: DynImage>) -> Self { + e!(value, |i| i.into()) + } +} +impl From>> for Image, 3> { + fn from(value: DynImage>) -> Self { + e!(value, |i| i.into()) + } +} +impl From>> for Image, 4> { + fn from(value: DynImage>) -> Self { + e!(value, |i| i.into()) + } +} + +impl DynImage { + /// Gets out the Y image, if its there, else returning self. + /// + /// If you want to convert, see [`DynImage::to_y`]. + pub fn get_y(self) -> Result, Self> { + match self { + Self::Y(i) => Ok(i), + _ => Err(self), + } + } + + /// Gets out the YA image, if its there, else returning self. + /// + /// If you want to convert, see [`DynImage::to_ya`]. + pub fn get_ya(self) -> Result, Self> { + match self { + Self::Ya(i) => Ok(i), + _ => Err(self), + } + } + + /// Gets out the RGB image, if its there, else returning self. + /// + /// If you want to convert, see [`DynImage::to_rgb`]. + pub fn get_rgb(self) -> Result, Self> { + match self { + Self::Rgb(i) => Ok(i), + _ => Err(self), + } + } + + /// Gets out the RGBA image, if its there, else returning self. + /// + /// If you want to convert, see [`DynImage::to_rgba`]. + pub fn get_rgba(self) -> Result, Self> { + match self { + Self::Rgba(i) => Ok(i), + _ => Err(self), + } + } +} + +impl DynImage> { + /// Convert this dyn image into a Y image. + pub fn to_y(self) -> Image, 1> { + self.into() + } + + /// Convert this dyn image into a YA image. + pub fn to_ya(self) -> Image, 2> { + self.into() + } + + /// Convert this dyn image into a RGB image. + pub fn to_rgb(self) -> Image, 3> { + self.into() + } + + /// Convert this dyn image into a RGBA image. + pub fn to_rgba(self) -> Image, 4> { + self.into() + } +} + +impl> DynImage { + /// Produce a Y image from this dyn image. + pub fn y(&self) -> Image, 1> { + e!(self, |i| i.as_ref().into()) + } + + /// Produce a YA image from this dyn image. + pub fn ya(&self) -> Image, 2> { + e!(self, |i| i.as_ref().into()) + } + + /// Produce a RGB image from this dyn image. + pub fn rgb(&self) -> Image, 3> { + e!(self, |i| i.as_ref().into()) + } + + /// Produce a RGBA image from this dyn image. + pub fn rgba(&self) -> Image, 4> { + e!(self, |i| i.as_ref().into()) + } +} diff --git a/src/dyn/mod.rs b/src/dyn/mod.rs new file mode 100644 index 0000000..b86742e --- /dev/null +++ b/src/dyn/mod.rs @@ -0,0 +1,121 @@ +use crate::Image; +mod affine; +mod convert; +#[cfg(feature = "scale")] +mod scale; + +#[derive(Clone, Debug, PartialEq)] +/// Dynamic image. +/// Can be any of {`Y8`, `YA8`, `RGB8`, `RGB16`}. +pub enum DynImage { + /// Y image + Y(Image), + /// YA image + Ya(Image), + /// RGB image + Rgb(Image), + /// RGBA image + Rgba(Image), +} + +impl Copy for DynImage<&[u8]> {} + +macro_rules! e { + ($dyn:expr => |$image: pat_param| $do:expr) => { + match $dyn { + DynImage::Y($image) => DynImage::Y($do), + DynImage::Ya($image) => DynImage::Ya($do), + DynImage::Rgb($image) => DynImage::Rgb($do), + DynImage::Rgba($image) => DynImage::Rgba($do), + } + }; + ($dyn:expr, |$image: pat_param| $do:expr) => { + match $dyn { + DynImage::Y($image) => $do, + DynImage::Ya($image) => $do, + DynImage::Rgb($image) => $do, + DynImage::Rgba($image) => $do, + } + }; +} +use e; + +impl DynImage { + /// Get the width of this image. + pub const fn width(&self) -> u32 { + e!(self, |i| i.width()) + } + + /// Get the height of this image. + pub const fn height(&self) -> u32 { + e!(self, |i| i.height()) + } + + /// Get the image buffer. + pub const fn buffer(&self) -> &T { + e!(self, |i| i.buffer()) + } + + /// Take the image buffer. + pub fn take_buffer(self) -> T { + e!(self, |i| i.take_buffer()) + } +} + +impl> DynImage { + /// Reference this image. + pub fn as_ref(&self) -> DynImage<&[u8]> { + e!(self => |i| i.as_ref()) + } + + /// Bytes of this image. + pub fn bytes(&self) -> &[u8] { + e!(self, |i| i.bytes()) + } +} + +impl DynImage> { + #[cfg(feature = "save")] + /// Open a PNG image + pub fn open(f: impl AsRef) -> Self { + use png::Transformations as T; + let p = std::fs::File::open(f).unwrap(); + let r = std::io::BufReader::new(p); + let mut dec = png::Decoder::new(r); + dec.set_transformations(T::STRIP_16 | T::EXPAND); + let mut reader = dec.read_info().unwrap(); + let mut buf = vec![0; reader.output_buffer_size()].into_boxed_slice(); + let info = reader.next_frame(&mut buf).unwrap(); + use png::ColorType::*; + match info.color_type { + Indexed | Grayscale => Self::Y(Image::build(info.width, info.height).buf(buf)), + Rgb => Self::Rgb(Image::build(info.width, info.height).buf(buf)), + Rgba => Self::Rgba(Image::build(info.width, info.height).buf(buf)), + GrayscaleAlpha => Self::Ya(Image::build(info.width, info.height).buf(buf)), + } + } + + #[cfg(feature = "save")] + /// Save this image to a PNG. + pub fn save(&self, f: impl AsRef) { + 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_depth(png::BitDepth::Eight); + enc.set_color(match self { + Self::Y(_) => png::ColorType::Grayscale, + Self::Ya(_) => png::ColorType::GrayscaleAlpha, + Self::Rgb(_) => png::ColorType::Rgb, + Self::Rgba(_) => png::ColorType::Rgba, + }); + 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.bytes()).unwrap(); + } +} diff --git a/src/dyn/scale.rs b/src/dyn/scale.rs new file mode 100644 index 0000000..f827a1f --- /dev/null +++ b/src/dyn/scale.rs @@ -0,0 +1,10 @@ +use crate::scale::traits::ScalingAlgorithm; + +use super::{e, DynImage}; + +impl + AsRef<[u8]>> DynImage { + /// Scale this image with a given scaling algorithm. + pub fn scale(&mut self, width: u32, height: u32) -> DynImage> { + e!(self => |i| i.scale::(width, height)) + } +} diff --git a/src/lib.rs b/src/lib.rs index 3f5d7a3..e325c6c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,39 @@ //! # fimg //! //! Provides fast image operations, such as rotation, flipping, and overlaying. +//! +//! ## Organization +//! +//! Image types: +//! +//! - [`Image`]: the main image type. +//! - [`DynImage`]: This is the image type you use when, say, loading a png. You should immediately convert this into a +//! - [`ImageCloner`]: this is... a [`Image`], but about to be cloned. It just allows some simple out-of-place optimizations, that `.clone().op()` dont allow. (produce with [`Image::cloner`]) +//! +//! ### Operations +//! +//! Affine: +//! - [`Image::rot_90`] +//! - [`Image::rot_180`] +//! - [`Image::rot_270`] +//! - [`Image::flip_h`] +//! - [`Image::flip_v`] +//! +//! Drawing: +//! - [`Image::box`], [`Image::filled_box`], [`Image::stroked_box`] +//! - [`Image::circle`], [`Image::border_circle`] +//! - [`Image::line`], [`Image::thick_line`] +//! - [`Image::points`] +//! - [`Image::quad`] +//! - [`Image::poly`], [`Image::border_poly`] +//! - [`Image::tri`] +//! - [`Image::text`] +//! +//! Scaling: [`Image::scale`] +//! +//! Misc image ops: +//! - [`Image::repeated`] +//! - [`Image::overlay`](Overlay), [`Image::overlay_at`](OverlayAt), [`Image::overlay_blended`](BlendingOverlay) #![feature( slice_swap_unchecked, generic_const_exprs, @@ -27,16 +60,21 @@ use std::{num::NonZeroU32, slice::SliceIndex}; mod affine; +#[doc(hidden)] pub mod builder; +#[doc(hidden)] pub mod cloner; +mod convert; mod drawing; +mod r#dyn; pub(crate) mod math; mod overlay; pub mod pixels; #[cfg(feature = "scale")] pub mod scale; -use cloner::ImageCloner; +pub use cloner::ImageCloner; pub use overlay::{BlendingOverlay, ClonerOverlay, ClonerOverlayAt, Overlay, OverlayAt}; +pub use r#dyn::DynImage; /// like assert!(), but causes undefined behaviour at runtime when the condition is not met. /// @@ -145,14 +183,14 @@ impl Clone for Image { impl Image { #[inline] /// get the height as a [`u32`] - pub fn height(&self) -> u32 { - self.height.into() + pub const fn height(&self) -> u32 { + self.height.get() } #[inline] /// get the width as a [`u32`] - pub fn width(&self) -> u32 { - self.width.into() + pub const fn width(&self) -> u32 { + self.width.get() } #[inline] @@ -299,7 +337,12 @@ impl, const CHANNELS: usize> Image { /// The size of the underlying buffer. #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> usize { - self.buffer.as_ref().len() + self.bytes().len() + } + + /// Bytes of this image. + pub fn bytes(&self) -> &[u8] { + self.buffer.as_ref() } /// # Safety @@ -342,7 +385,7 @@ impl, const CHANNELS: usize> Image { /// Reference this image. 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.as_ref()) } + unsafe { Image::new(self.width, self.height, self.bytes()) } } #[inline] @@ -352,14 +395,14 @@ impl, const CHANNELS: usize> Image { unsafe { assert_unchecked!(self.len() > CHANNELS) }; // SAFETY: no half pixels! unsafe { assert_unchecked!(self.len() % CHANNELS == 0) }; - self.buffer.as_ref().array_chunks::() + self.bytes().array_chunks::() } #[inline] /// Flatten the chunks of this image into a slice of slices. pub fn flatten(&self) -> &[[u8; CHANNELS]] { // SAFETY: buffer cannot have half pixels - unsafe { self.buffer.as_ref().as_chunks_unchecked::() } + unsafe { self.bytes().as_chunks_unchecked::() } } /// Return a pixel at (x, y). @@ -427,7 +470,7 @@ impl + AsRef<[u8]>, const CHANNELS: usize> Image { unsafe fn copy_within(&mut self, src: std::ops::Range, dest: usize) { let std::ops::Range { start, end } = src; debug_assert!( - dest <= self.buffer.as_ref().len() - end - start, + dest <= self.bytes().len() - end - start, "dest is out of bounds" ); #[allow(clippy::multiple_unsafe_ops_per_block)] @@ -516,7 +559,7 @@ macro_rules! save { (0.15000, 0.06000), )); let mut writer = enc.write_header().unwrap(); - writer.write_image_data(self.buffer.as_ref()).unwrap(); + writer.write_image_data(self.bytes()).unwrap(); } } }; @@ -531,13 +574,10 @@ impl Image, CHANNELS> { let r = std::io::BufReader::new(p); let mut dec = png::Decoder::new(r); match CHANNELS { - 1 => dec.set_transformations(T::STRIP_16 | T::EXPAND), - 2 => dec.set_transformations(T::STRIP_16 | T::ALPHA), - 3 => dec.set_transformations(T::STRIP_16 | T::EXPAND), - 4 => dec.set_transformations(T::STRIP_16 | T::ALPHA), + 1 | 3 => dec.set_transformations(T::STRIP_16 | T::EXPAND), + 2 | 4 => dec.set_transformations(T::STRIP_16 | T::ALPHA), _ => (), } - dec.set_transformations(png::Transformations::EXPAND); let mut reader = dec.read_info().unwrap(); let mut buf = vec![0; reader.output_buffer_size()]; let info = reader.next_frame(&mut buf).unwrap(); diff --git a/src/pixels/convert.rs b/src/pixels/convert.rs new file mode 100644 index 0000000..55c9338 --- /dev/null +++ b/src/pixels/convert.rs @@ -0,0 +1,97 @@ +//! provides From's for pixels. + +use super::{Push, Trunc}; + +/// Converts a pixel to another pixel. +pub trait PFrom { + /// Convert a pixel to this pixel. + fn pfrom(f: [u8; N]) -> Self; +} + +impl PFrom for [u8; N] { + fn pfrom(f: [u8; N]) -> Self { + f + } +} + +/// Y pixel +pub type Y = [u8; 1]; +impl PFrom<2> for Y { + fn pfrom(f: YA) -> Self { + f.trunc() + } +} + +impl PFrom<3> for Y { + fn pfrom([r, g, b]: RGB) -> Self { + [((2126 * r as u16 + 7152 * g as u16 + 722 * b as u16) / 10000) as u8] + } +} + +impl PFrom<4> for Y { + fn pfrom(f: RGBA) -> Self { + PFrom::pfrom(f.trunc()) + } +} + +/// YA pixel +pub type YA = [u8; 2]; +impl PFrom<1> for YA { + fn pfrom(f: Y) -> Self { + f.and(255) + } +} + +impl PFrom<3> for YA { + fn pfrom(f: RGB) -> Self { + Y::pfrom(f).and(255) + } +} + +impl PFrom<4> for YA { + fn pfrom(f: RGBA) -> Self { + Y::pfrom(f.trunc()).and(255) + } +} + +/// RGB pixel +pub type RGB = [u8; 3]; + +impl PFrom<1> for RGB { + fn pfrom([y]: Y) -> Self { + [y; 3] + } +} + +impl PFrom<2> for RGB { + fn pfrom([y, _]: YA) -> Self { + [y; 3] + } +} + +impl PFrom<4> for RGB { + fn pfrom(f: RGBA) -> Self { + f.trunc() + } +} + +/// RGBA pixel +pub type RGBA = [u8; 4]; + +impl PFrom<1> for RGBA { + fn pfrom([y]: Y) -> Self { + [y; 3].and(255) + } +} + +impl PFrom<2> for RGBA { + fn pfrom([y, a]: YA) -> Self { + [y; 3].and(a) + } +} + +impl PFrom<3> for RGBA { + fn pfrom(f: [u8; 3]) -> Self { + f.and(255) + } +} diff --git a/src/pixels/mod.rs b/src/pixels/mod.rs index 503115a..8607206 100644 --- a/src/pixels/mod.rs +++ b/src/pixels/mod.rs @@ -4,5 +4,6 @@ pub mod blending; mod utility; mod wam; pub use blending::Blend; -pub(crate) use utility::{float, unfloat, Floatify, PMap, Trunc, Unfloatify}; +pub(crate) use utility::{float, unfloat, Floatify, PMap, Push, Trunc, Unfloatify}; pub(crate) use wam::Wam; +pub mod convert; diff --git a/src/pixels/utility.rs b/src/pixels/utility.rs index 5184f58..aabadb5 100644 --- a/src/pixels/utility.rs +++ b/src/pixels/utility.rs @@ -53,7 +53,7 @@ impl PMap for [T; N] { } pub trait Trunc { - /// it does `a[..a.len() - 1].try_into().unwrap()``. + /// it does `a[..a.len() - 1].try_into().unwrap()`. fn trunc(&self) -> [T; N - 1]; } @@ -63,6 +63,17 @@ impl Trunc for [T; N] { } } +pub trait Push { + fn and(self, and: T) -> [T; N + 1]; +} + +impl Push for [T; N] { + fn and(self, and: T) -> [T; N + 1] { + let mut iter = self.into_iter().chain(std::iter::once(and)); + std::array::from_fn(|_| iter.next().unwrap()) + } +} + #[test] fn trunc() { let x = [1]; @@ -70,3 +81,9 @@ fn trunc() { let x = [1, 2, 3, 4]; assert_eq!(x.trunc(), [1, 2, 3]); } + +#[test] +fn append() { + let x = [1]; + assert_eq!(x.and(5), [1, 5]); +} diff --git a/src/pixels/wam.rs b/src/pixels/wam.rs index 30016fd..cf0f461 100644 --- a/src/pixels/wam.rs +++ b/src/pixels/wam.rs @@ -7,7 +7,7 @@ pub trait Wam { /// /// # Safety /// - /// pls make l = 0..=f32::MAX/2, r = 0..=f32::MAX/2 + /// pls make l = 0..=[f32::MAX]/2, r = 0..=[f32::MAX]/2 unsafe fn wam(self, b: Self, l: FF32, r: FF32) -> Self; }