1use 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#[derive(Debug, thiserror::Error)]
42#[non_exhaustive]
43pub enum ImageCompatError {
44 #[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
58pub struct DjVuDecoder<'a> {
65 page: &'a DjVuPage,
66 width: u32,
67 height: u32,
68}
69
70impl<'a> DjVuDecoder<'a> {
71 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 #[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 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
109impl<'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
144impl<'a> ImageDecoderRect for DjVuDecoder<'a> {
147 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 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 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]; 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 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 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 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 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}