From b7ff62489878b470387014eb78769a45e7c46919 Mon Sep 17 00:00:00 2001 From: G2-Games Date: Sun, 5 May 2024 04:48:03 -0500 Subject: [PATCH] Implemented CZ2 reading --- cz/Cargo.toml | 1 + cz/src/binio.rs | 65 +++++++++++++++++ cz/src/common.rs | 14 ++-- cz/src/compression.rs | 80 +++++++++++++++++++-- cz/src/formats/cz0.rs | 2 +- cz/src/formats/cz1.rs | 2 +- cz/src/formats/cz2.rs | 158 ++++++++++++++++++++++++++++++++++++++++++ cz/src/formats/cz3.rs | 7 +- cz/src/lib.rs | 6 +- utils/src/main.rs | 10 +-- 10 files changed, 325 insertions(+), 20 deletions(-) create mode 100644 cz/src/binio.rs create mode 100644 cz/src/formats/cz2.rs diff --git a/cz/Cargo.toml b/cz/Cargo.toml index f9d4df2..0b239ef 100644 --- a/cz/Cargo.toml +++ b/cz/Cargo.toml @@ -11,3 +11,4 @@ A encoder/decoder for CZ# image files used in image = "0.25" byteorder = "1.5.0" thiserror = "1.0.59" +bitstream-io = "2.2.0" diff --git a/cz/src/binio.rs b/cz/src/binio.rs new file mode 100644 index 0000000..10db2f3 --- /dev/null +++ b/cz/src/binio.rs @@ -0,0 +1,65 @@ +pub struct BitIO { + data: Vec, + byte_offset: usize, + bit_offset: usize, +} + +impl BitIO { + pub fn new(data: Vec) -> Self { + Self { + data, + byte_offset: 0, + bit_offset: 0, + } + } + + pub fn byte_offset(&self) -> usize { + self.byte_offset + } + + pub fn bit_offset(&self) -> usize { + self.byte_offset + } + + pub fn to_vec(self) -> Vec { + self.data + } + + pub fn read_bit(&mut self, bit_len: usize) -> u64 { + //print!("{}: ", bit_len); + if bit_len > 8 * 8 { + panic!() + } + + if bit_len % 8 == 0 && self.bit_offset == 0 { + return self.read(bit_len / 8) + } + + let mut result = 0; + for i in 0..bit_len { + let bit_value = ((self.data[self.byte_offset] as usize >> self.bit_offset as usize) & 1) as u64; + self.bit_offset += 1; + + if self.bit_offset == 8 { + self.byte_offset += 1; + self.bit_offset = 0; + } + + result |= bit_value << i; + } + + return result + } + + pub fn read(&mut self, byte_len: usize) -> u64 { + if byte_len > 8 { + panic!() + } + + let mut padded_slice = [0u8; 8]; + padded_slice.copy_from_slice(&self.data[self.byte_offset..self.byte_offset + byte_len]); + self.byte_offset += byte_len; + + u64::from_le_bytes(padded_slice) + } +} diff --git a/cz/src/common.rs b/cz/src/common.rs index 7ab08b2..3fadc46 100644 --- a/cz/src/common.rs +++ b/cz/src/common.rs @@ -11,14 +11,10 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum CzError { #[error("Version in header does not match expected version")] - VersionMismatch, + VersionMismatch(u8, u8), - #[error( - "Format of supplied file is incorrect; expected {} bytes, got {}", - expected, - got - )] - InvalidFormat { expected: usize, got: usize }, + #[error("Format of supplied file is not a CZ#")] + InvalidFormat, #[error("Failed to read/write input/output")] IoError(#[from] io::Error), @@ -81,6 +77,10 @@ impl CzHeader for CommonHeader { let mut magic = [0u8; 4]; bytes.read_exact(&mut magic)?; + if magic[0..2] != [b'C', b'Z'] { + return Err(CzError::InvalidFormat) + } + Ok(Self { version: magic[2] - b'0', length: bytes.read_u32::()?, diff --git a/cz/src/compression.rs b/cz/src/compression.rs index c5479b1..12ba576 100644 --- a/cz/src/compression.rs +++ b/cz/src/compression.rs @@ -1,6 +1,9 @@ -use std::io::{Read, Seek}; +use std::{collections::BTreeMap, io::{Cursor, Read, Seek, Write}}; use byteorder::{LittleEndian, ReadBytesExt}; +use bitstream_io::{read::BitReader, BitRead}; + use crate::common::CzError; +use crate::binio::BitIO; #[derive(Debug, Clone, Copy)] pub struct ChunkInfo { @@ -29,7 +32,7 @@ pub fn parse_chunk_info(bytes: &mut T) -> Result< // Loop over the compressed bytes for _ in 0..parts_count { - let compressed_size = bytes.read_u32::()? * 2; + let compressed_size = bytes.read_u32::()?; total_size += compressed_size; let raw_size = bytes.read_u32::()?; @@ -50,7 +53,7 @@ pub fn parse_chunk_info(bytes: &mut T) -> Result< }) } -/// Decompress an LZW compressed stream, like CZ1 +/// Decompress an LZW compressed stream like CZ1 pub fn decompress( input: &mut T, chunk_info: &CompressionInfo, @@ -58,7 +61,7 @@ pub fn decompress( let mut m_dst = 0; let mut bitmap = vec![0; chunk_info.total_size_raw]; for chunk in &chunk_info.chunks { - let mut part = vec![0u8; chunk.size_compressed]; + let mut part = vec![0u8; chunk.size_compressed * 2]; input.read_exact(&mut part)?; for j in (0..part.len()).step_by(2) { @@ -76,6 +79,75 @@ pub fn decompress( Ok(bitmap) } +/// Decompress an LZW compressed stream like CZ2 +pub fn decompress_2( + input: &mut T, + chunk_info: &CompressionInfo, +) -> Result, CzError> { + let mut output_buf: Vec = vec![]; + + for block in &chunk_info.chunks { + let mut buffer = vec![0u8; block.size_compressed]; + input.read_exact(&mut buffer).unwrap(); + + let raw_buf = decompress_lzw2(&buffer, block.size_raw); + + output_buf.write_all(&raw_buf).unwrap(); + } + + Ok(output_buf) +} + +pub fn decompress_lzw2(input_data: &[u8], size: usize) -> Vec { + let mut data = input_data.to_vec(); + data[0..2].copy_from_slice(&[0, 0]); + let mut dictionary = BTreeMap::new(); + for i in 0..256 { + dictionary.insert(i as u64, vec![i as u8]); + } + let mut dictionary_count = dictionary.len() as u64; + let mut result = Vec::with_capacity(size); + + let data_size = input_data.len(); + data.extend_from_slice(&[0, 0]); + let mut bit_io = BitIO::new(data); + let mut w = dictionary.get(&0).unwrap().clone(); + + let mut element; + loop { + let flag = bit_io.read_bit(1); + if flag == 0 { + element = bit_io.read_bit(15); + } else { + element = bit_io.read_bit(18); + } + + if bit_io.byte_offset() > data_size { + break + } + + let mut entry; + if let Some(x) = dictionary.get(&element) { + // If the element was already in the dict, get it + entry = x.clone() + } else if element == dictionary_count { + entry = w.clone(); + entry.push(w[0]) + } else { + panic!("Bad compressed element: {}", element) + } + + //println!("{}", element); + + result.write(&entry).unwrap(); + w.push(entry[0]); + dictionary.insert(dictionary_count, w.clone()); + dictionary_count += 1; + w = entry.clone(); + } + result +} + fn get_offset(input: &[u8], src: usize) -> usize { (((input[src] as usize) | (input[src + 1] as usize) << 8) - 0x101) * 2 } diff --git a/cz/src/formats/cz0.rs b/cz/src/formats/cz0.rs index 5f89247..a72c03d 100644 --- a/cz/src/formats/cz0.rs +++ b/cz/src/formats/cz0.rs @@ -47,7 +47,7 @@ impl CzHeader for Cz0Header { let common = CommonHeader::new(bytes)?; if common.version() != 0 { - return Err(CzError::VersionMismatch); + return Err(CzError::VersionMismatch(common.version(), 0)); } let mut unknown_1 = [0u8; 5]; diff --git a/cz/src/formats/cz1.rs b/cz/src/formats/cz1.rs index 7e834f5..0410dc9 100644 --- a/cz/src/formats/cz1.rs +++ b/cz/src/formats/cz1.rs @@ -26,7 +26,7 @@ impl CzImage for Cz1Image { bytes.seek(SeekFrom::Start(header.length() as u64))?; if header.version() != 1 { - return Err(CzError::VersionMismatch) + return Err(CzError::VersionMismatch(header.version(), 1)); } // The color palette, gotten for 8 and 4 BPP images diff --git a/cz/src/formats/cz2.rs b/cz/src/formats/cz2.rs new file mode 100644 index 0000000..0fd5bcc --- /dev/null +++ b/cz/src/formats/cz2.rs @@ -0,0 +1,158 @@ +use byteorder::{ReadBytesExt, WriteBytesExt}; +use image::{ImageFormat, Rgba}; +use std::{ + fs::File, + io::{BufWriter, Read, Seek, SeekFrom, Write}, + path::PathBuf +}; + +use crate::compression::{decompress, decompress_2, parse_chunk_info}; +use crate::common::{apply_palette, parse_colormap, CommonHeader, CzError, CzHeader, CzImage}; + +#[derive(Debug, Clone, Copy)] +pub struct Cz2Header { + common: CommonHeader, + unknown_1: u8, + unknown_2: u8, + unknown_3: u8, +} + +impl CzHeader for Cz2Header { + fn new(bytes: &mut T) -> Result + where + Self: Sized + { + let common = CommonHeader::new(bytes)?; + + if common.version() != 2 { + return Err(CzError::VersionMismatch(common.version(), 2)); + } + + Ok(Self { + common, + unknown_1: bytes.read_u8()?, + unknown_2: bytes.read_u8()?, + unknown_3: bytes.read_u8()?, + }) + } + + fn to_bytes(&self) -> Result, std::io::Error> { + let mut buf = vec![]; + + buf.write_all(&self.common.to_bytes()?)?; + buf.write_u8(self.unknown_1)?; + buf.write_u8(self.unknown_2)?; + buf.write_u8(self.unknown_3)?; + + Ok(buf) + } + + fn version(&self) -> u8 { + self.common.version() + } + + fn length(&self) -> usize { + self.common.length() + } + + fn width(&self) -> u16 { + self.common.width() + } + + fn height(&self) -> u16 { + self.common.height() + } + + fn depth(&self) -> u16 { + self.common.depth() + } + + fn color_block(&self) -> u8 { + self.common.color_block() + } +} + +#[derive(Debug, Clone)] +pub struct Cz2Image { + header: Cz2Header, + bitmap: Vec, + palette: Vec>, +} + +impl CzImage for Cz2Image { + type Header = Cz2Header; + + fn decode(bytes: &mut T) -> Result { + let header = Cz2Header::new(bytes).unwrap(); + bytes.seek(SeekFrom::Start(header.length() as u64))?; + + if header.version() != 2 { + return Err(CzError::VersionMismatch(header.version(), 2)); + } + + dbg!(header); + + // The color palette, gotten for 8 and 4 BPP images + let mut palette = None; + if header.depth() == 8 || header.depth() == 4 { + palette = Some(parse_colormap(bytes, 1 << header.depth())?); + } + + let chunk_info = parse_chunk_info(bytes)?; + bytes.seek(SeekFrom::Start(chunk_info.length as u64))?; + + let mut bitmap = decompress_2(bytes, &chunk_info).unwrap(); + + // Apply the palette if it exists + if let Some(pal) = &palette { + bitmap = apply_palette(&mut bitmap, pal); + } + + let image = Self { + header, + bitmap, + palette: palette.unwrap(), + }; + + Ok(image) + } + + fn save_as_png(&self, name: &str) -> Result<(), image::error::ImageError> { + let img = image::RgbaImage::from_raw( + self.header.width() as u32 - 1, + self.header.height() as u32, + self.bitmap.clone(), + ).unwrap(); + + img.save(name)?; + + Ok(()) + } + + fn header(&self) -> &Self::Header { + &self.header + } + + fn set_header(&mut self, header: Self::Header) { + self.header = header + } + + fn into_bitmap(self) -> Vec { + self.bitmap + } + + fn save_as_cz>(&self, path: T) -> Result<(), CzError> { + let mut output_file = BufWriter::new(File::create(path.into())?); + + output_file.write_all(&self.header.to_bytes()?)?; + + + + output_file.flush()?; + Ok(()) + } + + fn set_bitmap(&mut self, bitmap: &[u8], header: &Self::Header) { + todo!() + } +} diff --git a/cz/src/formats/cz3.rs b/cz/src/formats/cz3.rs index 38be0a6..b38cf6e 100644 --- a/cz/src/formats/cz3.rs +++ b/cz/src/formats/cz3.rs @@ -1,4 +1,7 @@ -use std::{io::{self, Cursor, Read, Seek, SeekFrom}, path::PathBuf}; +use std::{ + io::{self, Read, Seek, SeekFrom}, + path::PathBuf +}; use byteorder::{LittleEndian, ReadBytesExt}; @@ -37,7 +40,7 @@ impl CzHeader for Cz3Header { let common = CommonHeader::new(bytes)?; if common.version() != 3 { - return Err(CzError::VersionMismatch); + return Err(CzError::VersionMismatch(common.version(), 3)); } let mut unknown_1 = [0u8; 5]; diff --git a/cz/src/lib.rs b/cz/src/lib.rs index 3529899..ea5d738 100644 --- a/cz/src/lib.rs +++ b/cz/src/lib.rs @@ -1,8 +1,10 @@ pub mod common; -pub mod compression; +mod binio; +mod compression; pub mod formats { pub mod cz0; pub mod cz1; + pub mod cz2; pub mod cz3; } @@ -11,6 +13,8 @@ pub use formats::cz0::Cz0Image; #[doc(inline)] pub use formats::cz1::Cz1Image; #[doc(inline)] +pub use formats::cz2::Cz2Image; +#[doc(inline)] pub use formats::cz3::Cz3Image; /// Traits for CZ# images diff --git a/utils/src/main.rs b/utils/src/main.rs index 686cb69..d9d64ec 100644 --- a/utils/src/main.rs +++ b/utils/src/main.rs @@ -1,10 +1,12 @@ use std::fs; - -use cz::{Cz3Image, CzImage}; +use cz::{Cz2Image, CzImage}; fn main() { - let mut input = fs::File::open("../../test_files/Old_TestFiles/129.CZ3").unwrap(); - let img_file = Cz3Image::decode(&mut input).unwrap(); + let mut input = fs::File::open("../../test_files/font_files/24.cz2") + .expect("Failed to open file"); + + let img_file = Cz2Image::decode(&mut input) + .expect("Failed to decode image"); img_file.save_as_png("test1.png").unwrap(); }