1use std::io::Cursor;
25
26use rff_codec::{Codec, CodecRegistry, Decoder, Encoder as RffEncoder};
27use rff_core::{Dictionary, Error, Frame, MediaType, Packet, PixelFormat, Result, VideoFrame};
28use rusty_jpeg::encode::{ColorType, Encoder, SamplingFactor};
29
30const DEFAULT_QUALITY: u8 = 90;
32
33pub fn register(registry: &mut CodecRegistry) {
35 registry.register(Codec {
36 id: rff_core::CodecId::Jpeg,
37 name: "mjpeg",
38 long_name: "JPEG / MJPEG (Motion JPEG) image",
39 media_type: MediaType::Video,
40 decoder: Some(|| Box::new(JpegDecoder::default())),
41 encoder: Some(|| Box::new(JpegEncoder::default())),
42 });
43}
44
45#[derive(Default)]
46struct JpegDecoder {
47 frame: Option<Frame>,
48 eof: bool,
49}
50
51impl Decoder for JpegDecoder {
52 fn send_packet(&mut self, packet: &Packet) -> Result<()> {
53 self.frame = Some(decode_jpeg(&packet.data)?);
54 Ok(())
55 }
56
57 fn receive_frame(&mut self) -> Result<Frame> {
58 if let Some(frame) = self.frame.take() {
59 return Ok(frame);
60 }
61 if self.eof {
62 Err(Error::Eof)
63 } else {
64 Err(Error::Again)
65 }
66 }
67
68 fn flush(&mut self) {
69 self.eof = true;
70 }
71}
72
73fn decode_jpeg(data: &[u8]) -> Result<Frame> {
74 if let Some(frame) = decode_jpeg_planar(data)? {
79 return Ok(frame);
80 }
81 decode_jpeg_rgb(data)
82}
83
84fn decode_jpeg_planar(data: &[u8]) -> Result<Option<Frame>> {
87 let mut decoder = rusty_jpeg::decode::Decoder::new(Cursor::new(data));
88 let planar = match decoder.decode_planar() {
91 Ok(p) => p,
92 Err(rusty_jpeg::decode::Error::Unsupported(_)) => return Ok(None),
93 Err(e) => return Err(Error::invalid(format!("jpeg decode: {e}"))),
94 };
95 let Some(sub) = planar.chroma_subsampling() else {
96 return Ok(None);
97 };
98 let format = match sub {
99 (1, 1) => PixelFormat::Yuv444p,
100 (2, 1) => PixelFormat::Yuv422p,
101 (2, 2) => PixelFormat::Yuv420p,
102 _ => return Ok(None),
103 };
104 let (mut planes, mut strides) = (Vec::with_capacity(3), Vec::with_capacity(3));
107 for c in planar.components {
108 strides.push(c.stride);
109 planes.push(c.data);
110 }
111 Ok(Some(Frame::Video(VideoFrame {
112 width: planar.width as u32,
113 height: planar.height as u32,
114 format,
115 planes,
116 strides,
117 pts: None,
118 })))
119}
120
121fn decode_jpeg_rgb(data: &[u8]) -> Result<Frame> {
122 let mut decoder = rusty_jpeg::decode::Decoder::new(Cursor::new(data));
123 let pixels = decoder
124 .decode()
125 .map_err(|e| Error::invalid(format!("jpeg decode: {e}")))?;
126 let info = decoder
127 .info()
128 .ok_or_else(|| Error::invalid("jpeg decode: missing image info"))?;
129 let (w, h) = (info.width as usize, info.height as usize);
130
131 let (planes, stride) = match info.pixel_format {
132 rusty_jpeg::decode::PixelFormat::RGB24 => (pixels, w * 3),
133 rusty_jpeg::decode::PixelFormat::L8 => {
134 let mut rgb = vec![0u8; w * h * 3];
136 for (i, &g) in pixels.iter().enumerate() {
137 rgb[i * 3..i * 3 + 3].copy_from_slice(&[g, g, g]);
138 }
139 (rgb, w * 3)
140 }
141 other => {
142 return Err(Error::unsupported(format!(
143 "jpeg decode: pixel format {other:?} (only RGB24/L8)"
144 )))
145 }
146 };
147
148 Ok(Frame::Video(VideoFrame {
149 width: info.width as u32,
150 height: info.height as u32,
151 format: PixelFormat::Rgb24,
152 planes: vec![planes],
153 strides: vec![stride],
154 pts: None,
155 }))
156}
157
158struct JpegSettings {
160 quality: u8,
162 sampling: Option<SamplingFactor>,
164 progressive: bool,
165 optimize_huffman: bool,
166 restart_interval: Option<u16>,
167}
168
169impl Default for JpegSettings {
170 fn default() -> Self {
171 Self {
172 quality: DEFAULT_QUALITY,
173 sampling: None,
174 progressive: false,
175 optimize_huffman: true,
183 restart_interval: None,
184 }
185 }
186}
187
188fn quality_from_qscale(qscale: f32) -> u8 {
207 let q = 100.0 - 2.05 * qscale.max(1.0).powf(1.25);
208 q.round().clamp(1.0, 100.0) as u8
209}
210
211fn parse_sampling(value: &str) -> Option<SamplingFactor> {
212 match value.trim() {
213 "444" | "4:4:4" => Some(SamplingFactor::R_4_4_4),
214 "440" | "4:4:0" => Some(SamplingFactor::R_4_4_0),
215 "422" | "4:2:2" => Some(SamplingFactor::R_4_2_2),
216 "420" | "4:2:0" => Some(SamplingFactor::R_4_2_0),
217 "411" | "4:1:1" => Some(SamplingFactor::R_4_1_1),
218 _ => None,
219 }
220}
221
222fn parse_bool(value: &str) -> Option<bool> {
223 match value.trim() {
224 "1" | "true" | "yes" | "on" => Some(true),
225 "0" | "false" | "no" | "off" => Some(false),
226 _ => None,
227 }
228}
229
230#[derive(Default)]
231struct JpegEncoder {
232 packet: Option<Packet>,
233 eof: bool,
234 settings: JpegSettings,
235}
236
237impl RffEncoder for JpegEncoder {
238 fn accepted_pixel_formats(&self) -> Option<Vec<PixelFormat>> {
242 Some(vec![
243 PixelFormat::Yuv420p,
244 PixelFormat::Yuv422p,
245 PixelFormat::Yuv444p,
246 PixelFormat::Rgb24,
247 PixelFormat::Rgba,
248 ])
249 }
250
251 fn configure(&mut self, options: &Dictionary) -> Result<()> {
252 for key in ["q", "qscale", "qp", "crf"] {
255 if let Some(v) = options.get(key).and_then(|v| v.trim().parse::<f32>().ok()) {
256 self.settings.quality = quality_from_qscale(v);
257 break;
258 }
259 }
260 if let Some(v) = options
261 .get("jpeg_quality")
262 .and_then(|v| v.trim().parse::<u8>().ok())
263 {
264 self.settings.quality = v.clamp(1, 100);
265 }
266 if let Some(v) = options
267 .get("sampling")
268 .or_else(|| options.get("jpeg_sampling"))
269 {
270 match parse_sampling(v) {
271 Some(s) => self.settings.sampling = Some(s),
272 None => {
273 return Err(Error::unsupported(format!(
274 "jpeg encode: unknown sampling `{v}` (want 444/440/422/420/411)"
275 )))
276 }
277 }
278 }
279 if let Some(v) = options.get("progressive").and_then(|v| parse_bool(v)) {
280 self.settings.progressive = v;
281 }
282 if let Some(v) = options.get("optimize_huffman").and_then(|v| parse_bool(v)) {
283 self.settings.optimize_huffman = v;
284 }
285 if let Some(v) = options
286 .get("restart_interval")
287 .and_then(|v| v.trim().parse::<u16>().ok())
288 {
289 self.settings.restart_interval = Some(v);
290 }
291 Ok(())
292 }
293
294 fn send_frame(&mut self, frame: &Frame) -> Result<()> {
295 let vf = match frame {
296 Frame::Video(v) => v,
297 Frame::Audio(_) => {
298 return Err(Error::unsupported(
299 "jpeg encode: audio frame on an image codec",
300 ))
301 }
302 };
303 self.packet = Some(Packet::from_data(0, encode_jpeg(vf, &self.settings)?));
304 Ok(())
305 }
306
307 fn receive_packet(&mut self) -> Result<Packet> {
308 if let Some(packet) = self.packet.take() {
309 return Ok(packet);
310 }
311 if self.eof {
312 Err(Error::Eof)
313 } else {
314 Err(Error::Again)
315 }
316 }
317
318 fn flush(&mut self) {
319 self.eof = true;
320 }
321}
322
323fn encode_jpeg(vf: &VideoFrame, settings: &JpegSettings) -> Result<Vec<u8>> {
324 if vf.width > u16::MAX as u32 || vf.height > u16::MAX as u32 {
325 return Err(Error::unsupported("jpeg encode: dimensions exceed 65535"));
326 }
327 if let Some(sub) = planar_subsampling(vf.format) {
332 return encode_planar(vf, settings, sub);
333 }
334 let (color, channels) = match vf.format {
335 PixelFormat::Rgb24 => (ColorType::Rgb, 3usize),
336 PixelFormat::Rgba => (ColorType::Rgba, 4usize),
337 other => {
338 return Err(Error::unsupported(format!(
339 "jpeg encode: needs rgb24/rgba or planar yuv, got `{}` (convert with -vf format=rgb24)",
340 other.name()
341 )))
342 }
343 };
344 let (w, h) = (vf.width as usize, vf.height as usize);
345 let row = w * channels;
346 let stride = vf.strides[0];
347
348 let packed: Vec<u8> = if stride == row {
350 vf.planes[0].clone()
351 } else {
352 let mut p = Vec::with_capacity(row * h);
353 for j in 0..h {
354 p.extend_from_slice(&vf.planes[0][j * stride..j * stride + row]);
355 }
356 p
357 };
358
359 let mut out = Vec::new();
360 let mut encoder = Encoder::new(&mut out, settings.quality);
361 if let Some(sampling) = settings.sampling {
362 encoder.set_sampling_factor(sampling);
363 }
364 encoder.set_progressive(settings.progressive);
365 encoder.set_optimized_huffman_tables(settings.optimize_huffman);
366 if let Some(interval) = settings.restart_interval {
367 encoder.set_restart_interval(interval);
368 }
369 encoder
370 .encode(&packed, vf.width as u16, vf.height as u16, color)
371 .map_err(|e| Error::invalid(format!("jpeg encode: {e}")))?;
372 Ok(out)
373}
374
375fn planar_subsampling(format: PixelFormat) -> Option<(usize, usize)> {
378 match format {
379 PixelFormat::Yuv444p => Some((1, 1)),
380 PixelFormat::Yuv422p => Some((2, 1)),
381 PixelFormat::Yuv420p => Some((2, 2)),
382 _ => None,
383 }
384}
385
386fn encode_planar(vf: &VideoFrame, settings: &JpegSettings, sub: (usize, usize)) -> Result<Vec<u8>> {
389 if vf.planes.len() < 3 || vf.strides.len() < 3 {
390 return Err(Error::invalid(
391 "jpeg encode: planar yuv frame is missing planes",
392 ));
393 }
394 let image = rusty_jpeg::encode::PlanarYcbcrImage::new(
395 &vf.planes[0],
396 &vf.planes[1],
397 &vf.planes[2],
398 [vf.strides[0], vf.strides[1], vf.strides[2]],
399 vf.width as u16,
400 vf.height as u16,
401 sub,
402 )
403 .ok_or_else(|| Error::invalid("jpeg encode: planar yuv planes too small for the frame"))?;
404
405 let native = image.sampling_factor();
410 if let Some(requested) = settings.sampling {
411 if requested != native {
412 return Err(Error::unsupported(format!(
413 "jpeg encode: -sampling conflicts with the source's chroma layout \
414 (`{}`); convert first with `-vf format=...`, or drop -sampling to \
415 encode the source layout as-is",
416 vf.format.name()
417 )));
418 }
419 }
420
421 let mut out = Vec::new();
422 let mut encoder = Encoder::new(&mut out, settings.quality);
423 encoder.set_sampling_factor(native);
424 encoder.set_progressive(settings.progressive);
425 encoder.set_optimized_huffman_tables(settings.optimize_huffman);
426 if let Some(interval) = settings.restart_interval {
427 encoder.set_restart_interval(interval);
428 }
429 encoder
430 .encode_image(image)
431 .map_err(|e| Error::invalid(format!("jpeg encode: {e}")))?;
432 Ok(out)
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[test]
440 fn jpeg_encode_decode_roundtrips_approximately() {
441 let (w, h) = (48u32, 32u32);
442 let (wi, hi) = (w as usize, h as usize);
443 let mut rgb = vec![0u8; wi * hi * 3];
444 for j in 0..hi {
445 for i in 0..wi {
446 let o = (j * wi + i) * 3;
447 rgb[o] = (i * 255 / (wi - 1)) as u8;
448 rgb[o + 1] = (j * 255 / (hi - 1)) as u8;
449 rgb[o + 2] = 128;
450 }
451 }
452 let src = VideoFrame {
453 width: w,
454 height: h,
455 format: PixelFormat::Rgb24,
456 planes: vec![rgb.clone()],
457 strides: vec![wi * 3],
458 pts: None,
459 };
460
461 let bytes = encode_jpeg(&src, &JpegSettings::default()).unwrap();
462 assert_eq!(&bytes[0..3], &[0xFF, 0xD8, 0xFF]); let Frame::Video(decoded) = decode_jpeg(&bytes).unwrap() else {
465 unreachable!()
466 };
467 assert_eq!((decoded.width, decoded.height), (w, h));
468 assert_eq!(decoded.format, PixelFormat::Rgb24);
469 let total: u64 = rgb
471 .iter()
472 .zip(&decoded.planes[0])
473 .map(|(a, b)| (*a as i16 - *b as i16).unsigned_abs() as u64)
474 .sum();
475 let mean = total as f64 / (wi * hi * 3) as f64;
476 assert!(mean < 12.0, "jpeg round-trip drifted too far: {mean:.2}");
477 }
478}