1use crate::error::{Error, Result};
2use crate::format::Format;
3
4use super::{Codec, EncodeOptions, ImageData};
5
6pub struct JpegCodec;
8
9impl Codec for JpegCodec {
10 fn format(&self) -> Format {
11 Format::Jpeg
12 }
13
14 fn decode(&self, data: &[u8]) -> Result<ImageData> {
15 let data = data.to_vec();
18 let result = std::panic::catch_unwind(move || -> Result<ImageData> {
19 let decompress = mozjpeg::Decompress::new_mem(&data)
20 .map_err(|e| Error::Decode(format!("mozjpeg decompress init: {e}")))?;
21
22 let width = decompress.width() as u32;
23 let height = decompress.height() as u32;
24
25 let mut decompressor = decompress
26 .rgba()
27 .map_err(|e| Error::Decode(format!("mozjpeg rgba conversion: {e}")))?;
28
29 let pixels: Vec<[u8; 4]> = decompressor
30 .read_scanlines()
31 .map_err(|e| Error::Decode(format!("mozjpeg read scanlines: {e}")))?;
32
33 decompressor
34 .finish()
35 .map_err(|e| Error::Decode(format!("mozjpeg finish: {e}")))?;
36
37 let rgba_data: Vec<u8> = pixels.into_iter().flatten().collect();
38
39 Ok(ImageData::new(width, height, rgba_data))
40 });
41
42 match result {
43 Ok(inner) => inner,
44 Err(panic) => {
45 let msg = panic_message(&panic);
46 Err(Error::Decode(format!("mozjpeg panicked: {msg}")))
47 }
48 }
49 }
50
51 fn encode(&self, image: &ImageData, options: &EncodeOptions) -> Result<Vec<u8>> {
52 let width = image.width;
53 let height = image.height;
54 let rgb_data = image.to_rgb();
55 let quality = options.quality as f32;
56
57 let result = std::panic::catch_unwind(move || -> Result<Vec<u8>> {
58 let mut compress = mozjpeg::Compress::new(mozjpeg::ColorSpace::JCS_RGB);
59
60 compress.set_size(width as usize, height as usize);
61 compress.set_quality(quality);
62 compress.set_progressive_mode();
63 compress.set_optimize_scans(true);
64 compress.set_optimize_coding(true);
65
66 let mut compressor = compress
67 .start_compress(Vec::new())
68 .map_err(|e| Error::Encode(format!("mozjpeg compress start: {e}")))?;
69
70 compressor
71 .write_scanlines(&rgb_data)
72 .map_err(|e| Error::Encode(format!("mozjpeg write scanlines: {e}")))?;
73
74 let output = compressor
75 .finish()
76 .map_err(|e| Error::Encode(format!("mozjpeg finish: {e}")))?;
77
78 Ok(output)
79 });
80
81 match result {
82 Ok(inner) => inner,
83 Err(panic) => {
84 let msg = panic_message(&panic);
85 Err(Error::Encode(format!("mozjpeg panicked: {msg}")))
86 }
87 }
88 }
89}
90
91fn panic_message(panic: &Box<dyn std::any::Any + Send>) -> String {
93 if let Some(s) = panic.downcast_ref::<&str>() {
94 (*s).to_string()
95 } else if let Some(s) = panic.downcast_ref::<String>() {
96 s.clone()
97 } else {
98 "unknown panic".to_string()
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 fn create_test_image(width: u32, height: u32) -> ImageData {
107 let size = (width * height * 4) as usize;
108 let mut data = vec![0u8; size];
109 for y in 0..height {
110 for x in 0..width {
111 let i = ((y * width + x) * 4) as usize;
112 data[i] = (x * 255 / width) as u8; data[i + 1] = (y * 255 / height) as u8; data[i + 2] = 128; data[i + 3] = 255; }
117 }
118 ImageData::new(width, height, data)
119 }
120
121 #[test]
122 fn encode_and_decode_roundtrip() {
123 let codec = JpegCodec;
124 let original = create_test_image(64, 48);
125 let options = EncodeOptions { quality: 90 };
126
127 let encoded = codec.encode(&original, &options).expect("encode failed");
128
129 assert!(
131 encoded.len() >= 3,
132 "encoded data too short: {} bytes",
133 encoded.len()
134 );
135 assert_eq!(
136 &encoded[..3],
137 &[0xFF, 0xD8, 0xFF],
138 "missing JPEG magic bytes"
139 );
140
141 let decoded = codec.decode(&encoded).expect("decode failed");
143 assert_eq!(decoded.width, original.width);
144 assert_eq!(decoded.height, original.height);
145 assert_eq!(
146 decoded.data.len(),
147 (decoded.width * decoded.height * 4) as usize
148 );
149 }
150
151 #[test]
152 fn encode_produces_smaller_at_lower_quality() {
153 let codec = JpegCodec;
154 let image = create_test_image(128, 96);
155
156 let high = codec
157 .encode(&image, &EncodeOptions { quality: 95 })
158 .expect("encode q95 failed");
159 let low = codec
160 .encode(&image, &EncodeOptions { quality: 30 })
161 .expect("encode q30 failed");
162
163 assert!(
164 low.len() < high.len(),
165 "low quality ({} bytes) should be smaller than high quality ({} bytes)",
166 low.len(),
167 high.len(),
168 );
169 }
170
171 #[test]
172 fn decode_invalid_data_returns_error() {
173 let codec = JpegCodec;
174 let result = codec.decode(b"not a jpeg");
175 assert!(result.is_err(), "decoding invalid data should fail");
176 }
177}