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
29/// Why [`Pixmap::try_new`] refused to allocate.
30///
31/// Both variants describe the requested size, so a caller can report it or
32/// map it into its own limit error.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34#[non_exhaustive]
35pub enum PixmapError {
36    /// `width * height` does not fit in `usize` on this target.
37    Overflow {
38        /// Requested width in pixels.
39        width: u32,
40        /// Requested height in pixels.
41        height: u32,
42    },
43    /// `width * height` exceeds [`Pixmap::MAX_PIXELS`].
44    TooLarge {
45        /// Requested width in pixels.
46        width: u32,
47        /// Requested height in pixels.
48        height: u32,
49        /// Requested pixel count (`width * height`).
50        pixels: usize,
51        /// The ceiling it exceeded, [`Pixmap::MAX_PIXELS`].
52        max: usize,
53    },
54}
55
56impl core::fmt::Display for PixmapError {
57    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
58        match self {
59            PixmapError::Overflow { width, height } => {
60                write!(f, "pixmap {width}x{height} overflows the pixel count")
61            }
62            PixmapError::TooLarge {
63                width,
64                height,
65                pixels,
66                max,
67            } => {
68                write!(
69                    f,
70                    "pixmap {width}x{height} = {pixels} pixels exceeds the limit of {max}"
71                )
72            }
73        }
74    }
75}
76
77impl core::error::Error for PixmapError {}
78
79impl Pixmap {
80    /// Maximum pixels per pixmap (64 megapixels, 256 MiB of RGBA).
81    ///
82    /// Anything beyond this is a runaway DPI. [`Pixmap::try_new`] refuses
83    /// it with [`PixmapError::TooLarge`] instead of attempting the
84    /// allocation.
85    pub const MAX_PIXELS: usize = 64 * 1024 * 1024;
86
87    /// Create a new pixmap filled with the given RGBA color.
88    ///
89    /// # Errors
90    ///
91    /// [`PixmapError::Overflow`] when `width * height` does not fit in
92    /// `usize`; [`PixmapError::TooLarge`] when it exceeds
93    /// [`Pixmap::MAX_PIXELS`]. Nothing is allocated in either case.
94    pub fn try_new(
95        width: u32,
96        height: u32,
97        r: u8,
98        g: u8,
99        b: u8,
100        a: u8,
101    ) -> Result<Self, PixmapError> {
102        let Some(pixel_count) = (width as usize).checked_mul(height as usize) else {
103            return Err(PixmapError::Overflow { width, height });
104        };
105        if pixel_count > Self::MAX_PIXELS {
106            return Err(PixmapError::TooLarge {
107                width,
108                height,
109                pixels: pixel_count,
110                max: Self::MAX_PIXELS,
111            });
112        }
113        // Fast path: all channels equal — single memset.
114        if r == g && g == b && b == a {
115            return Ok(Pixmap {
116                width,
117                height,
118                data: vec![r; pixel_count * 4],
119            });
120        }
121        // General path: repeat the 4-byte RGBA pattern `pixel_count` times.
122        // `slice::repeat` uses a doubling memcpy strategy and is highly optimised.
123        let data = [r, g, b, a].repeat(pixel_count);
124        Ok(Pixmap {
125            width,
126            height,
127            data,
128        })
129    }
130
131    /// Create a new pixmap filled with the given RGBA color.
132    ///
133    /// Returns an empty 0×0 pixmap if `width * height` would exceed
134    /// [`Pixmap::MAX_PIXELS`] or overflow `usize`. That silent fallback is
135    /// why this constructor is deprecated: use [`Pixmap::try_new`] and
136    /// handle the [`PixmapError`] instead.
137    #[deprecated(
138        since = "0.34.0",
139        note = "use `Pixmap::try_new`, which reports an oversized request instead of returning an empty pixmap"
140    )]
141    pub fn new(width: u32, height: u32, r: u8, g: u8, b: u8, a: u8) -> Self {
142        Self::try_new(width, height, r, g, b, a).unwrap_or_default()
143    }
144
145    /// Create a white opaque pixmap.
146    ///
147    /// # Errors
148    ///
149    /// The same as [`Pixmap::try_new`].
150    pub fn try_white(width: u32, height: u32) -> Result<Self, PixmapError> {
151        Self::try_new(width, height, 255, 255, 255, 255)
152    }
153
154    /// Create a white opaque pixmap.
155    ///
156    /// Returns an empty 0×0 pixmap when the size is refused; see
157    /// [`Pixmap::try_white`] for the variant that reports why.
158    pub fn white(width: u32, height: u32) -> Self {
159        Self::try_white(width, height).unwrap_or_default()
160    }
161
162    /// Set pixel at (x, y) to an RGB value (alpha = 255).
163    /// Silently ignores out-of-bounds writes (e.g. on an empty overflow pixmap).
164    #[inline]
165    pub fn set_rgb(&mut self, x: u32, y: u32, r: u8, g: u8, b: u8) {
166        let idx = (y as usize * self.width as usize + x as usize) * 4;
167        if let Some(pixel) = self.data.get_mut(idx..idx + 4) {
168            pixel[0] = r;
169            pixel[1] = g;
170            pixel[2] = b;
171            pixel[3] = 255;
172        }
173    }
174
175    /// Get the 4 RGBA bytes at pixel (x, y), or `None` if out of bounds.
176    #[inline]
177    pub fn get_pixel(&self, x: u32, y: u32) -> Option<&[u8]> {
178        if x >= self.width || y >= self.height {
179            return None;
180        }
181        let idx = (y as usize * self.width as usize + x as usize) * 4;
182        self.data.get(idx..idx + 4)
183    }
184
185    /// Get RGB at (x, y). Returns (0, 0, 0) for out-of-bounds reads.
186    #[inline]
187    pub fn get_rgb(&self, x: u32, y: u32) -> (u8, u8, u8) {
188        let idx = (y as usize * self.width as usize + x as usize) * 4;
189        if let Some(pixel) = self.data.get(idx..idx + 4) {
190            (pixel[0], pixel[1], pixel[2])
191        } else {
192            (0, 0, 0)
193        }
194    }
195
196    /// Extract RGB pixel data (3 bytes per pixel), discarding alpha.
197    pub fn to_rgb(&self) -> Vec<u8> {
198        let pixel_count = self.data.len() / 4;
199        let mut out = Vec::with_capacity(pixel_count * 3);
200        for chunk in self.data.as_chunks::<4>().0 {
201            out.push(chunk[0]);
202            out.push(chunk[1]);
203            out.push(chunk[2]);
204        }
205        out
206    }
207
208    /// Encode as PPM (binary, P6 format).
209    /// This is the format produced by `ddjvu -format=ppm`.
210    /// Discards alpha channel.
211    pub fn to_ppm(&self) -> Vec<u8> {
212        let header = format!("P6\n{} {}\n255\n", self.width, self.height);
213        let pixel_count = self.data.len() / 4;
214        let mut out = Vec::with_capacity(header.len() + pixel_count * 3);
215        out.extend_from_slice(header.as_bytes());
216        for chunk in self.data.as_chunks::<4>().0 {
217            out.push(chunk[0]); // R
218            out.push(chunk[1]); // G
219            out.push(chunk[2]); // B
220        }
221        out
222    }
223
224    /// Rotate this pixmap 90° clockwise.
225    pub fn rotate_cw90(&self) -> Self {
226        let (w, h) = (self.width, self.height);
227        let mut dst = vec![0u8; (w * h * 4) as usize];
228        for y in 0..h {
229            for x in 0..w {
230                let src_off = ((y * w + x) * 4) as usize;
231                let dst_x = h - 1 - y;
232                let dst_y = x;
233                let dst_off = ((dst_y * h + dst_x) * 4) as usize;
234                dst[dst_off..dst_off + 4].copy_from_slice(&self.data[src_off..src_off + 4]);
235            }
236        }
237        Pixmap {
238            width: h,
239            height: w,
240            data: dst,
241        }
242    }
243
244    /// Rotate this pixmap 180°.
245    pub fn rotate_180(&self) -> Self {
246        let (w, h) = (self.width, self.height);
247        let mut dst = vec![0u8; (w * h * 4) as usize];
248        for y in 0..h {
249            for x in 0..w {
250                let src_off = ((y * w + x) * 4) as usize;
251                let dst_off = (((h - 1 - y) * w + (w - 1 - x)) * 4) as usize;
252                dst[dst_off..dst_off + 4].copy_from_slice(&self.data[src_off..src_off + 4]);
253            }
254        }
255        Pixmap {
256            width: w,
257            height: h,
258            data: dst,
259        }
260    }
261
262    /// Rotate this pixmap 90° counter-clockwise.
263    pub fn rotate_ccw90(&self) -> Self {
264        let (w, h) = (self.width, self.height);
265        let mut dst = vec![0u8; (w * h * 4) as usize];
266        for y in 0..h {
267            for x in 0..w {
268                let src_off = ((y * w + x) * 4) as usize;
269                let dst_x = y;
270                let dst_y = w - 1 - x;
271                let dst_off = ((dst_y * h + dst_x) * 4) as usize;
272                dst[dst_off..dst_off + 4].copy_from_slice(&self.data[src_off..src_off + 4]);
273            }
274        }
275        Pixmap {
276            width: h,
277            height: w,
278            data: dst,
279        }
280    }
281
282    /// Convert to 8-bit grayscale using ITU-R BT.601 luminance weights.
283    ///
284    /// `Y = 0.299·R + 0.587·G + 0.114·B`
285    ///
286    /// Returns a [`GrayPixmap`] with `data.len() == width * height`.
287    pub fn to_gray8(&self) -> GrayPixmap {
288        let pixel_count = self.data.len() / 4;
289        let mut data = Vec::with_capacity(pixel_count);
290        for chunk in self.data.as_chunks::<4>().0 {
291            let r = chunk[0] as u32;
292            let g = chunk[1] as u32;
293            let b = chunk[2] as u32;
294            // Fixed-point: weights × 1024 → 306 + 601 + 117 = 1024
295            let y = (r * 306 + g * 601 + b * 117) >> 10;
296            data.push(y.min(255) as u8);
297        }
298        GrayPixmap {
299            width: self.width,
300            height: self.height,
301            data,
302        }
303    }
304}
305
306/// An 8-bit grayscale image, 1 byte per pixel.
307///
308/// Row-major, top-to-bottom. `data.len() == width * height`.
309/// Produced by [`Pixmap::to_gray8`] or [`crate::djvu_render::render_gray8`].
310#[derive(Debug, Clone, Default, PartialEq, Eq)]
311pub struct GrayPixmap {
312    pub width: u32,
313    pub height: u32,
314    /// Grayscale pixel data, row-major. Length = `width * height`.
315    pub data: Vec<u8>,
316}
317
318impl GrayPixmap {
319    /// Get the luminance value at pixel (x, y).
320    #[inline]
321    pub fn get(&self, x: u32, y: u32) -> u8 {
322        self.data[(y as usize * self.width as usize) + x as usize]
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn white_pixmap() {
332        let pm = Pixmap::white(2, 2);
333        assert_eq!(pm.data.len(), 16);
334        for chunk in pm.data.chunks(4) {
335            assert_eq!(chunk, &[255, 255, 255, 255]);
336        }
337    }
338
339    #[test]
340    fn set_get_rgb() {
341        let mut pm = Pixmap::white(3, 3);
342        pm.set_rgb(1, 1, 100, 150, 200);
343        assert_eq!(pm.get_rgb(1, 1), (100, 150, 200));
344        assert_eq!(pm.get_rgb(0, 0), (255, 255, 255));
345    }
346
347    #[test]
348    fn rotate_cw90_swaps_dimensions() {
349        let pm = Pixmap::white(4, 2);
350        let r = pm.rotate_cw90();
351        assert_eq!((r.width, r.height), (2, 4));
352    }
353
354    #[test]
355    fn rotate_180_preserves_dimensions() {
356        let pm = Pixmap::white(4, 2);
357        let r = pm.rotate_180();
358        assert_eq!((r.width, r.height), (4, 2));
359    }
360
361    #[test]
362    fn rotate_ccw90_swaps_dimensions() {
363        let pm = Pixmap::white(4, 2);
364        let r = pm.rotate_ccw90();
365        assert_eq!((r.width, r.height), (2, 4));
366    }
367
368    #[test]
369    fn rotate_cw90_then_ccw90_is_identity() {
370        let mut pm = Pixmap::white(3, 2);
371        pm.set_rgb(0, 0, 255, 0, 0); // red top-left
372        pm.set_rgb(2, 1, 0, 0, 255); // blue bottom-right
373        let roundtrip = pm.rotate_cw90().rotate_ccw90();
374        assert_eq!(roundtrip.data, pm.data);
375        assert_eq!((roundtrip.width, roundtrip.height), (pm.width, pm.height));
376    }
377
378    #[test]
379    fn rotate_180_twice_is_identity() {
380        let mut pm = Pixmap::white(3, 2);
381        pm.set_rgb(1, 0, 10, 20, 30);
382        let roundtrip = pm.rotate_180().rotate_180();
383        assert_eq!(roundtrip.data, pm.data);
384    }
385
386    #[test]
387    fn rotate_cw90_moves_top_left_to_top_right() {
388        // 2×1 pixmap: red pixel at (0,0), white at (1,0)
389        let mut pm = Pixmap::white(2, 1);
390        pm.set_rgb(0, 0, 255, 0, 0);
391        // After CW90: 1×2, red should be at (0,0) in new coords
392        // new_x = h-1-y = 1-1-0=0, new_y = x = 0 → (0,0)
393        let r = pm.rotate_cw90();
394        assert_eq!(r.width, 1);
395        assert_eq!(r.height, 2);
396        assert_eq!(r.get_rgb(0, 0), (255, 0, 0));
397        assert_eq!(r.get_rgb(0, 1), (255, 255, 255));
398    }
399
400    // Lines 24-25: AsRef<[u8]> impl
401    #[test]
402    fn as_ref_returns_data_slice() {
403        let pm = Pixmap::white(1, 1);
404        let slice: &[u8] = pm.as_ref();
405        assert_eq!(slice.len(), 4);
406    }
407
408    // `Pixmap::try_new` refusals: overflow and MAX_PIXELS exceeded; `new`
409    // maps both to an empty pixmap.
410    #[test]
411    fn try_new_largest_request_is_refused() {
412        // `u32::MAX * u32::MAX` overflows a 32-bit `usize` and is merely too
413        // large on a 64-bit one; either way nothing is allocated.
414        let err = Pixmap::try_new(u32::MAX, u32::MAX, 0, 0, 0, 0).unwrap_err();
415        #[cfg(target_pointer_width = "32")]
416        assert_eq!(
417            err,
418            PixmapError::Overflow {
419                width: u32::MAX,
420                height: u32::MAX
421            }
422        );
423        #[cfg(not(target_pointer_width = "32"))]
424        assert!(matches!(
425            err,
426            PixmapError::TooLarge {
427                width: u32::MAX,
428                height: u32::MAX,
429                max: Pixmap::MAX_PIXELS,
430                ..
431            }
432        ));
433    }
434
435    #[test]
436    fn overflow_error_names_the_request() {
437        let err = PixmapError::Overflow {
438            width: 70000,
439            height: 70000,
440        };
441        assert_eq!(
442            format!("{err}"),
443            "pixmap 70000x70000 overflows the pixel count"
444        );
445    }
446
447    #[test]
448    fn try_new_exceeds_max_pixels_reports_count() {
449        // 10000 * 10000 = 100 M pixels > 64 Mi pixels on every target.
450        let err = Pixmap::try_new(10000, 10000, 255, 0, 0, 255).unwrap_err();
451        assert_eq!(
452            err,
453            PixmapError::TooLarge {
454                width: 10000,
455                height: 10000,
456                pixels: 100_000_000,
457                max: Pixmap::MAX_PIXELS
458            }
459        );
460        assert!(format!("{err}").contains("100000000"));
461    }
462
463    #[test]
464    fn try_new_at_max_pixels_allocates() {
465        // Exactly the ceiling is allowed; one row of MAX_PIXELS pixels.
466        let pm = Pixmap::try_new(Pixmap::MAX_PIXELS as u32, 1, 1, 2, 3, 4).unwrap();
467        assert_eq!(pm.data.len(), Pixmap::MAX_PIXELS * 4);
468        assert_eq!(&pm.data[..4], &[1, 2, 3, 4]);
469    }
470
471    #[test]
472    #[allow(deprecated)]
473    fn new_returns_empty_on_refusal() {
474        let pm = Pixmap::new(u32::MAX, u32::MAX, 0, 0, 0, 0);
475        assert_eq!((pm.width, pm.height, pm.data.len()), (0, 0, 0));
476        let pm = Pixmap::new(10000, 10000, 255, 0, 0, 255);
477        assert_eq!((pm.width, pm.height, pm.data.len()), (0, 0, 0));
478    }
479
480    #[test]
481    fn try_white_matches_white() {
482        let pm = Pixmap::try_white(3, 2).unwrap();
483        assert_eq!(pm, Pixmap::white(3, 2));
484        assert!(Pixmap::try_white(u32::MAX, u32::MAX).is_err());
485        assert_eq!(Pixmap::white(u32::MAX, u32::MAX).width, 0);
486    }
487
488    // Lines 85-90: get_pixel() — bounds check and Some result
489    #[test]
490    fn get_pixel_out_of_bounds_returns_none() {
491        let pm = Pixmap::white(2, 2);
492        assert!(pm.get_pixel(2, 0).is_none());
493        assert!(pm.get_pixel(0, 2).is_none());
494    }
495
496    #[test]
497    fn get_pixel_in_bounds_returns_some() {
498        let mut pm = Pixmap::white(2, 2);
499        pm.set_rgb(1, 0, 10, 20, 30);
500        let p = pm.get_pixel(1, 0).expect("in bounds");
501        assert_eq!(&p[..3], &[10, 20, 30]);
502    }
503
504    // Line 100: get_rgb() out-of-bounds returns (0, 0, 0)
505    #[test]
506    fn get_rgb_out_of_bounds_returns_zero() {
507        let pm = Pixmap::white(2, 2);
508        assert_eq!(pm.get_rgb(5, 5), (0, 0, 0));
509    }
510
511    #[test]
512    fn to_ppm_format() {
513        let mut pm = Pixmap::white(2, 1);
514        pm.set_rgb(0, 0, 255, 0, 0); // red
515        pm.set_rgb(1, 0, 0, 0, 255); // blue
516        let ppm = pm.to_ppm();
517        let header = b"P6\n2 1\n255\n";
518        assert_eq!(&ppm[..header.len()], header);
519        assert_eq!(&ppm[header.len()..], &[255, 0, 0, 0, 0, 255]);
520    }
521}