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 = ℑ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}