mirror of
https://github.com/G2-Games/lbee-utils.git
synced 2025-04-19 07:12:55 -05:00
348 lines
8.4 KiB
Rust
348 lines
8.4 KiB
Rust
//! Shared types and traits between CZ# files
|
|
|
|
use std::io::{self, Read, Seek, Write};
|
|
|
|
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
|
use thiserror::Error;
|
|
|
|
#[derive(Error, Debug)]
|
|
pub enum CzError {
|
|
#[error("Expected CZ{}, got CZ{}", 0, 1)]
|
|
VersionMismatch(u8, u8),
|
|
|
|
#[error("Could not parse color index palette")]
|
|
PaletteError,
|
|
|
|
#[error("Bitmap size does not match image size")]
|
|
BitmapFormat,
|
|
|
|
#[error("File data is incorrect, it might be corrupt")]
|
|
Corrupt,
|
|
|
|
#[error("File is not a CZ image")]
|
|
NotCzFile,
|
|
|
|
#[error("Failed to read/write input/output")]
|
|
IoError(#[from] io::Error),
|
|
|
|
#[error("Problem while decoding file")]
|
|
DecodeError,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
|
pub enum CzVersion {
|
|
CZ0,
|
|
CZ1,
|
|
CZ2,
|
|
CZ3,
|
|
CZ4,
|
|
CZ5,
|
|
}
|
|
|
|
impl TryFrom<u8> for CzVersion {
|
|
type Error = &'static str;
|
|
|
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
|
let value = match value {
|
|
0 => Self::CZ0,
|
|
1 => Self::CZ1,
|
|
2 => Self::CZ2,
|
|
3 => Self::CZ3,
|
|
4 => Self::CZ4,
|
|
5 => Self::CZ5,
|
|
_ => return Err("Value is not a valid CZ version"),
|
|
};
|
|
|
|
Ok(value)
|
|
}
|
|
}
|
|
|
|
pub trait CzHeader {
|
|
fn new<T: Seek + ReadBytesExt + Read>(bytes: &mut T) -> Result<Self, CzError>
|
|
where
|
|
Self: Sized;
|
|
|
|
/// The [CommonHeader] header from the image
|
|
fn common(&self) -> &CommonHeader;
|
|
|
|
/// Turn the header into bytes equivalent to the original header from the file
|
|
fn write_into<T: Seek + WriteBytesExt + Write>(&self, output: &mut T) -> Result<usize, io::Error>;
|
|
|
|
/// The version of the image
|
|
fn version(&self) -> CzVersion;
|
|
|
|
/// Set the version of the image
|
|
fn set_version(&mut self, version: CzVersion);
|
|
|
|
/// The length of the header in bytes
|
|
fn length(&self) -> usize;
|
|
|
|
/// The width of the image
|
|
fn width(&self) -> u16;
|
|
|
|
/// Set the width of the image
|
|
fn set_width(&mut self, width: u16);
|
|
|
|
/// The height of the image
|
|
fn height(&self) -> u16;
|
|
|
|
/// Set the height of the image
|
|
fn set_height(&mut self, height: u16);
|
|
|
|
/// The bit depth of the image (BPP)
|
|
fn depth(&self) -> u16;
|
|
|
|
fn set_depth(&mut self) {
|
|
unimplemented!()
|
|
}
|
|
|
|
/// An unknown value?
|
|
fn color_block(&self) -> u8;
|
|
}
|
|
|
|
/// The common first part of a header of a CZ# file
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct CommonHeader {
|
|
/// Format version from the magic bytes, (eg. CZ3, CZ4)
|
|
version: CzVersion,
|
|
|
|
/// Length of the header in bytes
|
|
length: u32,
|
|
|
|
/// Width of the image in pixels
|
|
width: u16,
|
|
|
|
/// Height of the image in pixels
|
|
height: u16,
|
|
|
|
/// Bit depth in Bits Per Pixel (BPP)
|
|
depth: u16,
|
|
|
|
/// Color block? This byte's purpose is unclear
|
|
unknown: u8,
|
|
}
|
|
|
|
impl CzHeader for CommonHeader {
|
|
fn new<T: Seek + ReadBytesExt + Read>(bytes: &mut T) -> Result<Self, CzError>
|
|
where
|
|
Self: Sized,
|
|
{
|
|
let mut magic = [0u8; 4];
|
|
bytes.read_exact(&mut magic)?;
|
|
|
|
if magic[0..2] != [b'C', b'Z'] {
|
|
return Err(CzError::NotCzFile);
|
|
}
|
|
|
|
// Ensure the version matches a CZ file type
|
|
let version = match CzVersion::try_from(magic[2] - b'0') {
|
|
Ok(ver) => ver,
|
|
Err(_) => return Err(CzError::NotCzFile),
|
|
};
|
|
|
|
let mut header = Self {
|
|
version,
|
|
length: bytes.read_u32::<LittleEndian>()?,
|
|
width: bytes.read_u16::<LittleEndian>()?,
|
|
height: bytes.read_u16::<LittleEndian>()?,
|
|
depth: bytes.read_u16::<LittleEndian>()?,
|
|
unknown: bytes.read_u8()?,
|
|
};
|
|
|
|
// Lock the color depth to 8 if it's over 32
|
|
// This is obviously wrong, but why is it wrong?
|
|
if header.depth() > 32 {
|
|
header.depth = 8
|
|
}
|
|
|
|
Ok(header)
|
|
}
|
|
|
|
fn common(&self) -> &CommonHeader {
|
|
self
|
|
}
|
|
|
|
fn version(&self) -> CzVersion {
|
|
self.version
|
|
}
|
|
|
|
fn set_version(&mut self, version: CzVersion) {
|
|
self.version = version
|
|
}
|
|
|
|
fn length(&self) -> usize {
|
|
self.length as usize
|
|
}
|
|
|
|
fn width(&self) -> u16 {
|
|
self.width
|
|
}
|
|
|
|
fn set_width(&mut self, width: u16) {
|
|
self.width = width
|
|
}
|
|
|
|
fn height(&self) -> u16 {
|
|
self.height
|
|
}
|
|
|
|
fn set_height(&mut self, height: u16) {
|
|
self.height = height
|
|
}
|
|
|
|
fn depth(&self) -> u16 {
|
|
self.depth
|
|
}
|
|
|
|
fn color_block(&self) -> u8 {
|
|
self.unknown
|
|
}
|
|
|
|
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'];
|
|
|
|
output.write_all(&magic_bytes)?;
|
|
output.write_u32::<LittleEndian>(self.length() as u32)?;
|
|
output.write_u16::<LittleEndian>(self.width())?;
|
|
output.write_u16::<LittleEndian>(self.height())?;
|
|
output.write_u16::<LittleEndian>(self.depth())?;
|
|
output.write_u8(self.color_block())?;
|
|
|
|
Ok((output.stream_position()? - pos) as usize)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct ExtendedHeader {
|
|
/// Unknown bytes
|
|
unknown_1: [u8; 5],
|
|
|
|
/// Width of cropped image area
|
|
pub crop_width: u16,
|
|
|
|
/// Height of cropped image area
|
|
pub crop_height: u16,
|
|
|
|
/// Bounding box width
|
|
pub bounds_width: u16,
|
|
|
|
/// Bounding box height
|
|
pub bounds_height: u16,
|
|
|
|
/// Offset width
|
|
pub offset_width: Option<u16>,
|
|
|
|
/// Offset height
|
|
pub offset_height: Option<u16>,
|
|
|
|
unknown_2: Option<u32>,
|
|
}
|
|
|
|
impl ExtendedHeader {
|
|
pub fn new<T: Seek + ReadBytesExt + Read>(
|
|
input: &mut T,
|
|
common_header: &CommonHeader
|
|
) -> Result<Self, CzError> {
|
|
let mut unknown_1 = [0u8; 5];
|
|
input.read_exact(&mut unknown_1)?;
|
|
|
|
let crop_width = input.read_u16::<LittleEndian>()?;
|
|
let crop_height = input.read_u16::<LittleEndian>()?;
|
|
|
|
let bounds_width = input.read_u16::<LittleEndian>()?;
|
|
let bounds_height = input.read_u16::<LittleEndian>()?;
|
|
|
|
let mut offset_width = None;
|
|
let mut offset_height = None;
|
|
let mut unknown_2 = None;
|
|
if common_header.length() > 28 {
|
|
offset_width = Some(input.read_u16::<LittleEndian>()?);
|
|
offset_height = Some(input.read_u16::<LittleEndian>()?);
|
|
|
|
unknown_2 = Some(input.read_u32::<LittleEndian>()?);
|
|
}
|
|
|
|
Ok(Self {
|
|
unknown_1,
|
|
|
|
crop_width,
|
|
crop_height,
|
|
|
|
bounds_width,
|
|
bounds_height,
|
|
|
|
offset_width,
|
|
offset_height,
|
|
|
|
unknown_2,
|
|
})
|
|
}
|
|
|
|
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_u16::<LittleEndian>(self.crop_width)?;
|
|
output.write_u16::<LittleEndian>(self.crop_height)?;
|
|
output.write_u16::<LittleEndian>(self.bounds_width)?;
|
|
output.write_u16::<LittleEndian>(self.bounds_height)?;
|
|
|
|
if self.offset_width.is_some() {
|
|
output.write_u16::<LittleEndian>(self.offset_width.unwrap())?;
|
|
output.write_u16::<LittleEndian>(self.offset_height.unwrap())?;
|
|
output.write_u32::<LittleEndian>(self.unknown_2.unwrap())?;
|
|
}
|
|
|
|
Ok((output.stream_position()? - pos) as usize)
|
|
}
|
|
}
|
|
|
|
pub fn get_palette<T: Seek + ReadBytesExt + Read>(
|
|
input: &mut T,
|
|
num_colors: usize,
|
|
) -> Result<Vec<[u8; 4]>, CzError> {
|
|
let mut colormap = Vec::with_capacity(num_colors);
|
|
let mut rgba_buf = [0u8; 4];
|
|
|
|
for _ in 0..num_colors {
|
|
input.read_exact(&mut rgba_buf)?;
|
|
colormap.push(rgba_buf);
|
|
}
|
|
|
|
Ok(colormap)
|
|
}
|
|
|
|
pub fn apply_palette(
|
|
input: &[u8],
|
|
palette: &[[u8; 4]]
|
|
) -> Result<Vec<u8>, CzError> {
|
|
let mut output_map = Vec::new();
|
|
|
|
for byte in input.iter() {
|
|
let color = palette.get(*byte as usize);
|
|
if let Some(color) = color {
|
|
output_map.extend_from_slice(color);
|
|
} else {
|
|
return Err(CzError::PaletteError)
|
|
}
|
|
}
|
|
|
|
Ok(output_map)
|
|
}
|
|
|
|
pub fn rgba_to_indexed(input: &[u8], palette: &[[u8; 4]]) -> Result<Vec<u8>, CzError> {
|
|
let mut output_map = Vec::new();
|
|
|
|
for rgba in input.windows(4).step_by(4) {
|
|
dbg!(rgba);
|
|
}
|
|
|
|
Ok(output_map)
|
|
}
|