jmmt: Textool now works

Most of the POC reader code was moved into jmmt crate
This commit is contained in:
Lily Tsuru 2023-06-28 00:54:06 -04:00
parent 69e82236c6
commit 11abf7668a
8 changed files with 293 additions and 49 deletions

View File

@ -5,3 +5,5 @@ edition = "2021"
[dependencies] [dependencies]
binext = "1.0.0" binext = "1.0.0"
image = "0.24.6"
thiserror = "1.0.40"

View File

@ -29,12 +29,6 @@ pub struct Ps2TextureHeader {
impl Ps2TextureHeader { impl Ps2TextureHeader {
/// 'TEX1' /// 'TEX1'
pub const VALID_FOURCC: u32 = 0x31584554; 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 { impl Validatable for Ps2TextureHeader {

View File

@ -7,8 +7,10 @@ pub mod hash;
pub mod format; pub mod format;
pub mod lzss; pub mod lzss;
pub mod util;
// higher level I/O? // higher level I/O?
// pub mod read; pub mod read;
// pub mod write; // pub mod write;
// Maybe, using package.toc? // Maybe, using package.toc?

View File

@ -0,0 +1 @@
pub mod texture;

View File

@ -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<T> = std::result::Result<T, Error>;
/// 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<u8>,
has_palette: bool,
palette_header: Ps2PaletteHeader,
palette_data: Vec<u8>,
}
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::<Ps2TextureHeader>()?;
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_<name>.b pattern.
// the palette for these is in a palette_<name>.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::<Ps2PaletteHeader>()?;
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<u8> 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
}
}

View File

@ -0,0 +1,33 @@
//! General utility code, used throughout the JMMT crate
/// Like image::Rgba<u8>, 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<u8> {
// avoid multiplication overflow
if self.a as u32 * 2 > 255 {
return image::Rgba::<u8>([self.r, self.g, self.b, 255]);
}
image::Rgba::<u8>([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
};
}
}

View File

@ -5,8 +5,4 @@ edition = "2021"
[dependencies] [dependencies]
clap = { version = "4.3.8", features = ["derive"] } clap = { version = "4.3.8", features = ["derive"] }
image = "0.24.6"
jmmt = { path = "../jmmt" } jmmt = { path = "../jmmt" }
# Temporary, until the reader code is thrown into jmmt crate
binext = "1.0.0"

View File

@ -1,15 +1,7 @@
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use std::default;
use std::path::Path; use std::path::Path;
use std::fs::{ use jmmt::read::texture::Ps2TextureReader;
File
};
use jmmt::format::{
ps2_palette::*,
ps2_texture::*
};
#[derive(Parser)] #[derive(Parser)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
@ -23,43 +15,42 @@ struct Cli {
enum Commands { enum Commands {
/// Exports the texture to a .png file /// Exports the texture to a .png file
Export { path: String }, Export { path: String },
//Import //Import
} }
struct ImageReader {
file: File,
header: Ps2TextureHeader,
pal_header: Ps2PaletteHeader,
image_data: Vec<u8>,
palette_data: Vec<u8>
}
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() { fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
match &cli.command { match &cli.command {
Commands::Export { path } => { Commands::Export { path } => {
println!("exporting {}", path);
let path = Path::new(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 ();
}
}
} }
} }
} }