Skip to main content

djvu_pixmap/
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/// An RGBA pixel image, 4 bytes per pixel.
13///
14/// Row-major, top-to-bottom. Alpha is always 255 for DjVu pages.
15#[derive(Debug, Clone, Default, PartialEq, Eq)]
16pub struct Pixmap {
17    pub width: u32,
18    pub height: u32,
19    /// RGBA pixel data, row-major. Length = width * height * 4.
20    pub data: Vec<u8>,
21}
22
23impl AsRef<[u8]> for Pixmap {
24    fn as_ref(&self) -> &[u8] {
25        &self.data
26    }
27}
28
29impl Pixmap {
30    /// Maximum pixels per pixmap (~64 megapixels = ~256 MB RGBA).
31    /// Anything beyond this is a runaway DPI — return an empty pixmap
32    /// so the caller gets a harmless blank instead of OOM or overflow.
33    const MAX_PIXELS: usize = 64 * 1024 * 1024;
34
35    /// Create a new pixmap filled with the given RGBA color.
36    ///
37    /// Returns an empty 0×0 pixmap if `width * height` would exceed
38    /// 64 MiB pixels or overflow `usize`, preventing OOM from extreme
39    /// DPI values.
40    pub fn new(width: u32, height: u32, r: u8, g: u8, b: u8, a: u8) -> Self {
41        let Some(pixel_count) = (width as usize).checked_mul(height as usize) else {
42            return Self::default();
43        };
44        if pixel_count > Self::MAX_PIXELS {
45            return Self::default();
46        }
47        // Fast path: all channels equal — single memset.
48        if r == g && g == b && b == a {
49            return Pixmap {
50                width,
51                height,
52                data: vec![r; pixel_count * 4],
53            };
54        }
55        // General path: repeat the 4-byte RGBA pattern `pixel_count` times.
56        // `slice::repeat` uses a doubling memcpy strategy and is highly optimised.
57        let data = [r, g, b, a].repeat(pixel_count);
58        Pixmap {
59            width,
60            height,
61            data,
62        }
63    }
64
65    /// Create a white opaque pixmap.
66    pub fn white(width: u32, height: u32) -> Self {
67        Self::new(width, height, 255, 255, 255, 255)
68    }
69
70    /// Set pixel at (x, y) to an RGB value (alpha = 255).
71    /// Silently ignores out-of-bounds writes (e.g. on an empty overflow pixmap).
72    #[inline]
73    pub fn set_rgb(&mut self, x: u32, y: u32, r: u8, g: u8, b: u8) {
74        let idx = (y as usize * self.width as usize + x as usize) * 4;
75        if let Some(pixel) = self.data.get_mut(idx..idx + 4) {
76            pixel[0] = r;
77            pixel[1] = g;
78            pixel[2] = b;
79            pixel[3] = 255;
80        }
81    }
82
83    /// Get the 4 RGBA bytes at pixel (x, y), or `None` if out of bounds.
84    #[inline]
85    pub fn get_pixel(&self, x: u32, y: u32) -> Option<&[u8]> {
86        if x >= self.width || y >= self.height {
87            return None;
88        }
89        let idx = (y as usize * self.width as usize + x as usize) * 4;
90        self.data.get(idx..idx + 4)
91    }
92
93    /// Get RGB at (x, y). Returns (0, 0, 0) for out-of-bounds reads.
94    #[inline]
95    pub fn get_rgb(&self, x: u32, y: u32) -> (u8, u8, u8) {
96        let idx = (y as usize * self.width as usize + x as usize) * 4;
97        if let Some(pixel) = self.data.get(idx..idx + 4) {
98            (pixel[0], pixel[1], pixel[2])
99        } else {
100            (0, 0, 0)
101        }
102    }
103
104    /// Extract RGB pixel data (3 bytes per pixel), discarding alpha.
105    pub fn to_rgb(&self) -> Vec<u8> {
106        let pixel_count = self.data.len() / 4;
107        let mut out = Vec::with_capacity(pixel_count * 3);
108        for chunk in self.data.chunks_exact(4) {
109            out.push(chunk[0]);
110            out.push(chunk[1]);
111            out.push(chunk[2]);
112        }
113        out
114    }
115
116    /// Encode as PPM (binary, P6 format).
117    /// This is the format produced by `ddjvu -format=ppm`.
118    /// Discards alpha channel.
119    pub fn to_ppm(&self) -> Vec<u8> {
120        let header = format!("P6\n{} {}\n255\n", self.width, self.height);
121        let pixel_count = self.data.len() / 4;
122        let mut out = Vec::with_capacity(header.len() + pixel_count * 3);
123        out.extend_from_slice(header.as_bytes());
124        for chunk in self.data.chunks_exact(4) {
125            out.push(chunk[0]); // R
126            out.push(chunk[1]); // G
127            out.push(chunk[2]); // B
128        }
129        out
130    }
131
132    /// Rotate this pixmap 90° clockwise.
133    pub fn rotate_cw90(&self) -> Self {
134        let (w, h) = (self.width, self.height);
135        let mut dst = vec![0u8; (w * h * 4) as usize];
136        for y in 0..h {
137            for x in 0..w {
138                let src_off = ((y * w + x) * 4) as usize;
139                let dst_x = h - 1 - y;
140                let dst_y = x;
141                let dst_off = ((dst_y * h + dst_x) * 4) as usize;
142                dst[dst_off..dst_off + 4].copy_from_slice(&self.data[src_off..src_off + 4]);
143            }
144        }
145        Pixmap {
146            width: h,
147            height: w,
148            data: dst,
149        }
150    }
151
152    /// Rotate this pixmap 180°.
153    pub fn rotate_180(&self) -> Self {
154        let (w, h) = (self.width, self.height);
155        let mut dst = vec![0u8; (w * h * 4) as usize];
156        for y in 0..h {
157            for x in 0..w {
158                let src_off = ((y * w + x) * 4) as usize;
159                let dst_off = (((h - 1 - y) * w + (w - 1 - x)) * 4) as usize;
160                dst[dst_off..dst_off + 4].copy_from_slice(&self.data[src_off..src_off + 4]);
161            }
162        }
163        Pixmap {
164            width: w,
165            height: h,
166            data: dst,
167        }
168    }
169
170    /// Rotate this pixmap 90° counter-clockwise.
171    pub fn rotate_ccw90(&self) -> Self {
172        let (w, h) = (self.width, self.height);
173        let mut dst = vec![0u8; (w * h * 4) as usize];
174        for y in 0..h {
175            for x in 0..w {
176                let src_off = ((y * w + x) * 4) as usize;
177                let dst_x = y;
178                let dst_y = w - 1 - x;
179                let dst_off = ((dst_y * h + dst_x) * 4) as usize;
180                dst[dst_off..dst_off + 4].copy_from_slice(&self.data[src_off..src_off + 4]);
181            }
182        }
183        Pixmap {
184            width: h,
185            height: w,
186            data: dst,
187        }
188    }
189
190    /// Convert to 8-bit grayscale using ITU-R BT.601 luminance weights.
191    ///
192    /// `Y = 0.299·R + 0.587·G + 0.114·B`
193    ///
194    /// Returns a [`GrayPixmap`] with `data.len() == width * height`.
195    pub fn to_gray8(&self) -> GrayPixmap {
196        let pixel_count = self.data.len() / 4;
197        let mut data = Vec::with_capacity(pixel_count);
198        for chunk in self.data.chunks_exact(4) {
199            let r = chunk[0] as u32;
200            let g = chunk[1] as u32;
201            let b = chunk[2] as u32;
202            // Fixed-point: weights × 1024 → 306 + 601 + 117 = 1024
203            let y = (r * 306 + g * 601 + b * 117) >> 10;
204            data.push(y.min(255) as u8);
205        }
206        GrayPixmap {
207            width: self.width,
208            height: self.height,
209            data,
210        }
211    }
212}
213
214/// An 8-bit grayscale image, 1 byte per pixel.
215///
216/// Row-major, top-to-bottom. `data.len() == width * height`.
217/// Produced by [`Pixmap::to_gray8`] or [`crate::djvu_render::render_gray8`].
218#[derive(Debug, Clone, Default, PartialEq, Eq)]
219pub struct GrayPixmap {
220    pub width: u32,
221    pub height: u32,
222    /// Grayscale pixel data, row-major. Length = `width * height`.
223    pub data: Vec<u8>,
224}
225
226impl GrayPixmap {
227    /// Get the luminance value at pixel (x, y).
228    #[inline]
229    pub fn get(&self, x: u32, y: u32) -> u8 {
230        self.data[(y as usize * self.width as usize) + x as usize]
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn white_pixmap() {
240        let pm = Pixmap::white(2, 2);
241        assert_eq!(pm.data.len(), 16);
242        for chunk in pm.data.chunks(4) {
243            assert_eq!(chunk, &[255, 255, 255, 255]);
244        }
245    }
246
247    #[test]
248    fn set_get_rgb() {
249        let mut pm = Pixmap::white(3, 3);
250        pm.set_rgb(1, 1, 100, 150, 200);
251        assert_eq!(pm.get_rgb(1, 1), (100, 150, 200));
252        assert_eq!(pm.get_rgb(0, 0), (255, 255, 255));
253    }
254
255    #[test]
256    fn rotate_cw90_swaps_dimensions() {
257        let pm = Pixmap::white(4, 2);
258        let r = pm.rotate_cw90();
259        assert_eq!((r.width, r.height), (2, 4));
260    }
261
262    #[test]
263    fn rotate_180_preserves_dimensions() {
264        let pm = Pixmap::white(4, 2);
265        let r = pm.rotate_180();
266        assert_eq!((r.width, r.height), (4, 2));
267    }
268
269    #[test]
270    fn rotate_ccw90_swaps_dimensions() {
271        let pm = Pixmap::white(4, 2);
272        let r = pm.rotate_ccw90();
273        assert_eq!((r.width, r.height), (2, 4));
274    }
275
276    #[test]
277    fn rotate_cw90_then_ccw90_is_identity() {
278        let mut pm = Pixmap::white(3, 2);
279        pm.set_rgb(0, 0, 255, 0, 0); // red top-left
280        pm.set_rgb(2, 1, 0, 0, 255); // blue bottom-right
281        let roundtrip = pm.rotate_cw90().rotate_ccw90();
282        assert_eq!(roundtrip.data, pm.data);
283        assert_eq!((roundtrip.width, roundtrip.height), (pm.width, pm.height));
284    }
285
286    #[test]
287    fn rotate_180_twice_is_identity() {
288        let mut pm = Pixmap::white(3, 2);
289        pm.set_rgb(1, 0, 10, 20, 30);
290        let roundtrip = pm.rotate_180().rotate_180();
291        assert_eq!(roundtrip.data, pm.data);
292    }
293
294    #[test]
295    fn rotate_cw90_moves_top_left_to_top_right() {
296        // 2×1 pixmap: red pixel at (0,0), white at (1,0)
297        let mut pm = Pixmap::white(2, 1);
298        pm.set_rgb(0, 0, 255, 0, 0);
299        // After CW90: 1×2, red should be at (0,0) in new coords
300        // new_x = h-1-y = 1-1-0=0, new_y = x = 0 → (0,0)
301        let r = pm.rotate_cw90();
302        assert_eq!(r.width, 1);
303        assert_eq!(r.height, 2);
304        assert_eq!(r.get_rgb(0, 0), (255, 0, 0));
305        assert_eq!(r.get_rgb(0, 1), (255, 255, 255));
306    }
307
308    // Lines 24-25: AsRef<[u8]> impl
309    #[test]
310    fn as_ref_returns_data_slice() {
311        let pm = Pixmap::white(1, 1);
312        let slice: &[u8] = pm.as_ref();
313        assert_eq!(slice.len(), 4);
314    }
315
316    // Lines 42, 45: Pixmap::new early returns for overflow and MAX_PIXELS exceeded
317    #[test]
318    fn new_overflow_returns_empty() {
319        let pm = Pixmap::new(u32::MAX, u32::MAX, 0, 0, 0, 0);
320        assert_eq!(pm.width, 0);
321        assert_eq!(pm.height, 0);
322    }
323
324    #[test]
325    fn new_exceeds_max_pixels_returns_empty() {
326        // 65536 * 65536 = 2^32 overflows usize on 32-bit but on 64-bit it's > MAX_PIXELS
327        let pm = Pixmap::new(10000, 10000, 255, 0, 0, 255);
328        assert_eq!(pm.width, 0);
329        assert_eq!(pm.height, 0);
330    }
331
332    // Lines 85-90: get_pixel() — bounds check and Some result
333    #[test]
334    fn get_pixel_out_of_bounds_returns_none() {
335        let pm = Pixmap::white(2, 2);
336        assert!(pm.get_pixel(2, 0).is_none());
337        assert!(pm.get_pixel(0, 2).is_none());
338    }
339
340    #[test]
341    fn get_pixel_in_bounds_returns_some() {
342        let mut pm = Pixmap::white(2, 2);
343        pm.set_rgb(1, 0, 10, 20, 30);
344        let p = pm.get_pixel(1, 0).expect("in bounds");
345        assert_eq!(&p[..3], &[10, 20, 30]);
346    }
347
348    // Line 100: get_rgb() out-of-bounds returns (0, 0, 0)
349    #[test]
350    fn get_rgb_out_of_bounds_returns_zero() {
351        let pm = Pixmap::white(2, 2);
352        assert_eq!(pm.get_rgb(5, 5), (0, 0, 0));
353    }
354
355    #[test]
356    fn to_ppm_format() {
357        let mut pm = Pixmap::white(2, 1);
358        pm.set_rgb(0, 0, 255, 0, 0); // red
359        pm.set_rgb(1, 0, 0, 0, 255); // blue
360        let ppm = pm.to_ppm();
361        let header = b"P6\n2 1\n255\n";
362        assert_eq!(&ppm[..header.len()], header);
363        assert_eq!(&ppm[header.len()..], &[255, 0, 0, 0, 0, 255]);
364    }
365}