GIF support
[nihav.git] / nihav-commonfmt / src / muxers / gif.rs
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]);
+    }
+}