smartcrop/
lib.rs

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
18/// Trait for images to be procressed by Smartcrop
19pub trait Image: Sized {
20    /// Get the width of the image
21    fn width(&self) -> u32;
22    /// Get the height of the image
23    fn height(&self) -> u32;
24    /// Get the color of a pixel
25    fn get(&self, x: u32, y: u32) -> RGB;
26}
27
28/// Trait for images to be resized by Smartcrop
29///
30/// Smartcrop downscales images to improve performance
31pub trait ResizableImage<I: Image> {
32    /// Resize the image to the specified dimensions
33    fn resize(&self, width: u32, height: u32) -> I;
34    /// First crop the image, then resize it to the specified dimensions
35    fn crop_and_resize(&self, crop: Crop, width: u32, height: u32) -> I;
36}
37
38/// Error that occurred during a Smartcrop operation
39#[derive(PartialEq, Debug)]
40pub enum Error {
41    /// The given image is of size zero
42    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/// 24bit RGB color
54#[derive(Copy, Clone, PartialEq, Debug)]
55pub struct RGB {
56    /// Red (0-255)
57    pub r: u8,
58    /// Green (0-255)
59    pub g: u8,
60    /// Blue (0-255)
61    pub b: u8,
62}
63
64impl RGB {
65    /// Create a new 24bit RGB color
66    pub fn new(r: u8, g: u8, b: u8) -> RGB {
67        RGB { r, g, b }
68    }
69
70    fn cie(self: &RGB) -> f64 {
71        //TODO: Change it as soon as https://github.com/jwagner/smartcrop.js/issues/77 is closed
72        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/// Score used to determine the best crop
116#[derive(Clone, PartialEq, Debug)]
117pub struct Score {
118    /// Detail score
119    pub detail: f64,
120    /// Saturation score
121    pub saturation: f64,
122    /// Skin color score
123    pub skin: f64,
124    /// Total weighted score of the crop
125    pub total: f64,
126}
127
128/// Crop position and size
129#[derive(Clone, PartialEq, Debug)]
130pub struct Crop {
131    /// x position of the cropped image
132    pub x: u32,
133    /// y position of the cropped image
134    pub y: u32,
135    /// width of the cropped image
136    pub width: u32,
137    /// height of the cropped image
138    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/// Crop with attached score
162#[derive(Debug)]
163pub struct ScoredCrop {
164    /// Crop position and size
165    pub crop: Crop,
166    /// Score used to determine the best crop
167    pub score: Score,
168}
169
170impl ScoredCrop {
171    /// Scale the crop to be applied to an image of a different size
172    pub fn scale(&self, ratio: f64) -> ScoredCrop {
173        ScoredCrop {
174            crop: self.crop.scale(ratio),
175            score: self.score.clone(),
176        }
177    }
178}
179
180/// Image wrapper for cropping images without modifying the underlying data structure
181struct 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
222/// Analyze the image and find the best crop of the given aspect ratio
223/// which excludes black borders
224pub 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}