Skip to main content

djvu_rs/
image_compat.rs

1//! `image::ImageDecoder` integration for DjVu pages.
2//!
3//! This module provides [`DjVuDecoder`], which implements the
4//! [`image::ImageDecoder`] and [`image::ImageDecoderRect`] traits from the
5//! `image` crate, making djvu-rs a first-class image format usable anywhere
6//! image-rs pipelines are used.
7//!
8//! ## Key public types
9//!
10//! - [`DjVuDecoder`] — implements `image::ImageDecoder` and `image::ImageDecoderRect`
11//!   for a single DjVu page (compatible with `image` 0.25+)
12//! - [`ImageCompatError`] — typed errors from this module
13//!
14//! ## Usage
15//!
16//! ```no_run
17//! use djvu_rs::{DjVuDocument, image_compat::DjVuDecoder};
18//! use image::ImageDecoder;
19//!
20//! let data = std::fs::read("file.djvu").unwrap();
21//! let doc = DjVuDocument::parse(&data).unwrap();
22//! let page = doc.page(0).unwrap();
23//!
24//! let decoder = DjVuDecoder::new(&page).unwrap();
25//! let (w, h) = decoder.dimensions();
26//! let mut buf = vec![0u8; decoder.total_bytes() as usize];
27//! decoder.read_image(&mut buf).unwrap();
28//! ```
29
30use image::{
31    ColorType, ImageDecoder, ImageDecoderRect, ImageResult,
32    error::{DecodingError, ImageError, ImageFormatHint},
33};
34
35use crate::djvu_document::DjVuPage;
36use crate::djvu_render::{RenderError, RenderOptions};
37
38// ---- Error ------------------------------------------------------------------
39
40/// Errors from the image-rs integration layer.
41#[derive(Debug, thiserror::Error)]
42#[non_exhaustive]
43pub enum ImageCompatError {
44    /// The underlying render pipeline failed.
45    #[error("render error: {0}")]
46    Render(#[from] RenderError),
47}
48
49impl From<ImageCompatError> for ImageError {
50    fn from(e: ImageCompatError) -> Self {
51        ImageError::Decoding(DecodingError::new(
52            ImageFormatHint::Name("DjVu".to_string()),
53            e,
54        ))
55    }
56}
57
58// ---- DjVuDecoder ------------------------------------------------------------
59
60/// An `image::ImageDecoder` and `image::ImageDecoderRect` for a single DjVu page.
61///
62/// By default renders at the native page resolution. Use [`DjVuDecoder::with_size`]
63/// to override the output dimensions.
64pub struct DjVuDecoder<'a> {
65    page: &'a DjVuPage,
66    width: u32,
67    height: u32,
68}
69
70impl<'a> DjVuDecoder<'a> {
71    /// Construct a decoder from a [`DjVuPage`] reference.
72    ///
73    /// The output dimensions default to the native page size from the INFO chunk.
74    pub fn new(page: &'a DjVuPage) -> Result<Self, ImageCompatError> {
75        Ok(Self {
76            width: page.width() as u32,
77            height: page.height() as u32,
78            page,
79        })
80    }
81
82    /// Override the output dimensions.
83    ///
84    /// The rendered image will be scaled to `width × height` using bilinear
85    /// interpolation via [`RenderOptions`].
86    #[must_use]
87    pub fn with_size(mut self, width: u32, height: u32) -> Self {
88        self.width = width;
89        self.height = height;
90        self
91    }
92
93    /// Render the full page into an RGBA byte buffer.
94    fn render_to_vec(&self) -> Result<Vec<u8>, ImageCompatError> {
95        let opts = RenderOptions {
96            width: self.width,
97            height: self.height,
98            ..RenderOptions::default()
99        };
100        let size = (self.width as usize)
101            .saturating_mul(self.height as usize)
102            .saturating_mul(4);
103        let mut buf = vec![0u8; size];
104        self.page.render_into(&opts, &mut buf)?;
105        Ok(buf)
106    }
107}
108
109// ---- ImageDecoder impl ------------------------------------------------------
110
111impl<'a> ImageDecoder for DjVuDecoder<'a> {
112    fn dimensions(&self) -> (u32, u32) {
113        (self.width, self.height)
114    }
115
116    fn color_type(&self) -> ColorType {
117        ColorType::Rgba8
118    }
119
120    fn read_image(self, buf: &mut [u8]) -> ImageResult<()>
121    where
122        Self: Sized,
123    {
124        let data = self.render_to_vec().map_err(ImageError::from)?;
125        if buf.len() != data.len() {
126            return Err(ImageError::Decoding(DecodingError::new(
127                ImageFormatHint::Name("DjVu".to_string()),
128                format!(
129                    "buffer size mismatch: expected {}, got {}",
130                    data.len(),
131                    buf.len()
132                ),
133            )));
134        }
135        buf.copy_from_slice(&data);
136        Ok(())
137    }
138
139    fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
140        (*self).read_image(buf)
141    }
142}
143
144// ---- ImageDecoderRect impl --------------------------------------------------
145
146impl<'a> ImageDecoderRect for DjVuDecoder<'a> {
147    /// Decode a rectangular region of the page.
148    ///
149    /// DjVu does not natively support partial rendering; this implementation
150    /// renders the full page and copies out the requested rectangle.
151    /// `row_pitch` is the number of bytes between the start of consecutive
152    /// rows in `buf` (must be ≥ `width * bytes_per_pixel`).
153    fn read_rect(
154        &mut self,
155        x: u32,
156        y: u32,
157        width: u32,
158        height: u32,
159        buf: &mut [u8],
160        row_pitch: usize,
161    ) -> ImageResult<()> {
162        let bytes_per_pixel = self.color_type().bytes_per_pixel() as usize;
163        let full_row_bytes = self.width as usize * bytes_per_pixel;
164        let rect_row_bytes = width as usize * bytes_per_pixel;
165
166        // Validate rectangle stays within image bounds.
167        let x_end = x.checked_add(width).ok_or_else(|| {
168            ImageError::Decoding(DecodingError::new(
169                ImageFormatHint::Name("DjVu".to_string()),
170                "rectangle x+width overflows u32",
171            ))
172        })?;
173        let y_end = y.checked_add(height).ok_or_else(|| {
174            ImageError::Decoding(DecodingError::new(
175                ImageFormatHint::Name("DjVu".to_string()),
176                "rectangle y+height overflows u32",
177            ))
178        })?;
179        if x_end > self.width || y_end > self.height {
180            return Err(ImageError::Decoding(DecodingError::new(
181                ImageFormatHint::Name("DjVu".to_string()),
182                format!(
183                    "rectangle ({x},{y},{width},{height}) out of image bounds ({}×{})",
184                    self.width, self.height
185                ),
186            )));
187        }
188
189        let full = self.render_to_vec().map_err(ImageError::from)?;
190
191        for row in 0..height as usize {
192            let src_y = y as usize + row;
193            let src_start = src_y * full_row_bytes + x as usize * bytes_per_pixel;
194            let src_end = src_start + rect_row_bytes;
195            let dst_start = row * row_pitch;
196            let dst_end = dst_start + rect_row_bytes;
197
198            buf[dst_start..dst_end].copy_from_slice(&full[src_start..src_end]);
199        }
200
201        Ok(())
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use image::{ColorType, ImageDecoder};
209
210    fn assets_path() -> std::path::PathBuf {
211        std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
212            .join("references/djvujs/library/assets")
213    }
214
215    fn load_page(name: &str) -> crate::djvu_document::DjVuDocument {
216        let data =
217            std::fs::read(assets_path().join(name)).unwrap_or_else(|_| panic!("{name} must exist"));
218        crate::djvu_document::DjVuDocument::parse(&data).unwrap_or_else(|e| panic!("parse: {e}"))
219    }
220
221    #[test]
222    fn dimensions_match_page_size() {
223        let doc = load_page("chicken.djvu");
224        let page = doc.page(0).unwrap();
225        let decoder = DjVuDecoder::new(page).unwrap();
226        let (w, h) = decoder.dimensions();
227        assert_eq!(w, page.width() as u32);
228        assert_eq!(h, page.height() as u32);
229    }
230
231    #[test]
232    fn color_type_is_rgba8() {
233        let doc = load_page("chicken.djvu");
234        let page = doc.page(0).unwrap();
235        let decoder = DjVuDecoder::new(page).unwrap();
236        assert_eq!(decoder.color_type(), ColorType::Rgba8);
237    }
238
239    #[test]
240    fn total_bytes_equals_w_times_h_times_4() {
241        let doc = load_page("chicken.djvu");
242        let page = doc.page(0).unwrap();
243        let decoder = DjVuDecoder::new(page).unwrap();
244        let (w, h) = decoder.dimensions();
245        assert_eq!(decoder.total_bytes(), (w as u64) * (h as u64) * 4);
246    }
247
248    #[test]
249    fn read_image_fills_buffer() {
250        let doc = load_page("chicken.djvu");
251        let page = doc.page(0).unwrap();
252        let decoder = DjVuDecoder::new(page).unwrap();
253        let n = decoder.total_bytes() as usize;
254        let mut buf = vec![0u8; n];
255        decoder.read_image(&mut buf).unwrap();
256        // At least some pixels must be non-zero for a real page
257        assert!(
258            buf.iter().any(|&b| b != 0),
259            "rendered image must have non-zero pixels"
260        );
261    }
262
263    #[test]
264    fn read_image_wrong_buffer_size_returns_error() {
265        let doc = load_page("chicken.djvu");
266        let page = doc.page(0).unwrap();
267        let decoder = DjVuDecoder::new(page).unwrap();
268        let mut buf = vec![0u8; 4]; // far too small
269        let result = decoder.read_image(&mut buf);
270        assert!(result.is_err(), "wrong buffer size must return error");
271    }
272
273    #[test]
274    fn with_size_overrides_dimensions() {
275        let doc = load_page("chicken.djvu");
276        let page = doc.page(0).unwrap();
277        let decoder = DjVuDecoder::new(page).unwrap().with_size(64, 48);
278        let (w, h) = decoder.dimensions();
279        assert_eq!((w, h), (64, 48));
280    }
281
282    #[test]
283    fn read_rect_full_page_matches_read_image() {
284        use image::ImageDecoderRect;
285        let doc = load_page("chicken.djvu");
286        let page = doc.page(0).unwrap();
287        let decoder = DjVuDecoder::new(page).unwrap().with_size(32, 24);
288        let (w, h) = decoder.dimensions();
289        let n = (w as usize) * (h as usize) * 4;
290
291        let mut full = vec![0u8; n];
292        // Use a separate decoder for read_image (consumes self)
293        let decoder2 = DjVuDecoder::new(page).unwrap().with_size(32, 24);
294        decoder2.read_image(&mut full).unwrap();
295
296        let mut rect_buf = vec![0u8; n];
297        let mut decoder3 = DjVuDecoder::new(page).unwrap().with_size(32, 24);
298        decoder3
299            .read_rect(0, 0, w, h, &mut rect_buf, w as usize * 4)
300            .unwrap();
301
302        assert_eq!(
303            full, rect_buf,
304            "full read_image and read_rect(full page) must match"
305        );
306    }
307
308    #[test]
309    fn read_rect_out_of_bounds_returns_error() {
310        use image::ImageDecoderRect;
311        let doc = load_page("chicken.djvu");
312        let page = doc.page(0).unwrap();
313        let mut decoder = DjVuDecoder::new(page).unwrap().with_size(32, 24);
314        let mut buf = vec![0u8; 32 * 24 * 4];
315        // x + width > image width
316        let result = decoder.read_rect(10, 0, 30, 24, &mut buf, 30 * 4);
317        assert!(result.is_err(), "out-of-bounds read_rect must return error");
318    }
319
320    #[test]
321    fn from_image_compat_error_for_image_error() {
322        use image::error::ImageError;
323        let err = ImageCompatError::Render(crate::djvu_render::RenderError::InvalidDimensions {
324            width: 0,
325            height: 0,
326        });
327        let image_err: ImageError = err.into();
328        let s = image_err.to_string();
329        assert!(s.contains("DjVu"), "ImageError must mention DjVu: {s}");
330    }
331
332    #[test]
333    fn read_image_boxed_works() {
334        let doc = load_page("chicken.djvu");
335        let page = doc.page(0).unwrap();
336        let decoder = Box::new(DjVuDecoder::new(page).unwrap().with_size(16, 12));
337        let n = (16 * 12 * 4) as usize;
338        let mut buf = vec![0u8; n];
339        decoder.read_image_boxed(&mut buf).unwrap();
340        assert!(buf.iter().any(|&b| b != 0));
341    }
342
343    #[test]
344    fn read_rect_x_plus_width_overflow_returns_error() {
345        use image::ImageDecoderRect;
346        let doc = load_page("chicken.djvu");
347        let page = doc.page(0).unwrap();
348        let mut decoder = DjVuDecoder::new(page).unwrap().with_size(32, 24);
349        let mut buf = vec![0u8; 4];
350        // x = u32::MAX, width = 1 → overflow
351        let result = decoder.read_rect(u32::MAX, 0, 1, 1, &mut buf, 4);
352        assert!(result.is_err(), "x+width overflow must return error");
353    }
354
355    #[test]
356    fn read_rect_y_plus_height_overflow_returns_error() {
357        use image::ImageDecoderRect;
358        let doc = load_page("chicken.djvu");
359        let page = doc.page(0).unwrap();
360        let mut decoder = DjVuDecoder::new(page).unwrap().with_size(32, 24);
361        let mut buf = vec![0u8; 4];
362        // y = u32::MAX, height = 1 → overflow
363        let result = decoder.read_rect(0, u32::MAX, 1, 1, &mut buf, 4);
364        assert!(result.is_err(), "y+height overflow must return error");
365    }
366}