version 0.6: implement image file output
This commit is contained in:
parent
6933239b10
commit
0fb6e337c2
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1 @@
|
||||
/target
|
||||
|
||||
# test images
|
||||
*.png
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -765,7 +765,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "qtizer"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"image",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "qtizer"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
edition = "2024"
|
||||
description = "Quantization/palette-generation tool using k-means clustering on pixel data"
|
||||
readme = "README.md"
|
||||
|
||||
12
README.md
12
README.md
@ -5,6 +5,8 @@ command line quantization/palette-generation tool using k-means clustering on pi
|
||||
|
||||
- [features](#features)
|
||||
- [usage](#usage)
|
||||
- [example: palette creation in rgb format with alpha](#example-palette-creation-in-rgb-format-with-alpha-output-is-colored-accordingly-in-terminals)
|
||||
- [example: image quantization to reduced palette](#example-image-quantization-to-reduced-palette-file-formats-inferred-based-on-extension)
|
||||
- [installation](#installation)
|
||||
|
||||
|
||||
@ -31,12 +33,13 @@ Options:
|
||||
-s, --seed <number> Optional RNG seed for reproducible results
|
||||
-o, --output <output> Output file path
|
||||
- If not provided, outputs to stdout
|
||||
-f, --format <fmt> Palette output format [default: hex] [possible values: hex, rgb]
|
||||
- With image file extensions, outputs an image file
|
||||
-f, --format <fmt> Palette output format [possible values: hex, rgb]
|
||||
-h, --help Print help (see more with '--help')
|
||||
-V, --version Print version
|
||||
```
|
||||
|
||||
Example (output is colored accordingly in terminals):
|
||||
#### Example: palette creation in rgb format with alpha (output is colored accordingly in terminals):
|
||||
```sh
|
||||
$ qtizer wallpaper.png -k 3 -af rgb
|
||||
|
||||
@ -45,6 +48,11 @@ rgba(191, 150, 132, 254)
|
||||
rgba(48, 45, 51, 254)
|
||||
```
|
||||
|
||||
#### Example: image quantization to reduced palette (file formats inferred based on extension):
|
||||
```sh
|
||||
$ qtizer wallpaper.png -k 8 quantized.png
|
||||
```
|
||||
|
||||
|
||||
## installation
|
||||
|
||||
|
||||
68
src/cli.rs
68
src/cli.rs
@ -1,5 +1,7 @@
|
||||
use crate::colors::ColorFormat;
|
||||
use clap::*;
|
||||
use image::*;
|
||||
|
||||
use crate::colors::ColorFormat;
|
||||
|
||||
/// Quantization/palette-generation tool using k-means clustering on pixel dataI
|
||||
#[derive(Parser, Debug)]
|
||||
@ -27,8 +29,7 @@ pub struct Args {
|
||||
|
||||
/// Output file path
|
||||
/// - If not provided, outputs to stdout
|
||||
// TODO: implement image output
|
||||
// /// - With image file extensions, outputs an image file
|
||||
/// - With image file extensions, outputs an image file
|
||||
#[arg(
|
||||
short = 'o',
|
||||
long = "output",
|
||||
@ -43,13 +44,14 @@ pub struct Args {
|
||||
pub output_positional: Option<String>,
|
||||
|
||||
/// Palette output format
|
||||
#[arg(
|
||||
short = 'f',
|
||||
long = "format",
|
||||
default_value = "hex",
|
||||
value_name = "fmt"
|
||||
)]
|
||||
#[arg(short = 'f', long = "format", value_name = "fmt")]
|
||||
pub format: Option<ColorFormat>,
|
||||
|
||||
// TODO: add input alpha policy for opaque output images
|
||||
// /// Transparency policy when input has alpha but output does not
|
||||
// #[arg(short = 'p', long = "alpha-policy", value_name = "policy",)]
|
||||
// pub alpha_policy: AlphaPolicy,
|
||||
|
||||
// TODO: implement multi-threading
|
||||
// /// Number of workers to use [default: core count]
|
||||
// #[arg(short = 'j', long = "jobs",
|
||||
@ -59,35 +61,43 @@ pub struct Args {
|
||||
}
|
||||
|
||||
/// semantic validation of arguments
|
||||
/// - `--format` cannot be specified when outputting an image file
|
||||
/// - some image formats do not support alpha (eg. jpg)
|
||||
pub fn semantically_validate(args: &Args) {
|
||||
// check if `--format` is specified AND output has image file extension
|
||||
if args.format.is_some()
|
||||
&& (args.output.clone())
|
||||
.or(args.output_positional.clone())
|
||||
.is_some_and(is_image_file_path)
|
||||
.is_some_and(|p| ImageFormat::from_path(p).is_ok())
|
||||
{
|
||||
Args::command()
|
||||
.error(
|
||||
err_exit(
|
||||
clap::error::ErrorKind::ArgumentConflict,
|
||||
"cannot specify color-code format when outputting an image file.",
|
||||
);
|
||||
}
|
||||
|
||||
// check if output image format supports alpha channel
|
||||
let output_opt = args.output.clone().or(args.output_positional.clone());
|
||||
if args.alpha && output_opt.is_some() {
|
||||
let output_file = output_opt.unwrap();
|
||||
let filetype = ImageFormat::from_path(&output_file);
|
||||
|
||||
use ImageFormat::*;
|
||||
if matches!(filetype, Ok(Jpeg | Bmp | Pnm | Tiff)) {
|
||||
err_exit(
|
||||
clap::error::ErrorKind::ArgumentConflict,
|
||||
"cannot specify color format when outputting an image file.",
|
||||
)
|
||||
.exit();
|
||||
format!(
|
||||
"the `{:?}` image format does not support alpha.",
|
||||
filetype.unwrap(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: implement image output
|
||||
pub fn is_image_file_path<P>(file: P) -> bool
|
||||
where
|
||||
P: AsRef<std::path::Path>,
|
||||
{
|
||||
file.as_ref()
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.is_some_and(|e| {
|
||||
matches!(
|
||||
e.to_lowercase().as_str(),
|
||||
"png" | "jpg" | "jpeg" | "bmp" | "tiff" | "ppm"
|
||||
)
|
||||
})
|
||||
/// shorthand for `Args::command().error(...).exit()`
|
||||
pub fn err_exit(kind: clap::error::ErrorKind, message: impl std::fmt::Display) {
|
||||
Args::command().error(kind, message).exit()
|
||||
}
|
||||
|
||||
// TODO: implement static logger functionality for parsed arguments
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use crate::kmeans::Kmeansable;
|
||||
use image::Rgba;
|
||||
|
||||
use crate::kmeans::Kmeansable;
|
||||
|
||||
// TODO: implement k-means for `Rgb<u8>` pixel format
|
||||
// for slightly less mem-usage and faster calculation
|
||||
|
||||
|
||||
63
src/main.rs
63
src/main.rs
@ -1,12 +1,13 @@
|
||||
use clap::{CommandFactory, Parser};
|
||||
use clap::*;
|
||||
use image::*;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::colors::ColorFormat;
|
||||
|
||||
mod cli;
|
||||
mod colors;
|
||||
mod kmeans;
|
||||
|
||||
use crate::colors::ColorFormat;
|
||||
|
||||
fn main() {
|
||||
let args = cli::Args::parse();
|
||||
cli::semantically_validate(&args);
|
||||
@ -34,7 +35,7 @@ fn main() {
|
||||
|
||||
// run kmeans
|
||||
let pixels = img.pixels().cloned().collect::<Vec<_>>();
|
||||
let (clusters, _assignments) = context.k_means(&pixels, args.number, args.iterations);
|
||||
let (clusters, assignments) = context.k_means(&pixels, args.number, args.iterations);
|
||||
|
||||
// handle output
|
||||
match args.output.or(args.output_positional) {
|
||||
@ -45,8 +46,10 @@ fn main() {
|
||||
&args.format.unwrap_or_default(),
|
||||
),
|
||||
|
||||
// TODO: implement image output
|
||||
// Some(output_file) if is_image_file_path(&output_file) => image_file_handler(&clusters, &assignments, output_file),
|
||||
Some(output_file) if ImageFormat::from_path(&output_file).is_ok() => {
|
||||
image_file_handler(&img, &clusters, &assignments, args.alpha, output_file)
|
||||
}
|
||||
|
||||
Some(output_file) => {
|
||||
let mut file =
|
||||
std::fs::File::create(output_file).expect("failed to create output file");
|
||||
@ -60,6 +63,7 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
/// handle palette output to terminal or file
|
||||
fn palette_handler<W>(
|
||||
clusters: &[image::Rgba<u8>],
|
||||
writer: &mut W,
|
||||
@ -82,3 +86,50 @@ fn palette_handler<W>(
|
||||
writeln!(writer).expect("failed to write color to output");
|
||||
}
|
||||
}
|
||||
|
||||
/// handle image output to file
|
||||
fn image_file_handler(
|
||||
img: &ImageBuffer<Rgba<u8>, Vec<u8>>,
|
||||
clusters: &[image::Rgba<u8>],
|
||||
assignments: &[usize],
|
||||
alpha: bool,
|
||||
output_file: String,
|
||||
) {
|
||||
let (width, height) = img.dimensions();
|
||||
|
||||
// create new image by replacing each pixel with its cluster center
|
||||
let quantized = assignments
|
||||
.iter()
|
||||
.flat_map(|&i| &clusters[i].0[..if alpha { 4 } else { 3 }])
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let status = match alpha {
|
||||
true => {
|
||||
let img = ImageBuffer::from_vec(width, height, quantized);
|
||||
let img: ImageBuffer<Rgba<u8>, _> = img.expect("failed to create quantized image");
|
||||
img.save(&output_file)
|
||||
}
|
||||
false => {
|
||||
let img = ImageBuffer::from_vec(width, height, quantized);
|
||||
let img: ImageBuffer<Rgb<u8>, _> = img.expect("failed to create quantized image");
|
||||
img.save(&output_file)
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: better errors handling logger
|
||||
// save image with inferred format
|
||||
match status {
|
||||
Ok(_) => println!("saved quantized image to {}", output_file),
|
||||
Err(err) => {
|
||||
// errors here are unexpected, since extension alpha-capability
|
||||
// is validated in `cli::semantically_validate`
|
||||
cli::err_exit(
|
||||
clap::error::ErrorKind::InvalidValue,
|
||||
"unexpectedly failed to save quantized image.\n".to_string()
|
||||
+ "try checking the output file format. (does it support alpha?)\n"
|
||||
+ &format!(" ({err})"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user