image_effects/
lib.rs

1//! This crate provides multiple effects that can be applied on an image.
2//!
3//! Currently there's two classes of effects:
4//!
5//! 1. [**Dithering**](./dither/index.html) - Limiting the colour palette of a given image while still
6//!    retaining more detail than a purely quantized approach would.
7//! 2. [**Filtering**](./filter/algorithms/index.html) - Some more common effects applied on an image, such as brightness,
8//!    contrast, gradient mapping, and more.
9//!
10//! The library lets these effects work on a variety of types, including some from the `image` crate. If you're not using
11//! `image` however, you can rely on the implementations on intermediate types:
12//! 
13//! - For pixels, it's `[u8; 3]` for RGB and `[u8; 4]` for RGBA.
14//! - For images, it's `Vec<Vec<{Pixel}>>` - where `Pixel` is anything listed above.
15//!
16//! The *prelude* is useful for importing some common functionality, like the algorithms themselves
17//! alongside some traits.
18//! 
19//! **Note:** Colour palettes _currently_ require the `palette` crate - as 
20//! they are defined using its `Srgb` type.
21//! 
22//! # Usage
23//! 
24//! This usage is simplified to focus on the logic in this crate, rather than on `image` or `palette`.
25//! 
26//! ```ignore
27//! use image::DynamicImage;
28//! use image_effects::{
29//!     prelude::*
30//!     dither::FLOYD_STEINBERG,
31//!     palette::named,
32//! }
33//! 
34//! fn get_image() -> DynamicImage { /* ... */ }
35//! 
36//! fn main() {
37//!     let image = get_image();
38//!     let palette = vec![
39//!         named::BLACK, named::WHITE
40//!     ];
41//! 
42//!     image
43//!         .apply(&filters::HueRotate(180.0))
44//!         .apply(&FLOYD_STEINBERG.with_palette(palette))
45//!         .save("image.png");
46//! }
47//! ```
48//! 
49//! # Effects
50//! 
51//! [`Effect<T>`](effect/trait.Effect.html) is the trait to implement here, since
52//! [`Affectable<T, E>`](effect/trait.Affectable.html) will be automatically implemented.
53//! 
54//! Basically, `Effect<T>` defines an effect that can be applied on `T` - so in turn `Affectable` can depend on
55//! this implementation to attach an `.apply` method _onto_ `T` that accepts `Effect<T>`.
56//! 
57//! In other words, if I implement `Effect<Image>` on an effect called `Brighten`, I can then call `.apply` on
58//! any `Image` and pass in a reference to `Brighten`.
59//! 
60//! This also means that although you can define your own effects easily, the same isn't for new kinds of image.
61//! You can implement `Affectable`, but not `Effect` due to the external trait rule:
62//! 
63//! > When implementing a trait, you must either own _the trait_ or _the struct_.
64//! 
65//! Since external crates don't own `Effect<T>` _nor_ the effects provided by this library, this effectively locks
66//! you out of defining new `T`s directly.
67//! 
68//! However, since most effects get implemented using an intermediate format, such as `[u8; 3]` for an RGB pixel or
69//! `Vec<Vec<[u8; 3]>>` for an image, theoretically you just need to convert whatever image/medium you'd like to apply
70//! effects on into one of these intermediate formats.
71//! 
72//! Also, when creating an effect you don't need to define every single image it's compatible too - as auto-implementation
73//! happens here as well. For example, if you implement an effect that can be applied on `[u8; 3]`, this will also result 
74//! in implementations for RGBA `[u8; 4]`, images, and beyond. As a result, it's always best to define an `Effect` on the simplest
75//! possible type.
76
77
78/// Various dithering algorithms, including both error propagation and ordered.
79/// 
80/// For error propagation, existing algorithms are setup as constants. These aren't
81/// directly usable as an effect since they need to be configured with a palette.
82/// 
83/// As for `Bayer`, it requires a palette to be initialized with, so it doesn't face
84/// this issue.
85pub mod dither;
86
87/// Filters that can be applied to the image - such as brightness, contrast, and more.
88pub mod filter;
89
90/// Utilities. Mostly just for the test cases - will probably be removed.
91mod utils;
92
93/// Colour related logic, such as distance functions, palettes, gradient generation, etc.
94pub mod colour;
95
96/// Traits and implementations for _effects_ and anything that can be affected by them.
97pub mod effect;
98
99/// Prelude for including the useful elements from the library - including algorithms, traits, and constants.
100pub mod prelude {
101    // algorithms
102    pub use crate::dither;
103    pub use crate::filter::filters;
104    
105    // traits
106    pub use crate::effect::Effect;
107    pub use crate::effect::Affectable;
108    pub use crate::colour::gradient::{
109        IntoGradient,
110        IntoGradientHsl,
111        IntoGradientLch,
112        IntoGradientOklch,
113    };
114
115    // constants
116    pub use crate::colour::colours::srgb as SrgbColour;
117    pub use crate::colour::palettes;
118}
119
120#[macro_export]
121/// Helps construct a gradient map from colours.
122///
123/// You *could* construct the map yourself, however the purpose of this is mostly to
124/// provide an easily usable and *clean* way to construct a gradient map.
125///
126/// The following is an example usage of this macro:
127/// ```ignore
128/// let hsl: GradientMap<Hsl<Srgb>> = gradient_map!(
129///     0.00 => Hsl::new(0.0, 0.0, 0.0),
130///     1.00 => Hsl::new(0.0, 0.0, 1.0),
131/// );
132/// ```
133macro_rules! gradient_map {
134    [$($threshold:expr => $color:expr),*] => {
135        &[
136            $(
137                ($color, $threshold)
138            ),*
139        ]
140    };
141}
142
143pub type GradientMap<'a, Color> = &'a [(Color, f32)];
144
145
146#[cfg(test)]
147mod test {
148    use std::error::Error;
149
150    use image::{DynamicImage, ImageResult, GenericImageView, imageops};
151    use palette::{Srgb, named};
152
153    use crate::{
154        colour::{utils::ONE_BIT},
155        prelude::{*, palettes::{WEB_SAFE, EIGHT_BIT}}, dither::{FLOYD_STEINBERG, JARVIS_JUDICE_NINKE, STUCKI, ATKINSON, BURKES, SIERRA, SIERRA_TWO_ROW, SIERRA_LITE, bayer::Bayer},
156    };
157
158    type UtilResult<T> = Result<T,Box<dyn Error>>;
159
160    const IMAGE_URL: &'static str = "https://clipart-library.com/image_gallery/n781743.png";
161    const MAX_DIM: Option<usize> = Some(500);
162
163    fn get_image() -> UtilResult<DynamicImage> {
164        let img_bytes = reqwest::blocking::get(IMAGE_URL)?.bytes()?;
165        let image = image::load_from_memory(&img_bytes)?;
166        
167        let image = if let Some(max_dim) = MAX_DIM {
168            let (x, y) = image.dimensions();
169            if max_dim < x.max(y) as usize {
170                let image = &image;let factor = max_dim as f32 / x.max(y) as f32;
171                let mul = |int: u32, float: f32| (int as f32 * float) as u32;
172                image.resize(mul(x, factor), mul(y, factor), imageops::Nearest)
173            } else { image }
174        } else { image };
175
176        Ok(image)
177    }
178
179    #[test]
180    fn dither_test() -> UtilResult<()> {
181        let image = get_image()?;
182
183        let palette = [
184            named::PURPLE.into_format().build_gradient_lch(10),
185            named::GOLD.into_format().build_gradient_lch(10),
186        ].concat();
187
188        dither(&image, ONE_BIT.to_vec(), Some("-mono"))?;
189        dither(&image, WEB_SAFE.to_vec(), Some("-web-safe"))?;
190        dither(&image, EIGHT_BIT.to_vec(), Some("-8-bit"))?;
191        dither(&image, palette, Some("-custom-palette"))?;
192
193        Ok(())
194    }
195
196    #[test]
197    fn filter_effects_test() -> UtilResult<()> {
198        let image = get_image()?;
199
200        image
201            .clone()
202            .apply(&filters::HueRotate(180.0))
203            .save("data/colour/rotate-hue-180.png")?;
204        image
205            .clone()
206            .apply(&filters::Brighten( 0.2))
207            .save("data/colour/brighten+0.2.png")?;
208        image
209            .clone()
210            .apply(&filters::Brighten(-0.2))
211            .save("data/colour/brighten-0.2.png")?;
212        image
213            .clone()
214            .apply(&filters::Saturate( 0.2))
215            .save("data/colour/saturate+0.2.png")?;
216        image
217            .clone()
218            .apply(&filters::Saturate(-0.2))
219            .save("data/colour/saturate-0.2.png")?;
220        image
221            .clone()
222            .apply(&filters::Contrast(0.5))
223            .save("data/colour/contrast.0.5.png")?;
224        image
225            .clone()
226            .apply(&filters::Contrast(1.5))
227            .save("data/colour/contrast.1.5.png")?;
228
229        let _gradient_map = [
230            (Srgb::new(0.0, 0.0, 1.0), 0.00),
231            (Srgb::new(1.0, 0.0, 0.0), 0.50),
232            (Srgb::new(0.0, 1.0, 0.0), 1.00),
233        ];
234
235        let mut gradient_map = filters::GradientMap::new();
236        gradient_map
237            .add_entry(Srgb::new(0.0, 0.0, 1.0), 0.00)
238            .add_entry(Srgb::new(1.0, 0.0, 0.0), 0.50)
239            .add_entry(Srgb::new(0.0, 1.0, 0.0), 1.00);
240
241        image
242            .clone()
243            .apply(&gradient_map)
244            .save("data/colour/gradient-mapped.png")?;
245
246        let hue_palette = vec![180.0, 300.0];
247
248        image
249            .clone()
250            .apply(&filters::QuantizeHue::with_hues(hue_palette))
251            .save("data/colour/quantize-hue.png")?;
252
253        image
254            .clone()
255            .apply(&filters::MultiplyHue(4.0))
256            .save("data/colour/multiply-hue.4.0.png")?;
257
258        image
259            .clone()
260            .apply(&filters::MultiplyHue(12.0))
261            .save("data/colour/multiply-hue.12.0.png")?;
262
263        Ok(())
264    }
265
266    fn dither(
267        image: &DynamicImage,
268        palette: Vec<Srgb>,
269        opt_postfix: Option<&str>,
270    ) -> ImageResult<()> {
271        let postfix = opt_postfix.unwrap_or("");
272
273        let error_propagators = vec![
274            FLOYD_STEINBERG,
275            JARVIS_JUDICE_NINKE,
276            STUCKI,
277            ATKINSON,
278            BURKES,
279            SIERRA,
280            SIERRA_TWO_ROW,
281            SIERRA_LITE
282        ];
283
284        for propagator in error_propagators.into_iter() {
285            image.clone()
286                .apply(&propagator.with_palette(palette.clone()))
287                .save(format!("data/dither/{}{}.png", propagator.name, postfix))?;
288        }
289
290        image.clone().apply(&Bayer::new(2, palette.clone()))
291            .save(format!("data/dither/bayer-2x2{}.png", postfix))?;
292        image.clone().apply(&Bayer::new(4, palette.clone()))
293            .save(format!("data/dither/bayer-4x4{}.png", postfix))?;
294        image.clone().apply(&Bayer::new(8, palette.clone()))
295            .save(format!("data/dither/bayer-8x8{}.png", postfix))?;
296        image.clone().apply(&Bayer::new(16, palette.clone()))
297            .save(format!("data/dither/bayer-16x16{}.png", postfix))?;
298        Ok(())
299    }
300}