Add support for an Oklab backend; Improve contrast
This commit is contained in:
parent
5689bff525
commit
93a44c16c1
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "colorpickle"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
authors = ["Agatha V. Lovelace <agatha@technogothic.net>"]
|
||||
description = "A colorscheme generator"
|
||||
|
@ -9,8 +9,10 @@ license = "NVPLv7+"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
pastel = "0.9.0"
|
||||
clap = { version = "4.4.6", features = ["derive"] }
|
||||
color-thief = "0.2.2"
|
||||
miette = { version = "5.7.0", features = ["fancy"] }
|
||||
image = "0.24.6"
|
||||
clap = { version = "4.2.1", features = ["derive"] }
|
||||
image = "0.24.7"
|
||||
miette = { version = "5.10.0", features = ["fancy"] }
|
||||
nalgebra = "0.32.3"
|
||||
okolors = "0.4.0"
|
||||
pastel = "0.9.0"
|
||||
|
|
18
flake.lock
18
flake.lock
|
@ -21,11 +21,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1694343207,
|
||||
"narHash": "sha256-jWi7OwFxU5Owi4k2JmiL1sa/OuBCQtpaAesuj5LXC8w=",
|
||||
"lastModified": 1696234590,
|
||||
"narHash": "sha256-mgOzQYTvaTT4bFopVOadlndy2RPwLy60rDjIWOGujwo=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "78058d810644f5ed276804ce7ea9e82d92bee293",
|
||||
"rev": "f902cb49892d300ff15cb237e48aa1cad79d68c3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -35,11 +35,11 @@
|
|||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1694343207,
|
||||
"narHash": "sha256-jWi7OwFxU5Owi4k2JmiL1sa/OuBCQtpaAesuj5LXC8w=",
|
||||
"lastModified": 1696234590,
|
||||
"narHash": "sha256-mgOzQYTvaTT4bFopVOadlndy2RPwLy60rDjIWOGujwo=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "78058d810644f5ed276804ce7ea9e82d92bee293",
|
||||
"rev": "f902cb49892d300ff15cb237e48aa1cad79d68c3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -76,11 +76,11 @@
|
|||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1692799911,
|
||||
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
|
||||
"lastModified": 1694529238,
|
||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
|
||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
84
src/main.rs
84
src/main.rs
|
@ -1,18 +1,22 @@
|
|||
use clap::Parser;
|
||||
use color_thief::ColorFormat;
|
||||
use image::io::Reader as ImageReader;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use pastel::ansi::{self, Brush, ToAnsiStyle};
|
||||
use std::io::{stdout, IsTerminal};
|
||||
use utils::{generate_palette, validate_color_delta, Backend};
|
||||
|
||||
mod utils;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about)]
|
||||
struct Args {
|
||||
pub struct Args {
|
||||
/// Path to image to pick colors from
|
||||
image: String,
|
||||
/// Number of colors to generate (excluding bold colors)
|
||||
#[arg(short, long, default_value_t = 8)]
|
||||
colors: u8,
|
||||
/// Path to image to pick colors from
|
||||
image: String,
|
||||
/// Generate a light colorscheme
|
||||
#[arg(short, long)]
|
||||
light: bool,
|
||||
/// Skip generating bold color variants
|
||||
#[arg(short = 'b', long)]
|
||||
no_bold: bool,
|
||||
|
@ -22,61 +26,31 @@ struct Args {
|
|||
/// Rotate colors along the hue axis
|
||||
#[arg(long, default_value_t = 0.0, allow_hyphen_values = true)]
|
||||
rotate_hue: f64,
|
||||
/// Lighten/darken colors
|
||||
#[arg(long, default_value_t = 0.0, value_parser = validate_color_delta, allow_hyphen_values = true)]
|
||||
lighten: f64,
|
||||
/// Saturate/desaturate colors
|
||||
#[arg(long, default_value_t = 0.0, value_parser = validate_color_delta, allow_hyphen_values = true)]
|
||||
saturate: f64,
|
||||
/// Generate a light colorscheme
|
||||
#[arg(short, long)]
|
||||
light: bool,
|
||||
/// Lighten/darken colors
|
||||
#[arg(long, default_value_t = 0.0, value_parser = validate_color_delta, allow_hyphen_values = true)]
|
||||
lighten: f64,
|
||||
/// Do not darken the background and lighten the foreground colors
|
||||
#[arg(long)]
|
||||
no_adjust: bool,
|
||||
/// Which algorithm to use
|
||||
#[arg(long, value_enum, rename_all = "PascalCase", default_value_t = Backend::Okolors)]
|
||||
backend: Backend,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
// Parse image into a list of pixels
|
||||
let pixels = ImageReader::open(args.image)
|
||||
.into_diagnostic()?
|
||||
.decode()
|
||||
.into_diagnostic()?
|
||||
.into_bytes();
|
||||
// Open image file
|
||||
let image = image::open(&args.image).into_diagnostic()?;
|
||||
|
||||
// Generate colorscheme
|
||||
let mut colors: Vec<_> =
|
||||
color_thief::get_palette(pixels.as_ref(), ColorFormat::Rgb, 1, args.colors + 1)
|
||||
.into_diagnostic()?
|
||||
.into_iter()
|
||||
.map(|c| pastel::Color::from_rgb(c.r, c.g, c.b))
|
||||
.collect();
|
||||
|
||||
// Sort colors by luminance
|
||||
colors.sort_by_key(|c| (c.luminance() * 1000.0) as i32);
|
||||
|
||||
if !args.no_bold {
|
||||
// Create second pairs of lighter colors
|
||||
let bold_colors = colors.clone();
|
||||
let bold_colors = bold_colors.iter().map(|c| c.lighten(args.bold_delta));
|
||||
colors.extend(bold_colors);
|
||||
}
|
||||
|
||||
// Apply color transformations, if any
|
||||
let mut colors: Vec<_> = colors
|
||||
.into_iter()
|
||||
.map(|c| c.rotate_hue(args.rotate_hue))
|
||||
.map(|c| c.lighten(args.lighten))
|
||||
.map(|c| c.saturate(args.saturate))
|
||||
.collect();
|
||||
|
||||
// Light theme transformations
|
||||
if args.light {
|
||||
colors.reverse();
|
||||
}
|
||||
|
||||
let brush = Brush::from_mode(Some(ansi::Mode::TrueColor));
|
||||
// Generate the colorscheme from image
|
||||
let colors = generate_palette(&image, &args)?;
|
||||
|
||||
// Print colors with formatting turned off for pipes
|
||||
let brush = Brush::from_mode(Some(ansi::Mode::TrueColor));
|
||||
colors.iter().for_each(|c| {
|
||||
println!(
|
||||
"{}",
|
||||
|
@ -90,15 +64,3 @@ fn main() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Make sure that input value is between -1.0 and 1.0
|
||||
fn validate_color_delta(s: &str) -> Result<f64, String> {
|
||||
let num = s
|
||||
.parse()
|
||||
.map_err(|_| format!("`{s}` is not a valid floating point number"))?;
|
||||
if (-1.0..=1.0).contains(&num) {
|
||||
Ok(num)
|
||||
} else {
|
||||
Err(format!("{num} is not in range [-1.0,1.0]"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
use clap::ValueEnum;
|
||||
|
||||
use color_thief::ColorFormat;
|
||||
use image::DynamicImage;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use nalgebra::{matrix, ArrayStorage, Const, Matrix};
|
||||
use okolors::OklabCounts;
|
||||
use pastel::{Color, Fraction};
|
||||
|
||||
use crate::Args;
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug)]
|
||||
pub enum Backend {
|
||||
ColorThief,
|
||||
Okolors,
|
||||
}
|
||||
|
||||
/// Make sure that input value is between -1.0 and 1.0
|
||||
pub fn validate_color_delta(s: &str) -> Result<f64, String> {
|
||||
let num = s
|
||||
.parse()
|
||||
.map_err(|_| format!("`{s}` is not a valid floating point number"))?;
|
||||
if (-1.0..=1.0).contains(&num) {
|
||||
Ok(num)
|
||||
} else {
|
||||
Err(format!("{num} is not in range [-1.0,1.0]"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_palette(image: &DynamicImage, args: &Args) -> Result<Vec<Color>> {
|
||||
match args.backend {
|
||||
Backend::ColorThief => generate_color_thief(image, args),
|
||||
Backend::Okolors => generate_okolors(image, args),
|
||||
}
|
||||
.map(|mut c| transform_colors(&mut c, args))
|
||||
}
|
||||
|
||||
/// Generate a palette using the color-thief algorithm
|
||||
fn generate_color_thief(image: &DynamicImage, args: &Args) -> Result<Vec<Color>> {
|
||||
let pixels = image.to_rgb8();
|
||||
|
||||
let colors: Vec<_> =
|
||||
color_thief::get_palette(pixels.as_ref(), ColorFormat::Rgb, 5, args.colors + 1)
|
||||
.into_diagnostic()?
|
||||
.into_iter()
|
||||
.map(|c| Color::from_rgb(c.r, c.g, c.b))
|
||||
.collect();
|
||||
|
||||
Ok(colors)
|
||||
}
|
||||
|
||||
/// Generate a palette using `okolors`
|
||||
fn generate_okolors(image: &DynamicImage, args: &Args) -> Result<Vec<Color>> {
|
||||
let oklab = OklabCounts::try_from_image(image, u8::MAX).into_diagnostic()?;
|
||||
|
||||
let colors: Vec<_> = okolors::run(&oklab, 5, args.colors, 0.05, 128, 0)
|
||||
.centroids
|
||||
.into_iter()
|
||||
.map(|c| oklab_to_xyz(c.l, c.a, c.b))
|
||||
.map(|c| Color::from_xyz(c.0 as f64, c.1 as f64, c.2 as f64, 1.0))
|
||||
.collect();
|
||||
|
||||
Ok(colors)
|
||||
}
|
||||
|
||||
fn transform_colors(colors: &mut Vec<Color>, args: &Args) -> Vec<Color> {
|
||||
// Sort colors by luminance
|
||||
colors.sort_by_key(|c| (c.luminance() * 1000.0) as i32);
|
||||
|
||||
if !args.no_adjust {
|
||||
// Darken the darkest color
|
||||
colors[0] = colors[0].mix::<pastel::Lab>(&Color::black(), Fraction::from(0.9));
|
||||
// Lighten the lightest color
|
||||
if let Some(c) = colors.last_mut() {
|
||||
*c = c.mix::<pastel::Lab>(&Color::white(), Fraction::from(0.9))
|
||||
}
|
||||
}
|
||||
|
||||
if !args.no_bold {
|
||||
// Create second pairs of lighter colors
|
||||
let bold_colors = colors.clone();
|
||||
let bold_colors = bold_colors.iter().map(|c| c.lighten(args.bold_delta));
|
||||
colors.extend(bold_colors);
|
||||
}
|
||||
|
||||
// Apply color transformations, if any
|
||||
let mut colors: Vec<_> = colors
|
||||
.iter_mut()
|
||||
.map(|c| c.saturate(args.saturate))
|
||||
.collect();
|
||||
|
||||
// Light theme transformations
|
||||
if args.light {
|
||||
colors.reverse();
|
||||
}
|
||||
|
||||
colors
|
||||
}
|
||||
|
||||
/// Convert Oklab coordinates to XYZ coordinates according to
|
||||
/// https://bottosson.github.io/posts/oklab
|
||||
fn oklab_to_xyz(l: f32, a: f32, b: f32) -> (f32, f32, f32) {
|
||||
// Pre-computed inversions of the M₁ and M₂ matrices
|
||||
const M1_INV: Matrix<f32, Const<3>, Const<3>, ArrayStorage<f32, 3, 3>> = matrix![
|
||||
1.227014, -0.5578, 0.28125617;
|
||||
-0.04058018, 1.1122569, -0.07167668;
|
||||
-0.07638129, -0.421482, 1.5861632;
|
||||
];
|
||||
|
||||
const M2_INV: Matrix<f32, Const<3>, Const<3>, ArrayStorage<f32, 3, 3>> = matrix![
|
||||
1.0000001, 0.3963378, 0.21580376;
|
||||
1.0, -0.105561346, -0.06385418;
|
||||
1.0000001, -0.08948418, -1.2914855;
|
||||
];
|
||||
|
||||
let l_m_s_ = M2_INV * matrix![l; a; b];
|
||||
|
||||
let lms = l_m_s_.map(|v| v.powi(3));
|
||||
|
||||
let xyz = M1_INV * lms;
|
||||
|
||||
(xyz.x, xyz.y, xyz.z)
|
||||
}
|
Loading…
Reference in New Issue