1use crate::style::Common;
11use std::sync::Arc;
12
13const MAX_PIXELS: u64 = 40_000_000;
18
19#[derive(Clone, Copy, PartialEq, Eq, Debug)]
20pub enum ImageFormat {
21 Jpeg,
22 Png,
23}
24
25#[derive(Clone, Copy, PartialEq, Eq, Debug)]
26pub enum ImageError {
27 UnsupportedFormat,
29 Malformed,
31 UnsupportedJpeg,
33 UnsupportedPng,
35 ImageTooLarge,
37}
38
39impl core::fmt::Display for ImageError {
40 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
41 match self {
42 ImageError::UnsupportedFormat => write!(f, "unsupported image format (need JPEG or PNG)"),
43 ImageError::Malformed => write!(f, "malformed image header"),
44 ImageError::UnsupportedJpeg => write!(f, "unsupported JPEG variant (need baseline Gray/RGB)"),
45 ImageError::UnsupportedPng => write!(f, "unsupported PNG variant (need non-interlaced 8-bit RGB/RGBA)"),
46 ImageError::ImageTooLarge => write!(f, "image pixel count exceeds the supported limit"),
47 }
48 }
49}
50
51const PNG_SIGNATURE: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
52
53fn parse_jpeg(bytes: &[u8]) -> Result<(u32, u32, u8), ImageError> {
60 if bytes.len() < 4 || bytes[0] != 0xFF || bytes[1] != 0xD8 {
61 return Err(ImageError::Malformed);
62 }
63 let mut i = 2usize;
64 while i + 1 < bytes.len() {
65 if bytes[i] != 0xFF {
66 return Err(ImageError::Malformed);
67 }
68 let mut marker_pos = i + 1;
69 while marker_pos < bytes.len() && bytes[marker_pos] == 0xFF {
70 marker_pos += 1; }
72 if marker_pos >= bytes.len() {
73 return Err(ImageError::Malformed);
74 }
75 let marker = bytes[marker_pos];
76 i = marker_pos + 1;
77
78 if marker == 0x01 || (0xD0..=0xD9).contains(&marker) {
80 continue;
81 }
82 if i + 2 > bytes.len() {
83 return Err(ImageError::Malformed);
84 }
85 let length = u16::from_be_bytes([bytes[i], bytes[i + 1]]) as usize;
86 if length < 2 || i + length > bytes.len() {
87 return Err(ImageError::Malformed);
88 }
89
90 let is_sof = (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC;
91 if is_sof {
92 if marker != 0xC0 {
93 return Err(ImageError::UnsupportedJpeg);
94 }
95 let payload = &bytes[i + 2..i + length];
96 if payload.len() < 6 {
97 return Err(ImageError::Malformed);
98 }
99 let height = u16::from_be_bytes([payload[1], payload[2]]) as u32;
100 let width = u16::from_be_bytes([payload[3], payload[4]]) as u32;
101 let components = payload[5];
102 if components != 1 && components != 3 {
103 return Err(ImageError::UnsupportedJpeg); }
105 return Ok((width, height, components));
106 }
107 if marker == 0xDA {
108 return Err(ImageError::Malformed); }
110 i += length;
111 }
112 Err(ImageError::Malformed)
113}
114
115fn parse_png(bytes: &[u8]) -> Result<(u32, u32, u8), ImageError> {
118 if bytes.len() < 8 + 8 + 13 || bytes[0..8] != PNG_SIGNATURE {
119 return Err(ImageError::Malformed);
120 }
121 let chunk_len = u32::from_be_bytes(bytes[8..12].try_into().unwrap()) as usize;
122 if &bytes[12..16] != b"IHDR" || chunk_len != 13 {
123 return Err(ImageError::Malformed);
124 }
125 let ihdr = &bytes[16..16 + 13];
126 let width = u32::from_be_bytes(ihdr[0..4].try_into().unwrap());
127 let height = u32::from_be_bytes(ihdr[4..8].try_into().unwrap());
128 let bit_depth = ihdr[8];
129 let color_type = ihdr[9];
130 let interlace = ihdr[12];
131 if width == 0 || height == 0 {
132 return Err(ImageError::Malformed);
133 }
134 if interlace != 0 || bit_depth != 8 {
135 return Err(ImageError::UnsupportedPng);
136 }
137 let components = match color_type {
138 2 => 3, 6 => 4, _ => return Err(ImageError::UnsupportedPng), };
142 Ok((width, height, components))
143}
144
145#[derive(Clone, Debug)]
149pub struct Image {
150 pub bytes: Arc<[u8]>,
151 pub format: ImageFormat,
152 pub width_px: u32,
153 pub height_px: u32,
154 pub components: u8,
156 pub common: Common,
157}
158
159impl Image {
160 pub fn new(bytes: impl Into<Arc<[u8]>>) -> Result<Self, ImageError> {
165 let bytes: Arc<[u8]> = bytes.into();
166 let (format, width_px, height_px, components) = if bytes.len() >= 2 && bytes[0] == 0xFF && bytes[1] == 0xD8 {
167 let (w, h, c) = parse_jpeg(&bytes)?;
168 (ImageFormat::Jpeg, w, h, c)
169 } else if bytes.len() >= 8 && bytes[0..8] == PNG_SIGNATURE {
170 let (w, h, c) = parse_png(&bytes)?;
171 (ImageFormat::Png, w, h, c)
172 } else {
173 return Err(ImageError::UnsupportedFormat);
174 };
175 if (width_px as u64) * (height_px as u64) > MAX_PIXELS {
176 return Err(ImageError::ImageTooLarge);
177 }
178 Ok(Image {
179 bytes,
180 format,
181 width_px,
182 height_px,
183 components,
184 common: Common::default(),
185 })
186 }
187
188 pub fn width(mut self, width: f32) -> Self {
189 self.common.width = Some(width);
190 self
191 }
192
193 pub fn height(mut self, height: f32) -> Self {
194 self.common.height = Some(height);
195 self
196 }
197
198 pub fn flex(mut self, factor: f32) -> Self {
199 self.common.flex = Some(factor);
200 self
201 }
202
203 pub fn keep_with_next(mut self) -> Self {
204 self.common.keep_with_next = true;
205 self
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 fn fixture(name: &str) -> Vec<u8> {
214 std::fs::read(format!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../test-fixtures/images/{}"), name)).expect("test fixture present")
215 }
216
217 #[test]
218 fn accepts_rgba_png_with_transparency() {
219 let img = Image::new(fixture("logo_rgba.png")).unwrap();
220 assert_eq!(img.format, ImageFormat::Png);
221 assert_eq!(img.components, 4);
222 assert_eq!((img.width_px, img.height_px), (64, 48));
223 }
224
225 #[test]
226 fn accepts_opaque_rgb_png() {
227 let img = Image::new(fixture("logo_rgb.png")).unwrap();
228 assert_eq!(img.components, 3);
229 assert_eq!((img.width_px, img.height_px), (40, 30));
230 }
231
232 #[test]
233 fn accepts_baseline_rgb_jpeg() {
234 let img = Image::new(fixture("logo_baseline.jpg")).unwrap();
235 assert_eq!(img.format, ImageFormat::Jpeg);
236 assert_eq!(img.components, 3);
237 assert_eq!((img.width_px, img.height_px), (80, 60));
238 }
239
240 #[test]
241 fn accepts_baseline_gray_jpeg() {
242 let img = Image::new(fixture("logo_gray.jpg")).unwrap();
243 assert_eq!(img.components, 1);
244 assert_eq!((img.width_px, img.height_px), (32, 32));
245 }
246
247 #[test]
248 fn rejects_progressive_jpeg() {
249 assert_eq!(Image::new(fixture("progressive.jpg")).unwrap_err(), ImageError::UnsupportedJpeg);
250 }
251
252 #[test]
253 fn rejects_cmyk_jpeg() {
254 assert_eq!(Image::new(fixture("cmyk.jpg")).unwrap_err(), ImageError::UnsupportedJpeg);
255 }
256
257 #[test]
258 fn rejects_palette_png() {
259 assert_eq!(Image::new(fixture("palette.png")).unwrap_err(), ImageError::UnsupportedPng);
260 }
261
262 #[test]
263 fn rejects_sixteen_bit_png() {
264 assert_eq!(Image::new(fixture("sixteen_bit.png")).unwrap_err(), ImageError::UnsupportedPng);
265 }
266
267 #[test]
268 fn rejects_interlaced_png() {
269 let mut bytes = fixture("logo_rgb.png");
274 assert_eq!(&bytes[12..16], b"IHDR");
275 bytes[16 + 12] = 1; assert_eq!(Image::new(bytes).unwrap_err(), ImageError::UnsupportedPng);
277 }
278
279 #[test]
280 fn rejects_garbage_bytes() {
281 assert_eq!(Image::new(vec![0u8; 16]).unwrap_err(), ImageError::UnsupportedFormat);
282 }
283
284 #[test]
285 fn rejects_oversized_declared_dimensions() {
286 let mut bytes = fixture("logo_rgb.png");
287 bytes[16..20].copy_from_slice(&10_000u32.to_be_bytes());
288 bytes[20..24].copy_from_slice(&10_000u32.to_be_bytes());
289 assert_eq!(Image::new(bytes).unwrap_err(), ImageError::ImageTooLarge);
290 }
291
292 #[test]
293 fn builder_methods_set_common_fields() {
294 let img = Image::new(fixture("logo_rgb.png"))
295 .unwrap()
296 .width(100.0)
297 .height(50.0)
298 .flex(1.0)
299 .keep_with_next();
300 assert_eq!(img.common.width, Some(100.0));
301 assert_eq!(img.common.height, Some(50.0));
302 assert_eq!(img.common.flex, Some(1.0));
303 assert!(img.common.keep_with_next);
304 }
305}