1#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub enum ImageFormat {
8 Png,
10 Jpeg,
12 Gif,
14 Webp,
16}
17
18pub fn format(bytes: &[u8]) -> Option<ImageFormat> {
20 if bytes.starts_with(b"\x89PNG\r\n\x1a\n") {
21 Some(ImageFormat::Png)
22 } else if bytes.starts_with(&[0xff, 0xd8]) {
23 Some(ImageFormat::Jpeg)
24 } else if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") {
25 Some(ImageFormat::Gif)
26 } else if bytes.starts_with(b"RIFF") && bytes.get(8..12) == Some(b"WEBP") {
27 Some(ImageFormat::Webp)
28 } else {
29 None
30 }
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub struct ImageDimensions {
36 pub width: u32,
38 pub height: u32,
40}
41
42pub fn dimensions(bytes: &[u8]) -> Option<ImageDimensions> {
47 match format(bytes)? {
48 ImageFormat::Png => png_dimensions(bytes),
49 ImageFormat::Jpeg => jpeg_dimensions(bytes),
50 ImageFormat::Gif => gif_dimensions(bytes),
51 ImageFormat::Webp => webp_dimensions(bytes),
52 }
53}
54
55fn image_dimensions(width: u32, height: u32) -> Option<ImageDimensions> {
56 (width != 0 && height != 0).then_some(ImageDimensions { width, height })
57}
58
59fn png_dimensions(bytes: &[u8]) -> Option<ImageDimensions> {
60 if bytes.get(12..16)? != b"IHDR" {
61 return None;
62 }
63 let width = u32::from_be_bytes(bytes.get(16..20)?.try_into().ok()?);
64 let height = u32::from_be_bytes(bytes.get(20..24)?.try_into().ok()?);
65 image_dimensions(width, height)
66}
67
68fn jpeg_dimensions(bytes: &[u8]) -> Option<ImageDimensions> {
69 let mut at = 2;
70 while at < bytes.len() {
71 if bytes[at] != 0xff {
72 at += 1;
73 continue;
74 }
75 while bytes.get(at) == Some(&0xff) {
77 at += 1;
78 }
79 let marker = *bytes.get(at)?;
80 at += 1;
81
82 match marker {
83 0x00 => continue,
86 0xd9 | 0xda => return None,
87 0x01 | 0xd0..=0xd8 => continue,
89 _ => {},
90 }
91
92 let length = usize::from(u16::from_be_bytes(bytes.get(at..at + 2)?.try_into().ok()?));
93 if length < 2 {
94 return None;
95 }
96 let end = at.checked_add(length)?;
97 if end > bytes.len() {
98 return None;
99 }
100
101 if matches!(marker, 0xc0..=0xcf) && !matches!(marker, 0xc4 | 0xc8 | 0xcc) {
103 if length < 7 {
104 return None;
105 }
106 let height = u32::from(u16::from_be_bytes(bytes.get(at + 3..at + 5)?.try_into().ok()?));
107 let width = u32::from(u16::from_be_bytes(bytes.get(at + 5..at + 7)?.try_into().ok()?));
108 return image_dimensions(width, height);
109 }
110 at = end;
111 }
112 None
113}
114
115fn gif_dimensions(bytes: &[u8]) -> Option<ImageDimensions> {
116 let width = u32::from(u16::from_le_bytes(bytes.get(6..8)?.try_into().ok()?));
117 let height = u32::from(u16::from_le_bytes(bytes.get(8..10)?.try_into().ok()?));
118 image_dimensions(width, height)
119}
120
121fn webp_dimensions(bytes: &[u8]) -> Option<ImageDimensions> {
122 if bytes.get(8..12)? != b"WEBP" {
123 return None;
124 }
125 match bytes.get(12..16)? {
126 b"VP8 " => {
127 let width = u32::from(u16::from_le_bytes(bytes.get(26..28)?.try_into().ok()?) & 0x3fff);
128 let height = u32::from(u16::from_le_bytes(bytes.get(28..30)?.try_into().ok()?) & 0x3fff);
129 image_dimensions(width, height)
130 },
131 b"VP8L" => {
132 let packed = u32::from_le_bytes(bytes.get(21..25)?.try_into().ok()?);
133 let width = (packed & 0x3fff) + 1;
134 let height = ((packed >> 14) & 0x3fff) + 1;
135 image_dimensions(width, height)
136 },
137 b"VP8X" => {
138 let packed = bytes.get(24..30)?;
139 let width = u32::from(packed[0]) | u32::from(packed[1]) << 8 | u32::from(packed[2]) << 16;
140 let height = u32::from(packed[3]) | u32::from(packed[4]) << 8 | u32::from(packed[5]) << 16;
141 image_dimensions(width + 1, height + 1)
142 },
143 _ => None,
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 fn assert_dimensions(bytes: &[u8], width: u32, height: u32) {
152 assert_eq!(dimensions(bytes), Some(ImageDimensions { width, height }));
153 }
154 #[test]
155 fn sniffs_supported_magic_bytes() {
156 assert_eq!(format(b"\x89PNG\r\n\x1a\n"), Some(ImageFormat::Png));
157 assert_eq!(format(&[0xff, 0xd8]), Some(ImageFormat::Jpeg));
158 assert_eq!(format(b"GIF89a"), Some(ImageFormat::Gif));
159 assert_eq!(format(b"RIFF\0\0\0\0WEBP"), Some(ImageFormat::Webp));
160 assert_eq!(format(b"RIFF\0\0\0\0WAVE"), None);
161 assert_eq!(format(b"garbage"), None);
162 }
163
164 #[test]
165 fn probes_png_ihdr() {
166 let mut bytes = b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR".to_vec();
167 bytes.extend_from_slice(&640_u32.to_be_bytes());
168 bytes.extend_from_slice(&480_u32.to_be_bytes());
169 assert_dimensions(&bytes, 640, 480);
170 }
171
172 #[test]
173 fn probes_jpeg_baseline_and_progressive_sof() {
174 for marker in [0xc0, 0xc2] {
175 let bytes = [
176 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x04, 0, 0, 0xff, marker, 0x00, 0x08, 8, 0x01, 0xe0,
177 0x02, 0x80, 1,
178 ];
179 assert_dimensions(&bytes, 640, 480);
180 }
181 }
182
183 #[test]
184 fn probes_gif87a_and_gif89a() {
185 for signature in [b"GIF87a", b"GIF89a"] {
186 let mut bytes = signature.to_vec();
187 bytes.extend_from_slice(&320_u16.to_le_bytes());
188 bytes.extend_from_slice(&200_u16.to_le_bytes());
189 assert_dimensions(&bytes, 320, 200);
190 }
191 }
192
193 #[test]
194 fn probes_webp_vp8() {
195 let mut bytes = vec![0; 30];
196 bytes[0..4].copy_from_slice(b"RIFF");
197 bytes[8..12].copy_from_slice(b"WEBP");
198 bytes[12..16].copy_from_slice(b"VP8 ");
199 bytes[26..28].copy_from_slice(&(0xc000_u16 | 0x03e8).to_le_bytes());
200 bytes[28..30].copy_from_slice(&(0x8000_u16 | 0x02bc).to_le_bytes());
201 assert_dimensions(&bytes, 1000, 700);
202 }
203
204 #[test]
205 fn probes_webp_vp8l() {
206 let mut bytes = vec![0; 25];
207 bytes[0..4].copy_from_slice(b"RIFF");
208 bytes[8..12].copy_from_slice(b"WEBP");
209 bytes[12..16].copy_from_slice(b"VP8L");
210 let packed = (511_u32 - 1) | ((257_u32 - 1) << 14);
211 bytes[21..25].copy_from_slice(&packed.to_le_bytes());
212 assert_dimensions(&bytes, 511, 257);
213 }
214
215 #[test]
216 fn probes_webp_vp8x() {
217 let mut bytes = vec![0; 30];
218 bytes[0..4].copy_from_slice(b"RIFF");
219 bytes[8..12].copy_from_slice(b"WEBP");
220 bytes[12..16].copy_from_slice(b"VP8X");
221 bytes[24..27].copy_from_slice(&[0xff, 0x00, 0x01]);
222 bytes[27..30].copy_from_slice(&[0x01, 0x02, 0x03]);
223 assert_dimensions(&bytes, 65_792, 197_122);
224 }
225
226 #[test]
227 fn rejects_truncated_headers() {
228 let headers: &[&[u8]] = &[
229 b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0",
230 &[0xff, 0xd8, 0xff, 0xc0, 0, 8, 8],
231 b"GIF89a\x01",
232 b"RIFF\0\0\0\0WEBPVP8 ",
233 b"RIFF\0\0\0\0WEBPVP8L",
234 b"RIFF\0\0\0\0WEBPVP8X",
235 ];
236 for header in headers {
237 assert_eq!(dimensions(header), None, "accepted truncated header: {header:?}");
238 }
239 }
240}