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
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; (w * h * 4) as usize];
27//! decoder.read_image(&mut buf).unwrap();
28//! ```
29
30use std::io::Cursor;
31
32use image::{
33    ColorType, ImageDecoder, ImageDecoderRect, ImageResult,
34    error::{DecodingError, ImageError, ImageFormatHint},
35};
36
37use crate::djvu_document::DjVuPage;
38use crate::djvu_render::{RenderError, RenderOptions};
39
40// ---- Error ------------------------------------------------------------------
41
42/// Errors from the image-rs integration layer.
43#[derive(Debug, thiserror::Error)]
44pub enum ImageCompatError {
45    /// The underlying render pipeline failed.
46    #[error("render error: {0}")]
47    Render(#[from] RenderError),
48}
49
50impl From<ImageCompatError> for ImageError {
51    fn from(e: ImageCompatError) -> Self {
52        ImageError::Decoding(DecodingError::new(
53            ImageFormatHint::Name("DjVu".to_string()),
54            e,
55        ))
56    }
57}
58
59// ---- DjVuDecoder ------------------------------------------------------------
60
61/// An `image::ImageDecoder` and `image::ImageDecoderRect` for a single DjVu page.
62///
63/// By default renders at the native page resolution. Use [`DjVuDecoder::with_size`]
64/// to override the output dimensions.
65pub struct DjVuDecoder<'a> {
66    page: &'a DjVuPage,
67    width: u32,
68    height: u32,
69}
70
71impl<'a> DjVuDecoder<'a> {
72    /// Construct a decoder from a [`DjVuPage`] reference.
73    ///
74    /// The output dimensions default to the native page size from the INFO chunk.
75    pub fn new(page: &'a DjVuPage) -> Result<Self, ImageCompatError> {
76        Ok(Self {
77            width: page.width() as u32,
78            height: page.height() as u32,
79            page,
80        })
81    }
82
83    /// Override the output dimensions.
84    ///
85    /// The rendered image will be scaled to `width × height` using bilinear
86    /// interpolation via [`RenderOptions`].
87    #[must_use]
88    pub fn with_size(mut self, width: u32, height: u32) -> Self {
89        self.width = width;
90        self.height = height;
91        self
92    }
93
94    /// Render the full page into an RGBA byte buffer.
95    fn render_to_vec(&self) -> Result<Vec<u8>, ImageCompatError> {
96        let opts = RenderOptions {
97            width: self.width,
98            height: self.height,
99            scale: self.width as f32 / self.page.width().max(1) as f32,
100            ..RenderOptions::default()
101        };
102        let size = (self.width as usize)
103            .saturating_mul(self.height as usize)
104            .saturating_mul(4);
105        let mut buf = vec![0u8; size];
106        self.page.render_into(&opts, &mut buf)?;
107        Ok(buf)
108    }
109}
110
111// ---- ImageDecoder impl ------------------------------------------------------
112
113impl<'a> ImageDecoder<'a> for DjVuDecoder<'a> {
114    type Reader = Cursor<Vec<u8>>;
115
116    fn dimensions(&self) -> (u32, u32) {
117        (self.width, self.height)
118    }
119
120    fn color_type(&self) -> ColorType {
121        ColorType::Rgba8
122    }
123
124    #[allow(deprecated)]
125    fn into_reader(self) -> ImageResult<Self::Reader> {
126        let data = self.render_to_vec().map_err(ImageError::from)?;
127        Ok(Cursor::new(data))
128    }
129
130    fn read_image(self, buf: &mut [u8]) -> ImageResult<()> {
131        let data = self.render_to_vec().map_err(ImageError::from)?;
132        if buf.len() != data.len() {
133            return Err(ImageError::Decoding(DecodingError::new(
134                ImageFormatHint::Name("DjVu".to_string()),
135                format!(
136                    "buffer size mismatch: expected {}, got {}",
137                    data.len(),
138                    buf.len()
139                ),
140            )));
141        }
142        buf.copy_from_slice(&data);
143        Ok(())
144    }
145}
146
147// ---- ImageDecoderRect impl --------------------------------------------------
148
149impl<'a> ImageDecoderRect<'a> for DjVuDecoder<'a> {
150    /// Decode a rectangular region of the page.
151    ///
152    /// DjVu does not natively support partial rendering; this implementation
153    /// renders the full page and copies out the requested rectangle.
154    /// The `buf` slice must be at least `bytes_per_pixel * width * height` bytes.
155    #[allow(deprecated)]
156    fn read_rect_with_progress<F: Fn(image::Progress)>(
157        &mut self,
158        x: u32,
159        y: u32,
160        width: u32,
161        height: u32,
162        buf: &mut [u8],
163        _progress_callback: F,
164    ) -> ImageResult<()> {
165        let bytes_per_pixel = self.color_type().bytes_per_pixel() as usize;
166        let row_stride = self.width as usize * bytes_per_pixel;
167        let rect_row_bytes = width as usize * bytes_per_pixel;
168
169        // Validate rectangle stays within image bounds.
170        let x_end = x.checked_add(width).ok_or_else(|| {
171            ImageError::Decoding(DecodingError::new(
172                ImageFormatHint::Name("DjVu".to_string()),
173                "rectangle x+width overflows u32",
174            ))
175        })?;
176        let y_end = y.checked_add(height).ok_or_else(|| {
177            ImageError::Decoding(DecodingError::new(
178                ImageFormatHint::Name("DjVu".to_string()),
179                "rectangle y+height overflows u32",
180            ))
181        })?;
182        if x_end > self.width || y_end > self.height {
183            return Err(ImageError::Decoding(DecodingError::new(
184                ImageFormatHint::Name("DjVu".to_string()),
185                format!(
186                    "rectangle ({x},{y},{width},{height}) out of image bounds ({}×{})",
187                    self.width, self.height
188                ),
189            )));
190        }
191
192        let full = self.render_to_vec().map_err(ImageError::from)?;
193
194        for row in 0..height as usize {
195            let src_y = y as usize + row;
196            let src_start = src_y * row_stride + x as usize * bytes_per_pixel;
197            let src_end = src_start + rect_row_bytes;
198            let dst_start = row * rect_row_bytes;
199            let dst_end = dst_start + rect_row_bytes;
200
201            buf[dst_start..dst_end].copy_from_slice(&full[src_start..src_end]);
202        }
203
204        Ok(())
205    }
206}