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"
|
||||
members = [
|
||||
"sqp",
|
||||
"sqp_tools",
|
||||
]
|
||||
|
||||
[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