diff --git a/cz/src/dynamic.rs b/cz/src/dynamic.rs index 5d2dd66..17ca330 100644 --- a/cz/src/dynamic.rs +++ b/cz/src/dynamic.rs @@ -13,7 +13,7 @@ use crate::{ /// A CZ# interface which can open and save any CZ file type. #[derive(Debug, Clone)] -pub struct DynamicCz { +pub struct CzFile { header_common: CommonHeader, header_extended: Option, @@ -24,7 +24,7 @@ pub struct DynamicCz { bitmap: Vec, } -impl DynamicCz { +impl CzFile { /// Decode a CZ# file from anything that implements [`Read`] and [`Seek`] /// /// The input must begin with the diff --git a/cz/src/lib.rs b/cz/src/lib.rs index e89e8b1..5ac0ca0 100644 --- a/cz/src/lib.rs +++ b/cz/src/lib.rs @@ -17,14 +17,14 @@ use common::CzError; use std::{io::BufReader, path::Path}; /// Open a CZ# file from a path -pub fn open>(path: &P) -> Result { +pub fn open>(path: &P) -> Result { let mut img_file = BufReader::new(std::fs::File::open(path)?); - DynamicCz::decode(&mut img_file) + CzFile::decode(&mut img_file) } #[doc(inline)] -pub use dynamic::DynamicCz; +pub use dynamic::CzFile; /* #[doc(inline)] diff --git a/cz/tests/round_trip.rs b/cz/tests/round_trip.rs index 86774ea..967d36f 100644 --- a/cz/tests/round_trip.rs +++ b/cz/tests/round_trip.rs @@ -1,6 +1,6 @@ use std::io::Cursor; -use cz::{common::CzVersion, DynamicCz}; +use cz::{common::CzVersion, CzFile}; const KODIM03: (u16, u16, &[u8]) = (128, 128, include_bytes!("test_images/kodim03.rgba")); const KODIM23: (u16, u16, &[u8]) = (225, 225, include_bytes!("test_images/kodim23.rgba")); @@ -13,13 +13,13 @@ const TEST_IMAGES: &[TestImage] = &[KODIM03, KODIM23, SQPTEXT, DPFLOGO]; #[test] fn cz0_round_trip() { for image in TEST_IMAGES { - let original_cz = DynamicCz::from_raw(CzVersion::CZ0, image.0, image.1, image.2.to_vec()); + let original_cz = CzFile::from_raw(CzVersion::CZ0, image.0, image.1, image.2.to_vec()); let mut cz_bytes = Vec::new(); original_cz.encode(&mut cz_bytes).unwrap(); let mut cz_bytes = Cursor::new(cz_bytes); - let decoded_cz = DynamicCz::decode(&mut cz_bytes).unwrap(); + let decoded_cz = CzFile::decode(&mut cz_bytes).unwrap(); assert_eq!(original_cz.as_raw(), decoded_cz.as_raw()); } @@ -28,13 +28,13 @@ fn cz0_round_trip() { #[test] fn cz1_round_trip() { for image in TEST_IMAGES { - let original_cz = DynamicCz::from_raw(CzVersion::CZ1, image.0, image.1, image.2.to_vec()); + let original_cz = CzFile::from_raw(CzVersion::CZ1, image.0, image.1, image.2.to_vec()); let mut cz_bytes = Vec::new(); original_cz.encode(&mut cz_bytes).unwrap(); let mut cz_bytes = Cursor::new(cz_bytes); - let decoded_cz = DynamicCz::decode(&mut cz_bytes).unwrap(); + let decoded_cz = CzFile::decode(&mut cz_bytes).unwrap(); assert_eq!(original_cz.as_raw(), decoded_cz.as_raw()); } @@ -42,32 +42,29 @@ fn cz1_round_trip() { #[test] fn cz2_round_trip() { - let mut i = 0; for image in TEST_IMAGES { - let original_cz = DynamicCz::from_raw(CzVersion::CZ2, image.0, image.1, image.2.to_vec()); + let original_cz = CzFile::from_raw(CzVersion::CZ2, image.0, image.1, image.2.to_vec()); let mut cz_bytes = Vec::new(); original_cz.encode(&mut cz_bytes).unwrap(); let mut cz_bytes = Cursor::new(cz_bytes); - let decoded_cz = DynamicCz::decode(&mut cz_bytes).unwrap(); + let decoded_cz = CzFile::decode(&mut cz_bytes).unwrap(); assert_eq!(original_cz.as_raw(), decoded_cz.as_raw()); - - i += 1; } } #[test] fn cz3_round_trip() { for image in TEST_IMAGES { - let original_cz = DynamicCz::from_raw(CzVersion::CZ3, image.0, image.1, image.2.to_vec()); + let original_cz = CzFile::from_raw(CzVersion::CZ3, image.0, image.1, image.2.to_vec()); let mut cz_bytes = Vec::new(); original_cz.encode(&mut cz_bytes).unwrap(); let mut cz_bytes = Cursor::new(cz_bytes); - let decoded_cz = DynamicCz::decode(&mut cz_bytes).unwrap(); + let decoded_cz = CzFile::decode(&mut cz_bytes).unwrap(); assert_eq!(original_cz.as_raw(), decoded_cz.as_raw()); } @@ -76,13 +73,13 @@ fn cz3_round_trip() { #[test] fn cz4_round_trip() { for image in TEST_IMAGES { - let original_cz = DynamicCz::from_raw(CzVersion::CZ4, image.0, image.1, image.2.to_vec()); + let original_cz = CzFile::from_raw(CzVersion::CZ4, image.0, image.1, image.2.to_vec()); let mut cz_bytes = Vec::new(); original_cz.encode(&mut cz_bytes).unwrap(); let mut cz_bytes = Cursor::new(cz_bytes); - let decoded_cz = DynamicCz::decode(&mut cz_bytes).unwrap(); + let decoded_cz = CzFile::decode(&mut cz_bytes).unwrap(); assert_eq!(original_cz.as_raw(), decoded_cz.as_raw()); } diff --git a/pak_explorer/src/main.rs b/pak_explorer/src/main.rs index a00da45..0968e3b 100644 --- a/pak_explorer/src/main.rs +++ b/pak_explorer/src/main.rs @@ -156,7 +156,7 @@ impl eframe::App for PakExplorer { .set_file_name(display_name) .save_file() { - let cz = cz::DynamicCz::decode(&mut std::io::Cursor::new( + let cz = cz::CzFile::decode(&mut std::io::Cursor::new( entry.as_bytes(), )) .unwrap(); @@ -175,7 +175,7 @@ impl eframe::App for PakExplorer { let texture: &TextureHandle = self.image_texture.get_or_insert_with(|| { let cz = - cz::DynamicCz::decode(&mut std::io::Cursor::new(entry.as_bytes())) + cz::CzFile::decode(&mut std::io::Cursor::new(entry.as_bytes())) .unwrap(); let image = ColorImage::from_rgba_unmultiplied( [cz.header().width() as usize, cz.header().height() as usize], diff --git a/utils/src/bin/czutil.rs b/utils/src/bin/czutil.rs index 79e6986..e2e93ff 100644 --- a/utils/src/bin/czutil.rs +++ b/utils/src/bin/czutil.rs @@ -1,10 +1,10 @@ use clap::{error::ErrorKind, Error, Parser, Subcommand}; +use cz::{common::CzVersion, CzFile}; use image::ColorType; use lbee_utils::version; +use owo_colors::OwoColorize; use std::{ - fs, - path::{Path, PathBuf}, - process::exit, + ascii::AsciiExt, fs, path::{Path, PathBuf}, process::exit }; /// Utility to maniuplate CZ image files from the LUCA System game engine by @@ -24,7 +24,7 @@ struct Cli { #[derive(Subcommand)] enum Commands { - /// Converts a CZ file to a PNG + /// Decode a CZ file to a PNG Decode { /// Decode a whole folder, and output to another folder #[arg(short, long)] @@ -39,7 +39,26 @@ enum Commands { output: Option, }, - /// Replace a CZ file's image data + /// Encode a PNG file to a CZ + Encode { + /// Input image to encode + #[arg(value_name = "INPUT")] + input: PathBuf, + + /// Output CZ file location + #[arg(value_name = "OUTPUT")] + output: PathBuf, + + /// Output CZ file version + #[arg(short, long, value_name = "CZ VERSION")] + version: Option, + + /// Output CZ file bit depth + #[arg(short, long, value_name = "CZ BIT DEPTH")] + depth: Option, + }, + + /// Replace an existing CZ file's image data Replace { /// Replace a whole folder, and output to another folder, /// using a folder of replacements @@ -265,6 +284,60 @@ fn main() { replace_cz(&input, &output, &replacement, version, depth).unwrap(); } } + Commands::Encode { + input, + output, + version, + depth + } => { + if !input.exists() { + pretty_error("The original file provided does not exist"); + exit(1); + } + if output.exists() { + pretty_error("The output path already exists; not overwriting"); + exit(1); + } + + let version = if let Some(v) = version { + match CzVersion::try_from(*v) { + Ok(v) => v, + Err(_) => { + pretty_error(&format!("Invalid CZ version {}; must be 0, 1, 2, 3, or 4", v)); + exit(1); + }, + } + } else if output.extension().is_some_and(|e| e.to_ascii_lowercase().to_string_lossy().starts_with("cz")) { + let ext_string = output.extension().unwrap().to_string_lossy(); + let last_char = ext_string.chars().last().unwrap(); + match CzVersion::try_from(last_char) { + Ok(v) => v, + Err(e) => { + pretty_error(&format!("Invalid CZ type {}", e)); + exit(1); + }, + } + } else { + pretty_error("CZ version not specified or not parseable from file path"); + exit(1); + }; + + let image = match image::open(input) { + Ok(i) => i.to_rgba8(), + Err(e) => { + pretty_error(&format!("Could not open input file: {e}")); + exit(1); + }, + }; + + let cz = CzFile::from_raw( + version, + image.width() as u16, + image.height() as u16, + image.into_vec() + ); + cz.save_as_cz(output).expect("Saving CZ file failed"); + } } } @@ -310,3 +383,7 @@ fn replace_cz>( Ok(()) } + +fn pretty_error(message: &str) { + eprintln!("{}: {}", "Error".red().italic(), message); +}