initial stab at gif load/save bitmap support
the lzw encode / decode algorithm seems to work well enough, but could be optimized and i'm worried there are still some lingering issues ...
This commit is contained in:
parent
8cb1d82b49
commit
0c1f50e63e
608
libretrogd/src/graphics/bitmap/gif.rs
Normal file
608
libretrogd/src/graphics/bitmap/gif.rs
Normal file
|
@ -0,0 +1,608 @@
|
|||
use std::fs::File;
|
||||
use std::io::{BufReader, BufWriter, Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::graphics::*;
|
||||
use crate::utils::lzw::{lzw_decode, lzw_encode, LzwError};
|
||||
|
||||
const BITS_FOR_256_COLORS: u32 = 7; // formula is `2 ^ (bits + 1) = num_colors`
|
||||
|
||||
fn bits_to_num_colors(bits: u32) -> u32 {
|
||||
1_u32.wrapping_shl(bits + 1)
|
||||
}
|
||||
|
||||
fn read_raw_sub_block_data<T: Read>(reader: &mut T) -> Result<Box<[u8]>, GifError> {
|
||||
let mut data = Vec::new();
|
||||
let mut count = reader.read_u8()?;
|
||||
while count > 0 {
|
||||
let mut sub_block = vec![0u8; count as usize];
|
||||
reader.read_exact(&mut sub_block)?;
|
||||
data.append(&mut sub_block);
|
||||
// read next sub block data size (or 0 if this is the end)
|
||||
count = reader.read_u8()?;
|
||||
}
|
||||
Ok(data.into_boxed_slice())
|
||||
}
|
||||
|
||||
fn write_raw_sub_block_data<T: Write>(data: &[u8], writer: &mut T) -> Result<(), GifError> {
|
||||
let mut bytes_left = data.len();
|
||||
let mut pos = 0;
|
||||
while bytes_left > 0 {
|
||||
let sub_block_length = if bytes_left >= 255 { 255 } else { bytes_left };
|
||||
writer.write_u8(sub_block_length as u8)?;
|
||||
let sub_block = &data[pos..sub_block_length];
|
||||
writer.write_all(sub_block)?;
|
||||
pos += sub_block_length;
|
||||
bytes_left -= sub_block_length;
|
||||
}
|
||||
// terminator (sub block of zero length)
|
||||
writer.write_u8(0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum GifError {
|
||||
#[error("Bad or unsupported GIF file: {0}")]
|
||||
BadFile(String),
|
||||
|
||||
#[error("GIF palette data error")]
|
||||
BadPalette(#[from] PaletteError),
|
||||
|
||||
#[error("Unknown extension block: {0}")]
|
||||
UnknownExtension(u8),
|
||||
|
||||
#[error("LZW encoding/decoding error")]
|
||||
LzwError(#[from] LzwError),
|
||||
|
||||
#[error("")]
|
||||
IOError(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
pub enum GifSettings {
|
||||
Default,
|
||||
TransparentColor(u8),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
struct GifHeader {
|
||||
signature: [u8; 3],
|
||||
version: [u8; 3],
|
||||
screen_width: u16,
|
||||
screen_height: u16,
|
||||
flags: u8,
|
||||
background_color: u8,
|
||||
aspect_ratio: u8,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl GifHeader {
|
||||
pub fn has_global_color_table(&self) -> bool {
|
||||
self.flags & 0b10000000 != 0
|
||||
}
|
||||
|
||||
pub fn set_global_color_table(&mut self, value: bool) {
|
||||
self.flags |= (value as u8).wrapping_shl(7);
|
||||
}
|
||||
|
||||
pub fn color_resolution_bits(&self) -> u8 {
|
||||
(self.flags & 0b01110000).wrapping_shr(4)
|
||||
}
|
||||
|
||||
pub fn set_color_resolution_bits(&mut self, value: u8) {
|
||||
self.flags |= (value & 0b111).wrapping_shl(4);
|
||||
}
|
||||
|
||||
pub fn is_color_table_entries_sorted(&self) -> bool {
|
||||
self.flags & 0b00001000 != 0
|
||||
}
|
||||
|
||||
pub fn set_color_table_entries_sorted(&mut self, value: bool) {
|
||||
self.flags |= (value as u8).wrapping_shl(3);
|
||||
}
|
||||
|
||||
pub fn global_color_table_bits(&self) -> u8 {
|
||||
self.flags & 0b00000111
|
||||
}
|
||||
|
||||
pub fn set_global_color_table_bits(&mut self, value: u8) {
|
||||
self.flags |= value & 0b111;
|
||||
}
|
||||
|
||||
pub fn read<T: Read>(reader: &mut T) -> Result<Self, GifError> {
|
||||
let mut signature = [0u8; 3];
|
||||
reader.read_exact(&mut signature)?;
|
||||
let mut version = [0u8; 3];
|
||||
reader.read_exact(&mut version)?;
|
||||
Ok(GifHeader {
|
||||
signature,
|
||||
version,
|
||||
screen_width: reader.read_u16::<LittleEndian>()?,
|
||||
screen_height: reader.read_u16::<LittleEndian>()?,
|
||||
flags: reader.read_u8()?,
|
||||
background_color: reader.read_u8()?,
|
||||
aspect_ratio: reader.read_u8()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write<T: Write>(&self, writer: &mut T) -> Result<(), GifError> {
|
||||
writer.write_all(&self.signature)?;
|
||||
writer.write_all(&self.version)?;
|
||||
writer.write_u16::<LittleEndian>(self.screen_width)?;
|
||||
writer.write_u16::<LittleEndian>(self.screen_height)?;
|
||||
writer.write_u8(self.flags)?;
|
||||
writer.write_u8(self.background_color)?;
|
||||
writer.write_u8(self.aspect_ratio)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const GIF_TRAILER: u8 = 0x3b;
|
||||
const EXTENSION_INTRODUCER: u8 = 0x21;
|
||||
const IMAGE_DESCRIPTOR_SEPARATOR: u8 = 0x2c;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
enum GifExtensionLabel {
|
||||
GraphicControl = 0xf9,
|
||||
PlainText = 0x01,
|
||||
Application = 0xff,
|
||||
Comment = 0xfe,
|
||||
}
|
||||
|
||||
impl GifExtensionLabel {
|
||||
pub fn from(value: u8) -> Result<Self, GifError> {
|
||||
use GifExtensionLabel::*;
|
||||
match value {
|
||||
0xf9 => Ok(GraphicControl),
|
||||
0x01 => Ok(PlainText),
|
||||
0xff => Ok(Application),
|
||||
0xfe => Ok(Comment),
|
||||
_ => Err(GifError::UnknownExtension(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
struct GraphicControlExtension {
|
||||
block_size: u8,
|
||||
flags: u8,
|
||||
delay: u16,
|
||||
transparent_color: u8,
|
||||
terminator: u8,
|
||||
}
|
||||
|
||||
impl GraphicControlExtension {
|
||||
pub fn read<T: Read>(reader: &mut T) -> Result<Self, GifError> {
|
||||
Ok(GraphicControlExtension {
|
||||
block_size: reader.read_u8()?,
|
||||
flags: reader.read_u8()?,
|
||||
delay: reader.read_u16::<LittleEndian>()?,
|
||||
transparent_color: reader.read_u8()?,
|
||||
terminator: reader.read_u8()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write<T: Write>(&self, writer: &mut T) -> Result<(), GifError> {
|
||||
writer.write_u8(self.block_size)?;
|
||||
writer.write_u8(self.flags)?;
|
||||
writer.write_u16::<LittleEndian>(self.delay)?;
|
||||
writer.write_u8(self.transparent_color)?;
|
||||
writer.write_u8(self.terminator)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PlainTextExtension {
|
||||
block_size: u8,
|
||||
text_x: u16,
|
||||
text_y: u16,
|
||||
text_width: u16,
|
||||
text_height: u16,
|
||||
cell_width: u8,
|
||||
cell_height: u8,
|
||||
foreground_color: u8,
|
||||
background_color: u8,
|
||||
data: Box<[u8]>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl PlainTextExtension {
|
||||
pub fn read<T: Read>(reader: &mut T) -> Result<Self, GifError> {
|
||||
Ok(PlainTextExtension {
|
||||
block_size: reader.read_u8()?,
|
||||
text_x: reader.read_u16::<LittleEndian>()?,
|
||||
text_y: reader.read_u16::<LittleEndian>()?,
|
||||
text_width: reader.read_u16::<LittleEndian>()?,
|
||||
text_height: reader.read_u16::<LittleEndian>()?,
|
||||
cell_width: reader.read_u8()?,
|
||||
cell_height: reader.read_u8()?,
|
||||
foreground_color: reader.read_u8()?,
|
||||
background_color: reader.read_u8()?,
|
||||
data: read_raw_sub_block_data(reader)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write<T: Write>(&self, writer: &mut T) -> Result<(), GifError> {
|
||||
writer.write_u8(self.block_size)?;
|
||||
writer.write_u16::<LittleEndian>(self.text_x)?;
|
||||
writer.write_u16::<LittleEndian>(self.text_y)?;
|
||||
writer.write_u16::<LittleEndian>(self.text_width)?;
|
||||
writer.write_u16::<LittleEndian>(self.text_height)?;
|
||||
writer.write_u8(self.cell_width)?;
|
||||
writer.write_u8(self.cell_height)?;
|
||||
writer.write_u8(self.foreground_color)?;
|
||||
writer.write_u8(self.background_color)?;
|
||||
write_raw_sub_block_data(&self.data, writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ApplicationExtension {
|
||||
block_size: u8,
|
||||
identifier: [u8; 8],
|
||||
authentication_code: [u8; 3],
|
||||
data: Box<[u8]>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl ApplicationExtension {
|
||||
pub fn read<T: Read>(reader: &mut T) -> Result<Self, GifError> {
|
||||
let block_size = reader.read_u8()?;
|
||||
let mut identifier = [0u8; 8];
|
||||
reader.read_exact(&mut identifier)?;
|
||||
let mut authentication_code = [0u8; 3];
|
||||
reader.read_exact(&mut authentication_code)?;
|
||||
Ok(ApplicationExtension {
|
||||
block_size,
|
||||
identifier,
|
||||
authentication_code,
|
||||
data: read_raw_sub_block_data(reader)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write<T: Write>(&self, writer: &mut T) -> Result<(), GifError> {
|
||||
writer.write_u8(self.block_size)?;
|
||||
writer.write_all(&self.identifier)?;
|
||||
writer.write_all(&self.authentication_code)?;
|
||||
write_raw_sub_block_data(&self.data, writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CommentExtension {
|
||||
data: Box<[u8]>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl CommentExtension {
|
||||
pub fn read<T: Read>(reader: &mut T) -> Result<Self, GifError> {
|
||||
Ok(CommentExtension {
|
||||
data: read_raw_sub_block_data(reader)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write<T: Write>(&self, writer: &mut T) -> Result<(), GifError> {
|
||||
write_raw_sub_block_data(&self.data, writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct LocalImageDescriptor {
|
||||
x: u16,
|
||||
y: u16,
|
||||
width: u16,
|
||||
height: u16,
|
||||
flags: u8,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl LocalImageDescriptor {
|
||||
pub fn has_local_color_table(&self) -> bool {
|
||||
self.flags & 0b10000000 != 0
|
||||
}
|
||||
|
||||
pub fn set_local_color_table(&mut self, value: bool) {
|
||||
self.flags |= (value as u8).wrapping_shl(7);
|
||||
}
|
||||
|
||||
pub fn is_color_table_entries_sorted(&self) -> bool {
|
||||
self.flags & 0b00100000 != 0
|
||||
}
|
||||
|
||||
pub fn set_color_table_entries_sorted(&mut self, value: bool) {
|
||||
self.flags |= (value as u8).wrapping_shl(5);
|
||||
}
|
||||
|
||||
pub fn local_color_table_bits(&self) -> u8 {
|
||||
self.flags & 0b00000111
|
||||
}
|
||||
|
||||
pub fn set_local_color_table_bits(&mut self, value: u8) {
|
||||
self.flags |= value & 0b111;
|
||||
}
|
||||
|
||||
pub fn read<T: Read>(reader: &mut T) -> Result<Self, GifError> {
|
||||
Ok(LocalImageDescriptor {
|
||||
x: reader.read_u16::<LittleEndian>()?,
|
||||
y: reader.read_u16::<LittleEndian>()?,
|
||||
width: reader.read_u16::<LittleEndian>()?,
|
||||
height: reader.read_u16::<LittleEndian>()?,
|
||||
flags: reader.read_u8()?
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write<T: Write>(&self, writer: &mut T) -> Result<(), GifError> {
|
||||
writer.write_u16::<LittleEndian>(self.x)?;
|
||||
writer.write_u16::<LittleEndian>(self.y)?;
|
||||
writer.write_u16::<LittleEndian>(self.width)?;
|
||||
writer.write_u16::<LittleEndian>(self.height)?;
|
||||
writer.write_u8(self.flags)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn load_image_section<T: ReadBytesExt>(
|
||||
reader: &mut T,
|
||||
gif_header: &GifHeader,
|
||||
_graphic_control: &Option<GraphicControlExtension>,
|
||||
) -> Result<(Bitmap, Option<Palette>), GifError> {
|
||||
let descriptor = LocalImageDescriptor::read(reader)?;
|
||||
|
||||
let palette: Option<Palette>;
|
||||
if descriptor.has_local_color_table() {
|
||||
let num_colors = bits_to_num_colors(descriptor.local_color_table_bits() as u32) as usize;
|
||||
palette = Some(Palette::load_num_colors_from_bytes(
|
||||
reader,
|
||||
PaletteFormat::Normal,
|
||||
num_colors,
|
||||
)?);
|
||||
} else {
|
||||
palette = None; // we expect to find a local color table later
|
||||
}
|
||||
|
||||
let lzw_minimum_code_size = reader.read_u8()?;
|
||||
let mut bitmap = Bitmap::new(gif_header.screen_width as u32, gif_header.screen_height as u32).unwrap();
|
||||
let mut writer = bitmap.pixels_mut();
|
||||
lzw_decode(reader, &mut writer, lzw_minimum_code_size as usize)?;
|
||||
|
||||
Ok((bitmap, palette))
|
||||
}
|
||||
|
||||
fn save_image_section<T: WriteBytesExt>(
|
||||
writer: &mut T,
|
||||
bitmap: &Bitmap,
|
||||
) -> Result<(), GifError> {
|
||||
writer.write_u8(IMAGE_DESCRIPTOR_SEPARATOR)?;
|
||||
let image_descriptor = LocalImageDescriptor {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: bitmap.width as u16,
|
||||
height: bitmap.height as u16,
|
||||
flags: 0, // again, we're not using local color tables, so no flags to set here
|
||||
};
|
||||
image_descriptor.write(writer)?;
|
||||
|
||||
let lzw_minimum_code_size = 8;
|
||||
writer.write_u8(lzw_minimum_code_size)?;
|
||||
|
||||
let mut reader = bitmap.pixels();
|
||||
lzw_encode(&mut reader, writer, lzw_minimum_code_size as usize)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Bitmap {
|
||||
pub fn load_gif_bytes<T: ReadBytesExt>(
|
||||
reader: &mut T,
|
||||
) -> Result<(Bitmap, Palette), GifError> {
|
||||
let header = GifHeader::read(reader)?;
|
||||
if header.signature != *b"GIF" || header.version != *b"89a" {
|
||||
return Err(GifError::BadFile(String::from("Expected GIF89a header signature")));
|
||||
}
|
||||
|
||||
// note that we might later overwrite this with a local color table (if this gif has one)
|
||||
let mut palette: Option<Palette>;
|
||||
if header.has_global_color_table() {
|
||||
let num_colors = bits_to_num_colors(header.global_color_table_bits() as u32) as usize;
|
||||
palette = Some(Palette::load_num_colors_from_bytes(
|
||||
reader,
|
||||
PaletteFormat::Normal,
|
||||
num_colors,
|
||||
)?);
|
||||
} else {
|
||||
palette = None; // we expect to find a local color table later
|
||||
}
|
||||
|
||||
let mut bitmap: Option<Bitmap> = None;
|
||||
let mut current_graphic_control: Option<GraphicControlExtension> = None;
|
||||
|
||||
loop {
|
||||
let current_byte = reader.read_u8()?;
|
||||
|
||||
// check for eof via the gif's "trailer" block ...
|
||||
if current_byte == 0x3b {
|
||||
break;
|
||||
}
|
||||
// if we have already successfully read a bitmap and palette from this file, we can
|
||||
// stop reading the rest. we only care about the first frame (if there are multiple)
|
||||
// and palette we find
|
||||
if bitmap.is_some() && palette.is_some() {
|
||||
break;
|
||||
}
|
||||
|
||||
match current_byte {
|
||||
GIF_TRAILER => break,
|
||||
IMAGE_DESCRIPTOR_SEPARATOR => {
|
||||
let (frame_bitmap, frame_palette) = load_image_section(reader, &header, ¤t_graphic_control)?;
|
||||
bitmap = Some(frame_bitmap);
|
||||
if frame_palette.is_some() {
|
||||
palette = frame_palette;
|
||||
}
|
||||
},
|
||||
EXTENSION_INTRODUCER => {
|
||||
let label = GifExtensionLabel::from(reader.read_u8()?)?;
|
||||
match label {
|
||||
GifExtensionLabel::GraphicControl => {
|
||||
current_graphic_control = Some(GraphicControlExtension::read(reader)?);
|
||||
},
|
||||
GifExtensionLabel::PlainText => {
|
||||
let _plain_text = PlainTextExtension::read(reader)?;
|
||||
// todo: do something with this maybe
|
||||
},
|
||||
GifExtensionLabel::Application => {
|
||||
let _application = ApplicationExtension::read(reader)?;
|
||||
// todo: do something with this maybe
|
||||
},
|
||||
GifExtensionLabel::Comment => {
|
||||
let _comment = CommentExtension::read(reader)?;
|
||||
// todo: do something with this maybe
|
||||
},
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err(GifError::BadFile(format!("Unexpected byte found {} not a file trailer, image separator or extension introducer", current_byte)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bitmap.is_none() {
|
||||
return Err(GifError::BadFile(String::from("No image data was found")));
|
||||
}
|
||||
if palette.is_none() {
|
||||
return Err(GifError::BadFile(String::from("No palette data was found")));
|
||||
}
|
||||
|
||||
Ok((bitmap.unwrap(), palette.unwrap()))
|
||||
}
|
||||
|
||||
pub fn load_gif_file(path: &Path) -> Result<(Bitmap, Palette), GifError> {
|
||||
let f = File::open(path)?;
|
||||
let mut reader = BufReader::new(f);
|
||||
Self::load_gif_bytes(&mut reader)
|
||||
}
|
||||
|
||||
pub fn to_gif_bytes<T: WriteBytesExt>(
|
||||
&self,
|
||||
writer: &mut T,
|
||||
palette: &Palette,
|
||||
settings: GifSettings,
|
||||
) -> Result<(), GifError> {
|
||||
|
||||
let mut header = GifHeader {
|
||||
signature: *b"GIF",
|
||||
version: *b"89a",
|
||||
screen_width: self.width as u16,
|
||||
screen_height: self.height as u16,
|
||||
flags: 0,
|
||||
background_color: 0,
|
||||
aspect_ratio: 0,
|
||||
};
|
||||
header.set_global_color_table(true);
|
||||
header.set_global_color_table_bits(BITS_FOR_256_COLORS as u8);
|
||||
header.set_color_resolution_bits(BITS_FOR_256_COLORS as u8);
|
||||
header.write(writer)?;
|
||||
|
||||
// write the provided palette out as the global color table. we will not be providing any
|
||||
// local color tables.
|
||||
palette.to_bytes(writer, PaletteFormat::Normal)?;
|
||||
|
||||
let transparent_color: u8;
|
||||
match settings {
|
||||
GifSettings::Default => {
|
||||
transparent_color = 0;
|
||||
},
|
||||
GifSettings::TransparentColor(color) => {
|
||||
transparent_color = color;
|
||||
}
|
||||
}
|
||||
|
||||
writer.write_u8(EXTENSION_INTRODUCER)?;
|
||||
writer.write_u8(GifExtensionLabel::GraphicControl as u8)?;
|
||||
let graphic_control = GraphicControlExtension {
|
||||
block_size: 4,
|
||||
flags: 0,
|
||||
delay: 0,
|
||||
transparent_color,
|
||||
terminator: 0,
|
||||
};
|
||||
graphic_control.write(writer)?;
|
||||
|
||||
save_image_section(writer, &self)?;
|
||||
|
||||
writer.write_u8(GIF_TRAILER)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_gif_file(
|
||||
&self,
|
||||
path: &Path,
|
||||
palette: &Palette,
|
||||
settings: GifSettings
|
||||
) -> Result<(), GifError> {
|
||||
let f = File::create(path)?;
|
||||
let mut writer = BufWriter::new(f);
|
||||
self.to_gif_bytes(&mut writer, palette, settings)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub static TEST_BMP_PIXELS_RAW: &[u8] = include_bytes!("../../../test-assets/test_bmp_pixels_raw.bin");
|
||||
pub static TEST_LARGE_BMP_PIXELS_RAW: &[u8] = include_bytes!("../../../test-assets/test_large_bmp_pixels_raw.bin");
|
||||
|
||||
#[test]
|
||||
fn load_and_save() -> Result<(), GifError> {
|
||||
let dp2_palette =
|
||||
Palette::load_from_file(Path::new("./test-assets/dp2.pal"), PaletteFormat::Normal)
|
||||
.unwrap();
|
||||
let tmp_dir = TempDir::new()?;
|
||||
|
||||
let (bmp, palette) = Bitmap::load_gif_file(Path::new("./test-assets/test.gif"))?;
|
||||
assert_eq!(16, bmp.width());
|
||||
assert_eq!(16, bmp.height());
|
||||
assert_eq!(bmp.pixels(), TEST_BMP_PIXELS_RAW);
|
||||
assert_eq!(palette, dp2_palette);
|
||||
|
||||
let save_path = tmp_dir.path().join("test_save.gif");
|
||||
bmp.to_gif_file(&save_path, &palette, GifSettings::Default)?;
|
||||
let (reloaded_bmp, reloaded_palette) = Bitmap::load_gif_file(&save_path)?;
|
||||
assert_eq!(16, reloaded_bmp.width());
|
||||
assert_eq!(16, reloaded_bmp.height());
|
||||
assert_eq!(reloaded_bmp.pixels(), TEST_BMP_PIXELS_RAW);
|
||||
assert_eq!(reloaded_palette, dp2_palette);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_and_save_larger_image() -> Result<(), GifError> {
|
||||
// this test is mostly useful to get a LZW decode and encode that includes at least one
|
||||
// "clear code" and accompanying table reset
|
||||
|
||||
let tmp_dir = TempDir::new()?;
|
||||
|
||||
let (bmp, palette) = Bitmap::load_gif_file(Path::new("./test-assets/test_image.gif"))?;
|
||||
assert_eq!(320, bmp.width());
|
||||
assert_eq!(200, bmp.height());
|
||||
assert_eq!(bmp.pixels(), TEST_LARGE_BMP_PIXELS_RAW);
|
||||
|
||||
let save_path = tmp_dir.path().join("test_save.gif");
|
||||
bmp.to_gif_file(&save_path, &palette, GifSettings::Default)?;
|
||||
let (reloaded_bmp, _) = Bitmap::load_gif_file(&save_path)?;
|
||||
assert_eq!(320, reloaded_bmp.width());
|
||||
assert_eq!(200, reloaded_bmp.height());
|
||||
assert_eq!(reloaded_bmp.pixels(), TEST_LARGE_BMP_PIXELS_RAW);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -8,11 +8,13 @@ use crate::graphics::*;
|
|||
use crate::math::*;
|
||||
|
||||
pub use self::blit::*;
|
||||
pub use self::gif::*;
|
||||
pub use self::iff::*;
|
||||
pub use self::pcx::*;
|
||||
pub use self::primitives::*;
|
||||
|
||||
pub mod blit;
|
||||
pub mod gif;
|
||||
pub mod iff;
|
||||
pub mod pcx;
|
||||
pub mod primitives;
|
||||
|
@ -33,6 +35,9 @@ pub enum BitmapError {
|
|||
|
||||
#[error("Bitmap PCX file error")]
|
||||
PcxError(#[from] pcx::PcxError),
|
||||
|
||||
#[error("Bitmap GIF file error")]
|
||||
GifError(#[from] gif::GifError),
|
||||
}
|
||||
|
||||
/// Container for 256 color 2D pixel/image data that can be rendered to the screen. Pixel data
|
||||
|
@ -110,6 +115,7 @@ impl Bitmap {
|
|||
let extension = extension.to_ascii_lowercase();
|
||||
match extension.to_str() {
|
||||
Some("pcx") => Ok(Self::load_pcx_file(path)?),
|
||||
Some("gif") => Ok(Self::load_gif_file(path)?),
|
||||
Some("iff") | Some("lbm") | Some("pbm") | Some("bbm") => {
|
||||
Ok(Self::load_iff_file(path)?)
|
||||
}
|
||||
|
|
693
libretrogd/src/utils/lzw.rs
Normal file
693
libretrogd/src/utils/lzw.rs
Normal file
|
@ -0,0 +1,693 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum LzwBytePackingError {
|
||||
#[error("Code size bits {0} is unsupported")]
|
||||
UnsupportedCodeSizeBits(usize),
|
||||
|
||||
#[error("Not enough bits available in the buffer to push new value in")]
|
||||
NotEnoughBits,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum LzwError {
|
||||
#[error("Code size bits {0} is unsupported")]
|
||||
UnsupportedCodeSizeBits(usize),
|
||||
|
||||
#[error("LZW byte packing/unpacking error")]
|
||||
BytePackingError(#[from] LzwBytePackingError),
|
||||
|
||||
#[error("Encoding/decoding error: {0}")]
|
||||
EncodingError(String),
|
||||
|
||||
#[error("LZW I/O error")]
|
||||
IOError(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
type LzwCode = u16;
|
||||
|
||||
const GIF_MAX_SUB_CHUNK_SIZE: usize = 255;
|
||||
const GIF_MAX_CODE_SIZE_BITS: usize = 8;
|
||||
const MIN_BITS: usize = 2;
|
||||
const MAX_BITS: usize = 12;
|
||||
|
||||
fn is_valid_min_code_size_bits(min_code_size_bits: usize) -> bool {
|
||||
min_code_size_bits >= MIN_BITS && min_code_size_bits <= MAX_BITS
|
||||
}
|
||||
|
||||
fn is_valid_gif_min_code_size_bits(min_code_size_bits: usize) -> bool {
|
||||
min_code_size_bits >= MIN_BITS && min_code_size_bits <= GIF_MAX_CODE_SIZE_BITS
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
struct LzwDataBuffer {
|
||||
prefix: Vec<u16>,
|
||||
value: u16,
|
||||
}
|
||||
|
||||
impl LzwDataBuffer {
|
||||
pub fn new(value: u16) -> Self {
|
||||
LzwDataBuffer {
|
||||
prefix: Vec::new(),
|
||||
value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_value(&mut self, value: u16) {
|
||||
self.prefix.push(self.value);
|
||||
self.value = value;
|
||||
}
|
||||
|
||||
pub fn first(&self) -> u16 {
|
||||
if let Some(value) = self.prefix.first() {
|
||||
*value
|
||||
} else {
|
||||
self.value
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_all<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), LzwError> {
|
||||
// todo: this feels icky to me?
|
||||
for value in self.prefix.iter() {
|
||||
writer.write_u8(*value as u8)?;
|
||||
}
|
||||
writer.write_u8(self.value as u8)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_bitmask_for_bits(bits: usize) -> u32 {
|
||||
let mut bitmask = 0;
|
||||
for i in 0..bits {
|
||||
bitmask |= 1u32.wrapping_shl(i as u32);
|
||||
}
|
||||
bitmask
|
||||
}
|
||||
|
||||
fn get_table_size_for_bits(bits: usize) -> usize {
|
||||
1usize.wrapping_shl(bits as u32)
|
||||
}
|
||||
|
||||
fn get_max_code_value_for_bits(bits: usize) -> LzwCode {
|
||||
(1 as LzwCode).wrapping_shl(bits as u32) - 1
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LzwBytePacker {
|
||||
buffer: u32,
|
||||
buffer_length: usize,
|
||||
current_bit_size: usize,
|
||||
bitmask: u32,
|
||||
initial_bit_size: usize,
|
||||
}
|
||||
|
||||
impl LzwBytePacker {
|
||||
pub fn new(initial_bit_size: usize) -> Result<Self, LzwBytePackingError> {
|
||||
if !is_valid_min_code_size_bits(initial_bit_size) {
|
||||
return Err(LzwBytePackingError::UnsupportedCodeSizeBits(initial_bit_size));
|
||||
}
|
||||
|
||||
Ok(LzwBytePacker {
|
||||
buffer: 0,
|
||||
buffer_length: 0,
|
||||
current_bit_size: initial_bit_size,
|
||||
bitmask: get_bitmask_for_bits(initial_bit_size),
|
||||
initial_bit_size,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn remaining_space(&self) -> usize {
|
||||
32 - self.buffer_length
|
||||
}
|
||||
|
||||
pub fn increase_bit_size(&mut self) -> Result<usize, LzwBytePackingError> {
|
||||
if self.current_bit_size >= MAX_BITS {
|
||||
return Err(LzwBytePackingError::UnsupportedCodeSizeBits(self.current_bit_size + 1));
|
||||
} else {
|
||||
self.current_bit_size += 1;
|
||||
self.bitmask = get_bitmask_for_bits(self.current_bit_size);
|
||||
Ok(self.current_bit_size)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_bit_size(&mut self) {
|
||||
self.current_bit_size = self.initial_bit_size;
|
||||
self.bitmask = get_bitmask_for_bits(self.current_bit_size);
|
||||
}
|
||||
|
||||
pub fn push_code(&mut self, code: LzwCode) -> Result<(), LzwBytePackingError> {
|
||||
if self.remaining_space() >= self.current_bit_size {
|
||||
let value = (code as u32 & self.bitmask).wrapping_shl(self.buffer_length as u32);
|
||||
self.buffer |= value;
|
||||
self.buffer_length += self.current_bit_size;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(LzwBytePackingError::NotEnoughBits)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_byte(&mut self) -> Option<u8> {
|
||||
if self.buffer_length >= 8 {
|
||||
let byte = (self.buffer & 0xff) as u8;
|
||||
self.buffer = self.buffer.wrapping_shr(8);
|
||||
self.buffer_length -= 8;
|
||||
Some(byte)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flush_byte(&mut self) -> Option<u8> {
|
||||
if self.buffer_length > 0 {
|
||||
let byte = (self.buffer & 0xff) as u8;
|
||||
self.buffer = self.buffer.wrapping_shr(8);
|
||||
if self.buffer_length >= 8 {
|
||||
self.buffer_length -= 8;
|
||||
} else {
|
||||
self.buffer_length = 0;
|
||||
}
|
||||
Some(byte)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LzwBytesWriter {
|
||||
packer: LzwBytePacker,
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl LzwBytesWriter {
|
||||
pub fn new(min_code_size_bits: usize) -> Result<Self, LzwError> {
|
||||
if !is_valid_min_code_size_bits(min_code_size_bits) {
|
||||
return Err(LzwError::UnsupportedCodeSizeBits(min_code_size_bits));
|
||||
}
|
||||
|
||||
let packer = LzwBytePacker::new(min_code_size_bits)?;
|
||||
|
||||
Ok(LzwBytesWriter {
|
||||
packer,
|
||||
buffer: Vec::with_capacity(GIF_MAX_SUB_CHUNK_SIZE),
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn increase_bit_size(&mut self) -> Result<usize, LzwError> {
|
||||
Ok(self.packer.increase_bit_size()?)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn reset_bit_size(&mut self) {
|
||||
self.packer.reset_bit_size()
|
||||
}
|
||||
|
||||
fn write_buffer<T: WriteBytesExt>(&mut self, writer: &mut T) -> Result<(), LzwError> {
|
||||
if !self.buffer.is_empty() {
|
||||
writer.write_u8(self.buffer.len() as u8)?;
|
||||
writer.write_all(&self.buffer)?;
|
||||
self.buffer.clear();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_code<T: WriteBytesExt>(
|
||||
&mut self,
|
||||
writer: &mut T,
|
||||
code: LzwCode
|
||||
) -> Result<(), LzwError> {
|
||||
self.packer.push_code(code)?;
|
||||
|
||||
while let Some(byte) = self.packer.take_byte() {
|
||||
self.buffer.push(byte);
|
||||
if self.buffer.len() == GIF_MAX_SUB_CHUNK_SIZE {
|
||||
self.write_buffer(writer)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn flush<T: WriteBytesExt>(&mut self, writer: &mut T) -> Result<(), LzwError> {
|
||||
while let Some(byte) = self.packer.flush_byte() {
|
||||
self.buffer.push(byte);
|
||||
if self.buffer.len() == GIF_MAX_SUB_CHUNK_SIZE {
|
||||
self.write_buffer(writer)?;
|
||||
}
|
||||
}
|
||||
self.write_buffer(writer)?;
|
||||
// block terminator for data sub-block sequence
|
||||
writer.write_u8(0)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LzwByteUnpacker {
|
||||
buffer: u32,
|
||||
buffer_length: usize,
|
||||
current_bit_size: usize,
|
||||
bitmask: u32,
|
||||
initial_bit_size: usize,
|
||||
}
|
||||
|
||||
impl LzwByteUnpacker {
|
||||
pub fn new(initial_bit_size: usize) -> Result<Self, LzwBytePackingError> {
|
||||
if !is_valid_min_code_size_bits(initial_bit_size) {
|
||||
return Err(LzwBytePackingError::UnsupportedCodeSizeBits(initial_bit_size));
|
||||
}
|
||||
|
||||
Ok(LzwByteUnpacker {
|
||||
buffer: 0,
|
||||
buffer_length: 0,
|
||||
current_bit_size: initial_bit_size,
|
||||
bitmask: get_bitmask_for_bits(initial_bit_size),
|
||||
initial_bit_size,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn remaining_space(&self) -> usize {
|
||||
32 - self.buffer_length
|
||||
}
|
||||
|
||||
pub fn increase_bit_size(&mut self) -> Result<usize, LzwBytePackingError> {
|
||||
if self.current_bit_size >= MAX_BITS {
|
||||
return Err(LzwBytePackingError::UnsupportedCodeSizeBits(self.current_bit_size + 1));
|
||||
} else {
|
||||
self.current_bit_size += 1;
|
||||
self.bitmask = get_bitmask_for_bits(self.current_bit_size);
|
||||
Ok(self.current_bit_size)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_bit_size(&mut self) {
|
||||
self.current_bit_size = self.initial_bit_size;
|
||||
self.bitmask = get_bitmask_for_bits(self.current_bit_size);
|
||||
}
|
||||
|
||||
pub fn push_byte(&mut self, byte: u8) -> Result<(), LzwBytePackingError> {
|
||||
if self.remaining_space() >= 8 {
|
||||
let value = (byte as u32).wrapping_shl(self.buffer_length as u32);
|
||||
self.buffer |= value;
|
||||
self.buffer_length += 8;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(LzwBytePackingError::NotEnoughBits)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_code(&mut self) -> Option<LzwCode> {
|
||||
if self.buffer_length >= self.current_bit_size {
|
||||
let code = (self.buffer & self.bitmask) as LzwCode;
|
||||
self.buffer = self.buffer.wrapping_shr(self.current_bit_size as u32);
|
||||
self.buffer_length -= self.current_bit_size;
|
||||
Some(code)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LzwBytesReader {
|
||||
unpacker: LzwByteUnpacker,
|
||||
sub_chunk_remaining_bytes: u8,
|
||||
reached_end: bool,
|
||||
}
|
||||
|
||||
impl LzwBytesReader {
|
||||
pub fn new(min_code_size_bits: usize) -> Result<Self, LzwError> {
|
||||
if !is_valid_min_code_size_bits(min_code_size_bits) {
|
||||
return Err(LzwError::UnsupportedCodeSizeBits(min_code_size_bits));
|
||||
}
|
||||
|
||||
let unpacker = LzwByteUnpacker::new(min_code_size_bits)?;
|
||||
|
||||
Ok(LzwBytesReader {
|
||||
unpacker,
|
||||
sub_chunk_remaining_bytes: 0,
|
||||
reached_end: false,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn increase_bit_size(&mut self) -> Result<usize, LzwError> {
|
||||
Ok(self.unpacker.increase_bit_size()?)
|
||||
}
|
||||
|
||||
pub fn reset_bit_size(&mut self) {
|
||||
self.unpacker.reset_bit_size()
|
||||
}
|
||||
|
||||
fn read_byte<T: ReadBytesExt>(&mut self, reader: &mut T) -> Result<Option<u8>, LzwError> {
|
||||
if self.reached_end {
|
||||
return Ok(None);
|
||||
}
|
||||
if self.sub_chunk_remaining_bytes == 0 {
|
||||
self.sub_chunk_remaining_bytes = reader.read_u8()?;
|
||||
if self.sub_chunk_remaining_bytes == 0 {
|
||||
self.reached_end = true;
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
self.sub_chunk_remaining_bytes -= 1;
|
||||
Ok(Some(reader.read_u8()?))
|
||||
}
|
||||
|
||||
pub fn read_code<T: ReadBytesExt> (
|
||||
&mut self,
|
||||
reader: &mut T,
|
||||
) -> Result<Option<LzwCode>, LzwError> {
|
||||
loop {
|
||||
if let Some(code) = self.unpacker.take_code() {
|
||||
return Ok(Some(code))
|
||||
} else {
|
||||
match self.read_byte(reader) {
|
||||
Ok(Some(byte)) => self.unpacker.push_byte(byte)?,
|
||||
Ok(None) => return Ok(None),
|
||||
Err(LzwError::IOError(error)) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
|
||||
return Ok(None)
|
||||
},
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lzw_encode<S, D>(
|
||||
src: &mut S,
|
||||
dest: &mut D,
|
||||
min_code_size: usize
|
||||
) -> Result<(), LzwError>
|
||||
where
|
||||
S: ReadBytesExt,
|
||||
D: WriteBytesExt
|
||||
{
|
||||
if !is_valid_gif_min_code_size_bits(min_code_size) {
|
||||
return Err(LzwError::UnsupportedCodeSizeBits(min_code_size));
|
||||
}
|
||||
|
||||
// initialize the table, special codes, bit size info, etc
|
||||
|
||||
let initial_table_size = get_table_size_for_bits(min_code_size);
|
||||
let clear_code = initial_table_size as LzwCode;
|
||||
let end_of_info_code = initial_table_size as LzwCode + 1;
|
||||
let mut table = HashMap::<LzwDataBuffer, LzwCode>::with_capacity(initial_table_size + 2);
|
||||
for i in 0..initial_table_size {
|
||||
table.insert(LzwDataBuffer::new(i as u16), i as LzwCode);
|
||||
}
|
||||
table.insert(LzwDataBuffer::new(clear_code), clear_code);
|
||||
table.insert(LzwDataBuffer::new(end_of_info_code), end_of_info_code);
|
||||
let mut current_bit_size = min_code_size + 1;
|
||||
let mut max_code_value_for_bit_size = get_max_code_value_for_bits(current_bit_size);
|
||||
let mut next_code = initial_table_size as LzwCode + 2;
|
||||
|
||||
// begin the output code stream
|
||||
let mut writer = LzwBytesWriter::new(current_bit_size)?;
|
||||
writer.write_code(dest, clear_code)?;
|
||||
|
||||
// read first byte to start things off before the main loop.
|
||||
// if we eof here for some reason, just end the lzw stream like "normal" ... even though this
|
||||
// isn't really a normal situation
|
||||
let byte = match src.read_u8() {
|
||||
Ok(byte) => byte,
|
||||
Err(ref error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
|
||||
writer.write_code(dest, end_of_info_code)?;
|
||||
writer.flush(dest)?;
|
||||
return Ok(());
|
||||
},
|
||||
Err(error) => return Err(LzwError::IOError(error))
|
||||
};
|
||||
|
||||
let mut buffer = LzwDataBuffer::new(byte as u16);
|
||||
|
||||
loop {
|
||||
// grab the next byte
|
||||
let byte = match src.read_u8() {
|
||||
Ok(byte) => byte,
|
||||
Err(ref error) if error.kind() == std::io::ErrorKind::UnexpectedEof => break,
|
||||
Err(error) => return Err(LzwError::IOError(error))
|
||||
};
|
||||
|
||||
// check if the table currently contains a string composed of the current buffer plus
|
||||
// the byte we just read (
|
||||
let mut buffer_plus_byte = buffer.clone();
|
||||
buffer_plus_byte.push_value(byte as u16);
|
||||
|
||||
if table.contains_key(&buffer_plus_byte) {
|
||||
// we have a match, so lets just keep collecting bytes in our buffer ...
|
||||
buffer.push_value(byte as u16);
|
||||
} else {
|
||||
let mut was_bit_size_increased = false;
|
||||
let mut is_reset_needed = false;
|
||||
|
||||
// no match in the table, so we need to create a new code in the table for this
|
||||
// string of bytes (buffer + byte) and also emit the code for _just_ the buffer string
|
||||
let new_code = next_code;
|
||||
next_code += 1;
|
||||
if new_code > max_code_value_for_bit_size {
|
||||
current_bit_size += 1;
|
||||
if current_bit_size >= MAX_BITS {
|
||||
is_reset_needed = true;
|
||||
}
|
||||
max_code_value_for_bit_size = get_max_code_value_for_bits(current_bit_size);
|
||||
was_bit_size_increased = true;
|
||||
}
|
||||
|
||||
table.insert(buffer_plus_byte, new_code);
|
||||
|
||||
if let Some(code) = table.get(&buffer) {
|
||||
writer.write_code(dest, *code)?;
|
||||
} else {
|
||||
return Err(LzwError::EncodingError(format!("Expected to find code in table for buffer {:?} but none was found", buffer)));
|
||||
}
|
||||
|
||||
if was_bit_size_increased {
|
||||
writer.increase_bit_size()?;
|
||||
}
|
||||
|
||||
if is_reset_needed {
|
||||
// we reached the maximum code bit size, time to re-initialize the code table
|
||||
table = HashMap::with_capacity(initial_table_size + 2);
|
||||
for i in 0..initial_table_size {
|
||||
table.insert(LzwDataBuffer::new(i as u16), i as LzwCode);
|
||||
}
|
||||
table.insert(LzwDataBuffer::new(clear_code), clear_code);
|
||||
table.insert(LzwDataBuffer::new(end_of_info_code), end_of_info_code);
|
||||
current_bit_size = min_code_size + 1;
|
||||
max_code_value_for_bit_size = get_max_code_value_for_bits(current_bit_size);
|
||||
next_code = initial_table_size as LzwCode + 2;
|
||||
|
||||
// reset the output code stream
|
||||
writer.write_code(dest, clear_code)?;
|
||||
writer.reset_bit_size();
|
||||
}
|
||||
|
||||
buffer = LzwDataBuffer::new(byte as u16);
|
||||
}
|
||||
}
|
||||
|
||||
// flush the remaining buffer and finish up the output code stream
|
||||
|
||||
if let Some(code) = table.get(&buffer) {
|
||||
writer.write_code(dest, *code)?;
|
||||
} else {
|
||||
return Err(LzwError::EncodingError(format!("No matching code for buffer {:?} at end of input stream", buffer)));
|
||||
}
|
||||
|
||||
writer.write_code(dest, end_of_info_code)?;
|
||||
writer.flush(dest)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn lzw_decode<S, D>(
|
||||
src: &mut S,
|
||||
dest: &mut D,
|
||||
min_code_size: usize
|
||||
) -> Result<(), LzwError>
|
||||
where
|
||||
S: ReadBytesExt,
|
||||
D: WriteBytesExt
|
||||
{
|
||||
if !is_valid_gif_min_code_size_bits(min_code_size) {
|
||||
return Err(LzwError::UnsupportedCodeSizeBits(min_code_size));
|
||||
}
|
||||
|
||||
// initialize some basic properties for decoding and the table here ... we initialize the
|
||||
// actual table and the rest of the decoding properties we need a bit further on below
|
||||
|
||||
let mut current_bit_size = min_code_size + 1;
|
||||
let initial_table_size = get_table_size_for_bits(min_code_size);
|
||||
let clear_code = initial_table_size as LzwCode;
|
||||
let end_of_info_code = initial_table_size as LzwCode + 1;
|
||||
|
||||
let mut reader = LzwBytesReader::new(current_bit_size)?;
|
||||
|
||||
// read the first code from the input code stream.
|
||||
// we also return immediately without writing anything to the destination byte stream if there
|
||||
// are no codes to read (kind of a weird situation, but no real reason to error ...?)
|
||||
let mut code = match reader.read_code(src)? {
|
||||
Some(code) => code,
|
||||
None => return Ok(())
|
||||
};
|
||||
|
||||
// the first code in the stream SHOULD be a clear code ... which we can just ignore because
|
||||
// our table is freshly reset right now anyway. but we should flag this as an error if for some
|
||||
// reason we didn't just read a clear code!
|
||||
if code != clear_code {
|
||||
return Err(LzwError::EncodingError(String::from("Unexpected first code value (not a clear code)")));
|
||||
}
|
||||
|
||||
'outer: loop {
|
||||
// initialize the table and some extra bits of info here so that whenever we read in a
|
||||
// clear code from the input stream, we can just loop back here to handle it
|
||||
|
||||
let mut table = HashMap::<LzwCode, LzwDataBuffer>::with_capacity(initial_table_size + 2);
|
||||
for i in 0..initial_table_size {
|
||||
table.insert(i as LzwCode, LzwDataBuffer::new(i as u16));
|
||||
}
|
||||
table.insert(clear_code, LzwDataBuffer::new(clear_code));
|
||||
table.insert(end_of_info_code, LzwDataBuffer::new(end_of_info_code));
|
||||
let mut max_code_value_for_bit_size = get_max_code_value_for_bits(current_bit_size);
|
||||
let mut next_code = initial_table_size as LzwCode + 2;
|
||||
|
||||
// read the next code which should actually be the first "interesting" value of the code stream
|
||||
code = match reader.read_code(src)? {
|
||||
Some(code) if code == end_of_info_code => return Ok(()),
|
||||
Some(code) => code,
|
||||
None => return Err(LzwError::EncodingError(String::from("Unexpected end of code stream"))),
|
||||
};
|
||||
|
||||
// ok, now we're able to get started!
|
||||
|
||||
// simply write out the table string associated with the first code
|
||||
if let Some(string) = table.get(&code) {
|
||||
string.write_all(dest)?;
|
||||
} else {
|
||||
return Err(LzwError::EncodingError(format!("No table entry for code {}", code)));
|
||||
}
|
||||
|
||||
let mut prev_code = code;
|
||||
|
||||
'inner: loop {
|
||||
// grab the next code
|
||||
code = match reader.read_code(src)? {
|
||||
Some(code) if code == end_of_info_code => break 'outer,
|
||||
Some(code) if code == clear_code => {
|
||||
// reset the bit size and reader and then loop back to the outer loop which
|
||||
// will handle actually resetting the code table
|
||||
current_bit_size = min_code_size + 1;
|
||||
reader.reset_bit_size();
|
||||
break 'inner;
|
||||
},
|
||||
Some(code) => code,
|
||||
None => return Err(LzwError::EncodingError(String::from("Unexpected end of code stream"))),
|
||||
};
|
||||
|
||||
// note: prev_code should always be present since we looked it up in the table during a
|
||||
// previous loop iteration ...
|
||||
let prev_code_string = match table.get(&prev_code) {
|
||||
Some(prev_code_string) => prev_code_string,
|
||||
None => {
|
||||
return Err(LzwError::EncodingError(format!("Previous code {} not found in table", prev_code)));
|
||||
}
|
||||
};
|
||||
|
||||
let new_code = next_code;
|
||||
next_code += 1;
|
||||
|
||||
if let Some(string) = table.get(&code) {
|
||||
// write out the matching table string for the code just read
|
||||
string.write_all(dest)?;
|
||||
|
||||
// update the table accordingly
|
||||
let k = string.first();
|
||||
let mut new_string = prev_code_string.clone();
|
||||
new_string.push_value(k);
|
||||
table.insert(new_code, new_string);
|
||||
} else {
|
||||
// code is not yet present in the table.
|
||||
// add prev_code string + the code we just read to the table and also write it out
|
||||
let k = prev_code_string.first();
|
||||
let mut new_string = prev_code_string.clone();
|
||||
new_string.push_value(k);
|
||||
new_string.write_all(dest)?;
|
||||
table.insert(new_code, new_string);
|
||||
}
|
||||
|
||||
if new_code == max_code_value_for_bit_size {
|
||||
current_bit_size += 1;
|
||||
max_code_value_for_bit_size = get_max_code_value_for_bits(current_bit_size);
|
||||
reader.increase_bit_size()?;
|
||||
}
|
||||
|
||||
prev_code = code;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Cursor;
|
||||
|
||||
use super::*;
|
||||
|
||||
struct LzwTestData<'a> {
|
||||
min_code_size: usize,
|
||||
packed: &'a [u8],
|
||||
unpacked: &'a [u8],
|
||||
}
|
||||
|
||||
static LZW_TEST_DATA: &[LzwTestData] = &[
|
||||
LzwTestData {
|
||||
min_code_size: 2,
|
||||
packed: &[0x16, 0x8c, 0x2d, 0x99, 0x87, 0x2a, 0x1c, 0xdc, 0x33, 0xa0, 0x02, 0x75, 0xec, 0x95, 0xfa, 0xa8, 0xde, 0x60, 0x8c, 0x04, 0x91, 0x4c, 0x01, 0x00],
|
||||
unpacked: &[1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
},
|
||||
LzwTestData {
|
||||
min_code_size: 4,
|
||||
packed: &[0x21, 0x70, 0x49, 0x79, 0x6a, 0x9d, 0xcb, 0x39, 0x7b, 0xa6, 0xd6, 0x96, 0xa4, 0x3d, 0x0f, 0xd8, 0x8d, 0x64, 0xb9, 0x1d, 0x28, 0xa9, 0x2d, 0x15, 0xfa, 0xc2, 0xf1, 0x37, 0x71, 0x33, 0xc5, 0x61, 0x4b, 0x04, 0x00],
|
||||
unpacked: &[11, 11, 11, 11, 11, 7, 7, 7, 7, 7, 11, 11, 11, 11, 14, 14, 7, 7, 7, 7, 11, 11, 11, 14, 14, 14, 14, 7, 7, 7, 11, 11, 14, 14, 15, 15, 14, 14, 7, 7, 11, 14, 14, 15, 15, 15, 15, 14, 14, 7, 7, 14, 14, 15, 15, 15, 15, 14, 14, 11, 7, 7, 14, 14, 15, 15, 14, 14, 11, 11, 7, 7, 7, 14, 14, 14, 14, 11, 11, 11, 7, 7, 7, 7, 14, 14, 11, 11, 11, 11, 7, 7, 7, 7, 7, 11, 11, 11, 11, 11],
|
||||
},
|
||||
LzwTestData {
|
||||
min_code_size: 8,
|
||||
packed: &[0x0b, 0x00, 0x51, 0xfc, 0x1b, 0x28, 0x70, 0xa0, 0xc1, 0x83, 0x01, 0x01, 0x00],
|
||||
unpacked: &[0x28, 0xff, 0xff, 0xff, 0x28, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
|
||||
}
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn lzw_compresses() -> Result<(), LzwError> {
|
||||
for LzwTestData { packed, unpacked, min_code_size } in LZW_TEST_DATA {
|
||||
let mut src = Cursor::new(*unpacked);
|
||||
let mut dest = vec![0u8; 0];
|
||||
lzw_encode(&mut src, &mut dest, *min_code_size)?;
|
||||
assert_eq!(dest, *packed);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lzw_decompresses() -> Result<(), LzwError> {
|
||||
for LzwTestData { packed, unpacked, min_code_size } in LZW_TEST_DATA {
|
||||
let mut src = Cursor::new(*packed);
|
||||
let mut dest = vec![0u8; 0];
|
||||
lzw_decode(&mut src, &mut dest, *min_code_size)?;
|
||||
assert_eq!(dest, *unpacked);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ use rand::Rng;
|
|||
|
||||
pub mod bytes;
|
||||
pub mod io;
|
||||
pub mod lzw;
|
||||
pub mod packbits;
|
||||
|
||||
pub fn rnd_value<N: SampleUniform + PartialOrd>(low: N, high: N) -> N {
|
||||
|
|
BIN
libretrogd/test-assets/test.gif
Normal file
BIN
libretrogd/test-assets/test.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 919 B |
BIN
libretrogd/test-assets/test_image.gif
Normal file
BIN
libretrogd/test-assets/test_image.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
12
libretrogd/test-assets/test_large_bmp_pixels_raw.bin
Normal file
12
libretrogd/test-assets/test_large_bmp_pixels_raw.bin
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue