diff --git a/src/any.rs b/src/any.rs index 92ca256..1fd70eb 100644 --- a/src/any.rs +++ b/src/any.rs @@ -287,6 +287,7 @@ any! { Pasp, Taic, Fiel, + Jpeg, Hev1, Hvc1, Hvcc, Lhvc, Mp4a, diff --git a/src/moov/trak/mdia/minf/stbl/stsd/jpeg.rs b/src/moov/trak/mdia/minf/stbl/stsd/jpeg.rs new file mode 100644 index 0000000..e23f4e4 --- /dev/null +++ b/src/moov/trak/mdia/minf/stbl/stsd/jpeg.rs @@ -0,0 +1,122 @@ +use crate::*; + +/// Photo - JPEG ('jpeg'), QuickTime's Motion JPEG visual sample entry. +/// +/// Unlike most video codecs, JPEG has no separate decoder configuration box: +/// the quantization/Huffman tables and everything else needed to decode are +/// self-contained within each JPEG-coded sample (baseline JPEG per +/// [ITU-T T.81](https://www.itu.int/rec/T-REC-T.81-199209-I/en) | ISO/IEC 10918-1, the +/// core JPEG bitstream standard), so this is just a [`Visual`] sample entry +/// plus the usual optional extension boxes. +/// +/// `'jpeg'` itself is a QuickTime-registered codec, not part of the ISO +/// base media file format -- see its +/// [MP4RA registration](https://mp4ra.org/registered-types/codecs) (spec: +/// "QT") and Apple's QuickTime documentation on the +/// [Photo Compressor](https://developer.apple.com/Mac/library/documentation/QuickTime/RM/CompressDecompress/ImageComprMgr/E-Chapter/5CompressorsSupplied.html), +/// which each sample's JPEG bitstream conforms to. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Jpeg { + pub visual: Visual, + pub btrt: Option, + pub colr: Option, + pub pasp: Option, + pub fiel: Option, +} + +impl Atom for Jpeg { + const KIND: FourCC = FourCC::new(b"jpeg"); + + fn decode_body(buf: &mut B) -> Result { + let visual = Visual::decode(buf)?; + + let mut btrt = None; + let mut colr = None; + let mut pasp = None; + let mut fiel = None; + while let Some(atom) = Any::decode_maybe(buf)? { + match atom { + Any::Btrt(atom) => btrt = atom.into(), + Any::Colr(atom) => colr = atom.into(), + Any::Pasp(atom) => pasp = atom.into(), + Any::Fiel(atom) => fiel = atom.into(), + unknown => Self::decode_unknown(&unknown)?, + } + } + skip_trailing_padding(buf); + + Ok(Self { + visual, + btrt, + colr, + pasp, + fiel, + }) + } + + fn encode_body(&self, buf: &mut B) -> Result<()> { + self.visual.encode(buf)?; + self.btrt.encode(buf)?; + self.colr.encode(buf)?; + self.pasp.encode(buf)?; + self.fiel.encode(buf)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_jpeg_roundtrip() { + let expected = Jpeg { + visual: Visual { + data_reference_index: 1, + width: 1920, + height: 1080, + compressor: "Photo - JPEG".into(), + ..Default::default() + }, + btrt: Some(Btrt { + buffer_size_db: 0, + max_bitrate: 5_000_000, + avg_bitrate: 2_000_000, + }), + colr: None, + pasp: Some(Pasp { + h_spacing: 1, + v_spacing: 1, + }), + fiel: None, + }; + + let mut buf = Vec::new(); + expected.encode(&mut buf).unwrap(); + + let decoded = Jpeg::decode(&mut buf.as_slice()).expect("failed to decode jpeg"); + assert_eq!(decoded, expected); + } + + #[test] + fn test_jpeg_minimal() { + // No optional boxes at all -- just the bare VisualSampleEntry. + let expected = Jpeg { + visual: Visual { + data_reference_index: 1, + width: 640, + height: 480, + ..Default::default() + }, + ..Default::default() + }; + + let mut buf = Vec::new(); + expected.encode(&mut buf).unwrap(); + + let decoded = Jpeg::decode(&mut buf.as_slice()).expect("failed to decode jpeg"); + assert_eq!(decoded, expected); + } +} diff --git a/src/moov/trak/mdia/minf/stbl/stsd/mod.rs b/src/moov/trak/mdia/minf/stbl/stsd/mod.rs index 8c637ff..ecb8a95 100644 --- a/src/moov/trak/mdia/minf/stbl/stsd/mod.rs +++ b/src/moov/trak/mdia/minf/stbl/stsd/mod.rs @@ -13,6 +13,7 @@ mod flac; mod ftab; mod h264; mod hevc; +mod jpeg; mod mebx; mod metadata; mod mett; @@ -45,6 +46,7 @@ pub use flac::*; pub use ftab::*; pub use h264::*; pub use hevc::*; +pub use jpeg::*; pub use mebx::*; pub use metadata::*; pub use mett::*; @@ -79,6 +81,9 @@ pub enum Codec { // H264 Avc1(Avc1), + // Photo - JPEG (QuickTime Motion JPEG) + Jpeg(Jpeg), + // HEVC: SPS/PPS/VPS is inline Hev1(Hev1), @@ -160,6 +165,7 @@ impl Decode for Codec { let atom = Any::decode(buf)?; Ok(match atom { Any::Avc1(atom) => atom.into(), + Any::Jpeg(atom) => atom.into(), Any::Hev1(atom) => atom.into(), Any::Hvc1(atom) => atom.into(), Any::Vp08(atom) => atom.into(), @@ -209,6 +215,7 @@ impl Encode for Codec { match self { Self::Unknown(..) => Err(Error::UnknownCodec), Self::Avc1(atom) => atom.encode(buf), + Self::Jpeg(atom) => atom.encode(buf), Self::Hev1(atom) => atom.encode(buf), Self::Hvc1(atom) => atom.encode(buf), Self::Vp08(atom) => atom.encode(buf),