#![doc = include_str!("../README.md")]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
#![warn(unreachable_pub)]
#![deny(rustdoc::broken_intra_doc_links)]
mod error;
#[cfg(feature = "png")]
#[cfg_attr(docsrs, doc(cfg(feature = "png")))]
pub mod png;
#[cfg(feature = "jpeg")]
#[cfg_attr(docsrs, doc(cfg(feature = "jpeg")))]
pub mod jpeg;
#[cfg(feature = "bmp")]
#[cfg_attr(docsrs, doc(cfg(feature = "bmp")))]
pub mod bmp;
pub use error::IoError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageFormat {
Png,
Jpeg,
Bmp,
}
pub fn detect_format(bytes: &[u8]) -> Option<ImageFormat> {
if bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
Some(ImageFormat::Png)
} else if bytes.starts_with(&[0xFF, 0xD8, 0xFF]) {
Some(ImageFormat::Jpeg)
} else if bytes.starts_with(&[0x42, 0x4D]) {
Some(ImageFormat::Bmp)
} else {
None
}
}
#[non_exhaustive]
pub enum DecodedImage {
#[cfg(feature = "png")]
#[cfg_attr(docsrs, doc(cfg(feature = "png")))]
Png(png::PngDecoded),
#[cfg(feature = "jpeg")]
#[cfg_attr(docsrs, doc(cfg(feature = "jpeg")))]
Jpeg(jpeg::JpegDecoded),
#[cfg(feature = "bmp")]
#[cfg_attr(docsrs, doc(cfg(feature = "bmp")))]
Bmp(bmp::BmpDecoded),
}
pub fn load(bytes: &[u8]) -> Result<DecodedImage, IoError> {
match detect_format(bytes) {
#[cfg(feature = "png")]
Some(ImageFormat::Png) => Ok(DecodedImage::Png(png::decode(bytes)?)),
#[cfg(feature = "jpeg")]
Some(ImageFormat::Jpeg) => Ok(DecodedImage::Jpeg(jpeg::decode(bytes)?)),
#[cfg(feature = "bmp")]
Some(ImageFormat::Bmp) => Ok(DecodedImage::Bmp(bmp::decode(bytes)?)),
#[allow(unreachable_patterns)]
Some(_) => Err(IoError::InvalidFormat {
reason: "detected format is not supported (enable the corresponding feature)",
}),
None => Err(IoError::InvalidFormat {
reason: "unrecognised image format (magic bytes don't match any known codec)",
}),
}
}
pub fn load_reader(mut reader: impl std::io::Read) -> Result<DecodedImage, IoError> {
let mut buf = Vec::new();
reader.read_to_end(&mut buf)?;
load(&buf)
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(unused_imports)]
use fovea::image::ImageView;
#[test]
fn detect_format_png() {
let sig = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
assert_eq!(detect_format(&sig), Some(ImageFormat::Png));
}
#[test]
fn detect_format_png_with_trailing_data() {
let mut data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
data.extend_from_slice(&[0x00; 100]);
assert_eq!(detect_format(&data), Some(ImageFormat::Png));
}
#[test]
fn detect_format_jpeg() {
let sig = [0xFF, 0xD8, 0xFF];
assert_eq!(detect_format(&sig), Some(ImageFormat::Jpeg));
}
#[test]
fn detect_format_jpeg_with_trailing_data() {
let data = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10];
assert_eq!(detect_format(&data), Some(ImageFormat::Jpeg));
}
#[test]
fn detect_format_tiff_le_is_no_longer_recognised() {
let sig = [0x49, 0x49, 0x2A, 0x00];
assert_eq!(detect_format(&sig), None);
}
#[test]
fn detect_format_tiff_be_is_no_longer_recognised() {
let sig = [0x4D, 0x4D, 0x00, 0x2A];
assert_eq!(detect_format(&sig), None);
}
#[test]
fn detect_format_bmp() {
let sig = [0x42, 0x4D];
assert_eq!(detect_format(&sig), Some(ImageFormat::Bmp));
}
#[test]
fn detect_format_bmp_with_trailing_data() {
let data = [0x42, 0x4D, 0x00, 0x00, 0x00, 0x00];
assert_eq!(detect_format(&data), Some(ImageFormat::Bmp));
}
#[test]
fn detect_format_empty() {
assert_eq!(detect_format(&[]), None);
}
#[test]
fn detect_format_single_byte() {
assert_eq!(detect_format(&[0x89]), None);
}
#[test]
fn detect_format_unknown_signature() {
assert_eq!(detect_format(&[0x00, 0x00, 0x00, 0x00]), None);
}
#[test]
fn detect_format_short_for_png() {
let short = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A];
assert_eq!(detect_format(&short), None);
}
#[test]
fn detect_format_near_miss_png() {
let near = [0x89, 0x50, 0x4E, 0x47, 0x00, 0x00, 0x00, 0x00];
assert_eq!(detect_format(&near), None);
}
#[test]
fn detect_format_short_for_jpeg() {
assert_eq!(detect_format(&[0xFF, 0xD8]), None);
}
#[test]
fn detect_format_short_for_legacy_tiff_le_signature() {
assert_eq!(detect_format(&[0x49, 0x49, 0x2A]), None);
}
#[test]
fn image_format_debug_and_eq() {
let fmt = ImageFormat::Png;
let dbg = format!("{:?}", fmt);
assert_eq!(dbg, "Png");
assert_eq!(fmt, fmt.clone());
assert_ne!(ImageFormat::Png, ImageFormat::Jpeg);
let _ = format!("{:?}", ImageFormat::Jpeg);
let _ = format!("{:?}", ImageFormat::Bmp);
}
#[test]
fn detect_format_bmp_prefix_not_confused_with_longer() {
let data = [0x42, 0x4D, 0x49, 0x49, 0x2A, 0x00];
assert_eq!(detect_format(&data), Some(ImageFormat::Bmp));
}
#[cfg(feature = "jpeg")]
#[test]
fn load_jpeg_dispatches_correctly() {
use fovea::image::Image;
use fovea::pixel::Srgb8;
let img = Image::fill(4, 4, Srgb8::new(100, 150, 200));
let bytes = jpeg::encode(&img, &jpeg::JpegEncodeOptions::default()).unwrap();
let decoded = load(&bytes).unwrap();
match decoded {
DecodedImage::Jpeg(d) => match &d.image {
jpeg::JpegImage::Srgb8(img) => {
assert_eq!(img.width(), 4);
assert_eq!(img.height(), 4);
}
other => panic!("expected Srgb8, got {:?}", other),
},
#[allow(unreachable_patterns)]
_ => panic!("expected DecodedImage::Jpeg"),
}
}
#[cfg(feature = "jpeg")]
#[test]
fn load_reader_jpeg_dispatches_correctly() {
use fovea::image::Image;
use fovea::pixel::SrgbMono8;
let img = Image::fill(8, 6, SrgbMono8::new(128));
let bytes = jpeg::encode(&img, &jpeg::JpegEncodeOptions::default()).unwrap();
let decoded = load_reader(std::io::Cursor::new(&bytes)).unwrap();
match decoded {
DecodedImage::Jpeg(d) => match &d.image {
jpeg::JpegImage::SrgbMono8(img) => {
assert_eq!(img.width(), 8);
assert_eq!(img.height(), 6);
}
other => panic!("expected SrgbMono8, got {:?}", other),
},
#[allow(unreachable_patterns)]
_ => panic!("expected DecodedImage::Jpeg"),
}
}
#[test]
fn load_unknown_format_returns_error() {
let result = load(&[0x00, 0x00, 0x00, 0x00]);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn load_empty_returns_error() {
let result = load(&[]);
assert!(matches!(result, Err(IoError::InvalidFormat { .. })));
}
#[test]
fn load_unsupported_format_returns_error() {
let tiff_le = [0x49, 0x49, 0x2A, 0x00];
let result = load(&tiff_le);
assert!(matches!(result, Err(crate::IoError::InvalidFormat { .. })));
}
#[cfg(feature = "jpeg")]
#[test]
fn load_jpeg_returns_metadata() {
use fovea::image::Image;
use fovea::pixel::Srgb8;
let img = Image::fill(2, 2, Srgb8::new(0, 0, 0));
let bytes = jpeg::encode(&img, &jpeg::JpegEncodeOptions::default()).unwrap();
let decoded = load(&bytes).unwrap();
match decoded {
DecodedImage::Jpeg(d) => {
assert_eq!(d.metadata.source_bit_depth, jpeg::JpegBitDepth::Eight);
assert_eq!(d.metadata.color_space, jpeg::JpegColorSpace::Srgb);
}
#[allow(unreachable_patterns)]
_ => panic!("expected DecodedImage::Jpeg"),
}
}
#[cfg(feature = "bmp")]
#[test]
fn load_bmp_dispatches_correctly() {
use fovea::image::Image;
use fovea::pixel::Srgb8;
let img = Image::fill(4, 4, Srgb8::new(100, 150, 200));
let bytes = bmp::encode(&img, &bmp::BmpEncodeOptions::default()).unwrap();
let decoded = load(&bytes).unwrap();
match decoded {
DecodedImage::Bmp(d) => match &d.image {
bmp::BmpImage::Srgb8(img) => {
assert_eq!(img.width(), 4);
assert_eq!(img.height(), 4);
}
other => panic!("expected Srgb8, got {:?}", other),
},
#[allow(unreachable_patterns)]
_ => panic!("expected DecodedImage::Bmp"),
}
}
#[cfg(feature = "bmp")]
#[test]
fn load_reader_bmp_dispatches_correctly() {
use fovea::image::Image;
use fovea::pixel::Srgb8;
let img = Image::fill(3, 2, Srgb8::new(42, 84, 126));
let bytes = bmp::encode(&img, &bmp::BmpEncodeOptions::default()).unwrap();
let decoded = load_reader(std::io::Cursor::new(&bytes)).unwrap();
match decoded {
DecodedImage::Bmp(d) => match &d.image {
bmp::BmpImage::Srgb8(img) => {
assert_eq!(img.width(), 3);
assert_eq!(img.height(), 2);
}
other => panic!("expected Srgb8, got {:?}", other),
},
#[allow(unreachable_patterns)]
_ => panic!("expected DecodedImage::Bmp"),
}
}
#[cfg(feature = "bmp")]
#[test]
fn load_bmp_returns_metadata() {
use fovea::image::Image;
use fovea::pixel::Srgb8;
let img = Image::fill(2, 2, Srgb8::new(0, 0, 0));
let bytes = bmp::encode(&img, &bmp::BmpEncodeOptions::default()).unwrap();
let decoded = load(&bytes).unwrap();
match decoded {
DecodedImage::Bmp(d) => {
assert_eq!(d.metadata.source_bit_depth, bmp::BmpBitDepth::TwentyFour);
assert_eq!(d.metadata.color_space, bmp::BmpColorSpace::Srgb);
assert_eq!(d.metadata.header_version, bmp::BmpHeaderVersion::Info);
assert_eq!(d.metadata.compression, bmp::BmpCompression::None);
}
#[allow(unreachable_patterns)]
_ => panic!("expected DecodedImage::Bmp"),
}
}
}