Added CLI tool

This commit is contained in:
G2-Games 2025-05-29 14:54:02 -05:00
parent f5e1e7b05b
commit 0e0d643fdc
4 changed files with 258 additions and 0 deletions

View file

@ -2,6 +2,7 @@
resolver = "2" resolver = "2"
members = [ members = [
"sqp", "sqp",
"sqp_tools",
] ]
[workspace.package] [workspace.package]

15
sqp_tools/Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "sqp_tools"
version = "0.1.0"
edition = "2024"
authors.workspace = true
[dependencies]
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
image = "0.25"
sqp = { path = "../sqp" }
text_io = "0.1"
[lints]
workspace = true

168
sqp_tools/src/main.rs Normal file
View file

@ -0,0 +1,168 @@
mod utils;
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use image::ImageReader;
use anyhow::{bail, Result};
use sqp::{ColorFormat, CompressionType};
use utils::{color_format, color_format_to_type, color_type_to_format, exists_decision, Assume};
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Subcommands,
/// Overwrite output files
#[arg(short = 'n', long = "overwrite", conflicts_with = "assumeno")]
assumeyes: bool,
/// Do not overwrite output files
#[arg(short = 'y', long = "preserve", conflicts_with = "assumeyes")]
assumeno: bool,
}
#[derive(Debug, Subcommand)]
enum Subcommands {
/// Encode an image to SQP format
Encode(EncodeArgs),
/// Decode an SQP image into another format
Decode(DecodeArgs),
}
#[derive(Debug, Args)]
struct EncodeArgs {
/// Input image file of any type supported by `image`
input: PathBuf,
/// Output path to SQP location
output: PathBuf,
/// Quality setting, a higher value = higher quality.
#[arg(default_value_t = 100, short, long, conflicts_with = "uncompressed", value_parser = clap::value_parser!(u8).range(1..=100))]
quality: u8,
/// Create an uncompressed image.
///
/// Incompatible with quality setting.
#[arg(short, long, conflicts_with = "quality")]
uncompressed: bool,
/// The color format to use for the output image
///
/// Valid values:
/// - RGBA8
/// - RGB8
/// - GrayA8
/// - Gray8
#[arg(short, long, value_parser = color_format, verbatim_doc_comment)]
color_format: Option<ColorFormat>,
}
#[derive(Debug, Args)]
struct DecodeArgs {
/// Input SQP image file
input: PathBuf,
/// Output image file
output: PathBuf,
}
fn main() -> Result<()> {
let args = Cli::parse();
let assume = if args.assumeyes {
Some(Assume::Yes)
} else if args.assumeno {
Some(Assume::No)
} else {
None
};
match args.command {
Subcommands::Encode(a) => encode(a, assume),
Subcommands::Decode(a) => decode(a, assume),
}
}
fn encode(args: EncodeArgs, assume: Option<Assume>) -> Result<()> {
if !args.input.try_exists()? {
bail!("Input file {:?} does not exist", args.input);
}
if args.output.try_exists()?
&& !exists_decision("Output", "Overwrite", &args.output, assume)
{
return Ok(())
}
let image = ImageReader::open(args.input)?
.decode()?;
let width = image.width();
let height = image.height();
let color_format = args.color_format.or_else(
|| color_type_to_format(image.color())
).unwrap_or(ColorFormat::Rgba8);
let bitmap = match color_format {
ColorFormat::Rgba8 => image.into_rgba8().into_vec(),
ColorFormat::Rgb8 => image.into_rgb8().into_vec(),
ColorFormat::GrayA8 => image.into_luma_alpha8().into_vec(),
ColorFormat::Gray8 => image.into_luma8().into_vec(),
};
let (compression_type, quality) = if args.uncompressed {
(CompressionType::None, None)
} else if args.quality == 100 {
(CompressionType::Lossless, None)
} else {
(CompressionType::LossyDct, Some(args.quality))
};
let sqp_img = sqp::SquishyPicture::from_raw(
width,
height,
color_format,
compression_type,
quality,
bitmap,
);
sqp_img.save(&args.output)?;
Ok(())
}
fn decode(args: DecodeArgs, assume: Option<Assume>) -> Result<()> {
if !args.input.try_exists()? {
bail!("Input file {:?} does not exist", args.input);
}
if args.output.try_exists()?
&& !exists_decision("Output", "Overwrite", &args.output, assume)
{
return Ok(())
}
let sqp_img = sqp::open(args.input)?;
let width = sqp_img.width();
let height = sqp_img.height();
let color_format = color_format_to_type(sqp_img.color_format());
let mut img = sqp_img.into_raw();
img.truncate(width as usize * height as usize * color_format.bytes_per_pixel() as usize);
image::save_buffer(
args.output,
&img,
width,
height,
color_format,
)?;
Ok(())
}

74
sqp_tools/src/utils.rs Normal file
View file

@ -0,0 +1,74 @@
use std::path::Path;
use image::ColorType;
use sqp::ColorFormat;
use text_io::read;
pub enum Assume {
Yes,
No,
}
pub fn color_format(s: &str) -> Result<ColorFormat, String> {
if !s.is_ascii() {
return Err(format!("Invalid color format {}", s))
}
let s_lower = s.to_lowercase();
let color_format = match s_lower.as_str() {
"rgba8" => ColorFormat::Rgba8,
"rgb8" => ColorFormat::Rgb8,
"graya8" => ColorFormat::GrayA8,
"gray8" => ColorFormat::Gray8,
_ => return Err(format!("Invalid color format {}", s)),
};
Ok(color_format)
}
pub fn color_type_to_format(img_color_format: ColorType) -> Option<ColorFormat> {
Some(match img_color_format {
ColorType::L8 => ColorFormat::Gray8,
ColorType::La8 => ColorFormat::GrayA8,
ColorType::Rgb8 => ColorFormat::Rgb8,
ColorType::Rgba8 => ColorFormat::Rgba8,
_ => return None,
})
}
pub fn color_format_to_type(img_color_format: ColorFormat) -> ColorType {
match img_color_format {
ColorFormat::Gray8 => ColorType::L8,
ColorFormat::GrayA8 => ColorType::La8,
ColorFormat::Rgb8 => ColorType::Rgb8,
ColorFormat::Rgba8 => ColorType::Rgba8,
}
}
pub fn exists_decision<P: AsRef<Path>>(place: &str, action: &str, path: &P, assume: Option<Assume>) -> bool {
let path = path.as_ref();
match assume {
Some(Assume::Yes) => return true,
Some(Assume::No) => return false,
None => (),
}
loop {
print!("{place} file {path:?} already exists. {action}? [y/N] ");
let opt: String = read!("{}\n");
let opt = opt.to_lowercase();
if !opt.is_empty() && opt == "y" {
return true
} else if !opt.is_empty() && opt != "y" {
continue
}
if opt.is_empty() {
return false
}
}
}