Skip to main content

oxideav_mjpeg/
registry.rs

1//! `oxideav-core` integration layer for `oxideav-mjpeg`.
2//!
3//! Gated behind the default-on `registry` feature so image-library
4//! consumers can depend on `oxideav-mjpeg` with `default-features = false`
5//! and skip the `oxideav-core` dependency entirely.
6//!
7//! The module exposes:
8//! * [`register`] / [`register_codecs`] / [`register_containers`] — the
9//!   `CodecRegistry` / `ContainerRegistry` entry points the umbrella
10//!   `oxideav` crate calls during framework initialisation.
11//! * The [`MjpegEncoder`] struct that implements the framework
12//!   `Encoder` trait, plus the corresponding `MjpegDecoder` that
13//!   implements the `Decoder` trait. Both wrap the framework-free
14//!   [`crate::decoder::decode_jpeg`] / `encode_jpeg_*` entry points
15//!   defined in [`crate::encoder`].
16//! * The `From<MjpegError> for oxideav_core::Error` and
17//!   `From<MjpegFrame> for oxideav_core::VideoFrame` /
18//!   `From<MjpegPixelFormat> for oxideav_core::PixelFormat`
19//!   conversions used by the trait impls below.
20
21use std::collections::VecDeque;
22
23use oxideav_core::frame::VideoPlane;
24use oxideav_core::{
25    CodecCapabilities, CodecId, CodecInfo, CodecParameters, CodecRegistry, CodecTag,
26    ContainerRegistry, Decoder, Encoder, Error, Frame, MediaType, Packet, PixelFormat, Result,
27    RuntimeContext, TimeBase, VideoFrame,
28};
29
30use crate::container;
31use crate::decoder::decode_jpeg;
32use crate::encoder::{
33    encode_jpeg_cmyk, encode_jpeg_cmyk_progressive, encode_jpeg_grayscale_with_opts,
34    encode_jpeg_progressive, encode_jpeg_progressive_grayscale, encode_jpeg_rgb24_with_opts,
35    encode_jpeg_with_opts, encode_lossless_jpeg_grayscale, DEFAULT_QUALITY,
36};
37use crate::error::MjpegError;
38use crate::image::{MjpegFrame, MjpegPixelFormat, MjpegPlane};
39use crate::mjpeg_container;
40use crate::CODEC_ID_STR;
41
42// ---- Error / pixel-format / frame conversions --------------------------
43
44impl From<MjpegError> for Error {
45    fn from(e: MjpegError) -> Self {
46        match e {
47            MjpegError::InvalidData(s) => Error::InvalidData(s),
48            MjpegError::Unsupported(s) => Error::Unsupported(s),
49            MjpegError::Other(s) => Error::Other(s),
50            MjpegError::Eof => Error::Eof,
51            MjpegError::NeedMore => Error::NeedMore,
52        }
53    }
54}
55
56impl From<MjpegPixelFormat> for PixelFormat {
57    fn from(p: MjpegPixelFormat) -> Self {
58        match p {
59            MjpegPixelFormat::Gray8 => PixelFormat::Gray8,
60            MjpegPixelFormat::Gray10Le => PixelFormat::Gray10Le,
61            MjpegPixelFormat::Gray12Le => PixelFormat::Gray12Le,
62            MjpegPixelFormat::Gray16Le => PixelFormat::Gray16Le,
63            MjpegPixelFormat::Cmyk => PixelFormat::Cmyk,
64            MjpegPixelFormat::Rgb24 => PixelFormat::Rgb24,
65            MjpegPixelFormat::Rgb48Le => PixelFormat::Rgb48Le,
66            MjpegPixelFormat::Gbrp10Le => PixelFormat::Gbrp10Le,
67            MjpegPixelFormat::Gbrp12Le => PixelFormat::Gbrp12Le,
68            MjpegPixelFormat::Gbrp14Le => PixelFormat::Gbrp14Le,
69            MjpegPixelFormat::Yuv411P => PixelFormat::Yuv411P,
70            MjpegPixelFormat::Yuv420P => PixelFormat::Yuv420P,
71            MjpegPixelFormat::Yuv422P => PixelFormat::Yuv422P,
72            MjpegPixelFormat::Yuv444P => PixelFormat::Yuv444P,
73            MjpegPixelFormat::Yuv420P12Le => PixelFormat::Yuv420P12Le,
74            MjpegPixelFormat::Yuv422P12Le => PixelFormat::Yuv422P12Le,
75            MjpegPixelFormat::Yuv444P12Le => PixelFormat::Yuv444P12Le,
76        }
77    }
78}
79
80/// Inverse of [`From<MjpegPixelFormat> for PixelFormat`]. Returns
81/// `None` for any pixel format the JPEG codec does not produce or
82/// accept (so the encoder can reject unsupported `CodecParameters`
83/// up-front rather than failing inside `encode_jpeg_*`).
84fn pix_to_local(p: PixelFormat) -> Option<MjpegPixelFormat> {
85    Some(match p {
86        PixelFormat::Gray8 => MjpegPixelFormat::Gray8,
87        PixelFormat::Gray10Le => MjpegPixelFormat::Gray10Le,
88        PixelFormat::Gray12Le => MjpegPixelFormat::Gray12Le,
89        PixelFormat::Gray16Le => MjpegPixelFormat::Gray16Le,
90        PixelFormat::Cmyk => MjpegPixelFormat::Cmyk,
91        PixelFormat::Rgb24 => MjpegPixelFormat::Rgb24,
92        PixelFormat::Rgb48Le => MjpegPixelFormat::Rgb48Le,
93        PixelFormat::Gbrp10Le => MjpegPixelFormat::Gbrp10Le,
94        PixelFormat::Gbrp12Le => MjpegPixelFormat::Gbrp12Le,
95        PixelFormat::Gbrp14Le => MjpegPixelFormat::Gbrp14Le,
96        PixelFormat::Yuv411P => MjpegPixelFormat::Yuv411P,
97        PixelFormat::Yuv420P => MjpegPixelFormat::Yuv420P,
98        PixelFormat::Yuv422P => MjpegPixelFormat::Yuv422P,
99        PixelFormat::Yuv444P => MjpegPixelFormat::Yuv444P,
100        PixelFormat::Yuv420P12Le => MjpegPixelFormat::Yuv420P12Le,
101        PixelFormat::Yuv422P12Le => MjpegPixelFormat::Yuv422P12Le,
102        PixelFormat::Yuv444P12Le => MjpegPixelFormat::Yuv444P12Le,
103        _ => return None,
104    })
105}
106
107impl From<MjpegFrame> for VideoFrame {
108    fn from(f: MjpegFrame) -> Self {
109        VideoFrame {
110            pts: f.pts,
111            planes: f
112                .planes
113                .into_iter()
114                .map(|p| VideoPlane {
115                    stride: p.stride,
116                    data: p.data,
117                })
118                .collect(),
119        }
120    }
121}
122
123impl From<MjpegPlane> for VideoPlane {
124    fn from(p: MjpegPlane) -> Self {
125        VideoPlane {
126            stride: p.stride,
127            data: p.data,
128        }
129    }
130}
131
132// ---- CodecRegistry / ContainerRegistry entry points --------------------
133
134/// Register the JPEG / MJPEG codec (decoder + encoder) into the
135/// supplied [`CodecRegistry`].
136///
137/// Kept as a free function (rather than a method on a registry handle)
138/// so it matches the registration shape used by the umbrella
139/// `oxideav` crate.
140pub fn register_codecs(reg: &mut CodecRegistry) {
141    let caps = CodecCapabilities::video("mjpeg_sw")
142        .with_lossy(true)
143        .with_intra_only(true)
144        .with_max_size(16384, 16384);
145    reg.register(
146        CodecInfo::new(CodecId::new(CODEC_ID_STR))
147            .capabilities(caps)
148            .decoder(make_decoder)
149            .encoder(make_encoder)
150            .tags([
151                // AVI FourCC claims — all unambiguous MJPEG variants.
152                CodecTag::fourcc(b"MJPG"),
153                CodecTag::fourcc(b"AVRN"),
154                CodecTag::fourcc(b"LJPG"),
155                CodecTag::fourcc(b"JPGL"),
156            ]),
157    );
158}
159
160/// Register both JPEG-family containers:
161///
162/// - `jpeg` — still-image (`.jpg` / `.jpeg` / `.jpe` / `.jfif`), single
163///   packet per file.
164/// - `mjpeg-raw` — raw Motion-JPEG (`.mjpeg` / `.mjpg`), concatenated
165///   SOI..EOI frames, one packet per frame, with seek support.
166///
167/// Must be called alongside [`register_codecs`] when wiring up a
168/// pipeline that expects to read or write JPEG-family files.
169pub fn register_containers(reg: &mut ContainerRegistry) {
170    container::register(reg);
171    mjpeg_container::register(reg);
172}
173
174/// Unified entry point: install every codec and container provided by
175/// `oxideav-mjpeg` into a [`RuntimeContext`].
176///
177/// Also wired into [`oxideav_meta::register_all`] via the
178/// [`oxideav_core::register!`] macro below.
179pub fn register(ctx: &mut RuntimeContext) {
180    register_codecs(&mut ctx.codecs);
181    register_containers(&mut ctx.containers);
182}
183
184oxideav_core::register!("mjpeg", register);
185
186// ---- Decoder trait impl ------------------------------------------------
187
188pub fn make_decoder(params: &CodecParameters) -> Result<Box<dyn Decoder>> {
189    let codec_id = params.codec_id.clone();
190    Ok(Box::new(MjpegDecoder {
191        codec_id,
192        pending: None,
193        eof: false,
194    }))
195}
196
197struct MjpegDecoder {
198    codec_id: CodecId,
199    pending: Option<Packet>,
200    eof: bool,
201}
202
203impl Decoder for MjpegDecoder {
204    fn codec_id(&self) -> &CodecId {
205        &self.codec_id
206    }
207
208    fn send_packet(&mut self, packet: &Packet) -> Result<()> {
209        if self.pending.is_some() {
210            return Err(Error::other(
211                "MJPEG decoder: receive_frame must be called before sending another packet",
212            ));
213        }
214        self.pending = Some(packet.clone());
215        Ok(())
216    }
217
218    fn receive_frame(&mut self) -> Result<Frame> {
219        let Some(pkt) = self.pending.take() else {
220            return if self.eof {
221                Err(Error::Eof)
222            } else {
223                Err(Error::NeedMore)
224            };
225        };
226        // With the `registry` feature on, `decode_jpeg` already
227        // returns `oxideav_core::VideoFrame` (see the conditional
228        // alias in `decoder.rs`), so the trait surface needs nothing
229        // more than wrapping it in `Frame::Video`.
230        let vf = decode_jpeg(&pkt.data, pkt.pts)?;
231        Ok(Frame::Video(vf))
232    }
233
234    fn flush(&mut self) -> Result<()> {
235        self.eof = true;
236        Ok(())
237    }
238}
239
240// ---- Encoder trait impl ------------------------------------------------
241
242pub fn make_encoder(params: &CodecParameters) -> Result<Box<dyn Encoder>> {
243    Ok(MjpegEncoder::from_params(params)?)
244}
245
246/// JPEG encoder. Emits one self-contained JPEG bitstream (baseline SOF0
247/// or progressive SOF2) per video frame.
248pub struct MjpegEncoder {
249    output_params: CodecParameters,
250    pub(crate) width: u32,
251    pub(crate) height: u32,
252    pub(crate) pix: MjpegPixelFormat,
253    quality: u8,
254    /// MCU-per-restart-interval count. 0 disables DRI / `RSTn` emission.
255    /// Restart intervals are only honoured on the baseline (SOF0) path
256    /// for now; progressive emission ignores this field.
257    restart_interval: u16,
258    /// When true, emit SOF2 + multi-scan (spectral selection only).
259    progressive: bool,
260    /// When true, take the lossless (SOF3) path for single-component
261    /// grayscale input. Ignored for any non-grayscale `pix`.
262    lossless: bool,
263    /// Lossless predictor selector (T.81 Table H.1, 1..=7). Only
264    /// consulted on the lossless path. Defaults to 1 (Ra / left).
265    lossless_predictor: u8,
266    /// Adobe APP14 colour-transform marker for 4-component
267    /// (`MjpegPixelFormat::Cmyk`) input.
268    ///
269    /// * `None`     — no APP14 segment (plain "regular" CMYK).
270    /// * `Some(0)`  — Adobe CMYK: encoder inverts every component on
271    ///   the wire; decoder un-inverts on output.
272    /// * `Some(2)`  — Adobe YCCK: the packed input is interpreted as
273    ///   `[Y, Cb, Cr, K]` and only the K plane is inverted on the
274    ///   wire; the decoder performs YCbCr→RGB→CMY and flips K to
275    ///   recover CMYK.
276    ///
277    /// Defaults to `None`. Ignored for non-CMYK pixel formats.
278    cmyk_adobe_transform: Option<u8>,
279    time_base: TimeBase,
280    pending: VecDeque<Packet>,
281    eof: bool,
282}
283
284impl MjpegEncoder {
285    /// Build a concrete `MjpegEncoder` from video codec parameters.
286    /// Preferred over `make_encoder` when the caller wants to tweak
287    /// encoder-specific knobs (e.g. progressive mode, restart interval)
288    /// before feeding frames.
289    pub fn from_params(params: &CodecParameters) -> Result<Box<Self>> {
290        let width = params
291            .width
292            .ok_or_else(|| Error::invalid("MJPEG encoder: missing width"))?;
293        let height = params
294            .height
295            .ok_or_else(|| Error::invalid("MJPEG encoder: missing height"))?;
296        let pix_core = params.pixel_format.unwrap_or(PixelFormat::Yuv420P);
297        let pix = pix_to_local(pix_core).ok_or_else(|| {
298            Error::unsupported(format!(
299                "MJPEG encoder: pixel format {pix_core:?} not supported"
300            ))
301        })?;
302        match pix {
303            MjpegPixelFormat::Yuv420P | MjpegPixelFormat::Yuv422P | MjpegPixelFormat::Yuv444P => {}
304            // Grayscale takes the lossless (SOF3) path when requested via
305            // `set_lossless(true)`. Accepting it here lets callers wire a
306            // grayscale `CodecParameters` through the trait API directly
307            // rather than dropping to the standalone `encode_lossless_*`
308            // function.
309            MjpegPixelFormat::Gray8
310            | MjpegPixelFormat::Gray10Le
311            | MjpegPixelFormat::Gray12Le
312            | MjpegPixelFormat::Gray16Le => {}
313            // 4-component CMYK / YCCK input takes the dedicated CMYK
314            // encode path (baseline SOF0 by default, SOF2 when
315            // `set_progressive(true)` is used). Adobe APP14 transform
316            // selection comes from `set_adobe_transform`; default is
317            // no APP14 (plain "regular" CMYK).
318            MjpegPixelFormat::Cmyk => {}
319            // Packed `Rgb24` input takes the baseline-SOF0 RGB encode
320            // path: three components at IDs 'R'/'G'/'B', all H = V = 1,
321            // all bound to one quant table + one DC/AC Huffman pair.
322            // Adobe APP14 with transform = 0 is emitted so any
323            // conformant decoder honouring the colour-transform flag
324            // round-trips the samples as plain R/G/B. Progressive (SOF2)
325            // emission of RGB stays a follow-up for now; the lossless
326            // (SOF3) path is already available via `set_lossless(true)`
327            // on the existing 3-component lossless encoder if a caller
328            // needs bit-exactness.
329            MjpegPixelFormat::Rgb24 => {}
330            _ => {
331                return Err(Error::unsupported(format!(
332                    "MJPEG encoder: pixel format {pix_core:?} not supported"
333                )))
334            }
335        }
336
337        let mut output_params = params.clone();
338        output_params.media_type = MediaType::Video;
339        output_params.codec_id = CodecId::new(CODEC_ID_STR);
340        output_params.width = Some(width);
341        output_params.height = Some(height);
342        output_params.pixel_format = Some(pix.into());
343
344        Ok(Box::new(Self {
345            output_params,
346            width,
347            height,
348            pix,
349            quality: DEFAULT_QUALITY,
350            restart_interval: 0,
351            progressive: false,
352            // Lossless mode is opt-in even for grayscale. `Gray8` input
353            // takes the baseline (SOF0) single-component DCT path by
354            // default; flip `set_lossless(true)` to switch to the
355            // bit-exact SOF3 path instead.
356            lossless: false,
357            lossless_predictor: 1,
358            cmyk_adobe_transform: None,
359            time_base: params
360                .frame_rate
361                .map_or(TimeBase::new(1, 90_000), |r| TimeBase::new(r.den, r.num)),
362            pending: VecDeque::new(),
363            eof: false,
364        }))
365    }
366
367    /// Set the restart interval in MCUs (JPEG DRI field). `0` disables
368    /// restart marker emission (matches the default).
369    ///
370    /// Values are clamped to `u16::MAX` since the JPEG DRI field is a
371    /// 16-bit big-endian unsigned integer.
372    ///
373    /// Currently only applied on the baseline encode path; enabling
374    /// progressive output via [`Self::set_progressive`] suppresses
375    /// restart-marker emission.
376    pub fn set_restart_interval(&mut self, mcus: u32) {
377        self.restart_interval = mcus.min(u16::MAX as u32) as u16;
378    }
379
380    /// Current restart interval (MCUs between `RSTn` markers; 0 = off).
381    pub fn restart_interval(&self) -> u16 {
382        self.restart_interval
383    }
384
385    /// Enable or disable progressive (SOF2) JPEG emission. When enabled
386    /// the encoder produces one DC-first scan plus two per-component AC
387    /// band scans (Ss=1..5 then Ss=6..63). See module-level docs.
388    pub fn set_progressive(&mut self, on: bool) {
389        self.progressive = on;
390    }
391
392    /// True when progressive emission is enabled.
393    pub fn progressive(&self) -> bool {
394        self.progressive
395    }
396
397    /// Enable or disable lossless (SOF3) emission. Only honoured when
398    /// the input pixel format is `Gray8` / `Gray10Le` / `Gray12Le` /
399    /// `Gray16Le`; ignored for YUV inputs (which always take the
400    /// baseline / progressive DCT path).
401    ///
402    /// For `Gray8` input the flag is a real toggle: `false` takes the
403    /// baseline (SOF0) single-component DCT path (lossy, scaled by
404    /// `quality`), `true` takes the bit-exact lossless (SOF3) path.
405    /// The three higher-precision grayscale variants
406    /// (`Gray10Le` / `Gray12Le` / `Gray16Le`) require `set_lossless(true)`
407    /// — the DCT path is 8-bit by spec.
408    ///
409    /// The lossless path is bit-exact and reuses the predictor selected
410    /// by [`Self::set_lossless_predictor`] (default 1 = Ra / left). It
411    /// ignores [`Self::set_progressive`] and [`Self::set_restart_interval`].
412    pub fn set_lossless(&mut self, on: bool) {
413        self.lossless = on;
414    }
415
416    /// True when lossless (SOF3) emission is enabled.
417    pub fn lossless(&self) -> bool {
418        self.lossless
419    }
420
421    /// Set the lossless predictor selector (T.81 Table H.1, 1..=7).
422    /// Values outside `1..=7` are silently clamped to 1 so the setter
423    /// can't fail; the value is consulted only when [`Self::set_lossless`]
424    /// has been enabled and the input is grayscale.
425    pub fn set_lossless_predictor(&mut self, predictor: u8) {
426        self.lossless_predictor = if (1..=7).contains(&predictor) {
427            predictor
428        } else {
429            1
430        };
431    }
432
433    /// Current lossless predictor selector.
434    pub fn lossless_predictor(&self) -> u8 {
435        self.lossless_predictor
436    }
437
438    /// Configure the Adobe APP14 colour-transform marker for 4-component
439    /// (`MjpegPixelFormat::Cmyk`) input. Only honoured when the input
440    /// pixel format is `Cmyk`; ignored for every other format.
441    ///
442    /// * `None`     — emit no APP14 segment (the decoder treats the
443    ///   result as plain "regular" CMYK).
444    /// * `Some(0)`  — Adobe CMYK: every component is inverted on the
445    ///   wire; the decoder un-inverts on output.
446    /// * `Some(2)`  — Adobe YCCK: the packed input is interpreted as
447    ///   `[Y, Cb, Cr, K]`, and only the K plane is inverted on the
448    ///   wire. The decoder converts YCbCr→RGB→CMY (BT.601, full-range)
449    ///   and flips K to recover CMYK.
450    ///
451    /// Any other `Some(t)` value is rejected with `Error::InvalidData`
452    /// (only `0` and `2` round-trip through this crate's decoder).
453    pub fn set_adobe_transform(&mut self, transform: Option<u8>) -> Result<()> {
454        if let Some(t) = transform {
455            if t != 0 && t != 2 {
456                return Err(Error::invalid(
457                    "MJPEG encoder: Adobe APP14 transform must be 0 (CMYK) or 2 (YCCK)",
458                ));
459            }
460        }
461        self.cmyk_adobe_transform = transform;
462        Ok(())
463    }
464
465    /// Current Adobe APP14 colour-transform marker selection.
466    pub fn adobe_transform(&self) -> Option<u8> {
467        self.cmyk_adobe_transform
468    }
469}
470
471impl Encoder for MjpegEncoder {
472    fn codec_id(&self) -> &CodecId {
473        &self.output_params.codec_id
474    }
475
476    fn output_params(&self) -> &CodecParameters {
477        &self.output_params
478    }
479
480    fn send_frame(&mut self, frame: &Frame) -> Result<()> {
481        match frame {
482            Frame::Video(v) => {
483                // With the `registry` feature on, the public
484                // `encode_jpeg_*` functions already accept
485                // `&oxideav_core::VideoFrame` directly (see the
486                // conditional alias in `encoder.rs`), so we can pass
487                // the frame through without local-type bounce.
488                let pix = self.pix.into();
489                let data = match (self.pix, self.lossless) {
490                    // Grayscale + lossless → SOF3 path. Precision is
491                    // implied by the pixel format and we read row bytes
492                    // straight from plane 0.
493                    (MjpegPixelFormat::Gray8, true)
494                    | (MjpegPixelFormat::Gray10Le, true)
495                    | (MjpegPixelFormat::Gray12Le, true)
496                    | (MjpegPixelFormat::Gray16Le, true) => {
497                        if v.planes.is_empty() {
498                            return Err(Error::invalid(
499                                "MJPEG encoder: grayscale frame missing plane 0",
500                            ));
501                        }
502                        let plane = &v.planes[0];
503                        let precision: u8 = match self.pix {
504                            MjpegPixelFormat::Gray8 => 8,
505                            MjpegPixelFormat::Gray10Le => 10,
506                            MjpegPixelFormat::Gray12Le => 12,
507                            MjpegPixelFormat::Gray16Le => 16,
508                            _ => unreachable!(),
509                        };
510                        encode_lossless_jpeg_grayscale(
511                            self.width,
512                            self.height,
513                            &plane.data,
514                            plane.stride,
515                            precision,
516                            self.lossless_predictor,
517                        )?
518                    }
519                    // 8-bit grayscale without lossless mode takes the
520                    // baseline (SOF0) or progressive (SOF2) single-
521                    // component DCT path. The baseline bitstream layout
522                    // mirrors `encode_jpeg` reduced to one luma component
523                    // (one DQT + DC/AC luma Huffman tables + a one-entry
524                    // SOS); flipping `set_progressive(true)` takes the
525                    // matching SOF2 path (DC + AC-low + AC-high scans,
526                    // spectral-selection decomposition). Either way any
527                    // conformant decoder produces a `Gray8` frame
528                    // round-tripping with the usual DCT-quantise
529                    // distortion floor. `restart_interval` is ignored
530                    // on the progressive path because the 3-component
531                    // progressive encoder doesn't expose DRI emission
532                    // either — kept consistent so the flag has the same
533                    // meaning across every progressive variant.
534                    (MjpegPixelFormat::Gray8, false) => {
535                        if v.planes.is_empty() {
536                            return Err(Error::invalid(
537                                "MJPEG encoder: grayscale frame missing plane 0",
538                            ));
539                        }
540                        let plane = &v.planes[0];
541                        if self.progressive {
542                            encode_jpeg_progressive_grayscale(
543                                self.width,
544                                self.height,
545                                &plane.data,
546                                plane.stride,
547                                self.quality,
548                            )?
549                        } else {
550                            encode_jpeg_grayscale_with_opts(
551                                self.width,
552                                self.height,
553                                &plane.data,
554                                plane.stride,
555                                self.quality,
556                                self.restart_interval,
557                            )?
558                        }
559                    }
560                    // Higher-precision grayscale (10 / 12 / 16-bit)
561                    // still requires `set_lossless(true)` — the
562                    // baseline DCT path is 8-bit by spec. Surface a
563                    // clear error rather than silently downgrading.
564                    (
565                        MjpegPixelFormat::Gray10Le
566                        | MjpegPixelFormat::Gray12Le
567                        | MjpegPixelFormat::Gray16Le,
568                        false,
569                    ) => {
570                        return Err(Error::unsupported(
571                            "MJPEG encoder: high-bit-depth grayscale input requires set_lossless(true)",
572                        ));
573                    }
574                    // Packed `Rgb24` input takes the baseline-SOF0 RGB
575                    // path. The single plane is laid out as
576                    // `[R, G, B]` at 3 bytes per pixel, matching the
577                    // decoder's `Rgb24` output shape. Progressive
578                    // (SOF2) RGB is not yet wired in here — flipping
579                    // `set_progressive(true)` with `Rgb24` input still
580                    // takes the baseline path.
581                    (MjpegPixelFormat::Rgb24, _) => {
582                        if v.planes.is_empty() {
583                            return Err(Error::invalid(
584                                "MJPEG encoder: RGB24 frame missing plane 0",
585                            ));
586                        }
587                        let plane = &v.planes[0];
588                        let min_stride = (self.width as usize) * 3;
589                        if plane.stride < min_stride {
590                            return Err(Error::invalid(
591                                "MJPEG encoder: RGB24 plane stride must be at least width * 3",
592                            ));
593                        }
594                        encode_jpeg_rgb24_with_opts(
595                            self.width,
596                            self.height,
597                            &plane.data,
598                            plane.stride,
599                            self.quality,
600                            self.restart_interval,
601                        )?
602                    }
603                    // 4-component CMYK / YCCK input takes the dedicated
604                    // CMYK encode path. The single packed plane is laid
605                    // out as `[C, M, Y, K]` (or `[Y, Cb, Cr, K]` for
606                    // `set_adobe_transform(Some(2))`) at 4 bytes per
607                    // pixel, matching the decoder's output shape.
608                    (MjpegPixelFormat::Cmyk, _) => {
609                        if v.planes.is_empty() {
610                            return Err(Error::invalid(
611                                "MJPEG encoder: CMYK frame missing plane 0",
612                            ));
613                        }
614                        let plane = &v.planes[0];
615                        let min_stride = (self.width as usize) * 4;
616                        if plane.stride < min_stride {
617                            return Err(Error::invalid(
618                                "MJPEG encoder: CMYK plane stride must be at least width * 4",
619                            ));
620                        }
621                        if self.progressive {
622                            encode_jpeg_cmyk_progressive(
623                                self.width,
624                                self.height,
625                                &plane.data,
626                                plane.stride,
627                                self.quality,
628                                self.cmyk_adobe_transform,
629                            )?
630                        } else {
631                            encode_jpeg_cmyk(
632                                self.width,
633                                self.height,
634                                &plane.data,
635                                plane.stride,
636                                self.quality,
637                                self.cmyk_adobe_transform,
638                            )?
639                        }
640                    }
641                    // YUV inputs take the baseline / progressive DCT path.
642                    _ => {
643                        if self.progressive {
644                            encode_jpeg_progressive(v, self.width, self.height, pix, self.quality)?
645                        } else {
646                            encode_jpeg_with_opts(
647                                v,
648                                self.width,
649                                self.height,
650                                pix,
651                                self.quality,
652                                self.restart_interval,
653                            )?
654                        }
655                    }
656                };
657                let mut pkt = Packet::new(0, self.time_base, data);
658                pkt.pts = v.pts;
659                pkt.dts = v.pts;
660                pkt.flags.keyframe = true;
661                self.pending.push_back(pkt);
662                Ok(())
663            }
664            _ => Err(Error::invalid("MJPEG encoder: video frames only")),
665        }
666    }
667
668    fn receive_packet(&mut self) -> Result<Packet> {
669        self.pending.pop_front().ok_or(Error::NeedMore)
670    }
671
672    fn flush(&mut self) -> Result<()> {
673        self.eof = true;
674        Ok(())
675    }
676}