1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3#![warn(clippy::dbg_macro, clippy::todo, missing_docs)]
4
5#[cfg(feature = "image")]
6mod image;
7
8mod array2d;
9mod bordercrop;
10mod math;
11mod smartcrop;
12
13use std::{marker::PhantomData, num::NonZeroU32};
14
15pub use bordercrop::find_border_crop;
16pub use smartcrop::find_best_crop;
17
18pub trait Image: Sized {
20 fn width(&self) -> u32;
22 fn height(&self) -> u32;
24 fn get(&self, x: u32, y: u32) -> RGB;
26}
27
28pub trait ResizableImage<I: Image> {
32 fn resize(&self, width: u32, height: u32) -> I;
34 fn crop_and_resize(&self, crop: Crop, width: u32, height: u32) -> I;
36}
37
38#[derive(PartialEq, Debug)]
40pub enum Error {
41 ZeroSizedImage,
43}
44
45impl std::fmt::Display for Error {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 match self {
48 Error::ZeroSizedImage => f.write_str("zero-sized image"),
49 }
50 }
51}
52
53#[derive(Copy, Clone, PartialEq, Debug)]
55pub struct RGB {
56 pub r: u8,
58 pub g: u8,
60 pub b: u8,
62}
63
64impl RGB {
65 pub fn new(r: u8, g: u8, b: u8) -> RGB {
67 RGB { r, g, b }
68 }
69
70 fn cie(self: &RGB) -> f64 {
71 0.5126 * self.b as f64 + 0.7152 * self.g as f64 + 0.0722 * self.r as f64
73 }
74
75 fn saturation(self: &RGB) -> f64 {
76 let maximum = f64::max(
77 f64::max(self.r as f64 / 255.0, self.g as f64 / 255.0),
78 self.b as f64 / 255.0,
79 );
80 let minimum = f64::min(
81 f64::min(self.r as f64 / 255.0, self.g as f64 / 255.0),
82 self.b as f64 / 255.0,
83 );
84
85 if maximum == minimum {
86 return 0.0;
87 }
88
89 let l = (maximum + minimum) / 2.0;
90 let d = maximum - minimum;
91
92 if l > 0.5 {
93 d / (2.0 - maximum - minimum)
94 } else {
95 d / (maximum + minimum)
96 }
97 }
98
99 fn normalize(&self) -> [f64; 3] {
100 if self.r == self.g && self.g == self.b {
101 let inv_sqrt_3: f64 = 1.0 / 3.0f64.sqrt();
102 return [inv_sqrt_3, inv_sqrt_3, inv_sqrt_3];
103 }
104
105 let r = self.r as f64;
106 let g = self.g as f64;
107 let b = self.b as f64;
108
109 let mag = (r.powi(2) + g.powi(2) + b.powi(2)).sqrt();
110
111 [r / mag, g / mag, b / mag]
112 }
113}
114
115#[derive(Clone, PartialEq, Debug)]
117pub struct Score {
118 pub detail: f64,
120 pub saturation: f64,
122 pub skin: f64,
124 pub total: f64,
126}
127
128#[derive(Clone, PartialEq, Debug)]
130pub struct Crop {
131 pub x: u32,
133 pub y: u32,
135 pub width: u32,
137 pub height: u32,
139}
140
141impl Crop {
142 fn scale(&self, ratio: f64) -> Crop {
143 Crop {
144 x: (self.x as f64 * ratio).round() as u32,
145 y: (self.y as f64 * ratio).round() as u32,
146 width: (self.width as f64 * ratio).round() as u32,
147 height: (self.height as f64 * ratio).round() as u32,
148 }
149 }
150
151 fn base_on(&self, other: &Crop) -> Crop {
152 Crop {
153 x: self.x + other.x,
154 y: self.y + other.y,
155 width: self.width,
156 height: self.height,
157 }
158 }
159}
160
161#[derive(Debug)]
163pub struct ScoredCrop {
164 pub crop: Crop,
166 pub score: Score,
168}
169
170impl ScoredCrop {
171 pub fn scale(&self, ratio: f64) -> ScoredCrop {
173 ScoredCrop {
174 crop: self.crop.scale(ratio),
175 score: self.score.clone(),
176 }
177 }
178}
179
180struct CroppedImage<'a, RI: Image + ResizableImage<I>, I: Image> {
182 image: &'a RI,
183 crop: Crop,
184 _marker: PhantomData<I>,
185}
186
187impl<'a, RI: Image + ResizableImage<I>, I: Image> CroppedImage<'a, RI, I> {
188 fn new(image: &'a RI, crop: Crop) -> Self {
189 Self {
190 image,
191 crop,
192 _marker: PhantomData,
193 }
194 }
195}
196
197impl<RI: Image + ResizableImage<I>, I: Image> Image for CroppedImage<'_, RI, I> {
198 fn width(&self) -> u32 {
199 self.crop.width
200 }
201
202 fn height(&self) -> u32 {
203 self.crop.height
204 }
205
206 fn get(&self, x: u32, y: u32) -> RGB {
207 self.image.get(self.crop.x + x, self.crop.y + y)
208 }
209}
210
211impl<RI: Image + ResizableImage<I>, I: Image> ResizableImage<I> for CroppedImage<'_, RI, I> {
212 fn resize(&self, width: u32, height: u32) -> I {
213 self.image.crop_and_resize(self.crop.clone(), width, height)
214 }
215
216 fn crop_and_resize(&self, crop: Crop, width: u32, height: u32) -> I {
217 self.image
218 .crop_and_resize(crop.base_on(&self.crop), width, height)
219 }
220}
221
222pub fn find_best_crop_no_borders<I: Image + ResizableImage<RI>, RI: Image>(
225 img: &I,
226 width: NonZeroU32,
227 height: NonZeroU32,
228) -> Result<ScoredCrop, Error> {
229 let pre_crop = find_border_crop(img);
230
231 match pre_crop {
232 Some(crop) => {
233 let cropped = CroppedImage::new(img, crop.clone());
234 let mut best_crop = find_best_crop(&cropped, width, height)?;
235 best_crop.crop = best_crop.crop.base_on(&crop);
236 Ok(best_crop)
237 }
238 None => find_best_crop(img, width, height),
239 }
240}