use crate::base::{Error, Result};
use crate::gfx::bitmap::Bitmap;
use crate::gfx::{jpeg, png};
const PNG_MAGIC: [u8; 8] = [0x89, b'P', b'N', b'G', b'\r', b'\n', 0x1A, b'\n'];
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ImageFormat {
Png,
Jpeg,
Gif,
}
pub fn sniff_format(bytes: &[u8]) -> Option<ImageFormat> {
if bytes.starts_with(&PNG_MAGIC) {
Some(ImageFormat::Png)
} else if bytes.starts_with(&crate::gfx::gif::SIGNATURE) {
Some(ImageFormat::Gif)
} else if bytes.starts_with(&[0xFF, 0xD8]) {
Some(ImageFormat::Jpeg)
} else {
None
}
}
fn video_container(bytes: &[u8]) -> Option<&'static str> {
if bytes.len() < 12 {
return None;
}
if &bytes[4..8] == b"ftyp" || &bytes[4..8] == b"moov" {
return Some("mp4/mov");
}
if &bytes[..4] == b"RIFF" && &bytes[8..12] == b"AVI " {
return Some("avi");
}
if bytes.starts_with(&[0x1A, 0x45, 0xDF, 0xA3]) {
return Some("webm/mkv");
}
if bytes.starts_with(&[0x00, 0x00, 0x01, 0xBA]) {
return Some("mpeg");
}
if bytes.len() > 376 && bytes[0] == 0x47 && bytes[188] == 0x47 && bytes[376] == 0x47 {
return Some("mpeg-ts");
}
None
}
pub fn decode_image(bytes: &[u8]) -> Result<Bitmap> {
if let Some(container) = video_container(bytes) {
return Err(Error::Parse(format!(
"{container}: video is not decoded (animated GIF and APNG are). \
Convert with: ffmpeg -i IN -vf 'fps=12,scale=480:-1' OUT.gif"
)));
}
match sniff_format(bytes) {
Some(ImageFormat::Png) => png::decode(bytes),
Some(ImageFormat::Jpeg) => jpeg::decode(bytes),
Some(ImageFormat::Gif) => crate::gfx::gif::decode(bytes).and_then(|a| {
a.frames
.into_iter()
.next()
.map(|f| f.image)
.ok_or_else(|| Error::Parse("gif: no frames".into()))
}),
None => Err(Error::Parse(format!(
"image: unrecognized format (magic {:02X?}); PNG, JPEG, and GIF decode, \
WebP/AVIF/TIFF do not",
&bytes[..bytes.len().min(4)]
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gfx::png_test_encoder::encode_rgba;
#[test]
fn routes_png_by_magic() {
let bmp = Bitmap::from_fn(3, 2, |x, y| {
crate::base::Rgba::rgb((x * 80) as u8, (y * 100) as u8, 7)
});
let png = encode_rgba(&bmp);
let out = decode_image(&png).unwrap();
assert_eq!((out.width(), out.height()), (3, 2));
assert_eq!(out.get(1, 1), bmp.get(1, 1));
}
#[test]
fn routes_jpeg_by_magic() {
let jpg = crate::gfx::jpeg_fixtures::GRAD444;
assert_eq!(sniff_format(jpg), Some(ImageFormat::Jpeg));
let out = decode_image(jpg).unwrap();
assert!(out.width() > 0 && out.height() > 0);
}
#[test]
fn video_containers_are_named_without_stealing_pictures() {
let mp4 = crate::gfx::anim_fixtures::H264_MP4;
let err = decode_image(mp4).unwrap_err().to_string();
assert!(err.contains("mp4/mov"), "{err}");
assert!(err.contains("ffmpeg -i"), "carries the fix: {err}");
let gif = crate::gfx::anim_fixtures::GIF;
assert_eq!(gif[0], 0x47, "the trap: GIF starts with the TS sync byte");
assert!(decode_image(gif).is_ok(), "a GIF must still decode");
let mut ts = vec![0u8; 400];
for at in [0usize, 188, 376] {
ts[at] = 0x47;
}
assert!(decode_image(&ts)
.unwrap_err()
.to_string()
.contains("mpeg-ts"));
}
#[test]
fn unknown_magic_rejects_by_name() {
let err = decode_image(b"RIFF\x24\x00\x00\x00WEBPVP8 ").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("unrecognized format"), "{msg}");
assert!(msg.contains("PNG"), "must name what DOES decode: {msg}");
let err = decode_image(b"").unwrap_err();
assert!(err.to_string().contains("unrecognized format"));
let err = decode_image(b"GIF89a....").unwrap_err().to_string();
assert!(err.starts_with("gif:") || err.contains("gif:"), "{err}");
}
#[test]
fn truncated_after_magic_fails_in_the_decoder_not_the_sniffer() {
let err = decode_image(&[0xFF, 0xD8, 0xFF]).unwrap_err();
assert!(!err.to_string().contains("unrecognized"), "{err}");
}
#[test]
fn decode_image_survives_truncation_and_marker_soup() {
let png = encode_rgba(&Bitmap::from_fn(9, 7, |x, y| {
crate::base::Rgba::rgb((x * 29) as u8, (y * 37) as u8, 128)
}));
let jpg = crate::gfx::jpeg_fixtures::GRAD420;
for src in [&png[..], jpg] {
for cut in 0..src.len().min(96) {
let _ = decode_image(&src[..cut]);
}
let mut cut = 96;
while cut < src.len() {
let _ = decode_image(&src[..cut]);
cut += 7;
}
}
let mut state = 0xDEADBEEFCAFEF00Du64;
let mut rng = move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
for case in 0..300 {
let len = 8 + (rng() % 300) as usize;
let mut bytes: Vec<u8> = (0..len).map(|_| rng() as u8).collect();
match case % 3 {
0 => bytes[..8].copy_from_slice(&PNG_MAGIC),
1 => {
bytes[0] = 0xFF;
bytes[1] = 0xD8;
}
_ => {}
}
let _ = decode_image(&bytes);
}
for (i, src) in [&png[..], jpg].into_iter().enumerate() {
for k in 0..200 {
let mut mutated = src.to_vec();
let pos = (rng() as usize) % mutated.len();
mutated[pos] ^= 1 << (rng() % 8);
let _ = decode_image(&mutated);
let _ = (i, k);
}
}
}
}