Skip to main content

djvu_bitmap/
lib.rs

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