mirror of
https://github.com/Dangoware/sqp.git
synced 2025-06-22 14:42:54 -05:00
Added CLI tool
This commit is contained in:
parent
f5e1e7b05b
commit
0e0d643fdc
4 changed files with 258 additions and 0 deletions
|
@ -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
15
sqp_tools/Cargo.toml
Normal 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
168
sqp_tools/src/main.rs
Normal 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
74
sqp_tools/src/utils.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue