From 7552f869cc1eef89325284d1f98964c9d8ab3143 Mon Sep 17 00:00:00 2001 From: Kostya Shishkov Date: Mon, 12 Oct 2026 14:56:37 +0200 Subject: [PATCH] add support for GIF in MOV --- nihav-commonfmt/src/codecs/gif.rs | 59 ++++++++++++++++++++++++++++--- nihav-commonfmt/src/codecs/mod.rs | 2 ++ nihav-registry/src/register.rs | 2 ++ 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/nihav-commonfmt/src/codecs/gif.rs b/nihav-commonfmt/src/codecs/gif.rs index 570d4d2..443f922 100644 --- a/nihav-commonfmt/src/codecs/gif.rs +++ b/nihav-commonfmt/src/codecs/gif.rs @@ -37,7 +37,9 @@ impl<'a> BitReader<'a> { } self.left = self.src[self.pos]; self.pos += 1; - validate!(self.left > 0); + if self.left == 0 { // abuse this code to signal end of stream + return Err(DecoderError::MissingReference); + } if self.pos + usize::from(self.left) > self.src.len() { return Err(DecoderError::ShortData); } @@ -117,7 +119,14 @@ impl LZWState { let mut pos = 0; let mut lastidx = INVALID_POS; loop { - let idx = br.read(self.idx_bits)? as usize; + let idx = match br.read(self.idx_bits) { + Ok(val) => val as usize, + Err(DecoderError::MissingReference) => { + validate!(pos == dst.len()); + return Ok(()); + }, + Err(err) => return Err(err) + }; if idx == reset_sym { self.reset(bits); lastidx = INVALID_POS; @@ -167,6 +176,7 @@ struct GIFDecoder { height: usize, lzw: LZWState, transp: Option, + is_qt: bool, } fn deinterlace(sbuf: &[u8], dbuf: &mut [u8], width: usize) { @@ -179,7 +189,7 @@ fn deinterlace(sbuf: &[u8], dbuf: &mut [u8], width: usize) { } impl GIFDecoder { - fn new() -> Self { + fn new(is_qt: bool) -> Self { Self { info: NACodecInfoRef::default(), gpal: [0; 768], @@ -191,6 +201,7 @@ impl GIFDecoder { height: 0, lzw: LZWState::new(), transp: None, + is_qt, } } } @@ -235,6 +246,28 @@ impl NADecoder for GIFDecoder { } let mut br = MemoryReader::new_read(&src); + if self.is_qt { // skip header and image descriptor + if let Ok([b'G', b'I', b'F', b'8']) = br.peek_tag() { + br.read_skip(6)?; + let width = usize::from(br.read_u16le()?); + let height = usize::from(br.read_u16le()?); + validate!(width == self.width && height == self.height); + let flags = br.read_byte()?; + let edata_size = 2 + if (flags & 0x80) != 0 { 3 << ((flags & 7) + 1) } else { 0 }; + br.read_skip(edata_size)?; + } + while let Ok(0x21) = br.peek_byte() { + br.read_byte()?; + let _label = br.read_byte()?; + loop { + let len = usize::from(br.read_byte()?); + if len == 0 { + break; + } + br.read_skip(len)?; + } + } + } let tag = br.read_byte()?; validate!(tag == 0x2C); let left = usize::from(br.read_u16le()?); @@ -303,7 +336,10 @@ impl NAOptionHandler for GIFDecoder { } pub fn get_decoder() -> Box { - Box::new(GIFDecoder::new()) + Box::new(GIFDecoder::new(false)) +} +pub fn get_decoder_qt() -> Box { + Box::new(GIFDecoder::new(true)) } #[cfg(test)] @@ -328,4 +364,19 @@ mod test { [0xdf920e8c, 0xeb57c5f8, 0xd862507e, 0xd733fca3], [0x75bee5cb, 0xefb2076c, 0xfce61f8a, 0x2d2b30df]])); } + // sample from Tex-Edit Plus 4.x distribution + #[test] + fn test_gif_qt_decoder() { + let mut dmx_reg = RegisteredDemuxers::new(); + generic_register_all_demuxers(&mut dmx_reg); + let mut dec_reg = RegisteredDecoders::new(); + generic_register_all_decoders(&mut dec_reg); + + test_decoding("mov", "qt-gif", "assets/Misc/The Duke", + Some(2), &dmx_reg, &dec_reg, + ExpectedTestResult::MD5Frames(vec![ + [0x2c62c68c, 0x3c8a076f, 0xd8914e96, 0xb26819f6], + [0xc7243219, 0x322077f4, 0x7fca320f, 0xc96f6cc1], + [0x0eaeeb20, 0xdc8b2c70, 0x861ce6a1, 0xef0e1e4f]])); + } } diff --git a/nihav-commonfmt/src/codecs/mod.rs b/nihav-commonfmt/src/codecs/mod.rs index 793d2c6..4cfb6d8 100644 --- a/nihav-commonfmt/src/codecs/mod.rs +++ b/nihav-commonfmt/src/codecs/mod.rs @@ -51,6 +51,8 @@ const DECODERS: &[DecoderInfo] = &[ DecoderInfo { name: "clearvideo_rm", get_decoder: clearvideo::get_decoder_rm }, #[cfg(feature="decoder_gif")] DecoderInfo { name: "gif", get_decoder: gif::get_decoder }, +#[cfg(feature="decoder_gif")] + DecoderInfo { name: "qt-gif", get_decoder: gif::get_decoder_qt }, #[cfg(feature="decoder_jpeg")] DecoderInfo { name: "jpeg", get_decoder: jpeg::get_decoder }, #[cfg(feature="decoder_rawvideo")] diff --git a/nihav-registry/src/register.rs b/nihav-registry/src/register.rs index b0aa91a..5f69a3a 100644 --- a/nihav-registry/src/register.rs +++ b/nihav-registry/src/register.rs @@ -332,6 +332,7 @@ static CODEC_REGISTER: &[CodecDescription] = &[ desc!(audio-hyb; "wavpack", "WavPack"), desc!(video-ll; "gif", "GIF"), + desc!(video-ll; "qt-gif", "GIF (in MOV)"), desc!(video-im; "jpeg", "JPEG"), desc!(video; "h264", "ITU H.264", CODEC_CAP_COMPLEX_REORDER | CODEC_CAP_HYBRID), desc!(video-modern; "mpeg4asp", "MPEG-4 ASP"), @@ -478,6 +479,7 @@ static MOV_VIDEO_CODEC_REGISTER: &[(&[u8;4], &str)] = &[ (b"svqi", "sorenson-video"), (b"SVQ3", "sorenson-video3"), (b"svq3", "sorenson-video3"), + (b"gif ", "qt-gif"), (b"msvc", "msvideo1"), -- 2.39.5