1use rusty_esp_core::error::{Error, Result};
10use rusty_esp_core::frame::{Geometry, PixelFormat};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct JpegInfo {
15 pub geometry: Geometry,
17 pub progressive: bool,
19 pub components: u8,
21 pub precision: u8,
23 pub sof_offset: usize,
25}
26
27const SOI: u8 = 0xD8;
28const EOI: u8 = 0xD9;
29const SOS: u8 = 0xDA;
30
31fn is_sof(marker: u8) -> bool {
32 matches!(
33 marker,
34 0xC0 | 0xC1 | 0xC2 | 0xC3 | 0xC5 | 0xC6 | 0xC7 | 0xC9 | 0xCA | 0xCB | 0xCD | 0xCE | 0xCF
35 )
36}
37
38fn is_progressive(marker: u8) -> bool {
39 matches!(marker, 0xC2 | 0xC6 | 0xCA | 0xCE)
40}
41
42#[must_use]
44pub fn is_jpeg(bytes: &[u8]) -> bool {
45 bytes.len() >= 2 && bytes[0] == 0xFF && bytes[1] == SOI
46}
47
48pub fn probe(bytes: &[u8]) -> Result<JpegInfo> {
50 if !is_jpeg(bytes) {
51 return Err(Error::InvalidFormat);
52 }
53 let mut i = 2usize;
54 loop {
55 let &ff = bytes.get(i).ok_or(Error::InvalidFormat)?;
57 if ff != 0xFF {
58 return Err(Error::InvalidFormat);
59 }
60 while bytes.get(i) == Some(&0xFF) {
61 i += 1;
62 }
63 let &marker = bytes.get(i).ok_or(Error::InvalidFormat)?;
64 i += 1;
65 match marker {
66 0x00 => return Err(Error::InvalidFormat), 0x01 | 0xD0..=0xD7 => continue, EOI | SOS => return Err(Error::InvalidFormat), _ => {}
70 }
71 let len = read_u16(bytes, i)? as usize;
72 if len < 2 {
73 return Err(Error::InvalidFormat);
74 }
75 if is_sof(marker) {
76 let seg = bytes.get(i + 2..i + len).ok_or(Error::InvalidFormat)?;
77 if seg.len() < 6 {
78 return Err(Error::InvalidFormat);
79 }
80 let precision = seg[0];
81 let height = u32::from(u16::from_be_bytes([seg[1], seg[2]]));
82 let width = u32::from(u16::from_be_bytes([seg[3], seg[4]]));
83 let components = seg[5];
84 if height == 0 {
85 return Err(Error::Unsupported);
87 }
88 let geometry = Geometry::new(width, height, PixelFormat::Jpeg)?;
89 return Ok(JpegInfo {
90 geometry,
91 progressive: is_progressive(marker),
92 components,
93 precision,
94 sof_offset: i - 2,
95 });
96 }
97 i += len;
98 }
99}
100
101fn read_u16(bytes: &[u8], at: usize) -> Result<u16> {
102 let hi = *bytes.get(at).ok_or(Error::InvalidFormat)?;
103 let lo = *bytes.get(at + 1).ok_or(Error::InvalidFormat)?;
104 Ok(u16::from_be_bytes([hi, lo]))
105}
106
107#[must_use]
111pub fn find_eoi(bytes: &[u8]) -> Option<usize> {
112 if bytes.len() < 2 {
121 return None;
122 }
123 let mut i = bytes.len() - 1;
124 while i >= 1 {
125 if bytes[i] == EOI && bytes[i - 1] == 0xFF {
126 return Some(i + 1);
127 }
128 i -= 1;
129 }
130 None
131}
132
133#[cfg(feature = "jpeg")]
142pub mod encode {
143 use rusty_esp_core::error::{Error, Result};
144 use rusty_esp_core::frame::{Frame, Geometry, PixelFormat, Planes};
145 use rusty_jpeg::encode::{ColorType, Encoder, EncodingError, SliceWriter, YuyvImage};
146
147 #[must_use]
151 pub fn max_bytes(geometry: &Geometry) -> usize {
152 (geometry.width as usize)
153 .saturating_mul(geometry.height as usize)
154 .saturating_mul(3)
155 .saturating_add(4096)
156 }
157
158 pub fn encode_packed(
164 geometry: Geometry,
165 data: &[u8],
166 quality: u8,
167 out: &mut [u8],
168 ) -> Result<usize> {
169 let (w, h) = (
170 u16::try_from(geometry.width).map_err(|_| Error::InvalidGeometry)?,
171 u16::try_from(geometry.height).map_err(|_| Error::InvalidGeometry)?,
172 );
173 if w == 0 || h == 0 {
174 return Err(Error::InvalidGeometry);
175 }
176 let needed = geometry.byte_len().ok_or(Error::Unsupported)?;
177 if data.len() < needed {
178 return Err(Error::InvalidGeometry);
179 }
180 let quality = quality.clamp(1, 100);
181 let mut writer = SliceWriter::new(out);
182 let result = match geometry.format {
183 PixelFormat::Yuyv422 => {
184 let image =
185 YuyvImage::new(data, usize::from(w) * 2, w, h).ok_or(Error::InvalidGeometry)?;
186 Encoder::new(&mut writer, quality).encode_image(image)
187 }
188 PixelFormat::Rgb888 => {
189 Encoder::new(&mut writer, quality).encode(data, w, h, ColorType::Rgb)
190 }
191 PixelFormat::Bgr888 => {
192 Encoder::new(&mut writer, quality).encode(data, w, h, ColorType::Bgr)
193 }
194 PixelFormat::Rgba8888 => {
195 Encoder::new(&mut writer, quality).encode(data, w, h, ColorType::Rgba)
196 }
197 PixelFormat::Gray8 => {
198 Encoder::new(&mut writer, quality).encode(data, w, h, ColorType::Luma)
199 }
200 _ => return Err(Error::Unsupported),
201 };
202 match result {
203 Ok(()) => Ok(writer.written()),
204 Err(EncodingError::BufferTooSmall) => Err(Error::BufferTooSmall {
205 needed: max_bytes(&geometry),
206 }),
207 Err(_) => Err(Error::InvalidFormat),
208 }
209 }
210
211 pub fn encode_frame(frame: &Frame<'_>, quality: u8, out: &mut [u8]) -> Result<usize> {
213 match frame.planes {
214 Planes::Packed(data) => encode_packed(frame.geometry, data, quality, out),
215 Planes::Planar { .. } => Err(Error::Unsupported),
216 }
217 }
218
219 #[cfg(all(test, feature = "std"))]
220 mod tests {
221 use super::*;
222 use crate::jpeg::{find_eoi, probe};
223 use crate::source::{ImageSource, TestPattern};
224
225 #[test]
226 fn colour_bars_round_trip_through_the_house_decoder() {
227 let g = Geometry::new(96, 64, PixelFormat::Rgb888).unwrap();
228 let mut pattern = TestPattern::new(g, 10).unwrap();
229 let mut rgb = vec![0u8; g.byte_len().unwrap()];
230 let frame = pattern.grab(&mut rgb).unwrap();
231 let mut out = vec![0u8; max_bytes(&g)];
232 let n = encode_frame(&frame, 85, &mut out).unwrap();
233 let info = probe(&out[..n]).unwrap();
234 assert_eq!(info.geometry.width, 96);
235 assert_eq!(info.geometry.height, 64);
236 assert!(!info.progressive);
237 assert_eq!(find_eoi(&out[..n]), Some(n));
238 let mut d = rusty_jpeg::Decoder::new(&out[..n]);
239 let pixels = d.decode().unwrap();
240 let back = d.info().unwrap();
241 assert_eq!((back.width, back.height), (96, 64));
242 assert_eq!(pixels.len(), 96 * 64 * 3);
243 let err: u64 = pixels
244 .iter()
245 .zip(&rgb)
246 .map(|(&a, &b)| u64::from(a.abs_diff(b)))
247 .sum();
248 let mean = err / (96 * 64 * 3);
249 assert!(mean < 12, "mean abs error {mean}");
251 }
252
253 #[test]
254 fn yuyv_is_coded_as_delivered_and_the_luma_survives() {
255 let g = Geometry::new(64, 32, PixelFormat::Yuyv422).unwrap();
256 let mut yuyv = vec![0u8; g.byte_len().unwrap()];
257 for (i, px) in yuyv.chunks_exact_mut(4).enumerate() {
258 let y = ((i % 32) * 8) as u8;
259 px.copy_from_slice(&[y, 128, y, 128]);
260 }
261 let mut out = vec![0u8; max_bytes(&g)];
262 let n = encode_packed(g, &yuyv, 90, &mut out).unwrap();
263 assert_eq!(probe(&out[..n]).unwrap().geometry.width, 64);
264 let mut d = rusty_jpeg::Decoder::new(&out[..n]);
265 let pixels = d.decode().unwrap();
266 let info = d.info().unwrap();
267 assert_eq!((info.width, info.height), (64, 32));
268 let mut err = 0u64;
270 let mut count = 0u64;
271 for (px, y) in pixels
272 .chunks_exact(3)
273 .zip(yuyv.chunks_exact(2).map(|p| p[0]))
274 {
275 err += u64::from(px[1].abs_diff(y));
276 count += 1;
277 }
278 assert!(err / count < 8, "mean luma error {}", err / count);
279 }
280
281 #[test]
282 fn the_refusals_name_their_reason() {
283 let g = Geometry::new(64, 32, PixelFormat::Rgb888).unwrap();
284 let rgb = vec![90u8; g.byte_len().unwrap()];
285 let mut small = [0u8; 100];
286 assert_eq!(
287 encode_packed(g, &rgb, 80, &mut small),
288 Err(Error::BufferTooSmall {
289 needed: max_bytes(&g)
290 })
291 );
292 let mut out = vec![0u8; max_bytes(&g)];
293 assert_eq!(
294 encode_packed(g, &rgb[..10], 80, &mut out),
295 Err(Error::InvalidGeometry)
296 );
297 let planar = Geometry::new(64, 32, PixelFormat::Yuv420p).unwrap();
298 assert_eq!(
299 encode_packed(planar, &rgb, 80, &mut out),
300 Err(Error::Unsupported)
301 );
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 fn header(sof: u8, width: u16, height: u16, components: u8) -> [u8; 4 + 6 + 2 + 2 + 6] {
312 let mut h = [0u8; 20];
313 h[0..2].copy_from_slice(&[0xFF, 0xD8]);
314 h[2..4].copy_from_slice(&[0xFF, 0xE0]); h[4..6].copy_from_slice(&4u16.to_be_bytes()); h[6..8].copy_from_slice(b"JF");
317 h[8..10].copy_from_slice(&[0xFF, sof]);
318 h[10..12].copy_from_slice(&8u16.to_be_bytes()); h[12] = 8;
320 h[13..15].copy_from_slice(&height.to_be_bytes());
321 h[15..17].copy_from_slice(&width.to_be_bytes());
322 h[17] = components;
323 h[18..20].copy_from_slice(&[0xFF, 0xD9]);
324 h
325 }
326
327 #[test]
328 fn baseline_and_progressive_headers() {
329 let b = header(0xC0, 320, 240, 3);
330 let info = probe(&b).unwrap();
331 assert_eq!(info.geometry.width, 320);
332 assert_eq!(info.geometry.height, 240);
333 assert_eq!(info.geometry.format, PixelFormat::Jpeg);
334 assert!(!info.progressive);
335 assert_eq!(info.components, 3);
336 assert_eq!(info.precision, 8);
337 assert_eq!(info.sof_offset, 8);
338 let p = header(0xC2, 1600, 1200, 1);
339 let info = probe(&p).unwrap();
340 assert!(info.progressive);
341 assert_eq!(info.components, 1);
342 assert_eq!((info.geometry.width, info.geometry.height), (1600, 1200));
343 }
344
345 #[test]
346 fn rejects_non_jpeg_truncated_and_headerless() {
347 assert_eq!(probe(&[0x89, b'P', b'N', b'G']), Err(Error::InvalidFormat));
348 assert_eq!(probe(&[0xFF, 0xD8]), Err(Error::InvalidFormat));
349 let mut h = header(0xC0, 8, 8, 3);
350 assert!(probe(&h[..12]).is_err());
351 h[9] = 0xDA;
353 assert_eq!(probe(&h), Err(Error::InvalidFormat));
354 let z = header(0xC0, 8, 0, 3);
356 assert_eq!(probe(&z), Err(Error::Unsupported));
357 }
358
359 #[test]
360 fn eoi_scan_skips_padding() {
361 let mut buf = header(0xC0, 8, 8, 3).to_vec();
362 let len = buf.len();
363 buf.extend_from_slice(&[0, 0, 0, 0, 0xFF, 0xFF]);
364 assert_eq!(find_eoi(&buf), Some(len));
365 assert_eq!(find_eoi(&[0xFF, 0xD8, 0x00]), None);
366 assert_eq!(find_eoi(&[]), None);
367 }
368
369 #[test]
370 fn real_jpegs_from_the_house_encoder() {
371 for (w, h) in [(16u16, 8u16), (160, 120), (320, 240), (99, 33)] {
375 for progressive in [false, true] {
376 let rgb: std::vec::Vec<u8> = (0..(w as usize * h as usize * 3))
377 .map(|i| (i % 251) as u8)
378 .collect();
379 let mut out = std::vec::Vec::new();
380 let mut enc = rusty_jpeg::encode::Encoder::new(&mut out, 80);
381 enc.set_progressive(progressive);
382 enc.encode(&rgb, w, h, rusty_jpeg::encode::ColorType::Rgb)
383 .unwrap();
384 let info = probe(&out).unwrap();
385 assert_eq!(info.geometry.width, u32::from(w));
386 assert_eq!(info.geometry.height, u32::from(h));
387 assert_eq!(info.progressive, progressive, "{w}x{h}");
388 assert_eq!(info.components, 3);
389 assert_eq!(find_eoi(&out), Some(out.len()));
390
391 let gray: std::vec::Vec<u8> = rgb.iter().step_by(3).copied().collect();
392 let mut out = std::vec::Vec::new();
393 let mut enc = rusty_jpeg::encode::Encoder::new(&mut out, 80);
394 enc.set_progressive(progressive);
395 enc.encode(&gray, w, h, rusty_jpeg::encode::ColorType::Luma)
396 .unwrap();
397 let info = probe(&out).unwrap();
398 assert_eq!(info.components, 1);
399 assert_eq!(info.geometry.width, u32::from(w));
400 }
401 }
402 }
403}