1 | //! Functions for altering and converting the color of pixelbufs |
---|---|
2 | |
3 | use num_traits::NumCast; |
4 | use std::f64::consts::PI; |
5 | |
6 | use crate::color::{FromColor, IntoColor, Luma, LumaA, Rgba}; |
7 | use crate::image::{GenericImage, GenericImageView}; |
8 | use crate::traits::{Pixel, Primitive}; |
9 | use crate::utils::clamp; |
10 | use crate::ImageBuffer; |
11 | |
12 | type Subpixel<I> = <<I as GenericImageView>::Pixel as Pixel>::Subpixel; |
13 | |
14 | /// Convert the supplied image to grayscale. Alpha channel is discarded. |
15 | pub fn grayscale<I: GenericImageView>( |
16 | image: &I, |
17 | ) -> ImageBuffer<Luma<Subpixel<I>>, Vec<Subpixel<I>>> { |
18 | grayscale_with_type(image) |
19 | } |
20 | |
21 | /// Convert the supplied image to grayscale. Alpha channel is preserved. |
22 | pub fn grayscale_alpha<I: GenericImageView>( |
23 | image: &I, |
24 | ) -> ImageBuffer<LumaA<Subpixel<I>>, Vec<Subpixel<I>>> { |
25 | grayscale_with_type_alpha(image) |
26 | } |
27 | |
28 | /// Convert the supplied image to a grayscale image with the specified pixel type. Alpha channel is discarded. |
29 | pub fn grayscale_with_type<NewPixel, I: GenericImageView>( |
30 | image: &I, |
31 | ) -> ImageBuffer<NewPixel, Vec<NewPixel::Subpixel>> |
32 | where |
33 | NewPixel: Pixel + FromColor<Luma<Subpixel<I>>>, |
34 | { |
35 | let (width: u32, height: u32) = image.dimensions(); |
36 | let mut out: ImageBuffer |
37 | |
38 | for (x: u32, y: u32, pixel: ::Pixel) in image.pixels() { |
39 | let grayscale: Luma<<::Pixel as Pixel>::Subpixel> = pixel.to_luma(); |
40 | let new_pixel: NewPixel = grayscale.into_color(); // no-op for luma->luma |
41 | |
42 | out.put_pixel(x, y, new_pixel); |
43 | } |
44 | |
45 | out |
46 | } |
47 | |
48 | /// Convert the supplied image to a grayscale image with the specified pixel type. Alpha channel is preserved. |
49 | pub fn grayscale_with_type_alpha<NewPixel, I: GenericImageView>( |
50 | image: &I, |
51 | ) -> ImageBuffer<NewPixel, Vec<NewPixel::Subpixel>> |
52 | where |
53 | NewPixel: Pixel + FromColor<LumaA<Subpixel<I>>>, |
54 | { |
55 | let (width: u32, height: u32) = image.dimensions(); |
56 | let mut out: ImageBuffer |
57 | |
58 | for (x: u32, y: u32, pixel: ::Pixel) in image.pixels() { |
59 | let grayscale: LumaA<<::Pixel as Pixel>::Subpixel> = pixel.to_luma_alpha(); |
60 | let new_pixel: NewPixel = grayscale.into_color(); // no-op for luma->luma |
61 | |
62 | out.put_pixel(x, y, new_pixel); |
63 | } |
64 | |
65 | out |
66 | } |
67 | |
68 | /// Invert each pixel within the supplied image. |
69 | /// This function operates in place. |
70 | pub fn invert<I: GenericImage>(image: &mut I) { |
71 | // TODO find a way to use pixels? |
72 | let (width: u32, height: u32) = image.dimensions(); |
73 | |
74 | for y: u32 in 0..height { |
75 | for x: u32 in 0..width { |
76 | let mut p: ::Pixel = image.get_pixel(x, y); |
77 | p.invert(); |
78 | |
79 | image.put_pixel(x, y, pixel:p); |
80 | } |
81 | } |
82 | } |
83 | |
84 | /// Adjust the contrast of the supplied image. |
85 | /// ```contrast``` is the amount to adjust the contrast by. |
86 | /// Negative values decrease the contrast and positive values increase the contrast. |
87 | /// |
88 | /// *[See also `contrast_in_place`.][contrast_in_place]* |
89 | pub fn contrast<I, P, S>(image: &I, contrast: f32) -> ImageBuffer<P, Vec<S>> |
90 | where |
91 | I: GenericImageView<Pixel = P>, |
92 | P: Pixel<Subpixel = S> + 'static, |
93 | S: Primitive + 'static, |
94 | { |
95 | let (width: u32, height: u32) = image.dimensions(); |
96 | let mut out: ImageBuffer > = ImageBuffer::new(width, height); |
97 | |
98 | let max: S = S::DEFAULT_MAX_VALUE; |
99 | let max: f32 = NumCast::from(max).unwrap(); |
100 | |
101 | let percent: f32 = ((100.0 + contrast) / 100.0).powi(2); |
102 | |
103 | for (x: u32, y: u32, pixel: P) in image.pixels() { |
104 | let f: P = pixel.map(|b: S| { |
105 | let c: f32 = NumCast::from(b).unwrap(); |
106 | |
107 | let d: f32 = ((c / max - 0.5) * percent + 0.5) * max; |
108 | let e: f32 = clamp(a:d, min:0.0, max); |
109 | |
110 | NumCast::from(e).unwrap() |
111 | }); |
112 | out.put_pixel(x, y, pixel:f); |
113 | } |
114 | |
115 | out |
116 | } |
117 | |
118 | /// Adjust the contrast of the supplied image in place. |
119 | /// ```contrast``` is the amount to adjust the contrast by. |
120 | /// Negative values decrease the contrast and positive values increase the contrast. |
121 | /// |
122 | /// *[See also `contrast`.][contrast]* |
123 | pub fn contrast_in_place<I>(image: &mut I, contrast: f32) |
124 | where |
125 | I: GenericImage, |
126 | { |
127 | let (width: u32, height: u32) = image.dimensions(); |
128 | |
129 | let max: <::Pixel as Pixel>::Subpixel = <I::Pixel as Pixel>::Subpixel::DEFAULT_MAX_VALUE; |
130 | let max: f32 = NumCast::from(max).unwrap(); |
131 | |
132 | let percent: f32 = ((100.0 + contrast) / 100.0).powi(2); |
133 | |
134 | // TODO find a way to use pixels? |
135 | for y: u32 in 0..height { |
136 | for x: u32 in 0..width { |
137 | let f: ::Pixel = image.get_pixel(x, y).map(|b: <::Pixel as Pixel>::Subpixel| { |
138 | let c: f32 = NumCast::from(b).unwrap(); |
139 | |
140 | let d: f32 = ((c / max - 0.5) * percent + 0.5) * max; |
141 | let e: f32 = clamp(a:d, min:0.0, max); |
142 | |
143 | NumCast::from(e).unwrap() |
144 | }); |
145 | |
146 | image.put_pixel(x, y, pixel:f); |
147 | } |
148 | } |
149 | } |
150 | |
151 | /// Brighten the supplied image. |
152 | /// ```value``` is the amount to brighten each pixel by. |
153 | /// Negative values decrease the brightness and positive values increase it. |
154 | /// |
155 | /// *[See also `brighten_in_place`.][brighten_in_place]* |
156 | pub fn brighten<I, P, S>(image: &I, value: i32) -> ImageBuffer<P, Vec<S>> |
157 | where |
158 | I: GenericImageView<Pixel = P>, |
159 | P: Pixel<Subpixel = S> + 'static, |
160 | S: Primitive + 'static, |
161 | { |
162 | let (width: u32, height: u32) = image.dimensions(); |
163 | let mut out: ImageBuffer > = ImageBuffer::new(width, height); |
164 | |
165 | let max: S = S::DEFAULT_MAX_VALUE; |
166 | let max: i32 = NumCast::from(max).unwrap(); |
167 | |
168 | for (x: u32, y: u32, pixel: P) in image.pixels() { |
169 | let e: P = pixel.map_with_alpha( |
170 | |b| { |
171 | let c: i32 = NumCast::from(b).unwrap(); |
172 | let d = clamp(c + value, 0, max); |
173 | |
174 | NumCast::from(d).unwrap() |
175 | }, |
176 | |alpha: S| alpha, |
177 | ); |
178 | out.put_pixel(x, y, pixel:e); |
179 | } |
180 | |
181 | out |
182 | } |
183 | |
184 | /// Brighten the supplied image in place. |
185 | /// ```value``` is the amount to brighten each pixel by. |
186 | /// Negative values decrease the brightness and positive values increase it. |
187 | /// |
188 | /// *[See also `brighten`.][brighten]* |
189 | pub fn brighten_in_place<I>(image: &mut I, value: i32) |
190 | where |
191 | I: GenericImage, |
192 | { |
193 | let (width: u32, height: u32) = image.dimensions(); |
194 | |
195 | let max: <::Pixel as Pixel>::Subpixel = <I::Pixel as Pixel>::Subpixel::DEFAULT_MAX_VALUE; |
196 | let max: i32 = NumCast::from(max).unwrap(); // TODO what does this do for f32? clamp at 1?? |
197 | |
198 | // TODO find a way to use pixels? |
199 | for y: u32 in 0..height { |
200 | for x: u32 in 0..width { |
201 | let e: ::Pixel = image.get_pixel(x, y).map_with_alpha( |
202 | |b| { |
203 | let c: i32 = NumCast::from(b).unwrap(); |
204 | let d = clamp(c + value, 0, max); |
205 | |
206 | NumCast::from(d).unwrap() |
207 | }, |
208 | |alpha: <::Pixel as Pixel>::Subpixel| alpha, |
209 | ); |
210 | |
211 | image.put_pixel(x, y, pixel:e); |
212 | } |
213 | } |
214 | } |
215 | |
216 | /// Hue rotate the supplied image. |
217 | /// `value` is the degrees to rotate each pixel by. |
218 | /// 0 and 360 do nothing, the rest rotates by the given degree value. |
219 | /// just like the css webkit filter hue-rotate(180) |
220 | /// |
221 | /// *[See also `huerotate_in_place`.][huerotate_in_place]* |
222 | pub fn huerotate<I, P, S>(image: &I, value: i32) -> ImageBuffer<P, Vec<S>> |
223 | where |
224 | I: GenericImageView<Pixel = P>, |
225 | P: Pixel<Subpixel = S> + 'static, |
226 | S: Primitive + 'static, |
227 | { |
228 | let (width, height) = image.dimensions(); |
229 | let mut out = ImageBuffer::new(width, height); |
230 | |
231 | let angle: f64 = NumCast::from(value).unwrap(); |
232 | |
233 | let cosv = (angle * PI / 180.0).cos(); |
234 | let sinv = (angle * PI / 180.0).sin(); |
235 | let matrix: [f64; 9] = [ |
236 | // Reds |
237 | 0.213 + cosv * 0.787 - sinv * 0.213, |
238 | 0.715 - cosv * 0.715 - sinv * 0.715, |
239 | 0.072 - cosv * 0.072 + sinv * 0.928, |
240 | // Greens |
241 | 0.213 - cosv * 0.213 + sinv * 0.143, |
242 | 0.715 + cosv * 0.285 + sinv * 0.140, |
243 | 0.072 - cosv * 0.072 - sinv * 0.283, |
244 | // Blues |
245 | 0.213 - cosv * 0.213 - sinv * 0.787, |
246 | 0.715 - cosv * 0.715 + sinv * 0.715, |
247 | 0.072 + cosv * 0.928 + sinv * 0.072, |
248 | ]; |
249 | for (x, y, pixel) in out.enumerate_pixels_mut() { |
250 | let p = image.get_pixel(x, y); |
251 | |
252 | #[allow(deprecated)] |
253 | let (k1, k2, k3, k4) = p.channels4(); |
254 | let vec: (f64, f64, f64, f64) = ( |
255 | NumCast::from(k1).unwrap(), |
256 | NumCast::from(k2).unwrap(), |
257 | NumCast::from(k3).unwrap(), |
258 | NumCast::from(k4).unwrap(), |
259 | ); |
260 | |
261 | let r = vec.0; |
262 | let g = vec.1; |
263 | let b = vec.2; |
264 | |
265 | let new_r = matrix[0] * r + matrix[1] * g + matrix[2] * b; |
266 | let new_g = matrix[3] * r + matrix[4] * g + matrix[5] * b; |
267 | let new_b = matrix[6] * r + matrix[7] * g + matrix[8] * b; |
268 | let max = 255f64; |
269 | |
270 | #[allow(deprecated)] |
271 | let outpixel = Pixel::from_channels( |
272 | NumCast::from(clamp(new_r, 0.0, max)).unwrap(), |
273 | NumCast::from(clamp(new_g, 0.0, max)).unwrap(), |
274 | NumCast::from(clamp(new_b, 0.0, max)).unwrap(), |
275 | NumCast::from(clamp(vec.3, 0.0, max)).unwrap(), |
276 | ); |
277 | *pixel = outpixel; |
278 | } |
279 | out |
280 | } |
281 | |
282 | /// Hue rotate the supplied image in place. |
283 | /// `value` is the degrees to rotate each pixel by. |
284 | /// 0 and 360 do nothing, the rest rotates by the given degree value. |
285 | /// just like the css webkit filter hue-rotate(180) |
286 | /// |
287 | /// *[See also `huerotate`.][huerotate]* |
288 | pub fn huerotate_in_place<I>(image: &mut I, value: i32) |
289 | where |
290 | I: GenericImage, |
291 | { |
292 | let (width, height) = image.dimensions(); |
293 | |
294 | let angle: f64 = NumCast::from(value).unwrap(); |
295 | |
296 | let cosv = (angle * PI / 180.0).cos(); |
297 | let sinv = (angle * PI / 180.0).sin(); |
298 | let matrix: [f64; 9] = [ |
299 | // Reds |
300 | 0.213 + cosv * 0.787 - sinv * 0.213, |
301 | 0.715 - cosv * 0.715 - sinv * 0.715, |
302 | 0.072 - cosv * 0.072 + sinv * 0.928, |
303 | // Greens |
304 | 0.213 - cosv * 0.213 + sinv * 0.143, |
305 | 0.715 + cosv * 0.285 + sinv * 0.140, |
306 | 0.072 - cosv * 0.072 - sinv * 0.283, |
307 | // Blues |
308 | 0.213 - cosv * 0.213 - sinv * 0.787, |
309 | 0.715 - cosv * 0.715 + sinv * 0.715, |
310 | 0.072 + cosv * 0.928 + sinv * 0.072, |
311 | ]; |
312 | |
313 | // TODO find a way to use pixels? |
314 | for y in 0..height { |
315 | for x in 0..width { |
316 | let pixel = image.get_pixel(x, y); |
317 | |
318 | #[allow(deprecated)] |
319 | let (k1, k2, k3, k4) = pixel.channels4(); |
320 | |
321 | let vec: (f64, f64, f64, f64) = ( |
322 | NumCast::from(k1).unwrap(), |
323 | NumCast::from(k2).unwrap(), |
324 | NumCast::from(k3).unwrap(), |
325 | NumCast::from(k4).unwrap(), |
326 | ); |
327 | |
328 | let r = vec.0; |
329 | let g = vec.1; |
330 | let b = vec.2; |
331 | |
332 | let new_r = matrix[0] * r + matrix[1] * g + matrix[2] * b; |
333 | let new_g = matrix[3] * r + matrix[4] * g + matrix[5] * b; |
334 | let new_b = matrix[6] * r + matrix[7] * g + matrix[8] * b; |
335 | let max = 255f64; |
336 | |
337 | #[allow(deprecated)] |
338 | let outpixel = Pixel::from_channels( |
339 | NumCast::from(clamp(new_r, 0.0, max)).unwrap(), |
340 | NumCast::from(clamp(new_g, 0.0, max)).unwrap(), |
341 | NumCast::from(clamp(new_b, 0.0, max)).unwrap(), |
342 | NumCast::from(clamp(vec.3, 0.0, max)).unwrap(), |
343 | ); |
344 | |
345 | image.put_pixel(x, y, outpixel); |
346 | } |
347 | } |
348 | } |
349 | |
350 | /// A color map |
351 | pub trait ColorMap { |
352 | /// The color type on which the map operates on |
353 | type Color; |
354 | /// Returns the index of the closest match of `color` |
355 | /// in the color map. |
356 | fn index_of(&self, color: &Self::Color) -> usize; |
357 | /// Looks up color by index in the color map. If `idx` is out of range for the color map, or |
358 | /// ColorMap doesn't implement `lookup` `None` is returned. |
359 | fn lookup(&self, index: usize) -> Option<Self::Color> { |
360 | let _ = index; |
361 | None |
362 | } |
363 | /// Determine if this implementation of ColorMap overrides the default `lookup`. |
364 | fn has_lookup(&self) -> bool { |
365 | false |
366 | } |
367 | /// Maps `color` to the closest color in the color map. |
368 | fn map_color(&self, color: &mut Self::Color); |
369 | } |
370 | |
371 | /// A bi-level color map |
372 | /// |
373 | /// # Examples |
374 | /// ``` |
375 | /// use image::imageops::colorops::{index_colors, BiLevel, ColorMap}; |
376 | /// use image::{ImageBuffer, Luma}; |
377 | /// |
378 | /// let (w, h) = (16, 16); |
379 | /// // Create an image with a smooth horizontal gradient from black (0) to white (255). |
380 | /// let gray = ImageBuffer::from_fn(w, h, |x, y| -> Luma<u8> { [(255 * x / w) as u8].into() }); |
381 | /// // Mapping the gray image through the `BiLevel` filter should map gray pixels less than half |
382 | /// // intensity (127) to black (0), and anything greater to white (255). |
383 | /// let cmap = BiLevel; |
384 | /// let palletized = index_colors(&gray, &cmap); |
385 | /// let mapped = ImageBuffer::from_fn(w, h, |x, y| { |
386 | /// let p = palletized.get_pixel(x, y); |
387 | /// cmap.lookup(p.0[0] as usize) |
388 | /// .expect("indexed color out-of-range") |
389 | /// }); |
390 | /// // Create an black and white image of expected output. |
391 | /// let bw = ImageBuffer::from_fn(w, h, |x, y| -> Luma<u8> { |
392 | /// if x <= (w / 2) { |
393 | /// [0].into() |
394 | /// } else { |
395 | /// [255].into() |
396 | /// } |
397 | /// }); |
398 | /// assert_eq!(mapped, bw); |
399 | /// ``` |
400 | #[derive(Clone, Copy)] |
401 | pub struct BiLevel; |
402 | |
403 | impl ColorMap for BiLevel { |
404 | type Color = Luma<u8>; |
405 | |
406 | #[inline(always)] |
407 | fn index_of(&self, color: &Luma<u8>) -> usize { |
408 | let luma = color.0; |
409 | if luma[0] > 127 { |
410 | 1 |
411 | } else { |
412 | 0 |
413 | } |
414 | } |
415 | |
416 | #[inline(always)] |
417 | fn lookup(&self, idx: usize) -> Option<Self::Color> { |
418 | match idx { |
419 | 0 => Some([0].into()), |
420 | 1 => Some([255].into()), |
421 | _ => None, |
422 | } |
423 | } |
424 | |
425 | /// Indicate NeuQuant implements `lookup`. |
426 | fn has_lookup(&self) -> bool { |
427 | true |
428 | } |
429 | |
430 | #[inline(always)] |
431 | fn map_color(&self, color: &mut Luma<u8>) { |
432 | let new_color = 0xFF * self.index_of(color) as u8; |
433 | let luma = &mut color.0; |
434 | luma[0] = new_color; |
435 | } |
436 | } |
437 | |
438 | impl ColorMap for color_quant::NeuQuant { |
439 | type Color = Rgba<u8>; |
440 | |
441 | #[inline(always)] |
442 | fn index_of(&self, color: &Rgba<u8>) -> usize { |
443 | self.index_of(pixel:color.channels()) |
444 | } |
445 | |
446 | #[inline(always)] |
447 | fn lookup(&self, idx: usize) -> Option<Self::Color> { |
448 | self.lookup(idx).map(|p: [u8; 4]| p.into()) |
449 | } |
450 | |
451 | /// Indicate NeuQuant implements `lookup`. |
452 | fn has_lookup(&self) -> bool { |
453 | true |
454 | } |
455 | |
456 | #[inline(always)] |
457 | fn map_color(&self, color: &mut Rgba<u8>) { |
458 | self.map_pixel(color.channels_mut()) |
459 | } |
460 | } |
461 | |
462 | /// Floyd-Steinberg error diffusion |
463 | fn diffuse_err<P: Pixel<Subpixel = u8>>(pixel: &mut P, error: [i16; 3], factor: i16) { |
464 | for (e: &i16, c: &mut u8) in error.iter().zip(pixel.channels_mut().iter_mut()) { |
465 | *c = match <i16 as From<_>>::from(*c) + e * factor / 16 { |
466 | val: i16 if val < 0 => 0, |
467 | val: i16 if val > 0xFF => 0xFF, |
468 | val: i16 => val as u8, |
469 | } |
470 | } |
471 | } |
472 | |
473 | macro_rules! do_dithering( |
474 | ($map:expr, $image:expr, $err:expr, $x:expr, $y:expr) => ( |
475 | { |
476 | let old_pixel = $image[($x, $y)]; |
477 | let new_pixel = $image.get_pixel_mut($x, $y); |
478 | $map.map_color(new_pixel); |
479 | for ((e, &old), &new) in $err.iter_mut() |
480 | .zip(old_pixel.channels().iter()) |
481 | .zip(new_pixel.channels().iter()) |
482 | { |
483 | *e = <i16 as From<_>>::from(old) - <i16 as From<_>>::from(new) |
484 | } |
485 | } |
486 | ) |
487 | ); |
488 | |
489 | /// Reduces the colors of the image using the supplied `color_map` while applying |
490 | /// Floyd-Steinberg dithering to improve the visual conception |
491 | pub fn dither<Pix, Map>(image: &mut ImageBuffer<Pix, Vec<u8>>, color_map: &Map) |
492 | where |
493 | Map: ColorMap<Color = Pix> + ?Sized, |
494 | Pix: Pixel<Subpixel = u8> + 'static, |
495 | { |
496 | let (width, height) = image.dimensions(); |
497 | let mut err: [i16; 3] = [0; 3]; |
498 | for y in 0..height - 1 { |
499 | let x = 0; |
500 | do_dithering!(color_map, image, err, x, y); |
501 | diffuse_err(image.get_pixel_mut(x + 1, y), err, 7); |
502 | diffuse_err(image.get_pixel_mut(x, y + 1), err, 5); |
503 | diffuse_err(image.get_pixel_mut(x + 1, y + 1), err, 1); |
504 | for x in 1..width - 1 { |
505 | do_dithering!(color_map, image, err, x, y); |
506 | diffuse_err(image.get_pixel_mut(x + 1, y), err, 7); |
507 | diffuse_err(image.get_pixel_mut(x - 1, y + 1), err, 3); |
508 | diffuse_err(image.get_pixel_mut(x, y + 1), err, 5); |
509 | diffuse_err(image.get_pixel_mut(x + 1, y + 1), err, 1); |
510 | } |
511 | let x = width - 1; |
512 | do_dithering!(color_map, image, err, x, y); |
513 | diffuse_err(image.get_pixel_mut(x - 1, y + 1), err, 3); |
514 | diffuse_err(image.get_pixel_mut(x, y + 1), err, 5); |
515 | } |
516 | let y = height - 1; |
517 | let x = 0; |
518 | do_dithering!(color_map, image, err, x, y); |
519 | diffuse_err(image.get_pixel_mut(x + 1, y), err, 7); |
520 | for x in 1..width - 1 { |
521 | do_dithering!(color_map, image, err, x, y); |
522 | diffuse_err(image.get_pixel_mut(x + 1, y), err, 7); |
523 | } |
524 | let x = width - 1; |
525 | do_dithering!(color_map, image, err, x, y); |
526 | } |
527 | |
528 | /// Reduces the colors using the supplied `color_map` and returns an image of the indices |
529 | pub fn index_colors<Pix, Map>( |
530 | image: &ImageBuffer<Pix, Vec<u8>>, |
531 | color_map: &Map, |
532 | ) -> ImageBuffer<Luma<u8>, Vec<u8>> |
533 | where |
534 | Map: ColorMap<Color = Pix> + ?Sized, |
535 | Pix: Pixel<Subpixel = u8> + 'static, |
536 | { |
537 | let mut indices: ImageBuffer |
538 | for (pixel: &Pix, idx: &mut Luma |
539 | *idx = Luma([color_map.index_of(color:pixel) as u8]) |
540 | } |
541 | indices |
542 | } |
543 | |
544 | #[cfg(test)] |
545 | mod test { |
546 | |
547 | use super::*; |
548 | use crate::GrayImage; |
549 | |
550 | macro_rules! assert_pixels_eq { |
551 | ($actual:expr, $expected:expr) => {{ |
552 | let actual_dim = $actual.dimensions(); |
553 | let expected_dim = $expected.dimensions(); |
554 | |
555 | if actual_dim != expected_dim { |
556 | panic!( |
557 | "dimensions do not match. \ |
558 | actual: {:?}, expected: {:?}", |
559 | actual_dim, expected_dim |
560 | ) |
561 | } |
562 | |
563 | let diffs = pixel_diffs($actual, $expected); |
564 | |
565 | if !diffs.is_empty() { |
566 | let mut err = "".to_string(); |
567 | |
568 | let diff_messages = diffs |
569 | .iter() |
570 | .take(5) |
571 | .map(|d| format!("\n actual: {:?}, expected {:?} ", d.0, d.1)) |
572 | .collect::<Vec<_>>() |
573 | .join(""); |
574 | |
575 | err.push_str(&diff_messages); |
576 | panic!("pixels do not match. {:?}", err) |
577 | } |
578 | }}; |
579 | } |
580 | |
581 | #[test] |
582 | fn test_dither() { |
583 | let mut image = ImageBuffer::from_raw(2, 2, vec![127, 127, 127, 127]).unwrap(); |
584 | let cmap = BiLevel; |
585 | dither(&mut image, &cmap); |
586 | assert_eq!(&*image, &[0, 0xFF, 0xFF, 0]); |
587 | assert_eq!(index_colors(&image, &cmap).into_raw(), vec![0, 1, 1, 0]) |
588 | } |
589 | |
590 | #[test] |
591 | fn test_grayscale() { |
592 | let image: GrayImage = |
593 | ImageBuffer::from_raw(3, 2, vec![0u8, 1u8, 2u8, 10u8, 11u8, 12u8]).unwrap(); |
594 | |
595 | let expected: GrayImage = |
596 | ImageBuffer::from_raw(3, 2, vec![0u8, 1u8, 2u8, 10u8, 11u8, 12u8]).unwrap(); |
597 | |
598 | assert_pixels_eq!(&grayscale(&image), &expected); |
599 | } |
600 | |
601 | #[test] |
602 | fn test_invert() { |
603 | let mut image: GrayImage = |
604 | ImageBuffer::from_raw(3, 2, vec![0u8, 1u8, 2u8, 10u8, 11u8, 12u8]).unwrap(); |
605 | |
606 | let expected: GrayImage = |
607 | ImageBuffer::from_raw(3, 2, vec![255u8, 254u8, 253u8, 245u8, 244u8, 243u8]).unwrap(); |
608 | |
609 | invert(&mut image); |
610 | assert_pixels_eq!(&image, &expected); |
611 | } |
612 | #[test] |
613 | fn test_brighten() { |
614 | let image: GrayImage = |
615 | ImageBuffer::from_raw(3, 2, vec![0u8, 1u8, 2u8, 10u8, 11u8, 12u8]).unwrap(); |
616 | |
617 | let expected: GrayImage = |
618 | ImageBuffer::from_raw(3, 2, vec![10u8, 11u8, 12u8, 20u8, 21u8, 22u8]).unwrap(); |
619 | |
620 | assert_pixels_eq!(&brighten(&image, 10), &expected); |
621 | } |
622 | |
623 | #[test] |
624 | fn test_brighten_place() { |
625 | let mut image: GrayImage = |
626 | ImageBuffer::from_raw(3, 2, vec![0u8, 1u8, 2u8, 10u8, 11u8, 12u8]).unwrap(); |
627 | |
628 | let expected: GrayImage = |
629 | ImageBuffer::from_raw(3, 2, vec![10u8, 11u8, 12u8, 20u8, 21u8, 22u8]).unwrap(); |
630 | |
631 | brighten_in_place(&mut image, 10); |
632 | assert_pixels_eq!(&image, &expected); |
633 | } |
634 | |
635 | #[allow(clippy::type_complexity)] |
636 | fn pixel_diffs<I, J, P>(left: &I, right: &J) -> Vec<((u32, u32, P), (u32, u32, P))> |
637 | where |
638 | I: GenericImage<Pixel = P>, |
639 | J: GenericImage<Pixel = P>, |
640 | P: Pixel + Eq, |
641 | { |
642 | left.pixels() |
643 | .zip(right.pixels()) |
644 | .filter(|&(p, q)| p != q) |
645 | .collect::<Vec<_>>() |
646 | } |
647 | } |
648 |