1pub const MAX_SIDE: usize = 2048;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Image {
12 pub width: u32,
14 pub height: u32,
16 pub rgba: Vec<u8>,
18}
19
20pub fn decode(bytes: &[u8]) -> Option<Image> {
22 if bytes.starts_with(b"\x89PNG\r\n\x1a\n") {
23 png(bytes)
24 } else if bytes.starts_with(&[0xff, 0xd8]) {
25 jpeg(bytes)
26 } else if bytes.len() > 12 && &bytes[..4] == b"RIFF" && &bytes[8..12] == b"WEBP" {
27 webp(bytes)
28 } else {
29 None
30 }
31}
32
33fn webp(bytes: &[u8]) -> Option<Image> {
34 let mut decoder = image_webp::WebPDecoder::new(std::io::Cursor::new(bytes)).ok()?;
35 let (w, h) = decoder.dimensions();
36 if w as usize > MAX_SIDE || h as usize > MAX_SIDE {
37 return None;
38 }
39 let mut buf = vec![0; decoder.output_buffer_size()?];
40 decoder.read_image(&mut buf).ok()?;
41 let rgba = if decoder.has_alpha() {
42 buf
43 } else {
44 buf.chunks(3)
45 .flat_map(|p| [p[0], p[1], p[2], 255])
46 .collect()
47 };
48 Some(Image {
49 width: w,
50 height: h,
51 rgba,
52 })
53}
54
55fn png(bytes: &[u8]) -> Option<Image> {
56 let mut decoder = png::Decoder::new(std::io::Cursor::new(bytes));
57 decoder.set_transformations(png::Transformations::normalize_to_color8());
58 let mut reader = decoder.read_info().ok()?;
59 let (w, h) = (reader.info().width as usize, reader.info().height as usize);
60 if w > MAX_SIDE || h > MAX_SIDE {
61 return None;
62 }
63 let mut buf = vec![0; reader.output_buffer_size()?];
64 let info = reader.next_frame(&mut buf).ok()?;
65 let rgba: Vec<u8> = match info.color_type {
66 png::ColorType::Rgba => buf[..w * h * 4].to_vec(),
67 png::ColorType::Rgb => buf[..w * h * 3]
68 .chunks(3)
69 .flat_map(|p| [p[0], p[1], p[2], 255])
70 .collect(),
71 png::ColorType::GrayscaleAlpha => buf[..w * h * 2]
72 .chunks(2)
73 .flat_map(|p| [p[0], p[0], p[0], p[1]])
74 .collect(),
75 png::ColorType::Grayscale => buf[..w * h].iter().flat_map(|&g| [g, g, g, 255]).collect(),
76 png::ColorType::Indexed => return None,
77 };
78 Some(Image {
79 width: w as u32,
80 height: h as u32,
81 rgba,
82 })
83}
84
85fn jpeg(bytes: &[u8]) -> Option<Image> {
86 use zune_core::{bytestream::ZCursor, colorspace::ColorSpace, options::DecoderOptions};
87 let options = DecoderOptions::default()
88 .jpeg_set_out_colorspace(ColorSpace::RGBA)
89 .set_max_width(MAX_SIDE)
90 .set_max_height(MAX_SIDE);
91 let mut decoder = zune_jpeg::JpegDecoder::new_with_options(ZCursor::new(bytes), options);
92 let rgba = decoder.decode().ok()?;
93 let info = decoder.info()?;
94 let (w, h) = (info.width as u32, info.height as u32);
95 (rgba.len() == (w * h * 4) as usize).then_some(Image {
96 width: w,
97 height: h,
98 rgba,
99 })
100}
101
102#[cfg(test)]
103mod tests {
104 #[test]
105 fn refuses_what_it_does_not_know() {
106 assert_eq!(super::decode(b"GIF89a"), None);
107 assert_eq!(super::decode(b"\x89PNG\r\n\x1a\nbroken"), None);
108 assert_eq!(super::decode(&[0xff, 0xd8, 0xff, 0x00]), None);
109 assert_eq!(super::decode(b"RIFF\0\0\0\0WEBPVP8 broken"), None);
110 }
111}