This commit is contained in:
bendn 2023-10-31 07:59:46 +07:00
parent bb33b6cb10
commit 3e01a15713
No known key found for this signature in database
GPG key ID: 0D9D3A2A3B2A93D6
11 changed files with 518 additions and 20 deletions

View file

@ -1,6 +1,6 @@
[package] [package]
name = "fimg" name = "fimg"
version = "0.4.21" version = "0.4.22"
authors = ["bend-n <bend.n@outlook.com>"] authors = ["bend-n <bend.n@outlook.com>"]
license = "MIT" license = "MIT"
edition = "2021" edition = "2021"

64
src/convert.rs Normal file
View file

@ -0,0 +1,64 @@
//! define From's for images.
//! these conversions are defined by [`PFrom`].
use crate::{pixels::convert::PFrom, Image};
fn map<const A: usize, const B: usize>(image: Image<&[u8], A>) -> Image<Box<[u8]>, B>
where
[u8; B]: PFrom<A>,
{
let buffer = image
.chunked()
.copied()
.flat_map(<[u8; B] as PFrom<A>>::pfrom)
.collect::<Vec<_>>()
.into();
// SAFETY: ctor
unsafe { Image::new(image.width, image.height, buffer) }
}
macro_rules! convert {
($a:literal => $b:literal) => {
impl From<Image<&[u8], $b>> for Image<Box<[u8]>, $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<Image<Box<[u8]>, $b>> for Image<Box<[u8]>, $a> {
fn from(value: Image<Box<[u8]>, $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);

38
src/dyn/affine.rs Normal file
View file

@ -0,0 +1,38 @@
use super::{e, DynImage};
impl<T: AsMut<[u8]> + AsRef<[u8]>> DynImage<T> {
/// 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())
}
}

110
src/dyn/convert.rs Normal file
View file

@ -0,0 +1,110 @@
#![allow(clippy::useless_conversion)]
use super::{e, DynImage, Image};
impl From<DynImage<Box<[u8]>>> for Image<Box<[u8]>, 1> {
fn from(value: DynImage<Box<[u8]>>) -> Self {
e!(value, |i| i.into())
}
}
impl From<DynImage<Box<[u8]>>> for Image<Box<[u8]>, 2> {
fn from(value: DynImage<Box<[u8]>>) -> Self {
e!(value, |i| i.into())
}
}
impl From<DynImage<Box<[u8]>>> for Image<Box<[u8]>, 3> {
fn from(value: DynImage<Box<[u8]>>) -> Self {
e!(value, |i| i.into())
}
}
impl From<DynImage<Box<[u8]>>> for Image<Box<[u8]>, 4> {
fn from(value: DynImage<Box<[u8]>>) -> Self {
e!(value, |i| i.into())
}
}
impl<T> DynImage<T> {
/// 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<Image<T, 1>, 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<Image<T, 2>, 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<Image<T, 3>, 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<Image<T, 4>, Self> {
match self {
Self::Rgba(i) => Ok(i),
_ => Err(self),
}
}
}
impl DynImage<Box<[u8]>> {
/// Convert this dyn image into a Y image.
pub fn to_y(self) -> Image<Box<[u8]>, 1> {
self.into()
}
/// Convert this dyn image into a YA image.
pub fn to_ya(self) -> Image<Box<[u8]>, 2> {
self.into()
}
/// Convert this dyn image into a RGB image.
pub fn to_rgb(self) -> Image<Box<[u8]>, 3> {
self.into()
}
/// Convert this dyn image into a RGBA image.
pub fn to_rgba(self) -> Image<Box<[u8]>, 4> {
self.into()
}
}
impl<T: AsRef<[u8]>> DynImage<T> {
/// Produce a Y image from this dyn image.
pub fn y(&self) -> Image<Box<[u8]>, 1> {
e!(self, |i| i.as_ref().into())
}
/// Produce a YA image from this dyn image.
pub fn ya(&self) -> Image<Box<[u8]>, 2> {
e!(self, |i| i.as_ref().into())
}
/// Produce a RGB image from this dyn image.
pub fn rgb(&self) -> Image<Box<[u8]>, 3> {
e!(self, |i| i.as_ref().into())
}
/// Produce a RGBA image from this dyn image.
pub fn rgba(&self) -> Image<Box<[u8]>, 4> {
e!(self, |i| i.as_ref().into())
}
}

121
src/dyn/mod.rs Normal file
View file

@ -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<T> {
/// Y image
Y(Image<T, 1>),
/// YA image
Ya(Image<T, 2>),
/// RGB image
Rgb(Image<T, 3>),
/// RGBA image
Rgba(Image<T, 4>),
}
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<T> DynImage<T> {
/// 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<T: AsRef<[u8]>> DynImage<T> {
/// 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<Box<[u8]>> {
#[cfg(feature = "save")]
/// Open a PNG image
pub fn open(f: impl AsRef<std::path::Path>) -> 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<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_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();
}
}

10
src/dyn/scale.rs Normal file
View file

@ -0,0 +1,10 @@
use crate::scale::traits::ScalingAlgorithm;
use super::{e, DynImage};
impl<T: AsMut<[u8]> + AsRef<[u8]>> DynImage<T> {
/// Scale this image with a given scaling algorithm.
pub fn scale<A: ScalingAlgorithm>(&mut self, width: u32, height: u32) -> DynImage<Box<[u8]>> {
e!(self => |i| i.scale::<A>(width, height))
}
}

View file

@ -1,6 +1,39 @@
//! # fimg //! # fimg
//! //!
//! Provides fast image operations, such as rotation, flipping, and overlaying. //! 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( #![feature(
slice_swap_unchecked, slice_swap_unchecked,
generic_const_exprs, generic_const_exprs,
@ -27,16 +60,21 @@
use std::{num::NonZeroU32, slice::SliceIndex}; use std::{num::NonZeroU32, slice::SliceIndex};
mod affine; mod affine;
#[doc(hidden)]
pub mod builder; pub mod builder;
#[doc(hidden)]
pub mod cloner; pub mod cloner;
mod convert;
mod drawing; mod drawing;
mod r#dyn;
pub(crate) mod math; pub(crate) mod math;
mod overlay; mod overlay;
pub mod pixels; pub mod pixels;
#[cfg(feature = "scale")] #[cfg(feature = "scale")]
pub mod scale; pub mod scale;
use cloner::ImageCloner; pub use cloner::ImageCloner;
pub use overlay::{BlendingOverlay, ClonerOverlay, ClonerOverlayAt, Overlay, OverlayAt}; 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. /// like assert!(), but causes undefined behaviour at runtime when the condition is not met.
/// ///
@ -145,14 +183,14 @@ impl<T: Clone, const CHANNELS: usize> Clone for Image<T, CHANNELS> {
impl<T, const CHANNELS: usize> Image<T, CHANNELS> { impl<T, const CHANNELS: usize> Image<T, CHANNELS> {
#[inline] #[inline]
/// get the height as a [`u32`] /// get the height as a [`u32`]
pub fn height(&self) -> u32 { pub const fn height(&self) -> u32 {
self.height.into() self.height.get()
} }
#[inline] #[inline]
/// get the width as a [`u32`] /// get the width as a [`u32`]
pub fn width(&self) -> u32 { pub const fn width(&self) -> u32 {
self.width.into() self.width.get()
} }
#[inline] #[inline]
@ -299,7 +337,12 @@ impl<T: AsRef<[u8]>, const CHANNELS: usize> Image<T, CHANNELS> {
/// The size of the underlying buffer. /// The size of the underlying buffer.
#[allow(clippy::len_without_is_empty)] #[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize { 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 /// # Safety
@ -342,7 +385,7 @@ impl<T: AsRef<[u8]>, const CHANNELS: usize> Image<T, CHANNELS> {
/// Reference this image. /// Reference this image.
pub fn as_ref(&self) -> Image<&[u8], CHANNELS> { pub fn as_ref(&self) -> Image<&[u8], CHANNELS> {
// SAFETY: we got constructed okay, parameters must be valid // 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] #[inline]
@ -352,14 +395,14 @@ impl<T: AsRef<[u8]>, const CHANNELS: usize> Image<T, CHANNELS> {
unsafe { assert_unchecked!(self.len() > CHANNELS) }; unsafe { assert_unchecked!(self.len() > CHANNELS) };
// SAFETY: no half pixels! // SAFETY: no half pixels!
unsafe { assert_unchecked!(self.len() % CHANNELS == 0) }; unsafe { assert_unchecked!(self.len() % CHANNELS == 0) };
self.buffer.as_ref().array_chunks::<CHANNELS>() self.bytes().array_chunks::<CHANNELS>()
} }
#[inline] #[inline]
/// Flatten the chunks of this image into a slice of slices. /// Flatten the chunks of this image into a slice of slices.
pub fn flatten(&self) -> &[[u8; CHANNELS]] { pub fn flatten(&self) -> &[[u8; CHANNELS]] {
// SAFETY: buffer cannot have half pixels // SAFETY: buffer cannot have half pixels
unsafe { self.buffer.as_ref().as_chunks_unchecked::<CHANNELS>() } unsafe { self.bytes().as_chunks_unchecked::<CHANNELS>() }
} }
/// Return a pixel at (x, y). /// Return a pixel at (x, y).
@ -427,7 +470,7 @@ impl<T: AsMut<[u8]> + AsRef<[u8]>, const CHANNELS: usize> Image<T, CHANNELS> {
unsafe fn copy_within(&mut self, src: std::ops::Range<usize>, dest: usize) { unsafe fn copy_within(&mut self, src: std::ops::Range<usize>, dest: usize) {
let std::ops::Range { start, end } = src; let std::ops::Range { start, end } = src;
debug_assert!( debug_assert!(
dest <= self.buffer.as_ref().len() - end - start, dest <= self.bytes().len() - end - start,
"dest is out of bounds" "dest is out of bounds"
); );
#[allow(clippy::multiple_unsafe_ops_per_block)] #[allow(clippy::multiple_unsafe_ops_per_block)]
@ -516,7 +559,7 @@ macro_rules! save {
(0.15000, 0.06000), (0.15000, 0.06000),
)); ));
let mut writer = enc.write_header().unwrap(); 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<const CHANNELS: usize> Image<Vec<u8>, CHANNELS> {
let r = std::io::BufReader::new(p); let r = std::io::BufReader::new(p);
let mut dec = png::Decoder::new(r); let mut dec = png::Decoder::new(r);
match CHANNELS { match CHANNELS {
1 => dec.set_transformations(T::STRIP_16 | T::EXPAND), 1 | 3 => dec.set_transformations(T::STRIP_16 | T::EXPAND),
2 => dec.set_transformations(T::STRIP_16 | T::ALPHA), 2 | 4 => 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),
_ => (), _ => (),
} }
dec.set_transformations(png::Transformations::EXPAND);
let mut reader = dec.read_info().unwrap(); let mut reader = dec.read_info().unwrap();
let mut buf = vec![0; reader.output_buffer_size()]; let mut buf = vec![0; reader.output_buffer_size()];
let info = reader.next_frame(&mut buf).unwrap(); let info = reader.next_frame(&mut buf).unwrap();

97
src/pixels/convert.rs Normal file
View file

@ -0,0 +1,97 @@
//! provides From's for pixels.
use super::{Push, Trunc};
/// Converts a pixel to another pixel.
pub trait PFrom<const N: usize> {
/// Convert a pixel to this pixel.
fn pfrom(f: [u8; N]) -> Self;
}
impl<const N: usize> PFrom<N> 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)
}
}

View file

@ -4,5 +4,6 @@ pub mod blending;
mod utility; mod utility;
mod wam; mod wam;
pub use blending::Blend; 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(crate) use wam::Wam;
pub mod convert;

View file

@ -53,7 +53,7 @@ impl<const N: usize, T: Copy, R: Copy> PMap<T, R, N> for [T; N] {
} }
pub trait Trunc<T, const N: usize> { pub trait Trunc<T, const N: usize> {
/// it does `a[..a.len() - 1].try_into().unwrap()``. /// it does `a[..a.len() - 1].try_into().unwrap()`.
fn trunc(&self) -> [T; N - 1]; fn trunc(&self) -> [T; N - 1];
} }
@ -63,6 +63,17 @@ impl<const N: usize, T: Copy> Trunc<T, N> for [T; N] {
} }
} }
pub trait Push<T, const N: usize> {
fn and(self, and: T) -> [T; N + 1];
}
impl<const N: usize, T> Push<T, N> 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] #[test]
fn trunc() { fn trunc() {
let x = [1]; let x = [1];
@ -70,3 +81,9 @@ fn trunc() {
let x = [1, 2, 3, 4]; let x = [1, 2, 3, 4];
assert_eq!(x.trunc(), [1, 2, 3]); assert_eq!(x.trunc(), [1, 2, 3]);
} }
#[test]
fn append() {
let x = [1];
assert_eq!(x.and(5), [1, 5]);
}

View file

@ -7,7 +7,7 @@ pub trait Wam {
/// ///
/// # Safety /// # Safety
/// ///
/// pls make l = 0..=f32::MAX/2, r = 0..=f32::MAX/2 /// pls make l = <code>0..=[f32::MAX]/2</code>, r = <code>0..=[f32::MAX]/2</code>
unsafe fn wam(self, b: Self, l: FF32, r: FF32) -> Self; unsafe fn wam(self, b: Self, l: FF32, r: FF32) -> Self;
} }