mirror of
https://github.com/G2-Games/lbee-utils.git
synced 2025-06-22 22:52:55 -05:00
Compare commits
4 commits
9e6ba57dee
...
721e61f98c
Author | SHA1 | Date | |
---|---|---|---|
721e61f98c | |||
a1b4b04208 | |||
976fffed1f | |||
4f2f192fa1 |
13 changed files with 274 additions and 197 deletions
|
@ -15,3 +15,6 @@ unsafe_code = "forbid"
|
|||
[profile.production]
|
||||
inherits = "release"
|
||||
lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
|
|
@ -10,7 +10,10 @@ A encoder/decoder for CZ# image files used in the LUCA System Engine.
|
|||
png = ["dep:image"]
|
||||
|
||||
[dependencies]
|
||||
byteorder = "1.5.0"
|
||||
thiserror = "1.0.59"
|
||||
imagequant = "4.3.1"
|
||||
byteorder = "1.5"
|
||||
thiserror = "1.0"
|
||||
imagequant = "4.3"
|
||||
rgb = "0.8"
|
||||
|
||||
# Only active on feature "png"
|
||||
image = { version = "0.25", default-features = false, features = ["png"], optional = true }
|
||||
|
|
|
@ -1,27 +1,40 @@
|
|||
use byteorder::ReadBytesExt;
|
||||
use imagequant::Attributes;
|
||||
use rgb::{ComponentSlice, RGBA8};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::{Read, Seek},
|
||||
};
|
||||
|
||||
use byteorder::ReadBytesExt;
|
||||
use imagequant::Attributes;
|
||||
|
||||
use crate::common::{CommonHeader, CzError};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
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)]
|
||||
/// A palette of RGBA values for indexed color
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Palette {
|
||||
pub colors: Vec<Rgba>
|
||||
colors: Vec<RGBA8>,
|
||||
}
|
||||
|
||||
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>(
|
||||
input: &mut T,
|
||||
num_colors: usize,
|
||||
|
@ -37,15 +50,15 @@ pub fn get_palette<T: Seek + ReadBytesExt + Read>(
|
|||
Ok(Palette { colors: colormap })
|
||||
}
|
||||
|
||||
/// Take a bitmap of indicies, and map a given palette to it, returning a new
|
||||
/// RGBA bitmap
|
||||
pub fn apply_palette(input: &[u8], palette: &Palette) -> Result<Vec<u8>, CzError> {
|
||||
/// Takes an indexed color bitmap and maps a given palette to it, returning an
|
||||
/// RGBA bitmap.
|
||||
pub fn indexed_to_rgba(input: &[u8], palette: &Palette) -> Result<Vec<u8>, CzError> {
|
||||
let mut output_map = Vec::new();
|
||||
|
||||
for byte in input.iter() {
|
||||
let color = palette.colors.get(*byte as usize);
|
||||
let color = palette.get(*byte as usize);
|
||||
if let Some(color) = color {
|
||||
output_map.extend_from_slice(&color.0);
|
||||
output_map.extend_from_slice(color.as_slice());
|
||||
} else {
|
||||
return Err(CzError::PaletteError);
|
||||
}
|
||||
|
@ -54,6 +67,7 @@ pub fn apply_palette(input: &[u8], palette: &Palette) -> Result<Vec<u8>, CzError
|
|||
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> {
|
||||
let mut output_map = Vec::new();
|
||||
let mut cache = HashMap::new();
|
||||
|
@ -62,7 +76,12 @@ pub fn rgba_to_indexed(input: &[u8], palette: &Palette) -> Result<Vec<u8>, CzErr
|
|||
let value = match cache.get(rgba) {
|
||||
Some(val) => *val,
|
||||
None => {
|
||||
let value = palette.colors.iter().position(|e| e.0 == rgba).unwrap_or_default() as u8;
|
||||
let value = palette
|
||||
.colors()
|
||||
.iter()
|
||||
.position(|e| e.as_slice() == rgba)
|
||||
.unwrap_or_default() as u8;
|
||||
|
||||
cache.insert(rgba, value);
|
||||
value
|
||||
}
|
||||
|
@ -74,10 +93,11 @@ pub fn rgba_to_indexed(input: &[u8], palette: &Palette) -> Result<Vec<u8>, CzErr
|
|||
Ok(output_map)
|
||||
}
|
||||
|
||||
/// Generate and a bitmap for a given input of RGBA pixels.
|
||||
pub fn indexed_gen_palette(
|
||||
input: &[u8],
|
||||
header: &CommonHeader,
|
||||
) -> Result<(Vec<u8>, Vec<Rgba>), CzError> {
|
||||
) -> Result<(Vec<u8>, Vec<RGBA8>), CzError> {
|
||||
let size = (header.width() as u32 * header.height() as u32) * 4;
|
||||
|
||||
let mut buf: Vec<u8> = vec![0; size as usize];
|
||||
|
@ -91,33 +111,30 @@ pub fn indexed_gen_palette(
|
|||
let mut quant = Attributes::new();
|
||||
quant.set_speed(1).unwrap();
|
||||
|
||||
let mut image = quant.new_image(
|
||||
buf,
|
||||
header.width() as usize,
|
||||
header.height() as usize,
|
||||
0.0
|
||||
).unwrap();
|
||||
let mut image = quant
|
||||
.new_image(buf, header.width() as usize, header.height() as usize, 0.0)
|
||||
.unwrap();
|
||||
|
||||
let mut quant_result = quant.quantize(&mut image).unwrap();
|
||||
|
||||
let (palette, indicies) = quant_result.remapped(&mut image).unwrap();
|
||||
|
||||
let gen_palette: Vec<Rgba> = palette
|
||||
let gen_palette: Vec<RGBA8> = palette
|
||||
.iter()
|
||||
.map(|c| Rgba([c.r, c.g, c.b, c.a]))
|
||||
.map(|c| RGBA8::from([c.r, c.g, c.b, c.a]))
|
||||
.collect();
|
||||
|
||||
let mut output_palette = vec![Rgba([0, 0, 0, 0]); 256];
|
||||
let mut output_palette = vec![RGBA8::from([0, 0, 0, 0]); 256];
|
||||
output_palette[0..gen_palette.len()].copy_from_slice(&gen_palette);
|
||||
|
||||
Ok((indicies, output_palette))
|
||||
}
|
||||
|
||||
pub fn _default_palette() -> Vec<Rgba> {
|
||||
pub fn _default_palette() -> Vec<RGBA8> {
|
||||
let mut colormap = Vec::new();
|
||||
|
||||
for i in 0..=0xFF {
|
||||
colormap.push(Rgba([0xFF, 0xFF, 0xFF, i]))
|
||||
colormap.push(RGBA8::from([0xFF, 0xFF, 0xFF, i]))
|
||||
}
|
||||
|
||||
colormap
|
||||
|
|
|
@ -159,7 +159,10 @@ impl CommonHeader {
|
|||
self.version
|
||||
}
|
||||
|
||||
pub fn set_version<I: TryInto<CzVersion> + Into<u32> + Clone>(&mut self, version: I) -> Result<(), CzError> {
|
||||
pub fn set_version<I: TryInto<CzVersion> + Into<u32> + Clone>(
|
||||
&mut self,
|
||||
version: I,
|
||||
) -> Result<(), CzError> {
|
||||
self.version = match version.clone().try_into() {
|
||||
Ok(val) => val,
|
||||
Err(_) => return Err(CzError::InvalidVersion(version.into())),
|
||||
|
@ -200,11 +203,7 @@ impl CommonHeader {
|
|||
self.unknown
|
||||
}
|
||||
|
||||
pub fn write_into<T: Seek + WriteBytesExt + Write>(
|
||||
&self,
|
||||
output: &mut T,
|
||||
) -> Result<usize, io::Error> {
|
||||
let pos = output.stream_position()?;
|
||||
pub fn write_into<T: WriteBytesExt + Write>(&self, output: &mut T) -> Result<(), io::Error> {
|
||||
let magic_bytes = [b'C', b'Z', b'0' + self.version as u8, b'\0'];
|
||||
|
||||
output.write_all(&magic_bytes)?;
|
||||
|
@ -214,7 +213,7 @@ impl CommonHeader {
|
|||
output.write_u16::<LittleEndian>(self.depth())?;
|
||||
output.write_u8(self.color_block())?;
|
||||
|
||||
Ok((output.stream_position()? - pos) as usize)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -325,12 +324,7 @@ impl ExtendedHeader {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn write_into<T: Seek + WriteBytesExt + Write>(
|
||||
&self,
|
||||
output: &mut T,
|
||||
) -> Result<usize, io::Error> {
|
||||
let pos = output.stream_position()?;
|
||||
|
||||
pub fn write_into<T: WriteBytesExt + Write>(&self, output: &mut T) -> Result<(), io::Error> {
|
||||
output.write_all(&self.unknown_1)?;
|
||||
output.write_u16::<LittleEndian>(self.crop_width)?;
|
||||
output.write_u16::<LittleEndian>(self.crop_height)?;
|
||||
|
@ -343,6 +337,6 @@ impl ExtendedHeader {
|
|||
output.write_u32::<LittleEndian>(self.unknown_2.unwrap())?;
|
||||
}
|
||||
|
||||
Ok((output.stream_position()? - pos) as usize)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,10 +110,7 @@ pub fn decompress<T: Seek + ReadBytesExt + Read>(
|
|||
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();
|
||||
for i in 0..256 {
|
||||
dictionary.insert(i as u16, vec![i as u8]);
|
||||
|
@ -146,7 +143,6 @@ fn decompress_lzw(
|
|||
result
|
||||
}
|
||||
|
||||
|
||||
/// Decompress an LZW compressed stream like CZ2
|
||||
pub fn decompress2<T: Seek + ReadBytesExt + Read>(
|
||||
input: &mut T,
|
||||
|
@ -166,10 +162,7 @@ pub fn decompress2<T: Seek + ReadBytesExt + Read>(
|
|||
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();
|
||||
data[0] = 0;
|
||||
let mut dictionary = HashMap::new();
|
||||
|
@ -219,10 +212,7 @@ fn decompress_lzw2(
|
|||
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;
|
||||
if size == 0 {
|
||||
size = 0xFEFD
|
||||
|
@ -243,7 +233,7 @@ pub fn compress(
|
|||
loop {
|
||||
(count, part_data, last) = compress_lzw(&data[offset..], size, last);
|
||||
if count == 0 {
|
||||
break
|
||||
break;
|
||||
}
|
||||
offset += count;
|
||||
|
||||
|
@ -253,7 +243,7 @@ pub fn compress(
|
|||
|
||||
output_info.chunks.push(ChunkInfo {
|
||||
size_compressed: part_data.len(),
|
||||
size_raw: count
|
||||
size_raw: count,
|
||||
});
|
||||
|
||||
output_info.chunk_count += 1;
|
||||
|
@ -271,11 +261,7 @@ pub fn compress(
|
|||
(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 dictionary = HashMap::new();
|
||||
for i in 0..=255 {
|
||||
|
@ -305,7 +291,7 @@ fn compress_lzw(
|
|||
count += 1;
|
||||
|
||||
if size > 0 && compressed.len() == size {
|
||||
break
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -316,21 +302,18 @@ fn compress_lzw(
|
|||
compressed.push(*dictionary.get(&vec![c]).unwrap());
|
||||
}
|
||||
}
|
||||
return (count, compressed, Vec::new())
|
||||
return (count, compressed, Vec::new());
|
||||
} else if compressed.len() < size {
|
||||
if !last_element.is_empty() {
|
||||
compressed.push(*dictionary.get(&last_element).unwrap());
|
||||
}
|
||||
return (count, compressed, Vec::new())
|
||||
return (count, compressed, Vec::new());
|
||||
}
|
||||
|
||||
(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 mut part_data;
|
||||
|
@ -373,11 +356,7 @@ pub fn compress2(
|
|||
(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();
|
||||
if !data.is_empty() {
|
||||
data[0] = 0;
|
||||
|
|
|
@ -1,35 +1,31 @@
|
|||
use byteorder::ReadBytesExt;
|
||||
use rgb::ComponentSlice;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write},
|
||||
path::Path,
|
||||
io::{BufWriter, Read, Seek, SeekFrom, Write},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
color::{apply_palette, get_palette, indexed_gen_palette, rgba_to_indexed, Palette},
|
||||
color::{get_palette, indexed_gen_palette, indexed_to_rgba, rgba_to_indexed, Palette},
|
||||
common::{CommonHeader, CzError, CzVersion, ExtendedHeader},
|
||||
formats::{cz0, cz1, cz2, cz3, cz4},
|
||||
};
|
||||
|
||||
/// A CZ# interface which abstracts the CZ# generic file interface for
|
||||
/// convenience.
|
||||
#[derive(Debug)]
|
||||
/// A CZ# interface which can open and save any CZ file type.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DynamicCz {
|
||||
header_common: CommonHeader,
|
||||
header_extended: Option<ExtendedHeader>,
|
||||
|
||||
/// A palette of RGBA values for indexed color
|
||||
palette: Option<Palette>,
|
||||
|
||||
/// 32bpp RGBA bitmap representation of the file contents
|
||||
bitmap: Vec<u8>,
|
||||
}
|
||||
|
||||
impl DynamicCz {
|
||||
/// 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`]
|
||||
/// Decode a CZ# file from anything that implements [`Read`] and [`Seek`]
|
||||
///
|
||||
/// The input must begin with the
|
||||
/// [magic bytes](https://en.wikipedia.org/wiki/File_format#Magic_number)
|
||||
|
@ -74,12 +70,11 @@ impl DynamicCz {
|
|||
|
||||
match header_common.depth() {
|
||||
4 => {
|
||||
eprintln!("Files with a bit depth of 4 are not yet supported");
|
||||
todo!()
|
||||
todo!("Files with a bit depth of 4 are not yet supported")
|
||||
}
|
||||
8 => {
|
||||
if let Some(palette) = &palette {
|
||||
bitmap = apply_palette(&bitmap, palette)?;
|
||||
bitmap = indexed_to_rgba(&bitmap, palette)?;
|
||||
} else {
|
||||
return Err(CzError::PaletteError);
|
||||
}
|
||||
|
@ -111,20 +106,32 @@ impl DynamicCz {
|
|||
/// Save the `DynamicCz` as a CZ# file. The format saved in is determined
|
||||
/// from the format in the header. Check [`CommonHeader::set_version()`]
|
||||
/// to change the CZ# version.
|
||||
pub fn save_as_cz<T: Into<std::path::PathBuf>>(&self, path: T) -> Result<(), CzError> {
|
||||
let mut out_file = BufWriter::new(File::create(path.into())?);
|
||||
pub fn save_as_cz<P: ?Sized + AsRef<std::path::Path>>(&self, path: &P) -> Result<(), CzError> {
|
||||
let mut out_file = BufWriter::new(File::create(path.as_ref())?);
|
||||
|
||||
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();
|
||||
|
||||
if header.version() == CzVersion::CZ2 {
|
||||
header.set_length(0x12)
|
||||
}
|
||||
header.write_into(&mut out_file)?;
|
||||
header.write_into(&mut output)?;
|
||||
|
||||
if header.version() == CzVersion::CZ2 {
|
||||
// CZ2 files have this odd section instead of an extended header...?
|
||||
out_file.write_all(&[0, 0, 0])?;
|
||||
// TODO: CZ2 files have this odd section instead of an extended header...?
|
||||
output.write_all(&[0, 0, 0])?;
|
||||
} else if let Some(ext) = self.header_extended {
|
||||
ext.write_into(&mut out_file)?;
|
||||
ext.write_into(&mut output)?;
|
||||
}
|
||||
|
||||
let output_bitmap;
|
||||
|
@ -139,8 +146,8 @@ impl DynamicCz {
|
|||
// Use the existing palette to palette the image
|
||||
output_bitmap = rgba_to_indexed(self.bitmap(), pal)?;
|
||||
|
||||
for rgba in &pal.colors {
|
||||
out_file.write_all(&rgba.0)?;
|
||||
for rgba in pal.colors() {
|
||||
output.write_all(rgba.as_slice())?;
|
||||
}
|
||||
} else {
|
||||
// Generate a palette and corresponding indexed bitmap if there is none
|
||||
|
@ -150,12 +157,7 @@ impl DynamicCz {
|
|||
let palette = result.1;
|
||||
|
||||
for rgba in palette {
|
||||
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)?;
|
||||
output.write_all(rgba.as_slice())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -179,11 +181,11 @@ impl DynamicCz {
|
|||
}
|
||||
|
||||
match self.header_common.version() {
|
||||
CzVersion::CZ0 => cz0::encode(&mut out_file, &output_bitmap)?,
|
||||
CzVersion::CZ1 => cz1::encode(&mut out_file, &output_bitmap)?,
|
||||
CzVersion::CZ2 => cz2::encode(&mut out_file, &output_bitmap)?,
|
||||
CzVersion::CZ3 => cz3::encode(&mut out_file, &output_bitmap, &self.header_common)?,
|
||||
CzVersion::CZ4 => cz4::encode(&mut out_file, &output_bitmap, &self.header_common)?,
|
||||
CzVersion::CZ0 => cz0::encode(&mut output, &output_bitmap)?,
|
||||
CzVersion::CZ1 => cz1::encode(&mut output, &output_bitmap)?,
|
||||
CzVersion::CZ2 => cz2::encode(&mut output, &output_bitmap)?,
|
||||
CzVersion::CZ3 => cz3::encode(&mut output, &output_bitmap, &self.header_common)?,
|
||||
CzVersion::CZ4 => cz4::encode(&mut output, &output_bitmap, &self.header_common)?,
|
||||
CzVersion::CZ5 => todo!(),
|
||||
}
|
||||
|
||||
|
@ -196,7 +198,7 @@ impl DynamicCz {
|
|||
/// which is the highest encountered in CZ# files, therefore saving them
|
||||
/// as a PNG of the same or better quality is lossless.
|
||||
#[cfg(feature = "png")]
|
||||
pub fn save_as_png<P: ?Sized + AsRef<Path>>(
|
||||
pub fn save_as_png<P: ?Sized + AsRef<std::path::Path>>(
|
||||
&self,
|
||||
path: &P,
|
||||
) -> Result<(), image::error::EncodingError> {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::common::{CommonHeader, CzError};
|
||||
use crate::compression::{compress, decompress, get_chunk_info};
|
||||
|
@ -12,13 +11,9 @@ pub fn decode<T: Seek + ReadBytesExt + Read>(
|
|||
let block_info = get_chunk_info(bytes)?;
|
||||
bytes.seek(SeekFrom::Start(block_info.length as u64))?;
|
||||
|
||||
let timer = Instant::now();
|
||||
let bitmap = decompress(bytes, &block_info)?;
|
||||
dbg!(timer.elapsed());
|
||||
let data = decompress(bytes, &block_info)?;
|
||||
|
||||
let timer = Instant::now();
|
||||
let bitmap = line_diff(header, &bitmap);
|
||||
dbg!(timer.elapsed());
|
||||
let bitmap = line_diff(header, &data);
|
||||
|
||||
Ok(bitmap)
|
||||
}
|
||||
|
@ -60,9 +55,10 @@ fn line_diff(header: &CommonHeader, data: &[u8]) -> Vec<u8> {
|
|||
curr_line = data[index..index + line_byte_count].to_vec();
|
||||
|
||||
if y % block_height as u32 != 0 {
|
||||
curr_line.iter_mut().zip(&prev_line).for_each(|(curr_p, prev_p)| {
|
||||
*curr_p = curr_p.wrapping_add(*prev_p)
|
||||
});
|
||||
curr_line
|
||||
.iter_mut()
|
||||
.zip(&prev_line)
|
||||
.for_each(|(curr_p, prev_p)| *curr_p = curr_p.wrapping_add(*prev_p));
|
||||
}
|
||||
|
||||
prev_line.clone_from(&curr_line);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::common::{CommonHeader, CzError};
|
||||
use crate::compression::{compress, decompress, get_chunk_info};
|
||||
|
@ -12,15 +11,11 @@ pub fn decode<T: Seek + ReadBytesExt + Read>(
|
|||
let block_info = get_chunk_info(bytes)?;
|
||||
bytes.seek(SeekFrom::Start(block_info.length as u64))?;
|
||||
|
||||
let timer = Instant::now();
|
||||
let data = decompress(bytes, &block_info)?;
|
||||
dbg!(timer.elapsed());
|
||||
|
||||
let timer = Instant::now();
|
||||
let output = line_diff(header, &data);
|
||||
dbg!(timer.elapsed());
|
||||
let bitmap = line_diff(header, &data);
|
||||
|
||||
Ok(output)
|
||||
Ok(bitmap)
|
||||
}
|
||||
|
||||
pub fn encode<T: WriteBytesExt + Write>(
|
||||
|
@ -60,10 +55,16 @@ fn line_diff(header: &CommonHeader, data: &[u8]) -> Vec<u8> {
|
|||
curr_alpha = data[alpha_index..alpha_index + width as usize].to_vec();
|
||||
|
||||
if y % block_height != 0 {
|
||||
curr_line.iter_mut().zip(&prev_line).for_each(|(curr_p, prev_p)| {
|
||||
curr_line
|
||||
.iter_mut()
|
||||
.zip(&prev_line)
|
||||
.for_each(|(curr_p, prev_p)| {
|
||||
*curr_p = curr_p.wrapping_add(*prev_p);
|
||||
});
|
||||
curr_alpha.iter_mut().zip(&prev_alpha).for_each(|(curr_a, prev_a)| {
|
||||
curr_alpha
|
||||
.iter_mut()
|
||||
.zip(&prev_alpha)
|
||||
.for_each(|(curr_a, prev_a)| {
|
||||
*curr_a = curr_a.wrapping_add(*prev_a);
|
||||
});
|
||||
}
|
||||
|
@ -74,12 +75,7 @@ fn line_diff(header: &CommonHeader, data: &[u8]) -> Vec<u8> {
|
|||
.step_by(3)
|
||||
.zip(&curr_alpha)
|
||||
.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);
|
||||
|
@ -111,8 +107,18 @@ fn diff_line(header: &CommonHeader, input: &[u8]) -> Vec<u8> {
|
|||
|
||||
let mut i = 0;
|
||||
for y in 0..height {
|
||||
curr_line = input[i..i + line_byte_count].windows(4).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();
|
||||
curr_line = input[i..i + line_byte_count]
|
||||
.windows(4)
|
||||
.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 {
|
||||
for x in 0..width as usize * 3 {
|
||||
|
|
|
@ -13,6 +13,16 @@ mod formats {
|
|||
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)]
|
||||
pub use dynamic::DynamicCz;
|
||||
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
fn main() {
|
||||
println!("Hello, world!");
|
||||
let mut cz_file = cz::open("test_file.cz3").unwrap();
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
use std::{error::Error, fs::File, io::{BufWriter, Write}, path::Path};
|
||||
use std::{
|
||||
error::Error,
|
||||
fs::File,
|
||||
io::{BufWriter, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
/// A single file entry in a PAK file
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
mod entry;
|
||||
mod header;
|
||||
|
||||
use std::{fs::File, io::{self, BufRead, BufReader, Read, Seek, SeekFrom}, path::{Path, PathBuf}};
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use header::Header;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufRead, BufReader, Read, Seek, SeekFrom},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::entry::Entry;
|
||||
|
@ -61,7 +65,10 @@ impl Pak {
|
|||
}
|
||||
|
||||
/// Decode a PAK file from a byte stream
|
||||
pub fn decode<T: Seek + ReadBytesExt + Read>(input: &mut T, path: PathBuf) -> Result<Self, PakError> {
|
||||
pub fn decode<T: Seek + ReadBytesExt + Read>(
|
||||
input: &mut T,
|
||||
path: PathBuf,
|
||||
) -> Result<Self, PakError> {
|
||||
let mut input = BufReader::new(input);
|
||||
|
||||
// Read in all the header bytes
|
||||
|
@ -93,7 +100,7 @@ impl Pak {
|
|||
dbg!(unknown_pre_data.len());
|
||||
|
||||
if input.stream_position()? == header.data_offset() as u64 {
|
||||
return Err(PakError::HeaderError)
|
||||
return Err(PakError::HeaderError);
|
||||
}
|
||||
|
||||
// Read all the offsets and lengths
|
||||
|
@ -124,7 +131,11 @@ impl Pak {
|
|||
let mut entries: Vec<Entry> = Vec::new();
|
||||
for i in 0..header.entry_count() as usize {
|
||||
// Seek to and read the entry data
|
||||
input.seek(SeekFrom::Start(offsets[i].0 as u64 * header.block_size() as u64)).unwrap();
|
||||
input
|
||||
.seek(SeekFrom::Start(
|
||||
offsets[i].0 as u64 * header.block_size() as u64,
|
||||
))
|
||||
.unwrap();
|
||||
let mut data = vec![0u8; offsets[i].1 as usize];
|
||||
input.read_exact(&mut data).unwrap();
|
||||
|
||||
|
@ -174,8 +185,7 @@ impl Pak {
|
|||
pub fn contains_name(&self, name: String) -> bool {
|
||||
self.entries
|
||||
.iter()
|
||||
.find(|e|
|
||||
e.name.as_ref().is_some_and(|n| n == &name)
|
||||
).is_some()
|
||||
.find(|e| e.name.as_ref().is_some_and(|n| n == &name))
|
||||
.is_some()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use cz::DynamicCz;
|
||||
use std::path::{Path, PathBuf};
|
||||
use clap::{error::ErrorKind, Error, Parser, Subcommand};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "CZ Utils")]
|
||||
|
@ -53,7 +52,7 @@ enum Commands {
|
|||
/// Output CZ file bit depth
|
||||
#[arg(short, long, value_name = "BIT DEPTH")]
|
||||
depth: Option<u16>,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
@ -61,18 +60,34 @@ fn main() {
|
|||
|
||||
// Check what subcommand was run
|
||||
match &cli.command {
|
||||
Commands::Decode { input, output, batch } => {
|
||||
Commands::Decode {
|
||||
input,
|
||||
output,
|
||||
batch,
|
||||
} => {
|
||||
if !input.exists() {
|
||||
Error::raw(ErrorKind::ValueValidation, "The input file/folder provided does not exist\n").exit()
|
||||
Error::raw(
|
||||
ErrorKind::ValueValidation,
|
||||
"The input file/folder provided does not exist\n",
|
||||
)
|
||||
.exit()
|
||||
}
|
||||
|
||||
if *batch {
|
||||
if input.is_file() {
|
||||
Error::raw(ErrorKind::ValueValidation, "Batch input must be a directory\n").exit()
|
||||
Error::raw(
|
||||
ErrorKind::ValueValidation,
|
||||
"Batch input must be a directory\n",
|
||||
)
|
||||
.exit()
|
||||
}
|
||||
|
||||
if output.is_none() || output.as_ref().unwrap().is_file() {
|
||||
Error::raw(ErrorKind::ValueValidation, "Batch output must be a directory\n").exit()
|
||||
Error::raw(
|
||||
ErrorKind::ValueValidation,
|
||||
"Batch output must be a directory\n",
|
||||
)
|
||||
.exit()
|
||||
}
|
||||
|
||||
for entry in walkdir::WalkDir::new(input).max_depth(1) {
|
||||
|
@ -87,21 +102,26 @@ fn main() {
|
|||
let mut final_path = output.clone().unwrap();
|
||||
final_path.push(filename);
|
||||
|
||||
let cz = match DynamicCz::open(&path) {
|
||||
let cz = match cz::open(&path) {
|
||||
Ok(cz) => cz,
|
||||
Err(_) => {
|
||||
Error::raw(
|
||||
ErrorKind::ValueValidation,
|
||||
format!("Could not open input as a CZ file: {}\n", path.into_os_string().to_str().unwrap())
|
||||
).print().unwrap();
|
||||
format!(
|
||||
"Could not open input as a CZ file: {}\n",
|
||||
path.into_os_string().to_str().unwrap()
|
||||
),
|
||||
)
|
||||
.print()
|
||||
.unwrap();
|
||||
continue;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
cz.save_as_png(&final_path).unwrap();
|
||||
}
|
||||
} else {
|
||||
let cz = DynamicCz::open(input).unwrap();
|
||||
let cz = cz::open(input).unwrap();
|
||||
|
||||
if let Some(output) = output {
|
||||
cz.save_as_png(output).unwrap();
|
||||
|
@ -111,27 +131,54 @@ fn main() {
|
|||
}
|
||||
}
|
||||
}
|
||||
Commands::Replace { batch, input, replacement, output, version, depth } => {
|
||||
Commands::Replace {
|
||||
batch,
|
||||
input,
|
||||
replacement,
|
||||
output,
|
||||
version,
|
||||
depth,
|
||||
} => {
|
||||
if !input.exists() {
|
||||
Error::raw(ErrorKind::ValueValidation, "The original file provided does not exist\n").exit()
|
||||
Error::raw(
|
||||
ErrorKind::ValueValidation,
|
||||
"The original file provided does not exist\n",
|
||||
)
|
||||
.exit()
|
||||
}
|
||||
|
||||
if !replacement.exists() {
|
||||
Error::raw(ErrorKind::ValueValidation, "The replacement file provided does not exist\n").exit()
|
||||
Error::raw(
|
||||
ErrorKind::ValueValidation,
|
||||
"The replacement file provided does not exist\n",
|
||||
)
|
||||
.exit()
|
||||
}
|
||||
|
||||
// If it's a batch replacement, we want directories to search
|
||||
if *batch {
|
||||
if !input.is_dir() {
|
||||
Error::raw(ErrorKind::ValueValidation, "Batch input location must be a directory\n").exit()
|
||||
Error::raw(
|
||||
ErrorKind::ValueValidation,
|
||||
"Batch input location must be a directory\n",
|
||||
)
|
||||
.exit()
|
||||
}
|
||||
|
||||
if !replacement.is_dir() {
|
||||
Error::raw(ErrorKind::ValueValidation, "Batch replacement location must be a directory\n").exit()
|
||||
Error::raw(
|
||||
ErrorKind::ValueValidation,
|
||||
"Batch replacement location must be a directory\n",
|
||||
)
|
||||
.exit()
|
||||
}
|
||||
|
||||
if !output.is_dir() {
|
||||
Error::raw(ErrorKind::ValueValidation, "Batch output location must be a directory\n").exit()
|
||||
Error::raw(
|
||||
ErrorKind::ValueValidation,
|
||||
"Batch output location must be a directory\n",
|
||||
)
|
||||
.exit()
|
||||
}
|
||||
|
||||
// Replace all the files within the directory and print errors for them
|
||||
|
@ -141,25 +188,27 @@ fn main() {
|
|||
{
|
||||
let path = entry.unwrap().into_path();
|
||||
if !path.is_file() {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set the replacement image to the same name as the original file
|
||||
let mut final_replacement = replacement.to_path_buf();
|
||||
final_replacement.push(PathBuf::from(path.file_name().unwrap()).with_extension("png"));
|
||||
final_replacement
|
||||
.push(PathBuf::from(path.file_name().unwrap()).with_extension("png"));
|
||||
|
||||
// Set the replacement image to the same name as the original file
|
||||
let mut final_output = output.to_path_buf();
|
||||
final_output.push(path.file_name().unwrap());
|
||||
|
||||
if let Err(error) = replace_cz(
|
||||
&path,
|
||||
&final_output,
|
||||
&final_replacement,
|
||||
version,
|
||||
depth
|
||||
) {
|
||||
Error::raw(ErrorKind::ValueValidation, format!("{:?} - {}\n", path, error)).print().unwrap();
|
||||
if let Err(error) =
|
||||
replace_cz(&path, &final_output, &final_replacement, version, depth)
|
||||
{
|
||||
Error::raw(
|
||||
ErrorKind::ValueValidation,
|
||||
format!("{:?} - {}\n", path, error),
|
||||
)
|
||||
.print()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -172,19 +221,17 @@ fn main() {
|
|||
}
|
||||
|
||||
if !output.is_file() {
|
||||
Error::raw(ErrorKind::ValueValidation, "Replacement output must be a file\n").exit()
|
||||
Error::raw(
|
||||
ErrorKind::ValueValidation,
|
||||
"Replacement output must be a file\n",
|
||||
)
|
||||
.exit()
|
||||
}
|
||||
|
||||
// Replace the input file with the new image
|
||||
replace_cz(
|
||||
&input,
|
||||
&output,
|
||||
&replacement,
|
||||
version,
|
||||
depth
|
||||
).unwrap();
|
||||
replace_cz(&input, &output, &replacement, version, depth).unwrap();
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -198,18 +245,18 @@ fn replace_cz<P: ?Sized + AsRef<Path>>(
|
|||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let path = input_path.as_ref();
|
||||
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() {
|
||||
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
|
||||
let repl_img = image::open(&replacement_path)?.to_rgba8();
|
||||
|
||||
// Open the original CZ file
|
||||
let mut cz = DynamicCz::open(&path)?;
|
||||
let mut cz = cz::open(&path)?;
|
||||
|
||||
// Set CZ header parameters and the new bitmap
|
||||
cz.header_mut().set_width(repl_img.width() as u16);
|
||||
|
|
Loading…
Reference in a new issue