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
|
/target
|
||||||
|
|
||||||
# test images
|
|
||||||
*.png
|
|
||||||
|
|||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -765,7 +765,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "qtizer"
|
name = "qtizer"
|
||||||
version = "0.5.0"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"image",
|
"image",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "qtizer"
|
name = "qtizer"
|
||||||
version = "0.5.0"
|
version = "0.6.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Quantization/palette-generation tool using k-means clustering on pixel data"
|
description = "Quantization/palette-generation tool using k-means clustering on pixel data"
|
||||||
readme = "README.md"
|
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)
|
- [features](#features)
|
||||||
- [usage](#usage)
|
- [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)
|
- [installation](#installation)
|
||||||
|
|
||||||
|
|
||||||
@ -31,12 +33,13 @@ Options:
|
|||||||
-s, --seed <number> Optional RNG seed for reproducible results
|
-s, --seed <number> Optional RNG seed for reproducible results
|
||||||
-o, --output <output> Output file path
|
-o, --output <output> Output file path
|
||||||
- If not provided, outputs to stdout
|
- 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')
|
-h, --help Print help (see more with '--help')
|
||||||
-V, --version Print version
|
-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
|
```sh
|
||||||
$ qtizer wallpaper.png -k 3 -af rgb
|
$ qtizer wallpaper.png -k 3 -af rgb
|
||||||
|
|
||||||
@ -45,6 +48,11 @@ rgba(191, 150, 132, 254)
|
|||||||
rgba(48, 45, 51, 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
|
## installation
|
||||||
|
|
||||||
|
|||||||
68
src/cli.rs
68
src/cli.rs
@ -1,5 +1,7 @@
|
|||||||
use crate::colors::ColorFormat;
|
|
||||||
use clap::*;
|
use clap::*;
|
||||||
|
use image::*;
|
||||||
|
|
||||||
|
use crate::colors::ColorFormat;
|
||||||
|
|
||||||
/// Quantization/palette-generation tool using k-means clustering on pixel dataI
|
/// Quantization/palette-generation tool using k-means clustering on pixel dataI
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
@ -27,8 +29,7 @@ pub struct Args {
|
|||||||
|
|
||||||
/// Output file path
|
/// Output file path
|
||||||
/// - If not provided, outputs to stdout
|
/// - 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(
|
#[arg(
|
||||||
short = 'o',
|
short = 'o',
|
||||||
long = "output",
|
long = "output",
|
||||||
@ -43,13 +44,14 @@ pub struct Args {
|
|||||||
pub output_positional: Option<String>,
|
pub output_positional: Option<String>,
|
||||||
|
|
||||||
/// Palette output format
|
/// Palette output format
|
||||||
#[arg(
|
#[arg(short = 'f', long = "format", value_name = "fmt")]
|
||||||
short = 'f',
|
|
||||||
long = "format",
|
|
||||||
default_value = "hex",
|
|
||||||
value_name = "fmt"
|
|
||||||
)]
|
|
||||||
pub format: Option<ColorFormat>,
|
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
|
// TODO: implement multi-threading
|
||||||
// /// Number of workers to use [default: core count]
|
// /// Number of workers to use [default: core count]
|
||||||
// #[arg(short = 'j', long = "jobs",
|
// #[arg(short = 'j', long = "jobs",
|
||||||
@ -59,35 +61,43 @@ pub struct Args {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// semantic validation of arguments
|
/// 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) {
|
pub fn semantically_validate(args: &Args) {
|
||||||
|
// check if `--format` is specified AND output has image file extension
|
||||||
if args.format.is_some()
|
if args.format.is_some()
|
||||||
&& (args.output.clone())
|
&& (args.output.clone())
|
||||||
.or(args.output_positional.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()
|
err_exit(
|
||||||
.error(
|
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,
|
clap::error::ErrorKind::ArgumentConflict,
|
||||||
"cannot specify color format when outputting an image file.",
|
format!(
|
||||||
)
|
"the `{:?}` image format does not support alpha.",
|
||||||
.exit();
|
filetype.unwrap(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: implement image output
|
/// shorthand for `Args::command().error(...).exit()`
|
||||||
pub fn is_image_file_path<P>(file: P) -> bool
|
pub fn err_exit(kind: clap::error::ErrorKind, message: impl std::fmt::Display) {
|
||||||
where
|
Args::command().error(kind, message).exit()
|
||||||
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"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: implement static logger functionality for parsed arguments
|
// TODO: implement static logger functionality for parsed arguments
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
use crate::kmeans::Kmeansable;
|
|
||||||
use image::Rgba;
|
use image::Rgba;
|
||||||
|
|
||||||
|
use crate::kmeans::Kmeansable;
|
||||||
|
|
||||||
// TODO: implement k-means for `Rgb<u8>` pixel format
|
// TODO: implement k-means for `Rgb<u8>` pixel format
|
||||||
// for slightly less mem-usage and faster calculation
|
// 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 std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use crate::colors::ColorFormat;
|
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
mod colors;
|
mod colors;
|
||||||
mod kmeans;
|
mod kmeans;
|
||||||
|
|
||||||
|
use crate::colors::ColorFormat;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let args = cli::Args::parse();
|
let args = cli::Args::parse();
|
||||||
cli::semantically_validate(&args);
|
cli::semantically_validate(&args);
|
||||||
@ -34,7 +35,7 @@ fn main() {
|
|||||||
|
|
||||||
// run kmeans
|
// run kmeans
|
||||||
let pixels = img.pixels().cloned().collect::<Vec<_>>();
|
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
|
// handle output
|
||||||
match args.output.or(args.output_positional) {
|
match args.output.or(args.output_positional) {
|
||||||
@ -45,8 +46,10 @@ fn main() {
|
|||||||
&args.format.unwrap_or_default(),
|
&args.format.unwrap_or_default(),
|
||||||
),
|
),
|
||||||
|
|
||||||
// TODO: implement image output
|
Some(output_file) if ImageFormat::from_path(&output_file).is_ok() => {
|
||||||
// Some(output_file) if is_image_file_path(&output_file) => image_file_handler(&clusters, &assignments, output_file),
|
image_file_handler(&img, &clusters, &assignments, args.alpha, output_file)
|
||||||
|
}
|
||||||
|
|
||||||
Some(output_file) => {
|
Some(output_file) => {
|
||||||
let mut file =
|
let mut file =
|
||||||
std::fs::File::create(output_file).expect("failed to create output 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>(
|
fn palette_handler<W>(
|
||||||
clusters: &[image::Rgba<u8>],
|
clusters: &[image::Rgba<u8>],
|
||||||
writer: &mut W,
|
writer: &mut W,
|
||||||
@ -82,3 +86,50 @@ fn palette_handler<W>(
|
|||||||
writeln!(writer).expect("failed to write color to output");
|
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