From 3ee622e5e587ca72fe3e3e4dbf82f242a2364f0e Mon Sep 17 00:00:00 2001 From: David Chavez Date: Wed, 19 Jul 2023 18:46:08 +0200 Subject: [PATCH] Feature: Extend CLI (#14) * Add cli paths for bin->png * Add support for flips * Address feedback --- src/cli/binary.rs | 126 ++++++++++++++++++++++++++ src/cli/defines.rs | 65 ++++++++++++++ src/cli/macros.rs | 17 ++++ src/cli/mod.rs | 5 ++ src/cli/png.rs | 105 ++++++++++++++++++++++ src/image/png_image.rs | 11 +++ src/main.rs | 199 ++++++----------------------------------- 7 files changed, 356 insertions(+), 172 deletions(-) create mode 100644 src/cli/binary.rs create mode 100644 src/cli/defines.rs create mode 100644 src/cli/macros.rs create mode 100644 src/cli/mod.rs create mode 100644 src/cli/png.rs diff --git a/src/cli/binary.rs b/src/cli/binary.rs new file mode 100644 index 0000000..639fc6a --- /dev/null +++ b/src/cli/binary.rs @@ -0,0 +1,126 @@ +use crate::cli::defines::BinaryFormat; +use crate::write_buf_as_raw_array; +use anyhow::Result; +use clap::{Args, ValueEnum}; +use std::{ + fs::File, + io::{self, BufReader, BufWriter, Write}, + mem, + path::PathBuf, +}; + +// MARK: - Args + +#[derive(Args, Debug)] +pub struct BinaryArgs { + /// Path to the PNG input file + input: String, + + /// Output file. Defaults to input file name with ".bin" appended + #[arg(short)] + output: Option, + + /// Output format + #[arg(value_enum, short, long)] + format: BinaryFormat, + + /// Flip the image on the x axis + #[arg(long)] + flip_x: bool, + + /// Flip the image on the y axis + #[arg(long)] + flip_y: bool, + + /// Output a raw C array which can be `#include`d in a file. The default output type width matches the FORMAT provided, but it can be overridden with --c_array_width + #[arg(long)] + c_array: bool, + + /// Overrides the natural fit of each format when outputting a C array + #[arg(long, value_enum)] + c_array_width: Option, +} + +// MARK: - Handlers + +pub fn handle_binary(args: &BinaryArgs) -> Result<()> { + let input_file = File::open(&args.input).expect("could not open input file"); + let mut input_reader = BufReader::new(input_file); + + // Convert the image + let mut bin: Vec = Vec::new(); + if let BinaryFormat::Palette = args.format { + pigment64::create_palette_from_png(&mut input_reader, &mut bin)?; + } else { + let mut image = pigment64::PNGImage::read(&mut input_reader)?; + + if args.flip_x || args.flip_y { + image = image.flip(args.flip_x, args.flip_y); + } + + image.as_native(&mut bin, args.format.as_native())?; + }; + + let mut output_file: Box; + + if args.c_array && args.output.is_none() { + output_file = Box::from(io::stdout()); + } else { + let output_path = PathBuf::from(args.output.clone().unwrap_or_else(|| { + let mut path = args.input.clone(); + if args.c_array { + path.push_str(".inc.c"); + } else { + path.push_str(".bin"); + } + path + })); + + let file = File::create(output_path)?; + output_file = Box::from(file); + } + + if args.c_array { + // Override array width if the user passed the appropriate flag + let c_array_width = args.c_array_width.unwrap_or(args.format.get_width()); + + match c_array_width { + CArrayWidth::U8 => write_buf_as_u8(&mut output_file, &bin), + CArrayWidth::U16 => write_buf_as_u16(&mut output_file, &bin), + CArrayWidth::U32 => write_buf_as_u32(&mut output_file, &bin), + CArrayWidth::U64 => write_buf_as_u64(&mut output_file, &bin), + } + } else { + BufWriter::new(output_file).write_all(&bin)?; + } + + Ok(()) +} + +// MARK: - Structs + +#[derive(Copy, Clone, PartialEq, Eq, ValueEnum, Debug)] +pub enum CArrayWidth { + U8, + U16, + U32, + U64, +} + +// MARK: - Helpers + +fn write_buf_as_u8(output_file: &mut Box, bin: &[u8]) { + write_buf_as_raw_array!(output_file, bin, u8); +} + +fn write_buf_as_u16(output_file: &mut Box, bin: &[u8]) { + write_buf_as_raw_array!(output_file, bin, u16); +} + +fn write_buf_as_u32(output_file: &mut Box, bin: &[u8]) { + write_buf_as_raw_array!(output_file, bin, u32); +} + +fn write_buf_as_u64(output_file: &mut Box, bin: &[u8]) { + write_buf_as_raw_array!(output_file, bin, u64); +} diff --git a/src/cli/defines.rs b/src/cli/defines.rs new file mode 100644 index 0000000..4d07f20 --- /dev/null +++ b/src/cli/defines.rs @@ -0,0 +1,65 @@ +use crate::cli::binary::CArrayWidth; +use clap::ValueEnum; +use pigment64::ImageSize::Bits4; +use pigment64::{ImageSize, ImageType}; + +#[derive(Copy, Clone, PartialEq, Eq, ValueEnum, Debug)] +pub enum BinaryFormat { + Ci4, + Ci8, + I4, + I8, + Ia4, + Ia8, + Ia16, + Rgba16, + Rgba32, + Palette, +} + +impl BinaryFormat { + pub fn get_width(&self) -> CArrayWidth { + match self { + BinaryFormat::Ci4 => CArrayWidth::U8, + BinaryFormat::Ci8 => CArrayWidth::U8, + BinaryFormat::I4 => CArrayWidth::U8, + BinaryFormat::I8 => CArrayWidth::U8, + BinaryFormat::Ia4 => CArrayWidth::U8, + BinaryFormat::Ia8 => CArrayWidth::U8, + BinaryFormat::Ia16 => CArrayWidth::U16, + BinaryFormat::Rgba16 => CArrayWidth::U16, + BinaryFormat::Rgba32 => CArrayWidth::U32, + BinaryFormat::Palette => CArrayWidth::U16, + } + } + + pub fn as_native(&self) -> ImageType { + match self { + BinaryFormat::Ci4 => ImageType::Ci4, + BinaryFormat::Ci8 => ImageType::Ci8, + BinaryFormat::I4 => ImageType::I4, + BinaryFormat::I8 => ImageType::I8, + BinaryFormat::Ia4 => ImageType::Ia4, + BinaryFormat::Ia8 => ImageType::Ia8, + BinaryFormat::Ia16 => ImageType::Ia16, + BinaryFormat::Rgba16 => ImageType::Rgba16, + BinaryFormat::Rgba32 => ImageType::Rgba32, + BinaryFormat::Palette => panic!("cannot convert palette to native format"), + } + } + + pub fn get_size(&self) -> ImageSize { + match self { + BinaryFormat::Ci4 => Bits4, + BinaryFormat::Ci8 => ImageSize::Bits8, + BinaryFormat::I4 => ImageSize::Bits4, + BinaryFormat::I8 => ImageSize::Bits8, + BinaryFormat::Ia4 => ImageSize::Bits4, + BinaryFormat::Ia8 => ImageSize::Bits8, + BinaryFormat::Ia16 => ImageSize::Bits16, + BinaryFormat::Rgba16 => ImageSize::Bits16, + BinaryFormat::Rgba32 => ImageSize::Bits32, + BinaryFormat::Palette => panic!("cannot convert palette to native format"), + } + } +} diff --git a/src/cli/macros.rs b/src/cli/macros.rs new file mode 100644 index 0000000..21c36fc --- /dev/null +++ b/src/cli/macros.rs @@ -0,0 +1,17 @@ +#[macro_export] +macro_rules! write_buf_as_raw_array { + ($dst:expr, $bin:expr, $type_width:ident) => { + let width = mem::size_of::<$type_width>(); + + for row in $bin.chunks(16) { + let mut line_list = Vec::new(); + for bytes in row.chunks(width) { + let value = $type_width::from_be_bytes(bytes.try_into().unwrap()); + + line_list.push(format!("0x{value:00$X}", 2 * width)); + } + let line = line_list.join(", "); + write!($dst, " {line},\n").expect("could not write to output file"); + } + }; +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..7c144a0 --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,5 @@ +pub mod defines; +pub mod macros; + +pub mod binary; +pub mod png; diff --git a/src/cli/png.rs b/src/cli/png.rs new file mode 100644 index 0000000..9c77cb5 --- /dev/null +++ b/src/cli/png.rs @@ -0,0 +1,105 @@ +use crate::cli::defines::BinaryFormat; +use anyhow::Result; +use clap::Args; +use pigment64::image::native_image::parse_tlut; +use pigment64::TextureLUT; +use std::fs::File; +use std::io::{BufReader, BufWriter, Read, Write}; +use std::path::PathBuf; + +// MARK: - Args + +#[derive(Args, Debug)] +pub struct PngArgs { + /// Path to the binary input file + input: String, + + /// Width of the binary image + #[arg(long)] + width: u32, + + /// Height of the binary image + #[arg(long)] + height: u32, + + /// Input format + #[arg(value_enum, short, long)] + format: BinaryFormat, + + /// Output file. Defaults to input file name with ".png" appended + #[arg(short, long)] + output: Option, + + /// Path to the palette binary file (only required for CI formats) + #[arg(short, long)] + palette: Option, + + /// Flip the image on the x axis + #[arg(long)] + flip_x: bool, + + /// Flip the image on the y axis + #[arg(long)] + flip_y: bool, +} + +// MARK: - Handlers + +pub fn handle_png(args: &PngArgs) -> Result<()> { + assert_ne!(args.format, BinaryFormat::Palette, "palette is not a supported standalone output format. Use format ci4/ci8 with --palette instead."); + + // Open the input file + let input_file = File::open(&args.input).expect("could not open input file"); + let mut input_reader = BufReader::new(input_file); + + // Convert the image + let image = pigment64::NativeImage::read( + &mut input_reader, + args.format.as_native(), + args.width, + args.height, + )?; + + let mut output: Vec = Vec::new(); + + // if format is ci4/ci8, read the palette + if let BinaryFormat::Ci4 | BinaryFormat::Ci8 = args.format { + // Read the palette + let palette_file = File::open( + args.palette + .as_ref() + .expect("palette is required for ci4/ci8 formats"), + ) + .expect("could not open palette file"); + let mut palette_reader = BufReader::new(palette_file); + let mut palette_bytes = Vec::new(); + palette_reader.read_to_end(&mut palette_bytes)?; + + let palette = parse_tlut(&palette_bytes, args.format.get_size(), TextureLUT::Rgba16)?; + image.as_png(&mut output, Some(&palette))?; + } else { + image.as_png(&mut output, None)?; + } + + // Handle flips, we do this on the already produced PNG because it's easier + if args.flip_x || args.flip_y { + let mut image = pigment64::PNGImage::read(&mut output.as_slice())?; + image = image.flip(args.flip_x, args.flip_y); + output.clear(); + image.as_png(&mut output)?; + } + + // Write the file + let output_file: Box; + let output_path = PathBuf::from(args.output.clone().unwrap_or_else(|| { + let mut path = args.input.clone(); + path.push_str(".png"); + path + })); + + let file = File::create(output_path)?; + output_file = Box::from(file); + BufWriter::new(output_file).write_all(&output)?; + + Ok(()) +} diff --git a/src/image/png_image.rs b/src/image/png_image.rs index 5acb949..7eab89e 100644 --- a/src/image/png_image.rs +++ b/src/image/png_image.rs @@ -94,6 +94,17 @@ impl PNGImage { } } + /// Writes the image as a PNG to the given writer. + /// This is useful for when you need to flip an existing image. + pub fn as_png(&self, writer: &mut W) -> Result<()> { + let mut encoder = png::Encoder::new(writer, self.width, self.height); + encoder.set_color(self.color_type); + encoder.set_depth(self.bit_depth); + let mut writer = encoder.write_header()?; + writer.write_image_data(&self.data)?; + Ok(()) + } + pub fn as_native(&self, writer: &mut W, image_type: ImageType) -> Result<()> { match image_type { ImageType::I4 => self.as_i4(writer), diff --git a/src/main.rs b/src/main.rs index 502b36b..0c69dbc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,185 +1,40 @@ use anyhow::Result; -use clap::{Parser, ValueEnum}; -use pigment64::ImageType; -use std::fs::File; -use std::io::{self, prelude::*}; -use std::io::{BufReader, BufWriter}; -use std::mem; -use std::path::PathBuf; +use clap::{Parser, Subcommand}; + +mod cli; -/// PNG to N64 image converter #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] -struct Args { - /// Input PNG file - input: String, - - /// Output file. Defaults to input file name with ".bin" appended - #[arg(short)] - output: Option, - - /// Output format - #[arg(value_enum)] - format: OutputFormat, - - /// Flip the image on the x axis - #[arg(long)] - flip_x: bool, - - /// Flip the image on the y axis - #[arg(long)] - flip_y: bool, - - /// Output a raw C array which can be `#include`d in a file. The default output type width matches the FORMAT provided, but it can be overridden with --c_array_width - #[arg(short, long)] - c_array: bool, - - /// Overrides the natural fit of each format when outputting a C array - #[arg(short, long, value_enum)] - c_array_width: Option, -} - -#[derive(Copy, Clone, PartialEq, Eq, ValueEnum, Debug)] -enum OutputFormat { - Ci4, - Ci8, - I4, - I8, - Ia4, - Ia8, - Ia16, - Rgba16, - Rgba32, - Palette, -} - -impl OutputFormat { - fn get_width(&self) -> CArrayWidth { - match self { - OutputFormat::Ci4 => CArrayWidth::U8, - OutputFormat::Ci8 => CArrayWidth::U8, - OutputFormat::I4 => CArrayWidth::U8, - OutputFormat::I8 => CArrayWidth::U8, - OutputFormat::Ia4 => CArrayWidth::U8, - OutputFormat::Ia8 => CArrayWidth::U8, - OutputFormat::Ia16 => CArrayWidth::U16, - OutputFormat::Rgba16 => CArrayWidth::U16, - OutputFormat::Rgba32 => CArrayWidth::U32, - OutputFormat::Palette => CArrayWidth::U16, - } - } - - fn as_native(&self) -> ImageType { - match self { - OutputFormat::Ci4 => ImageType::Ci4, - OutputFormat::Ci8 => ImageType::Ci8, - OutputFormat::I4 => ImageType::I4, - OutputFormat::I8 => ImageType::I8, - OutputFormat::Ia4 => ImageType::Ia4, - OutputFormat::Ia8 => ImageType::Ia8, - OutputFormat::Ia16 => ImageType::Ia16, - OutputFormat::Rgba16 => ImageType::Rgba16, - OutputFormat::Rgba32 => ImageType::Rgba32, - _ => panic!("cannot convert palette to native format"), - } - } -} - -#[derive(Copy, Clone, PartialEq, Eq, ValueEnum, Debug)] -enum CArrayWidth { - U8, - U16, - U32, - U64, -} - -#[macro_export] -macro_rules! write_buf_as_raw_array { - ($dst:expr, $bin:expr, $type_width:ident) => { - let width = mem::size_of::<$type_width>(); - - for row in $bin.chunks(16) { - let mut line_list = Vec::new(); - for bytes in row.chunks(width) { - let value = $type_width::from_be_bytes(bytes.try_into().unwrap()); - - line_list.push(format!("0x{value:00$X}", 2 * width)); - } - let line = line_list.join(", "); - write!($dst, " {line},\n").expect("could not write to output file"); - } - }; -} - -fn write_buf_as_u8(output_file: &mut Box, bin: &[u8]) { - write_buf_as_raw_array!(output_file, bin, u8); -} - -fn write_buf_as_u16(output_file: &mut Box, bin: &[u8]) { - write_buf_as_raw_array!(output_file, bin, u16); -} - -fn write_buf_as_u32(output_file: &mut Box, bin: &[u8]) { - write_buf_as_raw_array!(output_file, bin, u32); -} - -fn write_buf_as_u64(output_file: &mut Box, bin: &[u8]) { - write_buf_as_raw_array!(output_file, bin, u64); +#[command(propagate_version = true)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// Converts a binary image to a PNG + ToPng { + #[clap(flatten)] + args: cli::png::PngArgs, + }, + /// Converts a PNG to a binary image + ToBin { + #[clap(flatten)] + args: cli::binary::BinaryArgs, + }, } fn main() -> Result<()> { - let args = Args::parse(); + let cli = Cli::parse(); - let input_file = File::open(&args.input).expect("could not open input file"); - let mut input_reader = BufReader::new(input_file); - - // Convert the image - let mut bin: Vec = Vec::new(); - if let OutputFormat::Palette = args.format { - pigment64::create_palette_from_png(&mut input_reader, &mut bin)?; - } else { - let mut image = pigment64::PNGImage::read(&mut input_reader)?; - - if args.flip_x || args.flip_y { - image = image.flip(args.flip_x, args.flip_y); + match &cli.command { + Commands::ToPng { args } => { + cli::png::handle_png(args)?; } - - image.as_native(&mut bin, args.format.as_native())?; - }; - - let mut output_file: Box; - - if args.c_array && args.output.is_none() { - output_file = Box::from(io::stdout()); - } else { - let output_path = PathBuf::from(args.output.unwrap_or_else(|| { - let mut path = args.input.clone(); - if args.c_array { - path.push_str(".inc.c"); - } else { - path.push_str(".bin"); - } - path - })); - - let file = File::create(output_path)?; - output_file = Box::from(file); - } - - if args.c_array { - let mut c_array_width = args.format.get_width(); - - // Override if the user passed the appropriate flag - c_array_width = args.c_array_width.unwrap_or(c_array_width); - - match c_array_width { - CArrayWidth::U8 => write_buf_as_u8(&mut output_file, &bin), - CArrayWidth::U16 => write_buf_as_u16(&mut output_file, &bin), - CArrayWidth::U32 => write_buf_as_u32(&mut output_file, &bin), - CArrayWidth::U64 => write_buf_as_u64(&mut output_file, &bin), + Commands::ToBin { args } => { + cli::binary::handle_binary(args)?; } - } else { - BufWriter::new(output_file).write_all(&bin)?; } Ok(())