use alloc::borrow::Cow;
use alloc::vec::Vec;
pub(crate) mod builtins;
#[non_exhaustive]
pub struct ImageFormatDefinition {
pub name: &'static str,
pub image_format: Option<ImageFormat>,
pub display_name: &'static str,
pub preferred_extension: &'static str,
pub extensions: &'static [&'static str],
pub preferred_mime_type: &'static str,
pub mime_types: &'static [&'static str],
pub supports_alpha: bool,
pub supports_animation: bool,
pub supports_lossless: bool,
pub supports_lossy: bool,
pub magic_bytes_needed: usize,
pub detect: fn(&[u8]) -> bool,
}
impl ImageFormatDefinition {
#[allow(clippy::too_many_arguments)]
pub const fn new(
name: &'static str,
image_format: Option<ImageFormat>,
display_name: &'static str,
preferred_extension: &'static str,
extensions: &'static [&'static str],
preferred_mime_type: &'static str,
mime_types: &'static [&'static str],
supports_alpha: bool,
supports_animation: bool,
supports_lossless: bool,
supports_lossy: bool,
magic_bytes_needed: usize,
detect: fn(&[u8]) -> bool,
) -> Self {
Self {
name,
image_format,
display_name,
preferred_extension,
extensions,
preferred_mime_type,
mime_types,
supports_alpha,
supports_animation,
supports_lossless,
supports_lossy,
magic_bytes_needed,
detect,
}
}
pub fn to_image_format(&'static self) -> ImageFormat {
self.image_format.unwrap_or(ImageFormat::Custom(self))
}
}
impl PartialEq for ImageFormatDefinition {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl Eq for ImageFormatDefinition {}
impl core::hash::Hash for ImageFormatDefinition {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
impl core::fmt::Debug for ImageFormatDefinition {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ImageFormatDefinition")
.field("name", &self.name)
.field("display_name", &self.display_name)
.finish()
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ImageFormat {
Jpeg,
Png,
Gif,
WebP,
Avif,
Jxl,
Heic,
Bmp,
Tiff,
Ico,
Pnm,
Farbfeld,
Qoi,
Pdf,
Exr,
Hdr,
Tga,
Jp2,
Dng,
Raw,
Svg,
Unknown,
Custom(&'static ImageFormatDefinition),
}
impl ImageFormat {
pub fn definition(self) -> Option<&'static ImageFormatDefinition> {
match self {
ImageFormat::Jpeg => Some(&builtins::JPEG),
ImageFormat::Png => Some(&builtins::PNG),
ImageFormat::Gif => Some(&builtins::GIF),
ImageFormat::WebP => Some(&builtins::WEBP),
ImageFormat::Avif => Some(&builtins::AVIF),
ImageFormat::Jxl => Some(&builtins::JXL),
ImageFormat::Heic => Some(&builtins::HEIC),
ImageFormat::Bmp => Some(&builtins::BMP),
ImageFormat::Tiff => Some(&builtins::TIFF),
ImageFormat::Ico => Some(&builtins::ICO),
ImageFormat::Pnm => Some(&builtins::PNM),
ImageFormat::Farbfeld => Some(&builtins::FARBFELD),
ImageFormat::Qoi => Some(&builtins::QOI),
ImageFormat::Pdf => Some(&builtins::PDF),
ImageFormat::Exr => Some(&builtins::EXR),
ImageFormat::Hdr => Some(&builtins::HDR),
ImageFormat::Tga => Some(&builtins::TGA),
ImageFormat::Jp2 => Some(&builtins::JP2),
ImageFormat::Dng => Some(&builtins::DNG),
ImageFormat::Raw => Some(&builtins::RAW),
ImageFormat::Svg => Some(&builtins::SVG),
ImageFormat::Custom(def) => Some(def),
ImageFormat::Unknown => None,
}
}
pub fn mime_type(self) -> &'static str {
self.definition()
.map_or("application/octet-stream", |d| d.preferred_mime_type)
}
pub fn mime_types(self) -> &'static [&'static str] {
self.definition().map_or(&[], |d| d.mime_types)
}
pub fn extension(self) -> &'static str {
self.definition().map_or("bin", |d| d.preferred_extension)
}
pub fn extensions(self) -> &'static [&'static str] {
self.definition().map_or(&[], |d| d.extensions)
}
pub fn supports_lossy(self) -> bool {
self.definition().is_some_and(|d| d.supports_lossy)
}
pub fn supports_lossless(self) -> bool {
self.definition().is_some_and(|d| d.supports_lossless)
}
pub fn supports_animation(self) -> bool {
self.definition().is_some_and(|d| d.supports_animation)
}
pub fn supports_alpha(self) -> bool {
self.definition().is_some_and(|d| d.supports_alpha)
}
pub const RECOMMENDED_PROBE_BYTES: usize = 4096;
pub fn magic_bytes_needed(self) -> usize {
self.definition().map_or(0, |d| d.magic_bytes_needed)
}
}
impl core::fmt::Display for ImageFormat {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self.definition() {
Some(def) => f.write_str(def.display_name),
None => f.write_str("Unknown"),
}
}
}
#[derive(Clone, Debug)]
pub struct ImageFormatRegistry {
formats: Cow<'static, [&'static ImageFormatDefinition]>,
}
impl ImageFormatRegistry {
pub fn common() -> Self {
Self {
formats: Cow::Borrowed(builtins::ALL),
}
}
pub fn from_static(defs: &'static [&'static ImageFormatDefinition]) -> Self {
Self {
formats: Cow::Borrowed(defs),
}
}
pub fn from_vec(defs: Vec<&'static ImageFormatDefinition>) -> Self {
Self {
formats: Cow::Owned(defs),
}
}
pub fn formats(&self) -> &[&'static ImageFormatDefinition] {
&self.formats
}
pub fn detect(&self, data: &[u8]) -> Option<ImageFormat> {
for def in self.formats.iter() {
if (def.detect)(data) {
return Some(def.image_format.unwrap_or(ImageFormat::Custom(def)));
}
}
None
}
pub fn from_extension(&self, ext: &str) -> Option<ImageFormat> {
let ext_bytes = ext.as_bytes();
for def in self.formats.iter() {
for &def_ext in def.extensions {
if ext_bytes.len() == def_ext.len()
&& ext_bytes
.iter()
.zip(def_ext.as_bytes())
.all(|(&a, &b)| a.to_ascii_lowercase() == b)
{
return Some(def.image_format.unwrap_or(ImageFormat::Custom(def)));
}
}
}
None
}
pub fn from_mime_type(&self, mime: &str) -> Option<ImageFormat> {
for def in self.formats.iter() {
for &def_mime in def.mime_types {
if mime.eq_ignore_ascii_case(def_mime) {
return Some(def.image_format.unwrap_or(ImageFormat::Custom(def)));
}
}
}
None
}
}
impl Default for ImageFormatRegistry {
fn default() -> Self {
Self::common()
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
fn reg() -> ImageFormatRegistry {
ImageFormatRegistry::common()
}
#[test]
fn detect_jpeg() {
assert_eq!(
reg().detect(&[0xFF, 0xD8, 0xFF, 0xE0]),
Some(ImageFormat::Jpeg)
);
}
#[test]
fn detect_png() {
assert_eq!(
reg().detect(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
Some(ImageFormat::Png)
);
}
#[test]
fn detect_gif() {
assert_eq!(reg().detect(b"GIF89a\x00\x00"), Some(ImageFormat::Gif));
}
#[test]
fn detect_webp() {
assert_eq!(
reg().detect(b"RIFF\x00\x00\x00\x00WEBP"),
Some(ImageFormat::WebP)
);
}
#[test]
fn detect_avif() {
assert_eq!(
reg().detect(b"\x00\x00\x00\x18ftypavif"),
Some(ImageFormat::Avif)
);
}
#[test]
fn detect_jxl_codestream() {
assert_eq!(reg().detect(&[0xFF, 0x0A]), Some(ImageFormat::Jxl));
}
#[test]
fn detect_jxl_container() {
assert_eq!(
reg().detect(&[
0x00, 0x00, 0x00, 0x0C, b'J', b'X', b'L', b' ', 0x0D, 0x0A, 0x87, 0x0A
]),
Some(ImageFormat::Jxl)
);
}
#[test]
fn detect_unknown() {
assert_eq!(reg().detect(b"nope"), None);
assert_eq!(reg().detect(&[]), None);
}
#[test]
fn from_extension_case_insensitive() {
assert_eq!(reg().from_extension("JPG"), Some(ImageFormat::Jpeg));
assert_eq!(reg().from_extension("WebP"), Some(ImageFormat::WebP));
assert_eq!(reg().from_extension("unknown"), None);
}
#[test]
fn mime_types_primary() {
assert_eq!(ImageFormat::Jpeg.mime_type(), "image/jpeg");
assert_eq!(ImageFormat::Jxl.mime_type(), "image/jxl");
}
#[test]
fn mime_types_all() {
assert_eq!(ImageFormat::Jpeg.mime_types(), &["image/jpeg"]);
assert!(ImageFormat::Heic.mime_types().contains(&"image/heif"));
assert!(ImageFormat::Heic.mime_types().contains(&"image/heic"));
}
#[test]
fn probe_constants() {
assert_eq!(ImageFormat::RECOMMENDED_PROBE_BYTES, 4096);
assert!(ImageFormat::Jpeg.magic_bytes_needed() > ImageFormat::Png.magic_bytes_needed());
}
#[test]
fn display_format() {
assert_eq!(alloc::format!("{}", ImageFormat::Jpeg), "JPEG");
assert_eq!(alloc::format!("{}", ImageFormat::WebP), "WebP");
assert_eq!(alloc::format!("{}", ImageFormat::Gif), "GIF");
assert_eq!(alloc::format!("{}", ImageFormat::Png), "PNG");
assert_eq!(alloc::format!("{}", ImageFormat::Avif), "AVIF");
assert_eq!(alloc::format!("{}", ImageFormat::Jxl), "JPEG XL");
}
#[test]
fn from_extension_all_variants() {
assert_eq!(reg().from_extension("jpg"), Some(ImageFormat::Jpeg));
assert_eq!(reg().from_extension("jpeg"), Some(ImageFormat::Jpeg));
assert_eq!(reg().from_extension("jpe"), Some(ImageFormat::Jpeg));
assert_eq!(reg().from_extension("jfif"), Some(ImageFormat::Jpeg));
assert_eq!(reg().from_extension("JPEG"), Some(ImageFormat::Jpeg));
assert_eq!(reg().from_extension("webp"), Some(ImageFormat::WebP));
assert_eq!(reg().from_extension("gif"), Some(ImageFormat::Gif));
assert_eq!(reg().from_extension("png"), Some(ImageFormat::Png));
assert_eq!(reg().from_extension("avif"), Some(ImageFormat::Avif));
assert_eq!(reg().from_extension("jxl"), Some(ImageFormat::Jxl));
}
#[test]
fn from_extension_edge_cases() {
assert_eq!(reg().from_extension(""), None);
assert_eq!(reg().from_extension("tiff"), Some(ImageFormat::Tiff));
assert_eq!(reg().from_extension("very_long_extension"), None);
}
#[test]
fn capabilities() {
assert!(ImageFormat::Jpeg.supports_lossy());
assert!(!ImageFormat::Jpeg.supports_lossless());
assert!(!ImageFormat::Jpeg.supports_animation());
assert!(!ImageFormat::Jpeg.supports_alpha());
assert!(ImageFormat::Png.supports_lossless());
assert!(!ImageFormat::Png.supports_lossy());
assert!(ImageFormat::Png.supports_alpha());
assert!(ImageFormat::Png.supports_animation());
assert!(ImageFormat::WebP.supports_lossy());
assert!(ImageFormat::WebP.supports_lossless());
assert!(ImageFormat::WebP.supports_animation());
assert!(ImageFormat::WebP.supports_alpha());
assert!(ImageFormat::Gif.supports_animation());
assert!(ImageFormat::Gif.supports_lossless());
assert!(ImageFormat::Gif.supports_alpha());
assert!(ImageFormat::Jxl.supports_lossy());
assert!(ImageFormat::Jxl.supports_lossless());
assert!(ImageFormat::Jxl.supports_animation());
}
#[test]
fn extensions() {
assert!(ImageFormat::Jpeg.extensions().contains(&"jpg"));
assert!(ImageFormat::Jpeg.extensions().contains(&"jpeg"));
assert_eq!(ImageFormat::Png.extensions(), &["png"]);
}
#[test]
fn detect_pnm_p5() {
assert_eq!(reg().detect(b"P5\n3 2\n255\n"), Some(ImageFormat::Pnm));
}
#[test]
fn detect_pnm_p6() {
assert_eq!(reg().detect(b"P6\n3 2\n255\n"), Some(ImageFormat::Pnm));
}
#[test]
fn detect_pnm_p7() {
assert_eq!(reg().detect(b"P7\nWIDTH 2\n"), Some(ImageFormat::Pnm));
}
#[test]
fn detect_pnm_pfm_color() {
assert_eq!(reg().detect(b"PF\n3 2\n"), Some(ImageFormat::Pnm));
}
#[test]
fn detect_pnm_pfm_gray() {
assert_eq!(reg().detect(b"Pf\n3 2\n"), Some(ImageFormat::Pnm));
}
#[test]
fn detect_pnm_p1_ascii() {
assert_eq!(reg().detect(b"P1\n3 2\n"), Some(ImageFormat::Pnm));
}
#[test]
fn from_extension_pnm_variants() {
assert_eq!(reg().from_extension("pnm"), Some(ImageFormat::Pnm));
assert_eq!(reg().from_extension("ppm"), Some(ImageFormat::Pnm));
assert_eq!(reg().from_extension("pgm"), Some(ImageFormat::Pnm));
assert_eq!(reg().from_extension("pbm"), Some(ImageFormat::Pnm));
assert_eq!(reg().from_extension("pam"), Some(ImageFormat::Pnm));
assert_eq!(reg().from_extension("pfm"), Some(ImageFormat::Pnm));
assert_eq!(reg().from_extension("PNM"), Some(ImageFormat::Pnm));
}
#[test]
fn pnm_capabilities() {
assert!(!ImageFormat::Pnm.supports_lossy());
assert!(ImageFormat::Pnm.supports_lossless());
assert!(!ImageFormat::Pnm.supports_animation());
assert!(ImageFormat::Pnm.supports_alpha());
}
#[test]
fn pnm_mime_type() {
assert_eq!(ImageFormat::Pnm.mime_type(), "image/x-portable-anymap");
}
#[test]
fn pnm_extensions() {
let exts = ImageFormat::Pnm.extensions();
assert!(exts.contains(&"pnm"));
assert!(exts.contains(&"ppm"));
assert!(exts.contains(&"pgm"));
assert!(exts.contains(&"pbm"));
assert!(exts.contains(&"pam"));
assert!(exts.contains(&"pfm"));
}
#[test]
fn pnm_display() {
assert_eq!(alloc::format!("{}", ImageFormat::Pnm), "PNM");
}
#[test]
fn pnm_magic_bytes_needed() {
assert_eq!(ImageFormat::Pnm.magic_bytes_needed(), 20);
}
#[test]
fn detect_bmp() {
assert_eq!(reg().detect(b"BM\x00\x00"), Some(ImageFormat::Bmp));
}
#[test]
fn detect_farbfeld() {
assert_eq!(
reg().detect(b"farbfeld\x00\x00\x00\x01\x00\x00\x00\x01"),
Some(ImageFormat::Farbfeld)
);
}
#[test]
fn from_extension_bmp() {
assert_eq!(reg().from_extension("bmp"), Some(ImageFormat::Bmp));
assert_eq!(reg().from_extension("BMP"), Some(ImageFormat::Bmp));
}
#[test]
fn from_extension_farbfeld() {
assert_eq!(reg().from_extension("ff"), Some(ImageFormat::Farbfeld));
}
#[test]
fn bmp_capabilities() {
assert!(!ImageFormat::Bmp.supports_lossy());
assert!(ImageFormat::Bmp.supports_lossless());
assert!(!ImageFormat::Bmp.supports_animation());
assert!(ImageFormat::Bmp.supports_alpha());
}
#[test]
fn farbfeld_capabilities() {
assert!(!ImageFormat::Farbfeld.supports_lossy());
assert!(ImageFormat::Farbfeld.supports_lossless());
assert!(!ImageFormat::Farbfeld.supports_animation());
assert!(ImageFormat::Farbfeld.supports_alpha());
}
#[test]
fn bmp_display() {
assert_eq!(alloc::format!("{}", ImageFormat::Bmp), "BMP");
}
#[test]
fn farbfeld_display() {
assert_eq!(alloc::format!("{}", ImageFormat::Farbfeld), "farbfeld");
}
#[test]
fn bmp_mime_type() {
assert_eq!(ImageFormat::Bmp.mime_type(), "image/bmp");
}
#[test]
fn farbfeld_mime_type() {
assert_eq!(ImageFormat::Farbfeld.mime_type(), "image/x-farbfeld");
}
#[test]
fn bmp_extensions() {
assert_eq!(ImageFormat::Bmp.extensions(), &["bmp"]);
}
#[test]
fn farbfeld_extensions() {
assert_eq!(ImageFormat::Farbfeld.extensions(), &["ff"]);
}
#[test]
fn detect_heic() {
let mut data = vec![0u8; 20];
data[0..4].copy_from_slice(&20u32.to_be_bytes());
data[4..8].copy_from_slice(b"ftyp");
data[8..12].copy_from_slice(b"heic");
data[12..16].copy_from_slice(&[0, 0, 0, 0]);
assert_eq!(reg().detect(&data), Some(ImageFormat::Heic));
}
#[test]
fn detect_heic_heix_brand() {
let mut data = vec![0u8; 20];
data[0..4].copy_from_slice(&20u32.to_be_bytes());
data[4..8].copy_from_slice(b"ftyp");
data[8..12].copy_from_slice(b"heix");
data[12..16].copy_from_slice(&[0, 0, 0, 0]);
assert_eq!(reg().detect(&data), Some(ImageFormat::Heic));
}
#[test]
fn detect_heic_hevc_brand() {
let mut data = vec![0u8; 20];
data[0..4].copy_from_slice(&20u32.to_be_bytes());
data[4..8].copy_from_slice(b"ftyp");
data[8..12].copy_from_slice(b"hevc");
data[12..16].copy_from_slice(&[0, 0, 0, 0]);
assert_eq!(reg().detect(&data), Some(ImageFormat::Heic));
}
#[test]
fn detect_avif_still_works() {
let mut data = vec![0u8; 20];
data[0..4].copy_from_slice(&20u32.to_be_bytes());
data[4..8].copy_from_slice(b"ftyp");
data[8..12].copy_from_slice(b"avif");
data[12..16].copy_from_slice(&[0, 0, 0, 0]);
assert_eq!(reg().detect(&data), Some(ImageFormat::Avif));
data[8..12].copy_from_slice(b"avis");
assert_eq!(reg().detect(&data), Some(ImageFormat::Avif));
}
#[test]
fn detect_mif1_with_heic_compat() {
let mut data = vec![0u8; 24];
data[0..4].copy_from_slice(&24u32.to_be_bytes());
data[4..8].copy_from_slice(b"ftyp");
data[8..12].copy_from_slice(b"mif1");
data[12..16].copy_from_slice(&[0, 0, 0, 0]);
data[16..20].copy_from_slice(b"heic");
data[20..24].copy_from_slice(b"hevx");
assert_eq!(reg().detect(&data), Some(ImageFormat::Heic));
}
#[test]
fn detect_mif1_with_avif_compat() {
let mut data = vec![0u8; 24];
data[0..4].copy_from_slice(&24u32.to_be_bytes());
data[4..8].copy_from_slice(b"ftyp");
data[8..12].copy_from_slice(b"mif1");
data[12..16].copy_from_slice(&[0, 0, 0, 0]);
data[16..20].copy_from_slice(b"avif");
data[20..24].copy_from_slice(b"heic");
assert_eq!(reg().detect(&data), Some(ImageFormat::Avif));
}
#[test]
fn detect_mif1_no_known_compat() {
let mut data = vec![0u8; 20];
data[0..4].copy_from_slice(&20u32.to_be_bytes());
data[4..8].copy_from_slice(b"ftyp");
data[8..12].copy_from_slice(b"mif1");
data[12..16].copy_from_slice(&[0, 0, 0, 0]);
data[16..20].copy_from_slice(b"xxxx");
assert_eq!(reg().detect(&data), None);
}
#[test]
fn from_extension_heic() {
assert_eq!(reg().from_extension("heic"), Some(ImageFormat::Heic));
assert_eq!(reg().from_extension("heif"), Some(ImageFormat::Heic));
assert_eq!(reg().from_extension("hif"), Some(ImageFormat::Heic));
assert_eq!(reg().from_extension("HEIC"), Some(ImageFormat::Heic));
assert_eq!(reg().from_extension("HEIF"), Some(ImageFormat::Heic));
}
#[test]
fn heic_capabilities() {
assert!(ImageFormat::Heic.supports_lossy());
assert!(!ImageFormat::Heic.supports_lossless());
assert!(!ImageFormat::Heic.supports_animation());
assert!(ImageFormat::Heic.supports_alpha());
}
#[test]
fn heif_display() {
assert_eq!(alloc::format!("{}", ImageFormat::Heic), "HEIC");
}
#[test]
fn heif_mime_type() {
assert_eq!(ImageFormat::Heic.mime_type(), "image/heif");
}
#[test]
fn heic_extensions() {
let exts = ImageFormat::Heic.extensions();
assert!(exts.contains(&"heic"));
assert!(exts.contains(&"heif"));
assert!(exts.contains(&"hif"));
}
#[test]
fn heic_min_probe_bytes() {
assert_eq!(ImageFormat::Heic.magic_bytes_needed(), 512);
}
#[test]
fn detect_msf1_with_heic_compat() {
let mut data = vec![0u8; 24];
data[0..4].copy_from_slice(&24u32.to_be_bytes());
data[4..8].copy_from_slice(b"ftyp");
data[8..12].copy_from_slice(b"msf1");
data[12..16].copy_from_slice(&[0, 0, 0, 0]);
data[16..20].copy_from_slice(b"hevc");
data[20..24].copy_from_slice(b"heic");
assert_eq!(reg().detect(&data), Some(ImageFormat::Heic));
}
#[test]
fn detect_msf1_with_avif_compat() {
let mut data = vec![0u8; 24];
data[0..4].copy_from_slice(&24u32.to_be_bytes());
data[4..8].copy_from_slice(b"ftyp");
data[8..12].copy_from_slice(b"msf1");
data[12..16].copy_from_slice(&[0, 0, 0, 0]);
data[16..20].copy_from_slice(b"avis");
assert_eq!(reg().detect(&data), Some(ImageFormat::Avif));
}
fn detect_test_format(data: &[u8]) -> bool {
data.len() >= 4 && data[..4] == *b"TEST"
}
static TEST_FORMAT: ImageFormatDefinition = ImageFormatDefinition {
name: "testformat",
image_format: None,
display_name: "Test Format",
preferred_extension: "test",
extensions: &["test", "tst"],
preferred_mime_type: "image/x-test",
mime_types: &["image/x-test", "application/x-test"],
supports_alpha: true,
supports_animation: false,
supports_lossless: true,
supports_lossy: false,
magic_bytes_needed: 4,
detect: detect_test_format,
};
static TEST_FORMAT_2: ImageFormatDefinition = ImageFormatDefinition {
name: "testformat",
image_format: None,
display_name: "Test Format 2",
preferred_extension: "tf2",
extensions: &["tf2"],
preferred_mime_type: "image/x-test2",
mime_types: &["image/x-test2"],
supports_alpha: false,
supports_animation: false,
supports_lossless: false,
supports_lossy: false,
magic_bytes_needed: 0,
detect: |_| false,
};
#[test]
fn custom_format_metadata() {
let fmt = ImageFormat::Custom(&TEST_FORMAT);
assert_eq!(fmt.mime_type(), "image/x-test");
assert_eq!(fmt.mime_types(), &["image/x-test", "application/x-test"]);
assert_eq!(fmt.extension(), "test");
assert_eq!(fmt.extensions(), &["test", "tst"]);
assert!(fmt.supports_alpha());
assert!(!fmt.supports_animation());
assert!(fmt.supports_lossless());
assert!(!fmt.supports_lossy());
assert_eq!(fmt.magic_bytes_needed(), 4);
}
#[test]
fn custom_format_display() {
let fmt = ImageFormat::Custom(&TEST_FORMAT);
assert_eq!(alloc::format!("{fmt}"), "Test Format");
}
#[test]
fn custom_format_detect() {
assert!((TEST_FORMAT.detect)(b"TESTdata"));
assert!(!(TEST_FORMAT.detect)(b"NOPE"));
}
#[test]
fn custom_format_equality_by_name() {
let a = ImageFormat::Custom(&TEST_FORMAT);
let b = ImageFormat::Custom(&TEST_FORMAT_2);
assert_eq!(a, b);
static OTHER: ImageFormatDefinition = ImageFormatDefinition {
name: "other",
image_format: None,
display_name: "Other",
preferred_extension: "oth",
extensions: &["oth"],
preferred_mime_type: "image/x-other",
mime_types: &["image/x-other"],
supports_alpha: false,
supports_animation: false,
supports_lossless: false,
supports_lossy: false,
magic_bytes_needed: 0,
detect: |_| false,
};
assert_ne!(a, ImageFormat::Custom(&OTHER));
}
#[test]
fn custom_format_hash() {
use core::hash::{Hash, Hasher};
struct SimpleHasher(u64);
impl Hasher for SimpleHasher {
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, bytes: &[u8]) {
for &b in bytes {
self.0 = self.0.wrapping_mul(31).wrapping_add(b as u64);
}
}
}
fn hash_of(fmt: ImageFormat) -> u64 {
let mut hasher = SimpleHasher(0);
fmt.hash(&mut hasher);
hasher.finish()
}
assert_eq!(
hash_of(ImageFormat::Custom(&TEST_FORMAT)),
hash_of(ImageFormat::Custom(&TEST_FORMAT_2))
);
}
#[test]
fn custom_not_equal_to_builtin() {
let custom = ImageFormat::Custom(&TEST_FORMAT);
assert_ne!(custom, ImageFormat::Jpeg);
assert_ne!(custom, ImageFormat::Unknown);
}
#[test]
fn custom_format_is_copy() {
let a = ImageFormat::Custom(&TEST_FORMAT);
let b = a; assert_eq!(a, b);
}
#[test]
fn to_image_format_builtin() {
let fmt = builtins::JPEG.to_image_format();
assert_eq!(fmt, ImageFormat::Jpeg);
}
#[test]
fn to_image_format_custom() {
let fmt = TEST_FORMAT.to_image_format();
assert_eq!(fmt, ImageFormat::Custom(&TEST_FORMAT));
}
#[test]
fn from_mime_type_builtin() {
assert_eq!(reg().from_mime_type("image/jpeg"), Some(ImageFormat::Jpeg));
assert_eq!(reg().from_mime_type("image/heif"), Some(ImageFormat::Heic));
assert_eq!(reg().from_mime_type("image/heic"), Some(ImageFormat::Heic));
assert_eq!(reg().from_mime_type("video/mp4"), None);
}
#[test]
fn registry_common_detect() {
let reg = ImageFormatRegistry::common();
assert_eq!(
reg.detect(&[0xFF, 0xD8, 0xFF, 0xE0]),
Some(ImageFormat::Jpeg)
);
assert_eq!(
reg.detect(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
Some(ImageFormat::Png)
);
assert_eq!(reg.detect(b"nope"), None);
}
#[test]
fn registry_common_from_extension() {
let reg = ImageFormatRegistry::common();
assert_eq!(reg.from_extension("jpg"), Some(ImageFormat::Jpeg));
assert_eq!(reg.from_extension("PNG"), Some(ImageFormat::Png));
assert_eq!(reg.from_extension("unknown"), None);
}
#[test]
fn registry_common_from_mime_type() {
let reg = ImageFormatRegistry::common();
assert_eq!(reg.from_mime_type("image/jpeg"), Some(ImageFormat::Jpeg));
assert_eq!(reg.from_mime_type("image/webp"), Some(ImageFormat::WebP));
assert_eq!(reg.from_mime_type("video/mp4"), None);
}
fn reg_with_test_format() -> ImageFormatRegistry {
let mut defs: Vec<&'static ImageFormatDefinition> = builtins::ALL.to_vec();
defs.push(&TEST_FORMAT);
ImageFormatRegistry::from_vec(defs)
}
#[test]
fn registry_from_vec_custom() {
let reg = reg_with_test_format();
assert_eq!(
reg.detect(b"TESTdata"),
Some(ImageFormat::Custom(&TEST_FORMAT))
);
assert_eq!(reg.detect(&[0xFF, 0xD8, 0xFF]), Some(ImageFormat::Jpeg));
}
#[test]
fn registry_from_vec_custom_extension() {
let reg = reg_with_test_format();
assert_eq!(
reg.from_extension("test"),
Some(ImageFormat::Custom(&TEST_FORMAT))
);
assert_eq!(
reg.from_extension("TST"),
Some(ImageFormat::Custom(&TEST_FORMAT))
);
assert_eq!(reg.from_extension("jpg"), Some(ImageFormat::Jpeg));
}
#[test]
fn registry_from_vec_custom_mime_type() {
let reg = reg_with_test_format();
assert_eq!(
reg.from_mime_type("image/x-test"),
Some(ImageFormat::Custom(&TEST_FORMAT))
);
assert_eq!(
reg.from_mime_type("application/x-test"),
Some(ImageFormat::Custom(&TEST_FORMAT))
);
}
#[test]
fn registry_from_static() {
static DEFS: &[&ImageFormatDefinition] = &[&builtins::PNG, &builtins::JPEG];
let reg = ImageFormatRegistry::from_static(DEFS);
assert_eq!(reg.formats().len(), 2);
assert_eq!(
reg.detect(&[0xFF, 0xD8, 0xFF, 0xE0]),
Some(ImageFormat::Jpeg)
);
assert_eq!(reg.detect(b"GIF89a\x00\x00"), None); }
#[test]
fn registry_from_static_custom_only() {
static DEFS: &[&ImageFormatDefinition] = &[&TEST_FORMAT];
let reg = ImageFormatRegistry::from_static(DEFS);
assert_eq!(
reg.detect(b"TESTdata"),
Some(ImageFormat::Custom(&TEST_FORMAT))
);
assert_eq!(reg.detect(&[0xFF, 0xD8, 0xFF]), None); assert_eq!(reg.formats().len(), 1);
}
#[test]
fn registry_formats_list() {
let reg = ImageFormatRegistry::common();
assert_eq!(reg.formats().len(), 21);
assert_eq!(reg.formats()[0].name, "jpeg");
}
#[test]
fn registry_default_is_common() {
let def = ImageFormatRegistry::default();
let com = ImageFormatRegistry::common();
assert_eq!(def.formats().len(), com.formats().len());
}
#[test]
fn registry_new_from_vec() {
let reg = ImageFormatRegistry::from_vec(vec![&builtins::PNG, &builtins::JPEG]);
assert_eq!(reg.formats().len(), 2);
assert_eq!(
reg.detect(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
Some(ImageFormat::Png)
);
assert_eq!(reg.detect(&[0xFF, 0xD8, 0xFF]), Some(ImageFormat::Jpeg));
assert_eq!(reg.detect(b"GIF89a\x00\x00"), None);
}
fn build_ftyp(box_size: u32, major: &[u8; 4], compat: &[&[u8; 4]]) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&box_size.to_be_bytes());
data.extend_from_slice(b"ftyp");
data.extend_from_slice(major);
data.extend_from_slice(&[0u8; 4]); for brand in compat {
data.extend_from_slice(*brand);
}
data
}
#[test]
fn scan_compat_brands_normal_box() {
let data = build_ftyp(24, b"mif1", &[b"avif"]);
assert_eq!(reg().detect(&data), Some(ImageFormat::Avif));
}
#[test]
fn scan_compat_brands_box_size_zero() {
let mut data = build_ftyp(0, b"mif1", &[b"avif"]);
data.extend_from_slice(&[0u8; 8]); assert_eq!(reg().detect(&data), Some(ImageFormat::Avif));
}
#[test]
fn scan_compat_brands_box_size_zero_no_brands() {
let data = build_ftyp(0, b"mif1", &[]);
assert_eq!(reg().detect(&data), None);
}
#[test]
fn scan_compat_brands_box_size_one_extended() {
let mut data = Vec::new();
data.extend_from_slice(&1u32.to_be_bytes()); data.extend_from_slice(b"ftyp");
let mut ext = [0u8; 8];
ext[0..4].copy_from_slice(b"mif1");
ext[4..8].copy_from_slice(&36u32.to_be_bytes());
data.extend_from_slice(&ext);
data.extend_from_slice(b"mif1"); data.extend_from_slice(&[0u8; 4]); data.extend_from_slice(b"avif"); data.resize(36, 0); assert_eq!(reg().detect(&data), Some(ImageFormat::Avif));
}
#[test]
fn scan_compat_brands_extended_too_short_for_header() {
let data = [
0x00, 0x00, 0x00, 0x01, b'f', b't', b'y', b'p', b'm', b'i', b'f', b'1',
];
assert_eq!(reg().detect(&data), None);
}
#[test]
fn scan_compat_brands_box_smaller_than_16() {
let mut data = vec![0u8; 20];
data[0..4].copy_from_slice(&8u32.to_be_bytes());
data[4..8].copy_from_slice(b"ftyp");
data[8..12].copy_from_slice(b"mif1");
data[12..16].copy_from_slice(&[0u8; 4]);
data[16..20].copy_from_slice(b"avif"); assert_eq!(reg().detect(&data), None);
}
#[test]
fn scan_compat_brands_multiple_brands_last_match() {
let data = build_ftyp(36, b"mif1", &[b"isom", b"iso2", b"mp41", b"avif"]);
assert_eq!(reg().detect(&data), Some(ImageFormat::Avif));
}
#[test]
fn scan_compat_brands_truncated_box() {
let data = build_ftyp(100, b"mif1", &[b"avif"]);
assert_eq!(data.len(), 20); assert_eq!(reg().detect(&data), Some(ImageFormat::Avif));
}
fn build_tiff_le(tag: u16) -> Vec<u8> {
let mut d = vec![0u8; 22];
d[0] = b'I';
d[1] = b'I';
d[2] = 42;
d[3] = 0; d[4..8].copy_from_slice(&8u32.to_le_bytes()); d[8..10].copy_from_slice(&1u16.to_le_bytes()); d[10..12].copy_from_slice(&tag.to_le_bytes()); d
}
#[test]
fn detect_dng_by_version_tag() {
let data = build_tiff_le(0xC612);
assert_eq!(reg().detect(&data), Some(ImageFormat::Dng));
}
#[test]
fn detect_dng_apple_signature() {
let mut data = vec![0u8; 24];
data[0] = b'M';
data[1] = b'M';
data[2] = 0;
data[3] = 42; data[4..8].copy_from_slice(&16u32.to_be_bytes()); data[8..16].copy_from_slice(b"APPLEDNG");
assert_eq!(reg().detect(&data), Some(ImageFormat::Dng));
}
#[test]
fn detect_tiff_not_dng() {
let data = build_tiff_le(0x0100); assert_eq!(reg().detect(&data), Some(ImageFormat::Tiff));
}
#[test]
fn detect_raw_cr2() {
let mut d = vec![0u8; 22];
d[0] = b'I';
d[1] = b'I';
d[2] = 42;
d[3] = 0;
d[4..8].copy_from_slice(&16u32.to_le_bytes()); d[8] = b'C';
d[9] = b'R'; d[16..18].copy_from_slice(&0u16.to_le_bytes()); assert_eq!(reg().detect(&d), Some(ImageFormat::Raw));
}
#[test]
fn detect_raw_cr3() {
let mut d = vec![0u8; 16];
d[0..4].copy_from_slice(&16u32.to_be_bytes());
d[4..8].copy_from_slice(b"ftyp");
d[8..12].copy_from_slice(b"crx ");
assert_eq!(reg().detect(&d), Some(ImageFormat::Raw));
}
#[test]
fn detect_raw_raf() {
assert_eq!(
reg().detect(b"FUJIFILM\x00\x00\x00\x00"),
Some(ImageFormat::Raw)
);
}
#[test]
fn detect_raw_rw2() {
assert_eq!(
reg().detect(&[b'I', b'I', 0x55, 0x00, 0, 0, 0, 0]),
Some(ImageFormat::Raw)
);
}
#[test]
fn detect_raw_orf() {
assert_eq!(
reg().detect(&[b'I', b'I', 0x52, 0x4F, 0, 0, 0, 0]),
Some(ImageFormat::Raw)
);
}
#[test]
fn detect_svg_tag() {
assert_eq!(reg().detect(b"<svg xmlns="), Some(ImageFormat::Svg));
}
#[test]
fn detect_svg_xml_decl() {
assert_eq!(
reg().detect(b"<?xml version=\"1.0\"?><svg"),
Some(ImageFormat::Svg)
);
}
#[test]
fn detect_svg_with_bom() {
let mut d = vec![0xEF, 0xBB, 0xBF]; d.extend_from_slice(b"<svg>");
assert_eq!(reg().detect(&d), Some(ImageFormat::Svg));
}
#[test]
fn detect_svg_doctype() {
assert_eq!(
reg().detect(b"<!DOCTYPE svg PUBLIC"),
Some(ImageFormat::Svg)
);
}
#[test]
fn detect_svg_not_xml() {
assert_eq!(reg().detect(b"<?xml version=\"1.0\"?><html>"), None);
}
#[test]
fn detect_jp2_container() {
assert_eq!(
reg().detect(b"\x00\x00\x00\x0C\x6A\x50\x20\x20\x0D\x0A\x87\x0A"),
Some(ImageFormat::Jp2)
);
}
#[test]
fn detect_j2k_codestream() {
assert_eq!(
reg().detect(&[0xFF, 0x4F, 0xFF, 0x51]),
Some(ImageFormat::Jp2)
);
}
#[test]
fn from_extension_new_formats() {
assert_eq!(reg().from_extension("dng"), Some(ImageFormat::Dng));
assert_eq!(reg().from_extension("cr2"), Some(ImageFormat::Raw));
assert_eq!(reg().from_extension("cr3"), Some(ImageFormat::Raw));
assert_eq!(reg().from_extension("nef"), Some(ImageFormat::Raw));
assert_eq!(reg().from_extension("arw"), Some(ImageFormat::Raw));
assert_eq!(reg().from_extension("raf"), Some(ImageFormat::Raw));
assert_eq!(reg().from_extension("svg"), Some(ImageFormat::Svg));
assert_eq!(reg().from_extension("svgz"), Some(ImageFormat::Svg));
assert_eq!(reg().from_extension("jp2"), Some(ImageFormat::Jp2));
assert_eq!(reg().from_extension("j2k"), Some(ImageFormat::Jp2));
}
#[test]
fn new_format_metadata() {
assert_eq!(ImageFormat::Dng.mime_type(), "image/x-adobe-dng");
assert_eq!(ImageFormat::Raw.mime_type(), "image/x-raw");
assert_eq!(ImageFormat::Svg.mime_type(), "image/svg+xml");
assert_eq!(ImageFormat::Jp2.mime_type(), "image/jp2");
assert_eq!(alloc::format!("{}", ImageFormat::Dng), "Digital Negative");
assert_eq!(alloc::format!("{}", ImageFormat::Raw), "Camera RAW");
assert_eq!(alloc::format!("{}", ImageFormat::Svg), "SVG");
assert_eq!(alloc::format!("{}", ImageFormat::Jp2), "JPEG 2000");
}
#[test]
fn new_format_capabilities() {
assert!(!ImageFormat::Dng.supports_animation());
assert!(ImageFormat::Dng.supports_lossless());
assert!(ImageFormat::Dng.supports_lossy());
assert!(ImageFormat::Svg.supports_alpha());
assert!(ImageFormat::Svg.supports_lossless());
assert!(!ImageFormat::Svg.supports_lossy());
}
#[test]
fn dng_before_tiff_priority() {
let data = build_tiff_le(0xC612);
assert_eq!(reg().detect(&data), Some(ImageFormat::Dng));
}
}