//! A minimal 1-bit image type. use std::fmt; /// A 1-bit image. `true` means black (a printed dot), `false` means white. /// /// Pixels are addressed as `(x, y)` with `(0, 0)` in the top-left corner, /// `x` growing to the right and `y` growing downwards (the same convention as /// PIL). #[derive(Clone, PartialEq, Eq, Hash)] pub struct Bitmap { width: u32, height: u32, /// Row-major pixels, `true` = black. pixels: Vec, } impl Bitmap { /// A new all-white bitmap. pub fn new(width: u32, height: u32) -> Self { Self::filled(width, height, false) } /// A new bitmap with every pixel set to `black`. pub fn filled(width: u32, height: u32, black: bool) -> Self { let len = width as usize * height as usize; Self { width, height, pixels: vec![black; len], } } /// Builds a bitmap by evaluating `f(x, y)` (returns `true` for black) for every pixel. pub fn from_fn(width: u32, height: u32, mut f: impl FnMut(u32, u32) -> bool) -> Self { let mut pixels = Vec::with_capacity(width as usize * height as usize); for y in 0..height { for x in 0..width { pixels.push(f(x, y)); } } Self { width, height, pixels, } } /// Width in pixels. pub fn width(&self) -> u32 { self.width } /// Height in pixels. pub fn height(&self) -> u32 { self.height } /// `(width, height)`. pub fn size(&self) -> (u32, u32) { (self.width, self.height) } fn index(&self, x: u32, y: u32) -> usize { assert!( x < self.width && y < self.height, "pixel ({x}, {y}) out of bounds for a {}x{} bitmap", self.width, self.height ); y as usize * self.width as usize + x as usize } /// Returns `true` if the pixel is black. /// /// # Panics /// If `(x, y)` is outside the bitmap. pub fn get(&self, x: u32, y: u32) -> bool { self.pixels[self.index(x, y)] } /// Sets a pixel (`true` = black). /// /// # Panics /// If `(x, y)` is outside the bitmap. pub fn set(&mut self, x: u32, y: u32, black: bool) { let i = self.index(x, y); self.pixels[i] = black; } /// Sets a pixel given signed coordinates; pixels outside the bitmap are ignored. pub fn set_clipped(&mut self, x: i64, y: i64, black: bool) { if x >= 0 && y >= 0 && x < i64::from(self.width) && y < i64::from(self.height) { self.set(x as u32, y as u32, black); } } /// Fills the rectangle `[x, x + w) x [y, y + h)` (clipped to the bitmap). pub fn fill_rect(&mut self, x: i64, y: i64, w: u32, h: u32, black: bool) { let x0 = x.max(0); let y0 = y.max(0); let x1 = (x + i64::from(w)).min(i64::from(self.width)); let y1 = (y + i64::from(h)).min(i64::from(self.height)); for yy in y0..y1 { for xx in x0..x1 { self.set(xx as u32, yy as u32, black); } } } /// Copies `src` into this bitmap with its top-left corner at `(x, y)`, /// overwriting both black and white pixels (like PIL `Image.paste` without /// a mask). Parts falling outside are clipped. pub fn paste(&mut self, src: &Bitmap, x: i64, y: i64) { for sy in 0..src.height { let dy = y + i64::from(sy); if dy < 0 || dy >= i64::from(self.height) { continue; } for sx in 0..src.width { let dx = x + i64::from(sx); if dx < 0 || dx >= i64::from(self.width) { continue; } self.set(dx as u32, dy as u32, src.get(sx, sy)); } } } /// Returns the sub-image `[x, x + w) x [y, y + h)`; pixels outside the /// source are white. pub fn crop(&self, x: i64, y: i64, w: u32, h: u32) -> Bitmap { Bitmap::from_fn(w, h, |cx, cy| { let sx = x + i64::from(cx); let sy = y + i64::from(cy); sx >= 0 && sy >= 0 && sx < i64::from(self.width) && sy < i64::from(self.height) && self.get(sx as u32, sy as u32) }) } /// Rotates 90 degrees counter-clockwise (PIL `Image.rotate(90, expand=True)` /// / `Transpose.ROTATE_90`). pub fn rotate90(&self) -> Bitmap { let (w, h) = (self.width, self.height); Bitmap::from_fn(h, w, |x, y| self.get(w - 1 - y, x)) } /// Rotates 180 degrees. pub fn rotate180(&self) -> Bitmap { let (w, h) = (self.width, self.height); Bitmap::from_fn(w, h, |x, y| self.get(w - 1 - x, h - 1 - y)) } /// Rotates 270 degrees counter-clockwise (= 90 degrees clockwise, PIL /// `Transpose.ROTATE_270`). pub fn rotate270(&self) -> Bitmap { let (w, h) = (self.width, self.height); Bitmap::from_fn(h, w, |x, y| self.get(y, h - 1 - x)) } /// Mirrors left-right (PIL `Transpose.FLIP_LEFT_RIGHT`). pub fn flip_horizontal(&self) -> Bitmap { let w = self.width; Bitmap::from_fn(w, self.height, |x, y| self.get(w - 1 - x, y)) } /// Nearest-neighbour resize exactly as Pillow does it for mode `"1"` /// images (Pillow forces `NEAREST` for 1-bit images whatever filter is /// requested): source coordinate = `floor(scale * 0.5 + i * scale)` with the /// position accumulated in `f64` like `ImagingScaleAffine`. pub fn resize_nearest(&self, width: u32, height: u32) -> Bitmap { if width == self.width && height == self.height { return self.clone(); } let xmap = nearest_map(self.width, width); let ymap = nearest_map(self.height, height); Bitmap::from_fn(width, height, |x, y| { match (xmap[x as usize], ymap[y as usize]) { (Some(sx), Some(sy)) => self.get(sx, sy), _ => false, } }) } /// Number of black pixels. pub fn count_black(&self) -> usize { self.pixels.iter().filter(|&&p| p).count() } /// Row `y` as booleans (`true` = black). pub fn row(&self, y: u32) -> &[bool] { let start = self.index(0, y); &self.pixels[start..start + self.width as usize] } /// All pixels in row-major order (`true` = black). pub fn pixels(&self) -> &[bool] { &self.pixels } } fn nearest_map(src: u32, dst: u32) -> Vec> { let scale = f64::from(src) / f64::from(dst); let mut pos = scale * 0.5; let mut out = Vec::with_capacity(dst as usize); for _ in 0..dst { // COORD(v) in Pillow: v < 0 ? -1 : (int)v let i = if pos < 0.0 { -1 } else { pos as i64 }; out.push(if i >= 0 && i < i64::from(src) { Some(i as u32) } else { None }); pos += scale; } out } impl fmt::Debug for Bitmap { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Bitmap") .field("width", &self.width) .field("height", &self.height) .field("black_pixels", &self.count_black()) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn rotations_match_pil_direction() { // single black pixel in the top-left corner of a 3x2 image let mut b = Bitmap::new(3, 2); b.set(0, 0, true); // PIL rotate(90) is counter-clockwise: top-left goes to bottom-left let r = b.rotate90(); assert_eq!(r.size(), (2, 3)); assert!(r.get(0, 2)); let r = b.rotate270(); assert!(r.get(1, 0)); let r = b.rotate180(); assert!(r.get(2, 1)); assert_eq!(b.rotate90().rotate90(), b.rotate180()); assert_eq!(b.rotate90().rotate270(), b); assert!(b.flip_horizontal().get(2, 0)); } #[test] fn paste_and_crop_clip() { let mut b = Bitmap::new(4, 4); let src = Bitmap::filled(3, 3, true); b.paste(&src, -1, 2); assert_eq!(b.count_black(), 4); assert!(b.get(0, 2) && b.get(1, 3)); let c = b.crop(-1, 2, 3, 3); assert_eq!(c.count_black(), 4); } #[test] fn resize_halves_like_pillow() { // PIL picks the odd source columns when halving (floor(2*i + 1)). let b = Bitmap::from_fn(4, 1, |x, _| x % 2 == 1); let r = b.resize_nearest(2, 1); assert!(r.get(0, 0) && r.get(1, 0)); } }