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    /// Convert to 8-bit grayscale using ITU-R BT.601 luminance weights.
133    ///
134    /// `Y = 0.299·R + 0.587·G + 0.114·B`
135    ///
136    /// Returns a [`GrayPixmap`] with `data.len() == width * height`.
137    pub fn to_gray8(&self) -> GrayPixmap {
138        let pixel_count = self.data.len() / 4;
139        let mut data = Vec::with_capacity(pixel_count);
140        for chunk in self.data.chunks_exact(4) {
141            let r = chunk[0] as u32;
142            let g = chunk[1] as u32;
143            let b = chunk[2] as u32;
144            // Fixed-point: weights × 1024 → 306 + 601 + 117 = 1024
145            let y = (r * 306 + g * 601 + b * 117) >> 10;
146            data.push(y.min(255) as u8);
147        }
148        GrayPixmap {
149            width: self.width,
150            height: self.height,
151            data,
152        }
153    }
154}
155
156/// An 8-bit grayscale image, 1 byte per pixel.
157///
158/// Row-major, top-to-bottom. `data.len() == width * height`.
159/// Produced by [`Pixmap::to_gray8`] or [`crate::djvu_render::render_gray8`].
160#[derive(Debug, Clone, Default, PartialEq, Eq)]
161pub struct GrayPixmap {
162    pub width: u32,
163    pub height: u32,
164    /// Grayscale pixel data, row-major. Length = `width * height`.
165    pub data: Vec<u8>,
166}
167
168impl GrayPixmap {
169    /// Get the luminance value at pixel (x, y).
170    #[inline]
171    pub fn get(&self, x: u32, y: u32) -> u8 {
172        self.data[(y as usize * self.width as usize) + x as usize]
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn white_pixmap() {
182        let pm = Pixmap::white(2, 2);
183        assert_eq!(pm.data.len(), 16);
184        for chunk in pm.data.chunks(4) {
185            assert_eq!(chunk, &[255, 255, 255, 255]);
186        }
187    }
188
189    #[test]
190    fn set_get_rgb() {
191        let mut pm = Pixmap::white(3, 3);
192        pm.set_rgb(1, 1, 100, 150, 200);
193        assert_eq!(pm.get_rgb(1, 1), (100, 150, 200));
194        assert_eq!(pm.get_rgb(0, 0), (255, 255, 255));
195    }
196
197    #[test]
198    fn to_ppm_format() {
199        let mut pm = Pixmap::white(2, 1);
200        pm.set_rgb(0, 0, 255, 0, 0); // red
201        pm.set_rgb(1, 0, 0, 0, 255); // blue
202        let ppm = pm.to_ppm();
203        let header = b"P6\n2 1\n255\n";
204        assert_eq!(&ppm[..header.len()], header);
205        assert_eq!(&ppm[header.len()..], &[255, 0, 0, 0, 0, 255]);
206    }
207}