1use crate::format::ImageFormat;
2
3const PNG_SIGNATURE: &[u8] = b"\x89PNG\r\n\x1a\n";
4const JPEG_SIGNATURE: &[u8] = &[0xFF, 0xD8, 0xFF];
5
6fn is_webp(bytes: &[u8]) -> bool {
7 bytes.len() >= 12 && &bytes[..4] == b"RIFF" && &bytes[8..12] == b"WEBP"
8}
9
10fn is_tiff(bytes: &[u8]) -> bool {
11 matches!(
12 bytes,
13 [0x49, 0x49, 0x2A, 0x00, ..]
14 | [0x4D, 0x4D, 0x00, 0x2A, ..]
15 | [0x49, 0x49, 0x2B, 0x00, ..]
16 | [0x4D, 0x4D, 0x00, 0x2B, ..]
17 )
18}
19
20fn is_avif(bytes: &[u8]) -> bool {
21 if bytes.len() < 16 || bytes.get(4..8) != Some(b"ftyp") {
22 return false;
23 }
24
25 let search_end = bytes.len().min(32);
26
27 bytes[8..search_end]
28 .chunks(4)
29 .any(|chunk| chunk == b"avif" || chunk == b"avis")
30}
31
32pub(crate) fn looks_like_svg(bytes: &[u8]) -> bool {
33 if bytes.is_empty() {
34 return false;
35 }
36
37 let sniff_len = bytes.len().min(512);
38 let text = String::from_utf8_lossy(&bytes[..sniff_len]);
39 let text = text.strip_prefix('\u{feff}').unwrap_or(&text);
40 let lowered = text.to_ascii_lowercase();
41 let trimmed = lowered.trim_start();
42
43 trimmed.starts_with("<svg") || (trimmed.starts_with("<?xml") && trimmed.contains("<svg"))
44}
45
46#[must_use]
48pub fn detect_image_format_from_bytes(bytes: &[u8]) -> ImageFormat {
49 if bytes.starts_with(PNG_SIGNATURE) {
50 return ImageFormat::Png;
51 }
52
53 if bytes.starts_with(JPEG_SIGNATURE) {
54 return ImageFormat::Jpeg;
55 }
56
57 if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") {
58 return ImageFormat::Gif;
59 }
60
61 if is_webp(bytes) {
62 return ImageFormat::Webp;
63 }
64
65 if bytes.starts_with(&[0x00, 0x00, 0x01, 0x00]) {
66 return ImageFormat::Ico;
67 }
68
69 if bytes.starts_with(b"BM") {
70 return ImageFormat::Bmp;
71 }
72
73 if is_tiff(bytes) {
74 return ImageFormat::Tiff;
75 }
76
77 if is_avif(bytes) {
78 return ImageFormat::Avif;
79 }
80
81 if looks_like_svg(bytes) {
82 return ImageFormat::Svg;
83 }
84
85 ImageFormat::Unknown
86}