aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKostya Shishkov <kostya.shishkov@gmail.com>2023-09-10 18:46:32 +0200
committerKostya Shishkov <kostya.shishkov@gmail.com>2023-09-10 18:46:32 +0200
commitfc39649daffd54a1f7277656f470df2751fcf620 (patch)
tree3f805416000f85dec6b16c21dcc9b71d24b399d7
parent94c520bf3df0c0e0b436d7c9a2e4b0b40550a453 (diff)
downloadnihav-fc39649daffd54a1f7277656f470df2751fcf620.tar.gz
GIF support
-rw-r--r--nihav-commonfmt/Cargo.toml12
-rw-r--r--nihav-commonfmt/src/codecs/gif.rs316
-rw-r--r--nihav-commonfmt/src/codecs/gifenc.rs671
-rw-r--r--nihav-commonfmt/src/codecs/mod.rs8
-rw-r--r--nihav-commonfmt/src/demuxers/gif.rs197
-rw-r--r--nihav-commonfmt/src/demuxers/mod.rs4
-rw-r--r--nihav-commonfmt/src/muxers/gif.rs232
-rw-r--r--nihav-commonfmt/src/muxers/mod.rs4
-rw-r--r--nihav-registry/src/detect.rs6
-rw-r--r--nihav-registry/src/register.rs1
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),