| 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 |
