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 trellis: bool,
168 restart_interval: Option<u16>,
169}
170
171impl Default for JpegSettings {
172 fn default() -> Self {
173 Self {
174 quality: DEFAULT_QUALITY,
175 sampling: None,
176 progressive: false,
177 optimize_huffman: false,
184 trellis: false,
187 restart_interval: None,
188 }
189 }
190}
191
192fn quality_from_qscale(qscale: f32) -> u8 {
211 let q = 100.0 - 2.05 * qscale.max(1.0).powf(1.25);
212 q.round().clamp(1.0, 100.0) as u8
213}
214
215fn parse_sampling(value: &str) -> Option<SamplingFactor> {
216 match value.trim() {
217 "444" | "4:4:4" => Some(SamplingFactor::R_4_4_4),
218 "440" | "4:4:0" => Some(SamplingFactor::R_4_4_0),
219 "422" | "4:2:2" => Some(SamplingFactor::R_4_2_2),
220 "420" | "4:2:0" => Some(SamplingFactor::R_4_2_0),
221 "411" | "4:1:1" => Some(SamplingFactor::R_4_1_1),
222 _ => None,
223 }
224}
225
226fn parse_bool(value: &str) -> Option<bool> {
227 match value.trim() {
228 "1" | "true" | "yes" | "on" => Some(true),
229 "0" | "false" | "no" | "off" => Some(false),
230 _ => None,
231 }
232}
233
234#[derive(Default)]
235struct JpegEncoder {
236 packet: Option<Packet>,
237 eof: bool,
238 settings: JpegSettings,
239}
240
241impl RffEncoder for JpegEncoder {
242 fn accepted_pixel_formats(&self) -> Option<Vec<PixelFormat>> {
246 Some(vec![
247 PixelFormat::Yuv420p,
248 PixelFormat::Yuv422p,
249 PixelFormat::Yuv444p,
250 PixelFormat::Rgb24,
251 PixelFormat::Rgba,
252 ])
253 }
254
255 fn configure(&mut self, options: &Dictionary) -> Result<()> {
256 for key in ["q", "qscale", "qp", "crf"] {
259 if let Some(v) = options.get(key).and_then(|v| v.trim().parse::<f32>().ok()) {
260 self.settings.quality = quality_from_qscale(v);
261 break;
262 }
263 }
264 if let Some(v) = options
265 .get("jpeg_quality")
266 .and_then(|v| v.trim().parse::<u8>().ok())
267 {
268 self.settings.quality = v.clamp(1, 100);
269 }
270 if let Some(v) = options
271 .get("sampling")
272 .or_else(|| options.get("jpeg_sampling"))
273 {
274 match parse_sampling(v) {
275 Some(s) => self.settings.sampling = Some(s),
276 None => {
277 return Err(Error::unsupported(format!(
278 "jpeg encode: unknown sampling `{v}` (want 444/440/422/420/411)"
279 )))
280 }
281 }
282 }
283 if let Some(v) = options.get("progressive").and_then(|v| parse_bool(v)) {
284 self.settings.progressive = v;
285 }
286 if let Some(v) = options.get("trellis").and_then(|v| parse_bool(v)) {
287 self.settings.trellis = v;
288 }
289 if let Some(v) = options.get("optimize_huffman").and_then(|v| parse_bool(v)) {
290 self.settings.optimize_huffman = v;
291 }
292 if let Some(v) = options
293 .get("restart_interval")
294 .and_then(|v| v.trim().parse::<u16>().ok())
295 {
296 self.settings.restart_interval = Some(v);
297 }
298 Ok(())
299 }
300
301 fn send_frame(&mut self, frame: &Frame) -> Result<()> {
302 let vf = match frame {
303 Frame::Video(v) => v,
304 Frame::Audio(_) => {
305 return Err(Error::unsupported(
306 "jpeg encode: audio frame on an image codec",
307 ))
308 }
309 };
310 self.packet = Some(Packet::from_data(0, encode_jpeg(vf, &self.settings)?));
311 Ok(())
312 }
313
314 fn receive_packet(&mut self) -> Result<Packet> {
315 if let Some(packet) = self.packet.take() {
316 return Ok(packet);
317 }
318 if self.eof {
319 Err(Error::Eof)
320 } else {
321 Err(Error::Again)
322 }
323 }
324
325 fn flush(&mut self) {
326 self.eof = true;
327 }
328}
329
330fn encode_jpeg(vf: &VideoFrame, settings: &JpegSettings) -> Result<Vec<u8>> {
331 if vf.width > u16::MAX as u32 || vf.height > u16::MAX as u32 {
332 return Err(Error::unsupported("jpeg encode: dimensions exceed 65535"));
333 }
334 if let Some(sub) = planar_subsampling(vf.format) {
339 return encode_planar(vf, settings, sub);
340 }
341 let (color, channels) = match vf.format {
342 PixelFormat::Rgb24 => (ColorType::Rgb, 3usize),
343 PixelFormat::Rgba => (ColorType::Rgba, 4usize),
344 other => {
345 return Err(Error::unsupported(format!(
346 "jpeg encode: needs rgb24/rgba or planar yuv, got `{}` (convert with -vf format=rgb24)",
347 other.name()
348 )))
349 }
350 };
351 let (w, h) = (vf.width as usize, vf.height as usize);
352 let row = w * channels;
353 let stride = vf.strides[0];
354
355 let packed: Vec<u8> = if stride == row {
357 vf.planes[0].clone()
358 } else {
359 let mut p = Vec::with_capacity(row * h);
360 for j in 0..h {
361 p.extend_from_slice(&vf.planes[0][j * stride..j * stride + row]);
362 }
363 p
364 };
365
366 let mut out = Vec::new();
367 let mut encoder = Encoder::new(&mut out, settings.quality);
368 if let Some(sampling) = settings.sampling {
369 encoder.set_sampling_factor(sampling);
370 }
371 encoder.set_progressive(settings.progressive);
372 encoder.set_optimized_huffman_tables(settings.optimize_huffman);
373 encoder.set_trellis(settings.trellis);
374 if let Some(interval) = settings.restart_interval {
375 encoder.set_restart_interval(interval);
376 }
377 encoder
378 .encode(&packed, vf.width as u16, vf.height as u16, color)
379 .map_err(|e| Error::invalid(format!("jpeg encode: {e}")))?;
380 Ok(out)
381}
382
383fn planar_subsampling(format: PixelFormat) -> Option<(usize, usize)> {
386 match format {
387 PixelFormat::Yuv444p => Some((1, 1)),
388 PixelFormat::Yuv422p => Some((2, 1)),
389 PixelFormat::Yuv420p => Some((2, 2)),
390 _ => None,
391 }
392}
393
394fn encode_planar(vf: &VideoFrame, settings: &JpegSettings, sub: (usize, usize)) -> Result<Vec<u8>> {
397 if vf.planes.len() < 3 || vf.strides.len() < 3 {
398 return Err(Error::invalid(
399 "jpeg encode: planar yuv frame is missing planes",
400 ));
401 }
402 let image = rusty_jpeg::encode::PlanarYcbcrImage::new(
403 &vf.planes[0],
404 &vf.planes[1],
405 &vf.planes[2],
406 [vf.strides[0], vf.strides[1], vf.strides[2]],
407 vf.width as u16,
408 vf.height as u16,
409 sub,
410 )
411 .ok_or_else(|| Error::invalid("jpeg encode: planar yuv planes too small for the frame"))?;
412
413 let native = image.sampling_factor();
418 if let Some(requested) = settings.sampling {
419 if requested != native {
420 return Err(Error::unsupported(format!(
421 "jpeg encode: -sampling conflicts with the source's chroma layout \
422 (`{}`); convert first with `-vf format=...`, or drop -sampling to \
423 encode the source layout as-is",
424 vf.format.name()
425 )));
426 }
427 }
428
429 let mut out = Vec::new();
430 let mut encoder = Encoder::new(&mut out, settings.quality);
431 encoder.set_sampling_factor(native);
432 encoder.set_progressive(settings.progressive);
433 encoder.set_optimized_huffman_tables(settings.optimize_huffman);
434 encoder.set_trellis(settings.trellis);
435 if let Some(interval) = settings.restart_interval {
436 encoder.set_restart_interval(interval);
437 }
438 encoder
439 .encode_image(image)
440 .map_err(|e| Error::invalid(format!("jpeg encode: {e}")))?;
441 Ok(out)
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[test]
449 fn jpeg_encode_decode_roundtrips_approximately() {
450 let (w, h) = (48u32, 32u32);
451 let (wi, hi) = (w as usize, h as usize);
452 let mut rgb = vec![0u8; wi * hi * 3];
453 for j in 0..hi {
454 for i in 0..wi {
455 let o = (j * wi + i) * 3;
456 rgb[o] = (i * 255 / (wi - 1)) as u8;
457 rgb[o + 1] = (j * 255 / (hi - 1)) as u8;
458 rgb[o + 2] = 128;
459 }
460 }
461 let src = VideoFrame {
462 width: w,
463 height: h,
464 format: PixelFormat::Rgb24,
465 planes: vec![rgb.clone()],
466 strides: vec![wi * 3],
467 pts: None,
468 };
469
470 let bytes = encode_jpeg(&src, &JpegSettings::default()).unwrap();
471 assert_eq!(&bytes[0..3], &[0xFF, 0xD8, 0xFF]); let Frame::Video(decoded) = decode_jpeg(&bytes).unwrap() else {
474 unreachable!()
475 };
476 assert_eq!((decoded.width, decoded.height), (w, h));
477 assert_eq!(decoded.format, PixelFormat::Yuv444p);
481 assert_eq!(decoded.planes.len(), 3);
482
483 let (yp, cb, cr) = (&decoded.planes[0], &decoded.planes[1], &decoded.planes[2]);
487 let (ys, cbs, crs) = (decoded.strides[0], decoded.strides[1], decoded.strides[2]);
488 let mut total = 0u64;
489 for j in 0..hi {
490 for i in 0..wi {
491 let yy = yp[j * ys + i] as f32;
492 let b = cb[j * cbs + i] as f32 - 128.0;
493 let r = cr[j * crs + i] as f32 - 128.0;
494 let got = [
495 yy + 1.402 * r,
496 yy - 0.344_136 * b - 0.714_136 * r,
497 yy + 1.772 * b,
498 ];
499 for (c, g) in got.iter().enumerate() {
500 let want = rgb[(j * wi + i) * 3 + c] as f32;
501 total += (g.clamp(0.0, 255.0) - want).abs() as u64;
502 }
503 }
504 }
505 let mean = total as f64 / (wi * hi * 3) as f64;
506 assert!(mean < 12.0, "jpeg round-trip drifted too far: {mean:.2}");
507 }
508}