Rename fconv to pxfconv
This commit is contained in:
15
src/Tools/psxfileconv/Cargo.toml
Normal file
15
src/Tools/psxfileconv/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "psxfileconv"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
||||
[dependencies]
|
||||
clap = {version = "4.4.11", features = ["derive"]}
|
||||
image = "0.24.7"
|
||||
hound = "3.5.1"
|
||||
paste = "1.0.14"
|
||||
png = "0.17.10"
|
||||
tool_helper = {path = "../tool_helper"}
|
||||
13
src/Tools/psxfileconv/Makefile
Normal file
13
src/Tools/psxfileconv/Makefile
Normal file
@@ -0,0 +1,13 @@
|
||||
include ../Common.mk
|
||||
|
||||
ARTIFACT = psxfileconv
|
||||
|
||||
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
|
||||
$(WINDOWS_ARTIFACT):
|
||||
$(call cargo_windows_default)
|
||||
|
||||
$(UNIX_ARTIFACT):
|
||||
$(call cargo_unix_default)
|
||||
|
||||
all-windows: $(WINDOWS_ARTIFACT)
|
||||
all: $(UNIX_ARTIFACT)
|
||||
44
src/Tools/psxfileconv/src/audio/mod.rs
Normal file
44
src/Tools/psxfileconv/src/audio/mod.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
pub mod my_vag;
|
||||
pub mod vag;
|
||||
pub mod xa;
|
||||
|
||||
use std::{env, path::PathBuf, process::Command};
|
||||
use tool_helper::Error;
|
||||
|
||||
fn run_psxavenc<I, S>(input: PathBuf, output: PathBuf, args: I) -> Result<(), Error>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<std::ffi::OsStr>, {
|
||||
let psxavenc = get_psxavenc_path()?;
|
||||
let result = Command::new(psxavenc).args(args).arg(input.to_string_lossy().as_ref()).arg(output.to_string_lossy().as_ref()).output()?;
|
||||
|
||||
let stderr = tool_helper::vec_helper::to_string(result.stderr)?;
|
||||
let stdout = tool_helper::vec_helper::to_string(result.stdout)?;
|
||||
|
||||
if !result.status.success() {
|
||||
return Err(Error::from_text(format!("psxavenc returned: {}. {}", result.status, stderr)));
|
||||
}
|
||||
|
||||
if !stdout.is_empty() {
|
||||
println!("{}", stdout);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_psxavenc_path() -> Result<PathBuf, Error> {
|
||||
let tool_path = {
|
||||
let mut my_path = env::current_exe()?;
|
||||
|
||||
my_path.pop();
|
||||
my_path.push("extern");
|
||||
my_path.push("psxavenc");
|
||||
|
||||
my_path
|
||||
};
|
||||
|
||||
if !tool_path.exists() {
|
||||
return Err(Error::from_str("Could not locate psxavenc. Make sure it is installed under \"<Jaby Engine>/bin/extern\""));
|
||||
}
|
||||
|
||||
Ok(tool_path)
|
||||
}
|
||||
35
src/Tools/psxfileconv/src/audio/my_vag/mod.rs
Normal file
35
src/Tools/psxfileconv/src/audio/my_vag/mod.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
pub mod types;
|
||||
|
||||
use std::io::Write;
|
||||
use tool_helper::{Error, Input};
|
||||
use types::MonoADPCMIterator;
|
||||
|
||||
pub fn convert(input: Input, _output: &mut dyn Write) -> Result<(), Error> {
|
||||
let mut wav_file = hound::WavReader::new(input)?;
|
||||
let wav_header = wav_file.spec();
|
||||
|
||||
validate(&wav_header)?;
|
||||
let mut sample_count = 0;
|
||||
for _adpcm_samples in MonoADPCMIterator::create(wav_file.samples::<i16>()) {
|
||||
sample_count += 1;
|
||||
}
|
||||
|
||||
println!("Parsed {} vag samples", sample_count);
|
||||
Err(Error::not_implemented("my vag convert"))
|
||||
}
|
||||
|
||||
fn validate(wav_header: &hound::WavSpec) -> Result<(), Error> {
|
||||
if wav_header.sample_format != hound::SampleFormat::Int {
|
||||
return Err(Error::from_str("VAG: Only integer samples are supported as input."));
|
||||
}
|
||||
|
||||
if wav_header.bits_per_sample != 16 {
|
||||
return Err(Error::from_str("VAG: Only 16bits samples are currently supported as input."));
|
||||
}
|
||||
|
||||
if wav_header.channels != 1 {
|
||||
return Err(Error::from_str("VAG: Only mono samples are currently supported"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
45
src/Tools/psxfileconv/src/audio/my_vag/types.rs
Normal file
45
src/Tools/psxfileconv/src/audio/my_vag/types.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use tool_helper::Error;
|
||||
|
||||
pub struct VAGADPCM {
|
||||
data: [u32; 4]
|
||||
}
|
||||
|
||||
impl VAGADPCM {
|
||||
const ADPCM_SAMPLES_PER_VAGADPCM:usize = 28;
|
||||
|
||||
pub fn create() -> VAGADPCM {
|
||||
VAGADPCM{data: [0; 4]}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MonoADPCMIterator<I: std::iter::Iterator<Item=Result<i16, hound::Error>>> {
|
||||
iter: I
|
||||
}
|
||||
|
||||
impl<I:std::iter::Iterator<Item=Result<i16, hound::Error>>> MonoADPCMIterator<I>{
|
||||
pub fn create(iter: I) -> MonoADPCMIterator<I> {
|
||||
MonoADPCMIterator{iter}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I:std::iter::Iterator<Item=Result<i16, hound::Error>>> std::iter::Iterator for MonoADPCMIterator<I> {
|
||||
type Item = Result<[i16; VAGADPCM::ADPCM_SAMPLES_PER_VAGADPCM], Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
const STREAM_GONE_ERROR: &'static str = "Reading ADPCM sample failed";
|
||||
|
||||
if let Some(next_sample) = self.iter.next() {
|
||||
let Ok(next_sample) = next_sample else {return Some(Err(Error::from_str(STREAM_GONE_ERROR)));};
|
||||
|
||||
let mut sample = [0;VAGADPCM::ADPCM_SAMPLES_PER_VAGADPCM];
|
||||
sample[0] = next_sample;
|
||||
|
||||
for idx in 1..VAGADPCM::ADPCM_SAMPLES_PER_VAGADPCM {
|
||||
let Ok(next_sample) = self.iter.next().unwrap_or(Ok(0)) else {return Some(Err(Error::from_str(STREAM_GONE_ERROR)));};
|
||||
sample[idx] = next_sample;
|
||||
}
|
||||
return Some(Ok(sample));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
22
src/Tools/psxfileconv/src/audio/vag/mod.rs
Normal file
22
src/Tools/psxfileconv/src/audio/vag/mod.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use clap::Args;
|
||||
use std::path::PathBuf;
|
||||
use tool_helper::Error;
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct Arguments {
|
||||
frequency: Option<u32>
|
||||
}
|
||||
|
||||
pub fn convert(args: Arguments, input: PathBuf, output: PathBuf) -> Result<(), Error> {
|
||||
let mut cmd_args = vec!["-t", "vag"];
|
||||
let frequency_str;
|
||||
|
||||
if let Some(frequency) = args.frequency {
|
||||
frequency_str = frequency.to_string().to_owned();
|
||||
|
||||
cmd_args.push("-f");
|
||||
cmd_args.push(frequency_str.as_ref());
|
||||
}
|
||||
|
||||
super::run_psxavenc(input, output, cmd_args)
|
||||
}
|
||||
55
src/Tools/psxfileconv/src/audio/xa/mod.rs
Normal file
55
src/Tools/psxfileconv/src/audio/xa/mod.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use clap::{Args, ValueEnum};
|
||||
use std::{path::PathBuf, str};
|
||||
use tool_helper::Error;
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct Arguments {
|
||||
#[clap(value_enum, value_parser, default_value_t=Quality::High)]
|
||||
quality: Quality,
|
||||
#[clap(value_enum, value_parser, default_value_t=Sample::Stereo)]
|
||||
sample: Sample
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, ValueEnum)]
|
||||
pub enum Quality {
|
||||
Low,
|
||||
High
|
||||
}
|
||||
|
||||
impl Quality {
|
||||
fn get_frequency(&self) -> u64 {
|
||||
match self {
|
||||
Self::Low => 18900,
|
||||
Self::High => 37800,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, ValueEnum)]
|
||||
pub enum Sample {
|
||||
Mono,
|
||||
Stereo
|
||||
}
|
||||
|
||||
impl Sample {
|
||||
fn get_channel(&self) -> u64 {
|
||||
match self {
|
||||
Self::Mono => 1,
|
||||
Self::Stereo => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert(args: Arguments, input: PathBuf, output: PathBuf) -> Result<(), Error> {
|
||||
let quality = args.quality;
|
||||
let sample = args.sample;
|
||||
|
||||
super::run_psxavenc(input, output, [
|
||||
"-t", "xacd",
|
||||
"-f", quality.get_frequency().to_string().as_ref(),
|
||||
"-b", "4",
|
||||
"-c", sample.get_channel().to_string().as_ref(),
|
||||
"-F", "0",
|
||||
"-C", "0",
|
||||
])
|
||||
}
|
||||
1
src/Tools/psxfileconv/src/images/mod.rs
Normal file
1
src/Tools/psxfileconv/src/images/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod reduced_tim;
|
||||
141
src/Tools/psxfileconv/src/images/reduced_tim/color_clut.rs
Normal file
141
src/Tools/psxfileconv/src/images/reduced_tim/color_clut.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use tool_helper::Error;
|
||||
use super::types::{Color, PSXImageConverter};
|
||||
|
||||
pub enum OutputType {
|
||||
FourBit,
|
||||
EightBit,
|
||||
}
|
||||
|
||||
enum IndexPerByte {
|
||||
OneIndex,
|
||||
TwoIndices,
|
||||
}
|
||||
|
||||
pub struct IndexedImage {
|
||||
pub palette: Vec<Color>,
|
||||
raw_data: std::vec::IntoIter<u8>,
|
||||
index_byte: IndexPerByte,
|
||||
next_func: fn(&mut Self) -> Option<Color>,
|
||||
width: u16,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
impl IndexedImage {
|
||||
pub fn new<R: std::io::Read>(mut reader: png::Reader<R>, output_type: OutputType) -> Result<IndexedImage, Error> {
|
||||
let action_name = "Creating IndexedImage";
|
||||
|
||||
let mut buffer = vec![0; reader.output_buffer_size()];
|
||||
match reader.next_frame(&mut buffer) {
|
||||
Ok(info) => {
|
||||
let width = info.width as u16;
|
||||
let height = info.height as u16;
|
||||
let index_byte = {
|
||||
match info.bit_depth {
|
||||
png::BitDepth::Four => IndexPerByte::TwoIndices,
|
||||
png::BitDepth::Eight => IndexPerByte::OneIndex,
|
||||
_ => {
|
||||
return Err(Error::from_text(format!("{}: Only 4 and 8bit color depth are supported", action_name)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if info.color_type != png::ColorType::Indexed {
|
||||
return Err(Error::from_text(format!("{}: PNG file must be indexed", action_name)));
|
||||
}
|
||||
|
||||
else {
|
||||
Ok(IndexedImage{palette: Self::make_palette(reader.info()), raw_data: buffer.into_iter(), index_byte, next_func: {
|
||||
match output_type {
|
||||
OutputType::FourBit => Self::next_four_bit,
|
||||
OutputType::EightBit => Self::next_eight_bit,
|
||||
}
|
||||
}, width, height})
|
||||
}
|
||||
}
|
||||
|
||||
Err(err) => {
|
||||
Err(Error::from_text(format!("{}: {}", action_name, err.to_string())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_palette(png_info: &png::Info) -> Vec<Color> {
|
||||
fn make_color(iter: &mut dyn std::iter::Iterator<Item = &u8>) -> Option<Color> {
|
||||
let r = iter.next()?;
|
||||
let g = iter.next()?;
|
||||
let b = iter.next()?;
|
||||
|
||||
Some(Color::non_transparent(*r, *g, *b))
|
||||
}
|
||||
|
||||
let mut new_palette = Vec::new();
|
||||
|
||||
if let Some(palette) = &png_info.palette {
|
||||
let mut iter = palette.iter();
|
||||
|
||||
while let Some(color) = make_color(&mut iter) {
|
||||
new_palette.push(color);
|
||||
}
|
||||
}
|
||||
new_palette
|
||||
}
|
||||
|
||||
fn split_into_two_indices(index: u8) -> (u8, u8) {
|
||||
(index >> 4, index & 0xF)
|
||||
}
|
||||
|
||||
fn next_four_bit(&mut self) -> Option<Color> {
|
||||
let (pixel0, pixel1, pixel2, pixel3) = {
|
||||
match self.index_byte {
|
||||
IndexPerByte::OneIndex => {
|
||||
(self.raw_data.next()?, self.raw_data.next()?, self.raw_data.next()?, self.raw_data.next()?)
|
||||
}
|
||||
IndexPerByte::TwoIndices => {
|
||||
let (pixel0, pixel1) = Self::split_into_two_indices(self.raw_data.next()?);
|
||||
let (pixel2, pixel3) = Self::split_into_two_indices(self.raw_data.next()?);
|
||||
|
||||
(pixel0, pixel1, pixel2, pixel3)
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
Some(Color::new_4bit_entry(pixel0, pixel1, pixel2, pixel3))
|
||||
}
|
||||
|
||||
fn next_eight_bit(&mut self) -> Option<Color> {
|
||||
let (pixel0, pixel1) = {
|
||||
match self.index_byte {
|
||||
IndexPerByte::OneIndex => {
|
||||
(self.raw_data.next()?, self.raw_data.next()?)
|
||||
},
|
||||
IndexPerByte::TwoIndices => {
|
||||
Self::split_into_two_indices(self.raw_data.next()?)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Some(Color::new_8bit_entry(pixel0, pixel1))
|
||||
}
|
||||
}
|
||||
|
||||
impl PSXImageConverter for IndexedImage {
|
||||
fn width(&self) -> u16 {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> u16 {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn get_palette(&self) -> Option<&Vec<Color>> {
|
||||
Some(&self.palette)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::Iterator for IndexedImage {
|
||||
type Item = Color;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
(self.next_func)(self)
|
||||
}
|
||||
}
|
||||
92
src/Tools/psxfileconv/src/images/reduced_tim/color_full16.rs
Normal file
92
src/Tools/psxfileconv/src/images/reduced_tim/color_full16.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use super::types::{Color, PSXImageConverter};
|
||||
|
||||
pub struct RgbImage {
|
||||
raw_data: std::vec::IntoIter<u8>,
|
||||
width: u16,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
impl RgbImage {
|
||||
pub fn new(image: image::RgbImage) -> RgbImage {
|
||||
let width = image.width() as u16;
|
||||
let height = image.height() as u16;
|
||||
let raw_data = image.into_raw().into_iter();
|
||||
|
||||
RgbImage{raw_data, width, height}
|
||||
}
|
||||
}
|
||||
|
||||
impl PSXImageConverter for RgbImage {
|
||||
fn width(&self) -> u16 {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> u16 {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn get_palette(&self) -> Option<&Vec<Color>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::Iterator for RgbImage {
|
||||
type Item = Color;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let r = self.raw_data.next()?;
|
||||
let g = self.raw_data.next()?;
|
||||
let b = self.raw_data.next()?;
|
||||
|
||||
Some(Color::non_transparent(r, g, b))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RgbaImage {
|
||||
raw_data: std::vec::IntoIter<u8>,
|
||||
width: u16,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
impl RgbaImage {
|
||||
pub fn new(image: image::RgbaImage) -> RgbaImage {
|
||||
let width = image.width() as u16;
|
||||
let height = image.height() as u16;
|
||||
let raw_data = image.into_raw().into_iter();
|
||||
|
||||
RgbaImage{raw_data, width, height}
|
||||
}
|
||||
}
|
||||
|
||||
impl PSXImageConverter for RgbaImage {
|
||||
fn width(&self) -> u16 {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> u16 {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn get_palette(&self) -> Option<&Vec<Color>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::Iterator for RgbaImage {
|
||||
type Item = Color;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let r = self.raw_data.next()?;
|
||||
let g = self.raw_data.next()?;
|
||||
let b = self.raw_data.next()?;
|
||||
let a = self.raw_data.next()?;
|
||||
|
||||
Some(
|
||||
match a {
|
||||
0x0 => Color::transparent(),
|
||||
0xFF => Color::non_transparent(r, g, b),
|
||||
_ => Color::semi_transparent(r, g, b),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
180
src/Tools/psxfileconv/src/images/reduced_tim/mod.rs
Normal file
180
src/Tools/psxfileconv/src/images/reduced_tim/mod.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
use clap::{Args, ValueEnum};
|
||||
use image::{DynamicImage, io::Reader as ImageReader};
|
||||
use color_clut::IndexedImage;
|
||||
use color_full16::{RgbaImage, RgbImage};
|
||||
use std::io::{Cursor, Write};
|
||||
use tool_helper::{Error, Input};
|
||||
use types::{Header, Color as PSXColor, PSXImageConverter};
|
||||
|
||||
mod types;
|
||||
mod color_clut;
|
||||
mod color_full16;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
pub enum ColorType{
|
||||
Clut4,
|
||||
Clut8,
|
||||
Full16,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
pub enum ClutAlignment {
|
||||
None,
|
||||
Linear,
|
||||
Block
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct Arguments {
|
||||
#[clap(value_enum, value_parser)]
|
||||
color_depth: ColorType,
|
||||
|
||||
#[clap(value_enum, value_parser, default_value_t=ClutAlignment::None)]
|
||||
clut_align: ClutAlignment,
|
||||
|
||||
#[clap(long="semi-trans", default_value_t=false)]
|
||||
semi_transparent: bool,
|
||||
#[clap(long="color-trans", default_value_t=false)]
|
||||
transparent_palette: bool
|
||||
}
|
||||
|
||||
fn modify_palette(mut image: IndexedImage, clut_align: ClutAlignment, semi_transparent: bool, transparent_palette: bool) -> IndexedImage {
|
||||
if semi_transparent {
|
||||
for color in image.palette.iter_mut() {
|
||||
*color = PSXColor::semi_transparent(color.get_red(), color.get_green(), color.get_blue());
|
||||
}
|
||||
}
|
||||
|
||||
if transparent_palette {
|
||||
if clut_align == ClutAlignment::Block {
|
||||
for color in image.palette.iter_mut().step_by(16) {
|
||||
*color = PSXColor::transparent();
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
if let Some(first_color) = image.palette.get_mut(0) {
|
||||
*first_color = PSXColor::transparent();
|
||||
}
|
||||
}
|
||||
}
|
||||
image
|
||||
}
|
||||
|
||||
fn encode<T: PSXImageConverter>(image: T, color_depth: ColorType, clut_align: ClutAlignment, output: &mut dyn Write) -> Result<(), Error> {
|
||||
let (width, height) = {
|
||||
fn return_error(clut_type: u32, div: u32, width: u16, height: u16) -> Result<(u16, u16), Error> {
|
||||
return Err(Error::from_callback(|| {format!("CLUT {} images require a width divideable by {} (found width: {}/{}={}, height: {})", clut_type, div, width, div, (width as f32/div as f32), height)}));
|
||||
}
|
||||
|
||||
let width = image.width();
|
||||
let height = image.height();
|
||||
match color_depth {
|
||||
ColorType::Clut4 => {
|
||||
if width & 3 == 0 {
|
||||
Ok((width/4, height))
|
||||
}
|
||||
|
||||
else {
|
||||
return_error(4, 4, width, height)
|
||||
}
|
||||
},
|
||||
ColorType::Clut8 => {
|
||||
if width & 1 == 0 {
|
||||
Ok((width/2, height))
|
||||
}
|
||||
|
||||
else {
|
||||
return_error(8, 2, width, height)
|
||||
}
|
||||
},
|
||||
ColorType::Full16 => {
|
||||
Ok((width, height))
|
||||
}
|
||||
}
|
||||
}?;
|
||||
let palette = image.get_palette();
|
||||
let (pal_width, pal_height) = {
|
||||
if let Some(palette) = &palette {
|
||||
let pal_length_adjusted = {
|
||||
let pal_length = palette.len();
|
||||
if pal_length <= 16 {
|
||||
16u16
|
||||
}
|
||||
|
||||
else {
|
||||
256u16
|
||||
}
|
||||
};
|
||||
match clut_align {
|
||||
ClutAlignment::None |
|
||||
ClutAlignment::Linear => (pal_length_adjusted, 1u16),
|
||||
ClutAlignment::Block => (16u16, pal_length_adjusted/16u16),
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
(0u16, 0u16)
|
||||
}
|
||||
};
|
||||
|
||||
let header = Header::encode(width, height, pal_width, pal_height).ok_or(Error::from_callback(|| {format!("Image size (width: {}, height: {}) needs to be even", width, height)}))?;
|
||||
|
||||
tool_helper::raw::write_raw(output, &header)?;
|
||||
if let Some(palette) = palette {
|
||||
let mut color_count = pal_width*pal_height;
|
||||
for color in palette {
|
||||
tool_helper::raw::write_raw(output, color)?;
|
||||
color_count -= 1;
|
||||
}
|
||||
|
||||
while color_count > 0 {
|
||||
tool_helper::raw::write_raw(output, &PSXColor::black())?;
|
||||
color_count -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
for color in image {
|
||||
tool_helper::raw::write_raw(output, &color)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_full16(input: Input, output: &mut dyn Write) -> Result<(), Error> {
|
||||
match ImageReader::new(Cursor::new(tool_helper::input_to_vec(input)?)).with_guessed_format()?.decode() {
|
||||
Ok(image) => {
|
||||
match image {
|
||||
DynamicImage::ImageRgb8(image) => encode(RgbImage::new(image), ColorType::Full16, ClutAlignment::None, output),
|
||||
DynamicImage::ImageRgba8(image) => encode(RgbaImage::new(image), ColorType::Full16, ClutAlignment::None, output),
|
||||
|
||||
_ => Err(Error::from_str("Only RGB and RGBA images are supported for 16bit encoding"))
|
||||
}
|
||||
},
|
||||
Err(error) => Err(Error::from_error(error))
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_palette_based(input: Input, output: &mut dyn Write, color_type: ColorType, clut_align: ClutAlignment, semi_transparent: bool, transparent_palette: bool) -> Result<(), Error> {
|
||||
match png::Decoder::new(input).read_info() {
|
||||
Ok(reader) => {
|
||||
let output_type = {
|
||||
match color_type {
|
||||
ColorType::Clut4 => color_clut::OutputType::FourBit,
|
||||
ColorType::Clut8 => color_clut::OutputType::EightBit,
|
||||
_ => return Err(Error::from_str("ColorType not supported"))
|
||||
}
|
||||
};
|
||||
|
||||
encode(modify_palette(IndexedImage::new(reader, output_type)?, clut_align, semi_transparent, transparent_palette), color_type, clut_align, output)
|
||||
},
|
||||
Err(error) => Err(Error::from_error(error))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert(args: Arguments, input: Input, output: &mut dyn Write) -> Result<(), Error> {
|
||||
match args.color_depth {
|
||||
ColorType::Full16 => convert_full16(input, output),
|
||||
_ => convert_palette_based(input, output, args.color_depth, args.clut_align, args.semi_transparent, args.transparent_palette),
|
||||
}
|
||||
}
|
||||
162
src/Tools/psxfileconv/src/images/reduced_tim/types.rs
Normal file
162
src/Tools/psxfileconv/src/images/reduced_tim/types.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
use tool_helper::{*, bits::BitRange, raw::RawConversion};
|
||||
|
||||
#[repr(packed(1))]
|
||||
#[allow(dead_code)]
|
||||
pub struct Color {
|
||||
value: u16
|
||||
}
|
||||
|
||||
#[repr(packed(1))]
|
||||
#[allow(dead_code)]
|
||||
pub struct Header {
|
||||
value: u32
|
||||
}
|
||||
|
||||
macro_rules! set_member_value {
|
||||
($dst:expr, $color:ident, $shift:expr, $bits:ident) => {
|
||||
paste::item! {
|
||||
bits::[< set_value_ $bits >]($dst, ($color >> $shift) as $bits, &Self::[< $color:upper _BIT_RANGE >])
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! make_member_getter_setter {
|
||||
($color:ident, $shift:expr, $bits:ident) => {
|
||||
paste::item! {
|
||||
pub fn [< set_ $color >](&mut self, $color: u8) {
|
||||
self.value = set_member_value!(self.value, $color, $shift, $bits);
|
||||
}
|
||||
|
||||
pub fn [< get_ $color >](&self) -> u8 {
|
||||
(bits::[< get_value_ $bits >](self.value, &Self::[< $color:upper _BIT_RANGE >]) as u8) << $shift
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Color {
|
||||
const COLOR_SHIFT: u16 = 3;
|
||||
const STP_BIT_RANGE: BitRange = BitRange::from_to(15, 15);
|
||||
const RED_BIT_RANGE: BitRange = BitRange::from_to(0, 4);
|
||||
const GREEN_BIT_RANGE: BitRange = BitRange::from_to(5, 9);
|
||||
const BLUE_BIT_RANGE: BitRange = BitRange::from_to(10, 14);
|
||||
|
||||
const PIXEL3_4_BIT_RANGE: BitRange = BitRange::from_to(12, 15);
|
||||
const PIXEL2_4_BIT_RANGE: BitRange = BitRange::from_to(8, 11);
|
||||
const PIXEL1_4_BIT_RANGE: BitRange = BitRange::from_to(4, 7);
|
||||
const PIXEL0_4_BIT_RANGE: BitRange = BitRange::from_to(0, 3);
|
||||
|
||||
const PIXEL1_8_BIT_RANGE: BitRange = BitRange::from_to(8, 15);
|
||||
const PIXEL0_8_BIT_RANGE: BitRange = BitRange::from_to(0, 7);
|
||||
|
||||
pub const fn transparent() -> Color {
|
||||
Color::new(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
pub const fn non_transparent(red: u8, green: u8, blue: u8) -> Color {
|
||||
if red == 0 && green == 0 && blue == 0 {
|
||||
Color::black()
|
||||
}
|
||||
|
||||
else {
|
||||
Color::new(0, red, green, blue)
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn semi_transparent(mut red: u8, mut green: u8, mut blue: u8) -> Color {
|
||||
if red == 0 {
|
||||
red = 1;
|
||||
}
|
||||
|
||||
if green == 0 {
|
||||
green = 1;
|
||||
}
|
||||
|
||||
if blue == 0 {
|
||||
blue = 1;
|
||||
}
|
||||
|
||||
Color::new(1, red, green, blue)
|
||||
}
|
||||
|
||||
pub const fn black() -> Color {
|
||||
Color::new(1, 0, 0, 0)
|
||||
}
|
||||
|
||||
pub const fn new_4bit_entry(pixel0_4: u8, pixel1_4: u8, pixel2_4: u8, pixel3_4: u8) -> Color {
|
||||
let value = set_member_value!(set_member_value!(set_member_value!(set_member_value!(0,
|
||||
pixel0_4, 0, u16),
|
||||
pixel1_4, 0, u16),
|
||||
pixel2_4, 0, u16),
|
||||
pixel3_4, 0, u16);
|
||||
|
||||
Color{value}
|
||||
}
|
||||
|
||||
pub const fn new_8bit_entry(pixel0_8: u8, pixel1_8: u8) -> Color {
|
||||
let value = set_member_value!(set_member_value!(0,
|
||||
pixel0_8, 0, u16),
|
||||
pixel1_8, 0, u16);
|
||||
|
||||
Color{value}
|
||||
}
|
||||
|
||||
const fn new(stp: u8, red: u8, green: u8, blue: u8) -> Color {
|
||||
let value = set_member_value!(set_member_value!(set_member_value!(set_member_value!(0,
|
||||
stp, 0, u16),
|
||||
red, Self::COLOR_SHIFT, u16),
|
||||
green, Self::COLOR_SHIFT, u16),
|
||||
blue, Self::COLOR_SHIFT, u16);
|
||||
|
||||
Color{value}
|
||||
}
|
||||
|
||||
make_member_getter_setter!(stp, 0, u16);
|
||||
make_member_getter_setter!(red, Self::COLOR_SHIFT, u16);
|
||||
make_member_getter_setter!(green, Self::COLOR_SHIFT, u16);
|
||||
make_member_getter_setter!(blue, Self::COLOR_SHIFT, u16);
|
||||
}
|
||||
|
||||
impl RawConversion<2> for Color {
|
||||
fn convert_to_raw(&self) -> [u8; 2] {
|
||||
self.value.to_le_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Header {
|
||||
const TEX_WIDTH_BIT_RANGE: BitRange = BitRange::from_to(0, 8);
|
||||
const TEX_HEIGHT_BIT_RANGE: BitRange = BitRange::from_to(9, 16);
|
||||
const CLUT_WIDTH_BIT_RANGE: BitRange = BitRange::from_to(17, 22);
|
||||
const CLUT_HEIGHT_BIT_RANGE: BitRange = BitRange::from_to(23, 31);
|
||||
|
||||
pub fn encode(tex_width: u16, tex_height: u16, clut_width: u16, clut_height: u16) -> Option<Header> {
|
||||
if tex_width & 1 == 1 || tex_height & 1 == 1 {
|
||||
None
|
||||
}
|
||||
|
||||
else {
|
||||
let value = set_member_value!(set_member_value!(set_member_value!(set_member_value!(0,
|
||||
tex_width, 1, u32),
|
||||
tex_height, 1, u32),
|
||||
clut_width, 4, u32),
|
||||
clut_height, 0, u32);
|
||||
|
||||
Some(Header{value})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RawConversion<4> for Header {
|
||||
fn convert_to_raw(&self) -> [u8; 4] {
|
||||
self.value.to_le_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PSXImageConverter: std::iter::Iterator<Item = Color> {
|
||||
fn width(&self) -> u16;
|
||||
fn height(&self) -> u16;
|
||||
|
||||
fn get_palette(&self) -> Option<&Vec<Color>>;
|
||||
}
|
||||
3
src/Tools/psxfileconv/src/lib.rs
Normal file
3
src/Tools/psxfileconv/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod audio;
|
||||
pub mod images;
|
||||
pub mod nothing;
|
||||
135
src/Tools/psxfileconv/src/main.rs
Normal file
135
src/Tools/psxfileconv/src/main.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use psxfileconv::{audio::{self, *}, images::*, nothing};
|
||||
use std::path::PathBuf;
|
||||
use tool_helper::{exit_with_error, print_warning, Error};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(about = "Converts files to various JabyEngine related file formats", long_about = None)]
|
||||
struct CommandLine {
|
||||
#[clap(long="lz4", default_value_t=false)]
|
||||
compress_lz4: bool,
|
||||
|
||||
#[clap(short='o')]
|
||||
output_file: Option<PathBuf>,
|
||||
|
||||
#[clap(value_parser)]
|
||||
input_file: Option<PathBuf>,
|
||||
|
||||
#[clap(subcommand)]
|
||||
sub_command: SubCommands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum SubCommands {
|
||||
// === Internal Commands ===
|
||||
Nothing,
|
||||
SimpleTIM(reduced_tim::Arguments),
|
||||
MyVAG,
|
||||
|
||||
// === External Commands ===
|
||||
VAG(vag::Arguments),
|
||||
XA(xa::Arguments),
|
||||
}
|
||||
|
||||
impl SubCommands {
|
||||
pub fn is_external_command(&self) -> bool {
|
||||
match self {
|
||||
SubCommands::Nothing => false,
|
||||
SubCommands::SimpleTIM(_) => false,
|
||||
SubCommands::MyVAG => false,
|
||||
|
||||
SubCommands::VAG(_) => true,
|
||||
SubCommands::XA(_) => true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_internal_conversion(cmd: CommandLine) -> Result<(), Error> {
|
||||
let mut input = tool_helper::open_input(&cmd.input_file)?;
|
||||
let mut buffer = Vec::<u8>::new();
|
||||
let mut output_file = tool_helper::open_output(&cmd.output_file)?;
|
||||
let dst_buffer = {
|
||||
if cmd.compress_lz4 {
|
||||
&mut buffer as &mut dyn std::io::Write
|
||||
}
|
||||
|
||||
else {
|
||||
&mut output_file as &mut dyn std::io::Write
|
||||
}
|
||||
};
|
||||
|
||||
let cmd_result: Result<(), Error> = {
|
||||
match cmd.sub_command {
|
||||
SubCommands::Nothing => nothing::copy(&mut input, dst_buffer),
|
||||
SubCommands::SimpleTIM(args) => reduced_tim::convert(args, input, dst_buffer),
|
||||
SubCommands::MyVAG => audio::my_vag::convert(input, dst_buffer),
|
||||
_ => Err(Error::from_str("External functions can not be called for internal conversion"))
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(cmd_error) = cmd_result {
|
||||
if let Some(file_path) = cmd.output_file {
|
||||
let _result = std::fs::remove_file(file_path);
|
||||
}
|
||||
|
||||
else {
|
||||
tool_helper::print_warning("Open stream detected! Incomplete file can not be deleted".to_owned());
|
||||
}
|
||||
|
||||
return Err(cmd_error);
|
||||
}
|
||||
|
||||
// We encoded the file to a temporary buffer and now need to write it
|
||||
if cmd.compress_lz4 {
|
||||
let buffer = tool_helper::compress::psx_default::lz4(&buffer)?;
|
||||
output_file.write(&buffer)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_external_conversion(cmd: CommandLine) -> Result<(), Error> {
|
||||
let input_file = cmd.input_file.ok_or(Error::from_str("Input has to be a file"))?;
|
||||
let output_file = cmd.output_file.ok_or(Error::from_str("Output has to be a file"))?;
|
||||
let is_xa = matches!(cmd.sub_command, SubCommands::XA(_));
|
||||
|
||||
match cmd.sub_command {
|
||||
SubCommands::VAG(args) => vag::convert(args, input_file, output_file.clone()),
|
||||
SubCommands::XA(args) => xa::convert(args, input_file, output_file.clone()),
|
||||
_ => Err(Error::from_str("Internal functions can not be called for external conversion"))
|
||||
}?;
|
||||
|
||||
if cmd.compress_lz4 {
|
||||
if is_xa {
|
||||
print_warning(format!("File {} is of type XA and can not be compressed", output_file.to_string_lossy()));
|
||||
}
|
||||
|
||||
else {
|
||||
let compressed_file = tool_helper::compress::psx_default::lz4(&tool_helper::read_file(&output_file)?)?;
|
||||
return tool_helper::write_file(&output_file, compressed_file);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_main(cmd: CommandLine) -> Result<(), Error> {
|
||||
if cmd.sub_command.is_external_command() {
|
||||
run_external_conversion(cmd)
|
||||
}
|
||||
|
||||
else {
|
||||
run_internal_conversion(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
match CommandLine::try_parse() {
|
||||
Ok(cmd) => {
|
||||
if let Err(error) = run_main(cmd) {
|
||||
exit_with_error(error);
|
||||
}
|
||||
},
|
||||
Err(error) => println!("{}", error)
|
||||
}
|
||||
}
|
||||
7
src/Tools/psxfileconv/src/nothing/mod.rs
Normal file
7
src/Tools/psxfileconv/src/nothing/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use std::io::Write;
|
||||
use tool_helper::{Error, Input};
|
||||
|
||||
pub fn copy(input: &mut Input, output: &mut dyn Write) -> Result<(), Error> {
|
||||
std::io::copy(input, output)?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user