Compare commits

..

No commits in common. "721e61f98c2ce4e5ab097b15445e19ef9a9d0050" and "9e6ba57deed2ec4f93b3039e491c6523d9ddb784" have entirely different histories.

13 changed files with 198 additions and 275 deletions

View file

@ -15,6 +15,3 @@ unsafe_code = "forbid"
[profile.production] [profile.production]
inherits = "release" inherits = "release"
lto = true lto = true
strip = true
codegen-units = 1
panic = "abort"

View file

@ -10,10 +10,7 @@ A encoder/decoder for CZ# image files used in the LUCA System Engine.
png = ["dep:image"] png = ["dep:image"]
[dependencies] [dependencies]
byteorder = "1.5" byteorder = "1.5.0"
thiserror = "1.0" thiserror = "1.0.59"
imagequant = "4.3" imagequant = "4.3.1"
rgb = "0.8"
# Only active on feature "png"
image = { version = "0.25", default-features = false, features = ["png"], optional = true } image = { version = "0.25", default-features = false, features = ["png"], optional = true }

View file

@ -1,40 +1,27 @@
use byteorder::ReadBytesExt;
use imagequant::Attributes;
use rgb::{ComponentSlice, RGBA8};
use std::{ use std::{
collections::HashMap, collections::HashMap,
io::{Read, Seek}, io::{Read, Seek},
}; };
use byteorder::ReadBytesExt;
use imagequant::Attributes;
use crate::common::{CommonHeader, CzError}; use crate::common::{CommonHeader, CzError};
/// A palette of RGBA values for indexed color #[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone)] pub struct Rgba(pub [u8; 4]);
impl From<[u8; 4]> for Rgba {
fn from(value: [u8; 4]) -> Self {
Self([value[0], value[1], value[2], value[3]])
}
}
#[derive(Debug)]
pub struct Palette { pub struct Palette {
colors: Vec<RGBA8>, pub colors: Vec<Rgba>
} }
impl Palette {
/// Get the list of colors from the palette
pub fn colors(&self) -> &Vec<RGBA8> {
&self.colors
}
/// Consume the palette, returning a list of colors
pub fn into_colors(self) -> Vec<RGBA8> {
self.colors
}
pub fn len(&self) -> usize {
self.colors.len()
}
pub fn get(&self, index: usize) -> Option<&RGBA8> {
self.colors.get(index)
}
}
/// Get a palette from the input stream, beginning where the palette starts.
pub fn get_palette<T: Seek + ReadBytesExt + Read>( pub fn get_palette<T: Seek + ReadBytesExt + Read>(
input: &mut T, input: &mut T,
num_colors: usize, num_colors: usize,
@ -50,15 +37,15 @@ pub fn get_palette<T: Seek + ReadBytesExt + Read>(
Ok(Palette { colors: colormap }) Ok(Palette { colors: colormap })
} }
/// Takes an indexed color bitmap and maps a given palette to it, returning an /// Take a bitmap of indicies, and map a given palette to it, returning a new
/// RGBA bitmap. /// RGBA bitmap
pub fn indexed_to_rgba(input: &[u8], palette: &Palette) -> Result<Vec<u8>, CzError> { pub fn apply_palette(input: &[u8], palette: &Palette) -> Result<Vec<u8>, CzError> {
let mut output_map = Vec::new(); let mut output_map = Vec::new();
for byte in input.iter() { for byte in input.iter() {
let color = palette.get(*byte as usize); let color = palette.colors.get(*byte as usize);
if let Some(color) = color { if let Some(color) = color {
output_map.extend_from_slice(color.as_slice()); output_map.extend_from_slice(&color.0);
} else { } else {
return Err(CzError::PaletteError); return Err(CzError::PaletteError);
} }
@ -67,7 +54,6 @@ pub fn indexed_to_rgba(input: &[u8], palette: &Palette) -> Result<Vec<u8>, CzErr
Ok(output_map) Ok(output_map)
} }
/// Takes an RGBA bitmap and maps the colors in it to indices of an indexed bitmap.
pub fn rgba_to_indexed(input: &[u8], palette: &Palette) -> Result<Vec<u8>, CzError> { pub fn rgba_to_indexed(input: &[u8], palette: &Palette) -> Result<Vec<u8>, CzError> {
let mut output_map = Vec::new(); let mut output_map = Vec::new();
let mut cache = HashMap::new(); let mut cache = HashMap::new();
@ -76,12 +62,7 @@ pub fn rgba_to_indexed(input: &[u8], palette: &Palette) -> Result<Vec<u8>, CzErr
let value = match cache.get(rgba) { let value = match cache.get(rgba) {
Some(val) => *val, Some(val) => *val,
None => { None => {
let value = palette let value = palette.colors.iter().position(|e| e.0 == rgba).unwrap_or_default() as u8;
.colors()
.iter()
.position(|e| e.as_slice() == rgba)
.unwrap_or_default() as u8;
cache.insert(rgba, value); cache.insert(rgba, value);
value value
} }
@ -93,11 +74,10 @@ pub fn rgba_to_indexed(input: &[u8], palette: &Palette) -> Result<Vec<u8>, CzErr
Ok(output_map) Ok(output_map)
} }
/// Generate and a bitmap for a given input of RGBA pixels.
pub fn indexed_gen_palette( pub fn indexed_gen_palette(
input: &[u8], input: &[u8],
header: &CommonHeader, header: &CommonHeader,
) -> Result<(Vec<u8>, Vec<RGBA8>), CzError> { ) -> Result<(Vec<u8>, Vec<Rgba>), CzError> {
let size = (header.width() as u32 * header.height() as u32) * 4; let size = (header.width() as u32 * header.height() as u32) * 4;
let mut buf: Vec<u8> = vec![0; size as usize]; let mut buf: Vec<u8> = vec![0; size as usize];
@ -111,30 +91,33 @@ pub fn indexed_gen_palette(
let mut quant = Attributes::new(); let mut quant = Attributes::new();
quant.set_speed(1).unwrap(); quant.set_speed(1).unwrap();
let mut image = quant let mut image = quant.new_image(
.new_image(buf, header.width() as usize, header.height() as usize, 0.0) buf,
.unwrap(); header.width() as usize,
header.height() as usize,
0.0
).unwrap();
let mut quant_result = quant.quantize(&mut image).unwrap(); let mut quant_result = quant.quantize(&mut image).unwrap();
let (palette, indicies) = quant_result.remapped(&mut image).unwrap(); let (palette, indicies) = quant_result.remapped(&mut image).unwrap();
let gen_palette: Vec<RGBA8> = palette let gen_palette: Vec<Rgba> = palette
.iter() .iter()
.map(|c| RGBA8::from([c.r, c.g, c.b, c.a])) .map(|c| Rgba([c.r, c.g, c.b, c.a]))
.collect(); .collect();
let mut output_palette = vec![RGBA8::from([0, 0, 0, 0]); 256]; let mut output_palette = vec![Rgba([0, 0, 0, 0]); 256];
output_palette[0..gen_palette.len()].copy_from_slice(&gen_palette); output_palette[0..gen_palette.len()].copy_from_slice(&gen_palette);
Ok((indicies, output_palette)) Ok((indicies, output_palette))
} }
pub fn _default_palette() -> Vec<RGBA8> { pub fn _default_palette() -> Vec<Rgba> {
let mut colormap = Vec::new(); let mut colormap = Vec::new();
for i in 0..=0xFF { for i in 0..=0xFF {
colormap.push(RGBA8::from([0xFF, 0xFF, 0xFF, i])) colormap.push(Rgba([0xFF, 0xFF, 0xFF, i]))
} }
colormap colormap

View file

@ -159,10 +159,7 @@ impl CommonHeader {
self.version self.version
} }
pub fn set_version<I: TryInto<CzVersion> + Into<u32> + Clone>( pub fn set_version<I: TryInto<CzVersion> + Into<u32> + Clone>(&mut self, version: I) -> Result<(), CzError> {
&mut self,
version: I,
) -> Result<(), CzError> {
self.version = match version.clone().try_into() { self.version = match version.clone().try_into() {
Ok(val) => val, Ok(val) => val,
Err(_) => return Err(CzError::InvalidVersion(version.into())), Err(_) => return Err(CzError::InvalidVersion(version.into())),
@ -203,7 +200,11 @@ impl CommonHeader {
self.unknown self.unknown
} }
pub fn write_into<T: WriteBytesExt + Write>(&self, output: &mut T) -> Result<(), io::Error> { pub fn write_into<T: Seek + WriteBytesExt + Write>(
&self,
output: &mut T,
) -> Result<usize, io::Error> {
let pos = output.stream_position()?;
let magic_bytes = [b'C', b'Z', b'0' + self.version as u8, b'\0']; let magic_bytes = [b'C', b'Z', b'0' + self.version as u8, b'\0'];
output.write_all(&magic_bytes)?; output.write_all(&magic_bytes)?;
@ -213,7 +214,7 @@ impl CommonHeader {
output.write_u16::<LittleEndian>(self.depth())?; output.write_u16::<LittleEndian>(self.depth())?;
output.write_u8(self.color_block())?; output.write_u8(self.color_block())?;
Ok(()) Ok((output.stream_position()? - pos) as usize)
} }
} }
@ -324,7 +325,12 @@ impl ExtendedHeader {
}) })
} }
pub fn write_into<T: WriteBytesExt + Write>(&self, output: &mut T) -> Result<(), io::Error> { pub fn write_into<T: Seek + WriteBytesExt + Write>(
&self,
output: &mut T,
) -> Result<usize, io::Error> {
let pos = output.stream_position()?;
output.write_all(&self.unknown_1)?; output.write_all(&self.unknown_1)?;
output.write_u16::<LittleEndian>(self.crop_width)?; output.write_u16::<LittleEndian>(self.crop_width)?;
output.write_u16::<LittleEndian>(self.crop_height)?; output.write_u16::<LittleEndian>(self.crop_height)?;
@ -337,6 +343,6 @@ impl ExtendedHeader {
output.write_u32::<LittleEndian>(self.unknown_2.unwrap())?; output.write_u32::<LittleEndian>(self.unknown_2.unwrap())?;
} }
Ok(()) Ok((output.stream_position()? - pos) as usize)
} }
} }

View file

@ -110,7 +110,10 @@ pub fn decompress<T: Seek + ReadBytesExt + Read>(
Ok(output_buf) Ok(output_buf)
} }
fn decompress_lzw(input_data: &[u16], size: usize) -> Vec<u8> { fn decompress_lzw(
input_data: &[u16],
size: usize
) -> Vec<u8> {
let mut dictionary: HashMap<u16, Vec<u8>> = HashMap::new(); let mut dictionary: HashMap<u16, Vec<u8>> = HashMap::new();
for i in 0..256 { for i in 0..256 {
dictionary.insert(i as u16, vec![i as u8]); dictionary.insert(i as u16, vec![i as u8]);
@ -143,6 +146,7 @@ fn decompress_lzw(input_data: &[u16], size: usize) -> Vec<u8> {
result result
} }
/// Decompress an LZW compressed stream like CZ2 /// Decompress an LZW compressed stream like CZ2
pub fn decompress2<T: Seek + ReadBytesExt + Read>( pub fn decompress2<T: Seek + ReadBytesExt + Read>(
input: &mut T, input: &mut T,
@ -162,7 +166,10 @@ pub fn decompress2<T: Seek + ReadBytesExt + Read>(
Ok(output_buf) Ok(output_buf)
} }
fn decompress_lzw2(input_data: &[u8], size: usize) -> Vec<u8> { fn decompress_lzw2(
input_data: &[u8],
size: usize
) -> Vec<u8> {
let mut data = input_data.to_vec(); let mut data = input_data.to_vec();
data[0] = 0; data[0] = 0;
let mut dictionary = HashMap::new(); let mut dictionary = HashMap::new();
@ -212,7 +219,10 @@ fn decompress_lzw2(input_data: &[u8], size: usize) -> Vec<u8> {
result result
} }
pub fn compress(data: &[u8], size: usize) -> (Vec<u8>, CompressionInfo) { pub fn compress(
data: &[u8],
size: usize,
) -> (Vec<u8>, CompressionInfo) {
let mut size = size; let mut size = size;
if size == 0 { if size == 0 {
size = 0xFEFD size = 0xFEFD
@ -233,7 +243,7 @@ pub fn compress(data: &[u8], size: usize) -> (Vec<u8>, CompressionInfo) {
loop { loop {
(count, part_data, last) = compress_lzw(&data[offset..], size, last); (count, part_data, last) = compress_lzw(&data[offset..], size, last);
if count == 0 { if count == 0 {
break; break
} }
offset += count; offset += count;
@ -243,7 +253,7 @@ pub fn compress(data: &[u8], size: usize) -> (Vec<u8>, CompressionInfo) {
output_info.chunks.push(ChunkInfo { output_info.chunks.push(ChunkInfo {
size_compressed: part_data.len(), size_compressed: part_data.len(),
size_raw: count, size_raw: count
}); });
output_info.chunk_count += 1; output_info.chunk_count += 1;
@ -261,7 +271,11 @@ pub fn compress(data: &[u8], size: usize) -> (Vec<u8>, CompressionInfo) {
(output_buf, output_info) (output_buf, output_info)
} }
fn compress_lzw(data: &[u8], size: usize, last: Vec<u8>) -> (usize, Vec<u16>, Vec<u8>) { fn compress_lzw(
data: &[u8],
size: usize,
last: Vec<u8>
) -> (usize, Vec<u16>, Vec<u8>) {
let mut count = 0; let mut count = 0;
let mut dictionary = HashMap::new(); let mut dictionary = HashMap::new();
for i in 0..=255 { for i in 0..=255 {
@ -291,7 +305,7 @@ fn compress_lzw(data: &[u8], size: usize, last: Vec<u8>) -> (usize, Vec<u16>, Ve
count += 1; count += 1;
if size > 0 && compressed.len() == size { if size > 0 && compressed.len() == size {
break; break
} }
} }
@ -302,18 +316,21 @@ fn compress_lzw(data: &[u8], size: usize, last: Vec<u8>) -> (usize, Vec<u16>, Ve
compressed.push(*dictionary.get(&vec![c]).unwrap()); compressed.push(*dictionary.get(&vec![c]).unwrap());
} }
} }
return (count, compressed, Vec::new()); return (count, compressed, Vec::new())
} else if compressed.len() < size { } else if compressed.len() < size {
if !last_element.is_empty() { if !last_element.is_empty() {
compressed.push(*dictionary.get(&last_element).unwrap()); compressed.push(*dictionary.get(&last_element).unwrap());
} }
return (count, compressed, Vec::new()); return (count, compressed, Vec::new())
} }
(count, compressed, last_element) (count, compressed, last_element)
} }
pub fn compress2(data: &[u8], size: usize) -> (Vec<u8>, CompressionInfo) { pub fn compress2(
data: &[u8],
size: usize
) -> (Vec<u8>, CompressionInfo) {
let size = if size == 0 { 0x87BDF } else { size }; let size = if size == 0 { 0x87BDF } else { size };
let mut part_data; let mut part_data;
@ -356,7 +373,11 @@ pub fn compress2(data: &[u8], size: usize) -> (Vec<u8>, CompressionInfo) {
(output_buf, output_info) (output_buf, output_info)
} }
fn compress_lzw2(data: &[u8], size: usize, last: Vec<u8>) -> (usize, Vec<u8>, Vec<u8>) { fn compress_lzw2(
data: &[u8],
size: usize,
last: Vec<u8>
) -> (usize, Vec<u8>, Vec<u8>) {
let mut data = data.to_vec(); let mut data = data.to_vec();
if !data.is_empty() { if !data.is_empty() {
data[0] = 0; data[0] = 0;

View file

@ -1,31 +1,35 @@
use byteorder::ReadBytesExt; use byteorder::ReadBytesExt;
use rgb::ComponentSlice;
use std::{ use std::{
fs::File, fs::File,
io::{BufWriter, Read, Seek, SeekFrom, Write}, io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write},
path::Path,
}; };
use crate::{ use crate::{
color::{get_palette, indexed_gen_palette, indexed_to_rgba, rgba_to_indexed, Palette}, color::{apply_palette, get_palette, indexed_gen_palette, rgba_to_indexed, Palette},
common::{CommonHeader, CzError, CzVersion, ExtendedHeader}, common::{CommonHeader, CzError, CzVersion, ExtendedHeader},
formats::{cz0, cz1, cz2, cz3, cz4}, formats::{cz0, cz1, cz2, cz3, cz4},
}; };
/// A CZ# interface which can open and save any CZ file type. /// A CZ# interface which abstracts the CZ# generic file interface for
#[derive(Debug, Clone)] /// convenience.
#[derive(Debug)]
pub struct DynamicCz { pub struct DynamicCz {
header_common: CommonHeader, header_common: CommonHeader,
header_extended: Option<ExtendedHeader>, header_extended: Option<ExtendedHeader>,
/// A palette of RGBA values for indexed color
palette: Option<Palette>, palette: Option<Palette>,
/// 32bpp RGBA bitmap representation of the file contents
bitmap: Vec<u8>, bitmap: Vec<u8>,
} }
impl DynamicCz { impl DynamicCz {
/// Decode a CZ# file from anything that implements [`Read`] and [`Seek`] /// Open a CZ# file from a path
pub fn open<P: ?Sized + AsRef<Path>>(path: &P) -> Result<Self, CzError> {
let mut img_file = BufReader::new(std::fs::File::open(path)?);
Self::decode(&mut img_file)
}
/// Decode a CZ# file from anything which implements [`Read`] and [`Seek`]
/// ///
/// The input must begin with the /// The input must begin with the
/// [magic bytes](https://en.wikipedia.org/wiki/File_format#Magic_number) /// [magic bytes](https://en.wikipedia.org/wiki/File_format#Magic_number)
@ -70,11 +74,12 @@ impl DynamicCz {
match header_common.depth() { match header_common.depth() {
4 => { 4 => {
todo!("Files with a bit depth of 4 are not yet supported") eprintln!("Files with a bit depth of 4 are not yet supported");
todo!()
} }
8 => { 8 => {
if let Some(palette) = &palette { if let Some(palette) = &palette {
bitmap = indexed_to_rgba(&bitmap, palette)?; bitmap = apply_palette(&bitmap, palette)?;
} else { } else {
return Err(CzError::PaletteError); return Err(CzError::PaletteError);
} }
@ -106,32 +111,20 @@ impl DynamicCz {
/// Save the `DynamicCz` as a CZ# file. The format saved in is determined /// Save the `DynamicCz` as a CZ# file. The format saved in is determined
/// from the format in the header. Check [`CommonHeader::set_version()`] /// from the format in the header. Check [`CommonHeader::set_version()`]
/// to change the CZ# version. /// to change the CZ# version.
pub fn save_as_cz<P: ?Sized + AsRef<std::path::Path>>(&self, path: &P) -> Result<(), CzError> { pub fn save_as_cz<T: Into<std::path::PathBuf>>(&self, path: T) -> Result<(), CzError> {
let mut out_file = BufWriter::new(File::create(path.as_ref())?); let mut out_file = BufWriter::new(File::create(path.into())?);
self.encode(&mut out_file)?;
Ok(())
}
/// Encode a CZ# file into anything that implements [`Write`] and [`Seek`]
///
/// This encodes everything based on options the header which have been
/// set by the user. For example, to change the version of file to be
/// saved, use [`CommonHeader::set_version()`]
pub fn encode<T: Write>(&self, mut output: &mut T) -> Result<(), CzError> {
let mut header = *self.header(); let mut header = *self.header();
if header.version() == CzVersion::CZ2 { if header.version() == CzVersion::CZ2 {
header.set_length(0x12) header.set_length(0x12)
} }
header.write_into(&mut output)?; header.write_into(&mut out_file)?;
if header.version() == CzVersion::CZ2 { if header.version() == CzVersion::CZ2 {
// TODO: CZ2 files have this odd section instead of an extended header...? // CZ2 files have this odd section instead of an extended header...?
output.write_all(&[0, 0, 0])?; out_file.write_all(&[0, 0, 0])?;
} else if let Some(ext) = self.header_extended { } else if let Some(ext) = self.header_extended {
ext.write_into(&mut output)?; ext.write_into(&mut out_file)?;
} }
let output_bitmap; let output_bitmap;
@ -146,8 +139,8 @@ impl DynamicCz {
// Use the existing palette to palette the image // Use the existing palette to palette the image
output_bitmap = rgba_to_indexed(self.bitmap(), pal)?; output_bitmap = rgba_to_indexed(self.bitmap(), pal)?;
for rgba in pal.colors() { for rgba in &pal.colors {
output.write_all(rgba.as_slice())?; out_file.write_all(&rgba.0)?;
} }
} else { } else {
// Generate a palette and corresponding indexed bitmap if there is none // Generate a palette and corresponding indexed bitmap if there is none
@ -157,7 +150,12 @@ impl DynamicCz {
let palette = result.1; let palette = result.1;
for rgba in palette { for rgba in palette {
output.write_all(rgba.as_slice())?; let mut rgba_clone = rgba.0;
if false {
// TODO: Make a toggle for this
rgba_clone[0..3].reverse();
}
out_file.write_all(&rgba_clone)?;
} }
} }
} }
@ -181,11 +179,11 @@ impl DynamicCz {
} }
match self.header_common.version() { match self.header_common.version() {
CzVersion::CZ0 => cz0::encode(&mut output, &output_bitmap)?, CzVersion::CZ0 => cz0::encode(&mut out_file, &output_bitmap)?,
CzVersion::CZ1 => cz1::encode(&mut output, &output_bitmap)?, CzVersion::CZ1 => cz1::encode(&mut out_file, &output_bitmap)?,
CzVersion::CZ2 => cz2::encode(&mut output, &output_bitmap)?, CzVersion::CZ2 => cz2::encode(&mut out_file, &output_bitmap)?,
CzVersion::CZ3 => cz3::encode(&mut output, &output_bitmap, &self.header_common)?, CzVersion::CZ3 => cz3::encode(&mut out_file, &output_bitmap, &self.header_common)?,
CzVersion::CZ4 => cz4::encode(&mut output, &output_bitmap, &self.header_common)?, CzVersion::CZ4 => cz4::encode(&mut out_file, &output_bitmap, &self.header_common)?,
CzVersion::CZ5 => todo!(), CzVersion::CZ5 => todo!(),
} }
@ -198,7 +196,7 @@ impl DynamicCz {
/// which is the highest encountered in CZ# files, therefore saving them /// which is the highest encountered in CZ# files, therefore saving them
/// as a PNG of the same or better quality is lossless. /// as a PNG of the same or better quality is lossless.
#[cfg(feature = "png")] #[cfg(feature = "png")]
pub fn save_as_png<P: ?Sized + AsRef<std::path::Path>>( pub fn save_as_png<P: ?Sized + AsRef<Path>>(
&self, &self,
path: &P, path: &P,
) -> Result<(), image::error::EncodingError> { ) -> Result<(), image::error::EncodingError> {

View file

@ -1,5 +1,6 @@
use byteorder::{ReadBytesExt, WriteBytesExt}; use byteorder::{ReadBytesExt, WriteBytesExt};
use std::io::{Read, Seek, SeekFrom, Write}; use std::io::{Read, Seek, SeekFrom, Write};
use std::time::Instant;
use crate::common::{CommonHeader, CzError}; use crate::common::{CommonHeader, CzError};
use crate::compression::{compress, decompress, get_chunk_info}; use crate::compression::{compress, decompress, get_chunk_info};
@ -11,9 +12,13 @@ pub fn decode<T: Seek + ReadBytesExt + Read>(
let block_info = get_chunk_info(bytes)?; let block_info = get_chunk_info(bytes)?;
bytes.seek(SeekFrom::Start(block_info.length as u64))?; bytes.seek(SeekFrom::Start(block_info.length as u64))?;
let data = decompress(bytes, &block_info)?; let timer = Instant::now();
let bitmap = decompress(bytes, &block_info)?;
dbg!(timer.elapsed());
let bitmap = line_diff(header, &data); let timer = Instant::now();
let bitmap = line_diff(header, &bitmap);
dbg!(timer.elapsed());
Ok(bitmap) Ok(bitmap)
} }
@ -55,10 +60,9 @@ fn line_diff(header: &CommonHeader, data: &[u8]) -> Vec<u8> {
curr_line = data[index..index + line_byte_count].to_vec(); curr_line = data[index..index + line_byte_count].to_vec();
if y % block_height as u32 != 0 { if y % block_height as u32 != 0 {
curr_line curr_line.iter_mut().zip(&prev_line).for_each(|(curr_p, prev_p)| {
.iter_mut() *curr_p = curr_p.wrapping_add(*prev_p)
.zip(&prev_line) });
.for_each(|(curr_p, prev_p)| *curr_p = curr_p.wrapping_add(*prev_p));
} }
prev_line.clone_from(&curr_line); prev_line.clone_from(&curr_line);

View file

@ -1,5 +1,6 @@
use byteorder::{ReadBytesExt, WriteBytesExt}; use byteorder::{ReadBytesExt, WriteBytesExt};
use std::io::{Read, Seek, SeekFrom, Write}; use std::io::{Read, Seek, SeekFrom, Write};
use std::time::Instant;
use crate::common::{CommonHeader, CzError}; use crate::common::{CommonHeader, CzError};
use crate::compression::{compress, decompress, get_chunk_info}; use crate::compression::{compress, decompress, get_chunk_info};
@ -11,11 +12,15 @@ pub fn decode<T: Seek + ReadBytesExt + Read>(
let block_info = get_chunk_info(bytes)?; let block_info = get_chunk_info(bytes)?;
bytes.seek(SeekFrom::Start(block_info.length as u64))?; bytes.seek(SeekFrom::Start(block_info.length as u64))?;
let timer = Instant::now();
let data = decompress(bytes, &block_info)?; let data = decompress(bytes, &block_info)?;
dbg!(timer.elapsed());
let bitmap = line_diff(header, &data); let timer = Instant::now();
let output = line_diff(header, &data);
dbg!(timer.elapsed());
Ok(bitmap) Ok(output)
} }
pub fn encode<T: WriteBytesExt + Write>( pub fn encode<T: WriteBytesExt + Write>(
@ -55,16 +60,10 @@ fn line_diff(header: &CommonHeader, data: &[u8]) -> Vec<u8> {
curr_alpha = data[alpha_index..alpha_index + width as usize].to_vec(); curr_alpha = data[alpha_index..alpha_index + width as usize].to_vec();
if y % block_height != 0 { if y % block_height != 0 {
curr_line curr_line.iter_mut().zip(&prev_line).for_each(|(curr_p, prev_p)| {
.iter_mut()
.zip(&prev_line)
.for_each(|(curr_p, prev_p)| {
*curr_p = curr_p.wrapping_add(*prev_p); *curr_p = curr_p.wrapping_add(*prev_p);
}); });
curr_alpha curr_alpha.iter_mut().zip(&prev_alpha).for_each(|(curr_a, prev_a)| {
.iter_mut()
.zip(&prev_alpha)
.for_each(|(curr_a, prev_a)| {
*curr_a = curr_a.wrapping_add(*prev_a); *curr_a = curr_a.wrapping_add(*prev_a);
}); });
} }
@ -75,7 +74,12 @@ fn line_diff(header: &CommonHeader, data: &[u8]) -> Vec<u8> {
.step_by(3) .step_by(3)
.zip(&curr_alpha) .zip(&curr_alpha)
.for_each(|(curr_p, alpha_p)| { .for_each(|(curr_p, alpha_p)| {
output_buf.extend_from_slice(&[curr_p[0], curr_p[1], curr_p[2], *alpha_p]); output_buf.extend_from_slice(&[
curr_p[0],
curr_p[1],
curr_p[2],
*alpha_p,
]);
}); });
prev_line.clone_from(&curr_line); prev_line.clone_from(&curr_line);
@ -107,18 +111,8 @@ fn diff_line(header: &CommonHeader, input: &[u8]) -> Vec<u8> {
let mut i = 0; let mut i = 0;
for y in 0..height { for y in 0..height {
curr_line = input[i..i + line_byte_count] curr_line = input[i..i + line_byte_count].windows(4).step_by(4).flat_map(|r| &r[0..3]).copied().collect();
.windows(4) curr_alpha = input[i..i + line_byte_count].iter().skip(3).step_by(4).copied().collect();
.step_by(4)
.flat_map(|r| &r[0..3])
.copied()
.collect();
curr_alpha = input[i..i + line_byte_count]
.iter()
.skip(3)
.step_by(4)
.copied()
.collect();
if y % block_height as u32 != 0 { if y % block_height as u32 != 0 {
for x in 0..width as usize * 3 { for x in 0..width as usize * 3 {

View file

@ -13,16 +13,6 @@ mod formats {
pub(crate) mod cz4; pub(crate) mod cz4;
} }
use common::CzError;
use std::{io::BufReader, path::Path};
/// Open a CZ# file from a path
pub fn open<P: ?Sized + AsRef<Path>>(path: &P) -> Result<DynamicCz, CzError> {
let mut img_file = BufReader::new(std::fs::File::open(path)?);
DynamicCz::decode(&mut img_file)
}
#[doc(inline)] #[doc(inline)]
pub use dynamic::DynamicCz; pub use dynamic::DynamicCz;

View file

@ -1,8 +1,3 @@
fn main() { fn main() {
let mut cz_file = cz::open("test_file.cz3").unwrap(); println!("Hello, world!");
cz_file.save_as_png("test.png").unwrap();
cz_file.header_mut().set_version(3).unwrap();
cz_file.save_as_cz("test_file.cz2").unwrap();
} }

View file

@ -1,9 +1,4 @@
use std::{ use std::{error::Error, fs::File, io::{BufWriter, Write}, path::Path};
error::Error,
fs::File,
io::{BufWriter, Write},
path::Path,
};
/// A single file entry in a PAK file /// A single file entry in a PAK file
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -1,13 +1,9 @@
mod entry; mod entry;
mod header; mod header;
use std::{fs::File, io::{self, BufRead, BufReader, Read, Seek, SeekFrom}, path::{Path, PathBuf}};
use byteorder::{LittleEndian, ReadBytesExt}; use byteorder::{LittleEndian, ReadBytesExt};
use header::Header; use header::Header;
use std::{
fs::File,
io::{self, BufRead, BufReader, Read, Seek, SeekFrom},
path::{Path, PathBuf},
};
use thiserror::Error; use thiserror::Error;
use crate::entry::Entry; use crate::entry::Entry;
@ -65,10 +61,7 @@ impl Pak {
} }
/// Decode a PAK file from a byte stream /// Decode a PAK file from a byte stream
pub fn decode<T: Seek + ReadBytesExt + Read>( pub fn decode<T: Seek + ReadBytesExt + Read>(input: &mut T, path: PathBuf) -> Result<Self, PakError> {
input: &mut T,
path: PathBuf,
) -> Result<Self, PakError> {
let mut input = BufReader::new(input); let mut input = BufReader::new(input);
// Read in all the header bytes // Read in all the header bytes
@ -100,7 +93,7 @@ impl Pak {
dbg!(unknown_pre_data.len()); dbg!(unknown_pre_data.len());
if input.stream_position()? == header.data_offset() as u64 { if input.stream_position()? == header.data_offset() as u64 {
return Err(PakError::HeaderError); return Err(PakError::HeaderError)
} }
// Read all the offsets and lengths // Read all the offsets and lengths
@ -131,11 +124,7 @@ impl Pak {
let mut entries: Vec<Entry> = Vec::new(); let mut entries: Vec<Entry> = Vec::new();
for i in 0..header.entry_count() as usize { for i in 0..header.entry_count() as usize {
// Seek to and read the entry data // Seek to and read the entry data
input input.seek(SeekFrom::Start(offsets[i].0 as u64 * header.block_size() as u64)).unwrap();
.seek(SeekFrom::Start(
offsets[i].0 as u64 * header.block_size() as u64,
))
.unwrap();
let mut data = vec![0u8; offsets[i].1 as usize]; let mut data = vec![0u8; offsets[i].1 as usize];
input.read_exact(&mut data).unwrap(); input.read_exact(&mut data).unwrap();
@ -185,7 +174,8 @@ impl Pak {
pub fn contains_name(&self, name: String) -> bool { pub fn contains_name(&self, name: String) -> bool {
self.entries self.entries
.iter() .iter()
.find(|e| e.name.as_ref().is_some_and(|n| n == &name)) .find(|e|
.is_some() e.name.as_ref().is_some_and(|n| n == &name)
).is_some()
} }
} }

View file

@ -1,5 +1,6 @@
use clap::{error::ErrorKind, Error, Parser, Subcommand}; use cz::DynamicCz;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use clap::{error::ErrorKind, Error, Parser, Subcommand};
#[derive(Parser)] #[derive(Parser)]
#[command(name = "CZ Utils")] #[command(name = "CZ Utils")]
@ -52,7 +53,7 @@ enum Commands {
/// Output CZ file bit depth /// Output CZ file bit depth
#[arg(short, long, value_name = "BIT DEPTH")] #[arg(short, long, value_name = "BIT DEPTH")]
depth: Option<u16>, depth: Option<u16>,
}, }
} }
fn main() { fn main() {
@ -60,34 +61,18 @@ fn main() {
// Check what subcommand was run // Check what subcommand was run
match &cli.command { match &cli.command {
Commands::Decode { Commands::Decode { input, output, batch } => {
input,
output,
batch,
} => {
if !input.exists() { if !input.exists() {
Error::raw( Error::raw(ErrorKind::ValueValidation, "The input file/folder provided does not exist\n").exit()
ErrorKind::ValueValidation,
"The input file/folder provided does not exist\n",
)
.exit()
} }
if *batch { if *batch {
if input.is_file() { if input.is_file() {
Error::raw( Error::raw(ErrorKind::ValueValidation, "Batch input must be a directory\n").exit()
ErrorKind::ValueValidation,
"Batch input must be a directory\n",
)
.exit()
} }
if output.is_none() || output.as_ref().unwrap().is_file() { if output.is_none() || output.as_ref().unwrap().is_file() {
Error::raw( Error::raw(ErrorKind::ValueValidation, "Batch output must be a directory\n").exit()
ErrorKind::ValueValidation,
"Batch output must be a directory\n",
)
.exit()
} }
for entry in walkdir::WalkDir::new(input).max_depth(1) { for entry in walkdir::WalkDir::new(input).max_depth(1) {
@ -102,26 +87,21 @@ fn main() {
let mut final_path = output.clone().unwrap(); let mut final_path = output.clone().unwrap();
final_path.push(filename); final_path.push(filename);
let cz = match cz::open(&path) { let cz = match DynamicCz::open(&path) {
Ok(cz) => cz, Ok(cz) => cz,
Err(_) => { Err(_) => {
Error::raw( Error::raw(
ErrorKind::ValueValidation, ErrorKind::ValueValidation,
format!( format!("Could not open input as a CZ file: {}\n", path.into_os_string().to_str().unwrap())
"Could not open input as a CZ file: {}\n", ).print().unwrap();
path.into_os_string().to_str().unwrap()
),
)
.print()
.unwrap();
continue; continue;
} },
}; };
cz.save_as_png(&final_path).unwrap(); cz.save_as_png(&final_path).unwrap();
} }
} else { } else {
let cz = cz::open(input).unwrap(); let cz = DynamicCz::open(input).unwrap();
if let Some(output) = output { if let Some(output) = output {
cz.save_as_png(output).unwrap(); cz.save_as_png(output).unwrap();
@ -131,54 +111,27 @@ fn main() {
} }
} }
} }
Commands::Replace { Commands::Replace { batch, input, replacement, output, version, depth } => {
batch,
input,
replacement,
output,
version,
depth,
} => {
if !input.exists() { if !input.exists() {
Error::raw( Error::raw(ErrorKind::ValueValidation, "The original file provided does not exist\n").exit()
ErrorKind::ValueValidation,
"The original file provided does not exist\n",
)
.exit()
} }
if !replacement.exists() { if !replacement.exists() {
Error::raw( Error::raw(ErrorKind::ValueValidation, "The replacement file provided does not exist\n").exit()
ErrorKind::ValueValidation,
"The replacement file provided does not exist\n",
)
.exit()
} }
// If it's a batch replacement, we want directories to search // If it's a batch replacement, we want directories to search
if *batch { if *batch {
if !input.is_dir() { if !input.is_dir() {
Error::raw( Error::raw(ErrorKind::ValueValidation, "Batch input location must be a directory\n").exit()
ErrorKind::ValueValidation,
"Batch input location must be a directory\n",
)
.exit()
} }
if !replacement.is_dir() { if !replacement.is_dir() {
Error::raw( Error::raw(ErrorKind::ValueValidation, "Batch replacement location must be a directory\n").exit()
ErrorKind::ValueValidation,
"Batch replacement location must be a directory\n",
)
.exit()
} }
if !output.is_dir() { if !output.is_dir() {
Error::raw( Error::raw(ErrorKind::ValueValidation, "Batch output location must be a directory\n").exit()
ErrorKind::ValueValidation,
"Batch output location must be a directory\n",
)
.exit()
} }
// Replace all the files within the directory and print errors for them // Replace all the files within the directory and print errors for them
@ -188,27 +141,25 @@ fn main() {
{ {
let path = entry.unwrap().into_path(); let path = entry.unwrap().into_path();
if !path.is_file() { if !path.is_file() {
continue; continue
} }
// Set the replacement image to the same name as the original file // Set the replacement image to the same name as the original file
let mut final_replacement = replacement.to_path_buf(); let mut final_replacement = replacement.to_path_buf();
final_replacement final_replacement.push(PathBuf::from(path.file_name().unwrap()).with_extension("png"));
.push(PathBuf::from(path.file_name().unwrap()).with_extension("png"));
// Set the replacement image to the same name as the original file // Set the replacement image to the same name as the original file
let mut final_output = output.to_path_buf(); let mut final_output = output.to_path_buf();
final_output.push(path.file_name().unwrap()); final_output.push(path.file_name().unwrap());
if let Err(error) = if let Err(error) = replace_cz(
replace_cz(&path, &final_output, &final_replacement, version, depth) &path,
{ &final_output,
Error::raw( &final_replacement,
ErrorKind::ValueValidation, version,
format!("{:?} - {}\n", path, error), depth
) ) {
.print() Error::raw(ErrorKind::ValueValidation, format!("{:?} - {}\n", path, error)).print().unwrap();
.unwrap();
} }
} }
} else { } else {
@ -221,17 +172,19 @@ fn main() {
} }
if !output.is_file() { if !output.is_file() {
Error::raw( Error::raw(ErrorKind::ValueValidation, "Replacement output must be a file\n").exit()
ErrorKind::ValueValidation,
"Replacement output must be a file\n",
)
.exit()
} }
// Replace the input file with the new image // Replace the input file with the new image
replace_cz(&input, &output, &replacement, version, depth).unwrap(); replace_cz(
} &input,
&output,
&replacement,
version,
depth
).unwrap();
} }
},
} }
} }
@ -245,18 +198,18 @@ fn replace_cz<P: ?Sized + AsRef<Path>>(
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let path = input_path.as_ref(); let path = input_path.as_ref();
if !path.is_file() { if !path.is_file() {
return Err("Input path is not a file".into()); return Err("Input path is not a file".into())
} }
if !replacement_path.as_ref().exists() || !replacement_path.as_ref().is_file() { if !replacement_path.as_ref().exists() || !replacement_path.as_ref().is_file() {
return Err("Replacement path does not exist or is not a file".into()); return Err("Replacement path does not exist or is not a file".into())
} }
// Open the replacement image and convert it to RGBA8 // Open the replacement image and convert it to RGBA8
let repl_img = image::open(&replacement_path)?.to_rgba8(); let repl_img = image::open(&replacement_path)?.to_rgba8();
// Open the original CZ file // Open the original CZ file
let mut cz = cz::open(&path)?; let mut cz = DynamicCz::open(&path)?;
// Set CZ header parameters and the new bitmap // Set CZ header parameters and the new bitmap
cz.header_mut().set_width(repl_img.width() as u16); cz.header_mut().set_width(repl_img.width() as u16);