From 9edc99c622b9961b09a092bbc483cf949a5ccdef Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Mon, 9 Mar 2015 13:52:43 -0700 Subject: [PATCH 1/6] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fc66eef..69c405a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ `rust-media` is a media player framework for Rust, similar in spirit to `libvlc` or GStreamer. It's designed for use in Servo but is intended to be widely useful for all sorts of projects. Possible use cases are background music and FMVs for video games, as well as media player applications. -`rust-media` is currently pinned to the same version of Rust that Servo uses. This means that, until the language is stable, you may have to forward-port it somewhat. +The `master` branch of `rust-media` is currently pinned to the same version of Rust that Servo uses. The `nightly` branch is intended to track the current Rust nightly; however, like many Rust projects, it may be out of date. The library is currently in very early stages; contributions are welcome! @@ -57,4 +57,4 @@ This will probably fail with an error about `#[derive(Copy)]` in `libc`. You wil * Play a YouTube video: - $ youtube-dl https://www.youtube.com/watch?v=dQw4w9WgXcQ --exec "target/release/example {} video/mp4" \ No newline at end of file + $ youtube-dl https://www.youtube.com/watch?v=dQw4w9WgXcQ --exec "target/release/example {} video/mp4" From 6dc5a916fb5ffbe8a47abd481953ab55af18c59f Mon Sep 17 00:00:00 2001 From: Alexis Beingessner Date: Fri, 12 Jun 2015 00:16:07 -0700 Subject: [PATCH 2/6] pondering some cleanup --- container.rs | 19 ++++++++++++------ containers/gif.rs | 50 ++++------------------------------------------- containers/mkv.rs | 12 +++++------- containers/mp4.rs | 14 ++++++------- 4 files changed, 28 insertions(+), 67 deletions(-) diff --git a/container.rs b/container.rs index 29290fd..67715bb 100644 --- a/container.rs +++ b/container.rs @@ -62,15 +62,15 @@ pub trait ContainerReader { pub trait Track<'x> { fn track_type(self: Box) -> TrackType<'x>; - fn is_video(&self) -> bool; - fn is_audio(&self) -> bool; + fn is_video(&self) -> bool { false } + fn is_audio(&self) -> bool { false } /// Returns the number of clusters in this track, if possible. Returns `None` if this container /// has no table of contents (so the number of clusters is unknown). - fn cluster_count(&self) -> Option; + fn cluster_count(&self) -> Option { None } - fn number(&self) -> c_long; - fn codec(&self) -> Option>; + fn number(&self) -> c_long { 0 } + fn codec(&self) -> Option> { None } fn cluster<'a>(&'a self, cluster_index: i32) -> Result,()>; } @@ -81,8 +81,15 @@ pub trait VideoTrack<'a> : Track<'a> { /// Returns the height of this track in pixels. fn height(&self) -> u16; + /// Returns the number of times this track should be played. + /// + /// `0` indicates infinite loop. + fn num_iterations(&self) -> u32 { 1 } + /// Returns the frame rate of this track in Hertz. - fn frame_rate(&self) -> c_double; + /// + /// Since this isn't constant for all codecs, this is largely a debugging tool. + fn frame_rate(&self) -> c_double { 0.0 } /// Returns a the pixel format of this track. This is usually derived from the codec. If the /// pixel format is indexed, ignore the associated palette; each frame can have its own diff --git a/containers/gif.rs b/containers/gif.rs index eb80f14..7967a94 100644 --- a/containers/gif.rs +++ b/containers/gif.rs @@ -487,70 +487,28 @@ impl container::ContainerReader for ContainerReaderImpl { 1 } fn track_by_index<'a>(&'a self, _: u16) -> Box { - Box::new(TrackImpl { + Box::new(VideoTrackImpl { file: &self.file, - }) as Box + }) } fn track_by_number<'a>(&'a self, _: c_long) -> Box { self.track_by_index(0) } } -struct TrackImpl<'a> { - file: &'a RefCell, -} - -impl<'a> container::Track<'a> for TrackImpl<'a> { - fn track_type(self: Box) -> container::TrackType<'a> { - container::TrackType::Video(Box::new(VideoTrackImpl { - file: self.file, - }) as Box + 'a>) - } - - fn is_video(&self) -> bool { true } - fn is_audio(&self) -> bool { false } - - fn cluster_count(&self) -> Option { - None - } - - fn number(&self) -> c_long { - 0 - } - - fn codec(&self) -> Option> { - Some(vec![b'G', b'I', b'F', b'f']) - } - - fn cluster<'b>(&'b self, cluster_index: i32) -> Result,()> { - get_cluster(self.file, cluster_index) - } -} - struct VideoTrackImpl<'a> { file: &'a RefCell, } impl<'a> container::Track<'a> for VideoTrackImpl<'a> { fn track_type(self: Box) -> container::TrackType<'a> { - container::TrackType::Video(Box::new(VideoTrackImpl { - file: self.file, - }) as Box + 'a>) + container::TrackType::Video(self) } fn is_video(&self) -> bool { true } - fn is_audio(&self) -> bool { false } - - fn cluster_count(&self) -> Option { - None - } - - fn number(&self) -> c_long { - 0 - } fn codec(&self) -> Option> { - Some(vec![b'G', b'I', b'F', b'f']) + Some(b"GIFf".to_vec()) } fn cluster<'b>(&'b self, cluster_index: i32) -> Result,()> { diff --git a/containers/mkv.rs b/containers/mkv.rs index 4bfbd49..6fc209f 100644 --- a/containers/mkv.rs +++ b/containers/mkv.rs @@ -683,21 +683,21 @@ impl<'a> container::Track<'a> for TrackImpl<'a> { track: track, segment: segment, reader: reader, - }) as Box + 'a>) + })) } TrackType::Audio(track) => { container::TrackType::Audio(Box::new(AudioTrackImpl { track: track, segment: segment, reader: reader, - }) as Box + 'a>) + })) } TrackType::Other(track) => { container::TrackType::Other(Box::new(TrackImpl { track: track, segment: segment, reader: reader, - }) as Box + 'a>) + })) } } } @@ -731,11 +731,10 @@ struct VideoTrackImpl<'a> { impl<'a> container::Track<'a> for VideoTrackImpl<'a> { fn track_type(self: Box) -> container::TrackType<'a> { - container::TrackType::Video(self as Box + 'a>) + container::TrackType::Video(self) } fn is_video(&self) -> bool { true } - fn is_audio(&self) -> bool { false } fn cluster_count(&self) -> Option { Some(self.segment.count() as c_int) @@ -787,10 +786,9 @@ struct AudioTrackImpl<'a> { impl<'a> container::Track<'a> for AudioTrackImpl<'a> { fn track_type(self: Box) -> container::TrackType<'a> { - container::TrackType::Audio(self as Box + 'a>) + container::TrackType::Audio(self) } - fn is_video(&self) -> bool { false } fn is_audio(&self) -> bool { true } fn cluster_count(&self) -> Option { diff --git a/containers/mp4.rs b/containers/mp4.rs index 3aa1850..c2729e4 100644 --- a/containers/mp4.rs +++ b/containers/mp4.rs @@ -444,14 +444,14 @@ impl<'a> container::Track<'a> for TrackImpl<'a> { container::TrackType::Video(Box::new(VideoTrackImpl { id: self.id, handle: self.handle, - }) as Box) + })) } else if track_type == ffi::MP4_AUDIO_TRACK_TYPE { container::TrackType::Audio(Box::new(AudioTrackImpl { id: self.id, handle: self.handle, - }) as Box) + })) } else { - container::TrackType::Other(self as Box + 'a>) + container::TrackType::Other(self) } } @@ -491,11 +491,10 @@ pub struct VideoTrackImpl<'a> { impl<'a> container::Track<'a> for VideoTrackImpl<'a> { fn track_type(self: Box) -> container::TrackType<'a> { - container::TrackType::Video(Box::new((*self).clone()) as Box) + container::TrackType::Video(self) } fn is_video(&self) -> bool { true } - fn is_audio(&self) -> bool { false } fn cluster_count(&self) -> Option { Some(1) @@ -558,11 +557,10 @@ pub struct AudioTrackImpl<'a> { impl<'a> container::Track<'a> for AudioTrackImpl<'a> { fn track_type(self: Box) -> container::TrackType<'a> { - container::TrackType::Audio(Box::new((*self).clone()) as Box + 'a>) + container::TrackType::Audio(self) } - fn is_video(&self) -> bool { false } - fn is_audio(&self) -> bool { false } + fn is_audio(&self) -> bool { true } fn cluster_count(&self) -> Option { Some(1) From fd96dbe894c46a9883c201cf3db0a25c8f4c1878 Mon Sep 17 00:00:00 2001 From: Alexis Beingessner Date: Fri, 12 Jun 2015 14:48:39 -0700 Subject: [PATCH 3/6] full gif support --- containers/gif.rs | 1401 +++++++++++++++++++++----------------------- example/example.rs | 14 +- pixelformat.rs | 123 +++- 3 files changed, 810 insertions(+), 728 deletions(-) diff --git a/containers/gif.rs b/containers/gif.rs index 7967a94..25fcca2 100644 --- a/containers/gif.rs +++ b/containers/gif.rs @@ -14,471 +14,31 @@ #![allow(non_snake_case)] use container; -use pixelformat::{Palette, PixelFormat, RgbColor}; +use pixelformat::PixelFormat; use streaming::StreamReader; use timing::Timestamp; use videodecoder; -use libc::{self, c_double, c_int, c_long, c_uchar, c_uint, c_void, size_t}; +use libc::{c_int, c_long, c_uint}; use std::cell::RefCell; -use std::i32; -use std::mem; -use std::io::{BufReader, BufWriter, Read, Write}; -use std::io::SeekFrom; -use std::ptr; -use std::slice; -use std::marker::PhantomData; - -use byteorder::{LittleEndian, WriteBytesExt, ReadBytesExt}; - -#[repr(C)] -#[unsafe_no_drop_flag] -pub struct FileType { - /// The underlying file. - file: *mut ffi::GifFileType, - /// The byte position in the stream of the next record we have yet to read. - next_record_byte_offset: u64, -} - -impl Drop for FileType { - fn drop(&mut self) { - unsafe { - ffi::DGifCloseFile(self.file, &mut 0); - } - } -} - -impl FileType { - pub fn new(reader: Box) -> Result { - let mut error = 0; - let file = unsafe { - ffi::DGifOpen(mem::transmute::>, *mut c_void>(Box::new(reader)), - read_func, - &mut error) - }; - if !file.is_null() { - let mut file = FileType { - file: file, - next_record_byte_offset: 0, - }; - file.next_record_byte_offset = file.reader().seek(SeekFrom::Current(0)).unwrap(); - Ok(file) - } else { - Err(error) - } - } - - pub fn reader<'a>(&'a mut self) -> &'a mut StreamReader { - unsafe { - let reader: &mut Box> = mem::transmute(&mut (*self.file).UserData); - &mut ***reader - } - } - - pub fn slurp(&mut self) -> Result<(),c_int> { - unsafe { - (*self.file).ExtensionBlocks = ptr::null_mut(); - (*self.file).ExtensionBlockCount = 0; - loop { - match self.read_record() { - Ok(true) => {} - Ok(false) => break, - Err(_) => return Err((*self.file).Error), - } - } - } - Ok(()) - } - - /// This function is a port of the inner loop of `DGifSlurp()`. Returns true if there are more - /// records or false if we're done. - pub fn read_record(&mut self) -> Result { - let next_record_byte_offset = self.next_record_byte_offset; - self.reader().seek(SeekFrom::Start(next_record_byte_offset)).unwrap(); - - let mut record_type = 0; - unsafe { - if ffi::DGifGetRecordType(self.file, &mut record_type) == ffi::GIF_ERROR { - return Err(()) - } - } - - match record_type { - ffi::IMAGE_DESC_RECORD_TYPE => { - unsafe { - if ffi::DGifGetImageDesc(self.file) == ffi::GIF_ERROR { - return Err(()) - } - - { - let saved_images = self.mut_saved_images(); - let saved_image = saved_images.last_mut().unwrap(); - saved_image.RasterBits = ptr::null_mut(); - } - - if !(*self.file).ExtensionBlocks.is_null() { - let extension_blocks = (*self.file).ExtensionBlocks; - let extension_block_count = (*self.file).ExtensionBlockCount; - { - let saved_images = self.mut_saved_images(); - let saved_image = saved_images.last_mut().unwrap(); - saved_image.ExtensionBlocks = extension_blocks; - saved_image.ExtensionBlockCount = extension_block_count; - } - (*self.file).ExtensionBlocks = ptr::null_mut(); - (*self.file).ExtensionBlockCount = 0; - } - } - - if self.read_image().is_err() { - return Err(()) - } - } - ffi::EXTENSION_RECORD_TYPE => { - unsafe { - let (mut ext_function, mut ext_data) = (0, ptr::null_mut()); - if ffi::DGifGetExtension(self.file, &mut ext_function, &mut ext_data) == - ffi::GIF_ERROR { - return Err(()) - } - - if !ext_data.is_null() { - if ffi::GifAddExtensionBlock(&mut (*self.file).ExtensionBlockCount, - &mut (*self.file).ExtensionBlocks, - ext_function, - *ext_data as c_uint, - ext_data.offset(1)) == ffi::GIF_ERROR { - return Err(()) - } - } - - while !ext_data.is_null() { - if ffi::DGifGetExtensionNext(self.file, &mut ext_data) == ffi::GIF_ERROR { - return Err(()) - } - if !ext_data.is_null() { - if ffi::GifAddExtensionBlock(&mut (*self.file).ExtensionBlockCount, - &mut (*self.file).ExtensionBlocks, - ffi::CONTINUE_EXT_FUNC_CODE, - *ext_data as c_uint, - ext_data.offset(1)) == ffi::GIF_ERROR { - return Err(()) - } - } - } - } - } - _ => return Ok(false), - } - - self.next_record_byte_offset = self.reader().seek(SeekFrom::Current(0)).unwrap(); - Ok(true) - } - - /// A port of a section of `DGifSlurp`. - fn read_image(&mut self) -> Result<(),()> { - unsafe { - let saved_image = self.mut_saved_images().last_mut().unwrap(); - if !saved_image.RasterBits.is_null() { - // Already read! - return Ok(()) - } - - let image_size = saved_image.ImageDesc.Width * saved_image.ImageDesc.Height; - let image_byte_size = image_size as usize * mem::size_of::(); - assert!(image_byte_size <= i32::MAX as usize); - saved_image.RasterBits = libc::malloc(image_size as size_t) as *mut c_uchar; - if saved_image.RasterBits.is_null() { - return Err(()) - } - let mut raster_bits = slice::from_raw_parts_mut(saved_image.RasterBits, - image_size as usize); - - if (*saved_image).ImageDesc.Interlace { - // From `DGifSlurp()`: "The way an interlaced image should be read: offsets and - // jumps…" - static INTERLACED_OFFSETS: [u8; 4] = [ 0, 4, 2, 1 ]; - static INTERLACED_JUMPS: [u8; 4] = [ 8, 8, 4, 2 ]; - - // Perform four passes on the image. - for (i, &j) in INTERLACED_OFFSETS.iter().enumerate() { - let mut j = j as c_int; - while j <= saved_image.ImageDesc.Height { - let width = saved_image.ImageDesc.Width; - let dest = - &mut raster_bits[((j * width) as usize)..((j + 1) * width) as usize]; - assert!(dest.len() <= i32::MAX as usize); - if ffi::DGifGetLine(self.file, dest.as_mut_ptr(), dest.len() as i32) == - ffi::GIF_ERROR { - return Err(()) - } - j += INTERLACED_JUMPS[i] as c_int; - } - } - } else if ffi::DGifGetLine(self.file, - raster_bits.as_mut_ptr(), - raster_bits.len() as i32) == - ffi::GIF_ERROR { - return Err(()) - } - } - Ok(()) - } - - pub fn width(&self) -> ffi::GifWord { - unsafe { - (*self.file).SWidth - } - } - - pub fn height(&self) -> ffi::GifWord { - unsafe { - (*self.file).SHeight - } - } - - pub fn color_map<'a>(&'a self) -> Option> { - unsafe { - if !(*self.file).SColorMap.is_null() { - Some(ColorMapObject { - map: (*self.file).SColorMap, - phantom: PhantomData, - }) - } else { - None - } - } - } - - pub fn saved_images<'a>(&'a self) -> &'a [SavedImage] { - unsafe { - slice::from_raw_parts((*self.file).SavedImages, (*self.file).ImageCount as usize) - } - } - - pub unsafe fn mut_saved_images<'a>(&'a mut self) -> &'a mut [SavedImage] { - slice::from_raw_parts_mut((*self.file).SavedImages, (*self.file).ImageCount as usize) - } - - pub fn extension_block_count(&self) -> c_int { - unsafe { - (*self.file).ExtensionBlockCount - } - } - - pub fn extension_block<'a>(&'a self, index: c_int) -> ExtensionBlock<'a> { - assert!(index >= 0 && index < self.extension_block_count()); - unsafe { - ExtensionBlock::from_ptr((*self.file).ExtensionBlocks.offset(index as isize)) - } - } -} - -extern "C" fn read_func(file: *mut ffi::GifFileType, buffer: *mut ffi::GifByteType, len: c_int) - -> c_int { - if len < 0 { - return -1 - } - - unsafe { - let reader: &mut Box> = mem::transmute(&mut (*file).UserData); - match reader.read(slice::from_raw_parts_mut(buffer, len as usize)) { - Ok(number_read) => number_read as c_int, - _ => -1 - } - } -} - -#[repr(C)] -pub struct SavedImage { - ImageDesc: ffi::GifImageDesc, - RasterBits: *mut ffi::GifByteType, - ExtensionBlockCount: c_int, - ExtensionBlocks: *mut ffi::ExtensionBlock, -} - -impl SavedImage { - pub fn raster_bits<'a>(&'a self) -> &'a [ffi::GifByteType] { - unsafe { - slice::from_raw_parts_mut(self.RasterBits, - self.ImageDesc.Width as usize * self.ImageDesc.Height as usize) - } - } - - pub fn image_desc<'a>(&'a self) -> ImageDesc<'a> { - ImageDesc { - desc: &self.ImageDesc, - } - } - - pub fn extension_block_count(&self) -> c_int { - self.ExtensionBlockCount - } - - pub fn extension_block<'a>(&'a self, index: c_int) -> ExtensionBlock<'a> { - assert!(index >= 0 && index < self.extension_block_count()); - unsafe { - ExtensionBlock::from_ptr(self.ExtensionBlocks.offset(index as isize)) - } - } -} - -#[derive(Copy, Clone)] -pub struct ImageDesc<'a> { - desc: &'a ffi::GifImageDesc, -} - -impl<'a> ImageDesc<'a> { - pub fn width(&self) -> ffi::GifWord { - self.desc.Width - } - - pub fn height(&self) -> ffi::GifWord { - self.desc.Height - } - - pub fn interlace(&self) -> bool { - self.desc.Interlace - } - - pub fn color_map(&self) -> Option> { - if !self.desc.ColorMap.is_null() { - Some(ColorMapObject { - map: self.desc.ColorMap, - phantom: PhantomData, - }) - } else { - None - } - } -} - -pub struct ColorMapObject<'a> { - map: *mut ffi::ColorMapObject, - phantom: PhantomData<&'a u8>, -} - -impl<'a> ColorMapObject<'a> { - pub fn bits_per_pixel(&self) -> c_int { - unsafe { - (*self.map).BitsPerPixel - } - } - - pub fn colors(&'a self) -> &'a [ffi::GifColorType] { - unsafe { - slice::from_raw_parts_mut((*self.map).Colors, (*self.map).ColorCount as usize) - } - } -} - -pub enum ExtensionBlock<'a> { - Continue, - Comment(&'a [u8]), - Graphics(GraphicsControlBlock), - Plaintext(&'a [u8]), - Application(&'a [u8]), - Other, -} - -impl<'a> ExtensionBlock<'a> { - unsafe fn from_ptr(block: *mut ffi::ExtensionBlock) -> ExtensionBlock<'a> { - return match (*block).Function { - ffi::CONTINUE_EXT_FUNC_CODE => ExtensionBlock::Continue, - ffi::COMMENT_EXT_FUNC_CODE => ExtensionBlock::Comment(to_byte_slice(block)), - ffi::GRAPHICS_EXT_FUNC_CODE => { - let mut graphics_control_block = ffi::GraphicsControlBlock { - DisposalMode: 0, - UserInputFlag: false, - DelayTime: 0, - TransparentColor: 0, - }; - assert!(ffi::DGifExtensionToGCB((*block).ByteCount as size_t, - (*block).Bytes, - &mut graphics_control_block) == ffi::GIF_OK); - ExtensionBlock::Graphics(GraphicsControlBlock { - block: graphics_control_block, - }) - } - ffi::PLAINTEXT_EXT_FUNC_CODE => ExtensionBlock::Plaintext(to_byte_slice(block)), - ffi::APPLICATION_EXT_FUNC_CODE => ExtensionBlock::Application(to_byte_slice(block)), - _ => ExtensionBlock::Other, - }; - - unsafe fn to_byte_slice<'a>(block: *mut ffi::ExtensionBlock) -> &'a [u8] { - assert!((*block).ByteCount >= 0); - slice::from_raw_parts_mut((*block).Bytes, (*block).ByteCount as usize) - } - } -} - -pub struct GraphicsControlBlock { - block: ffi::GraphicsControlBlock, -} - -impl GraphicsControlBlock { - /// Particular way to initialize the pixels of the frame - pub fn disposal_mode(&self) -> DisposalMode { - // FIXME(Gankro): didn't want to pull in a whole crate/syntex for FromPrimitive - match self.block.DisposalMode { - ffi::DISPOSAL_UNSPECIFIED => DisposalMode::Unspecified, - ffi::DISPOSE_DO_NOT => DisposalMode::DoNot, - ffi::DISPOSE_BACKGROUND => DisposalMode::Background, - ffi::DISPOSE_PREVIOUS => DisposalMode::Previous, - _ => unreachable!(), - } - } - - /// archaic; specifies that the gif should wait for user input before proceeding. - pub fn user_input_flag(&self) -> bool { - self.block.UserInputFlag - } - - pub fn delay_time(&self) -> c_int { - self.block.DelayTime - } - - /// Which colour index to interpret as a transparent pixel. - /// Note: this still overwrites a non-trasparent pixel. - pub fn transparent_color(&self) -> Option { - let color = self.block.TransparentColor; - if color >= 0 { - Some(color) - } else { - None - } - } -} - -#[repr(i32)] -#[derive(Copy, Clone)] -/// Specifies what state to initialize the frame's pixels in -pub enum DisposalMode { - // Treat this like Background, I guess - Unspecified = ffi::DISPOSAL_UNSPECIFIED, - // Use the previous frame as the starting point - DoNot = ffi::DISPOSE_DO_NOT, - // Blank the frame to the background colour - Background = ffi::DISPOSE_BACKGROUND, - // Use the previous-previous frame as the starting point (archaic?) - Previous = ffi::DISPOSE_PREVIOUS, -} - -// Implementation of the abstract `ContainerReader` interface +use std::io::{Read, Error}; +use std::io::Result as IoResult; +use std::io::ErrorKind::InvalidInput; pub struct ContainerReaderImpl { - file: RefCell, + gif: RefCell, } impl ContainerReaderImpl { pub fn new(reader: Box) -> Result,()> { - let file = match FileType::new(reader) { - Ok(file) => file, - Err(_) => return Err(()), + let gif = match Gif::new(reader) { + Ok(gif) => gif, + _ => return Err(()), }; + Ok(Box::new(ContainerReaderImpl { - file: RefCell::new(file), - }) as Box) + gif: RefCell::new(gif), + })) } } @@ -488,7 +48,7 @@ impl container::ContainerReader for ContainerReaderImpl { } fn track_by_index<'a>(&'a self, _: u16) -> Box { Box::new(VideoTrackImpl { - file: &self.file, + gif: &self.gif, }) } fn track_by_number<'a>(&'a self, _: c_long) -> Box { @@ -497,7 +57,7 @@ impl container::ContainerReader for ContainerReaderImpl { } struct VideoTrackImpl<'a> { - file: &'a RefCell, + gif: &'a RefCell, } impl<'a> container::Track<'a> for VideoTrackImpl<'a> { @@ -512,64 +72,46 @@ impl<'a> container::Track<'a> for VideoTrackImpl<'a> { } fn cluster<'b>(&'b self, cluster_index: i32) -> Result,()> { - get_cluster(self.file, cluster_index) + // Read and decode frames until we get to the given cluster index. + while self.gif.borrow().frames.len() < (cluster_index as usize + 1) { + match self.gif.borrow_mut().parse_next_frame() { + Err(_) | Ok(false) => return Err(()), + Ok(true) => {} + } + } + + Ok(Box::new(ClusterImpl { + gif: self.gif, + frame_index: cluster_index as usize, + })) } } impl<'a> container::VideoTrack<'a> for VideoTrackImpl<'a> { fn width(&self) -> u16 { - self.file.borrow().width() as u16 + self.gif.borrow().width as u16 } fn height(&self) -> u16 { - self.file.borrow().height() as u16 - } - - fn frame_rate(&self) -> c_double { - // NB: This assumes a constant frame rate. Not all GIFs have one, however… - for saved_image in self.file.borrow().saved_images().iter() { - for i in 0..saved_image.extension_block_count() { - if let ExtensionBlock::Graphics(block) = saved_image.extension_block(i) { - return 1.0 / ((block.delay_time() as c_double) * 0.01) - } - } - } - for i in 0..self.file.borrow().extension_block_count() { - if let ExtensionBlock::Graphics(block) = self.file.borrow().extension_block(i) { - return 1.0 / ((block.delay_time() as c_double) * 0.01) - } - } - 1.0 + self.gif.borrow().height as u16 } fn pixel_format(&self) -> PixelFormat<'static> { - PixelFormat::Indexed(Palette::empty()) + PIXEL_FORMAT } - fn headers(&self) -> Box { - Box::new(videodecoder::EmptyVideoHeadersImpl) as Box + fn num_iterations(&self) -> u32 { + self.gif.borrow().num_iterations as u32 } -} -fn get_cluster<'a>(file: &'a RefCell, cluster_index: i32) - -> Result,()> { - // Read and decode frames until we get to the given cluster index. - while file.borrow().saved_images().len() < (cluster_index as usize + 1) { - match file.borrow_mut().read_record() { - Err(_) | Ok(false) => return Err(()), - Ok(true) => {} - } + fn headers(&self) -> Box { + Box::new(videodecoder::EmptyVideoHeadersImpl) } - - Ok(Box::new(ClusterImpl { - file: file, - image_index: cluster_index as usize, - }) as Box) } struct ClusterImpl<'a> { - file: &'a RefCell, - image_index: usize, + gif: &'a RefCell, + frame_index: usize, } impl<'a> container::Cluster for ClusterImpl<'a> { @@ -577,8 +119,8 @@ impl<'a> container::Cluster for ClusterImpl<'a> { -> Result,()> { if frame_index == 0 { Ok(Box::new(FrameImpl { - file: self.file, - image_index: self.image_index, + gif: self.gif, + frame_index: self.frame_index, }) as Box) } else { Err(()) @@ -587,42 +129,19 @@ impl<'a> container::Cluster for ClusterImpl<'a> { } struct FrameImpl<'a> { - file: &'a RefCell, - image_index: usize, + gif: &'a RefCell, + frame_index: usize, } impl<'a> container::Frame for FrameImpl<'a> { fn len(&self) -> c_long { - let file = self.file.borrow(); - let saved_image = &file.saved_images()[self.image_index]; - let color_map = match saved_image.image_desc().color_map() { - Some(map) => map, - None => file.color_map().unwrap(), - }; - (2 + (color_map.colors().len() * 3) + saved_image.raster_bits().len()) as c_long + self.gif.borrow().frames[self.frame_index].data.len() as c_long } fn read(&self, buffer: &mut [u8]) -> Result<(),()> { - let file = self.file.borrow(); - let saved_image = &file.saved_images()[self.image_index]; - let mut writer = BufWriter::new(buffer); - let color_map = match saved_image.image_desc().color_map() { - Some(map) => map, - None => file.color_map().unwrap(), - }; - - if writer.write_u16::(color_map.colors().len() as u16).is_err() { - return Err(()) - } - for color in color_map.colors().iter() { - if writer.write_all(&[color.Red, color.Green, color.Blue]).is_err() { - return Err(()) - } - } - match writer.write_all(saved_image.raster_bits()) { - Ok(_) => Ok(()), - Err(_) => Err(()), - } + ::std::slice::bytes::copy_memory(&self.gif.borrow().frames[self.frame_index].data, + buffer); + Ok(()) } fn track_number(&self) -> c_long { @@ -630,7 +149,11 @@ impl<'a> container::Frame for FrameImpl<'a> { } fn time(&self) -> Timestamp { - get_time(self.file, self.image_index) + let time = self.gif.borrow().frames[self.frame_index].time; + Timestamp { + ticks: time as i64, + ticks_per_second: 100.0, + } } fn rendering_offset(&self) -> i64 { @@ -638,32 +161,6 @@ impl<'a> container::Frame for FrameImpl<'a> { } } -/// FIXME(pcwalton): This is O(n)! -fn get_time(file: &RefCell, image_index: usize) -> Timestamp { - let mut time_so_far = 0; - for (i, saved_image) in file.borrow().saved_images().iter().enumerate() { - if i >= image_index { - break - } - for j in 0..saved_image.extension_block_count() { - if let ExtensionBlock::Graphics(block) = saved_image.extension_block(j) { - time_so_far = time_so_far + block.delay_time() as i64 - } - } - } - if time_so_far == 0 { - for i in 0..file.borrow().extension_block_count() { - if let ExtensionBlock::Graphics(block) = file.borrow().extension_block(i) { - time_so_far = time_so_far + block.delay_time() as i64 - } - } - } - Timestamp { - ticks: time_so_far, - ticks_per_second: 100.0, - } -} - pub const CONTAINER_READER: container::RegisteredContainerReader = container::RegisteredContainerReader { mime_types: &["image/gif"], @@ -674,84 +171,54 @@ pub const CONTAINER_READER: container::RegisteredContainerReader = #[allow(missing_copy_implementations)] struct VideoDecoderImpl { - width: c_int, - height: c_int, + width: u32, + height: u32, } impl VideoDecoderImpl { fn new(_: &videodecoder::VideoHeaders, width: i32, height: i32) -> Result,()> { Ok(Box::new(VideoDecoderImpl { - width: width, - height: height, - }) as Box) + width: width as u32, + height: height as u32, + })) } } impl videodecoder::VideoDecoder for VideoDecoderImpl { fn decode_frame(&self, data: &[u8], presentation_time: &Timestamp) -> Result,()> { - let mut reader = BufReader::new(data); - let palette_size = match reader.read_u16::() { - Ok(size) => size, - Err(_) => return Err(()), - }; - let mut palette = Vec::new(); - let mut color_bytes = [0, 0, 0]; - for _ in 0 .. palette_size { - match reader.read(&mut color_bytes) { - Ok(3) => { - palette.push(RgbColor { - r: color_bytes[0], - g: color_bytes[1], - b: color_bytes[2], - }) - } - _ => return Err(()), - } - } - - let mut pixels = vec![]; - match reader.read_to_end(&mut pixels) { - Ok(_) => {}, // Should we check anything here? - Err(_) => return Err(()), - } - Ok(Box::new(DecodedVideoFrameImpl { width: self.width, height: self.height, - palette: palette, - pixels: pixels, + pixels: data.to_vec(), presentation_time: *presentation_time, - }) as Box) + })) } } struct DecodedVideoFrameImpl { - width: i32, - height: i32, - palette: Vec, + width: u32, + height: u32, pixels: Vec, presentation_time: Timestamp, } impl videodecoder::DecodedVideoFrame for DecodedVideoFrameImpl { fn width(&self) -> c_uint { - self.width as c_uint + self.width } fn height(&self) -> c_uint { - self.height as c_uint + self.height } fn stride(&self, _: usize) -> c_int { - self.width + (BYTES_PER_COL * self.width as usize) as c_int } fn pixel_format<'a>(&'a self) -> PixelFormat<'a> { - PixelFormat::Indexed(Palette { - palette: &self.palette, - }) + PIXEL_FORMAT } fn presentation_time(&self) -> Timestamp { @@ -777,138 +244,626 @@ impl<'a> videodecoder::DecodedVideoFrameLockGuard for DecodedVideoFrameLockGuard pub const VIDEO_DECODER: videodecoder::RegisteredVideoDecoder = videodecoder::RegisteredVideoDecoder { - id: [ b'G', b'I', b'F', b'f' ], + id: *b"GIFf", constructor: VideoDecoderImpl::new, }; -pub mod ffi { - use libc::{c_char, c_int, c_uchar, c_uint, c_void, size_t}; - - use containers::gif::SavedImage; - - pub const GIF_ERROR: c_int = 0; - pub const GIF_OK: c_int = 1; - - pub const CONTINUE_EXT_FUNC_CODE: c_int = 0x00; - pub const COMMENT_EXT_FUNC_CODE: c_int = 0xfe; - pub const GRAPHICS_EXT_FUNC_CODE: c_int = 0xf9; - pub const PLAINTEXT_EXT_FUNC_CODE: c_int = 0x01; - pub const APPLICATION_EXT_FUNC_CODE: c_int = 0xff; - - pub const DISPOSAL_UNSPECIFIED: c_int = 0; - pub const DISPOSE_DO_NOT: c_int = 1; - pub const DISPOSE_BACKGROUND: c_int = 2; - pub const DISPOSE_PREVIOUS: c_int = 3; - - pub const NO_TRANSPARENT_COLOR: c_int = -1; - - pub const UNDEFINED_RECORD_TYPE: GifRecordType = 0; - pub const SCREEN_DESC_RECORD_TYPE: GifRecordType = 1; - pub const IMAGE_DESC_RECORD_TYPE: GifRecordType = 2; - pub const EXTENSION_RECORD_TYPE: GifRecordType = 3; - pub const TERMINATE_RECORD_TYPE: GifRecordType = 4; - - pub type GifPixelType = c_uchar; - pub type GifRowType = *mut c_uchar; - pub type GifByteType = c_uchar; - pub type GifPrefixType = c_uint; - pub type GifWord = c_int; - pub type GifRecordType = c_int; - pub type InputFunc = extern "C" fn(*mut GifFileType, *mut GifByteType, c_int) -> c_int; - - #[repr(C)] - pub struct GifFileType { - pub SWidth: GifWord, - pub SHeight: GifWord, - pub SColorResolution: GifWord, - pub SBackGroundColor: GifWord, - pub AspectByte: GifByteType, - pub SColorMap: *mut ColorMapObject, - pub ImageCount: c_int, - pub Image: GifImageDesc, - pub SavedImages: *mut SavedImage, - pub ExtensionBlockCount: c_int, - pub ExtensionBlocks: *mut ExtensionBlock, - pub Error: c_int, - pub UserData: *mut c_void, - pub Private: *mut c_void, - } - - #[repr(C)] - #[allow(missing_copy_implementations)] - pub struct GifImageDesc { - pub Left: GifWord, - pub Top: GifWord, - pub Width: GifWord, - pub Height: GifWord, - pub Interlace: bool, - pub ColorMap: *mut ColorMapObject, - } - - #[repr(C)] - #[allow(missing_copy_implementations)] - pub struct ColorMapObject { - pub ColorCount: c_int, - pub BitsPerPixel: c_int, - pub SortFlag: bool, - pub Colors: *mut GifColorType, - } - - #[repr(C)] - #[allow(missing_copy_implementations)] - pub struct GifColorType { - pub Red: GifByteType, - pub Green: GifByteType, - pub Blue: GifByteType, - } - - #[repr(C)] - #[allow(missing_copy_implementations)] - pub struct ExtensionBlock { - pub ByteCount: c_int, - pub Bytes: *mut GifByteType, - pub Function: c_int, - } - - #[repr(C)] - #[allow(missing_copy_implementations)] - pub struct GraphicsControlBlock { - pub DisposalMode: c_int, - pub UserInputFlag: bool, - pub DelayTime: c_int, - pub TransparentColor: c_int, - } - - #[link(name = "gif")] - extern { - pub fn DGifOpenFileName(GifFileType: *const c_char, Error: *mut c_int) -> *mut GifFileType; - pub fn DGifOpenFileHandle(GifFileHandle: c_int, Error: *mut c_int) -> *mut GifFileType; - pub fn DGifOpen(userPtr: *mut c_void, readFunc: InputFunc, Error: *mut c_int) - -> *mut GifFileType; - pub fn DGifSlurp(GifFile: *mut GifFileType) -> c_int; - pub fn DGifCloseFile(GifFile: *mut GifFileType, ErrorCode: *mut c_int) -> c_int; - pub fn DGifGetRecordType(GifFile: *mut GifFileType, GifType: *mut GifRecordType) -> c_int; - pub fn DGifGetImageDesc(GifFile: *mut GifFileType) -> c_int; - pub fn DGifGetLine(GifFile: *mut GifFileType, - GifLine: *mut GifPixelType, - GifLineLen: c_int) - -> c_int; - pub fn DGifGetExtension(GifFile: *mut GifFileType, - GifExtCode: *mut c_int, - GifExtension: *mut *mut GifByteType) - -> c_int; - pub fn DGifGetExtensionNext(GifFile: *mut GifFileType, GifExtension: *mut *mut GifByteType) - -> c_int; - pub fn GifAddExtensionBlock(ExtensionBlock_Count: *mut c_int, - ExtensionBlocks: *mut *mut ExtensionBlock, - Function: c_int, - Len: c_uint, - ExtData: *mut c_uchar) - -> c_int; - pub fn DGifExtensionToGCB(GifExtensionLength: size_t, - GifExtension: *const GifByteType, - GCB: *mut GraphicsControlBlock) - -> c_int; + + + + + + + + + +const HEADER_LEN: usize = 6; +const GLOBAL_DESCRIPTOR_LEN: usize = 7; +const LOCAL_DESCRIPTOR_LEN: usize = 9; +const GRAPHICS_EXTENSION_LEN: usize = 6; +const APPLICATION_EXTENSION_LEN: usize = 17; +const MAX_COLOR_TABLE_SIZE: usize = 3 * 256; // 2^8 RGB colours + +const BLOCK_EOF: u8 = 0x3B; +const BLOCK_EXTENSION: u8 = 0x21; +const BLOCK_IMAGE: u8 = 0x2C; + +const EXTENSION_PLAIN: u8 = 0x01; +const EXTENSION_COMMENT: u8 = 0xFE; +const EXTENSION_GRAPHICS: u8 = 0xF9; +const EXTENSION_APPLICATION: u8 = 0xFF; + +const DISPOSAL_UNSPECIFIED: u8 = 0; +const DISPOSAL_CURRENT: u8 = 1; +const DISPOSAL_BG: u8 = 2; +const DISPOSAL_PREVIOUS: u8 = 3; + +const BYTES_PER_COL: usize = 4; +const PIXEL_FORMAT: PixelFormat<'static> = PixelFormat::Rgba32; + +pub struct Gif { + pub width: usize, + pub height: usize, + /// Defaults to 1, but at some point we may discover a new value. + /// Presumably this should only happen once. + pub num_iterations: u16, + pub frames: Vec, + gct_bg: usize, + gct: Option>, + data: Box, +} + +pub struct Frame { + pub time: u32, + pub duration: u32, + pub data: Vec +} + + +impl Gif { + /// Interpret the given Reader as an entire Gif file. Parses out the + /// prelude to get most metadata (some will show up later, maybe). + fn new (mut data: Box) -> IoResult { + + // ~~~~~~~~~ Image Prelude ~~~~~~~~~~ + let mut buf = [0; HEADER_LEN + GLOBAL_DESCRIPTOR_LEN]; + try!(read_to_full(&mut data, &mut buf)); + + let (header, descriptor) = buf.split_at(HEADER_LEN); + if header != b"GIF87a" && header != b"GIF89a" { return Err(malformed()); } + + let full_width = le_u16(descriptor[0], descriptor[1]); + let full_height = le_u16(descriptor[2], descriptor[3]); + let gct_mega_field = descriptor[4]; + let gct_background_color_index = descriptor[5] as usize; + let gct_flag = (gct_mega_field & 0b1000_0000) != 0; + let gct_size_exponent = gct_mega_field & 0b0000_0111; + let gct_size = 1usize << (gct_size_exponent + 1); // 2^(k+1) + + let gct = if gct_flag { + let mut gct_buf = Box::new([0; MAX_COLOR_TABLE_SIZE]); + { + let gct = &mut gct_buf[.. 3 * gct_size]; + try!(read_to_full(&mut data, gct)); + } + Some(gct_buf) + } else { + None + }; + + Ok(Gif { + width: full_width as usize, + height: full_height as usize, + num_iterations: 1, // This may be changed as we parse more + frames: vec![], + gct_bg: gct_background_color_index, + gct: gct, + data: data, + }) + } + + + /// Reads more of the stream until an entire new frame has been computed. + /// Returns `false` if the file ends, and `true` otherwise. + fn parse_next_frame(&mut self) -> IoResult { + // ~~~~~~~~~ Image Body ~~~~~~~~~~~ + + // local to this frame of the gif, but may be obtained at any time. + let mut transparent_index = None; + let mut frame_delay = 0; + let mut disposal_method = 0; + + loop { + match try!(read_byte(&mut self.data)) { + BLOCK_EOF => { + // TODO: check if this was a sane place to stop? + return Ok(false) + } + BLOCK_EXTENSION => { + // 3 to coalesce some checks we'll have to make in any branch + match try!(read_byte(&mut self.data)) { + EXTENSION_PLAIN | EXTENSION_COMMENT => { + // This is legacy garbage, but has a variable length so + // we need to parse it a bit to get over it. + try!(skip_blocks(&mut self.data)); + } + EXTENSION_GRAPHICS => { + // Frame delay and transparency settings + let mut ext = [0; GRAPHICS_EXTENSION_LEN]; + try!(read_to_full(&mut self.data, &mut ext)); + + let rendering_mega_field = ext[1]; + let transparency_flag = (rendering_mega_field & 0b0000_0001) != 0; + + disposal_method = (rendering_mega_field & 0b0001_1100) >> 2; + frame_delay = le_u16(ext[2], ext[3]); + transparent_index = if transparency_flag { + Some(ext[4]) + } else { + None + }; + } + EXTENSION_APPLICATION => { + // NETSCAPE 2.0 Looping Extension + + let mut ext = [0; APPLICATION_EXTENSION_LEN]; + try!(read_to_full(&mut self.data, &mut ext)); + + // TODO: Verify this is the NETSCAPE 2.0 extension? + + self.num_iterations = le_u16(ext[14], ext[15]); + } + _ => { return Err(Error::new(InvalidInput, "Unknown extension type found")); } + } + } + BLOCK_IMAGE => { + let mut descriptor = [0; LOCAL_DESCRIPTOR_LEN]; + try!(read_to_full(&mut self.data, &mut descriptor)); + + let x = le_u16(descriptor[0], descriptor[1]) as usize; + let y = le_u16(descriptor[2], descriptor[3]) as usize; + let width = le_u16(descriptor[4], descriptor[5]) as usize; + let height = le_u16(descriptor[6], descriptor[7]) as usize; + + let lct_mega_field = descriptor[8]; + let lct_flag = (lct_mega_field & 0b1000_0000) != 0; + let interlace = (lct_mega_field & 0b0100_0000) != 0; + let lct_size_exponent = (lct_mega_field & 0b0000_1110) >> 1; + let lct_size = 1usize << (lct_size_exponent + 1); // 2^(k+1) + + + let mut lct_buf = [0; MAX_COLOR_TABLE_SIZE]; + + let lct = if lct_flag { + let lct = &mut lct_buf[.. 3 * lct_size]; + try!(read_to_full(&mut self.data, lct)); + Some(&*lct) + } else { + None + }; + + let minimum_code_size = try!(read_byte(&mut self.data)); + + let mut indices = vec![0; width * height]; //TODO: not this + + let mut parse_state = create_parse_state(minimum_code_size, width * height); + + /* For debugging + println!(""); + println!("starting frame decoding: {}", frames.len()); + println!("x: {}, y: {}, w: {}, h: {}", x, y, width, height); + println!("trans: {:?}, interlace: {}", transparent_index, interlace); + println!("delay: {}, disposal: {}, iters: {:?}", + frame_delay, disposal_method, num_iterations); + println!("lct: {}, gct: {}", lct_flag, gct_flag); + */ + + // ~~~~~~~~~~~~~~ DECODE THE INDICES ~~~~~~~~~~~~~~~~ + + if interlace { + let interlaced_offset = [0, 4, 2, 1]; + let interlaced_jumps = [8, 8, 4, 2]; + for i in 0..4 { + let mut j = interlaced_offset[i]; + while j < height { + try!(get_indices(&mut parse_state, + &mut indices[j * width..], + width, + &mut self.data)); + j += interlaced_jumps[i]; + } + } + } else { + try!(get_indices(&mut parse_state, &mut indices, width * height, &mut self.data)); + } + + // ~~~~~~~~~~~~~~ INITIALIZE THE BACKGROUND ~~~~~~~~~~~ + + let num_bytes = self.width * self.height * BYTES_PER_COL; + + let mut pixels = match disposal_method { + DISPOSAL_UNSPECIFIED => { + vec![0; num_bytes] + } + DISPOSAL_CURRENT => { + self.frames.last().map(|frame| frame.data.clone()) + .unwrap_or_else(|| vec![0; num_bytes]) + } + DISPOSAL_BG => { + let col_idx = self.gct_bg as usize; + let color_map = self.gct.as_ref().unwrap(); + let is_transparent = transparent_index.map(|idx| idx as usize == col_idx) + .unwrap_or(false); + let (r, g, b, a) = if is_transparent { + (0, 0, 0, 0) + } else { + let col_idx = col_idx as usize; + let r = color_map[col_idx * 3 + 0]; + let g = color_map[col_idx * 3 + 1]; + let b = color_map[col_idx * 3 + 2]; + (r, g, b, 0xFF) + }; + + let mut buf = Vec::with_capacity(num_bytes); + while buf.len() < num_bytes { + buf.push(r); + buf.push(g); + buf.push(b); + buf.push(a); + } + buf + } + DISPOSAL_PREVIOUS => { + let num_frames = self.frames.len(); + if num_frames > 1 { + self.frames[num_frames - 2].data.clone() + } else { + vec![0; num_bytes] + } + } + _ => { + return Err(Error::new(InvalidInput, "unsupported disposal method")); + } + }; + + // ~~~~~~~~~~~~~~~~~~~ MAP INDICES TO COLORS ~~~~~~~~~~~~~~~~~~ + { + let color_map = lct.unwrap_or_else(|| &**self.gct.as_ref().unwrap()); + for (pix_idx, col_idx) in indices.into_iter().enumerate() { + let is_transparent = transparent_index.map(|idx| idx == col_idx) + .unwrap_or(false); + + // A transparent pixel "shows through" to whatever pixels + // were drawn before. True transparency can only be set + // in the disposal phase, as far as I can tell. + if is_transparent { continue; } + + let col_idx = col_idx as usize; + let r = color_map[col_idx * 3 + 0]; + let g = color_map[col_idx * 3 + 1]; + let b = color_map[col_idx * 3 + 2]; + let a = 0xFF; + + // we're blitting this frame on top of some perhaps larger + // canvas. We need to adjust accordingly. + let pix_idx = x + y * self.width + + if width == self.width { + pix_idx + } else { + let row = pix_idx / width; + let col = pix_idx % width; + row * self.width + col + }; + pixels[pix_idx * BYTES_PER_COL + 0] = r; + pixels[pix_idx * BYTES_PER_COL + 1] = g; + pixels[pix_idx * BYTES_PER_COL + 2] = b; + pixels[pix_idx * BYTES_PER_COL + 3] = a; + } + } + // ~~~~~~~~~~~~~~~~~~ DONE!!! ~~~~~~~~~~~~~~~~~~~~ + + let time = self.frames.last() + .map(|frame| frame.time + frame.duration) + .unwrap_or(0); + self.frames.push(Frame { + data: pixels, + duration: frame_delay as u32, + time: time, + }); + return Ok(true) + } + _ => { + return Err(Error::new(InvalidInput, "unknown block found")); + } + } + } } } + + + +// ~~~~~~~~~~~~~~~~~ utilities for decoding LZW data ~~~~~~~~~~~~~~~~~~~ + +const LZ_MAX_CODE: usize = 4095; +const LZ_BITS: usize = 12; + +const NO_SUCH_CODE: usize = 4098; // Impossible code, to signal empty. + +struct ParseState { + bits_per_pixel: usize, + clear_code: usize, + eof_code: usize, + running_code: usize, + running_bits: usize, + max_code_1: usize, + last_code: usize, + stack_ptr: usize, + current_shift_state: usize, + current_shift_dword: usize, + pixel_count: usize, + buf: [u8; 256], // [0] = len, [1] = cur_index + stack: [u8; LZ_MAX_CODE], + suffix: [u8; LZ_MAX_CODE + 1], + prefix: [usize; LZ_MAX_CODE + 1], +} + +fn create_parse_state(code_size: u8, pixel_count: usize) -> ParseState { + let bits_per_pixel = code_size as usize; + let clear_code = 1 << bits_per_pixel; + + ParseState { + buf: [0; 256], // giflib only inits the first byte to 0 + bits_per_pixel: bits_per_pixel, + clear_code: clear_code, + eof_code: clear_code + 1, + running_code: clear_code + 2, + running_bits: bits_per_pixel + 1, + max_code_1: 1 << (bits_per_pixel + 1), + stack_ptr: 0, + last_code: NO_SUCH_CODE, + current_shift_state: 0, + current_shift_dword: 0, + prefix: [NO_SUCH_CODE; LZ_MAX_CODE + 1], + suffix: [0; LZ_MAX_CODE + 1], + stack: [0; LZ_MAX_CODE], + pixel_count: pixel_count, + } +} + +fn get_indices(state: &mut ParseState, indices: &mut[u8], index_count: usize, data: &mut R) + -> IoResult<()> { + state.pixel_count -= index_count; + if state.pixel_count > 0xffff0000 { + return Err(Error::new(InvalidInput, "Gif has too much pixel data")); + } + + try!(decompress_indices(state, indices, index_count, data)); + + if state.pixel_count == 0 { + // There might be some more data hanging around. Finish walking through + // the data section. + try!(skip_blocks(data)); + } + + Ok(()) +} + +fn decompress_indices(state: &mut ParseState, indices: &mut[u8], index_count: usize, data: &mut R) + -> IoResult<()> { + let mut i = 0; + let mut current_prefix; // This is uninit in dgif + let &mut ParseState { + mut stack_ptr, + eof_code, + clear_code, + mut last_code, + .. + } = state; + + if stack_ptr > LZ_MAX_CODE { return Err(malformed()); } + while stack_ptr != 0 && i < index_count { + stack_ptr -= 1; + indices[i] = state.stack[stack_ptr]; + i += 1; + } + + while i < index_count { + let current_code = try!(decompress_input(state, data)); + + let &mut ParseState { + ref mut prefix, + ref mut suffix, + ref mut stack, + .. + } = state; + + if current_code == eof_code { return Err(eof()); } + + if current_code == clear_code { + // Reset all the sweet codez we learned + for j in 0..LZ_MAX_CODE { + prefix[j] = NO_SUCH_CODE; + } + + state.running_code = state.eof_code + 1; + state.running_bits = state.bits_per_pixel + 1; + state.max_code_1 = 1 << state.running_bits; + state.last_code = NO_SUCH_CODE; + last_code = state.last_code; + } else { + // Regular code + if current_code < clear_code { + // single index code, direct mapping to a colour index + indices[i] = current_code as u8; + i += 1; + } else { + // MULTI-CODE MULTI-CODE ENGAGE -- DASH DASH DASH!!!! + + if prefix[current_code] == NO_SUCH_CODE { + current_prefix = last_code; + + let code = if current_code == state.running_code - 2 { + last_code + } else { + current_code + }; + + let prefix_char = get_prefix_char(&*prefix, code, clear_code); + stack[stack_ptr] = prefix_char; + suffix[state.running_code - 2] = prefix_char; + stack_ptr += 1; + } else { + current_prefix = current_code; + } + + while stack_ptr < LZ_MAX_CODE + && current_prefix > clear_code + && current_prefix <= LZ_MAX_CODE { + + stack[stack_ptr] = suffix[current_prefix]; + stack_ptr += 1; + current_prefix = prefix[current_prefix]; + } + + if stack_ptr >= LZ_MAX_CODE || current_prefix > LZ_MAX_CODE { + return Err(malformed()); + } + + stack[stack_ptr] = current_prefix as u8; + stack_ptr += 1; + + while stack_ptr != 0 && i < index_count { + stack_ptr -= 1; + indices[i] = stack[stack_ptr]; + i += 1; + } + + } + + if last_code != NO_SUCH_CODE && prefix[state.running_code - 2] == NO_SUCH_CODE { + prefix[state.running_code - 2] = last_code; + + let code = if current_code == state.running_code - 2 { + last_code + } else { + current_code + }; + + suffix[state.running_code - 2] = get_prefix_char(&*prefix, code, clear_code); + } + + last_code = current_code; + } + } + + state.last_code = last_code; + state.stack_ptr = stack_ptr; + + Ok(()) +} + +// Prefix is a virtual linked list or something. +fn get_prefix_char(prefix: &[usize], mut code: usize, clear_code: usize) -> u8 { + let mut i = 0; + + loop { + if code <= clear_code { break; } + i += 1; + if i > LZ_MAX_CODE { break; } + if code > LZ_MAX_CODE { return NO_SUCH_CODE as u8; } + code = prefix[code]; + } + + code as u8 +} + +fn decompress_input(state: &mut ParseState, src: &mut R) -> IoResult { + let code_masks: [usize; 13] = [ + 0x0000, 0x0001, 0x0003, 0x0007, + 0x000f, 0x001f, 0x003f, 0x007f, + 0x00ff, 0x01ff, 0x03ff, 0x07ff, + 0x0fff + ]; + + if state.running_bits > LZ_BITS { return Err(malformed()) } + + while state.current_shift_state < state.running_bits { + // Get the next byte, which is either in this block or the next one + let next_byte = if state.buf[0] == 0 { + + // This block is done, get the next one + let len = try!(read_block(src, &mut state.buf[1..])); + state.buf[0] = len as u8; + + // Reaching the end is not expected here + if len == 0 { return Err(eof()); } + + let next_byte = state.buf[1]; + state.buf[1] = 2; + state.buf[0] -= 1; + next_byte + } else { + // Still got bytes in this block + let next_byte = state.buf[state.buf[1] as usize]; + // this overflows when the line is 255 bytes long, and that's ok + state.buf[1] = state.buf[1].wrapping_add(1); + state.buf[0] -= 1; + next_byte + }; + + state.current_shift_dword |= (next_byte as usize) << state.current_shift_state; + state.current_shift_state += 8; + } + + let code = state.current_shift_dword & code_masks[state.running_bits]; + state.current_shift_dword >>= state.running_bits; + state.current_shift_state -= state.running_bits; + + if state.running_code < LZ_MAX_CODE + 2 { + state.running_code += 1; + if state.running_code > state.max_code_1 && state.running_bits < LZ_BITS { + state.max_code_1 <<= 1; + state.running_bits += 1; + } + } + + Ok(code) +} + + +// ~~~~~~~~~~~~ Streaming reading utils ~~~~~~~~~~~~~~~ + +fn read_byte(reader: &mut R) -> IoResult { + let mut buf = [0]; + let bytes_read = try!(reader.read(&mut buf)); + if bytes_read != 1 { return Err(eof()); } + Ok(buf[0]) +} + +fn read_to_full(reader: &mut R, buf: &mut [u8]) -> IoResult<()> { + let mut read = 0; + loop { + if read == buf.len() { return Ok(()) } + + let bytes = try!(reader.read(&mut buf[read..])); + + if bytes == 0 { return Err(eof()) } + + read += bytes; + } +} + +/// A few places where you need to skip through some variable length region +/// without evaluating the results. This does that. +fn skip_blocks(reader: &mut R) -> IoResult<()> { + let mut black_hole = [0; 255]; + loop { + let len = try!(read_block(reader, &mut black_hole)); + if len == 0 { return Ok(()) } + } +} + +/// There are several variable length encoded regions in a GIF, +/// that look like [len, ..len]. This is a convenience for grabbing the next +/// block. Returns `len`. +fn read_block(reader: &mut R, buf: &mut [u8]) -> IoResult { + debug_assert!(buf.len() >= 255); + let len = try!(read_byte(reader)) as usize; + if len == 0 { return Ok(0) } // read_to_full will probably freak out + try!(read_to_full(reader, &mut buf[..len])); + Ok(len) +} + +fn le_u16(first: u8, second: u8) -> u16 { + ((second as u16) << 8) | (first as u16) +} + +fn malformed() -> Error { + Error::new(InvalidInput, "Malformed GIF") +} + +fn eof() -> Error { + Error::new(InvalidInput, "Unexpected end of GIF") +} + diff --git a/example/example.rs b/example/example.rs index e033c0c..42f2bad 100644 --- a/example/example.rs +++ b/example/example.rs @@ -111,9 +111,9 @@ impl<'a> ExampleVideoRenderer<'a> { } } - fn present(&mut self, - image: Box, - player: &mut Player, + fn present(&mut self, + image: Box, + player: &mut Player, sdl_context: &sdl2::Sdl) { let video_track = player.video_track().unwrap(); @@ -141,6 +141,7 @@ impl<'a> ExampleVideoRenderer<'a> { stride as usize * height + 2 * ((stride / 2) as usize * (height / 2)) } PixelFormat::Rgb24 => stride as usize * height, + PixelFormat::Rgba32 => stride as usize * height, _ => { panic!("SDL can't natively render in {:?}!", output_video_format.media_pixel_format) @@ -180,6 +181,10 @@ impl SdlVideoFormat { PixelFormat::Indexed(_) | PixelFormat::Rgb24 => { (PixelFormat::Rgb24, PixelFormatEnum::RGB24) } + // FIXME(Gankro): this should be Rgba32 and PixelFormatEnum::RGBA8888 + // unfortuantely something is wrong with our translation to RGBA8888, + // so for now let's just forget the alpha channel. + PixelFormat::Rgba32 => (PixelFormat::Rgb24, PixelFormatEnum::RGB24), }; SdlVideoFormat { media_pixel_format: media_pixel_format, @@ -285,6 +290,7 @@ fn upload_image(video_track: &VideoTrack, vec![output_stride as usize, output_chroma_stride, output_chroma_stride]) } PixelFormat::Rgb24 => (vec![output_pixels], vec![output_stride as usize]), + PixelFormat::Rgba32 => (vec![output_pixels], vec![output_stride as usize]), _ => panic!("SDL can't natively render in {:?}!", output_video_format.media_pixel_format), }; @@ -351,7 +357,7 @@ fn main() { let target_time = duration_from_nanos(media_player.playback_start_wallclock_time) + target_time_since_playback_start; let cur_time = duration_from_nanos(clock_ticks::precise_time_ns()); - + if cur_time < target_time { thread::sleep(target_time - cur_time); } diff --git a/pixelformat.rs b/pixelformat.rs index 21b6dd9..32bad3f 100644 --- a/pixelformat.rs +++ b/pixelformat.rs @@ -40,6 +40,10 @@ impl<'a> Palette<'a> { #[derive(Copy, Clone, Debug)] pub struct Rgb24; +/// 32-bit RGBA. +#[derive(Copy, Clone, Debug)] +pub struct Rgba32; + #[derive(Copy, Clone)] pub struct YuvColor { pub y: f64, @@ -54,6 +58,14 @@ pub struct RgbColor { pub b: u8, } +#[derive(Copy, Clone, Debug)] +pub struct RgbaColor { + pub r: u8, + pub g: u8, + pub b: u8, + pub a: u8, +} + /// Converts between pixel formats on the CPU. pub trait ConvertPixelFormat { fn convert(&self, @@ -235,6 +247,85 @@ impl ConvertPixelFormat for Rgb24 { } } +impl ConvertPixelFormat for Rgba32 { + fn convert(&self, + _: &Rgb24, + output_pixels: &mut [&mut [u8]], + output_strides: &[usize], + input_pixels: &[&[u8]], + input_strides: &[usize], + width: usize, + height: usize) + -> Result<(),()> { + let (y_input_pixels, y_input_stride) = (input_pixels[0], input_strides[0]); + let (mut input_index, mut output_index) = (0, 0); + for _ in 0 .. height { + let input_row = &y_input_pixels[input_index .. input_index + width * 4]; + let mut output_row = &mut output_pixels[0][output_index .. output_index + width * 3]; + for i in 0 .. width { + output_row[i * 3 + 0] = input_row[i * 4 + 0]; + output_row[i * 3 + 1] = input_row[i * 4 + 1]; + output_row[i * 3 + 2] = input_row[i * 4 + 2]; + } + input_index += y_input_stride; + output_index += output_strides[0]; + } + Ok(()) + } +} + +impl ConvertPixelFormat for Rgb24 { + fn convert(&self, + _: &Rgba32, + output_pixels: &mut [&mut [u8]], + output_strides: &[usize], + input_pixels: &[&[u8]], + input_strides: &[usize], + width: usize, + height: usize) + -> Result<(),()> { + let (y_input_pixels, y_input_stride) = (input_pixels[0], input_strides[0]); + let (mut input_index, mut output_index) = (0, 0); + for _ in 0 .. height { + let input_row = &y_input_pixels[input_index .. input_index + width * 3]; + let mut output_row = &mut output_pixels[0][output_index .. output_index + width * 4]; + for i in 0 .. width { + output_row[i * 4 + 0] = input_row[i * 3 + 0]; + output_row[i * 4 + 1] = input_row[i * 3 + 1]; + output_row[i * 4 + 2] = input_row[i * 3 + 2]; + output_row[i * 4 + 3] = 0xFF; + } + input_index += y_input_stride; + output_index += output_strides[0]; + } + Ok(()) + } +} + +impl ConvertPixelFormat for Rgba32 { + fn convert(&self, + _: &Rgba32, + output_pixels: &mut [&mut [u8]], + output_strides: &[usize], + input_pixels: &[&[u8]], + input_strides: &[usize], + width: usize, + height: usize) + -> Result<(),()> { + let (y_input_pixels, y_input_stride) = (input_pixels[0], input_strides[0]); + let (mut input_index, mut output_index) = (0, 0); + for _ in 0 .. height { + let input_row = &y_input_pixels[input_index..input_index + width * 4]; + let mut output_row = &mut output_pixels[0][output_index..output_index + width * 4]; + bytes::copy_memory(input_row, output_row); + input_index += y_input_stride; + output_index += output_strides[0]; + } + Ok(()) + } +} + + /// Converts between color formats on the CPU. pub trait ConvertColorFormat { fn convert(&self) -> To; @@ -268,6 +359,7 @@ pub enum PixelFormat<'a> { NV12, Indexed(Palette<'a>), Rgb24, + Rgba32, } impl<'a> ConvertPixelFormat> for PixelFormat<'a> { @@ -326,6 +418,33 @@ impl<'a> ConvertPixelFormat> for PixelFormat<'a> { width, height) } + (PixelFormat::Rgb24, PixelFormat::Rgba32) => { + Rgb24.convert(&Rgba32, + output_pixels, + output_strides, + input_pixels, + input_strides, + width, + height) + } + (PixelFormat::Rgba32, PixelFormat::Rgb24) => { + Rgba32.convert(&Rgb24, + output_pixels, + output_strides, + input_pixels, + input_strides, + width, + height) + } + (PixelFormat::Rgba32, PixelFormat::Rgba32) => { + Rgba32.convert(&Rgba32, + output_pixels, + output_strides, + input_pixels, + input_strides, + width, + height) + } (_, _) => Err(()), } } @@ -337,7 +456,9 @@ impl<'a> PixelFormat<'a> { match *self { PixelFormat::I420 => 3, PixelFormat::NV12 => 2, - PixelFormat::Indexed(_) | PixelFormat::Rgb24 => 1, + PixelFormat::Indexed(_) + | PixelFormat::Rgb24 + | PixelFormat::Rgba32 => 1, } } } From 97113c8a9342b1d786a62f92dab213446e012c39 Mon Sep 17 00:00:00 2001 From: Alexis Beingessner Date: Sat, 13 Jun 2015 11:31:46 -0700 Subject: [PATCH 4/6] initial gif reftest support --- Cargo.toml | 4 + tests/gif/1bit-255-trans/input.gif | Bin 0 -> 337 bytes tests/gif/1bit-255-trans/output00000.tga | Bin 0 -> 786 bytes tests/gif/423tribalChallenge/input.gif | Bin 0 -> 63377 bytes tests/gif/423tribalChallenge/output00000.tga | Bin 0 -> 268818 bytes tests/gif/423tribalChallenge/output00001.tga | Bin 0 -> 268818 bytes tests/gif/423tribalChallenge/output00002.tga | Bin 0 -> 268818 bytes tests/gif/423tribalChallenge/output00003.tga | Bin 0 -> 268818 bytes tests/gif/423tribalChallenge/output00004.tga | Bin 0 -> 268818 bytes tests/gif/423tribalChallenge/output00005.tga | Bin 0 -> 268818 bytes tests/gif/423tribalChallenge/output00006.tga | Bin 0 -> 268818 bytes tests/gif/423tribalChallenge/output00007.tga | Bin 0 -> 268818 bytes tests/gif/423tribalChallenge/output00008.tga | Bin 0 -> 268818 bytes tests/gif/animation1a/input.gif | Bin 0 -> 167 bytes tests/gif/animation1a/output00000.tga | Bin 0 -> 6435 bytes tests/gif/animation1a/output00001.tga | Bin 0 -> 6418 bytes tests/gif/animation2a-finalframe/input.gif | Bin 0 -> 107 bytes .../animation2a-finalframe/output00000.tga | Bin 0 -> 6435 bytes tests/gif/animation2a/input.gif | Bin 0 -> 167 bytes tests/gif/animation2a/output00000.tga | Bin 0 -> 6435 bytes tests/gif/animation2a/output00001.tga | Bin 0 -> 6418 bytes tests/gif/back/input.gif | Bin 0 -> 216 bytes tests/gif/back/output00000.tga | Bin 0 -> 1856 bytes tests/gif/blue/input.gif | Bin 0 -> 43 bytes tests/gif/blue/output00000.tga | Bin 0 -> 21 bytes tests/gif/cat-jump/input.gif | Bin 0 -> 2081885 bytes tests/gif/cat-jump/output00000.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00001.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00002.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00003.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00004.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00005.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00006.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00007.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00008.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00009.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00010.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00011.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00012.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00013.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00014.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00015.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00016.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00017.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00018.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00019.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00020.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00021.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00022.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00023.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00024.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00025.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00026.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00027.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00028.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00029.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00030.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00031.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00032.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00033.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00034.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00035.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00036.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00037.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00038.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00039.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00040.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00041.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00042.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00043.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00044.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00045.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00046.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00047.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00048.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00049.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00050.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00051.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00052.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00053.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00054.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00055.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00056.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00057.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00058.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00059.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00060.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00061.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00062.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00063.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00064.tga | Bin 0 -> 613458 bytes tests/gif/cat-jump/output00065.tga | Bin 0 -> 613458 bytes tests/gif/comment/input.gif | Bin 0 -> 68 bytes tests/gif/comment/output00000.tga | Bin 0 -> 795 bytes tests/gif/in-colormap-trans/input.gif | Bin 0 -> 355 bytes tests/gif/in-colormap-trans/output00000.tga | Bin 0 -> 1042 bytes tests/gif/one-color-offset-ref/input.gif | Bin 0 -> 69 bytes .../gif/one-color-offset-ref/output00000.tga | Bin 0 -> 1042 bytes tests/gif/one-color-offset/input.gif | Bin 0 -> 49 bytes tests/gif/one-color-offset/output00000.tga | Bin 0 -> 786 bytes tests/gif/out-of-colormap-trans/input.gif | Bin 0 -> 355 bytes .../gif/out-of-colormap-trans/output00000.tga | Bin 0 -> 786 bytes tests/gif/red/input.gif | Bin 0 -> 43 bytes tests/gif/red/output00000.tga | Bin 0 -> 21 bytes .../gif/small-background-size-2-ref/input.gif | Bin 0 -> 807 bytes .../output00000.tga | Bin 0 -> 63858 bytes tests/gif/small-background-size-2/input.gif | Bin 0 -> 572 bytes .../small-background-size-2/output00000.tga | Bin 0 -> 63858 bytes tests/gif/small-background-size-ref/input.gif | Bin 0 -> 1076 bytes .../small-background-size-ref/output00000.tga | Bin 0 -> 63858 bytes tests/gif/small-background-size/input.gif | Bin 0 -> 991 bytes .../gif/small-background-size/output00000.tga | Bin 0 -> 63858 bytes tests/gif/text/input.gif | Bin 0 -> 229 bytes tests/gif/text/output00000.tga | Bin 0 -> 1856 bytes tests/gif/tiletest/input.gif | Bin 0 -> 156 bytes tests/gif/tiletest/output00000.tga | Bin 0 -> 114 bytes .../input.gif | Bin 0 -> 121 bytes .../output00000.tga | Bin 0 -> 40018 bytes tests/gif/transparent-animation/input.gif | Bin 0 -> 527 bytes .../gif/transparent-animation/output00000.tga | Bin 0 -> 40018 bytes .../gif/transparent-animation/output00001.tga | Bin 0 -> 40018 bytes .../gif/transparent-animation/output00002.tga | Bin 0 -> 40018 bytes tests/reftest.rs | 149 ++++++++++++++++++ 123 files changed, 153 insertions(+) create mode 100644 tests/gif/1bit-255-trans/input.gif create mode 100644 tests/gif/1bit-255-trans/output00000.tga create mode 100644 tests/gif/423tribalChallenge/input.gif create mode 100644 tests/gif/423tribalChallenge/output00000.tga create mode 100644 tests/gif/423tribalChallenge/output00001.tga create mode 100644 tests/gif/423tribalChallenge/output00002.tga create mode 100644 tests/gif/423tribalChallenge/output00003.tga create mode 100644 tests/gif/423tribalChallenge/output00004.tga create mode 100644 tests/gif/423tribalChallenge/output00005.tga create mode 100644 tests/gif/423tribalChallenge/output00006.tga create mode 100644 tests/gif/423tribalChallenge/output00007.tga create mode 100644 tests/gif/423tribalChallenge/output00008.tga create mode 100644 tests/gif/animation1a/input.gif create mode 100644 tests/gif/animation1a/output00000.tga create mode 100644 tests/gif/animation1a/output00001.tga create mode 100644 tests/gif/animation2a-finalframe/input.gif create mode 100644 tests/gif/animation2a-finalframe/output00000.tga create mode 100644 tests/gif/animation2a/input.gif create mode 100644 tests/gif/animation2a/output00000.tga create mode 100644 tests/gif/animation2a/output00001.tga create mode 100644 tests/gif/back/input.gif create mode 100644 tests/gif/back/output00000.tga create mode 100644 tests/gif/blue/input.gif create mode 100644 tests/gif/blue/output00000.tga create mode 100644 tests/gif/cat-jump/input.gif create mode 100644 tests/gif/cat-jump/output00000.tga create mode 100644 tests/gif/cat-jump/output00001.tga create mode 100644 tests/gif/cat-jump/output00002.tga create mode 100644 tests/gif/cat-jump/output00003.tga create mode 100644 tests/gif/cat-jump/output00004.tga create mode 100644 tests/gif/cat-jump/output00005.tga create mode 100644 tests/gif/cat-jump/output00006.tga create mode 100644 tests/gif/cat-jump/output00007.tga create mode 100644 tests/gif/cat-jump/output00008.tga create mode 100644 tests/gif/cat-jump/output00009.tga create mode 100644 tests/gif/cat-jump/output00010.tga create mode 100644 tests/gif/cat-jump/output00011.tga create mode 100644 tests/gif/cat-jump/output00012.tga create mode 100644 tests/gif/cat-jump/output00013.tga create mode 100644 tests/gif/cat-jump/output00014.tga create mode 100644 tests/gif/cat-jump/output00015.tga create mode 100644 tests/gif/cat-jump/output00016.tga create mode 100644 tests/gif/cat-jump/output00017.tga create mode 100644 tests/gif/cat-jump/output00018.tga create mode 100644 tests/gif/cat-jump/output00019.tga create mode 100644 tests/gif/cat-jump/output00020.tga create mode 100644 tests/gif/cat-jump/output00021.tga create mode 100644 tests/gif/cat-jump/output00022.tga create mode 100644 tests/gif/cat-jump/output00023.tga create mode 100644 tests/gif/cat-jump/output00024.tga create mode 100644 tests/gif/cat-jump/output00025.tga create mode 100644 tests/gif/cat-jump/output00026.tga create mode 100644 tests/gif/cat-jump/output00027.tga create mode 100644 tests/gif/cat-jump/output00028.tga create mode 100644 tests/gif/cat-jump/output00029.tga create mode 100644 tests/gif/cat-jump/output00030.tga create mode 100644 tests/gif/cat-jump/output00031.tga create mode 100644 tests/gif/cat-jump/output00032.tga create mode 100644 tests/gif/cat-jump/output00033.tga create mode 100644 tests/gif/cat-jump/output00034.tga create mode 100644 tests/gif/cat-jump/output00035.tga create mode 100644 tests/gif/cat-jump/output00036.tga create mode 100644 tests/gif/cat-jump/output00037.tga create mode 100644 tests/gif/cat-jump/output00038.tga create mode 100644 tests/gif/cat-jump/output00039.tga create mode 100644 tests/gif/cat-jump/output00040.tga create mode 100644 tests/gif/cat-jump/output00041.tga create mode 100644 tests/gif/cat-jump/output00042.tga create mode 100644 tests/gif/cat-jump/output00043.tga create mode 100644 tests/gif/cat-jump/output00044.tga create mode 100644 tests/gif/cat-jump/output00045.tga create mode 100644 tests/gif/cat-jump/output00046.tga create mode 100644 tests/gif/cat-jump/output00047.tga create mode 100644 tests/gif/cat-jump/output00048.tga create mode 100644 tests/gif/cat-jump/output00049.tga create mode 100644 tests/gif/cat-jump/output00050.tga create mode 100644 tests/gif/cat-jump/output00051.tga create mode 100644 tests/gif/cat-jump/output00052.tga create mode 100644 tests/gif/cat-jump/output00053.tga create mode 100644 tests/gif/cat-jump/output00054.tga create mode 100644 tests/gif/cat-jump/output00055.tga create mode 100644 tests/gif/cat-jump/output00056.tga create mode 100644 tests/gif/cat-jump/output00057.tga create mode 100644 tests/gif/cat-jump/output00058.tga create mode 100644 tests/gif/cat-jump/output00059.tga create mode 100644 tests/gif/cat-jump/output00060.tga create mode 100644 tests/gif/cat-jump/output00061.tga create mode 100644 tests/gif/cat-jump/output00062.tga create mode 100644 tests/gif/cat-jump/output00063.tga create mode 100644 tests/gif/cat-jump/output00064.tga create mode 100644 tests/gif/cat-jump/output00065.tga create mode 100644 tests/gif/comment/input.gif create mode 100644 tests/gif/comment/output00000.tga create mode 100644 tests/gif/in-colormap-trans/input.gif create mode 100644 tests/gif/in-colormap-trans/output00000.tga create mode 100644 tests/gif/one-color-offset-ref/input.gif create mode 100644 tests/gif/one-color-offset-ref/output00000.tga create mode 100644 tests/gif/one-color-offset/input.gif create mode 100644 tests/gif/one-color-offset/output00000.tga create mode 100644 tests/gif/out-of-colormap-trans/input.gif create mode 100644 tests/gif/out-of-colormap-trans/output00000.tga create mode 100644 tests/gif/red/input.gif create mode 100644 tests/gif/red/output00000.tga create mode 100644 tests/gif/small-background-size-2-ref/input.gif create mode 100644 tests/gif/small-background-size-2-ref/output00000.tga create mode 100644 tests/gif/small-background-size-2/input.gif create mode 100644 tests/gif/small-background-size-2/output00000.tga create mode 100644 tests/gif/small-background-size-ref/input.gif create mode 100644 tests/gif/small-background-size-ref/output00000.tga create mode 100644 tests/gif/small-background-size/input.gif create mode 100644 tests/gif/small-background-size/output00000.tga create mode 100644 tests/gif/text/input.gif create mode 100644 tests/gif/text/output00000.tga create mode 100644 tests/gif/tiletest/input.gif create mode 100644 tests/gif/tiletest/output00000.tga create mode 100644 tests/gif/transparent-animation-finalframe/input.gif create mode 100644 tests/gif/transparent-animation-finalframe/output00000.tga create mode 100644 tests/gif/transparent-animation/input.gif create mode 100644 tests/gif/transparent-animation/output00000.tga create mode 100644 tests/gif/transparent-animation/output00001.tga create mode 100644 tests/gif/transparent-animation/output00002.tga create mode 100644 tests/reftest.rs diff --git a/Cargo.toml b/Cargo.toml index a35368f..bce6c59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,3 +55,7 @@ default = [] ffmpeg = [] +[[test]] +name = "reftest" +path = "tests/reftest.rs" +harness = false diff --git a/tests/gif/1bit-255-trans/input.gif b/tests/gif/1bit-255-trans/input.gif new file mode 100644 index 0000000000000000000000000000000000000000..60273ba81dfc884b2fcdc7b033512a9e12169616 GIT binary patch literal 337 zcmZ?wbhEHb6krfw_`tyMpJD6n{|t&hSvWy#9R?r($uMyIXJBOE5KwSvsQ>@(@9*#W z4h;;9Oe_o-A{+t?a5W5IHE6mR7}&rf3}AEK$0N)^5doV66_JOV19TL~c91RxCI%jm z?F@`yk^B4M?m-cOn1ieaWIF>w4Fk{@5@2(f{s2wBzrP;lFQ9=45e=w8NJ3D9U?M<+ KI0S$}WeosML4B+M literal 0 HcmV?d00001 diff --git a/tests/gif/1bit-255-trans/output00000.tga b/tests/gif/1bit-255-trans/output00000.tga new file mode 100644 index 0000000000000000000000000000000000000000..fb470f6cf92f81f4494fc9362a1c44d592f7ef5a GIT binary patch literal 786 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