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