1use crate::error::{DecodeError, DecodedImage};
9use crate::profile::Profile;
10use std::io::Cursor;
11use std::sync::atomic::AtomicBool;
12
13#[must_use]
19pub fn is_jpeg(src: &[u8]) -> bool {
20 src.first().is_some_and(|&b| b == 0xFF) && src.get(1).is_some_and(|&b| b == 0xD8)
21}
22
23pub fn decode(src: &[u8], _profile: &Profile, canceled: &AtomicBool) -> Result<DecodedImage, DecodeError> {
32 if src.len() < 2 {
33 return Err(DecodeError::BufferTooShort {
34 expected: 2,
35 actual: src.len(),
36 });
37 }
38 if !is_jpeg(src) {
39 return Err(DecodeError::InvalidFormat("not a JPEG stream".into()));
40 }
41
42 let mut decoder = jpeg_decoder::Decoder::new(Cursor::new(src));
43 let pixels = decoder.decode().map_err(|e| DecodeError::Jpeg(e.to_string()))?;
44
45 let info = decoder
46 .info()
47 .ok_or_else(|| DecodeError::Jpeg("no JPEG metadata".into()))?;
48
49 let w = u32::from(info.width);
50 let h = u32::from(info.height);
51
52 let pixel_count = (w * h) as usize;
54 let mut data = vec![0u8; pixel_count * 4];
55
56 for (i, chunk) in pixels.chunks_exact(3).enumerate() {
57 crate::pixel_utils::check_canceled(canceled, "jpeg decode canceled")?;
58 if i >= pixel_count {
59 break;
60 }
61 let dst = i * 4;
62 data[dst] = chunk[2]; data[dst + 1] = chunk[1]; data[dst + 2] = chunk[0]; data[dst + 3] = 255; }
67
68 let orientation = extract_exif_orientation(src);
70 if orientation > 1 {
71 let (rotated_data, rw, rh) = rotate_bgra(&data, w, h, orientation);
72 return Ok(DecodedImage {
73 data: rotated_data,
74 width: rw,
75 height: rh,
76 });
77 }
78
79 Ok(DecodedImage {
80 data,
81 width: w,
82 height: h,
83 })
84}
85
86fn extract_exif_orientation(src: &[u8]) -> u8 {
94 if src.len() < 20 {
96 return 1;
97 }
98
99 if src[0] != 0xFF || src[1] != 0xD8 {
101 return 1;
102 }
103
104 let mut pos = 2usize;
107 loop {
108 if pos + 4 > src.len() {
109 return 1;
110 }
111 if src[pos] != 0xFF {
112 return 1;
113 }
114 let marker = src[pos + 1];
115 if marker == 0xE1 {
116 let seg_len = usize::from(u16::from_be_bytes([src[pos + 2], src[pos + 3]]));
118 if pos + 2 + seg_len > src.len() {
119 return 1;
120 }
121 let exif_start = pos + 4; if seg_len < 6 + 8 {
123 return 1;
124 }
125 if &src[exif_start..exif_start + 6] != b"Exif\x00\x00" {
126 return 1;
127 }
128 return parse_tiff_orientation(&src[exif_start + 6..], seg_len - 6);
129 }
130 if marker == 0xD9 {
131 return 1;
133 }
134 #[allow(clippy::match_same_arms)]
138 match marker {
139 0x00 | 0xD0..=0xD7 | 0xD8 | 0xD9 | 0x01 => {
141 pos += 2;
142 }
143 _ => {
145 if pos + 4 > src.len() {
146 return 1;
147 }
148 let seg_len = usize::from(u16::from_be_bytes([src[pos + 2], src[pos + 3]]));
149 pos += 2 + seg_len;
150 }
151 }
152 }
153}
154
155fn parse_tiff_orientation(tiff: &[u8], _remaining: usize) -> u8 {
157 if tiff.len() < 8 {
158 return 1;
159 }
160
161 let le = match &tiff[..2] {
162 b"II" => true,
163 b"MM" => false,
164 _ => return 1,
165 };
166
167 let magic = if le {
169 u16::from_le_bytes([tiff[2], tiff[3]])
170 } else {
171 u16::from_be_bytes([tiff[2], tiff[3]])
172 };
173 if magic != 0x002A {
174 return 1;
175 }
176
177 let ifd0_offset = if le {
179 u32::from_le_bytes([tiff[4], tiff[5], tiff[6], tiff[7]])
180 } else {
181 u32::from_be_bytes([tiff[4], tiff[5], tiff[6], tiff[7]])
182 } as usize;
183
184 if ifd0_offset + 2 > tiff.len() {
185 return 1;
186 }
187
188 let entry_count = if le {
189 u16::from_le_bytes([tiff[ifd0_offset], tiff[ifd0_offset + 1]])
190 } else {
191 u16::from_be_bytes([tiff[ifd0_offset], tiff[ifd0_offset + 1]])
192 } as usize;
193
194 for i in 0..entry_count {
196 let entry_start = ifd0_offset + 2 + i * 12;
197 if entry_start + 12 > tiff.len() {
198 break;
199 }
200 let tag = if le {
201 u16::from_le_bytes([tiff[entry_start], tiff[entry_start + 1]])
202 } else {
203 u16::from_be_bytes([tiff[entry_start], tiff[entry_start + 1]])
204 };
205 if tag == 0x0112 {
206 let val = if le {
208 u16::from_le_bytes([tiff[entry_start + 8], tiff[entry_start + 9]])
209 } else {
210 u16::from_be_bytes([tiff[entry_start + 8], tiff[entry_start + 9]])
211 };
212 return val.min(8) as u8;
213 }
214 }
215
216 1
217}
218
219fn rotate_bgra(data: &[u8], w: u32, h: u32, orientation: u8) -> (Vec<u8>, u32, u32) {
232 let total = (w * h * 4) as usize;
233 let mut rotated = vec![0u8; total];
235 let wu = w as usize;
236 let hu = h as usize;
237
238 match orientation {
239 3 => {
240 for i in 0..(wu * hu) {
242 let src_idx = i * 4;
243 let dst_idx = (wu * hu - 1 - i) * 4;
244 rotated[dst_idx..dst_idx + 4].copy_from_slice(&data[src_idx..src_idx + 4]);
245 }
246 (rotated, w, h)
247 }
248 6 => {
249 for iy in 0..hu {
252 for ix in 0..wu {
253 let src_idx = (iy * wu + ix) * 4;
254 let ox = hu - 1 - iy;
255 let oy = ix;
256 let dst_idx = (oy * hu + ox) * 4;
257 rotated[dst_idx..dst_idx + 4].copy_from_slice(&data[src_idx..src_idx + 4]);
258 }
259 }
260 (rotated, h, w)
261 }
262 8 => {
263 let mut rotated = vec![0u8; total];
266 for iy in 0..hu {
267 for ix in 0..wu {
268 let src_idx = (iy * wu + ix) * 4;
269 let ox = iy;
271 #[allow(clippy::cast_possible_truncation)]
272 let oy = wu - 1 - ix;
273 let dst_idx = (oy * hu + ox) * 4;
274 rotated[dst_idx..dst_idx + 4].copy_from_slice(&data[src_idx..src_idx + 4]);
275 }
276 }
277 (rotated, h, w)
278 }
279 _ => (data.to_vec(), w, h),
280 }
281}
282
283#[cfg(test)]
288mod tests {
289 use super::*;
290 use std::sync::atomic::AtomicBool;
291
292 const TEST_JPEG: &[u8] = &[
294 0xff, 0xd8, 0xff, 0xfe, 0x00, 0x0f, 0x4c, 0x61, 0x76, 0x63, 0x36, 0x31, 0x2e, 0x33, 0x2e, 0x31, 0x30, 0x30,
295 0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x08, 0x04, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
296 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x08, 0x08,
297 0x08, 0x07, 0x07, 0x07, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08, 0x08, 0x08, 0x09, 0x09, 0x09, 0x08, 0x08, 0x08,
298 0x08, 0x09, 0x09, 0x0a, 0x0a, 0x0a, 0x0c, 0x0c, 0x0b, 0x0b, 0x0e, 0x0e, 0x0e, 0x11, 0x11, 0x14, 0xff, 0xc4,
299 0x00, 0x4b, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
300 0x00, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
301 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
302 0x00, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
303 0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x02, 0x00, 0x02, 0x03, 0x01, 0x12, 0x00, 0x02, 0x12,
304 0x00, 0x03, 0x12, 0x00, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00,
305 0x9f, 0xc0, 0x07, 0xff, 0xd9,
306 ];
307
308 const TEST_JPEG_EXIF6: &[u8] = &[
310 0xff, 0xd8, 0xff, 0xe1, 0x00, 0x20, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00, 0x49, 0x49, 0x2a, 0x00, 0x08, 0x00,
311 0x00, 0x00, 0x01, 0x00, 0x12, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00,
312 0xff, 0xfe, 0x00, 0x0f, 0x4c, 0x61, 0x76, 0x63, 0x36, 0x31, 0x2e, 0x33, 0x2e, 0x31, 0x30, 0x30, 0x00, 0xff,
313 0xdb, 0x00, 0x43, 0x00, 0x08, 0x04, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06,
314 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x08, 0x08, 0x08, 0x07,
315 0x07, 0x07, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08, 0x08, 0x08, 0x09, 0x09, 0x09, 0x08, 0x08, 0x08, 0x08, 0x09,
316 0x09, 0x0a, 0x0a, 0x0a, 0x0c, 0x0c, 0x0b, 0x0b, 0x0e, 0x0e, 0x0e, 0x11, 0x11, 0x14, 0xff, 0xc4, 0x00, 0x4b,
317 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
318 0x08, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
319 0x00, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
320 0x00, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
321 0x00, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x02, 0x00, 0x02, 0x03, 0x01, 0x12, 0x00, 0x02, 0x12, 0x00, 0x03,
322 0x12, 0x00, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0x9f, 0xc0,
323 0x07, 0xff, 0xd9,
324 ];
325
326 #[test]
327 fn is_jpeg_detects_soi() {
328 assert!(is_jpeg(&[0xFF, 0xD8]));
329 assert!(is_jpeg(&[0xFF, 0xD8, 0xFF, 0xE0]));
330 assert!(is_jpeg(TEST_JPEG));
331 }
332
333 #[test]
334 fn is_jpeg_rejects_non_jpeg() {
335 assert!(!is_jpeg(&[0x00, 0x00]));
336 assert!(!is_jpeg(&[0xFF, 0x00]));
337 assert!(!is_jpeg(&[0x00, 0xD8]));
338 }
339
340 #[test]
341 fn is_jpeg_rejects_short_input() {
342 assert!(!is_jpeg(&[]));
343 assert!(!is_jpeg(&[0xFF]));
344 }
345
346 #[test]
347 fn decode_short_input_returns_buffer_too_short() {
348 let profile = Profile::default();
349 let result = decode(&[], &profile, &AtomicBool::new(false));
350 assert!(matches!(
351 result,
352 Err(DecodeError::BufferTooShort { expected: 2, actual: 0 })
353 ));
354
355 let result = decode(&[0xFF], &profile, &AtomicBool::new(false));
356 assert!(matches!(
357 result,
358 Err(DecodeError::BufferTooShort { expected: 2, actual: 1 })
359 ));
360 }
361
362 #[test]
363 fn decode_non_jpeg_returns_invalid_format() {
364 let profile = Profile::default();
365 let result = decode(&[0x00, 0x00, 0x00, 0x00], &profile, &AtomicBool::new(false));
366 assert!(matches!(result, Err(DecodeError::InvalidFormat(..))));
367 }
368
369 #[test]
370 fn decode_invalid_jpeg_returns_jpeg_error() {
371 let profile = Profile::default();
373 let result = decode(&[0xFF, 0xD8, 0xFF, 0xD9], &profile, &AtomicBool::new(false));
374 assert!(matches!(result, Err(DecodeError::Jpeg(..))));
375 }
376
377 #[test]
378 fn decode_valid_jpeg() {
379 let profile = Profile::default();
380 let result = decode(TEST_JPEG, &profile, &AtomicBool::new(false));
381 assert!(result.is_ok(), "decode failed: {:?}", result.err());
382 let img = result.unwrap();
383 assert_eq!(img.width, 2);
385 assert_eq!(img.height, 2);
386 assert_eq!(img.data.len(), 2 * 2 * 4);
387 }
388
389 #[test]
390 fn decode_bgra_output_format() {
391 let profile = Profile::default();
392 let img = decode(TEST_JPEG, &profile, &AtomicBool::new(false)).unwrap();
393 for chunk in img.data.chunks_exact(4) {
395 assert_eq!(chunk[3], 255);
396 }
397 assert_eq!(img.data.len(), (img.width * img.height * 4) as usize);
399 }
400
401 #[test]
402 fn extract_exif_orientation_normal_when_no_exif() {
403 assert_eq!(extract_exif_orientation(TEST_JPEG), 1);
405 }
406
407 #[test]
408 fn extract_exif_orientation_found() {
409 assert_eq!(extract_exif_orientation(TEST_JPEG_EXIF6), 6);
411 }
412
413 #[test]
414 fn extract_exif_orientation_short_input() {
415 assert_eq!(extract_exif_orientation(&[]), 1);
416 assert_eq!(extract_exif_orientation(&[0xFF, 0xD8]), 1);
417 }
418
419 #[test]
420 fn exif_rotation_180() {
421 let w = 2u32;
424 let h = 2u32;
425 let mut data = vec![0u8; (w * h * 4) as usize];
426 data[0..4].copy_from_slice(&[0, 0, 255, 255]);
429 data[4..8].copy_from_slice(&[0, 255, 0, 255]);
431 data[8..12].copy_from_slice(&[255, 0, 0, 255]);
433 data[12..16].copy_from_slice(&[255, 255, 255, 255]);
435
436 let (rotated, rw, rh) = rotate_bgra(&data, w, h, 3);
439 assert_eq!(rw, 2);
440 assert_eq!(rh, 2);
441 assert_eq!(&rotated[12..16], &[0, 0, 255, 255]);
443 assert_eq!(&rotated[8..12], &[0, 255, 0, 255]);
445 assert_eq!(&rotated[4..8], &[255, 0, 0, 255]);
447 assert_eq!(&rotated[0..4], &[255, 255, 255, 255]);
449 }
450
451 #[test]
452 fn exif_rotation_90cw() {
453 let w = 2u32;
456 let h = 3u32;
457 let mut data = vec![0u8; (w * h * 4) as usize];
458 data[0..4].copy_from_slice(&[0, 0, 255, 255]);
461 data[4..8].copy_from_slice(&[0, 255, 0, 255]);
463 data[8..12].copy_from_slice(&[255, 0, 0, 255]);
465 data[12..16].copy_from_slice(&[0, 255, 255, 255]);
467 data[16..20].copy_from_slice(&[255, 255, 0, 255]);
469 data[20..24].copy_from_slice(&[255, 0, 255, 255]);
471
472 let (rotated, rw, rh) = rotate_bgra(&data, w, h, 6);
475 assert_eq!(rw, 3);
477 assert_eq!(rh, 2);
478
479 assert_eq!(&rotated[0..4], &[255, 255, 0, 255]);
482 assert_eq!(&rotated[4..8], &[255, 0, 0, 255]);
484 assert_eq!(&rotated[8..12], &[0, 0, 255, 255]);
486 assert_eq!(&rotated[12..16], &[255, 0, 255, 255]);
488 assert_eq!(&rotated[16..20], &[0, 255, 255, 255]);
490 assert_eq!(&rotated[20..24], &[0, 255, 0, 255]);
492 }
493
494 #[test]
495 fn exif_rotation_270cw() {
496 let w = 2u32;
497 let h = 3u32;
498 let mut data = vec![0u8; (w * h * 4) as usize];
499 data[0..4].copy_from_slice(&[0, 0, 255, 255]);
501 data[4..8].copy_from_slice(&[0, 255, 0, 255]);
503 data[8..12].copy_from_slice(&[255, 0, 0, 255]);
505 data[12..16].copy_from_slice(&[0, 255, 255, 255]);
507 data[16..20].copy_from_slice(&[255, 255, 0, 255]);
509 data[20..24].copy_from_slice(&[255, 0, 255, 255]);
511
512 let (rotated, rw, rh) = rotate_bgra(&data, w, h, 8);
515 assert_eq!(rw, 3);
516 assert_eq!(rh, 2);
517
518 assert_eq!(&rotated[0..4], &[0, 255, 0, 255]);
521 assert_eq!(&rotated[4..8], &[0, 255, 255, 255]);
523 assert_eq!(&rotated[8..12], &[255, 0, 255, 255]);
525 assert_eq!(&rotated[12..16], &[0, 0, 255, 255]);
527 assert_eq!(&rotated[16..20], &[255, 0, 0, 255]);
529 assert_eq!(&rotated[20..24], &[255, 255, 0, 255]);
531 }
532
533 #[test]
534 fn decode_with_exif_rotation() {
535 let profile = Profile::default();
536 let result = decode(TEST_JPEG_EXIF6, &profile, &AtomicBool::new(false));
537 assert!(result.is_ok(), "decode failed: {:?}", result.err());
540 }
541
542 #[test]
543 fn rotate_bgra_noop_for_normal_orientation() {
544 let data = vec![0u8; 16];
545 let (rotated, rw, rh) = rotate_bgra(&data, 2, 2, 1);
546 assert_eq!(rotated, data);
547 assert_eq!(rw, 2);
548 assert_eq!(rh, 2);
549 }
550}