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}