1#![doc = include_str!("../README.md")]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![deny(missing_docs)]
4#![warn(unreachable_pub)]
5#![deny(rustdoc::broken_intra_doc_links)]
6
7mod error;
8
9#[cfg(feature = "png")]
10#[cfg_attr(docsrs, doc(cfg(feature = "png")))]
11pub mod png;
12
13#[cfg(feature = "jpeg")]
14#[cfg_attr(docsrs, doc(cfg(feature = "jpeg")))]
15pub mod jpeg;
16
17#[cfg(feature = "bmp")]
18#[cfg_attr(docsrs, doc(cfg(feature = "bmp")))]
19pub mod bmp;
20
21pub use error::IoError;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum ImageFormat {
33 Png,
35 Jpeg,
37 Bmp,
39}
40
41pub fn detect_format(bytes: &[u8]) -> Option<ImageFormat> {
63 if bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
64 Some(ImageFormat::Png)
65 } else if bytes.starts_with(&[0xFF, 0xD8, 0xFF]) {
66 Some(ImageFormat::Jpeg)
67 } else if bytes.starts_with(&[0x42, 0x4D]) {
68 Some(ImageFormat::Bmp)
69 } else {
70 None
71 }
72}
73
74#[non_exhaustive]
108pub enum DecodedImage {
109 #[cfg(feature = "png")]
112 #[cfg_attr(docsrs, doc(cfg(feature = "png")))]
113 Png(png::PngDecoded),
114 #[cfg(feature = "jpeg")]
117 #[cfg_attr(docsrs, doc(cfg(feature = "jpeg")))]
118 Jpeg(jpeg::JpegDecoded),
119 #[cfg(feature = "bmp")]
122 #[cfg_attr(docsrs, doc(cfg(feature = "bmp")))]
123 Bmp(bmp::BmpDecoded),
124}
125
126pub fn load(bytes: &[u8]) -> Result<DecodedImage, IoError> {
160 match detect_format(bytes) {
161 #[cfg(feature = "png")]
162 Some(ImageFormat::Png) => Ok(DecodedImage::Png(png::decode(bytes)?)),
163 #[cfg(feature = "jpeg")]
164 Some(ImageFormat::Jpeg) => Ok(DecodedImage::Jpeg(jpeg::decode(bytes)?)),
165 #[cfg(feature = "bmp")]
166 Some(ImageFormat::Bmp) => Ok(DecodedImage::Bmp(bmp::decode(bytes)?)),
167 #[allow(unreachable_patterns)]
173 Some(_) => Err(IoError::InvalidFormat {
174 reason: "detected format is not supported (enable the corresponding feature)",
175 }),
176 None => Err(IoError::InvalidFormat {
177 reason: "unrecognised image format (magic bytes don't match any known codec)",
178 }),
179 }
180}
181
182pub fn load_reader(mut reader: impl std::io::Read) -> Result<DecodedImage, IoError> {
191 let mut buf = Vec::new();
192 reader.read_to_end(&mut buf)?;
193 load(&buf)
194}
195
196#[cfg(test)]
201mod tests {
202 use super::*;
203 #[allow(unused_imports)]
204 use fovea::image::ImageView;
205
206 #[test]
209 fn detect_format_png() {
210 let sig = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
211 assert_eq!(detect_format(&sig), Some(ImageFormat::Png));
212 }
213
214 #[test]
215 fn detect_format_png_with_trailing_data() {
216 let mut data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
217 data.extend_from_slice(&[0x00; 100]);
218 assert_eq!(detect_format(&data), Some(ImageFormat::Png));
219 }
220
221 #[test]
222 fn detect_format_jpeg() {
223 let sig = [0xFF, 0xD8, 0xFF];
224 assert_eq!(detect_format(&sig), Some(ImageFormat::Jpeg));
225 }
226
227 #[test]
228 fn detect_format_jpeg_with_trailing_data() {
229 let data = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10];
230 assert_eq!(detect_format(&data), Some(ImageFormat::Jpeg));
231 }
232
233 #[test]
234 fn detect_format_tiff_le_is_no_longer_recognised() {
235 let sig = [0x49, 0x49, 0x2A, 0x00];
238 assert_eq!(detect_format(&sig), None);
239 }
240
241 #[test]
242 fn detect_format_tiff_be_is_no_longer_recognised() {
243 let sig = [0x4D, 0x4D, 0x00, 0x2A];
244 assert_eq!(detect_format(&sig), None);
245 }
246
247 #[test]
248 fn detect_format_bmp() {
249 let sig = [0x42, 0x4D];
250 assert_eq!(detect_format(&sig), Some(ImageFormat::Bmp));
251 }
252
253 #[test]
254 fn detect_format_bmp_with_trailing_data() {
255 let data = [0x42, 0x4D, 0x00, 0x00, 0x00, 0x00];
256 assert_eq!(detect_format(&data), Some(ImageFormat::Bmp));
257 }
258
259 #[test]
262 fn detect_format_empty() {
263 assert_eq!(detect_format(&[]), None);
264 }
265
266 #[test]
267 fn detect_format_single_byte() {
268 assert_eq!(detect_format(&[0x89]), None);
269 }
270
271 #[test]
272 fn detect_format_unknown_signature() {
273 assert_eq!(detect_format(&[0x00, 0x00, 0x00, 0x00]), None);
274 }
275
276 #[test]
277 fn detect_format_short_for_png() {
278 let short = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A];
280 assert_eq!(detect_format(&short), None);
281 }
282
283 #[test]
284 fn detect_format_near_miss_png() {
285 let near = [0x89, 0x50, 0x4E, 0x47, 0x00, 0x00, 0x00, 0x00];
287 assert_eq!(detect_format(&near), None);
288 }
289
290 #[test]
291 fn detect_format_short_for_jpeg() {
292 assert_eq!(detect_format(&[0xFF, 0xD8]), None);
294 }
295
296 #[test]
297 fn detect_format_short_for_legacy_tiff_le_signature() {
298 assert_eq!(detect_format(&[0x49, 0x49, 0x2A]), None);
301 }
302
303 #[test]
306 fn image_format_debug_and_eq() {
307 let fmt = ImageFormat::Png;
308 let dbg = format!("{:?}", fmt);
309 assert_eq!(dbg, "Png");
310 assert_eq!(fmt, fmt.clone());
311 assert_ne!(ImageFormat::Png, ImageFormat::Jpeg);
312
313 let _ = format!("{:?}", ImageFormat::Jpeg);
315 let _ = format!("{:?}", ImageFormat::Bmp);
316 }
317
318 #[test]
321 fn detect_format_bmp_prefix_not_confused_with_longer() {
322 let data = [0x42, 0x4D, 0x49, 0x49, 0x2A, 0x00];
326 assert_eq!(detect_format(&data), Some(ImageFormat::Bmp));
327 }
328
329 #[cfg(feature = "jpeg")]
332 #[test]
333 fn load_jpeg_dispatches_correctly() {
334 use fovea::image::Image;
336 use fovea::pixel::Srgb8;
337 let img = Image::fill(4, 4, Srgb8::new(100, 150, 200));
338 let bytes = jpeg::encode(&img, &jpeg::JpegEncodeOptions::default()).unwrap();
339 let decoded = load(&bytes).unwrap();
340 match decoded {
341 DecodedImage::Jpeg(d) => match &d.image {
342 jpeg::JpegImage::Srgb8(img) => {
343 assert_eq!(img.width(), 4);
344 assert_eq!(img.height(), 4);
345 }
346 other => panic!("expected Srgb8, got {:?}", other),
347 },
348 #[allow(unreachable_patterns)]
349 _ => panic!("expected DecodedImage::Jpeg"),
350 }
351 }
352
353 #[cfg(feature = "jpeg")]
354 #[test]
355 fn load_reader_jpeg_dispatches_correctly() {
356 use fovea::image::Image;
357 use fovea::pixel::SrgbMono8;
358 let img = Image::fill(8, 6, SrgbMono8::new(128));
359 let bytes = jpeg::encode(&img, &jpeg::JpegEncodeOptions::default()).unwrap();
360 let decoded = load_reader(std::io::Cursor::new(&bytes)).unwrap();
361 match decoded {
362 DecodedImage::Jpeg(d) => match &d.image {
363 jpeg::JpegImage::SrgbMono8(img) => {
364 assert_eq!(img.width(), 8);
365 assert_eq!(img.height(), 6);
366 }
367 other => panic!("expected SrgbMono8, got {:?}", other),
368 },
369 #[allow(unreachable_patterns)]
370 _ => panic!("expected DecodedImage::Jpeg"),
371 }
372 }
373
374 #[test]
375 fn load_unknown_format_returns_error() {
376 let result = load(&[0x00, 0x00, 0x00, 0x00]);
377 assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
378 }
379
380 #[test]
381 fn load_empty_returns_error() {
382 let result = load(&[]);
383 assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
384 }
385
386 #[test]
387 fn load_unsupported_format_returns_error() {
388 let tiff_le = [0x49, 0x49, 0x2A, 0x00];
392 let result = load(&tiff_le);
393 assert!(matches!(result, Err(crate::IoError::InvalidFormat { .. })));
394 }
395
396 #[cfg(feature = "jpeg")]
397 #[test]
398 fn load_jpeg_returns_metadata() {
399 use fovea::image::Image;
400 use fovea::pixel::Srgb8;
401 let img = Image::fill(2, 2, Srgb8::new(0, 0, 0));
402 let bytes = jpeg::encode(&img, &jpeg::JpegEncodeOptions::default()).unwrap();
403 let decoded = load(&bytes).unwrap();
404 match decoded {
405 DecodedImage::Jpeg(d) => {
406 assert_eq!(d.metadata.source_bit_depth, jpeg::JpegBitDepth::Eight);
407 assert_eq!(d.metadata.color_space, jpeg::JpegColorSpace::Srgb);
408 }
409 #[allow(unreachable_patterns)]
410 _ => panic!("expected DecodedImage::Jpeg"),
411 }
412 }
413
414 #[cfg(feature = "bmp")]
415 #[test]
416 fn load_bmp_dispatches_correctly() {
417 use fovea::image::Image;
418 use fovea::pixel::Srgb8;
419 let img = Image::fill(4, 4, Srgb8::new(100, 150, 200));
420 let bytes = bmp::encode(&img, &bmp::BmpEncodeOptions::default()).unwrap();
421 let decoded = load(&bytes).unwrap();
422 match decoded {
423 DecodedImage::Bmp(d) => match &d.image {
424 bmp::BmpImage::Srgb8(img) => {
425 assert_eq!(img.width(), 4);
426 assert_eq!(img.height(), 4);
427 }
428 other => panic!("expected Srgb8, got {:?}", other),
429 },
430 #[allow(unreachable_patterns)]
431 _ => panic!("expected DecodedImage::Bmp"),
432 }
433 }
434
435 #[cfg(feature = "bmp")]
436 #[test]
437 fn load_reader_bmp_dispatches_correctly() {
438 use fovea::image::Image;
439 use fovea::pixel::Srgb8;
440 let img = Image::fill(3, 2, Srgb8::new(42, 84, 126));
441 let bytes = bmp::encode(&img, &bmp::BmpEncodeOptions::default()).unwrap();
442 let decoded = load_reader(std::io::Cursor::new(&bytes)).unwrap();
443 match decoded {
444 DecodedImage::Bmp(d) => match &d.image {
445 bmp::BmpImage::Srgb8(img) => {
446 assert_eq!(img.width(), 3);
447 assert_eq!(img.height(), 2);
448 }
449 other => panic!("expected Srgb8, got {:?}", other),
450 },
451 #[allow(unreachable_patterns)]
452 _ => panic!("expected DecodedImage::Bmp"),
453 }
454 }
455
456 #[cfg(feature = "bmp")]
457 #[test]
458 fn load_bmp_returns_metadata() {
459 use fovea::image::Image;
460 use fovea::pixel::Srgb8;
461 let img = Image::fill(2, 2, Srgb8::new(0, 0, 0));
462 let bytes = bmp::encode(&img, &bmp::BmpEncodeOptions::default()).unwrap();
463 let decoded = load(&bytes).unwrap();
464 match decoded {
465 DecodedImage::Bmp(d) => {
466 assert_eq!(d.metadata.source_bit_depth, bmp::BmpBitDepth::TwentyFour);
467 assert_eq!(d.metadata.color_space, bmp::BmpColorSpace::Srgb);
468 assert_eq!(d.metadata.header_version, bmp::BmpHeaderVersion::Info);
469 assert_eq!(d.metadata.compression, bmp::BmpCompression::None);
470 }
471 #[allow(unreachable_patterns)]
472 _ => panic!("expected DecodedImage::Bmp"),
473 }
474 }
475}