diff options
author | Kostya Shishkov <kostya.shishkov@gmail.com> | 2023-09-10 18:46:32 +0200 |
---|---|---|
committer | Kostya Shishkov <kostya.shishkov@gmail.com> | 2023-09-10 18:46:32 +0200 |
commit | fc39649daffd54a1f7277656f470df2751fcf620 (patch) | |
tree | 3f805416000f85dec6b16c21dcc9b71d24b399d7 | |
parent | 94c520bf3df0c0e0b436d7c9a2e4b0b40550a453 (diff) | |
download | nihav-fc39649daffd54a1f7277656f470df2751fcf620.tar.gz |
GIF support
-rw-r--r-- | nihav-commonfmt/Cargo.toml | 12 | ||||
-rw-r--r-- | nihav-commonfmt/src/codecs/gif.rs | 316 | ||||
-rw-r--r-- | nihav-commonfmt/src/codecs/gifenc.rs | 671 | ||||
-rw-r--r-- | nihav-commonfmt/src/codecs/mod.rs | 8 | ||||
-rw-r--r-- | nihav-commonfmt/src/demuxers/gif.rs | 197 | ||||
-rw-r--r-- | nihav-commonfmt/src/demuxers/mod.rs | 4 | ||||
-rw-r--r-- | nihav-commonfmt/src/muxers/gif.rs | 232 | ||||
-rw-r--r-- | nihav-commonfmt/src/muxers/mod.rs | 4 | ||||
-rw-r--r-- | nihav-registry/src/detect.rs | 6 | ||||
-rw-r--r-- | nihav-registry/src/register.rs | 1 |
10 files changed, 1447 insertions, 4 deletions
diff --git a/nihav-commonfmt/Cargo.toml b/nihav-commonfmt/Cargo.toml index adea42f..e7d3313 100644 --- a/nihav-commonfmt/Cargo.toml +++ b/nihav-commonfmt/Cargo.toml @@ -23,21 +23,24 @@ decoders = [] demuxers = [] encoders = [] muxers = [] -all_demuxers = ["demuxer_avi", "demuxer_mov", "demuxer_wav", "demuxer_y4m"] +all_demuxers = ["demuxer_avi", "demuxer_gif", "demuxer_mov", "demuxer_wav", "demuxer_y4m"] demuxer_avi = ["demuxers"] +demuxer_gif = ["demuxers"] demuxer_mov = ["demuxers"] demuxer_wav = ["demuxers"] demuxer_y4m = ["demuxers"] -all_muxers = ["muxer_avi", "muxer_wav", "muxer_y4m"] +all_muxers = ["muxer_avi", "muxer_gif", "muxer_wav", "muxer_y4m"] muxer_avi = ["muxers"] +muxer_gif = ["muxers"] muxer_wav = ["muxers"] muxer_y4m = ["muxers"] all_decoders = ["all_video_decoders", "all_audio_decoders"] -all_video_decoders = ["decoder_cinepak", "decoder_clearvideo", "decoder_jpeg", "decoder_rawvideo", "decoder_rawvideo_ms", "decoder_zmbv"] +all_video_decoders = ["decoder_cinepak", "decoder_clearvideo", "decoder_gif", "decoder_jpeg", "decoder_rawvideo", "decoder_rawvideo_ms", "decoder_zmbv"] decoder_cinepak = ["decoders"] decoder_clearvideo = ["decoders"] +decoder_gif = ["decoders"] decoder_jpeg = ["decoders"] decoder_rawvideo = ["decoders"] decoder_rawvideo_ms = ["decoders"] @@ -52,8 +55,9 @@ decoder_aac = ["decoders"] all_encoders = ["all_video_encoders", "all_audio_encoders"] -all_video_encoders = ["encoder_cinepak", "encoder_rawvideo", "encoder_zmbv"] +all_video_encoders = ["encoder_cinepak", "encoder_gif", "encoder_rawvideo", "encoder_zmbv"] encoder_cinepak = ["encoders"] +encoder_gif = ["encoders"] encoder_rawvideo = ["encoders"] encoder_zmbv = ["encoders"] diff --git a/nihav-commonfmt/src/codecs/gif.rs b/nihav-commonfmt/src/codecs/gif.rs new file mode 100644 index 0000000..ccc21c6 --- /dev/null +++ b/nihav-commonfmt/src/codecs/gif.rs @@ -0,0 +1,316 @@ +use nihav_core::codecs::*; +use nihav_core::io::byteio::*; + +const DICT_SIZE: usize = 4096; +const MAX_BITS: u8 = 12; +const INVALID_POS: usize = 65536; + +struct BitReader<'a> { + src: &'a [u8], + pos: usize, + left: u8, + bitbuf: u32, + bits: u8, +} + +impl<'a> BitReader<'a> { + fn new(src: &'a [u8]) -> Self { + Self { + src, + pos: 0, + left: 0, + bitbuf: 0, + bits: 0, + } + } + fn read(&mut self, nbits: u8) -> DecoderResult<u32> { + while self.bits < nbits { + while self.left > 0 && self.bits <= 24 { + self.bitbuf |= u32::from(self.src[self.pos]) << self.bits; + self.bits += 8; + self.pos += 1; + self.left -= 1; + } + if self.bits < nbits { + if self.pos >= self.src.len() { + return Err(DecoderError::ShortData); + } + self.left = self.src[self.pos]; + self.pos += 1; + validate!(self.left > 0); + if self.pos + usize::from(self.left) > self.src.len() { + return Err(DecoderError::ShortData); + } + } + } + let ret = self.bitbuf & ((1 << nbits) - 1); + self.bitbuf >>= nbits; + self.bits -= nbits; + Ok(ret) + } +} + +struct LZWState { + dict_sym: [u8; DICT_SIZE], + dict_prev: [u16; DICT_SIZE], + dict_pos: usize, + dict_lim: usize, + nsyms: usize, + idx_bits: u8, +} + +impl LZWState { + fn new() -> Self { + Self { + dict_sym: [0; DICT_SIZE], + dict_prev: [0; DICT_SIZE], + dict_pos: 0, + dict_lim: 0, + idx_bits: 0, + nsyms: 0, + } + } + fn reset(&mut self, bits: u8) { + self.nsyms = (1 << bits) + 2; + self.dict_pos = self.nsyms; + self.dict_lim = 1 << (bits + 1); + self.idx_bits = bits + 1; + } + fn add(&mut self, prev: usize, sym: u8) { + if self.dict_pos < self.dict_lim { + self.dict_sym [self.dict_pos] = sym; + self.dict_prev[self.dict_pos] = prev as u16; + self.dict_pos += 1; + } + } + fn decode_idx(&self, dst: &mut [u8], pos: usize, idx: usize) -> DecoderResult<usize> { + let mut tot_len = 1; + let mut tidx = idx; + while tidx >= self.nsyms { + tidx = self.dict_prev[tidx] as usize; + tot_len += 1; + } + validate!(pos + tot_len <= dst.len()); + + let mut end = pos + tot_len - 1; + let mut tidx = idx; + while tidx >= self.nsyms { + dst[end] = self.dict_sym[tidx]; + end -= 1; + tidx = self.dict_prev[tidx] as usize; + } + dst[end] = tidx as u8; + + Ok(tot_len) + } + fn unpack(&mut self, src: &[u8], dst: &mut [u8]) -> DecoderResult<()> { + validate!(src.len() >= 4); + let mut br = BitReader::new(&src[1..]); + + let bits = src[0]; + validate!(bits > 0); + let reset_sym = 1 << bits; + let end_sym = reset_sym + 1; + + self.reset(bits); + + let mut pos = 0; + let mut lastidx = INVALID_POS; + loop { + let idx = br.read(self.idx_bits)? as usize; + if idx == reset_sym { + self.reset(bits); + lastidx = INVALID_POS; + continue; + } + if idx == end_sym { + break; + } + validate!(idx <= self.dict_pos); + if idx != self.dict_pos { + let len = self.decode_idx(dst, pos, idx)?; + if lastidx != INVALID_POS { + self.add(lastidx, dst[pos]); + } + pos += len; + } else { + validate!(lastidx != INVALID_POS); + let len = self.decode_idx(dst, pos, lastidx)?; + let lastsym = dst[pos]; + pos += len; + validate!(pos < dst.len()); + dst[pos] = lastsym; + pos += 1; + self.add(lastidx, lastsym); + } + + lastidx = idx; + if self.dict_pos == self.dict_lim && self.idx_bits < MAX_BITS { + self.dict_lim <<= 1; + self.idx_bits += 1; + } + } + validate!(pos == dst.len()); + validate!(br.pos + 2 == src.len()); + Ok(()) + } +} + +struct GIFDecoder { + info: NACodecInfoRef, + gpal: [u8; 768], + lpal: [u8; 768], + frame: Vec<u8>, + dbuf: Vec<u8>, + width: usize, + height: usize, + lzw: LZWState, + transp: Option<u8>, +} + +impl GIFDecoder { + fn new() -> Self { + Self { + info: NACodecInfoRef::default(), + gpal: [0; 768], + lpal: [0; 768], + frame: Vec::new(), + dbuf: Vec::new(), + width: 0, + height: 0, + lzw: LZWState::new(), + transp: None, + } + } +} + +impl NADecoder for GIFDecoder { + fn init(&mut self, _supp: &mut NADecoderSupport, info: NACodecInfoRef) -> DecoderResult<()> { + if let NACodecTypeInfo::Video(vinfo) = info.get_properties() { + self.width = vinfo.width; + self.height = vinfo.height; + self.transp = None; + self.gpal = [0; 768]; + if let Some(ref edata) = info.get_extradata() { + validate!(edata.len() >= 3); + if edata[1] != 0 { + self.transp = Some(edata[1]); + } + self.gpal[..edata.len() - 3].copy_from_slice(&edata[3..]); + } + self.frame = vec![0; self.width * self.height]; + self.dbuf = vec![0; self.width * self.height]; + let myinfo = NACodecTypeInfo::Video(NAVideoInfo::new(self.width, self.height, false, PAL8_FORMAT)); + self.info = NACodecInfo::new_ref(info.get_name(), myinfo, info.get_extradata()).into_ref(); + + Ok(()) + } else { + Err(DecoderError::InvalidData) + } + } + fn decode(&mut self, _supp: &mut NADecoderSupport, pkt: &NAPacket) -> DecoderResult<NAFrameRef> { + let src = pkt.get_buffer(); + validate!(src.len() > 0); + + for sd in pkt.side_data.iter() { + if let NASideData::Palette(true, ref pal) = sd { + for (dst, src) in self.gpal.chunks_mut(3).zip(pal.chunks(4)) { + dst[0] = src[0]; + dst[1] = src[1]; + dst[2] = src[2]; + } + break; + } + } + + let mut mr = MemoryReader::new_read(&src); + let mut br = ByteReader::new(&mut mr); + let tag = br.read_byte()?; + validate!(tag == 0x2C); + let left = usize::from(br.read_u16le()?); + let top = usize::from(br.read_u16le()?); + let width = usize::from(br.read_u16le()?); + let height = usize::from(br.read_u16le()?); + validate!(width > 0 && height > 0); + validate!(left + width <= self.width && top + height <= self.height); + let flags = br.read_byte()?; + let local_pal = (flags & 0x80) != 0; + if local_pal { + let csize = 3 << ((flags & 7) + 1); + br.read_buf(&mut self.lpal[..csize])?; + } + + let start = br.tell() as usize; + self.dbuf.resize(width * height, 0); + self.lzw.unpack(&src[start..], &mut self.dbuf)?; + + if let Some(tpix) = self.transp { + for (dline, sline) in self.frame.chunks_exact_mut(self.width).skip(top) + .zip(self.dbuf.chunks_exact(width)) { + for (dst, &src) in dline[left..][..width].iter_mut().zip(sline.iter()) { + if src != tpix { + *dst = tpix; + } + } + dline[left..][..width].copy_from_slice(sline); + } + } else { + for (dline, sline) in self.frame.chunks_exact_mut(self.width).skip(top) + .zip(self.dbuf.chunks_exact(width)) { + dline[left..][..width].copy_from_slice(sline); + } + } + + let buf = alloc_video_buffer(self.info.get_properties().get_video_info().unwrap(), 0)?; + let mut vbuf = buf.get_vbuf().unwrap(); + let paloff = vbuf.get_offset(1); + let stride = vbuf.get_stride(0); + let data = vbuf.get_data_mut().unwrap(); + + for (drow, srow) in data.chunks_exact_mut(stride).zip(self.frame.chunks_exact(self.width)) { + drow[..self.width].copy_from_slice(srow); + } + data[paloff..][..768].copy_from_slice(if local_pal { &self.lpal } else { &self.gpal }); + + let mut frm = NAFrame::new_from_pkt(pkt, self.info.clone(), buf); + let ftype = if pkt.keyframe { FrameType::I } else { FrameType::P }; + frm.set_frame_type(ftype); + Ok(frm.into_ref()) + } + fn flush(&mut self) { + } +} + +impl NAOptionHandler for GIFDecoder { + fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] } + fn set_options(&mut self, _options: &[NAOption]) { } + fn query_option_value(&self, _name: &str) -> Option<NAValue> { None } +} + +pub fn get_decoder() -> Box<dyn NADecoder + Send> { + Box::new(GIFDecoder::new()) +} + +#[cfg(test)] +mod test { + use nihav_core::codecs::RegisteredDecoders; + use nihav_core::demuxers::RegisteredDemuxers; + use nihav_codec_support::test::dec_video::*; + use crate::*; + + // sample: https://samples.mplayerhq.hu/image-samples/GIF/3D.gif + #[test] + fn test_gif_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("gif", "gif", "assets/Misc/3D.gif", + Some(2), &dmx_reg, &dec_reg, + ExpectedTestResult::MD5Frames(vec![ + [0x95e68f8f, 0xe899ac86, 0x0af66a0a, 0x34a4a00e], + [0xdf920e8c, 0xeb57c5f8, 0xd862507e, 0xd733fca3], + [0x75bee5cb, 0xefb2076c, 0xfce61f8a, 0x2d2b30df]])); + } +} diff --git a/nihav-commonfmt/src/codecs/gifenc.rs b/nihav-commonfmt/src/codecs/gifenc.rs new file mode 100644 index 0000000..6bb9263 --- /dev/null +++ b/nihav-commonfmt/src/codecs/gifenc.rs @@ -0,0 +1,671 @@ +use nihav_core::codecs::*; +use nihav_core::io::byteio::*; +use nihav_core::io::bitwriter::*; + +#[derive(Clone,Copy,Default,PartialEq)] +enum CompressionLevel { + None, + Fast, + #[default] + Best +} + +impl std::string::ToString for CompressionLevel { + fn to_string(&self) -> String { + match *self { + CompressionLevel::None => "none".to_string(), + CompressionLevel::Fast => "fast".to_string(), + CompressionLevel::Best => "best".to_string(), + } + } +} + +const NO_CODE: u16 = 0; + +struct LZWDictionary { + cur_size: usize, + bit_len: u8, + clear_code: u16, + end_code: u16, + orig_len: u8, + trie: Vec<[u16; 257]>, +} + +impl LZWDictionary { + fn new() -> Self { + Self { + trie: Vec::with_capacity(4096), + cur_size: 0, + bit_len: 0, + clear_code: 0, + end_code: 0, + orig_len: 0, + } + } + fn init(&mut self, bits: u8) { + self.cur_size = (1 << bits) + 2; + self.bit_len = bits + 1; + self.clear_code = 1 << bits; + self.end_code = self.clear_code + 1; + self.orig_len = self.bit_len; + + self.trie.clear(); + for _ in 0..self.cur_size { + self.trie.push([NO_CODE; 257]); + } + for (idx, nodes) in self.trie.iter_mut().enumerate() { + nodes[256] = idx as u16; + } + } + fn find(&self, src: &[u8]) -> (u16, usize, usize) { + let mut idx = usize::from(src[0]); + let mut last_len = 0; + for (pos, &next) in src.iter().enumerate().skip(1) { + let next = usize::from(next); + if self.trie[idx][next] != NO_CODE { + idx = usize::from(self.trie[idx][next]); + } else { + return (self.trie[idx][256], pos, idx); + } + last_len = pos; + } + (self.trie[idx][256], last_len + 1, idx) + } + fn add(&mut self, lastidx: usize, next: u8) { + if self.cur_size >= (1 << 12) { + return; + } + let next = usize::from(next); + if self.trie[lastidx][next] == NO_CODE { + let newnode = self.trie.len(); + self.trie.push([NO_CODE; 257]); + self.trie[newnode][256] = self.cur_size as u16; + self.trie[lastidx][next] = newnode as u16; + } + if (self.cur_size & (self.cur_size - 1)) == 0 && self.bit_len < 12 { + self.bit_len += 1; + } + self.cur_size += 1; + } + fn reset(&mut self) { + self.bit_len = self.orig_len; + self.cur_size = usize::from(self.end_code) + 1; + self.trie.truncate(self.cur_size); + for nodes in self.trie.iter_mut() { + for el in nodes[..256].iter_mut() { + *el = NO_CODE; + } + } + } +} + +struct LZWEncoder { + dict: LZWDictionary, + level: CompressionLevel, + tmp: Vec<u8>, +} + +impl LZWEncoder { + fn new() -> Self { + Self { + dict: LZWDictionary::new(), + level: CompressionLevel::default(), + tmp: Vec::new(), + } + } + fn compress(&mut self, writer: &mut ByteWriter, src: &[u8]) -> EncoderResult<()> { + let clr_bits: u8 = if self.level != CompressionLevel::None { + let maxclr = u16::from(src.iter().fold(0u8, |acc, &a| acc.max(a))) + 1; + let mut bits = 2; + while (1 << bits) < maxclr { + bits += 1; + } + bits + } else { 8 }; + + self.dict.init(clr_bits); + + self.tmp.clear(); + let mut tbuf = Vec::new(); + std::mem::swap(&mut tbuf, &mut self.tmp); + let mut bw = BitWriter::new(tbuf, BitWriterMode::LE); + + bw.write(u32::from(self.dict.clear_code), self.dict.bit_len); + + match self.level { + CompressionLevel::None => { + for &b in src.iter() { + bw.write(u32::from(b), self.dict.bit_len); + self.dict.add(usize::from(b), 0); + } + }, + CompressionLevel::Fast => { + let mut pos = 0; + while pos < src.len() { + let (idx, len, trieidx) = self.dict.find(&src[pos..]); + bw.write(u32::from(idx), self.dict.bit_len); + pos += len; + if pos < src.len() { + self.dict.add(trieidx, src[pos]); + } + if self.dict.cur_size == 4096 { + bw.write(u32::from(self.dict.clear_code), self.dict.bit_len); + self.dict.reset(); + } + } + }, + CompressionLevel::Best => { + let mut pos = 0; + let mut hist = [0; 16]; + let mut avg = 0; + let mut avg1 = 0; + let mut hpos = 0; + while pos < src.len() { + let (idx, len, trieidx) = self.dict.find(&src[pos..]); + bw.write(u32::from(idx), self.dict.bit_len); + pos += len; + if pos >= src.len() { + break; + } + self.dict.add(trieidx, src[pos]); + + avg1 -= hist[(hpos + 1) & 0xF]; + avg1 += len; + if self.dict.cur_size == 4096 && (avg1 < avg - avg / 8) { + bw.write(u32::from(self.dict.clear_code), self.dict.bit_len); + self.dict.reset(); + } + avg = avg1; + hpos = (hpos + 1) & 0xF; + hist[hpos] = len; + } + }, + }; + + bw.write(u32::from(self.dict.end_code), self.dict.bit_len); + tbuf = bw.end(); + std::mem::swap(&mut tbuf, &mut self.tmp); + + writer.write_byte(clr_bits)?; + for chunk in self.tmp.chunks(255) { + writer.write_byte(chunk.len() as u8)?; + writer.write_buf(chunk)?; + } + writer.write_byte(0x00)?; // data end marker + Ok(()) + } +} + +struct GIFEncoder { + stream: Option<NAStreamRef>, + cur_frm: Vec<u8>, + prev_frm: Vec<u8>, + tmp_buf: Vec<u8>, + pal: [u8; 768], + pkt: Option<NAPacket>, + first: bool, + width: usize, + height: usize, + lzw: LZWEncoder, + p_trans: bool, + tr_idx: Option<u8>, +} + +impl GIFEncoder { + fn new() -> Self { + Self { + stream: None, + pkt: None, + cur_frm: Vec::new(), + prev_frm: Vec::new(), + pal: [0; 768], + tmp_buf: Vec::new(), + first: true, + width: 0, + height: 0, + lzw: LZWEncoder::new(), + p_trans: false, + tr_idx: None, + } + } + fn write_dummy_frame(&mut self, bw: &mut ByteWriter) -> EncoderResult<()> { + let mut pix = [self.cur_frm[0]]; + if let (true, Some(tr_idx)) = (self.p_trans, self.tr_idx) { + if tr_idx < pix[0] { + pix[0] = tr_idx; + } + } + + // 1x1 image descriptor + bw.write_buf(&[0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00])?; + self.lzw.compress(bw, &pix)?; + Ok(()) + } +} + +impl NAEncoder for GIFEncoder { + fn negotiate_format(&self, encinfo: &EncodeParameters) -> EncoderResult<EncodeParameters> { + match encinfo.format { + NACodecTypeInfo::None => { + Ok(EncodeParameters { + format: NACodecTypeInfo::Video(NAVideoInfo::new(0, 0, true, YUV420_FORMAT)), + ..Default::default() + }) + }, + NACodecTypeInfo::Audio(_) => Err(EncoderError::FormatError), + NACodecTypeInfo::Video(vinfo) => { + let outinfo = NAVideoInfo::new(vinfo.width, vinfo.height, false, PAL8_FORMAT); + let mut ofmt = *encinfo; + ofmt.format = NACodecTypeInfo::Video(outinfo); + Ok(ofmt) + } + } + } + fn get_capabilities(&self) -> u64 { ENC_CAPS_SKIPFRAME } + fn init(&mut self, stream_id: u32, encinfo: EncodeParameters) -> EncoderResult<NAStreamRef> { + match encinfo.format { + NACodecTypeInfo::None => Err(EncoderError::FormatError), + NACodecTypeInfo::Audio(_) => Err(EncoderError::FormatError), + NACodecTypeInfo::Video(vinfo) => { + if vinfo.width > 65535 || vinfo.height > 65535 { + return Err(EncoderError::FormatError); + } + self.width = vinfo.width; + self.height = vinfo.height; + + let edata = self.tr_idx.map(|val| vec![val]); + + let out_info = NAVideoInfo::new(vinfo.width, vinfo.height, false, PAL8_FORMAT); + let info = NACodecInfo::new("gif", NACodecTypeInfo::Video(out_info), edata); + let mut stream = NAStream::new(StreamType::Video, stream_id, info, encinfo.tb_num, encinfo.tb_den, 0); + stream.set_num(stream_id as usize); + let stream = stream.into_ref(); + + self.stream = Some(stream.clone()); + + self.cur_frm = vec![0; vinfo.width * vinfo.height]; + self.prev_frm = vec![0; vinfo.width * vinfo.height]; + self.tmp_buf.clear(); + self.tmp_buf.reserve(vinfo.width * vinfo.height); + + self.first = true; + + Ok(stream) + }, + } + } + fn encode(&mut self, frm: &NAFrame) -> EncoderResult<()> { + let mut dbuf = Vec::with_capacity(4); + let mut gw = GrowableMemoryWriter::new_write(&mut dbuf); + let mut bw = ByteWriter::new(&mut gw); + + self.tmp_buf.clear(); + + match frm.get_buffer() { + NABufferType::Video(ref buf) => { + let src = buf.get_data(); + let stride = buf.get_stride(0); + let src = &src[buf.get_offset(0)..]; + + for (dline, sline) in self.cur_frm.chunks_exact_mut(self.width) + .zip(src.chunks_exact(stride)) { + dline.copy_from_slice(&sline[..self.width]); + } + + let cur_pal = &src[buf.get_offset(1)..][..768]; + if self.first { + self.pal.copy_from_slice(cur_pal); + } + + let mut pal_changed = false; + if !self.first { + let mut used = [false; 256]; + for &b in self.cur_frm.iter() { + used[usize::from(b)] = true; + } + for (&used, (pal1, pal2)) in used.iter() + .zip(self.pal.chunks_exact(3).zip(cur_pal.chunks_exact(3))) { + if used && (pal1 != pal2) { + pal_changed = true; + break; + } + } + } + + if self.first { + bw.write_byte(0x2C)?; // image descriptor + bw.write_u16le(0)?; // left + bw.write_u16le(0)?; // top + bw.write_u16le(self.width as u16)?; + bw.write_u16le(self.height as u16)?; + bw.write_byte(0)?; // flags + self.lzw.compress(&mut bw, &self.cur_frm)?; + } else { + let mut top = 0; + for (y, (line1, line2)) in self.cur_frm.chunks_exact(self.width) + .zip(self.prev_frm.chunks_exact(self.width)).enumerate() { + if line1 == line2 { + top = y; + } else { + break; + } + } + if top != self.height - 1 { + let mut bot = self.height; + for (y, (line1, line2)) in self.cur_frm.chunks_exact(self.width) + .zip(self.prev_frm.chunks_exact(self.width)).enumerate().rev() { + if line1 == line2 { + bot = y + 1; + } else { + break; + } + } + let mut left = self.width - 1; + let mut right = 0; + for (line1, line2) in self.cur_frm.chunks_exact(self.width) + .zip(self.prev_frm.chunks_exact(self.width)) + .skip(top).take(bot - top) { + if left > 0 { + let mut cur_l = 0; + for (x, (&p1, &p2)) in line1.iter().zip(line2.iter()).enumerate() { + if p1 == p2 { + cur_l = x + 1; + } else { + break; + } + } + left = left.min(cur_l); + } + if right < self.width { + let mut cur_r = self.width; + for (x, (&p1, &p2)) in line1.iter().zip(line2.iter()) + .enumerate().rev() { + if p1 == p2 { + cur_r = x + 1; + } else { + break; + } + } + right = right.max(cur_r); + } + } + self.tmp_buf.clear(); + let use_transparency = self.p_trans && self.tr_idx.is_some(); + let full_frame = right == 0 && top == 0 && left == self.width && bot == self.height; + + let pic = match (use_transparency, full_frame) { + (true, _) => { + let tr_idx = self.tr_idx.unwrap_or(0); + for (cline, pline) in self.cur_frm.chunks_exact(self.width) + .zip(self.prev_frm.chunks_exact(self.width)) + .skip(top).take(bot - top) { + for (&cpix, &ppix) in cline[left..right].iter() + .zip(pline[left..right].iter()) { + self.tmp_buf.push(if cpix == ppix { tr_idx } else { cpix }); + } + } + &self.tmp_buf + }, + (false, true) => { + &self.cur_frm + }, + (false, false) => { + for line in self.cur_frm.chunks_exact(self.width) + .skip(top).take(bot - top) { + self.tmp_buf.extend_from_slice(&line[left..right]); + } + &self.tmp_buf + }, + }; + + bw.write_byte(0x2C)?; // image descriptor + bw.write_u16le(left as u16)?; + bw.write_u16le(top as u16)?; + bw.write_u16le((right - left) as u16)?; + bw.write_u16le((bot - top) as u16)?; + if !pal_changed { + bw.write_byte(0)?; // flags + } else { + let maxclr = pic.iter().fold(0u8, |acc, &a| acc.max(a)); + let clr_bits = if maxclr > 128 { + 8 + } else { + let mut bits = 1; + while (1 << bits) < maxclr { + bits += 1; + } + bits + }; + bw.write_byte(0x80 | (clr_bits - 1))?; + bw.write_buf(&cur_pal[..(3 << clr_bits)])?; + } + self.lzw.compress(&mut bw, pic)?; + } else { + self.write_dummy_frame(&mut bw)?; + } + } + }, + NABufferType::None if !self.first => { + self.write_dummy_frame(&mut bw)?; + }, + _ => return Err(EncoderError::InvalidParameters), + }; + + self.pkt = Some(NAPacket::new(self.stream.clone().unwrap(), frm.ts, self.first, dbuf)); + self.first = false; + + if let NABufferType::Video(ref buf) = frm.get_buffer() { + let paloff = buf.get_offset(1); + let data = buf.get_data(); + let mut pal = [0; 1024]; + let srcpal = &data[paloff..][..768]; + for (dclr, sclr) in pal.chunks_exact_mut(4).zip(srcpal.chunks_exact(3)) { + dclr[..3].copy_from_slice(sclr); + } + if let Some(ref mut pkt) = &mut self.pkt { + pkt.side_data.push(NASideData::Palette(true, Arc::new(pal))); + } + } + + std::mem::swap(&mut self.cur_frm, &mut self.prev_frm); + Ok(()) + } + fn get_packet(&mut self) -> EncoderResult<Option<NAPacket>> { + let mut npkt = None; + std::mem::swap(&mut self.pkt, &mut npkt); + Ok(npkt) + } + fn flush(&mut self) -> EncoderResult<()> { + Ok(()) + } +} + +const ENCODER_OPTS: &[NAOptionDefinition] = &[ + NAOptionDefinition { + name: "compr", description: "Compression level", + opt_type: NAOptionDefinitionType::String(Some(&["none", "fast", "best"])) }, + NAOptionDefinition { + name: "inter_transparent", description: "Code changed regions with transparency", + opt_type: NAOptionDefinitionType::Bool }, + NAOptionDefinition { + name: "transparent_idx", description: "Palette index to use for transparency (on inter frames too if requested)", + opt_type: NAOptionDefinitionType::Int(Some(-1), Some(255)) }, +]; + +impl NAOptionHandler for GIFEncoder { + fn get_supported_options(&self) -> &[NAOptionDefinition] { ENCODER_OPTS } + fn set_options(&mut self, options: &[NAOption]) { + for option in options.iter() { + for opt_def in ENCODER_OPTS.iter() { + if opt_def.check(option).is_ok() { + match option.name { + "compr" => { + if let NAValue::String(ref strval) = option.value { + match strval.as_str() { + "none" => self.lzw.level = CompressionLevel::None, + "fast" => self.lzw.level = CompressionLevel::Fast, + "best" => self.lzw.level = CompressionLevel::Best, + _ => {}, + }; + } + }, + "inter_transparent" => { + if let NAValue::Bool(bval) = option.value { + self.p_trans = bval; + } + }, + "transparent_idx" => { + if let NAValue::Int(ival) = option.value { + self.tr_idx = if ival >= 0 { Some(ival as u8) } else { None }; + } + }, + _ => {}, + }; + } + } + } + } + fn query_option_value(&self, name: &str) -> Option<NAValue> { + match name { + "compr" => Some(NAValue::String(self.lzw.level.to_string())), + "inter_transparent" => Some(NAValue::Bool(self.p_trans)), + "transparent_idx" => Some(NAValue::Int(self.tr_idx.map_or(-1i64, i64::from))), + _ => None, + } + } +} + +pub fn get_encoder() -> Box<dyn NAEncoder + Send> { + Box::new(GIFEncoder::new()) +} + +#[cfg(test)] +mod test { + use nihav_core::codecs::*; + use nihav_core::demuxers::*; + use nihav_core::muxers::*; + use crate::*; + use nihav_codec_support::test::enc_video::*; + + // sample: https://samples.mplayerhq.hu/V-codecs/Uncompressed/8bpp.avi + fn test_gif_encoder_single(out_name: &'static str, enc_options: &[NAOption], hash: &[u32; 4]) { + 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); + let mut mux_reg = RegisteredMuxers::new(); + generic_register_all_muxers(&mut mux_reg); + let mut enc_reg = RegisteredEncoders::new(); + generic_register_all_encoders(&mut enc_reg); + + let dec_config = DecoderTestParams { + demuxer: "avi", + in_name: "assets/Misc/8bpp.avi", + stream_type: StreamType::Video, + limit: Some(0), + dmx_reg, dec_reg, + }; + let enc_config = EncoderTestParams { + muxer: "gif", + enc_name: "gif", + out_name, + mux_reg, enc_reg, + }; + let dst_vinfo = NAVideoInfo { + width: 0, + height: 0, + format: PAL8_FORMAT, + flipped: false, + bits: 8, + }; + let enc_params = EncodeParameters { + format: NACodecTypeInfo::Video(dst_vinfo), + quality: 0, + bitrate: 0, + tb_num: 0, + tb_den: 0, + flags: 0, + }; + //test_encoding_to_file(&dec_config, &enc_config, enc_params, enc_options); + test_encoding_md5(&dec_config, &enc_config, enc_params, enc_options, hash); + } + // sample: https://samples.mplayerhq.hu/image-samples/GIF/3D.gif + fn test_gif_anim(out_name: &'static str, enc_options: &[NAOption], hash: &[u32; 4]) { + 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); + let mut mux_reg = RegisteredMuxers::new(); + generic_register_all_muxers(&mut mux_reg); + let mut enc_reg = RegisteredEncoders::new(); + generic_register_all_encoders(&mut enc_reg); + + let dec_config = DecoderTestParams { + demuxer: "gif", + in_name: "assets/Misc/3D.gif", + stream_type: StreamType::Video, + limit: None, + dmx_reg, dec_reg, + }; + let enc_config = EncoderTestParams { + muxer: "gif", + enc_name: "gif", + out_name, + mux_reg, enc_reg, + }; + let dst_vinfo = NAVideoInfo { + width: 0, + height: 0, + format: PAL8_FORMAT, + flipped: false, + bits: 8, + }; + let enc_params = EncodeParameters { + format: NACodecTypeInfo::Video(dst_vinfo), + quality: 0, + bitrate: 0, + tb_num: 0, + tb_den: 0, + flags: 0, + }; + //test_encoding_to_file(&dec_config, &enc_config, enc_params, enc_options); + test_encoding_md5(&dec_config, &enc_config, enc_params, enc_options, hash); + } + #[test] + fn test_gif_single_none() { + let enc_options = &[ + NAOption { name: "compr", value: NAValue::String("none".to_string()) }, + ]; + test_gif_encoder_single("none.gif", enc_options, &[0x2767a289, 0xdef9ad30, 0xca4c289b, 0x1fd0ec19]); + } + #[test] + fn test_gif_single_fast() { + let enc_options = &[ + NAOption { name: "compr", value: NAValue::String("fast".to_string()) }, + ]; + test_gif_encoder_single("fast.gif", enc_options, &[0x9644f682, 0x497593cd, 0xdabb483d, 0x8fce63f4]); + } + #[test] + fn test_gif_single_best() { + let enc_options = &[ + NAOption { name: "compr", value: NAValue::String("best".to_string()) }, + ]; + test_gif_encoder_single("best.gif", enc_options, &[0x9644f682, 0x497593cd, 0xdabb483d, 0x8fce63f4]); + } + #[test] + fn test_gif_anim_opaque() { + let enc_options = &[ + NAOption { name: "compr", value: NAValue::String("fast".to_string()) }, + ]; + test_gif_anim("anim-opaque.gif", enc_options, &[0x58489e31, 0x1721d75e, 0xaebf93f2, 0x3fea9c6e]); + } + #[test] + fn test_gif_anim_transparent() { + let enc_options = &[ + NAOption { name: "compr", value: NAValue::String("fast".to_string()) }, + NAOption { name: "inter_transparent", value: NAValue::Bool(true) }, + NAOption { name: "transparent_idx", value: NAValue::Int(0x7F) }, + ]; + test_gif_anim("anim-transp.gif", enc_options, &[0x62df6232, 0x0c334457, 0x73738404, 0xa8829dcc]); + } +} diff --git a/nihav-commonfmt/src/codecs/mod.rs b/nihav-commonfmt/src/codecs/mod.rs index ad8a8c6..686ba4f 100644 --- a/nihav-commonfmt/src/codecs/mod.rs +++ b/nihav-commonfmt/src/codecs/mod.rs @@ -13,6 +13,8 @@ macro_rules! validate { mod cinepak; #[cfg(feature="decoder_clearvideo")] mod clearvideo; +#[cfg(feature="decoder_gif")] +mod gif; #[cfg(feature="decoder_jpeg")] mod jpeg; #[cfg(feature="decoder_rawvideo")] @@ -45,6 +47,8 @@ const DECODERS: &[DecoderInfo] = &[ DecoderInfo { name: "clearvideo", get_decoder: clearvideo::get_decoder }, #[cfg(feature="decoder_clearvideo")] 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_jpeg")] DecoderInfo { name: "jpeg", get_decoder: jpeg::get_decoder }, #[cfg(feature="decoder_rawvideo")] @@ -78,6 +82,8 @@ pub fn generic_register_all_decoders(rd: &mut RegisteredDecoders) { #[cfg(feature="encoder_cinepak")] mod cinepakenc; +#[cfg(feature="encoder_gif")] +mod gifenc; #[cfg(feature="encoder_rawvideo")] mod rawvideoenc; #[cfg(feature="encoder_zmbv")] @@ -87,6 +93,8 @@ mod zmbvenc; const ENCODERS: &[EncoderInfo] = &[ #[cfg(feature="encoder_cinepak")] EncoderInfo { name: "cinepak", get_encoder: cinepakenc::get_encoder }, +#[cfg(feature="encoder_gif")] + EncoderInfo { name: "gif", get_encoder: gifenc::get_encoder }, #[cfg(feature="encoder_rawvideo")] EncoderInfo { name: "rawvideo", get_encoder: rawvideoenc::get_encoder }, #[cfg(feature="encoder_zmbv")] diff --git a/nihav-commonfmt/src/demuxers/gif.rs b/nihav-commonfmt/src/demuxers/gif.rs new file mode 100644 index 0000000..eef0a00 --- /dev/null +++ b/nihav-commonfmt/src/demuxers/gif.rs @@ -0,0 +1,197 @@ +use nihav_core::demuxers::*; + +struct GIFDemuxer<'a> { + src: &'a mut ByteReader<'a>, + frameno: u64, + is_87: bool, + pal: Arc<[u8; 1024]>, +} + +impl<'a> GIFDemuxer<'a> { + fn new(io: &'a mut ByteReader<'a>) -> Self { + Self { + src: io, + frameno: 0, + is_87: false, + pal: Arc::new([0; 1024]), + } + } + fn skip_blocks(&mut self) -> DemuxerResult<()> { + loop { + let size = self.src.read_byte()?; + if size == 0 { + break; + } + self.src.read_skip(usize::from(size))?; + } + Ok(()) + } +} + +impl<'a> DemuxCore<'a> for GIFDemuxer<'a> { + fn open(&mut self, strmgr: &mut StreamManager, _seek_index: &mut SeekIndex) -> DemuxerResult<()> { + let mut magic = [0; 6]; + self.src.read_buf(&mut magic)?; + validate!(&magic == b"GIF87a" || &magic == b"GIF89a"); + self.is_87 = &magic == b"GIF87a"; + + let width = usize::from(self.src.read_u16le()?); + let height = usize::from(self.src.read_u16le()?); + validate!(width > 0 && height > 0); + let flags = self.src.read_byte()?; + let edata_size = 1 + 2 + if (flags & 0x80) != 0 { 3 << ((flags & 7) + 1) } else { 0 }; + let mut edata = vec![0; edata_size]; + edata[0] = flags; + self.src.read_buf(&mut edata[1..])?; + if (flags & 0x80) != 0 { + let mut npal = [0; 1024]; + for (dpal, spal) in npal.chunks_exact_mut(4).zip(edata[3..].chunks_exact(3)) { + dpal[..3].copy_from_slice(spal); + } + self.pal = Arc::new(npal); + } + let mut delay = 0; + loop { + match self.src.peek_byte() { + Ok(0x2C) => break, + Ok(_) => {}, + Err(_err) => return Err(DemuxerError::IOError), + }; + let tag = self.src.read_byte()?; + match tag { + 0x21 => { + validate!(!self.is_87); + let subtype = self.src.read_byte()?; + match subtype { + 0xF9 => { + let bsize = self.src.read_byte()?; + validate!(bsize == 4); + let _flags = self.src.read_byte()?; + delay = self.src.read_u16le()?; + let _clr = self.src.read_byte()?; + self.skip_blocks()?; + }, + 0xFF => { + let bsize = self.src.read_byte()?; + validate!(bsize == 11); + let mut app_id = [0; 11]; + self.src.read_buf(&mut app_id)?; + if &app_id == b"NETSCAPE2.0" { + let bsize = self.src.read_byte()?; + validate!(bsize == 3); + let b = self.src.read_byte()?; + validate!(b == 1); + let _nloops = self.src.read_u16le()?; + } + self.skip_blocks()?; + }, + _ => { + self.skip_blocks()?; + }, + }; + }, + 0x2C => unreachable!(), + _ => return Err(DemuxerError::NotImplemented), + }; + } + + let vhdr = NAVideoInfo::new(width, height, false, PAL8_FORMAT); + let vci = NACodecTypeInfo::Video(vhdr); + let vinfo = NACodecInfo::new("gif", vci, Some(edata)); + if strmgr.add_stream(NAStream::new(StreamType::Video, 0, vinfo, u32::from(delay.max(1)), 100, 0)).is_none() { + return Err(DemuxerError::MemoryError); + } + + Ok(()) + } + + fn get_frame(&mut self, strmgr: &mut StreamManager) -> DemuxerResult<NAPacket> { + loop { + match self.src.read_byte()? { + 0x2C => { + let mut data = vec![0; 10]; + data[0] = 0x2C; + self.src.read_buf(&mut data[1..])?; + if (data[9] & 0x80) != 0 { + let cmap_size = 3 << ((data[9] & 7) + 1); + data.resize(10 + cmap_size, 0); + self.src.read_buf(&mut data[10..])?; + } + let lzw_bits = self.src.read_byte()?; + data.push(lzw_bits); + let mut tbuf = [0; 255]; + loop { + let bsize = usize::from(self.src.read_byte()?); + data.push(bsize as u8); + if bsize == 0 { + break; + } + self.src.read_buf(&mut tbuf[..bsize])?; + data.extend_from_slice(&tbuf[..bsize]); + } + + let stream = strmgr.get_stream(0).unwrap(); + let ts = stream.make_ts(Some(self.frameno), None, None); + let mut pkt = NAPacket::new(stream, ts, self.frameno == 0, data); + pkt.add_side_data(NASideData::Palette(false, self.pal.clone())); + self.frameno += 1; + return Ok(pkt); + }, + 0x21 => { + self.src.read_byte()?; + self.skip_blocks()?; + }, + 0x3B => return Err(DemuxerError::EOF), + _ => unimplemented!(), + }; + } + } + + fn seek(&mut self, _time: NATimePoint, _seek_index: &SeekIndex) -> DemuxerResult<()> { + Err(DemuxerError::NotPossible) + } + fn get_duration(&self) -> u64 { 0 } +} + +impl<'a> NAOptionHandler for GIFDemuxer<'a> { + fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] } + fn set_options(&mut self, _options: &[NAOption]) { } + fn query_option_value(&self, _name: &str) -> Option<NAValue> { None } +} + +pub struct GIFDemuxerCreator { } + +impl DemuxerCreator for GIFDemuxerCreator { + fn new_demuxer<'a>(&self, br: &'a mut ByteReader<'a>) -> Box<dyn DemuxCore<'a> + 'a> { + Box::new(GIFDemuxer::new(br)) + } + fn get_name(&self) -> &'static str { "gif" } +} + +#[cfg(test)] +mod test { + use super::*; + use std::fs::File; + + #[test] + fn test_gif_demux() { + // sample: https://samples.mplayerhq.hu/image-samples/GIF/3D.gif + let mut file = File::open("assets/Misc/3D.gif").unwrap(); + let mut fr = FileReader::new_read(&mut file); + let mut br = ByteReader::new(&mut fr); + let mut dmx = GIFDemuxer::new(&mut br); + let mut sm = StreamManager::new(); + let mut si = SeekIndex::new(); + dmx.open(&mut sm, &mut si).unwrap(); + + loop { + let pktres = dmx.get_frame(&mut sm); + if let Err(e) = pktres { + if e == DemuxerError::EOF { break; } + panic!("error"); + } + let pkt = pktres.unwrap(); + println!("Got {}", pkt); + } + } +} diff --git a/nihav-commonfmt/src/demuxers/mod.rs b/nihav-commonfmt/src/demuxers/mod.rs index 32fe113..42d1f11 100644 --- a/nihav-commonfmt/src/demuxers/mod.rs +++ b/nihav-commonfmt/src/demuxers/mod.rs @@ -14,6 +14,8 @@ macro_rules! validate { #[cfg(feature="demuxer_avi")] #[allow(clippy::cast_lossless)] mod avi; +#[cfg(feature="demuxer_gif")] +mod gif; #[cfg(feature="demuxer_mov")] #[allow(clippy::cast_lossless)] mod mov; @@ -25,6 +27,8 @@ mod y4m; const DEMUXERS: &[&dyn DemuxerCreator] = &[ #[cfg(feature="demuxer_avi")] &avi::AVIDemuxerCreator {}, +#[cfg(feature="demuxer_gif")] + &gif::GIFDemuxerCreator {}, #[cfg(feature="demuxer_mov")] &mov::MOVDemuxerCreator {}, #[cfg(feature="demuxer_mov")] diff --git a/nihav-commonfmt/src/muxers/gif.rs b/nihav-commonfmt/src/muxers/gif.rs new file mode 100644 index 0000000..435d1b8 --- /dev/null +++ b/nihav-commonfmt/src/muxers/gif.rs @@ -0,0 +1,232 @@ +use nihav_core::muxers::*; + +struct GIFMuxer<'a> { + bw: &'a mut ByteWriter<'a>, + single: bool, + gif87: bool, + pal_written: bool, + nloops: u16, +} + +impl<'a> GIFMuxer<'a> { + fn new(bw: &'a mut ByteWriter<'a>) -> Self { + Self { + bw, + single: false, + gif87: false, + pal_written: false, + nloops: 0, + } + } + fn write_pal(&mut self, pal: &[u8; 1024]) -> MuxerResult<()> { + let mut nclr = 256; + for quad in pal.chunks_exact(4).rev() { + if quad[0] == 0 && quad[1] == 0 && quad[2] == 0 { + nclr -= 1; + } else { + break; + } + } + let mut pal_bits = 1; + while (1 << pal_bits) < nclr { + pal_bits += 1; + } + self.bw.write_byte(0xF0 | (pal_bits - 1))?; + self.bw.write_byte(0)?; // background colour index + self.bw.write_byte(0)?; // aspect ratio + for quad in pal.chunks_exact(4).take(1 << pal_bits) { + self.bw.write_buf(&quad[..3])?; + } + Ok(()) + } +} + +impl<'a> MuxCore<'a> for GIFMuxer<'a> { + fn create(&mut self, strmgr: &StreamManager) -> MuxerResult<()> { + if strmgr.get_num_streams() != 1 { + return Err(MuxerError::InvalidArgument); + } + let vstr = strmgr.get_stream(0).unwrap(); + if vstr.get_media_type() != StreamType::Video { + return Err(MuxerError::UnsupportedFormat); + } + let info = vstr.get_info(); + let vinfo = info.get_properties().get_video_info().unwrap(); + if vinfo.width > 65535 || vinfo.height > 65535 || !vinfo.format.palette { + return Err(MuxerError::UnsupportedFormat); + } + + if self.gif87 { + self.single = true; + self.bw.write_buf(b"GIF87a")?; + } else { + self.bw.write_buf(b"GIF89a")?; + } + self.bw.write_u16le(vinfo.width as u16)?; + self.bw.write_u16le(vinfo.height as u16)?; + + Ok(()) + } + fn mux_frame(&mut self, strmgr: &StreamManager, pkt: NAPacket) -> MuxerResult<()> { + if self.bw.tell() == 0 { + return Err(MuxerError::NotCreated); + } + if !self.pal_written { + let info = strmgr.get_stream(0).unwrap().get_info(); + let mut tr_idx = None; + if let Some(ref edata) = info.get_extradata() { + if edata.len() == 1 { + tr_idx = Some(edata[0]); + } else if edata.len() >= 3 { + self.bw.write_buf(edata)?; + self.pal_written = true; + } + } + if !self.pal_written { + let mut pal_found = false; + for sdata in pkt.side_data.iter() { + if let NASideData::Palette(_, ref pal) = sdata { + self.write_pal(pal,)?; + pal_found = true; + break; + } + } + if !pal_found { + return Err(MuxerError::InvalidArgument); + } + } + self.pal_written = true; + + if !self.single { + let vstr = strmgr.get_stream(0).unwrap(); + + let delay = NATimeInfo::ts_to_time(1, 100, vstr.tb_num, vstr.tb_den) as u16; + self.bw.write_byte(0x21)?; // graphic control + self.bw.write_byte(0xF9)?; // graphic control extension + self.bw.write_byte(4)?; // block size + self.bw.write_byte(if tr_idx.is_some() { 1 } else { 0 })?; // flags + self.bw.write_u16le(delay)?; + self.bw.write_byte(tr_idx.unwrap_or(0))?; // transparent colour index + self.bw.write_byte(0x00)?; // block terminator + + self.bw.write_byte(0x21)?; // graphic control + self.bw.write_byte(0xFF)?; // application extension + let app_id = b"NETSCAPE2.0"; + self.bw.write_byte(app_id.len() as u8)?; + self.bw.write_buf(app_id)?; + self.bw.write_byte(3)?; // application data block length + self.bw.write_byte(0x01)?; + self.bw.write_u16le(self.nloops)?; + self.bw.write_byte(0x00)?; // block terminator + } + } else if self.single { // just one frame is expected + return Err(MuxerError::InvalidArgument); + } + + // buffer is supposed to have all the data starting from image descriptor + let src = pkt.get_buffer(); + self.bw.write_buf(&src)?; + Ok(()) + } + fn flush(&mut self) -> MuxerResult<()> { + Ok(()) + } + fn end(&mut self) -> MuxerResult<()> { + self.bw.write_byte(0x3B)?; // GIF terminator + Ok(()) + } +} + +const MUXER_OPTS: &[NAOptionDefinition] = &[ + NAOptionDefinition { + name: "gif87", description: "Create GIF87 image", + opt_type: NAOptionDefinitionType::Bool }, + NAOptionDefinition { + name: "single", description: "Create single image", + opt_type: NAOptionDefinitionType::Bool }, + NAOptionDefinition { + name: "loops", description: "Number of times to loop the animation", + opt_type: NAOptionDefinitionType::Int(Some(0), Some(65535)) }, +]; + +impl<'a> NAOptionHandler for GIFMuxer<'a> { + fn get_supported_options(&self) -> &[NAOptionDefinition] { MUXER_OPTS } + fn set_options(&mut self, options: &[NAOption]) { + for option in options.iter() { + for opt_def in MUXER_OPTS.iter() { + if opt_def.check(option).is_ok() { + match option.name { + "gif87" => { + if let NAValue::Bool(bval) = option.value { + self.gif87 = bval; + } + }, + "single" => { + if let NAValue::Bool(bval) = option.value { + self.single = bval; + } + }, + "loops" => { + if let NAValue::Int(ival) = option.value { + self.nloops = ival as u16; + } + }, + _ => {}, + }; + } + } + } + } + fn query_option_value(&self, name: &str) -> Option<NAValue> { + match name { + "gif87" => Some(NAValue::Bool(self.gif87)), + "single" => Some(NAValue::Bool(self.single)), + "loops" => Some(NAValue::Int(i64::from(self.nloops))), + _ => None, + } + } +} + +pub struct GIFMuxerCreator {} + +impl MuxerCreator for GIFMuxerCreator { + fn new_muxer<'a>(&self, bw: &'a mut ByteWriter<'a>) -> Box<dyn MuxCore<'a> + 'a> { + Box::new(GIFMuxer::new(bw)) + } + fn get_name(&self) -> &'static str { "gif" } + fn get_capabilities(&self) -> MuxerCapabilities { MuxerCapabilities::SingleVideo("gif") } +} + +#[cfg(test)] +mod test { + use nihav_core::codecs::*; + use nihav_core::demuxers::*; + use nihav_core::muxers::*; + use nihav_codec_support::test::enc_video::*; + use crate::*; + + #[test] + fn test_gif_muxer() { + let mut dmx_reg = RegisteredDemuxers::new(); + generic_register_all_demuxers(&mut dmx_reg); + // sample: https://samples.mplayerhq.hu/image-samples/GIF/3D.gif + let dec_config = DecoderTestParams { + demuxer: "gif", + in_name: "assets/Misc/3D.gif", + limit: None, + stream_type: StreamType::None, + dmx_reg, dec_reg: RegisteredDecoders::new(), + }; + let mut mux_reg = RegisteredMuxers::new(); + generic_register_all_muxers(&mut mux_reg); + /*let enc_config = EncoderTestParams { + muxer: "gif", + enc_name: "", + out_name: "muxed.gif", + mux_reg, enc_reg: RegisteredEncoders::new(), + }; + test_remuxing(&dec_config, &enc_config);*/ + test_remuxing_md5(&dec_config, "gif", &mux_reg, + [0x7192b724, 0x2bc4fd05, 0xaa65f268, 0x3929e8bf]); + } +} diff --git a/nihav-commonfmt/src/muxers/mod.rs b/nihav-commonfmt/src/muxers/mod.rs index 809283f..7aa19bd 100644 --- a/nihav-commonfmt/src/muxers/mod.rs +++ b/nihav-commonfmt/src/muxers/mod.rs @@ -2,6 +2,8 @@ use nihav_core::muxers::*; #[cfg(feature="muxer_avi")] mod avi; +#[cfg(feature="muxer_gif")] +mod gif; #[cfg(feature="muxer_wav")] mod wav; #[cfg(feature="muxer_y4m")] @@ -10,6 +12,8 @@ mod y4m; const MUXERS: &[&dyn MuxerCreator] = &[ #[cfg(feature="muxer_avi")] &avi::AVIMuxerCreator {}, +#[cfg(feature="muxer_gif")] + &gif::GIFMuxerCreator {}, #[cfg(feature="muxer_wav")] &wav::WAVMuxerCreator {}, #[cfg(feature="muxer_y4m")] diff --git a/nihav-registry/src/detect.rs b/nihav-registry/src/detect.rs index 6603c0d..cf17875 100644 --- a/nihav-registry/src/detect.rs +++ b/nihav-registry/src/detect.rs @@ -223,6 +223,12 @@ const DETECTORS: &[DetectConditions] = &[ &CC::Str(b"ftyp")) }], }, DetectConditions { + demux_name: "gif", + extensions: ".gif", + conditions: &[CheckItem{offs: 0, cond: &CC::Or(&CC::Str(b"GIF87a"), + &CC::Str(b"GIF89a")) }], + }, + DetectConditions { demux_name: "mov", extensions: ".mov", conditions: &[CheckItem{offs: 0, cond: &CC::Str(b"\x00\x00\x00\x08wide") }, diff --git a/nihav-registry/src/register.rs b/nihav-registry/src/register.rs index ac6feca..f9f0fb2 100644 --- a/nihav-registry/src/register.rs +++ b/nihav-registry/src/register.rs @@ -287,6 +287,7 @@ static CODEC_REGISTER: &[CodecDescription] = &[ desc!(audio-ll; "tta", "True Audio codec"), desc!(audio-hyb; "wavpack", "WavPack"), + desc!(video-ll; "gif", "GIF"), desc!(video-im; "jpeg", "JPEG"), desc!(video; "h264", "ITU H.264", CODEC_CAP_COMPLEX_REORDER | CODEC_CAP_HYBRID), |