Skip to main content

djvu_rs/
bitmap.rs

1#[cfg(not(feature = "std"))]
2use alloc::{format, vec, vec::Vec};
3
4/// A 1-bit-per-pixel packed bitmap image.
5///
6/// Pixels are packed 8-per-byte, MSB first within each byte.
7/// Each row is padded to a byte boundary.
8/// Pixel value 1 = black, 0 = white (matching PBM convention).
9#[derive(Debug, Clone, Default, PartialEq, Eq)]
10pub struct Bitmap {
11    pub width: u32,
12    pub height: u32,
13    /// Packed pixel data, row-major. Row stride = `row_stride()` bytes.
14    pub data: Vec<u8>,
15}
16
17impl Bitmap {
18    /// Create a new all-white (0) bitmap.
19    pub fn new(width: u32, height: u32) -> Self {
20        let stride = Self::compute_row_stride(width);
21        Bitmap {
22            width,
23            height,
24            data: vec![0u8; stride * height as usize],
25        }
26    }
27
28    /// Bytes per row (each row padded to byte boundary).
29    #[inline]
30    pub fn row_stride(&self) -> usize {
31        Self::compute_row_stride(self.width)
32    }
33
34    fn compute_row_stride(width: u32) -> usize {
35        (width as usize).div_ceil(8)
36    }
37
38    /// Get pixel value at (x, y). Returns `true` if black (1).
39    /// Panics if out of bounds.
40    #[inline]
41    pub fn get(&self, x: u32, y: u32) -> bool {
42        debug_assert!(x < self.width && y < self.height);
43        let stride = self.row_stride();
44        let byte_idx = y as usize * stride + (x as usize / 8);
45        let bit_idx = 7 - (x % 8);
46        (self.data[byte_idx] >> bit_idx) & 1 == 1
47    }
48
49    /// Set pixel value at (x, y). `val = true` means black (1).
50    /// Panics if out of bounds.
51    pub fn set(&mut self, x: u32, y: u32, val: bool) {
52        debug_assert!(x < self.width && y < self.height);
53        let stride = self.row_stride();
54        let byte_idx = y as usize * stride + (x as usize / 8);
55        let bit_idx = 7 - (x % 8);
56        if val {
57            self.data[byte_idx] |= 1 << bit_idx;
58        } else {
59            self.data[byte_idx] &= !(1 << bit_idx);
60        }
61    }
62
63    /// OR a black pixel at (x, y). Used for JB2 blit compositing.
64    pub fn set_black(&mut self, x: u32, y: u32) {
65        let stride = self.row_stride();
66        let byte_idx = y as usize * stride + (x as usize / 8);
67        let bit_idx = 7 - (x % 8);
68        self.data[byte_idx] |= 1 << bit_idx;
69    }
70
71    /// Return a new bitmap with each black pixel expanded to its 4-connected
72    /// neighbors (1-pixel morphological dilation). Thickens every stroke by
73    /// ~2 pixels total, improving legibility at reduced display sizes.
74    pub fn dilate(&self) -> Bitmap {
75        let mut out = self.clone();
76        for y in 0..self.height {
77            for x in 0..self.width {
78                if self.get(x, y) {
79                    if x > 0 {
80                        out.set_black(x - 1, y);
81                    }
82                    if x + 1 < self.width {
83                        out.set_black(x + 1, y);
84                    }
85                    if y > 0 {
86                        out.set_black(x, y - 1);
87                    }
88                    if y + 1 < self.height {
89                        out.set_black(x, y + 1);
90                    }
91                }
92            }
93        }
94        out
95    }
96
97    /// Encode as PBM (binary, P4 format).
98    /// This is the format produced by `ddjvu -format=pbm`.
99    pub fn to_pbm(&self) -> Vec<u8> {
100        let header = format!("P4\n{} {}\n", self.width, self.height);
101        let stride = self.row_stride();
102        let mut out = Vec::with_capacity(header.len() + stride * self.height as usize);
103        out.extend_from_slice(header.as_bytes());
104        // PBM P4 packs MSB-first, rows padded to byte boundary — same as our storage
105        for y in 0..self.height as usize {
106            let row_start = y * stride;
107            out.extend_from_slice(&self.data[row_start..row_start + stride]);
108        }
109        out
110    }
111
112    /// Perform `passes` rounds of 4-connected morphological dilation using
113    /// packed bitwise operations and two pre-allocated ping-pong buffers.
114    /// Zero allocations after the initial setup; much faster than calling
115    /// `dilate()` N times for passes > 1.
116    ///
117    /// Pixel layout (MSB-first): bit 7 of each byte = leftmost pixel in that
118    /// group of 8. Horizontal dilation uses `byte >> 1` (right) and
119    /// `byte << 1` (left) with cross-byte carries; vertical dilation ORs
120    /// each row into its neighbours.
121    pub fn dilate_n(self, passes: u32) -> Self {
122        if passes == 0 {
123            return self;
124        }
125        let width = self.width;
126        let height = self.height;
127        let stride = self.row_stride();
128        let h = height as usize;
129
130        let mut src = self.data;
131        let mut dst = vec![0u8; stride * h];
132
133        for _ in 0..passes {
134            for b in dst.iter_mut() {
135                *b = 0;
136            }
137
138            for y in 0..h {
139                let row = y * stride;
140
141                for i in 0..stride {
142                    let v = src[row + i];
143                    if v == 0 {
144                        continue;
145                    }
146
147                    dst[row + i] |= v; // original
148                    dst[row + i] |= v >> 1; // right neighbours (x+1) within byte
149                    dst[row + i] |= v << 1; // left neighbours (x-1) within byte
150
151                    // Carry: rightmost pixel of byte i → leftmost of byte i+1
152                    if i + 1 < stride {
153                        dst[row + i + 1] |= (v & 0x01) << 7;
154                    }
155                    // Carry: leftmost pixel of byte i → rightmost of byte i-1
156                    if i > 0 {
157                        dst[row + i - 1] |= (v & 0x80) >> 7;
158                    }
159
160                    // Vertical: down (y+1)
161                    if y + 1 < h {
162                        dst[(y + 1) * stride + i] |= v;
163                    }
164                    // Vertical: up (y-1)
165                    if y > 0 {
166                        dst[(y - 1) * stride + i] |= v;
167                    }
168                }
169            }
170
171            core::mem::swap(&mut src, &mut dst);
172        }
173
174        Bitmap {
175            width,
176            height,
177            data: src,
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn new_bitmap_is_all_white() {
188        let bm = Bitmap::new(10, 5);
189        for y in 0..5 {
190            for x in 0..10 {
191                assert!(!bm.get(x, y));
192            }
193        }
194    }
195
196    #[test]
197    fn set_and_get_roundtrip() {
198        let mut bm = Bitmap::new(16, 8);
199        bm.set(0, 0, true);
200        bm.set(15, 0, true);
201        bm.set(7, 3, true);
202        bm.set(0, 7, true);
203        bm.set(15, 7, true);
204
205        assert!(bm.get(0, 0));
206        assert!(bm.get(15, 0));
207        assert!(bm.get(7, 3));
208        assert!(bm.get(0, 7));
209        assert!(bm.get(15, 7));
210
211        // Adjacent pixels should be unaffected
212        assert!(!bm.get(1, 0));
213        assert!(!bm.get(14, 0));
214        assert!(!bm.get(6, 3));
215        assert!(!bm.get(8, 3));
216    }
217
218    #[test]
219    fn set_and_clear() {
220        let mut bm = Bitmap::new(8, 1);
221        bm.set(3, 0, true);
222        assert!(bm.get(3, 0));
223        bm.set(3, 0, false);
224        assert!(!bm.get(3, 0));
225    }
226
227    #[test]
228    fn non_byte_aligned_width() {
229        // Width=10, so stride=2 bytes (16 bits, 6 padding bits)
230        let mut bm = Bitmap::new(10, 2);
231        assert_eq!(bm.row_stride(), 2);
232        bm.set(9, 0, true);
233        bm.set(9, 1, true);
234        assert!(bm.get(9, 0));
235        assert!(bm.get(9, 1));
236    }
237
238    #[test]
239    fn to_pbm_format() {
240        let mut bm = Bitmap::new(8, 2);
241        // Row 0: pixel 0 and 7 black → 0b10000001 = 0x81
242        bm.set(0, 0, true);
243        bm.set(7, 0, true);
244        // Row 1: all black → 0xFF
245        for x in 0..8 {
246            bm.set(x, 1, true);
247        }
248        let pbm = bm.to_pbm();
249        let hdr = b"P4\n8 2\n";
250        assert_eq!(&pbm[..hdr.len()], hdr);
251        assert_eq!(pbm[hdr.len()], 0x81);
252        assert_eq!(pbm[hdr.len() + 1], 0xFF);
253        assert_eq!(pbm.len(), hdr.len() + 2);
254    }
255
256    #[test]
257    fn to_pbm_non_byte_aligned() {
258        // Width=3, stride=1 byte, only top 3 bits used
259        let mut bm = Bitmap::new(3, 1);
260        bm.set(0, 0, true);
261        bm.set(2, 0, true);
262        let pbm = bm.to_pbm();
263        let hdr = b"P4\n3 1\n";
264        assert_eq!(&pbm[..hdr.len()], hdr);
265        // Bits: 1 0 1 00000 = 0b10100000 = 0xA0
266        assert_eq!(pbm[hdr.len()], 0xA0);
267    }
268
269    // ── dilate_n tests ───────────────────────────────────────────────────────
270
271    /// Helper: collect all set pixels as (x,y) pairs, sorted.
272    fn set_pixels(bm: &Bitmap) -> Vec<(u32, u32)> {
273        let mut out = Vec::new();
274        for y in 0..bm.height {
275            for x in 0..bm.width {
276                if bm.get(x, y) {
277                    out.push((x, y));
278                }
279            }
280        }
281        out
282    }
283
284    #[test]
285    fn dilate_n_zero_passes_is_identity() {
286        let mut bm = Bitmap::new(8, 8);
287        bm.set(3, 3, true);
288        let orig = set_pixels(&bm);
289        let result = set_pixels(&bm.clone().dilate_n(0));
290        assert_eq!(orig, result);
291    }
292
293    #[test]
294    fn dilate_n_one_pass_matches_dilate() {
295        let mut bm = Bitmap::new(16, 16);
296        bm.set(7, 7, true);
297        bm.set(0, 0, true);
298        bm.set(15, 15, true);
299        let expected = set_pixels(&bm.dilate());
300        let got = set_pixels(&bm.clone().dilate_n(1));
301        assert_eq!(expected, got);
302    }
303
304    #[test]
305    fn dilate_n_two_passes_matches_dilate_twice() {
306        let mut bm = Bitmap::new(20, 20);
307        bm.set(10, 10, true);
308        bm.set(1, 1, true);
309        let expected = set_pixels(&bm.dilate().dilate());
310        let got = set_pixels(&bm.clone().dilate_n(2));
311        assert_eq!(expected, got);
312    }
313
314    #[test]
315    fn dilate_n_three_passes_matches_dilate_three_times() {
316        let mut bm = Bitmap::new(20, 20);
317        bm.set(10, 10, true);
318        let expected = set_pixels(&bm.dilate().dilate().dilate());
319        let got = set_pixels(&bm.clone().dilate_n(3));
320        assert_eq!(expected, got);
321    }
322
323    #[test]
324    fn dilate_n_cross_byte_boundary() {
325        // Pixel at position 7 (last in first byte) — its x+1 neighbour is pixel 8
326        let mut bm = Bitmap::new(16, 1);
327        bm.set(7, 0, true);
328        let result = bm.clone().dilate_n(1);
329        assert!(result.get(6, 0), "x-1 neighbour");
330        assert!(result.get(7, 0), "original");
331        assert!(result.get(8, 0), "x+1 neighbour (cross-byte)");
332        // Pixel at position 8 (first in second byte) — its x-1 neighbour is pixel 7
333        let mut bm2 = Bitmap::new(16, 1);
334        bm2.set(8, 0, true);
335        let result2 = bm2.clone().dilate_n(1);
336        assert!(result2.get(7, 0), "x-1 neighbour (cross-byte)");
337        assert!(result2.get(8, 0), "original");
338        assert!(result2.get(9, 0), "x+1 neighbour");
339    }
340
341    #[test]
342    fn dilate_n_boundary_pixels_dont_wrap() {
343        // Top-left corner pixel — should only expand right and down
344        let mut bm = Bitmap::new(16, 16);
345        bm.set(0, 0, true);
346        let result = bm.dilate_n(1);
347        assert!(result.get(0, 0));
348        assert!(result.get(1, 0));
349        assert!(result.get(0, 1));
350        // Nothing in last row/col from this pixel
351        assert!(!result.get(15, 15));
352    }
353}