mirror of
https://github.com/arabianq/colorgram-rust.git
synced 2026-04-27 22:21:22 +00:00
initial commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
.DS_Store
|
||||
.idea/
|
||||
Generated
+1241
File diff suppressed because it is too large
Load Diff
+31
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "colorgram"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
authors = ["arabian"]
|
||||
description = "Rust library that extracts colors from image. Port of colorgram.py"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
homepage = "https://github.com/arabianq/colorgram-rust"
|
||||
repository = "https://github.com/arabianq/colorgram-rust"
|
||||
keywords = ["colors", "image", "extract", "colorgram"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
image = { version = "0.25.6", features = ["default-formats"] }
|
||||
clap = { version = "4.5.37", features = ["derive"] }
|
||||
|
||||
[lib]
|
||||
name = "colorgram"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "colorgram-cli"
|
||||
path = "src/main.rs"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
opt-level = "z"
|
||||
panic = "abort"
|
||||
@@ -0,0 +1,38 @@
|
||||
# colorgram-rust
|
||||
|
||||
---
|
||||
|
||||
colorgram-rust is a rust program that lets you extract colors from image.
|
||||
It is a port of [colorgram.py](https://github.com/obskyr/colorgram.py) (which is itself a port
|
||||
of [colorgram.js](https://github.com/darosh/colorgram-js), so it's basically a port of a port =D)
|
||||
|
||||
**Why?** Well, it's ~25 times faster than colorgram.py (9 ms vs 225 ms for test.png on my laptop)
|
||||
|
||||
### Installation as CLI utility
|
||||
```bash
|
||||
cargo install colorgram
|
||||
```
|
||||
|
||||
|
||||
### Adding to your rust project
|
||||
```toml
|
||||
[dependencies]
|
||||
colorgram = "0.1.0"
|
||||
```
|
||||
|
||||
|
||||
### Usage Example (code from main.rs)
|
||||
|
||||
```rust
|
||||
use colorgram::extract;
|
||||
|
||||
fn main() {
|
||||
let img_path = "test.png";
|
||||
let colors_amount = 10;
|
||||
|
||||
let colors = extract(img_path, colors_amount).unwrap();
|
||||
for color in colors {
|
||||
println!("RGB: {} {} {} HSL: {} {} {} Proportion: {:.2}", color.rgb.r, color.rgb.g, color.rgb.b, color.hsl.h, color.hsl.s, color.hsl.l, color.proportion);
|
||||
}
|
||||
}
|
||||
```
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
use image;
|
||||
|
||||
pub struct Hsl {
|
||||
pub h: u8,
|
||||
pub s: u8,
|
||||
pub l: u8,
|
||||
}
|
||||
|
||||
pub struct Rgb {
|
||||
pub r: u8,
|
||||
pub g: u8,
|
||||
pub b: u8,
|
||||
}
|
||||
|
||||
pub struct Color {
|
||||
pub rgb: Rgb,
|
||||
pub hsl: Hsl,
|
||||
pub proportion: f32,
|
||||
}
|
||||
|
||||
impl Color {
|
||||
fn new(rgb: Rgb, proportion: f32) -> Color {
|
||||
let hsl = rgb_to_hsl(&rgb);
|
||||
Color {
|
||||
rgb,
|
||||
hsl,
|
||||
proportion,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rgb_to_hsl(rgb: &Rgb) -> Hsl {
|
||||
let r = rgb.r as i32;
|
||||
let g = rgb.g as i32;
|
||||
let b = rgb.b as i32;
|
||||
|
||||
let most = r.max(g).max(b);
|
||||
let least = r.min(g).min(b);
|
||||
|
||||
let l = (most + least) >> 1;
|
||||
|
||||
let (h, s) = if most == least {
|
||||
(0, 0)
|
||||
} else {
|
||||
let diff = most - least;
|
||||
let s = if l > 127 {
|
||||
diff * 255 / (510 - most - least)
|
||||
} else {
|
||||
diff * 255 / (most + least)
|
||||
};
|
||||
let h = if most == r {
|
||||
((g - b) * 255 / diff + if g < b { 1530 } else { 0 }) / 6
|
||||
} else if most == g {
|
||||
((b - r) * 255 / diff + 510) / 6
|
||||
} else {
|
||||
((r - g) * 255 / diff + 1020) / 6
|
||||
};
|
||||
(h as u8, s as u8)
|
||||
};
|
||||
|
||||
Hsl { h, s, l: l as u8 }
|
||||
}
|
||||
|
||||
pub fn extract<P: AsRef<std::path::Path>>(
|
||||
path: P,
|
||||
number_of_color: usize,
|
||||
) -> Result<Vec<Color>, Box<dyn std::error::Error>> {
|
||||
let img = image::open(path)?;
|
||||
let img = img.to_rgb8();
|
||||
|
||||
let mut samples = vec![0u32; 4 * 4096];
|
||||
|
||||
for pixel in img.pixels() {
|
||||
let rgb = Rgb {
|
||||
r: pixel[0],
|
||||
g: pixel[1],
|
||||
b: pixel[2],
|
||||
};
|
||||
let hsl = rgb_to_hsl(&rgb);
|
||||
|
||||
let y_val = ((rgb.r as f32 * 0.2126 + rgb.g as f32 * 0.7152 + rgb.b as f32 * 0.0722) as u8)
|
||||
& 0b1100_0000;
|
||||
let h = hsl.h & 0b1100_0000;
|
||||
let l = hsl.l & 0b1100_0000;
|
||||
|
||||
let packed = ((y_val as usize) << 4) | ((h as usize) << 2) | (l as usize);
|
||||
let idx = packed * 4;
|
||||
samples[idx] += rgb.r as u32;
|
||||
samples[idx + 1] += rgb.g as u32;
|
||||
samples[idx + 2] += rgb.b as u32;
|
||||
samples[idx + 3] += 1;
|
||||
}
|
||||
|
||||
let mut used = Vec::new();
|
||||
for (_i, chunk) in samples.chunks(4).enumerate() {
|
||||
let count = chunk[3];
|
||||
if count > 0 {
|
||||
let avg_r = (chunk[0] / count) as u8;
|
||||
let avg_g = (chunk[1] / count) as u8;
|
||||
let avg_b = (chunk[2] / count) as u8;
|
||||
let avg_rgb = Rgb {
|
||||
r: avg_r,
|
||||
g: avg_g,
|
||||
b: avg_b,
|
||||
};
|
||||
used.push((count, avg_rgb));
|
||||
}
|
||||
}
|
||||
|
||||
used.sort_unstable_by(|a, b| b.0.cmp(&a.0));
|
||||
|
||||
let top_used = &used[..number_of_color];
|
||||
let sum_counts: u32 = top_used.iter().map(|&(count, _)| count).sum();
|
||||
|
||||
let mut colors = Vec::with_capacity(number_of_color);
|
||||
for (count, rgb) in used.into_iter().take(number_of_color) {
|
||||
colors.push(Color::new(rgb, count as f32 / sum_counts as f32));
|
||||
}
|
||||
|
||||
Ok(colors)
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
use clap::Parser;
|
||||
use colorgram::extract;
|
||||
use std::path::{PathBuf, absolute};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Args {
|
||||
#[arg(short = 'i', long = "input")]
|
||||
input_file: PathBuf,
|
||||
|
||||
#[arg(short = 'c', long = "colors", default_value_t = 10)]
|
||||
colors_amount: usize,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
let input_path = absolute(&args.input_file).unwrap();
|
||||
let colors_amount = args.colors_amount;
|
||||
|
||||
assert!(input_path.exists(), "Input file does not exist");
|
||||
assert!(input_path.is_file(), "Input path is not a file");
|
||||
assert!(colors_amount > 0, "Colors amount must be greater than zero");
|
||||
|
||||
match extract(input_path, colors_amount) {
|
||||
Ok(colors) => {
|
||||
for color in colors {
|
||||
println!(
|
||||
"RGB: ({}, {}, {}), Proportion: {:.2}%",
|
||||
color.rgb.r,
|
||||
color.rgb.g,
|
||||
color.rgb.b,
|
||||
color.proportion * 100.0
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("Error: {}", e),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user