#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![warn(clippy::dbg_macro, clippy::todo, missing_docs)]
#[cfg(feature = "image")]
mod image;
mod array2d;
mod bordercrop;
mod math;
mod smartcrop;
use std::{marker::PhantomData, num::NonZeroU32};
pub use bordercrop::find_border_crop;
pub use smartcrop::find_best_crop;
pub trait Image: Sized {
fn width(&self) -> u32;
fn height(&self) -> u32;
fn get(&self, x: u32, y: u32) -> RGB;
}
pub trait ResizableImage<I: Image> {
fn resize(&self, width: u32, height: u32) -> I;
fn crop_and_resize(&self, crop: Crop, width: u32, height: u32) -> I;
}
#[derive(PartialEq, Debug)]
pub enum Error {
ZeroSizedImage,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::ZeroSizedImage => f.write_str("zero-sized image"),
}
}
}
#[derive(Copy, Clone, PartialEq, Debug)]
pub struct RGB {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl RGB {
pub fn new(r: u8, g: u8, b: u8) -> RGB {
RGB { r, g, b }
}
fn cie(self: &RGB) -> f64 {
0.5126 * self.b as f64 + 0.7152 * self.g as f64 + 0.0722 * self.r as f64
}
fn saturation(self: &RGB) -> f64 {
let maximum = f64::max(
f64::max(self.r as f64 / 255.0, self.g as f64 / 255.0),
self.b as f64 / 255.0,
);
let minimum = f64::min(
f64::min(self.r as f64 / 255.0, self.g as f64 / 255.0),
self.b as f64 / 255.0,
);
if maximum == minimum {
return 0.0;
}
let l = (maximum + minimum) / 2.0;
let d = maximum - minimum;
if l > 0.5 {
d / (2.0 - maximum - minimum)
} else {
d / (maximum + minimum)
}
}
fn normalize(&self) -> [f64; 3] {
if self.r == self.g && self.g == self.b {
let inv_sqrt_3: f64 = 1.0 / 3.0f64.sqrt();
return [inv_sqrt_3, inv_sqrt_3, inv_sqrt_3];
}
let r = self.r as f64;
let g = self.g as f64;
let b = self.b as f64;
let mag = (r.powi(2) + g.powi(2) + b.powi(2)).sqrt();
[r / mag, g / mag, b / mag]
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct Score {
pub detail: f64,
pub saturation: f64,
pub skin: f64,
pub total: f64,
}
#[derive(Clone, PartialEq, Debug)]
pub struct Crop {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl Crop {
fn scale(&self, ratio: f64) -> Crop {
Crop {
x: (self.x as f64 * ratio).round() as u32,
y: (self.y as f64 * ratio).round() as u32,
width: (self.width as f64 * ratio).round() as u32,
height: (self.height as f64 * ratio).round() as u32,
}
}
fn base_on(&self, other: &Crop) -> Crop {
Crop {
x: self.x + other.x,
y: self.y + other.y,
width: self.width,
height: self.height,
}
}
}
#[derive(Debug)]
pub struct ScoredCrop {
pub crop: Crop,
pub score: Score,
}
impl ScoredCrop {
pub fn scale(&self, ratio: f64) -> ScoredCrop {
ScoredCrop {
crop: self.crop.scale(ratio),
score: self.score.clone(),
}
}
}
struct CroppedImage<'a, RI: Image + ResizableImage<I>, I: Image> {
image: &'a RI,
crop: Crop,
_marker: PhantomData<I>,
}
impl<'a, RI: Image + ResizableImage<I>, I: Image> CroppedImage<'a, RI, I> {
fn new(image: &'a RI, crop: Crop) -> Self {
Self {
image,
crop,
_marker: PhantomData,
}
}
}
impl<RI: Image + ResizableImage<I>, I: Image> Image for CroppedImage<'_, RI, I> {
fn width(&self) -> u32 {
self.crop.width
}
fn height(&self) -> u32 {
self.crop.height
}
fn get(&self, x: u32, y: u32) -> RGB {
self.image.get(self.crop.x + x, self.crop.y + y)
}
}
impl<RI: Image + ResizableImage<I>, I: Image> ResizableImage<I> for CroppedImage<'_, RI, I> {
fn resize(&self, width: u32, height: u32) -> I {
self.image.crop_and_resize(self.crop.clone(), width, height)
}
fn crop_and_resize(&self, crop: Crop, width: u32, height: u32) -> I {
self.image
.crop_and_resize(crop.base_on(&self.crop), width, height)
}
}
pub fn find_best_crop_no_borders<I: Image + ResizableImage<RI>, RI: Image>(
img: &I,
width: NonZeroU32,
height: NonZeroU32,
) -> Result<ScoredCrop, Error> {
let pre_crop = find_border_crop(img);
match pre_crop {
Some(crop) => {
let cropped = CroppedImage::new(img, crop.clone());
let mut best_crop = find_best_crop(&cropped, width, height)?;
best_crop.crop = best_crop.crop.base_on(&crop);
Ok(best_crop)
}
None => find_best_crop(img, width, height),
}
}