GIF support
authorKostya Shishkov <kostya.shishkov@gmail.com>
Sun, 10 Sep 2023 16:46:32 +0000 (18:46 +0200)
committerKostya Shishkov <kostya.shishkov@gmail.com>
Sun, 10 Sep 2023 16:46:32 +0000 (18:46 +0200)
nihav-commonfmt/Cargo.toml
nihav-commonfmt/src/codecs/gif.rs [new file with mode: 0644]
nihav-commonfmt/src/codecs/gifenc.rs [new file with mode: 0644]
nihav-commonfmt/src/codecs/mod.rs
nihav-commonfmt/src/demuxers/gif.rs [new file with mode: 0644]
nihav-commonfmt/src/demuxers/mod.rs
nihav-commonfmt/src/muxers/gif.rs [new file with mode: 0644]
nihav-commonfmt/src/muxers/mod.rs
nihav-registry/src/detect.rs
nihav-registry/src/register.rs

index adea42f821871a902e48281035711e30a47a7d67..e7d331350bc9e26fd62c3b84b820a052435e1f7c 100644 (file)
@@ -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 (file)
index 0000000..ccc21c6
--- /dev/null
@@ -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 (file)
index 0000000..6bb9263
--- /dev/null
@@ -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]);
+    }
+}
index ad8a8c6604b40cd9ab3fa2e9305a25b3703b004e..686ba4f6eb3ea0b83e449c68776f2e59ac21a39c 100644 (file)
@@ -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 (file)
index 0000000..eef0a00
--- /dev/null
@@ -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);
+        }
+    }
+}
index 32fe113cd4e07097cd08bb028048df79ae05bc2d..42d1f11cfc0fe50ab14103e0aa27bffbd048eec6 100644 (file)
@@ -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 (file)
index 0000000..435d1b8
--- /dev/null
@@ -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]);
+    }
+}
index 809283f6c2705ad6c37429fb3386e272353e41c6..7aa19bd28a378ae042864015dca658f00d7bff1d 100644 (file)
@@ -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")]
index 6603c0db243eeb7a9262465d0c610129f8f1b7f1..cf178757e5509d304385c860db2841979c5c1403 100644 (file)
@@ -222,6 +222,12 @@ const DETECTORS: &[DetectConditions] = &[
                                                                &CC::Str(b"moov")),
                                                                &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",
index ac6fecae84e4ad0b9f913533bf6e038d72dc1fe9..f9f0fb298f3e4127347ac216a5170dad43466ef2 100644 (file)
@@ -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),