diff --git a/crates/jmmt/Cargo.toml b/crates/jmmt/Cargo.toml index ab697d2..30cd1d6 100644 --- a/crates/jmmt/Cargo.toml +++ b/crates/jmmt/Cargo.toml @@ -4,4 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] -binext = "1.0.0" \ No newline at end of file +binext = "1.0.0" +image = "0.24.6" +thiserror = "1.0.40" diff --git a/crates/jmmt/src/format/ps2_texture.rs b/crates/jmmt/src/format/ps2_texture.rs index 377a3e0..58e6666 100644 --- a/crates/jmmt/src/format/ps2_texture.rs +++ b/crates/jmmt/src/format/ps2_texture.rs @@ -29,12 +29,6 @@ pub struct Ps2TextureHeader { impl Ps2TextureHeader { /// 'TEX1' pub const VALID_FOURCC: u32 = 0x31584554; - - fn has_palette(&self) -> bool { - // if the BPP is less than or equal to 8 (I've only seen 8bpp and 16bpp), - // then the texture will be palettized. - self.bpp >= 8 - } } impl Validatable for Ps2TextureHeader { diff --git a/crates/jmmt/src/lib.rs b/crates/jmmt/src/lib.rs index bbb6a41..3bd1fa8 100644 --- a/crates/jmmt/src/lib.rs +++ b/crates/jmmt/src/lib.rs @@ -7,8 +7,10 @@ pub mod hash; pub mod format; pub mod lzss; +pub mod util; + // higher level I/O? -// pub mod read; +pub mod read; // pub mod write; // Maybe, using package.toc? diff --git a/crates/jmmt/src/read/mod.rs b/crates/jmmt/src/read/mod.rs new file mode 100644 index 0000000..0daf88a --- /dev/null +++ b/crates/jmmt/src/read/mod.rs @@ -0,0 +1 @@ +pub mod texture; diff --git a/crates/jmmt/src/read/texture.rs b/crates/jmmt/src/read/texture.rs new file mode 100644 index 0000000..e8d8db7 --- /dev/null +++ b/crates/jmmt/src/read/texture.rs @@ -0,0 +1,225 @@ +//! High-level .ps2_texture reader + +use std::fs::File; +use std::io::{Read, Seek, SeekFrom}; +use std::path::{Path, PathBuf}; + +use binext::BinaryRead; +use thiserror::Error; + +use crate::{ + format::{ps2_palette::*, ps2_texture::*, Validatable}, + util::Ps2Rgba, +}; + +#[derive(Error, Debug)] +pub enum Error { + #[error("underlying I/O error: {0}")] + IoError(#[from] std::io::Error), + + /// Invalid texture/palette file header + #[error("invalid texture/palette file header")] + InvalidHeader, + + /// Couldn't read enough bytes from either texture or palette file + #[error("read too few bytes in texture/palette file to complete texture")] + ReadTooSmall, +} + +pub type Result = std::result::Result; + +/// High-level reader for PS2 textures. +pub struct Ps2TextureReader { + /// The initial path to the .ps2_texture file. + path: PathBuf, + + texture_header: Ps2TextureHeader, + texture_data: Vec, + + has_palette: bool, + palette_header: Ps2PaletteHeader, + palette_data: Vec, +} + +impl Ps2TextureReader { + pub fn new(path: &Path) -> Self { + Ps2TextureReader { + path: path.to_path_buf(), + texture_header: Ps2TextureHeader::default(), + texture_data: Vec::default(), + has_palette: false, + palette_header: Ps2PaletteHeader::default(), + palette_data: Vec::default(), + } + } + + /// Reads texture data into the reader. For most textures, this will mean we open/read + /// two files: the .ps2_texture file, and the .ps2_palette file. + /// + /// This function does not parse or convert data, simply reads it for later conversion. + /// See [Ps2TextureReader::convert_to_image] for the function which does so. + pub fn read_data(&mut self) -> Result<()> { + // Open the .ps2_texture file + let mut texture_file = File::open(self.path.clone())?; + + self.texture_header = texture_file.read_binary::()?; + + if self.texture_header.valid() { + let texture_data_size: usize = ((self.texture_header.width as u32 + * self.texture_header.height as u32) + * (self.texture_header.bpp / 8) as u32) as usize; + + match self.texture_header.bpp { + 8 => { + self.has_palette = true; + } + _ => {} + } + + if self.has_palette { + let mut palette_path = self.path.clone(); + + // A lot of textures use the texture_.b pattern. + // the palette for these is in a palette_.b file, so we need to handle that specifically. + if let Some(ext) = self.path.extension() { + if ext == "b" { + match self.path.file_name() { + Some(name) => { + if let Some(name) = name.to_str() { + palette_path.set_file_name( + String::from(name).replace("texture_", "palette_"), + ); + } else { + // this is a bad error to return here, but for now it works I guess + return Err(Error::InvalidHeader); + } + } + None => { + // ditto + return Err(Error::InvalidHeader); + } + } + } else { + // We can assume this came from a regular .ps2_texture file, and just set the extension. + palette_path.set_extension("ps2_palette"); + } + } + + let mut palette_file = File::open(palette_path)?; + self.palette_header = palette_file.read_binary::()?; + + if self.palette_header.valid() { + // There are no known files in JMMT which use a non-32bpp palette, so I consider this, + // while hacky, a "ok" assumption. if this isn't actually true then Oh Well + let pal_data_size = (self.palette_header.color_count * 4) as usize; + palette_file.seek(SeekFrom::Start(self.palette_header.data_start as u64))?; + + // Read palette color data + self.palette_data.resize(pal_data_size, 0x0); + if palette_file.read(self.palette_data.as_mut_slice())? != pal_data_size { + return Err(Error::ReadTooSmall); + } + } else { + // I give up + return Err(Error::InvalidHeader); + } + } + + texture_file.seek(SeekFrom::Start( + self.texture_header.data_start_offset as u64, + ))?; + self.texture_data.resize(texture_data_size, 0x0); + + if texture_file.read(self.texture_data.as_mut_slice())? != texture_data_size { + return Err(Error::ReadTooSmall); + } + } else { + return Err(Error::InvalidHeader); + } + Ok(()) + } + + fn palette_rgba_slice(&self) -> &[Ps2Rgba] { + assert_eq!( + self.palette_header.palette_bpp, 32, + "Palette BPP invalid for usage with palette_rgba_slice" + ); + + // Safety: The palette data buffer will always be 32-bits aligned, so the newly created slice + // will not end up reading any memory out of bounds of the Vec allocation. + // We assert this as true before returning. + return unsafe { + std::slice::from_raw_parts( + self.palette_data.as_ptr() as *const Ps2Rgba, + self.palette_header.color_count as usize, + ) + }; + } + + /// Convert the read data into a image implemented by the [image] crate. + pub fn convert_to_image(&self) -> image::RgbaImage { + let mut image = image::RgbaImage::new( + self.texture_header.width as u32, + self.texture_header.height as u32, + ); + + if self.has_palette { + let palette_slice: &[Ps2Rgba] = self.palette_rgba_slice(); + + // this is shoddy and slow, but meh + for x in 0..self.texture_header.width as usize { + for y in 0..self.texture_header.height as usize { + image[(x as u32, y as u32)] = palette_slice + [self.texture_data[y * self.texture_header.width as usize + x] as usize] + .to_rgba(); + } + } + } else { + match self.texture_header.bpp { + 16 => { + let rgb565_slice = unsafe { + std::slice::from_raw_parts( + self.texture_data.as_ptr() as *const u16, + (self.texture_header.width as u32 * self.texture_header.height as u32) + as usize, + ) + }; + + for x in 0..self.texture_header.width as usize { + for y in 0..self.texture_header.height as usize { + image[(x as u32, y as u32)] = Ps2Rgba::from_rgb565( + rgb565_slice[y * self.texture_header.width as usize + x], + ) + .to_rgba(); + } + } + } + + // TODO: Find some less awful way to do this, that can be done without creating a new image object. + 32 => { + let rgba_slice = unsafe { + std::slice::from_raw_parts( + self.texture_data.as_ptr() as *const Ps2Rgba, + (self.texture_header.width as u32 * self.texture_header.height as u32) + as usize, + ) + }; + + for x in 0..self.texture_header.width as usize { + for y in 0..self.texture_header.height as usize { + image[(x as u32, y as u32)] = + rgba_slice[y * self.texture_header.width as usize + x].to_rgba(); + } + } + } + + _ => panic!( + "somehow got here with invalid bpp {}", + self.texture_header.bpp + ), + } + } + + image + } +} diff --git a/crates/jmmt/src/util/mod.rs b/crates/jmmt/src/util/mod.rs new file mode 100644 index 0000000..fce52c2 --- /dev/null +++ b/crates/jmmt/src/util/mod.rs @@ -0,0 +1,33 @@ +//! General utility code, used throughout the JMMT crate + +/// Like image::Rgba, but a safe C repressentation, +/// and alpha is multiplied to match PS2. Some helpers +/// are also provided to work with 16-bit colors. +#[derive(Clone)] +#[repr(C, packed)] +pub struct Ps2Rgba { + pub r: u8, + pub g: u8, + pub b: u8, + pub a: u8, +} + +impl Ps2Rgba { + pub const fn to_rgba(&self) -> image::Rgba { + // avoid multiplication overflow + if self.a as u32 * 2 > 255 { + return image::Rgba::([self.r, self.g, self.b, 255]); + } + image::Rgba::([self.r, self.g, self.b, self.a * 2]) + } + + /// Create a instance from an rgb565 16bpp pixel. + pub const fn from_rgb565(value: u16) -> Ps2Rgba { + return Ps2Rgba { + r: ((value & 0x7C00) >> 7) as u8, + g: ((value & 0x03E0) >> 2) as u8, + b: ((value & 0x001F) << 3) as u8, + a: 255 + }; + } +} diff --git a/crates/textool/Cargo.toml b/crates/textool/Cargo.toml index b3d9a47..39a3f3d 100644 --- a/crates/textool/Cargo.toml +++ b/crates/textool/Cargo.toml @@ -5,8 +5,4 @@ edition = "2021" [dependencies] clap = { version = "4.3.8", features = ["derive"] } -image = "0.24.6" jmmt = { path = "../jmmt" } - -# Temporary, until the reader code is thrown into jmmt crate -binext = "1.0.0" diff --git a/crates/textool/src/main.rs b/crates/textool/src/main.rs index 998ce74..f3b4d0b 100644 --- a/crates/textool/src/main.rs +++ b/crates/textool/src/main.rs @@ -1,15 +1,7 @@ use clap::{Parser, Subcommand}; -use std::default; use std::path::Path; -use std::fs::{ - File -}; - -use jmmt::format::{ - ps2_palette::*, - ps2_texture::* -}; +use jmmt::read::texture::Ps2TextureReader; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -23,43 +15,42 @@ struct Cli { enum Commands { /// Exports the texture to a .png file Export { path: String }, - //Import } -struct ImageReader { - file: File, - header: Ps2TextureHeader, - pal_header: Ps2PaletteHeader, - - image_data: Vec, - palette_data: Vec -} - -impl ImageReader { - fn new(file: &mut File) -> Self { - ImageReader { - file: file.try_clone().unwrap(), - header: Ps2TextureHeader::default(), - pal_header: Ps2PaletteHeader::default(), - image_data: Vec::default(), - palette_data: Vec::default() - } - } - - fn read(&mut self) -> std::io::Result<()> { - - Ok(()) - } -} - fn main() { let cli = Cli::parse(); match &cli.command { Commands::Export { path } => { - println!("exporting {}", path); let path = Path::new(path); + + if !path.is_file() { + println!("Need to provide a path to a file to export."); + return (); + } + + let mut reader = Ps2TextureReader::new(path); + + match reader.read_data() { + Ok(_) => { + let mut path = Path::new(path).to_path_buf(); + path.set_extension("png"); + + match reader.convert_to_image().save(path.clone()) { + Ok(_) => { + println!("Wrote image {}", path.display()) + } + Err(error) => { + println!("Error saving image: {}", error); + } + }; + } + Err(error) => { + println!("Error reading texture data: {}", error); + return (); + } + } } } }