1 | //! Functions for altering and converting the color of pixelbufs |
---|---|
2 | |
3 | use num_traits::NumCast; |
4 | |
5 | use crate::color::{FromColor, IntoColor, Luma, LumaA}; |
6 | use crate::image::{GenericImage, GenericImageView}; |
7 | use crate::traits::{Pixel, Primitive}; |
8 | use crate::utils::clamp; |
9 | use crate::ImageBuffer; |
10 | |
11 | type Subpixel<I> = <<I as GenericImageView>::Pixel as Pixel>::Subpixel; |
12 | |
13 | /// Convert the supplied image to grayscale. Alpha channel is discarded. |
14 | pub fn grayscale<I: GenericImageView>( |
15 | image: &I, |
16 | ) -> ImageBuffer<Luma<Subpixel<I>>, Vec<Subpixel<I>>> { |
17 | grayscale_with_type(image) |
18 | } |
19 | |
20 | /// Convert the supplied image to grayscale. Alpha channel is preserved. |
21 | pub fn grayscale_alpha<I: GenericImageView>( |
22 | image: &I, |
23 | ) -> ImageBuffer<LumaA<Subpixel<I>>, Vec<Subpixel<I>>> { |
24 | grayscale_with_type_alpha(image) |
25 | } |
26 | |
27 | /// Convert the supplied image to a grayscale image with the specified pixel type. Alpha channel is discarded. |
28 | pub fn grayscale_with_type<NewPixel, I: GenericImageView>( |
29 | image: &I, |
30 | ) -> ImageBuffer<NewPixel, Vec<NewPixel::Subpixel>> |
31 | where |
32 | NewPixel: Pixel + FromColor<Luma<Subpixel<I>>>, |
33 | { |
34 | let (width: u32, height: u32) = image.dimensions(); |
35 | let mut out: ImageBuffer |
36 | |
37 | for (x: u32, y: u32, pixel: ::Pixel) in image.pixels() { |
38 | let grayscale: Luma<<::Pixel as Pixel>::Subpixel> = pixel.to_luma(); |
39 | let new_pixel: NewPixel = grayscale.into_color(); // no-op for luma->luma |
40 | |
41 | out.put_pixel(x, y, new_pixel); |
42 | } |
43 | |
44 | out |
45 | } |
46 | |
47 | /// Convert the supplied image to a grayscale image with the specified pixel type. Alpha channel is preserved. |
48 | pub fn grayscale_with_type_alpha<NewPixel, I: GenericImageView>( |
49 | image: &I, |
50 | ) -> ImageBuffer<NewPixel, Vec<NewPixel::Subpixel>> |
51 | where |
52 | NewPixel: Pixel + FromColor<LumaA<Subpixel<I>>>, |
53 | { |
54 | let (width: u32, height: u32) = image.dimensions(); |
55 | let mut out: ImageBuffer |
56 | |
57 | for (x: u32, y: u32, pixel: ::Pixel) in image.pixels() { |
58 | let grayscale: LumaA<<::Pixel as Pixel>::Subpixel> = pixel.to_luma_alpha(); |
59 | let new_pixel: NewPixel = grayscale.into_color(); // no-op for luma->luma |
60 | |
61 | out.put_pixel(x, y, new_pixel); |
62 | } |
63 | |
64 | out |
65 | } |
66 | |
67 | /// Invert each pixel within the supplied image. |
68 | /// This function operates in place. |
69 | pub fn invert<I: GenericImage>(image: &mut I) { |
70 | // TODO find a way to use pixels? |
71 | let (width: u32, height: u32) = image.dimensions(); |
72 | |
73 | for y: u32 in 0..height { |
74 | for x: u32 in 0..width { |
75 | let mut p: ::Pixel = image.get_pixel(x, y); |
76 | p.invert(); |
77 | |
78 | image.put_pixel(x, y, pixel:p); |
79 | } |
80 | } |
81 | } |
82 | |
83 | /// Adjust the contrast of the supplied image. |
84 | /// ```contrast``` is the amount to adjust the contrast by. |
85 | /// Negative values decrease the contrast and positive values increase the contrast. |
86 | /// |
87 | /// *[See also `contrast_in_place`.][contrast_in_place]* |
88 | pub fn contrast<I, P, S>(image: &I, contrast: f32) -> ImageBuffer<P, Vec<S>> |
89 | where |
90 | I: GenericImageView<Pixel = P>, |
91 | P: Pixel<Subpixel = S> + 'static, |
92 | S: Primitive + 'static, |
93 | { |
94 | let (width: u32, height: u32) = image.dimensions(); |
95 | let mut out: ImageBuffer > = ImageBuffer::new(width, height); |
96 | |
97 | let max: S = S::DEFAULT_MAX_VALUE; |
98 | let max: f32 = NumCast::from(max).unwrap(); |
99 | |
100 | let percent: f32 = ((100.0 + contrast) / 100.0).powi(2); |
101 | |
102 | for (x: u32, y: u32, pixel: P) in image.pixels() { |
103 | let f: P = pixel.map(|b: S| { |
104 | let c: f32 = NumCast::from(b).unwrap(); |
105 | |
106 | let d: f32 = ((c / max - 0.5) * percent + 0.5) * max; |
107 | let e: f32 = clamp(a:d, min:0.0, max); |
108 | |
109 | NumCast::from(e).unwrap() |
110 | }); |
111 | out.put_pixel(x, y, pixel:f); |
112 | } |
113 | |
114 | out |
115 | } |
116 | |
117 | /// Adjust the contrast of the supplied image in place. |
118 | /// ```contrast``` is the amount to adjust the contrast by. |
119 | /// Negative values decrease the contrast and positive values increase the contrast. |
120 | /// |
121 | /// *[See also `contrast`.][contrast]* |
122 | pub fn contrast_in_place<I>(image: &mut I, contrast: f32) |
123 | where |
124 | I: GenericImage, |
125 | { |
126 | let (width: u32, height: u32) = image.dimensions(); |
127 | |
128 | let max: <::Pixel as Pixel>::Subpixel = <I::Pixel as Pixel>::Subpixel::DEFAULT_MAX_VALUE; |
129 | let max: f32 = NumCast::from(max).unwrap(); |
130 | |
131 | let percent: f32 = ((100.0 + contrast) / 100.0).powi(2); |
132 | |
133 | // TODO find a way to use pixels? |
134 | for y: u32 in 0..height { |
135 | for x: u32 in 0..width { |
136 | let f: ::Pixel = image.get_pixel(x, y).map(|b: <::Pixel as Pixel>::Subpixel| { |
137 | let c: f32 = NumCast::from(b).unwrap(); |
138 | |
139 | let d: f32 = ((c / max - 0.5) * percent + 0.5) * max; |
140 | let e: f32 = clamp(a:d, min:0.0, max); |
141 | |
142 | NumCast::from(e).unwrap() |
143 | }); |
144 | |
145 | image.put_pixel(x, y, pixel:f); |
146 | } |
147 | } |
148 | } |
149 | |
150 | /// Brighten the supplied image. |
151 | /// ```value``` is the amount to brighten each pixel by. |
152 | /// Negative values decrease the brightness and positive values increase it. |
153 | /// |
154 | /// *[See also `brighten_in_place`.][brighten_in_place]* |
155 | pub fn brighten<I, P, S>(image: &I, value: i32) -> ImageBuffer<P, Vec<S>> |
156 | where |
157 | I: GenericImageView<Pixel = P>, |
158 | P: Pixel<Subpixel = S> + 'static, |
159 | S: Primitive + 'static, |
160 | { |
161 | let (width: u32, height: u32) = image.dimensions(); |
162 | let mut out: ImageBuffer > = ImageBuffer::new(width, height); |
163 | |
164 | let max: S = S::DEFAULT_MAX_VALUE; |
165 | let max: i32 = NumCast::from(max).unwrap(); |
166 | |
167 | for (x: u32, y: u32, pixel: P) in image.pixels() { |
168 | let e: P = pixel.map_with_alpha( |
169 | |b| { |
170 | let c: i32 = NumCast::from(b).unwrap(); |
171 | let d = clamp(c + value, 0, max); |
172 | |
173 | NumCast::from(d).unwrap() |
174 | }, |
175 | |alpha: S| alpha, |
176 | ); |
177 | out.put_pixel(x, y, pixel:e); |
178 | } |
179 | |
180 | out |
181 | } |
182 | |
183 | /// Brighten the supplied image in place. |
184 | /// ```value``` is the amount to brighten each pixel by. |
185 | /// Negative values decrease the brightness and positive values increase it. |
186 | /// |
187 | /// *[See also `brighten`.][brighten]* |
188 | pub fn brighten_in_place<I>(image: &mut I, value: i32) |
189 | where |
190 | I: GenericImage, |
191 | { |
192 | let (width: u32, height: u32) = image.dimensions(); |
193 | |
194 | let max: <::Pixel as Pixel>::Subpixel = <I::Pixel as Pixel>::Subpixel::DEFAULT_MAX_VALUE; |
195 | let max: i32 = NumCast::from(max).unwrap(); // TODO what does this do for f32? clamp at 1?? |
196 | |
197 | // TODO find a way to use pixels? |
198 | for y: u32 in 0..height { |
199 | for x: u32 in 0..width { |
200 | let e: ::Pixel = image.get_pixel(x, y).map_with_alpha( |
201 | |b| { |
202 | let c: i32 = NumCast::from(b).unwrap(); |
203 | let d = clamp(c + value, 0, max); |
204 | |
205 | NumCast::from(d).unwrap() |
206 | }, |
207 | |alpha: <::Pixel as Pixel>::Subpixel| alpha, |
208 | ); |
209 | |
210 | image.put_pixel(x, y, pixel:e); |
211 | } |
212 | } |
213 | } |
214 | |
215 | /// Hue rotate the supplied image. |
216 | /// `value` is the degrees to rotate each pixel by. |
217 | /// 0 and 360 do nothing, the rest rotates by the given degree value. |
218 | /// just like the css webkit filter hue-rotate(180) |
219 | /// |
220 | /// *[See also `huerotate_in_place`.][huerotate_in_place]* |
221 | pub fn huerotate<I, P, S>(image: &I, value: i32) -> ImageBuffer<P, Vec<S>> |
222 | where |
223 | I: GenericImageView<Pixel = P>, |
224 | P: Pixel<Subpixel = S> + 'static, |
225 | S: Primitive + 'static, |
226 | { |
227 | let (width, height) = image.dimensions(); |
228 | let mut out = ImageBuffer::new(width, height); |
229 | |
230 | let angle: f64 = NumCast::from(value).unwrap(); |
231 | |
232 | let cosv = angle.to_radians().cos(); |
233 | let sinv = angle.to_radians().sin(); |
234 | let matrix: [f64; 9] = [ |
235 | // Reds |
236 | 0.213 + cosv * 0.787 - sinv * 0.213, |
237 | 0.715 - cosv * 0.715 - sinv * 0.715, |
238 | 0.072 - cosv * 0.072 + sinv * 0.928, |
239 | // Greens |
240 | 0.213 - cosv * 0.213 + sinv * 0.143, |
241 | 0.715 + cosv * 0.285 + sinv * 0.140, |
242 | 0.072 - cosv * 0.072 - sinv * 0.283, |
243 | // Blues |
244 | 0.213 - cosv * 0.213 - sinv * 0.787, |
245 | 0.715 - cosv * 0.715 + sinv * 0.715, |
246 | 0.072 + cosv * 0.928 + sinv * 0.072, |
247 | ]; |
248 | for (x, y, pixel) in out.enumerate_pixels_mut() { |
249 | let p = image.get_pixel(x, y); |
250 | |
251 | #[allow(deprecated)] |
252 | let (k1, k2, k3, k4) = p.channels4(); |
253 | let vec: (f64, f64, f64, f64) = ( |
254 | NumCast::from(k1).unwrap(), |
255 | NumCast::from(k2).unwrap(), |
256 | NumCast::from(k3).unwrap(), |
257 | NumCast::from(k4).unwrap(), |
258 | ); |
259 | |
260 | let r = vec.0; |
261 | let g = vec.1; |
262 | let b = vec.2; |
263 | |
264 | let new_r = matrix[0] * r + matrix[1] * g + matrix[2] * b; |
265 | let new_g = matrix[3] * r + matrix[4] * g + matrix[5] * b; |
266 | let new_b = matrix[6] * r + matrix[7] * g + matrix[8] * b; |
267 | let max = 255f64; |
268 | |
269 | #[allow(deprecated)] |
270 | let outpixel = Pixel::from_channels( |
271 | NumCast::from(clamp(new_r, 0.0, max)).unwrap(), |
272 | NumCast::from(clamp(new_g, 0.0, max)).unwrap(), |
273 | NumCast::from(clamp(new_b, 0.0, max)).unwrap(), |
274 | NumCast::from(clamp(vec.3, 0.0, max)).unwrap(), |
275 | ); |
276 | *pixel = outpixel; |
277 | } |
278 | out |
279 | } |
280 | |
281 | /// Hue rotate the supplied image in place. |
282 | /// |
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.to_radians().cos(); |
297 | let sinv = angle.to_radians().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 | #[cfg(feature = "color_quant")] |
439 | impl ColorMap for color_quant::NeuQuant { |
440 | type Color = crate::color::Rgba<u8>; |
441 | |
442 | #[inline(always)] |
443 | fn index_of(&self, color: &Self::Color) -> usize { |
444 | self.index_of(color.channels()) |
445 | } |
446 | |
447 | #[inline(always)] |
448 | fn lookup(&self, idx: usize) -> Option<Self::Color> { |
449 | self.lookup(idx).map(|p| p.into()) |
450 | } |
451 | |
452 | /// Indicate NeuQuant implements `lookup`. |
453 | fn has_lookup(&self) -> bool { |
454 | true |
455 | } |
456 | |
457 | #[inline(always)] |
458 | fn map_color(&self, color: &mut Self::Color) { |
459 | self.map_pixel(color.channels_mut()) |
460 | } |
461 | } |
462 | |
463 | /// Floyd-Steinberg error diffusion |
464 | fn diffuse_err<P: Pixel<Subpixel = u8>>(pixel: &mut P, error: [i16; 3], factor: i16) { |
465 | for (e: &i16, c: &mut u8) in error.iter().zip(pixel.channels_mut().iter_mut()) { |
466 | *c = match <i16 as From<_>>::from(*c) + e * factor / 16 { |
467 | val: i16 if val < 0 => 0, |
468 | val: i16 if val > 0xFF => 0xFF, |
469 | val: i16 => val as u8, |
470 | } |
471 | } |
472 | } |
473 | |
474 | macro_rules! do_dithering( |
475 | ($map:expr, $image:expr, $err:expr, $x:expr, $y:expr) => ( |
476 | { |
477 | let old_pixel = $image[($x, $y)]; |
478 | let new_pixel = $image.get_pixel_mut($x, $y); |
479 | $map.map_color(new_pixel); |
480 | for ((e, &old), &new) in $err.iter_mut() |
481 | .zip(old_pixel.channels().iter()) |
482 | .zip(new_pixel.channels().iter()) |
483 | { |
484 | *e = <i16 as From<_>>::from(old) - <i16 as From<_>>::from(new) |
485 | } |
486 | } |
487 | ) |
488 | ); |
489 | |
490 | /// Reduces the colors of the image using the supplied `color_map` while applying |
491 | /// Floyd-Steinberg dithering to improve the visual conception |
492 | pub fn dither<Pix, Map>(image: &mut ImageBuffer<Pix, Vec<u8>>, color_map: &Map) |
493 | where |
494 | Map: ColorMap<Color = Pix> + ?Sized, |
495 | Pix: Pixel<Subpixel = u8> + 'static, |
496 | { |
497 | let (width, height) = image.dimensions(); |
498 | let mut err: [i16; 3] = [0; 3]; |
499 | for y in 0..height - 1 { |
500 | let x = 0; |
501 | do_dithering!(color_map, image, err, x, y); |
502 | diffuse_err(image.get_pixel_mut(x + 1, y), err, 7); |
503 | diffuse_err(image.get_pixel_mut(x, y + 1), err, 5); |
504 | diffuse_err(image.get_pixel_mut(x + 1, y + 1), err, 1); |
505 | for x in 1..width - 1 { |
506 | do_dithering!(color_map, image, err, x, y); |
507 | diffuse_err(image.get_pixel_mut(x + 1, y), err, 7); |
508 | diffuse_err(image.get_pixel_mut(x - 1, y + 1), err, 3); |
509 | diffuse_err(image.get_pixel_mut(x, y + 1), err, 5); |
510 | diffuse_err(image.get_pixel_mut(x + 1, y + 1), err, 1); |
511 | } |
512 | let x = width - 1; |
513 | do_dithering!(color_map, image, err, x, y); |
514 | diffuse_err(image.get_pixel_mut(x - 1, y + 1), err, 3); |
515 | diffuse_err(image.get_pixel_mut(x, y + 1), err, 5); |
516 | } |
517 | let y = height - 1; |
518 | let x = 0; |
519 | do_dithering!(color_map, image, err, x, y); |
520 | diffuse_err(image.get_pixel_mut(x + 1, y), err, 7); |
521 | for x in 1..width - 1 { |
522 | do_dithering!(color_map, image, err, x, y); |
523 | diffuse_err(image.get_pixel_mut(x + 1, y), err, 7); |
524 | } |
525 | let x = width - 1; |
526 | do_dithering!(color_map, image, err, x, y); |
527 | } |
528 | |
529 | /// Reduces the colors using the supplied `color_map` and returns an image of the indices |
530 | pub fn index_colors<Pix, Map>( |
531 | image: &ImageBuffer<Pix, Vec<u8>>, |
532 | color_map: &Map, |
533 | ) -> ImageBuffer<Luma<u8>, Vec<u8>> |
534 | where |
535 | Map: ColorMap<Color = Pix> + ?Sized, |
536 | Pix: Pixel<Subpixel = u8> + 'static, |
537 | { |
538 | let mut indices: ImageBuffer |
539 | for (pixel: &Pix, idx: &mut Luma |
540 | *idx = Luma([color_map.index_of(color:pixel) as u8]); |
541 | } |
542 | indices |
543 | } |
544 | |
545 | #[cfg(test)] |
546 | mod test { |
547 | |
548 | use super::*; |
549 | use crate::GrayImage; |
550 | |
551 | macro_rules! assert_pixels_eq { |
552 | ($actual:expr, $expected:expr) => {{ |
553 | let actual_dim = $actual.dimensions(); |
554 | let expected_dim = $expected.dimensions(); |
555 | |
556 | if actual_dim != expected_dim { |
557 | panic!( |
558 | "dimensions do not match. \ |
559 | actual: {:?}, expected: {:?}", |
560 | actual_dim, expected_dim |
561 | ) |
562 | } |
563 | |
564 | let diffs = pixel_diffs($actual, $expected); |
565 | |
566 | if !diffs.is_empty() { |
567 | let mut err = "".to_string(); |
568 | |
569 | let diff_messages = diffs |
570 | .iter() |
571 | .take(5) |
572 | .map(|d| format!("\n actual: {:?}, expected {:?} ", d.0, d.1)) |
573 | .collect::<Vec<_>>() |
574 | .join(""); |
575 | |
576 | err.push_str(&diff_messages); |
577 | panic!("pixels do not match. {:?}", err) |
578 | } |
579 | }}; |
580 | } |
581 | |
582 | #[test] |
583 | fn test_dither() { |
584 | let mut image = ImageBuffer::from_raw(2, 2, vec![127, 127, 127, 127]).unwrap(); |
585 | let cmap = BiLevel; |
586 | dither(&mut image, &cmap); |
587 | assert_eq!(&*image, &[0, 0xFF, 0xFF, 0]); |
588 | assert_eq!(index_colors(&image, &cmap).into_raw(), vec![0, 1, 1, 0]); |
589 | } |
590 | |
591 | #[test] |
592 | fn test_grayscale() { |
593 | let image: GrayImage = |
594 | ImageBuffer::from_raw(3, 2, vec![0u8, 1u8, 2u8, 10u8, 11u8, 12u8]).unwrap(); |
595 | |
596 | let expected: GrayImage = |
597 | ImageBuffer::from_raw(3, 2, vec![0u8, 1u8, 2u8, 10u8, 11u8, 12u8]).unwrap(); |
598 | |
599 | assert_pixels_eq!(&grayscale(&image), &expected); |
600 | } |
601 | |
602 | #[test] |
603 | fn test_invert() { |
604 | let mut image: GrayImage = |
605 | ImageBuffer::from_raw(3, 2, vec![0u8, 1u8, 2u8, 10u8, 11u8, 12u8]).unwrap(); |
606 | |
607 | let expected: GrayImage = |
608 | ImageBuffer::from_raw(3, 2, vec![255u8, 254u8, 253u8, 245u8, 244u8, 243u8]).unwrap(); |
609 | |
610 | invert(&mut image); |
611 | assert_pixels_eq!(&image, &expected); |
612 | } |
613 | #[test] |
614 | fn test_brighten() { |
615 | let image: GrayImage = |
616 | ImageBuffer::from_raw(3, 2, vec![0u8, 1u8, 2u8, 10u8, 11u8, 12u8]).unwrap(); |
617 | |
618 | let expected: GrayImage = |
619 | ImageBuffer::from_raw(3, 2, vec![10u8, 11u8, 12u8, 20u8, 21u8, 22u8]).unwrap(); |
620 | |
621 | assert_pixels_eq!(&brighten(&image, 10), &expected); |
622 | } |
623 | |
624 | #[test] |
625 | fn test_brighten_place() { |
626 | let mut image: GrayImage = |
627 | ImageBuffer::from_raw(3, 2, vec![0u8, 1u8, 2u8, 10u8, 11u8, 12u8]).unwrap(); |
628 | |
629 | let expected: GrayImage = |
630 | ImageBuffer::from_raw(3, 2, vec![10u8, 11u8, 12u8, 20u8, 21u8, 22u8]).unwrap(); |
631 | |
632 | brighten_in_place(&mut image, 10); |
633 | assert_pixels_eq!(&image, &expected); |
634 | } |
635 | |
636 | #[allow(clippy::type_complexity)] |
637 | fn pixel_diffs<I, J, P>(left: &I, right: &J) -> Vec<((u32, u32, P), (u32, u32, P))> |
638 | where |
639 | I: GenericImage<Pixel = P>, |
640 | J: GenericImage<Pixel = P>, |
641 | P: Pixel + Eq, |
642 | { |
643 | left.pixels() |
644 | .zip(right.pixels()) |
645 | .filter(|&(p, q)| p != q) |
646 | .collect::<Vec<_>>() |
647 | } |
648 | } |
649 |