use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Format {
Jpeg,
Png,
WebP,
Avif,
Jxl,
Qoi,
}
impl Format {
pub fn from_extension(path: &Path) -> Option<Self> {
let ext = path.extension()?.to_str()?.to_ascii_lowercase();
match ext.as_str() {
"jpg" | "jpeg" => Some(Self::Jpeg),
"png" => Some(Self::Png),
"webp" => Some(Self::WebP),
"avif" => Some(Self::Avif),
"jxl" => Some(Self::Jxl),
"qoi" => Some(Self::Qoi),
_ => None,
}
}
pub fn from_magic_bytes(data: &[u8]) -> Option<Self> {
if data.len() >= 3 && data[..3] == [0xFF, 0xD8, 0xFF] {
return Some(Self::Jpeg);
}
if data.len() >= 4 && data[..4] == [0x89, 0x50, 0x4E, 0x47] {
return Some(Self::Png);
}
if data.len() >= 12 && &data[0..4] == b"RIFF" && &data[8..12] == b"WEBP" {
return Some(Self::WebP);
}
if data.len() >= 12 && &data[4..8] == b"ftyp" {
let brand = &data[8..12];
if brand.starts_with(b"avif") || brand.starts_with(b"avis") {
return Some(Self::Avif);
}
}
if data.len() >= 2 && data[..2] == [0xFF, 0x0A] {
return Some(Self::Jxl);
}
if data.len() >= 8 && data[..4] == [0x00, 0x00, 0x00, 0x0C] && &data[4..8] == b"JXL " {
return Some(Self::Jxl);
}
if data.len() >= 4 && &data[..4] == b"qoif" {
return Some(Self::Qoi);
}
None
}
pub fn extension(&self) -> &'static str {
match self {
Self::Jpeg => "jpg",
Self::Png => "png",
Self::WebP => "webp",
Self::Avif => "avif",
Self::Jxl => "jxl",
Self::Qoi => "qoi",
}
}
pub fn can_encode(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn extension_jpg() {
assert_eq!(
Format::from_extension(Path::new("photo.jpg")),
Some(Format::Jpeg)
);
}
#[test]
fn extension_jpeg() {
assert_eq!(
Format::from_extension(Path::new("photo.jpeg")),
Some(Format::Jpeg)
);
}
#[test]
fn extension_jpg_uppercase() {
assert_eq!(
Format::from_extension(Path::new("photo.JPG")),
Some(Format::Jpeg)
);
}
#[test]
fn extension_jpeg_mixed_case() {
assert_eq!(
Format::from_extension(Path::new("photo.JpEg")),
Some(Format::Jpeg)
);
}
#[test]
fn extension_png() {
assert_eq!(
Format::from_extension(Path::new("image.png")),
Some(Format::Png)
);
}
#[test]
fn extension_png_uppercase() {
assert_eq!(
Format::from_extension(Path::new("image.PNG")),
Some(Format::Png)
);
}
#[test]
fn extension_webp() {
assert_eq!(
Format::from_extension(Path::new("image.webp")),
Some(Format::WebP)
);
}
#[test]
fn extension_avif() {
assert_eq!(
Format::from_extension(Path::new("image.avif")),
Some(Format::Avif)
);
}
#[test]
fn extension_jxl() {
assert_eq!(
Format::from_extension(Path::new("image.jxl")),
Some(Format::Jxl)
);
}
#[test]
fn extension_qoi() {
assert_eq!(
Format::from_extension(Path::new("image.qoi")),
Some(Format::Qoi)
);
}
#[test]
fn extension_unknown() {
assert_eq!(Format::from_extension(Path::new("file.bmp")), None);
}
#[test]
fn extension_none() {
assert_eq!(Format::from_extension(Path::new("noext")), None);
}
#[test]
fn extension_empty_path() {
assert_eq!(Format::from_extension(&PathBuf::new()), None);
}
#[test]
fn magic_jpeg() {
let data = [0xFF, 0xD8, 0xFF, 0xE0, 0x00];
assert_eq!(Format::from_magic_bytes(&data), Some(Format::Jpeg));
}
#[test]
fn magic_png() {
let data = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
assert_eq!(Format::from_magic_bytes(&data), Some(Format::Png));
}
#[test]
fn magic_webp() {
let mut data = Vec::new();
data.extend_from_slice(b"RIFF");
data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); data.extend_from_slice(b"WEBP");
assert_eq!(Format::from_magic_bytes(&data), Some(Format::WebP));
}
#[test]
fn magic_avif_avif_brand() {
let mut data = vec![0x00, 0x00, 0x00, 0x20]; data.extend_from_slice(b"ftyp");
data.extend_from_slice(b"avif");
assert_eq!(Format::from_magic_bytes(&data), Some(Format::Avif));
}
#[test]
fn magic_avif_avis_brand() {
let mut data = vec![0x00, 0x00, 0x00, 0x20]; data.extend_from_slice(b"ftyp");
data.extend_from_slice(b"avis");
assert_eq!(Format::from_magic_bytes(&data), Some(Format::Avif));
}
#[test]
fn magic_jxl_bare_codestream() {
let data = [0xFF, 0x0A, 0x00, 0x00];
assert_eq!(Format::from_magic_bytes(&data), Some(Format::Jxl));
}
#[test]
fn magic_jxl_container() {
let mut data = vec![0x00, 0x00, 0x00, 0x0C];
data.extend_from_slice(b"JXL ");
data.extend_from_slice(&[0x0D, 0x0A, 0x87, 0x0A]);
assert_eq!(Format::from_magic_bytes(&data), Some(Format::Jxl));
}
#[test]
fn magic_qoi() {
let mut data = Vec::new();
data.extend_from_slice(b"qoif");
data.extend_from_slice(&[0x00; 10]);
assert_eq!(Format::from_magic_bytes(&data), Some(Format::Qoi));
}
#[test]
fn magic_unknown() {
let data = [0x00, 0x00, 0x00, 0x00];
assert_eq!(Format::from_magic_bytes(&data), None);
}
#[test]
fn magic_empty() {
assert_eq!(Format::from_magic_bytes(&[]), None);
}
#[test]
fn magic_too_short_for_jpeg() {
assert_eq!(Format::from_magic_bytes(&[0xFF, 0xD8]), None);
}
#[test]
fn extension_string() {
assert_eq!(Format::Jpeg.extension(), "jpg");
assert_eq!(Format::Png.extension(), "png");
assert_eq!(Format::WebP.extension(), "webp");
assert_eq!(Format::Avif.extension(), "avif");
assert_eq!(Format::Jxl.extension(), "jxl");
assert_eq!(Format::Qoi.extension(), "qoi");
}
#[test]
fn can_encode_all_formats() {
assert!(Format::Jpeg.can_encode());
assert!(Format::Png.can_encode());
assert!(Format::WebP.can_encode());
assert!(Format::Avif.can_encode());
assert!(Format::Jxl.can_encode());
assert!(Format::Qoi.can_encode());
}
}