├── clippy.toml ├── .gitignore ├── doc ├── pastel.gif ├── colorcheck.png ├── demo-scripts │ └── gradient.sh └── colorcheck.md ├── src ├── colorspace.rs ├── cli │ ├── commands │ │ ├── show.rs │ │ ├── prelude.rs │ │ ├── gray.rs │ │ ├── traits.rs │ │ ├── pick.rs │ │ ├── random.rs │ │ ├── list.rs │ │ ├── sort.rs │ │ ├── gradient.rs │ │ ├── paint.rs │ │ ├── colorcheck.rs │ │ ├── format.rs │ │ ├── mod.rs │ │ ├── io.rs │ │ ├── color_commands.rs │ │ └── distinct.rs │ ├── config.rs │ ├── utility.rs │ ├── colorspace.rs │ ├── error.rs │ ├── colorpicker.rs │ ├── output.rs │ ├── hdcanvas.rs │ ├── main.rs │ ├── colorpicker_tools.rs │ └── cli.rs ├── types.rs ├── random.rs ├── helper.rs ├── named.rs ├── delta_e.rs ├── ansi.rs ├── distinct.rs └── parser.rs ├── benches └── parse_color.rs ├── LICENSE-MIT ├── Cargo.toml ├── tests └── integration_tests.rs ├── CHANGELOG.md ├── README.md ├── LICENSE-APACHE └── .github └── workflows └── CICD.yml /clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.60.0" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /doc/pastel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharyvoase/pastel/master/doc/pastel.gif -------------------------------------------------------------------------------- /doc/colorcheck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharyvoase/pastel/master/doc/colorcheck.png -------------------------------------------------------------------------------- /src/colorspace.rs: -------------------------------------------------------------------------------- 1 | use crate::helper::Fraction; 2 | use crate::Color; 3 | 4 | pub trait ColorSpace { 5 | fn from_color(c: &Color) -> Self; 6 | fn into_color(self) -> Color; 7 | 8 | fn mix(&self, other: &Self, fraction: Fraction) -> Self; 9 | } 10 | -------------------------------------------------------------------------------- /src/cli/commands/show.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::prelude::*; 2 | 3 | pub struct ShowCommand; 4 | 5 | impl ColorCommand for ShowCommand { 6 | fn run(&self, out: &mut Output, _: &ArgMatches, config: &Config, color: &Color) -> Result<()> { 7 | out.show_color(config, color) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/cli/config.rs: -------------------------------------------------------------------------------- 1 | use pastel::ansi::Brush; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct Config<'p> { 5 | pub padding: usize, 6 | pub colorpicker_width: usize, 7 | pub colorcheck_width: usize, 8 | pub colorpicker: Option<&'p str>, 9 | pub interactive_mode: bool, 10 | pub brush: Brush, 11 | } 12 | -------------------------------------------------------------------------------- /src/cli/commands/prelude.rs: -------------------------------------------------------------------------------- 1 | pub use crate::config::Config; 2 | pub use crate::error::{PastelError, Result}; 3 | pub use crate::output::Output; 4 | 5 | pub use clap::ArgMatches; 6 | 7 | pub use super::io::*; 8 | pub use super::traits::*; 9 | 10 | pub use pastel::ansi::{AnsiColor, Brush, ToAnsiStyle}; 11 | pub use pastel::Color; 12 | -------------------------------------------------------------------------------- /src/cli/commands/gray.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::prelude::*; 2 | 3 | use pastel::Color; 4 | 5 | pub struct GrayCommand; 6 | 7 | impl GenericCommand for GrayCommand { 8 | fn run(&self, out: &mut Output, matches: &ArgMatches, config: &Config) -> Result<()> { 9 | let lightness = number_arg(matches, "lightness")?; 10 | let gray = Color::graytone(lightness); 11 | out.show_color(config, &gray) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/cli/utility.rs: -------------------------------------------------------------------------------- 1 | use pastel::named::{NamedColor, NAMED_COLORS}; 2 | use pastel::Color; 3 | 4 | /// Returns a list of named colors, sorted by the perceived distance to the given color 5 | pub fn similar_colors(color: &Color) -> Vec<&NamedColor> { 6 | let mut colors: Vec<&NamedColor> = NAMED_COLORS.iter().collect(); 7 | colors.sort_by_key(|nc| (1000.0 * nc.color.distance_delta_e_ciede2000(color)) as i32); 8 | colors.dedup_by(|n1, n2| n1.color == n2.color); 9 | colors 10 | } 11 | -------------------------------------------------------------------------------- /src/cli/commands/traits.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::output::Output; 3 | use crate::Result; 4 | 5 | use clap::ArgMatches; 6 | 7 | use pastel::Color; 8 | 9 | pub trait GenericCommand { 10 | fn run(&self, out: &mut Output, matches: &ArgMatches, config: &Config) -> Result<()>; 11 | } 12 | 13 | pub trait ColorCommand { 14 | fn run( 15 | &self, 16 | out: &mut Output, 17 | matches: &ArgMatches, 18 | config: &Config, 19 | color: &Color, 20 | ) -> Result<()>; 21 | } 22 | -------------------------------------------------------------------------------- /doc/demo-scripts/gradient.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function gradient() { 4 | local color_from="$1" 5 | local color_to="$2" 6 | local text="$3" 7 | local length=${#text} 8 | 9 | local colors 10 | colors=$(pastel gradient -n "$length" "$color_from" "$color_to" -sLCh) 11 | 12 | local i=0 13 | for color in $colors; do 14 | pastel paint -n "$color" "${text:$i:1}" 15 | i=$((i+1)) 16 | done 17 | printf "\n" 18 | } 19 | 20 | 21 | gradient yellow crimson 'look at these colors!' 22 | gradient lightseagreen lightgreen 'look at these colors!' 23 | -------------------------------------------------------------------------------- /src/cli/colorspace.rs: -------------------------------------------------------------------------------- 1 | use pastel::Color; 2 | use pastel::{Fraction, LCh, Lab, HSLA, RGBA}; 3 | 4 | pub fn get_mixing_function( 5 | colorspace_name: &str, 6 | ) -> Box Color> { 7 | match colorspace_name.to_lowercase().as_ref() { 8 | "rgb" => Box::new(|c1: &Color, c2: &Color, f: Fraction| c1.mix::>(c2, f)), 9 | "hsl" => Box::new(|c1: &Color, c2: &Color, f: Fraction| c1.mix::(c2, f)), 10 | "lab" => Box::new(|c1: &Color, c2: &Color, f: Fraction| c1.mix::(c2, f)), 11 | "lch" => Box::new(|c1: &Color, c2: &Color, f: Fraction| c1.mix::(c2, f)), 12 | _ => unreachable!("Unknown color space"), 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /benches/parse_color.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use pastel::parser::parse_color; 3 | 4 | fn criterion_benchmark(c: &mut Criterion) { 5 | c.bench_function("parse_hex", |b| { 6 | b.iter(|| { 7 | parse_color("#ff0077"); 8 | }) 9 | }); 10 | c.bench_function("parse_hex_short", |b| { 11 | b.iter(|| { 12 | parse_color("#f07"); 13 | }) 14 | }); 15 | c.bench_function("parse_rgb", |b| { 16 | b.iter(|| { 17 | parse_color("rgb(255, 125, 0)"); 18 | }) 19 | }); 20 | c.bench_function("parse_hsl", |b| b.iter(|| parse_color("hsl(280,20%,50%)"))); 21 | } 22 | 23 | criterion_group!(benches, criterion_benchmark); 24 | criterion_main!(benches); 25 | -------------------------------------------------------------------------------- /doc/colorcheck.md: -------------------------------------------------------------------------------- 1 | # colorcheck 2 | 3 | The `colorcheck` command can be used to test whether or not your terminal emulator 4 | properly supports 24-bit true color mode (16,777,216 colors). If this is not the case, `pastel` 5 | can only show 8-bit approximations (256 colors): 6 | 7 | ``` bash 8 | pastel colorcheck 9 | ``` 10 | 11 | If everything works correctly, the output should look likes this (the background color and font 12 | colors may be different, but the color panels should look the same): 13 | 14 | ![](colorcheck.png) 15 | 16 | If you find it hard to compare the colors visually, you can also use a colorpicker (`pastel pick`) 17 | to make sure that the three colors in the 24-bit panels are (from left to right): 18 | 19 | * `#492732`, `rgb(73, 39, 50)` 20 | * `#10331e`, `rgb(16, 51, 30)` 21 | * `#1d365a`, `rgb(29, 54, 90)` 22 | -------------------------------------------------------------------------------- /src/cli/commands/pick.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::prelude::*; 2 | 3 | use crate::colorpicker::{print_colorspectrum, run_external_colorpicker}; 4 | 5 | pub struct PickCommand; 6 | 7 | impl GenericCommand for PickCommand { 8 | fn run(&self, out: &mut Output, matches: &ArgMatches, config: &Config) -> Result<()> { 9 | let count = matches.value_of("count").expect("required argument"); 10 | let count = count 11 | .parse::() 12 | .map_err(|_| PastelError::CouldNotParseNumber(count.into()))?; 13 | 14 | print_colorspectrum(config)?; 15 | 16 | let mut color_strings = Vec::new(); 17 | for _ in 0..count { 18 | color_strings.push(run_external_colorpicker(config.colorpicker)?); 19 | } 20 | 21 | let mut print_spectrum = PrintSpectrum::No; 22 | 23 | for color_str in color_strings { 24 | let color = ColorArgIterator::from_color_arg(config, &color_str, &mut print_spectrum)?; 25 | out.show_color(config, &color)?; 26 | } 27 | 28 | Ok(()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. 18 | -------------------------------------------------------------------------------- /src/cli/commands/random.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::prelude::*; 2 | 3 | use pastel::random::strategies; 4 | use pastel::random::RandomizationStrategy; 5 | 6 | pub struct RandomCommand; 7 | 8 | impl GenericCommand for RandomCommand { 9 | fn run(&self, out: &mut Output, matches: &ArgMatches, config: &Config) -> Result<()> { 10 | let strategy_arg = matches.value_of("strategy").expect("required argument"); 11 | 12 | let count = matches.value_of("number").expect("required argument"); 13 | let count = count 14 | .parse::() 15 | .map_err(|_| PastelError::CouldNotParseNumber(count.into()))?; 16 | 17 | let mut strategy: Box = match strategy_arg { 18 | "vivid" => Box::new(strategies::Vivid), 19 | "rgb" => Box::new(strategies::UniformRGB), 20 | "gray" => Box::new(strategies::UniformGray), 21 | "lch_hue" => Box::new(strategies::UniformHueLCh), 22 | _ => unreachable!("Unknown randomization strategy"), 23 | }; 24 | 25 | for _ in 0..count { 26 | out.show_color(config, &strategy.generate())?; 27 | } 28 | 29 | Ok(()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use crate::helper::mod_positive; 2 | 3 | pub type Scalar = f64; 4 | 5 | /// The hue of a color, represented by an angle (degrees). 6 | #[derive(Debug, Clone, Copy)] 7 | pub struct Hue { 8 | unclipped: Scalar, 9 | } 10 | 11 | impl Hue { 12 | pub fn from(unclipped: Scalar) -> Hue { 13 | Hue { unclipped } 14 | } 15 | 16 | /// Return a hue value in the interval [0, 360]. 17 | pub fn value(self) -> Scalar { 18 | #![allow(clippy::float_cmp)] 19 | if self.unclipped == 360.0 { 20 | self.unclipped 21 | } else { 22 | mod_positive(self.unclipped, 360.0) 23 | } 24 | } 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use super::*; 30 | use approx::assert_relative_eq; 31 | 32 | #[test] 33 | fn test_mod_positive() { 34 | assert_relative_eq!(0.5, mod_positive(2.9, 2.4)); 35 | assert_relative_eq!(1.7, mod_positive(-0.3, 2.0)); 36 | } 37 | 38 | #[test] 39 | fn test_hue_clipping() { 40 | assert_eq!(43.0, Hue::from(43.0).value()); 41 | assert_eq!(13.0, Hue::from(373.0).value()); 42 | assert_eq!(300.0, Hue::from(-60.0).value()); 43 | assert_eq!(360.0, Hue::from(360.0).value()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["David Peter "] 3 | categories = ["command-line-utilities"] 4 | description = "A command-line tool to generate, analyze, convert and manipulate colors" 5 | homepage = "https://github.com/sharkdp/pastel" 6 | license = "MIT/Apache-2.0" 7 | name = "pastel" 8 | readme = "README.md" 9 | repository = "https://github.com/sharkdp/pastel" 10 | version = "0.9.0" 11 | edition = "2021" 12 | build = "build.rs" 13 | exclude = ["doc/pastel.gif"] 14 | 15 | [dependencies] 16 | # library dependencies 17 | atty = "0.2" 18 | nom = "7.0.0" 19 | once_cell = "1.9.0" 20 | output_vt100 = "0.1" 21 | rand = "0.8" 22 | 23 | # binary-only dependencies (see https://github.com/rust-lang/cargo/issues/1982) 24 | regex = "1.5" 25 | 26 | [dependencies.clap] 27 | version = "3" 28 | features = ["suggestions", "color", "wrap_help", "cargo"] 29 | 30 | [build-dependencies] 31 | clap = { version = "3", features = ["cargo"] } 32 | clap_complete = "3" 33 | once_cell = "1.9.0" 34 | output_vt100 = "0.1" 35 | 36 | [[bin]] 37 | name = "pastel" 38 | path = "src/cli/main.rs" 39 | 40 | 41 | [dev-dependencies] 42 | approx = "0.5.0" 43 | assert_cmd = "2.0.0" 44 | rand_xoshiro = "0.6.0" 45 | criterion = "0.3" 46 | 47 | [[bench]] 48 | name = "parse_color" 49 | harness = false 50 | -------------------------------------------------------------------------------- /src/cli/commands/list.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::prelude::*; 2 | use crate::commands::sort::key_function; 3 | 4 | use pastel::ansi::ToAnsiStyle; 5 | use pastel::named::{NamedColor, NAMED_COLORS}; 6 | 7 | pub struct ListCommand; 8 | 9 | impl GenericCommand for ListCommand { 10 | fn run(&self, out: &mut Output, matches: &ArgMatches, config: &Config) -> Result<()> { 11 | let sort_order = matches.value_of("sort-order").expect("required argument"); 12 | 13 | let mut colors: Vec<&NamedColor> = NAMED_COLORS.iter().collect(); 14 | colors.sort_by_key(|nc| key_function(sort_order, &nc.color)); 15 | colors.dedup_by(|n1, n2| n1.color == n2.color); 16 | 17 | if config.interactive_mode { 18 | for nc in colors { 19 | let bg = &nc.color; 20 | let fg = bg.text_color(); 21 | writeln!( 22 | out.handle, 23 | "{}", 24 | config 25 | .brush 26 | .paint(format!(" {:24}", nc.name), fg.ansi_style().on(bg)) 27 | )?; 28 | } 29 | } else { 30 | for nc in colors { 31 | let res = writeln!(out.handle, "{}", nc.name); 32 | if res.is_err() { 33 | break; 34 | } 35 | } 36 | } 37 | 38 | Ok(()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/cli/commands/sort.rs: -------------------------------------------------------------------------------- 1 | use rand::prelude::*; 2 | 3 | use crate::commands::prelude::*; 4 | 5 | pub struct SortCommand; 6 | 7 | pub fn key_function(sort_order: &str, color: &Color) -> i32 { 8 | match sort_order { 9 | "brightness" => (color.brightness() * 1000.0) as i32, 10 | "luminance" => (color.luminance() * 1000.0) as i32, 11 | "hue" => (color.to_lch().h * 1000.0) as i32, 12 | "chroma" => (color.to_lch().c * 1000.0) as i32, 13 | "random" => random(), 14 | _ => unreachable!("Unknown sort order"), 15 | } 16 | } 17 | 18 | impl GenericCommand for SortCommand { 19 | fn run(&self, out: &mut Output, matches: &ArgMatches, config: &Config) -> Result<()> { 20 | let sort_order = matches.value_of("sort-order").expect("required argument"); 21 | 22 | let mut colors: Vec = vec![]; 23 | for color in ColorArgIterator::from_args(config, matches.values_of("color"))? { 24 | colors.push(color?); 25 | } 26 | 27 | if matches.is_present("unique") { 28 | colors.sort_by_key(|c| c.to_u32()); 29 | colors.dedup_by_key(|c| c.to_u32()); 30 | } 31 | 32 | colors.sort_by_key(|c| key_function(sort_order, c)); 33 | 34 | if matches.is_present("reverse") { 35 | colors.reverse(); 36 | } 37 | 38 | for color in colors { 39 | out.show_color(config, &color)?; 40 | } 41 | 42 | Ok(()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/random.rs: -------------------------------------------------------------------------------- 1 | use crate::Color; 2 | 3 | use rand::prelude::*; 4 | 5 | pub trait RandomizationStrategy { 6 | fn generate(&mut self) -> Color { 7 | self.generate_with(&mut thread_rng()) 8 | } 9 | 10 | fn generate_with(&mut self, r: &mut dyn RngCore) -> Color; 11 | } 12 | 13 | pub mod strategies { 14 | use super::RandomizationStrategy; 15 | use crate::Color; 16 | 17 | use rand::prelude::*; 18 | 19 | pub struct Vivid; 20 | 21 | impl RandomizationStrategy for Vivid { 22 | fn generate_with(&mut self, rng: &mut dyn RngCore) -> Color { 23 | let hue = rng.gen::() * 360.0; 24 | let saturation = 0.2 + 0.6 * rng.gen::(); 25 | let lightness = 0.3 + 0.4 * rng.gen::(); 26 | 27 | Color::from_hsl(hue, saturation, lightness) 28 | } 29 | } 30 | 31 | pub struct UniformRGB; 32 | 33 | impl RandomizationStrategy for UniformRGB { 34 | fn generate_with(&mut self, rng: &mut dyn RngCore) -> Color { 35 | Color::from_rgb(rng.gen::(), rng.gen::(), rng.gen::()) 36 | } 37 | } 38 | 39 | pub struct UniformGray; 40 | 41 | impl RandomizationStrategy for UniformGray { 42 | fn generate_with(&mut self, rng: &mut dyn RngCore) -> Color { 43 | Color::graytone(rng.gen::()) 44 | } 45 | } 46 | 47 | pub struct UniformHueLCh; 48 | 49 | impl RandomizationStrategy for UniformHueLCh { 50 | fn generate_with(&mut self, rng: &mut dyn RngCore) -> Color { 51 | Color::from_lch(70.0, 35.0, 360.0 * rng.gen::(), 1.0) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/cli/commands/gradient.rs: -------------------------------------------------------------------------------- 1 | use crate::colorspace::get_mixing_function; 2 | use crate::commands::prelude::*; 3 | 4 | use pastel::ColorScale; 5 | use pastel::Fraction; 6 | 7 | pub struct GradientCommand; 8 | 9 | impl GenericCommand for GradientCommand { 10 | fn run(&self, out: &mut Output, matches: &ArgMatches, config: &Config) -> Result<()> { 11 | let count = matches.value_of("number").expect("required argument"); 12 | let count = count 13 | .parse::() 14 | .map_err(|_| PastelError::CouldNotParseNumber(count.into()))?; 15 | if count < 2 { 16 | return Err(PastelError::GradientNumberMustBeLargerThanOne); 17 | } 18 | 19 | let mut print_spectrum = PrintSpectrum::Yes; 20 | 21 | let mix = get_mixing_function(matches.value_of("colorspace").expect("required argument")); 22 | 23 | let colors = matches 24 | .values_of("color") 25 | .expect("required argument") 26 | .map(|color| ColorArgIterator::from_color_arg(config, color, &mut print_spectrum)); 27 | 28 | let color_count = colors.len(); 29 | if color_count < 2 { 30 | return Err(PastelError::GradientColorCountMustBeLargerThanOne); 31 | } 32 | 33 | let mut color_scale = ColorScale::empty(); 34 | 35 | for (i, color) in colors.enumerate() { 36 | let position = Fraction::from(i as f64 / (color_count as f64 - 1.0)); 37 | 38 | color_scale.add_stop(color?, position); 39 | } 40 | 41 | for i in 0..count { 42 | let position = Fraction::from(i as f64 / (count as f64 - 1.0)); 43 | 44 | let color = color_scale.sample(position, &mix).expect("gradient color"); 45 | 46 | out.show_color(config, &color)?; 47 | } 48 | 49 | Ok(()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/cli/commands/paint.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Read}; 2 | 3 | use crate::commands::prelude::*; 4 | 5 | use super::io::ColorArgIterator; 6 | 7 | use pastel::ansi::Style; 8 | use pastel::parser::parse_color; 9 | 10 | pub struct PaintCommand; 11 | 12 | impl GenericCommand for PaintCommand { 13 | fn run(&self, out: &mut Output, matches: &ArgMatches, config: &Config) -> Result<()> { 14 | let fg = matches.value_of("color").expect("required argument"); 15 | let fg = if fg.trim() == "default" { 16 | None 17 | } else { 18 | let mut print_spectrum = PrintSpectrum::Yes; 19 | Some(ColorArgIterator::from_color_arg( 20 | config, 21 | fg, 22 | &mut print_spectrum, 23 | )?) 24 | }; 25 | 26 | let bg = if let Some(bg) = matches.value_of("on") { 27 | Some(parse_color(bg).ok_or_else(|| PastelError::ColorParseError(bg.into()))?) 28 | } else { 29 | None 30 | }; 31 | 32 | let text = match matches.values_of("text") { 33 | Some(values) => values.map(|v| v.to_string()).collect::>().join(" "), 34 | _ => { 35 | let mut buffer = String::new(); 36 | io::stdin().read_to_string(&mut buffer)?; 37 | buffer 38 | } 39 | }; 40 | 41 | let mut style = Style::default(); 42 | 43 | if let Some(fg) = fg { 44 | style.foreground(&fg); 45 | } 46 | 47 | if let Some(bg) = bg { 48 | style.on(bg); 49 | } 50 | 51 | style.bold(matches.is_present("bold")); 52 | style.italic(matches.is_present("italic")); 53 | style.underline(matches.is_present("underline")); 54 | 55 | write!( 56 | out.handle, 57 | "{}{}", 58 | config.brush.paint(text, style), 59 | if matches.is_present("no-newline") { 60 | "" 61 | } else { 62 | "\n" 63 | } 64 | )?; 65 | 66 | Ok(()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/cli/commands/colorcheck.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::prelude::*; 2 | use crate::hdcanvas::Canvas; 3 | 4 | use pastel::ansi::{Brush, Mode}; 5 | 6 | pub struct ColorCheckCommand; 7 | 8 | fn print_board(out: &mut Output, config: &Config, mode: Mode) -> Result<()> { 9 | // These colors have been chosen/computed such that the perceived color difference (CIE delta-E 10 | // 2000) to the closest ANSI 8-bit color is maximal. 11 | let c1 = Color::from_rgb(73, 39, 50); 12 | let c2 = Color::from_rgb(16, 51, 30); 13 | let c3 = Color::from_rgb(29, 54, 90); 14 | 15 | let width = config.colorcheck_width; 16 | 17 | let mut canvas = Canvas::new( 18 | width + 2 * config.padding, 19 | 3 * width + 3 * config.padding, 20 | Brush::from_mode(Some(mode)), 21 | ); 22 | 23 | canvas.draw_rect(config.padding, config.padding, width, width, &c1); 24 | 25 | canvas.draw_rect( 26 | config.padding, 27 | 2 * config.padding + width, 28 | width, 29 | width, 30 | &c2, 31 | ); 32 | 33 | canvas.draw_rect( 34 | config.padding, 35 | 3 * config.padding + 2 * width, 36 | width, 37 | width, 38 | &c3, 39 | ); 40 | 41 | canvas.print(out.handle) 42 | } 43 | 44 | impl GenericCommand for ColorCheckCommand { 45 | fn run(&self, out: &mut Output, _: &ArgMatches, config: &Config) -> Result<()> { 46 | writeln!(out.handle, "\n8-bit mode:")?; 47 | print_board(out, config, Mode::Ansi8Bit)?; 48 | 49 | writeln!(out.handle, "24-bit mode:")?; 50 | print_board(out, config, Mode::TrueColor)?; 51 | 52 | writeln!( 53 | out.handle, 54 | "If your terminal emulator supports 24-bit colors, you should see three square color \ 55 | panels in the lower row and the colors should look similar (but slightly different \ 56 | from) the colors in the top row panels.\nThe panels in the lower row should look \ 57 | like squares that are filled with a uniform color (no stripes or other artifacts).\n\ 58 | \n\ 59 | You can also open https://github.com/sharkdp/pastel/blob/master/doc/colorcheck.md in \ 60 | a browser to compare how the output should look like." 61 | )?; 62 | 63 | Ok(()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/cli/commands/format.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::prelude::*; 2 | use crate::utility::similar_colors; 3 | 4 | use pastel::ansi::Mode; 5 | use pastel::Format; 6 | 7 | pub struct FormatCommand; 8 | 9 | impl ColorCommand for FormatCommand { 10 | fn run( 11 | &self, 12 | out: &mut Output, 13 | matches: &ArgMatches, 14 | config: &Config, 15 | color: &Color, 16 | ) -> Result<()> { 17 | let format_type = matches.value_of("type").expect("required argument"); 18 | let format_type = format_type.to_lowercase(); 19 | 20 | let replace_escape = |code: &str| code.replace('\x1b', "\\x1b"); 21 | 22 | let output = match format_type.as_ref() { 23 | "rgb" => color.to_rgb_string(Format::Spaces), 24 | "rgb-float" => color.to_rgb_float_string(Format::Spaces), 25 | "hex" => color.to_rgb_hex_string(true), 26 | "hsl" => color.to_hsl_string(Format::Spaces), 27 | "hsl-hue" => format!("{:.0}", color.to_hsla().h), 28 | "hsl-saturation" => format!("{:.4}", color.to_hsla().s), 29 | "hsl-lightness" => format!("{:.4}", color.to_hsla().l), 30 | "hsv" => color.to_hsv_string(Format::Spaces), 31 | "hsv-hue" => format!("{:.0}", color.to_hsva().h), 32 | "hsv-saturation" => format!("{:.4}", color.to_hsva().s), 33 | "hsv-value" => format!("{:.4}", color.to_hsva().v), 34 | "lch" => color.to_lch_string(Format::Spaces), 35 | "lch-lightness" => format!("{:.2}", color.to_lch().l), 36 | "lch-chroma" => format!("{:.2}", color.to_lch().c), 37 | "lch-hue" => format!("{:.2}", color.to_lch().h), 38 | "lab" => color.to_lab_string(Format::Spaces), 39 | "lab-a" => format!("{:.2}", color.to_lab().a), 40 | "lab-b" => format!("{:.2}", color.to_lab().b), 41 | "luminance" => format!("{:.3}", color.luminance()), 42 | "brightness" => format!("{:.3}", color.brightness()), 43 | "ansi-8bit" => replace_escape(&color.to_ansi_sequence(Mode::Ansi8Bit)), 44 | "ansi-24bit" => replace_escape(&color.to_ansi_sequence(Mode::TrueColor)), 45 | "ansi-8bit-escapecode" => color.to_ansi_sequence(Mode::Ansi8Bit), 46 | "ansi-24bit-escapecode" => color.to_ansi_sequence(Mode::TrueColor), 47 | "cmyk" => color.to_cmyk_string(Format::Spaces), 48 | "name" => similar_colors(color)[0].name.to_owned(), 49 | &_ => { 50 | unreachable!("Unknown format type"); 51 | } 52 | }; 53 | 54 | let write_colored_line = !matches!( 55 | format_type.as_ref(), 56 | "ansi-8bit-escapecode" | "ansi-24bit-escapecode" 57 | ); 58 | 59 | if write_colored_line { 60 | writeln!( 61 | out.handle, 62 | "{}", 63 | config 64 | .brush 65 | .paint(output, color.text_color().ansi_style().on(color)) 66 | )?; 67 | } else { 68 | write!(out.handle, "{}", output)?; 69 | } 70 | 71 | Ok(()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::Command; 2 | 3 | fn pastel() -> Command { 4 | let mut cmd = Command::cargo_bin("pastel").unwrap(); 5 | cmd.env_remove("PASTEL_COLOR_MODE"); 6 | cmd 7 | } 8 | 9 | #[test] 10 | fn color_reads_colors_from_args() { 11 | pastel() 12 | .arg("color") 13 | .arg("red") 14 | .assert() 15 | .success() 16 | .stdout("hsl(0,100.0%,50.0%)\n"); 17 | 18 | pastel() 19 | .arg("color") 20 | .arg("red") 21 | .arg("blue") 22 | .assert() 23 | .success() 24 | .stdout("hsl(0,100.0%,50.0%)\nhsl(240,100.0%,50.0%)\n"); 25 | 26 | pastel().arg("color").arg("no color").assert().failure(); 27 | } 28 | 29 | #[test] 30 | fn color_reads_colors_from_stdin() { 31 | pastel() 32 | .arg("color") 33 | .write_stdin("red\nblue\n") 34 | .assert() 35 | .success() 36 | .stdout("hsl(0,100.0%,50.0%)\nhsl(240,100.0%,50.0%)\n"); 37 | 38 | pastel() 39 | .arg("color") 40 | .write_stdin("no color") 41 | .assert() 42 | .failure(); 43 | } 44 | 45 | #[test] 46 | fn format_basic() { 47 | pastel() 48 | .arg("format") 49 | .arg("hex") 50 | .arg("red") 51 | .assert() 52 | .success() 53 | .stdout("#ff0000\n"); 54 | 55 | pastel() 56 | .arg("format") 57 | .arg("rgb") 58 | .arg("red") 59 | .arg("blue") 60 | .assert() 61 | .success() 62 | .stdout("rgb(255, 0, 0)\nrgb(0, 0, 255)\n"); 63 | } 64 | 65 | #[test] 66 | fn pipe_into_format_command() { 67 | let first = pastel() 68 | .arg("color") 69 | .arg("red") 70 | .arg("teal") 71 | .arg("hotpink") 72 | .assert() 73 | .success(); 74 | 75 | pastel() 76 | .arg("format") 77 | .arg("name") 78 | .write_stdin(String::from_utf8(first.get_output().stdout.clone()).unwrap()) 79 | .assert() 80 | .success() 81 | .stdout("red\nteal\nhotpink\n"); 82 | } 83 | 84 | #[test] 85 | fn sort_by_basic() { 86 | pastel() 87 | .arg("sort-by") 88 | .arg("luminance") 89 | .arg("gray") 90 | .arg("white") 91 | .arg("black") 92 | .assert() 93 | .success() 94 | .stdout("hsl(0,0.0%,0.0%)\nhsl(0,0.0%,50.2%)\nhsl(0,0.0%,100.0%)\n"); 95 | } 96 | 97 | #[test] 98 | fn set_basic() { 99 | pastel() 100 | .arg("set") 101 | .arg("hsl-hue") 102 | .arg("120") 103 | .arg("red") 104 | .assert() 105 | .success() 106 | .stdout("hsl(120,100.0%,50.0%)\n"); 107 | 108 | pastel() 109 | .arg("set") 110 | .arg("hsl-saturation") 111 | .arg("0.1") 112 | .arg("red") 113 | .assert() 114 | .success() 115 | .stdout("hsl(0,10.0%,50.0%)\n"); 116 | 117 | pastel() 118 | .arg("set") 119 | .arg("hsl-lightness") 120 | .arg("0.5") 121 | .arg("white") 122 | .assert() 123 | .success() 124 | .stdout("hsl(0,0.0%,50.0%)\n"); 125 | } 126 | -------------------------------------------------------------------------------- /src/cli/error.rs: -------------------------------------------------------------------------------- 1 | use crate::ansi; 2 | 3 | #[derive(Debug)] 4 | pub enum PastelError { 5 | UnknownColorMode(String), 6 | ColorParseError(String), 7 | ColorInvalidUTF8, 8 | CouldNotReadFromStdin, 9 | ColorArgRequired, 10 | CouldNotParseNumber(String), 11 | StdoutClosed, 12 | GradientNumberMustBeLargerThanOne, 13 | GradientColorCountMustBeLargerThanOne, 14 | DistinctColorCountMustBeLargerThanOne, 15 | DistinctColorFixedColorsCannotBeMoreThanCount, 16 | ColorPickerExecutionError(String), 17 | NoColorPickerFound, 18 | IoError(std::io::Error), 19 | } 20 | 21 | impl PastelError { 22 | pub fn message(&self) -> String { 23 | match self { 24 | PastelError::UnknownColorMode(mode) => { 25 | format!("Unknown PASTEL_COLOR_MODE value ({})", mode) 26 | } 27 | PastelError::ColorParseError(color) => format!("Could not parse color '{}'", color), 28 | PastelError::ColorInvalidUTF8 => "Color input contains invalid UTF8".into(), 29 | PastelError::CouldNotReadFromStdin => "Could not read color from standard input".into(), 30 | PastelError::ColorArgRequired => { 31 | "A color argument needs to be provided on the command line or via a pipe. \ 32 | Call this command again with '-h' or '--help' to get more information." 33 | .into() 34 | } 35 | PastelError::CouldNotParseNumber(number) => { 36 | format!("Could not parse number '{}'", number) 37 | } 38 | PastelError::StdoutClosed => "Output pipe has been closed".into(), 39 | PastelError::GradientNumberMustBeLargerThanOne => { 40 | "The specified color count must be larger than one".into() 41 | } 42 | PastelError::GradientColorCountMustBeLargerThanOne => { 43 | "The number of color arguments must be larger than one".into() 44 | } 45 | PastelError::DistinctColorCountMustBeLargerThanOne => { 46 | "The number of colors must be larger than one".into() 47 | } 48 | PastelError::DistinctColorFixedColorsCannotBeMoreThanCount => { 49 | "The number of fixed colors must be smaller than the total number of colors".into() 50 | } 51 | PastelError::ColorPickerExecutionError(name) => { 52 | format!("Error while running color picker '{}'", name) 53 | } 54 | PastelError::NoColorPickerFound => { 55 | "Could not find any external color picker tool. See 'pastel pick -h' for more information.".into() 56 | } 57 | PastelError::IoError(err) => format!("I/O error: {}", err), 58 | } 59 | } 60 | } 61 | 62 | impl From for PastelError { 63 | fn from(err: std::io::Error) -> PastelError { 64 | match err.kind() { 65 | std::io::ErrorKind::BrokenPipe => PastelError::StdoutClosed, 66 | _ => PastelError::IoError(err), 67 | } 68 | } 69 | } 70 | 71 | impl From for PastelError { 72 | fn from(err: ansi::UnknownColorModeError) -> PastelError { 73 | PastelError::UnknownColorMode(err.0) 74 | } 75 | } 76 | 77 | pub type Result = std::result::Result; 78 | -------------------------------------------------------------------------------- /src/cli/colorpicker.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | use std::process::Command; 3 | 4 | use crate::colorpicker_tools::COLOR_PICKER_TOOLS; 5 | use crate::config::Config; 6 | use crate::error::{PastelError, Result}; 7 | use crate::hdcanvas::Canvas; 8 | 9 | use pastel::ansi::{Brush, Stream}; 10 | use pastel::Color; 11 | 12 | /// Print a color spectrum to STDERR. 13 | pub fn print_colorspectrum(config: &Config) -> Result<()> { 14 | let width = config.colorpicker_width; 15 | 16 | let mut canvas = Canvas::new( 17 | width + 2 * config.padding, 18 | width + 2 * config.padding, 19 | Brush::from_environment(Stream::Stderr)?, 20 | ); 21 | canvas.draw_rect( 22 | config.padding, 23 | config.padding, 24 | width + 2, 25 | width + 2, 26 | &Color::white(), 27 | ); 28 | 29 | for y in 0..width { 30 | for x in 0..width { 31 | let rx = (x as f64) / (width as f64); 32 | let ry = (y as f64) / (width as f64); 33 | 34 | let h = 360.0 * rx; 35 | let s = 0.6; 36 | let l = 0.95 * ry; 37 | 38 | // Start with HSL 39 | let color = Color::from_hsl(h, s, l); 40 | 41 | // But (slightly) normalize the luminance 42 | let mut lch = color.to_lch(); 43 | lch.l = (lch.l + ry * 100.0) / 2.0; 44 | let color = Color::from_lch(lch.l, lch.c, lch.h, 1.0); 45 | 46 | canvas.draw_rect(config.padding + y + 1, config.padding + x + 1, 1, 1, &color); 47 | } 48 | } 49 | 50 | let stderr_handle = io::stderr(); 51 | let mut stderr = stderr_handle.lock(); 52 | 53 | canvas.print(&mut stderr)?; 54 | writeln!(&mut stderr)?; 55 | Ok(()) 56 | } 57 | 58 | /// Run an external color picker tool (e.g. gpick or xcolor) and get the output as a string. 59 | pub fn run_external_colorpicker(picker: Option<&str>) -> Result { 60 | for tool in COLOR_PICKER_TOOLS 61 | .iter() 62 | .filter(|t| picker.map_or(true, |p| t.command.eq_ignore_ascii_case(p))) 63 | { 64 | let result = Command::new(tool.command).args(tool.version_args).output(); 65 | 66 | let tool_is_available = match result { 67 | Ok(ref output) => { 68 | output.stdout.starts_with(tool.version_output_starts_with) 69 | || output.stderr.starts_with(tool.version_output_starts_with) 70 | } 71 | _ => false, 72 | }; 73 | 74 | if tool_is_available { 75 | let result = Command::new(tool.command).args(tool.args).output()?; 76 | if !result.status.success() { 77 | return Err(PastelError::ColorPickerExecutionError( 78 | tool.command.to_string(), 79 | )); 80 | } 81 | 82 | let color = 83 | String::from_utf8(result.stdout).map_err(|_| PastelError::ColorInvalidUTF8)?; 84 | let color = color.trim().to_string(); 85 | 86 | // Check if tool requires some post processing of the output 87 | if let Some(post_process) = tool.post_process { 88 | return post_process(color) 89 | .map_err(|error| PastelError::ColorParseError(error.to_string())); 90 | } else { 91 | return Ok(color); 92 | } 93 | } 94 | } 95 | 96 | Err(PastelError::NoColorPickerFound) 97 | } 98 | -------------------------------------------------------------------------------- /src/cli/commands/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::error::Result; 3 | use crate::output::Output; 4 | use clap::ArgMatches; 5 | 6 | mod color_commands; 7 | mod colorcheck; 8 | mod distinct; 9 | mod format; 10 | mod gradient; 11 | mod gray; 12 | mod io; 13 | mod list; 14 | mod paint; 15 | mod pick; 16 | mod prelude; 17 | mod random; 18 | mod show; 19 | mod sort; 20 | mod traits; 21 | 22 | use traits::{ColorCommand, GenericCommand}; 23 | 24 | use colorcheck::ColorCheckCommand; 25 | use distinct::DistinctCommand; 26 | use format::FormatCommand; 27 | use gradient::GradientCommand; 28 | use gray::GrayCommand; 29 | use list::ListCommand; 30 | use paint::PaintCommand; 31 | use pick::PickCommand; 32 | use random::RandomCommand; 33 | use sort::SortCommand; 34 | 35 | use io::ColorArgIterator; 36 | 37 | pub enum Command { 38 | WithColor(Box), 39 | Generic(Box), 40 | } 41 | 42 | impl Command { 43 | pub fn from_string(command: &str) -> Command { 44 | match command { 45 | "color" => Command::WithColor(Box::new(show::ShowCommand)), 46 | "saturate" => Command::WithColor(Box::new(color_commands::SaturateCommand)), 47 | "desaturate" => Command::WithColor(Box::new(color_commands::DesaturateCommand)), 48 | "lighten" => Command::WithColor(Box::new(color_commands::LightenCommand)), 49 | "darken" => Command::WithColor(Box::new(color_commands::DarkenCommand)), 50 | "rotate" => Command::WithColor(Box::new(color_commands::RotateCommand)), 51 | "colorblind" => Command::WithColor(Box::new(color_commands::ColorblindCommand)), 52 | "set" => Command::WithColor(Box::new(color_commands::SetCommand)), 53 | "complement" => Command::WithColor(Box::new(color_commands::ComplementCommand)), 54 | "mix" => Command::WithColor(Box::new(color_commands::MixCommand)), 55 | "to-gray" => Command::WithColor(Box::new(color_commands::ToGrayCommand)), 56 | "textcolor" => Command::WithColor(Box::new(color_commands::TextColorCommand)), 57 | "pick" => Command::Generic(Box::new(PickCommand)), 58 | "gray" => Command::Generic(Box::new(GrayCommand)), 59 | "list" => Command::Generic(Box::new(ListCommand)), 60 | "sort-by" => Command::Generic(Box::new(SortCommand)), 61 | "random" => Command::Generic(Box::new(RandomCommand)), 62 | "distinct" => Command::Generic(Box::new(DistinctCommand)), 63 | "gradient" => Command::Generic(Box::new(GradientCommand)), 64 | "paint" => Command::Generic(Box::new(PaintCommand)), 65 | "format" => Command::WithColor(Box::new(FormatCommand)), 66 | "colorcheck" => Command::Generic(Box::new(ColorCheckCommand)), 67 | _ => unreachable!("Unknown subcommand"), 68 | } 69 | } 70 | 71 | pub fn execute(&self, matches: &ArgMatches, config: &Config) -> Result<()> { 72 | let stdout = std::io::stdout(); 73 | let mut stdout_lock = stdout.lock(); 74 | let mut out = Output::new(&mut stdout_lock); 75 | 76 | match self { 77 | Command::Generic(cmd) => cmd.run(&mut out, matches, config), 78 | Command::WithColor(cmd) => { 79 | for color in ColorArgIterator::from_args(config, matches.values_of("color"))? { 80 | cmd.run(&mut out, matches, config, &color?)?; 81 | } 82 | 83 | Ok(()) 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/helper.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cmp::Ordering, 3 | fmt::{self, Display}, 4 | }; 5 | 6 | use crate::types::Scalar; 7 | 8 | /// Like `%`, but always positive. 9 | pub fn mod_positive(x: Scalar, y: Scalar) -> Scalar { 10 | (x % y + y) % y 11 | } 12 | 13 | /// Trim a number such that it fits into the range [lower, upper]. 14 | pub fn clamp(lower: Scalar, upper: Scalar, x: Scalar) -> Scalar { 15 | Scalar::max(Scalar::min(upper, x), lower) 16 | } 17 | 18 | #[derive(Debug, Clone, Copy)] 19 | pub struct Fraction { 20 | f: Scalar, 21 | } 22 | 23 | impl Fraction { 24 | pub fn from(s: Scalar) -> Self { 25 | Fraction { 26 | f: clamp(0.0, 1.0, s), 27 | } 28 | } 29 | 30 | pub fn value(self) -> Scalar { 31 | self.f 32 | } 33 | } 34 | 35 | /// Linearly interpolate between two values. 36 | pub fn interpolate(a: Scalar, b: Scalar, fraction: Fraction) -> Scalar { 37 | a + fraction.value() * (b - a) 38 | } 39 | 40 | /// Linearly interpolate between two angles. Always take the shortest path 41 | /// along the circle. 42 | pub fn interpolate_angle(a: Scalar, b: Scalar, fraction: Fraction) -> Scalar { 43 | let paths = [(a, b), (a, b + 360.0), (a + 360.0, b)]; 44 | 45 | let dist = |&(x, y): &(Scalar, Scalar)| (x - y).abs(); 46 | let shortest = paths 47 | .iter() 48 | .min_by(|p1, p2| dist(p1).partial_cmp(&dist(p2)).unwrap_or(Ordering::Less)) 49 | .unwrap(); 50 | 51 | mod_positive(interpolate(shortest.0, shortest.1, fraction), 360.0) 52 | } 53 | 54 | // `format!`-style format strings only allow specifying a fixed floating 55 | // point precision, e.g. `{:.3}` to print 3 decimal places. This always 56 | // displays trailing zeroes, while web colors generally omit them. For 57 | // example, we'd prefer to print `0.5` as `0.5` instead of `0.500`. 58 | // 59 | // Note that this will round using omitted decimal places: 60 | // 61 | // MaxPrecision::wrap(3, 0.5004) //=> 0.500 62 | // MaxPrecision::wrap(3, 0.5005) //=> 0.501 63 | // 64 | pub struct MaxPrecision { 65 | precision: u32, 66 | inner: f64, 67 | } 68 | 69 | impl MaxPrecision { 70 | pub fn wrap(precision: u32, inner: f64) -> Self { 71 | Self { precision, inner } 72 | } 73 | } 74 | 75 | impl Display for MaxPrecision { 76 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 77 | let pow_10 = 10u32.pow(self.precision) as f64; 78 | let rounded = (self.inner * pow_10).round() / pow_10; 79 | write!(f, "{}", rounded) 80 | } 81 | } 82 | 83 | #[test] 84 | fn test_interpolate() { 85 | assert_eq!(0.0, interpolate_angle(0.0, 90.0, Fraction::from(0.0))); 86 | assert_eq!(45.0, interpolate_angle(0.0, 90.0, Fraction::from(0.5))); 87 | assert_eq!(90.0, interpolate_angle(0.0, 90.0, Fraction::from(1.0))); 88 | assert_eq!(90.0, interpolate_angle(0.0, 90.0, Fraction::from(1.1))); 89 | } 90 | 91 | #[test] 92 | fn test_interpolate_angle() { 93 | assert_eq!(15.0, interpolate_angle(0.0, 30.0, Fraction::from(0.5))); 94 | assert_eq!(20.0, interpolate_angle(0.0, 100.0, Fraction::from(0.2))); 95 | assert_eq!(0.0, interpolate_angle(10.0, 350.0, Fraction::from(0.5))); 96 | assert_eq!(0.0, interpolate_angle(350.0, 10.0, Fraction::from(0.5))); 97 | } 98 | 99 | #[test] 100 | fn test_max_precision() { 101 | assert_eq!(format!("{}", MaxPrecision::wrap(3, 0.5)), "0.5"); 102 | assert_eq!(format!("{}", MaxPrecision::wrap(3, 0.51)), "0.51"); 103 | assert_eq!(format!("{}", MaxPrecision::wrap(3, 0.512)), "0.512"); 104 | assert_eq!(format!("{}", MaxPrecision::wrap(3, 0.5124)), "0.512"); 105 | assert_eq!(format!("{}", MaxPrecision::wrap(3, 0.5125)), "0.513"); 106 | } 107 | -------------------------------------------------------------------------------- /src/cli/commands/io.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, BufRead}; 2 | 3 | use clap::{ArgMatches, Values}; 4 | 5 | use crate::colorpicker::{print_colorspectrum, run_external_colorpicker}; 6 | use crate::config::Config; 7 | use crate::{PastelError, Result}; 8 | 9 | use pastel::parser::parse_color; 10 | use pastel::Color; 11 | 12 | pub fn number_arg(matches: &ArgMatches, name: &str) -> Result { 13 | let value_str = matches.value_of(name).expect("required argument"); 14 | value_str 15 | .parse::() 16 | .map_err(|_| PastelError::CouldNotParseNumber(value_str.into())) 17 | } 18 | 19 | #[derive(Debug, Clone, PartialEq)] 20 | pub enum PrintSpectrum { 21 | Yes, 22 | No, 23 | } 24 | 25 | pub enum ColorArgIterator<'a> { 26 | FromPositionalArguments(&'a Config<'a>, Values<'a>, PrintSpectrum), 27 | FromStdin, 28 | } 29 | 30 | impl<'a> ColorArgIterator<'a> { 31 | pub fn from_args(config: &'a Config, args: Option>) -> Result { 32 | match args { 33 | Some(positionals) => Ok(ColorArgIterator::FromPositionalArguments( 34 | config, 35 | positionals, 36 | PrintSpectrum::Yes, 37 | )), 38 | None => { 39 | use atty::Stream; 40 | if atty::is(Stream::Stdin) { 41 | return Err(PastelError::ColorArgRequired); 42 | } 43 | Ok(ColorArgIterator::FromStdin) 44 | } 45 | } 46 | } 47 | 48 | pub fn color_from_stdin() -> Result { 49 | let stdin = io::stdin(); 50 | let mut lock = stdin.lock(); 51 | 52 | let mut line = String::new(); 53 | let size = lock 54 | .read_line(&mut line) 55 | .map_err(|_| PastelError::ColorInvalidUTF8)?; 56 | 57 | if size == 0 { 58 | return Err(PastelError::CouldNotReadFromStdin); 59 | } 60 | 61 | let line = line.trim(); 62 | 63 | parse_color(line).ok_or_else(|| PastelError::ColorParseError(line.to_string())) 64 | } 65 | 66 | pub fn from_color_arg( 67 | config: &'a Config, 68 | arg: &str, 69 | print_spectrum: &mut PrintSpectrum, 70 | ) -> Result { 71 | match arg { 72 | "-" => Self::color_from_stdin(), 73 | "pick" => { 74 | if *print_spectrum == PrintSpectrum::Yes { 75 | print_colorspectrum(config)?; 76 | *print_spectrum = PrintSpectrum::No; 77 | } 78 | let color_str = run_external_colorpicker(config.colorpicker)?; 79 | ColorArgIterator::from_color_arg(config, &color_str, print_spectrum) 80 | } 81 | color_str => { 82 | parse_color(color_str).ok_or_else(|| PastelError::ColorParseError(color_str.into())) 83 | } 84 | } 85 | } 86 | } 87 | 88 | impl<'a> Iterator for ColorArgIterator<'a> { 89 | type Item = Result; 90 | 91 | fn next(&mut self) -> Option { 92 | match self { 93 | ColorArgIterator::FromPositionalArguments( 94 | ref mut config, 95 | ref mut args, 96 | ref mut print_spectrum, 97 | ) => args 98 | .next() 99 | .map(|color_arg| Self::from_color_arg(config, color_arg, print_spectrum)), 100 | 101 | ColorArgIterator::FromStdin => match Self::color_from_stdin() { 102 | Ok(color) => Some(Ok(color)), 103 | Err(PastelError::CouldNotReadFromStdin) => None, 104 | err @ Err(_) => Some(err), 105 | }, 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/cli/output.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use crate::config::Config; 4 | use crate::error::Result; 5 | use crate::hdcanvas::Canvas; 6 | use crate::utility::similar_colors; 7 | 8 | use pastel::Color; 9 | use pastel::Format; 10 | 11 | // #[derive(Debug)] 12 | pub struct Output<'a> { 13 | pub handle: &'a mut dyn Write, 14 | colors_shown: usize, 15 | } 16 | 17 | impl Output<'_> { 18 | pub fn new(handle: &mut dyn Write) -> Output { 19 | Output { 20 | handle, 21 | colors_shown: 0, 22 | } 23 | } 24 | 25 | pub fn show_color_tty(&mut self, config: &Config, color: &Color) -> Result<()> { 26 | let checkerboard_size: usize = 16; 27 | let color_panel_size: usize = 12; 28 | 29 | let checkerboard_position_y: usize = 0; 30 | let checkerboard_position_x: usize = config.padding; 31 | let color_panel_position_y: usize = 32 | checkerboard_position_y + (checkerboard_size - color_panel_size) / 2; 33 | let color_panel_position_x: usize = 34 | config.padding + (checkerboard_size - color_panel_size) / 2; 35 | let text_position_x: usize = checkerboard_size + 2 * config.padding; 36 | let text_position_y: usize = 0; 37 | 38 | let mut canvas = Canvas::new(checkerboard_size, 60, config.brush); 39 | canvas.draw_checkerboard( 40 | checkerboard_position_y, 41 | checkerboard_position_x, 42 | checkerboard_size, 43 | checkerboard_size, 44 | &Color::graytone(0.94), 45 | &Color::graytone(0.71), 46 | ); 47 | canvas.draw_rect( 48 | color_panel_position_y, 49 | color_panel_position_x, 50 | color_panel_size, 51 | color_panel_size, 52 | color, 53 | ); 54 | 55 | let mut text_y_offset = 0; 56 | let similar = similar_colors(color); 57 | 58 | for (i, nc) in similar.iter().enumerate().take(3) { 59 | if nc.color == *color { 60 | canvas.draw_text( 61 | text_position_y, 62 | text_position_x, 63 | &format!("Name: {}", nc.name), 64 | ); 65 | text_y_offset = 2; 66 | continue; 67 | } 68 | 69 | canvas.draw_text(text_position_y + 10 + 2 * i, text_position_x + 7, nc.name); 70 | canvas.draw_rect( 71 | text_position_y + 10 + 2 * i, 72 | text_position_x + 1, 73 | 2, 74 | 5, 75 | &nc.color, 76 | ); 77 | } 78 | 79 | #[allow(clippy::identity_op)] 80 | canvas.draw_text( 81 | text_position_y + 0 + text_y_offset, 82 | text_position_x, 83 | &format!("Hex: {}", color.to_rgb_hex_string(true)), 84 | ); 85 | canvas.draw_text( 86 | text_position_y + 2 + text_y_offset, 87 | text_position_x, 88 | &format!("RGB: {}", color.to_rgb_string(Format::Spaces)), 89 | ); 90 | canvas.draw_text( 91 | text_position_y + 4 + text_y_offset, 92 | text_position_x, 93 | &format!("HSL: {}", color.to_hsl_string(Format::Spaces)), 94 | ); 95 | 96 | canvas.draw_text( 97 | text_position_y + 8 + text_y_offset, 98 | text_position_x, 99 | "Most similar:", 100 | ); 101 | 102 | canvas.print(self.handle) 103 | } 104 | 105 | pub fn show_color(&mut self, config: &Config, color: &Color) -> Result<()> { 106 | if config.interactive_mode { 107 | if self.colors_shown < 1 { 108 | writeln!(self.handle)? 109 | }; 110 | self.show_color_tty(config, color)?; 111 | writeln!(self.handle)?; 112 | } else { 113 | writeln!(self.handle, "{}", color.to_hsl_string(Format::NoSpaces))?; 114 | } 115 | self.colors_shown += 1; 116 | 117 | Ok(()) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/cli/hdcanvas.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use pastel::ansi::{Brush, ToAnsiStyle}; 4 | use pastel::Color; 5 | 6 | use crate::Result; 7 | 8 | pub struct Canvas { 9 | height: usize, 10 | width: usize, 11 | pixels: Vec>, 12 | chars: Vec>, 13 | brush: Brush, 14 | } 15 | 16 | impl Canvas { 17 | pub fn new(height: usize, width: usize, brush: Brush) -> Self { 18 | assert!(height % 2 == 0); 19 | 20 | let mut pixels = vec![]; 21 | pixels.resize(height * width, None); 22 | let mut chars = vec![]; 23 | chars.resize(height / 2 * width, None); 24 | 25 | Canvas { 26 | height, 27 | width, 28 | pixels, 29 | chars, 30 | brush, 31 | } 32 | } 33 | 34 | pub fn draw_rect( 35 | &mut self, 36 | row: usize, 37 | col: usize, 38 | height: usize, 39 | width: usize, 40 | color: &Color, 41 | ) { 42 | for i in 0..height { 43 | for j in 0..width { 44 | let px = self.pixel_mut(row + i, col + j); 45 | *px = Some(match px { 46 | Some(backdrop) => backdrop.composite(color), 47 | None => color.clone(), 48 | }); 49 | } 50 | } 51 | } 52 | 53 | pub fn draw_checkerboard( 54 | &mut self, 55 | row: usize, 56 | col: usize, 57 | height: usize, 58 | width: usize, 59 | dark: &Color, 60 | light: &Color, 61 | ) { 62 | for i in 0..height { 63 | for j in 0..width { 64 | let color = if (i + j) % 2 == 0 { dark } else { light }; 65 | *self.pixel_mut(row + i, col + j) = Some(color.clone()); 66 | } 67 | } 68 | } 69 | 70 | pub fn draw_text(&mut self, row: usize, col: usize, text: &str) { 71 | assert!(row % 2 == 0); 72 | 73 | for (j, c) in text.chars().enumerate() { 74 | *self.char_mut(row / 2, col + j) = Some(c); 75 | } 76 | } 77 | 78 | pub fn print(&self, out: &mut dyn Write) -> Result<()> { 79 | for i_div_2 in 0..self.height / 2 { 80 | for j in 0..self.width { 81 | if let Some(c) = self.char(i_div_2, j) { 82 | write!(out, "{}", c)?; 83 | } else { 84 | let p_top = self.pixel(2 * i_div_2, j); 85 | let p_bottom = self.pixel(2 * i_div_2 + 1, j); 86 | 87 | match (p_top, p_bottom) { 88 | (Some(top), Some(bottom)) => write!( 89 | out, 90 | "{}", 91 | self.brush.paint("▀", top.ansi_style().on(bottom)) 92 | )?, 93 | (Some(top), None) => write!(out, "{}", self.brush.paint("▀", top))?, 94 | (None, Some(bottom)) => write!(out, "{}", self.brush.paint("▄", bottom))?, 95 | (None, None) => write!(out, " ")?, 96 | }; 97 | } 98 | } 99 | writeln!(out)?; 100 | } 101 | 102 | Ok(()) 103 | } 104 | 105 | fn pixel(&self, i: usize, j: usize) -> &Option { 106 | assert!(i < self.height); 107 | assert!(j < self.width); 108 | &self.pixels[i * self.width + j] 109 | } 110 | 111 | fn pixel_mut(&mut self, i: usize, j: usize) -> &mut Option { 112 | assert!(i < self.height); 113 | assert!(j < self.width); 114 | &mut self.pixels[i * self.width + j] 115 | } 116 | 117 | fn char(&self, i: usize, j: usize) -> &Option { 118 | assert!(i < self.height / 2); 119 | assert!(j < self.width); 120 | &self.chars[i * self.width + j] 121 | } 122 | 123 | fn char_mut(&mut self, i: usize, j: usize) -> &mut Option { 124 | assert!(i < self.height / 2); 125 | assert!(j < self.width); 126 | &mut self.chars[i * self.width + j] 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/cli/main.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | 3 | use atty::Stream; 4 | 5 | mod cli; 6 | mod colorpicker; 7 | mod colorpicker_tools; 8 | mod colorspace; 9 | mod commands; 10 | mod config; 11 | mod error; 12 | mod hdcanvas; 13 | mod output; 14 | mod utility; 15 | 16 | use commands::Command; 17 | use config::Config; 18 | use error::{PastelError, Result}; 19 | 20 | use pastel::ansi::{self, Brush, Mode}; 21 | use pastel::Color; 22 | 23 | type ExitCode = i32; 24 | 25 | fn write_stderr(c: Color, title: &str, message: &str) { 26 | writeln!( 27 | io::stderr(), 28 | "{}: {}", 29 | Brush::from_environment(Stream::Stdout) 30 | .unwrap_or_default() 31 | .paint(format!("[{}]", title), c), 32 | message 33 | ) 34 | .ok(); 35 | } 36 | 37 | fn print_pastel_warning() { 38 | write_stderr( 39 | Color::yellow(), 40 | "pastel warning", 41 | "Your terminal emulator does not appear to support 24-bit colors \ 42 | (this means that the COLORTERM environment variable is not set to \ 43 | 'truecolor' or '24bit'). \ 44 | pastel will fall back to 8-bit colors, but you will only be able \ 45 | to see rough approximations of the real colors.\n\n\ 46 | To fix this, follow these steps:\n \ 47 | 1. Run 'pastel colorcheck' to test if your terminal\n \ 48 | emulator does support 24-bit colors. If this is the\n \ 49 | case, set 'PASTEL_COLOR_MODE=24bit' to force 24-bit\n \ 50 | mode and to remove this warning. Alternatively, make\n \ 51 | sure that COLORTERM is properly set by your terminal\n \ 52 | emulator.\n \ 53 | 2. If your terminal emulator does not support 24-bit\n \ 54 | colors, set 'PASTEL_COLOR_MODE=8bit' to remove this\n \ 55 | warning or try a different terminal emulator.\n\n\ 56 | \ 57 | For more information, see https://gist.github.com/XVilka/8346728\n", 58 | ); 59 | } 60 | 61 | fn run() -> Result { 62 | let app = cli::build_cli(); 63 | let global_matches = app.get_matches(); 64 | 65 | let interactive_mode = atty::is(Stream::Stdout); 66 | 67 | let color_mode = if global_matches.is_present("force-color") { 68 | Some(ansi::Mode::TrueColor) 69 | } else { 70 | match global_matches 71 | .value_of("color-mode") 72 | .expect("required argument") 73 | { 74 | "24bit" => Some(ansi::Mode::TrueColor), 75 | "8bit" => Some(ansi::Mode::Ansi8Bit), 76 | "off" => None, 77 | "auto" => { 78 | if interactive_mode { 79 | let env_color_mode = std::env::var("PASTEL_COLOR_MODE").ok(); 80 | match env_color_mode.as_deref() { 81 | Some(mode_str) => Mode::from_mode_str(mode_str)?, 82 | None => { 83 | let mode = ansi::get_colormode(); 84 | if mode == Some(ansi::Mode::Ansi8Bit) 85 | && global_matches.subcommand_name() != Some("paint") 86 | && global_matches.subcommand_name() != Some("colorcheck") 87 | { 88 | print_pastel_warning(); 89 | } 90 | mode 91 | } 92 | } 93 | } else { 94 | None 95 | } 96 | } 97 | _ => unreachable!("Unknown --color-mode argument"), 98 | } 99 | }; 100 | 101 | let config = Config { 102 | padding: 2, 103 | colorpicker_width: 48, 104 | colorcheck_width: 8, 105 | interactive_mode, 106 | brush: Brush::from_mode(color_mode), 107 | colorpicker: global_matches.value_of("color-picker"), 108 | }; 109 | 110 | if let Some((subcommand, matches)) = global_matches.subcommand() { 111 | let command = Command::from_string(subcommand); 112 | command.execute(matches, &config)?; 113 | } else { 114 | unreachable!("Subcommand is required"); 115 | } 116 | 117 | Ok(0) 118 | } 119 | 120 | fn main() { 121 | let result = run(); 122 | match result { 123 | Err(PastelError::StdoutClosed) => {} 124 | Err(err) => { 125 | write_stderr(Color::red(), "pastel error", &err.message()); 126 | std::process::exit(1); 127 | } 128 | Ok(exit_code) => { 129 | std::process::exit(exit_code); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # unreleased 2 | 3 | ## Features 4 | 5 | - Added support for parsing LCh colors, see #2 and #167 (@MForster) 6 | 7 | ## Bugfixes 8 | 9 | - `pastel pick` does not display all colors in some terminals, see #121 and #168 (@Divoolej) 10 | 11 | ## Changes 12 | 13 | 14 | ## Other 15 | 16 | 17 | ## Packaging 18 | 19 | 20 | 21 | ## v0.9.0 22 | 23 | ## Features 24 | 25 | - Added support for transparency / alpha values, see #131 and #162 (@superhawk610) 26 | - Added support for `NO_COLOR` environment variable, see #143 (@djmattyg007) 27 | - Added new color pickers: Gnome/Wayland via gdbus, zenity, yad, and wcolor (@sigmaSd, @pvonmoradi) 28 | 29 | ## Packaging 30 | 31 | - Added shell completion files again, see #166 (@sharkdp) 32 | 33 | 34 | # v0.8.1 35 | 36 | ## Features 37 | 38 | - Added `From` and `Display` traits for each color struct, see #133 (@bresilla) 39 | 40 | ## Other 41 | 42 | - Updated `lexical-core` dependency to fix a compile error with newer Rust versions 43 | 44 | ## Packaging 45 | 46 | - `pastel` is now available on snapstore, see #130 (@purveshpatel511) 47 | 48 | 49 | # v0.8.0 50 | 51 | ## Features 52 | 53 | - Added CMYK output format, see #122 and #123 (@aeter) 54 | 55 | ## Other 56 | 57 | - Completely new CI/CD system via GitHub Actions, see #120 (@rivy) 58 | 59 | # v0.7.1 60 | 61 | ## Bugfixes 62 | 63 | - Fixed a bug with the new `ansi-*-escapecode` formats, see #116 (@bbkane) 64 | 65 | # v0.7.0 66 | 67 | ## Changes 68 | 69 | - **Breaking:** the existing `ansi-8bit` and `ansi-24bit` formats have been changed to 70 | print out an escaped ANSI sequence that a user can see in the terminal output. 71 | The previously existing formats are now available as `ansi-8bit-escapecode` and 72 | `ansi-24bit-escapecode`. See #113 and #111. 73 | 74 | ## Features 75 | 76 | - All CSS color formats are now supported (see #12) 77 | - Added support for multiple color stops for gradients (`pastel gradient red blue yellow`), see #49 (@felipe-fg) 78 | - Added `-f`/`--force-color` flag as an alias for `--mode=24bit`, see #48 (@samueldple) 79 | - Added `--color-picker ` to allow users to choose the colorpicker, see #96 (@d-dorazio) 80 | - Added input support for CIELAB, see #3/#101 (@MusiKid) 81 | - Added support for `rgb(255 0 119)`, `rgb(100%,0%,46.7%)`, `gray(20%)`, and many more new CSS syntaxes, see #103 (@MusiKid) 82 | - Faster and more flexible color parser, adding even more CSS color formats, see #105 (@halfbro) 83 | 84 | ## `pastel` library changes 85 | 86 | - `distinct_colors` is now available in the `pastel::distinct` module, see #95 (@rivy) 87 | 88 | ## Bugfixes 89 | 90 | - Added support for non-color consoles (Windows 7), see #91 (@rivy) 91 | 92 | ## Other 93 | 94 | - pastel is now available via Nix, see #100 (@davidtwco) 95 | 96 | # v0.6.1 97 | 98 | ## Other 99 | 100 | - Enabled builds for arm, aarch64, and i686 101 | - Fixed build on 32bit platforms 102 | 103 | # v0.6.0 104 | 105 | ## Features 106 | 107 | - Added colorblindness simulations via `pastel colorblind`, see #80 (@rozbb) 108 | - Added support for pre-determined colors in `pastel distinct`, see #88 (@d-dorazio) 109 | - Added a new `set` subcommand that can be used to set specific properties of a color (`pastel set lightness 0.4`, `pastel set red 0`, etc.), see #43 110 | - Show the color name in `pastel show` or `pastel color` if it is an exact match, for example: 111 | `pastel color ff00ff` will show "fuchsia", see #81 (@d-dorazio) 112 | - Add KColorChooser as a supported color picker, see #79 (@data-man) 113 | - Add macOS built-in color picker, see #84 (@erydo) 114 | - Added a new 'count' argument for `pastel pick []` 115 | 116 | ## Changes 117 | 118 | - `pastel distinct` has seen massive speedups, see #83 (@d-dorazio) 119 | 120 | ## Bugfixes 121 | 122 | - Mixing colors in HSL space with black or white will not rotate the hue towards red (hue 0°), see #76 123 | 124 | ## Other 125 | 126 | - Pastel is now available via Homebrew, see README and #70 (@liamdawson) 127 | 128 | # v0.5.3 129 | 130 | - Added `rgb-float` as a new format (e.g. `pastel random | pastel format rgb-float`). 131 | - `pastel pick` should now work in 24-bit on Windows, see #45 132 | - Fix crash for `pastel distinct N` with N < 2 (show an error message), see #69 133 | 134 | # v0.5.2 135 | 136 | * Truecolor support for Windows (@lzybkr) 137 | * Re-arranging of colors in `pastel distinct` so as to maximize the minimal distance to the predecessors 138 | * Fixed small numerical approximation problem in the 'similar colors' computation 139 | * Backported to Rust 1.34 140 | 141 | # v0.5.1 142 | 143 | - Added shell completion files for bash, zsh, fish and PowerShell. 144 | 145 | # v0.5.0 146 | 147 | - Added `pastel distinct N` command to generate a set of N visually distinct colors 148 | 149 | # v0.4.0 150 | 151 | Initial public release 152 | -------------------------------------------------------------------------------- /src/cli/commands/color_commands.rs: -------------------------------------------------------------------------------- 1 | use crate::colorspace::get_mixing_function; 2 | use crate::commands::prelude::*; 3 | 4 | use pastel::ColorblindnessType; 5 | use pastel::Fraction; 6 | 7 | fn clamp(lower: f64, upper: f64, x: f64) -> f64 { 8 | f64::max(f64::min(upper, x), lower) 9 | } 10 | 11 | macro_rules! color_command { 12 | ($cmd_name:ident, $config:ident, $matches:ident, $color:ident, $body:block) => { 13 | pub struct $cmd_name; 14 | 15 | impl ColorCommand for $cmd_name { 16 | fn run( 17 | &self, 18 | out: &mut Output, 19 | $matches: &ArgMatches, 20 | $config: &Config, 21 | $color: &Color, 22 | ) -> Result<()> { 23 | let output = $body; 24 | out.show_color($config, &output) 25 | } 26 | } 27 | }; 28 | } 29 | 30 | color_command!(SaturateCommand, _config, matches, color, { 31 | let amount = number_arg(matches, "amount")?; 32 | color.saturate(amount) 33 | }); 34 | 35 | color_command!(DesaturateCommand, _config, matches, color, { 36 | let amount = number_arg(matches, "amount")?; 37 | color.desaturate(amount) 38 | }); 39 | 40 | color_command!(LightenCommand, _config, matches, color, { 41 | let amount = number_arg(matches, "amount")?; 42 | color.lighten(amount) 43 | }); 44 | 45 | color_command!(DarkenCommand, _config, matches, color, { 46 | let amount = number_arg(matches, "amount")?; 47 | color.darken(amount) 48 | }); 49 | 50 | color_command!(RotateCommand, _config, matches, color, { 51 | let degrees = number_arg(matches, "degrees")?; 52 | color.rotate_hue(degrees) 53 | }); 54 | 55 | color_command!(ComplementCommand, _config, _matches, color, { 56 | color.complementary() 57 | }); 58 | 59 | color_command!(ToGrayCommand, _config, _matches, color, { color.to_gray() }); 60 | 61 | color_command!(TextColorCommand, _config, _matches, color, { 62 | color.text_color() 63 | }); 64 | 65 | color_command!(MixCommand, config, matches, color, { 66 | let mut print_spectrum = PrintSpectrum::Yes; 67 | 68 | let base = ColorArgIterator::from_color_arg( 69 | config, 70 | matches.value_of("base").expect("required argument"), 71 | &mut print_spectrum, 72 | )?; 73 | let fraction = Fraction::from(1.0 - number_arg(matches, "fraction")?); 74 | 75 | let mix = get_mixing_function(matches.value_of("colorspace").expect("required argument")); 76 | 77 | mix(&base, color, fraction) 78 | }); 79 | 80 | color_command!(ColorblindCommand, config, matches, color, { 81 | // The type of colorblindness selected (protanopia, deuteranopia, tritanopia) 82 | let cb_ty = matches.value_of("type").expect("required argument"); 83 | let cb_ty = cb_ty.to_lowercase(); 84 | 85 | // Convert the string to the corresponding enum variant 86 | let cb_ty = match cb_ty.as_ref() { 87 | "prot" => ColorblindnessType::Protanopia, 88 | "deuter" => ColorblindnessType::Deuteranopia, 89 | "trit" => ColorblindnessType::Tritanopia, 90 | &_ => { 91 | unreachable!("Unknown property"); 92 | } 93 | }; 94 | 95 | color.simulate_colorblindness(cb_ty) 96 | }); 97 | 98 | color_command!(SetCommand, config, matches, color, { 99 | let property = matches.value_of("property").expect("required argument"); 100 | let property = property.to_lowercase(); 101 | let property = property.as_ref(); 102 | 103 | let value = number_arg(matches, "value")?; 104 | 105 | match property { 106 | "red" | "green" | "blue" => { 107 | let mut rgba = color.to_rgba(); 108 | let value = clamp(0.0, 255.0, value) as u8; 109 | match property { 110 | "red" => { 111 | rgba.r = value; 112 | } 113 | "green" => { 114 | rgba.g = value; 115 | } 116 | "blue" => { 117 | rgba.b = value; 118 | } 119 | _ => unreachable!(), 120 | } 121 | Color::from_rgba(rgba.r, rgba.g, rgba.b, rgba.alpha) 122 | } 123 | "hsl-hue" | "hsl-saturation" | "hsl-lightness" => { 124 | let mut hsla = color.to_hsla(); 125 | match property { 126 | "hsl-hue" => { 127 | hsla.h = value; 128 | } 129 | "hsl-saturation" => { 130 | hsla.s = value; 131 | } 132 | "hsl-lightness" => { 133 | hsla.l = value; 134 | } 135 | _ => unreachable!(), 136 | } 137 | Color::from_hsla(hsla.h, hsla.s, hsla.l, hsla.alpha) 138 | } 139 | "lightness" | "lab-a" | "lab-b" => { 140 | let mut lab = color.to_lab(); 141 | match property { 142 | "lightness" => { 143 | lab.l = value; 144 | } 145 | "lab-a" => { 146 | lab.a = value; 147 | } 148 | "lab-b" => { 149 | lab.b = value; 150 | } 151 | _ => unreachable!(), 152 | } 153 | Color::from_lab(lab.l, lab.a, lab.b, lab.alpha) 154 | } 155 | "hue" | "chroma" => { 156 | let mut lch = color.to_lch(); 157 | match property { 158 | "hue" => { 159 | lch.h = value; 160 | } 161 | "chroma" => { 162 | lch.c = value; 163 | } 164 | _ => unreachable!(), 165 | } 166 | Color::from_lch(lch.l, lch.c, lch.h, lch.alpha) 167 | } 168 | &_ => { 169 | unreachable!("Unknown property"); 170 | } 171 | } 172 | }); 173 | -------------------------------------------------------------------------------- /src/cli/colorpicker_tools.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | 3 | pub struct ColorPickerTool { 4 | pub command: &'static str, 5 | pub args: &'static [&'static str], 6 | pub version_args: &'static [&'static str], 7 | pub version_output_starts_with: &'static [u8], 8 | #[allow(clippy::type_complexity)] 9 | /// Post-Process the output of the color picker tool 10 | pub post_process: Option Result>, 11 | } 12 | 13 | pub static COLOR_PICKER_TOOLS: Lazy> = Lazy::new(|| { 14 | vec![ 15 | #[cfg(target_os = "macos")] 16 | ColorPickerTool { 17 | command: "osascript", 18 | // NOTE: This does not use `console.log` to print the value as you might expect, 19 | // because that gets written to stderr instead of stdout regardless of the `-s o` flag. 20 | // (This is accurate as of macOS Mojave/10.14.6). 21 | // See related: https://apple.stackexchange.com/a/278395 22 | args: &[ 23 | "-l", 24 | "JavaScript", 25 | "-s", 26 | "o", 27 | "-e", 28 | " 29 | const app = Application.currentApplication();\n 30 | app.includeStandardAdditions = true;\n 31 | const rgb = app.chooseColor({defaultColor: [0.5, 0.5, 0.5]})\n 32 | .map(n => Math.round(n * 255))\n 33 | .join(', ');\n 34 | `rgb(${rgb})`;\n 35 | ", 36 | ], 37 | version_args: &["-l", "JavaScript", "-s", "o", "-e", "'ok';"], 38 | version_output_starts_with: b"ok", 39 | post_process: None, 40 | }, 41 | ColorPickerTool { 42 | command: "gpick", 43 | args: &["--pick", "--single", "--output"], 44 | version_args: &["--version"], 45 | version_output_starts_with: b"Gpick", 46 | post_process: None, 47 | }, 48 | ColorPickerTool { 49 | command: "xcolor", 50 | args: &["--format", "hex"], 51 | version_args: &["--version"], 52 | version_output_starts_with: b"xcolor", 53 | post_process: None, 54 | }, 55 | ColorPickerTool { 56 | command: "wcolor", 57 | args: &["--format", "hex"], 58 | version_args: &["--version"], 59 | version_output_starts_with: b"wcolor", 60 | post_process: None, 61 | }, 62 | ColorPickerTool { 63 | command: "grabc", 64 | args: &["-hex"], 65 | version_args: &["-v"], 66 | version_output_starts_with: b"grabc", 67 | post_process: None, 68 | }, 69 | ColorPickerTool { 70 | command: "colorpicker", 71 | args: &["--one-shot", "--short"], 72 | version_args: &["--help"], 73 | version_output_starts_with: b"", 74 | post_process: None, 75 | }, 76 | ColorPickerTool { 77 | command: "chameleon", 78 | args: &[], 79 | version_args: &["-h"], 80 | version_output_starts_with: b"", 81 | post_process: None, 82 | }, 83 | ColorPickerTool { 84 | command: "kcolorchooser", 85 | args: &["--print"], 86 | version_args: &["-v"], 87 | version_output_starts_with: b"kcolorchooser", 88 | post_process: None, 89 | }, 90 | ColorPickerTool { 91 | command: "zenity", 92 | args: &["--color-selection"], 93 | version_args: &["--version"], 94 | version_output_starts_with: b"", 95 | post_process: None, 96 | }, 97 | ColorPickerTool { 98 | command: "yad", 99 | args: &["--color"], 100 | version_args: &["--version"], 101 | version_output_starts_with: b"", 102 | post_process: None, 103 | }, 104 | #[cfg(target_os = "linux")] 105 | ColorPickerTool { 106 | command: "gdbus", 107 | args: &[ 108 | "call", 109 | "--session", 110 | "--dest", 111 | "org.gnome.Shell.Screenshot", 112 | "--object-path", 113 | "/org/gnome/Shell/Screenshot", 114 | "--method", 115 | "org.gnome.Shell.Screenshot.PickColor", 116 | ], 117 | version_args: &[ 118 | "introspect", 119 | "--session", 120 | "--dest", 121 | "org.gnome.Shell.Screenshot", 122 | "--object-path", 123 | "/org/gnome/Shell/Screenshot", 124 | ], 125 | version_output_starts_with: b"node /org/gnome/Shell/Screenshot", 126 | post_process: Some(gdbus_parse_color), 127 | }, 128 | ] 129 | }); 130 | 131 | pub static COLOR_PICKER_TOOL_NAMES: Lazy> = 132 | Lazy::new(|| COLOR_PICKER_TOOLS.iter().map(|t| t.command).collect()); 133 | 134 | #[cfg(target_os = "linux")] 135 | pub fn gdbus_parse_color(raw: String) -> Result { 136 | const PARSE_ERROR: &str = "Unexpected gdbus output format"; 137 | let rgb = raw 138 | .split('(') 139 | .nth(2) 140 | .ok_or(PARSE_ERROR)? 141 | .split(')') 142 | .next() 143 | .ok_or(PARSE_ERROR)?; 144 | let rgb = rgb 145 | .split(',') 146 | .map(|v| v.trim().parse::()) 147 | .collect::, _>>() 148 | .map_err(|_| PARSE_ERROR)?; 149 | if rgb.len() != 3 { 150 | return Err(PARSE_ERROR); 151 | } 152 | Ok(format!( 153 | "rgb({}%,{}%,{}%)", 154 | rgb[0] * 100., 155 | rgb[1] * 100., 156 | rgb[2] * 100. 157 | )) 158 | } 159 | -------------------------------------------------------------------------------- /src/cli/commands/distinct.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | 3 | use crate::commands::prelude::*; 4 | 5 | use pastel::ansi::Stream; 6 | use pastel::distinct::{self, DistanceMetric, IterationStatistics}; 7 | use pastel::{Fraction, HSLA}; 8 | 9 | pub struct DistinctCommand; 10 | 11 | fn print_iteration(out: &mut dyn Write, brush: Brush, stats: &IterationStatistics) -> Result<()> { 12 | let result = stats.distance_result; 13 | write!( 14 | out, 15 | "[{:10.}] D_mean = {:<6.2}; D_min = {:<6.2}; T = {:.6} ", 16 | stats.iteration, 17 | result.mean_closest_distance, 18 | result.min_closest_distance, 19 | stats.temperature 20 | )?; 21 | print_colors(out, brush, &stats.colors, Some(result.closest_pair))?; 22 | Ok(()) 23 | } 24 | 25 | fn print_colors( 26 | out: &mut dyn Write, 27 | brush: Brush, 28 | colors: &[Color], 29 | closest_pair: Option<(usize, usize)>, 30 | ) -> Result<()> { 31 | for (ci, c) in colors.iter().enumerate() { 32 | let tc = c.text_color(); 33 | let mut style = tc.ansi_style(); 34 | style.on(c); 35 | 36 | if let Some(pair) = closest_pair { 37 | if pair.0 == ci || pair.1 == ci { 38 | style.bold(true); 39 | style.underline(true); 40 | } 41 | } 42 | 43 | write!(out, "{} ", brush.paint(c.to_rgb_hex_string(false), style))?; 44 | } 45 | writeln!(out)?; 46 | Ok(()) 47 | } 48 | 49 | fn blue_red_yellow(f: f64) -> Color { 50 | let blue = Color::from_rgb(0, 0, 120); 51 | let red = Color::from_rgb(224, 0, 119); 52 | let yellow = Color::from_rgb(255, 255, 0); 53 | 54 | if f < 0.5 { 55 | blue.mix::(&red, Fraction::from(2.0 * f)) 56 | } else { 57 | red.mix::(&yellow, Fraction::from(2.0 * (f - 0.5))) 58 | } 59 | } 60 | 61 | fn print_distance_matrix( 62 | out: &mut dyn Write, 63 | brush: Brush, 64 | colors: &[Color], 65 | metric: DistanceMetric, 66 | ) -> Result<()> { 67 | let count = colors.len(); 68 | 69 | let distance = |c1: &Color, c2: &Color| match metric { 70 | DistanceMetric::CIE76 => c1.distance_delta_e_cie76(c2), 71 | DistanceMetric::CIEDE2000 => c1.distance_delta_e_ciede2000(c2), 72 | }; 73 | 74 | let mut min = std::f64::MAX; 75 | let mut max = 0.0; 76 | for i in 0..count { 77 | for j in 0..count { 78 | if i != j { 79 | let dist = distance(&colors[i], &colors[j]); 80 | if dist < min { 81 | min = dist; 82 | } 83 | if dist > max { 84 | max = dist; 85 | } 86 | } 87 | } 88 | } 89 | 90 | let color_to_string = |c: &Color| -> String { 91 | let tc = c.text_color(); 92 | let mut style = tc.ansi_style(); 93 | style.on(c); 94 | brush.paint(c.to_rgb_hex_string(false), style) 95 | }; 96 | 97 | write!(out, "\n\n{:6} ", "")?; 98 | for c in colors { 99 | write!(out, "{} ", color_to_string(c))?; 100 | } 101 | writeln!(out, "\n")?; 102 | 103 | for c1 in colors { 104 | write!(out, "{} ", color_to_string(c1))?; 105 | for c2 in colors { 106 | if c1 == c2 { 107 | write!(out, "{:6} ", "")?; 108 | } else { 109 | let dist = distance(c1, c2); 110 | 111 | let magnitude = (dist - min) / (max - min); 112 | let magnitude = 1.0 - magnitude.powf(0.3); 113 | 114 | let bg = blue_red_yellow(magnitude); 115 | let mut style = bg.text_color().ansi_style(); 116 | style.on(bg); 117 | 118 | write!(out, "{} ", brush.paint(format!("{:6.2}", dist), style))?; 119 | } 120 | } 121 | writeln!(out)?; 122 | } 123 | writeln!(out, "\n")?; 124 | 125 | Ok(()) 126 | } 127 | 128 | impl GenericCommand for DistinctCommand { 129 | fn run(&self, out: &mut Output, matches: &ArgMatches, config: &Config) -> Result<()> { 130 | let stderr = io::stderr(); 131 | let mut stderr_lock = stderr.lock(); 132 | let brush_stderr = Brush::from_environment(Stream::Stderr)?; 133 | let verbose_output = matches.is_present("verbose"); 134 | 135 | let count = matches.value_of("number").expect("required argument"); 136 | let count = count 137 | .parse::() 138 | .map_err(|_| PastelError::CouldNotParseNumber(count.into()))?; 139 | 140 | if count < 2 { 141 | return Err(PastelError::DistinctColorCountMustBeLargerThanOne); 142 | } 143 | 144 | let distance_metric = match matches.value_of("metric").expect("required argument") { 145 | "CIE76" => DistanceMetric::CIE76, 146 | "CIEDE2000" => DistanceMetric::CIEDE2000, 147 | _ => unreachable!("Unknown distance metric"), 148 | }; 149 | 150 | let fixed_colors = match matches.values_of("color") { 151 | None => vec![], 152 | Some(positionals) => { 153 | ColorArgIterator::FromPositionalArguments(config, positionals, PrintSpectrum::Yes) 154 | .collect::>>()? 155 | } 156 | }; 157 | 158 | let num_fixed_colors = fixed_colors.len(); 159 | if num_fixed_colors > count { 160 | return Err(PastelError::DistinctColorFixedColorsCannotBeMoreThanCount); 161 | } 162 | 163 | let mut callback: Box = if verbose_output { 164 | Box::new(|stats: &IterationStatistics| { 165 | print_iteration(&mut stderr_lock, brush_stderr, stats).ok(); 166 | }) 167 | } else { 168 | Box::new(|_: &IterationStatistics| {}) 169 | }; 170 | 171 | let (mut colors, distance_result) = 172 | distinct::distinct_colors(count, distance_metric, fixed_colors, callback.as_mut()); 173 | 174 | if matches.is_present("print-minimal-distance") { 175 | writeln!(out.handle, "{:.3}", distance_result.min_closest_distance)?; 176 | } else { 177 | distinct::rearrange_sequence(&mut colors, distance_metric); 178 | 179 | if verbose_output { 180 | print_distance_matrix(&mut stderr.lock(), brush_stderr, &colors, distance_metric)?; 181 | } 182 | 183 | for color in colors { 184 | out.show_color(config, &color)?; 185 | } 186 | } 187 | 188 | Ok(()) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pastel 2 | 3 | [![Build Status](https://img.shields.io/github/workflow/status/sharkdp/pastel/CICD?style=flat-square)](https://github.com/sharkdp/pastel/actions) 4 | [![](https://img.shields.io/github/v/release/sharkdp/pastel?colorB=d7a400&style=flat-square)](https://github.com/sharkdp/pastel/releases) 5 | [![](https://img.shields.io/crates/l/pastel.svg?colorB=ff7155&style=flat-square)](https://crates.io/crates/pastel) 6 | [![](https://img.shields.io/crates/v/pastel.svg?colorB=ff69b4&style=flat-square)](https://crates.io/crates/pastel) 7 | 8 | 9 | `pastel` is a command-line tool to generate, analyze, convert and manipulate colors. It supports many different color formats and color spaces like RGB (sRGB), HSL, CIELAB, CIELCh as well as ANSI 8-bit and 24-bit representations. 10 | 11 | ## In action 12 | 13 | ![pastel in action](doc/pastel.gif) 14 | 15 | ## Tutorial 16 | 17 | ### Getting help 18 | 19 | `pastel` provides a number of commands like `saturate`, `mix` or `paint`. To see a complete list, you can simply run 20 | ``` bash 21 | pastel 22 | ``` 23 | To get more information about a specific subcommand (say `mix`), you can call `pastel mix -h` or `pastel help mix`. 24 | 25 | ### Composition 26 | 27 | Many `pastel` commands can be composed by piping the output of one command to another, for example: 28 | ``` bash 29 | pastel random | pastel mix red | pastel lighten 0.2 | pastel format hex 30 | ``` 31 | 32 | ### Specifying colors 33 | 34 | Colors can be specified in many different formats: 35 | ``` 36 | lightslategray 37 | '#778899' 38 | 778899 39 | 789 40 | 'rgb(119, 136, 153)' 41 | '119,136,153' 42 | 'hsl(210, 14.3%, 53.3%)' 43 | ``` 44 | 45 | Colors can be passed as positional arguments, for example: 46 | ``` 47 | pastel lighten 0.2 orchid orange lawngreen 48 | ``` 49 | They can also be read from standard input. So this is equivalent: 50 | ``` 51 | printf "%s\n" orchid orange lawngreen | pastel lighten 0.2 52 | ``` 53 | You can also explicitly specify which colors you want to read from the input. For example, this mixes `red` (which is read from STDIN) with `blue` (which is passed on the command line): 54 | ``` 55 | pastel color red | pastel mix - blue 56 | ``` 57 | 58 | ### Use cases and demo 59 | 60 | #### Converting colors from one format to another 61 | 62 | ``` bash 63 | pastel format hsl ff8000 64 | ``` 65 | 66 | #### Show and analyze colors on the terminal 67 | 68 | ``` bash 69 | pastel color "rgb(255,50,127)" 70 | 71 | pastel color 556270 4ecdc4 c7f484 ff6b6b c44d58 72 | ``` 73 | 74 | #### Pick a color from somewhere on the screen 75 | 76 | ``` bash 77 | pastel pick 78 | ``` 79 | 80 | #### Generate a set of N visually distinct colors 81 | 82 | ``` 83 | pastel distinct 8 84 | ``` 85 | 86 | #### Get a list of all X11 / CSS color names 87 | 88 | ``` bash 89 | pastel list 90 | ``` 91 | 92 | #### Name a given color 93 | 94 | ``` bash 95 | pastel format name 44cc11 96 | ``` 97 | 98 | #### Print colorized text from a shell script 99 | 100 | ``` bash 101 | bg="hotpink" 102 | fg="$(pastel textcolor "$bg")" 103 | 104 | pastel paint "$fg" --on "$bg" "well readable text" 105 | ``` 106 | 107 | ``` bash 108 | pastel paint -n black --on red --bold " ERROR! " 109 | echo " A serious error" 110 | 111 | pastel paint -n black --on yellow --bold " WARNING! " 112 | echo " A warning message" 113 | 114 | pastel paint -n black --on limegreen --bold " INFO " 115 | echo -n " Informational message with a " 116 | echo -n "highlighted" | pastel paint -n default --underline 117 | echo " word" 118 | ``` 119 | 120 | ## Installation 121 | 122 | ### On Debian-based systems 123 | 124 | You can download the latest Debian package from the [release page](https://github.com/sharkdp/pastel/releases) and install it via `dpkg`: 125 | ``` bash 126 | wget "https://github.com/sharkdp/pastel/releases/download/v0.8.1/pastel_0.8.1_amd64.deb" 127 | sudo dpkg -i pastel_0.8.1_amd64.deb 128 | ``` 129 | 130 | ### On Arch Linux 131 | 132 | You can install `pastel` from the [Community](https://archlinux.org/packages/community/x86_64/pastel/) repositories: 133 | ``` 134 | sudo pacman -S pastel 135 | ``` 136 | 137 | ### On Nix 138 | 139 | You can install `pastel` from the [Nix package](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/misc/pastel/default.nix): 140 | ``` 141 | nix-env --install pastel 142 | ``` 143 | 144 | ### On MacOS 145 | 146 | You can install `pastel` via [Homebrew](https://formulae.brew.sh/formula/pastel) 147 | ``` 148 | brew install pastel 149 | ``` 150 | 151 | ### On Windows 152 | 153 | You can install `pastel` via [Scoop](https://github.com/ScoopInstaller/Main/blob/master/bucket/pastel.json) 154 | ``` 155 | scoop install pastel 156 | ``` 157 | 158 | ### Via snap package 159 | 160 | [Get it from the Snap Store](https://snapcraft.io/pastel): 161 | ``` 162 | sudo snap install pastel 163 | ``` 164 | 165 | ### On NetBSD 166 | Using the package manager: 167 | ``` 168 | pkgin install pastel 169 | ``` 170 | 171 | From source: 172 | ``` 173 | cd /usr/pkgsrc/graphics/pastel 174 | make install 175 | ``` 176 | 177 | ### On other distributions 178 | 179 | Check out the [release page](https://github.com/sharkdp/pastel/releases) for binary builds. 180 | 181 | ### Via cargo (source) 182 | 183 | If you do not have cargo, install using [rust's installation documentation](https://doc.rust-lang.org/book/ch01-01-installation.html). 184 | 185 | If you have Rust 1.43 or higher, you can install `pastel` from source via `cargo`: 186 | ``` 187 | cargo install pastel 188 | ``` 189 | 190 | Alternatively, you can install `pastel` directly from this repository by using 191 | ``` 192 | git clone https://github.com/sharkdp/pastel 193 | cargo install --path ./pastel 194 | ``` 195 | 196 | ## Resources 197 | 198 | Interesting Wikipedia pages: 199 | 200 | * [Color difference](https://en.wikipedia.org/wiki/Color_difference) 201 | * [CIE 1931 color space](https://en.wikipedia.org/wiki/CIE_1931_color_space) 202 | * [CIELAB color space](https://en.wikipedia.org/wiki/CIELAB_color_space) 203 | * [Line of purples](https://en.wikipedia.org/wiki/Line_of_purples) 204 | * [Impossible color](https://en.wikipedia.org/wiki/Impossible_color) 205 | * [sRGB](https://en.wikipedia.org/wiki/SRGB) 206 | * [Color theory](https://en.wikipedia.org/wiki/Color_theory) 207 | * [Eigengrau](https://en.wikipedia.org/wiki/Eigengrau) 208 | 209 | Color names: 210 | 211 | * [XKCD Color Survey Results](https://blog.xkcd.com/2010/05/03/color-survey-results/) 212 | * [Peachpuffs and Lemonchiffons - talk about named colors](https://www.youtube.com/watch?v=HmStJQzclHc) 213 | * [List of CSS color keywords](https://www.w3.org/TR/SVG11/types.html#ColorKeywords) 214 | 215 | Maximally distinct colors: 216 | 217 | * [How to automatically generate N "distinct" colors?](https://stackoverflow.com/q/470690/704831) 218 | * [Publication on two algorithms to generate (maximally) distinct colors](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.65.2790) 219 | 220 | Other articles and videos: 221 | 222 | * [Color Matching](https://www.youtube.com/watch?v=82ItpxqPP4I) 223 | * [Introduction to color spaces](https://ciechanow.ski/color-spaces/) 224 | 225 | ## License 226 | 227 | Licensed under either of 228 | 229 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 230 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 231 | 232 | at your option. 233 | -------------------------------------------------------------------------------- /src/named.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | 3 | use crate::Color; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct NamedColor { 7 | pub name: &'static str, 8 | pub color: Color, 9 | } 10 | 11 | fn named_color(name: &'static str, r: u8, g: u8, b: u8) -> NamedColor { 12 | NamedColor { 13 | name, 14 | color: Color::from_rgb(r, g, b), 15 | } 16 | } 17 | 18 | pub static NAMED_COLORS: Lazy<[NamedColor; 148]> = Lazy::new(|| { 19 | [ 20 | named_color("aliceblue", 240, 248, 255), 21 | named_color("antiquewhite", 250, 235, 215), 22 | named_color("aqua", 0, 255, 255), 23 | named_color("aquamarine", 127, 255, 212), 24 | named_color("azure", 240, 255, 255), 25 | named_color("beige", 245, 245, 220), 26 | named_color("bisque", 255, 228, 196), 27 | named_color("black", 0, 0, 0), 28 | named_color("blanchedalmond", 255, 235, 205), 29 | named_color("blue", 0, 0, 255), 30 | named_color("blueviolet", 138, 43, 226), 31 | named_color("brown", 165, 42, 42), 32 | named_color("burlywood", 222, 184, 135), 33 | named_color("cadetblue", 95, 158, 160), 34 | named_color("chartreuse", 127, 255, 0), 35 | named_color("chocolate", 210, 105, 30), 36 | named_color("coral", 255, 127, 80), 37 | named_color("cornflowerblue", 100, 149, 237), 38 | named_color("cornsilk", 255, 248, 220), 39 | named_color("crimson", 220, 20, 60), 40 | named_color("cyan", 0, 255, 255), 41 | named_color("darkblue", 0, 0, 139), 42 | named_color("darkcyan", 0, 139, 139), 43 | named_color("darkgoldenrod", 184, 134, 11), 44 | named_color("darkgray", 169, 169, 169), 45 | named_color("darkgreen", 0, 100, 0), 46 | named_color("darkgrey", 169, 169, 169), 47 | named_color("darkkhaki", 189, 183, 107), 48 | named_color("darkmagenta", 139, 0, 139), 49 | named_color("darkolivegreen", 85, 107, 47), 50 | named_color("darkorange", 255, 140, 0), 51 | named_color("darkorchid", 153, 50, 204), 52 | named_color("darkred", 139, 0, 0), 53 | named_color("darksalmon", 233, 150, 122), 54 | named_color("darkseagreen", 143, 188, 143), 55 | named_color("darkslateblue", 72, 61, 139), 56 | named_color("darkslategray", 47, 79, 79), 57 | named_color("darkslategrey", 47, 79, 79), 58 | named_color("darkturquoise", 0, 206, 209), 59 | named_color("darkviolet", 148, 0, 211), 60 | named_color("deeppink", 255, 20, 147), 61 | named_color("deepskyblue", 0, 191, 255), 62 | named_color("dimgray", 105, 105, 105), 63 | named_color("dimgrey", 105, 105, 105), 64 | named_color("dodgerblue", 30, 144, 255), 65 | named_color("firebrick", 178, 34, 34), 66 | named_color("floralwhite", 255, 250, 240), 67 | named_color("forestgreen", 34, 139, 34), 68 | named_color("fuchsia", 255, 0, 255), 69 | named_color("gainsboro", 220, 220, 220), 70 | named_color("ghostwhite", 248, 248, 255), 71 | named_color("gold", 255, 215, 0), 72 | named_color("goldenrod", 218, 165, 32), 73 | named_color("gray", 128, 128, 128), 74 | named_color("green", 0, 128, 0), 75 | named_color("greenyellow", 173, 255, 47), 76 | named_color("grey", 128, 128, 128), 77 | named_color("honeydew", 240, 255, 240), 78 | named_color("hotpink", 255, 105, 180), 79 | named_color("indianred", 205, 92, 92), 80 | named_color("indigo", 75, 0, 130), 81 | named_color("ivory", 255, 255, 240), 82 | named_color("khaki", 240, 230, 140), 83 | named_color("lavender", 230, 230, 250), 84 | named_color("lavenderblush", 255, 240, 245), 85 | named_color("lawngreen", 124, 252, 0), 86 | named_color("lemonchiffon", 255, 250, 205), 87 | named_color("lightblue", 173, 216, 230), 88 | named_color("lightcoral", 240, 128, 128), 89 | named_color("lightcyan", 224, 255, 255), 90 | named_color("lightgoldenrodyellow", 250, 250, 210), 91 | named_color("lightgray", 211, 211, 211), 92 | named_color("lightgreen", 144, 238, 144), 93 | named_color("lightgrey", 211, 211, 211), 94 | named_color("lightpink", 255, 182, 193), 95 | named_color("lightsalmon", 255, 160, 122), 96 | named_color("lightseagreen", 32, 178, 170), 97 | named_color("lightskyblue", 135, 206, 250), 98 | named_color("lightslategray", 119, 136, 153), 99 | named_color("lightslategrey", 119, 136, 153), 100 | named_color("lightsteelblue", 176, 196, 222), 101 | named_color("lightyellow", 255, 255, 224), 102 | named_color("lime", 0, 255, 0), 103 | named_color("limegreen", 50, 205, 50), 104 | named_color("linen", 250, 240, 230), 105 | named_color("magenta", 255, 0, 255), 106 | named_color("maroon", 128, 0, 0), 107 | named_color("mediumaquamarine", 102, 205, 170), 108 | named_color("mediumblue", 0, 0, 205), 109 | named_color("mediumorchid", 186, 85, 211), 110 | named_color("mediumpurple", 147, 112, 219), 111 | named_color("mediumseagreen", 60, 179, 113), 112 | named_color("mediumslateblue", 123, 104, 238), 113 | named_color("mediumspringgreen", 0, 250, 154), 114 | named_color("mediumturquoise", 72, 209, 204), 115 | named_color("mediumvioletred", 199, 21, 133), 116 | named_color("midnightblue", 25, 25, 112), 117 | named_color("mintcream", 245, 255, 250), 118 | named_color("mistyrose", 255, 228, 225), 119 | named_color("moccasin", 255, 228, 181), 120 | named_color("navajowhite", 255, 222, 173), 121 | named_color("navy", 0, 0, 128), 122 | named_color("oldlace", 253, 245, 230), 123 | named_color("olive", 128, 128, 0), 124 | named_color("olivedrab", 107, 142, 35), 125 | named_color("orange", 255, 165, 0), 126 | named_color("orangered", 255, 69, 0), 127 | named_color("orchid", 218, 112, 214), 128 | named_color("palegoldenrod", 238, 232, 170), 129 | named_color("palegreen", 152, 251, 152), 130 | named_color("paleturquoise", 175, 238, 238), 131 | named_color("palevioletred", 219, 112, 147), 132 | named_color("papayawhip", 255, 239, 213), 133 | named_color("peachpuff", 255, 218, 185), 134 | named_color("peru", 205, 133, 63), 135 | named_color("pink", 255, 192, 203), 136 | named_color("plum", 221, 160, 221), 137 | named_color("powderblue", 176, 224, 230), 138 | named_color("purple", 128, 0, 128), 139 | named_color("rebeccapurple", 102, 51, 153), 140 | named_color("red", 255, 0, 0), 141 | named_color("rosybrown", 188, 143, 143), 142 | named_color("royalblue", 65, 105, 225), 143 | named_color("saddlebrown", 139, 69, 19), 144 | named_color("salmon", 250, 128, 114), 145 | named_color("sandybrown", 244, 164, 96), 146 | named_color("seagreen", 46, 139, 87), 147 | named_color("seashell", 255, 245, 238), 148 | named_color("sienna", 160, 82, 45), 149 | named_color("silver", 192, 192, 192), 150 | named_color("skyblue", 135, 206, 235), 151 | named_color("slateblue", 106, 90, 205), 152 | named_color("slategray", 112, 128, 144), 153 | named_color("slategrey", 112, 128, 144), 154 | named_color("snow", 255, 250, 250), 155 | named_color("springgreen", 0, 255, 127), 156 | named_color("steelblue", 70, 130, 180), 157 | named_color("tan", 210, 180, 140), 158 | named_color("teal", 0, 128, 128), 159 | named_color("thistle", 216, 191, 216), 160 | named_color("tomato", 255, 99, 71), 161 | named_color("turquoise", 64, 224, 208), 162 | named_color("violet", 238, 130, 238), 163 | named_color("wheat", 245, 222, 179), 164 | named_color("white", 255, 255, 255), 165 | named_color("whitesmoke", 245, 245, 245), 166 | named_color("yellow", 255, 255, 0), 167 | named_color("yellowgreen", 154, 205, 50), 168 | ] 169 | }); 170 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/delta_e.rs: -------------------------------------------------------------------------------- 1 | use super::Lab; 2 | use std::f64; 3 | 4 | // The code below is adapted from https://github.com/elliotekj/DeltaE 5 | // 6 | // Original license: 7 | // 8 | // The MIT License (MIT) 9 | // 10 | // Copyright (c) 2017 Elliot Jackson 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to deal 14 | // in the Software without restriction, including without limitation the rights 15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | // copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in all 20 | // copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | // SOFTWARE. 29 | 30 | pub fn cie76(c1: &Lab, c2: &Lab) -> f64 { 31 | ((c1.l - c2.l).powi(2) + (c1.a - c2.a).powi(2) + (c1.b - c2.b).powi(2)).sqrt() 32 | } 33 | 34 | pub fn ciede2000(color1: &Lab, color2: &Lab) -> f64 { 35 | let ksub_l = 1.0; 36 | let ksub_c = 1.0; 37 | let ksub_h = 1.0; 38 | 39 | let delta_l_prime = color2.l - color1.l; 40 | 41 | let l_bar = (color1.l + color2.l) / 2.0; 42 | 43 | let c1 = (color1.a.powi(2) + color1.b.powi(2)).sqrt(); 44 | let c2 = (color2.a.powi(2) + color2.b.powi(2)).sqrt(); 45 | 46 | let c_bar = (c1 + c2) / 2.0; 47 | 48 | let a_prime_1 = color1.a 49 | + (color1.a / 2.0) * (1.0 - (c_bar.powi(7) / (c_bar.powi(7) + 25f64.powi(7))).sqrt()); 50 | let a_prime_2 = color2.a 51 | + (color2.a / 2.0) * (1.0 - (c_bar.powi(7) / (c_bar.powi(7) + 25f64.powi(7))).sqrt()); 52 | 53 | let c_prime_1 = (a_prime_1.powi(2) + color1.b.powi(2)).sqrt(); 54 | let c_prime_2 = (a_prime_2.powi(2) + color2.b.powi(2)).sqrt(); 55 | 56 | let c_bar_prime = (c_prime_1 + c_prime_2) / 2.0; 57 | 58 | let delta_c_prime = c_prime_2 - c_prime_1; 59 | 60 | let s_sub_l = 1.0 + ((0.015 * (l_bar - 50.0).powi(2)) / (20.0 + (l_bar - 50.0).powi(2)).sqrt()); 61 | 62 | let s_sub_c = 1.0 + 0.045 * c_bar_prime; 63 | 64 | let h_prime_1 = get_h_prime_fn(color1.b, a_prime_1); 65 | let h_prime_2 = get_h_prime_fn(color2.b, a_prime_2); 66 | 67 | let delta_h_prime = get_delta_h_prime(c1, c2, h_prime_1, h_prime_2); 68 | 69 | let delta_upcase_h_prime = 70 | 2.0 * (c_prime_1 * c_prime_2).sqrt() * (degrees_to_radians(delta_h_prime) / 2.0).sin(); 71 | 72 | let upcase_h_bar_prime = get_upcase_h_bar_prime(h_prime_1, h_prime_2); 73 | 74 | let upcase_t = get_upcase_t(upcase_h_bar_prime); 75 | 76 | let s_sub_upcase_h = 1.0 + 0.015 * c_bar_prime * upcase_t; 77 | 78 | let r_sub_t = get_r_sub_t(c_bar_prime, upcase_h_bar_prime); 79 | 80 | let lightness: f64 = delta_l_prime / (ksub_l * s_sub_l); 81 | 82 | let chroma: f64 = delta_c_prime / (ksub_c * s_sub_c); 83 | 84 | let hue: f64 = delta_upcase_h_prime / (ksub_h * s_sub_upcase_h); 85 | 86 | (lightness.powi(2) + chroma.powi(2) + hue.powi(2) + r_sub_t * chroma * hue).sqrt() 87 | } 88 | 89 | fn get_h_prime_fn(x: f64, y: f64) -> f64 { 90 | if x == 0.0 && y == 0.0 { 91 | return 0.0; 92 | } 93 | 94 | let mut hue_angle = radians_to_degrees(x.atan2(y)); 95 | 96 | if hue_angle < 0.0 { 97 | hue_angle += 360.0; 98 | } 99 | 100 | hue_angle 101 | } 102 | 103 | fn get_delta_h_prime(c1: f64, c2: f64, h_prime_1: f64, h_prime_2: f64) -> f64 { 104 | if 0.0 == c1 || 0.0 == c2 { 105 | return 0.0; 106 | } 107 | 108 | if (h_prime_1 - h_prime_2).abs() <= 180.0 { 109 | return h_prime_2 - h_prime_1; 110 | } 111 | 112 | if h_prime_2 <= h_prime_1 { 113 | h_prime_2 - h_prime_1 + 360.0 114 | } else { 115 | h_prime_2 - h_prime_1 - 360.0 116 | } 117 | } 118 | 119 | fn get_upcase_h_bar_prime(h_prime_1: f64, h_prime_2: f64) -> f64 { 120 | if (h_prime_1 - h_prime_2).abs() > 180.0 { 121 | return (h_prime_1 + h_prime_2 + 360.0) / 2.0; 122 | } 123 | 124 | (h_prime_1 + h_prime_2) / 2.0 125 | } 126 | 127 | fn get_upcase_t(upcase_h_bar_prime: f64) -> f64 { 128 | 1.0 - 0.17 * (degrees_to_radians(upcase_h_bar_prime - 30.0)).cos() 129 | + 0.24 * (degrees_to_radians(2.0 * upcase_h_bar_prime)).cos() 130 | + 0.32 * (degrees_to_radians(3.0 * upcase_h_bar_prime + 6.0)).cos() 131 | - 0.20 * (degrees_to_radians(4.0 * upcase_h_bar_prime - 63.0)).cos() 132 | } 133 | 134 | fn get_r_sub_t(c_bar_prime: f64, upcase_h_bar_prime: f64) -> f64 { 135 | -2.0 * (c_bar_prime.powi(7) / (c_bar_prime.powi(7) + 25f64.powi(7))).sqrt() 136 | * (degrees_to_radians(60.0 * (-(((upcase_h_bar_prime - 275.0) / 25.0).powi(2))).exp())) 137 | .sin() 138 | } 139 | 140 | fn radians_to_degrees(radians: f64) -> f64 { 141 | radians * (180.0 / f64::consts::PI) 142 | } 143 | 144 | fn degrees_to_radians(degrees: f64) -> f64 { 145 | degrees * (f64::consts::PI / 180.0) 146 | } 147 | 148 | #[cfg(test)] 149 | mod tests { 150 | use super::{ciede2000, Lab}; 151 | 152 | fn round(val: f64) -> f64 { 153 | let rounded = val * 10000_f64; 154 | rounded.round() / 10000_f64 155 | } 156 | 157 | fn assert_delta_e(expected: f64, lab1: &[f64; 3], lab2: &[f64; 3]) { 158 | let color1 = Lab { 159 | l: lab1[0], 160 | a: lab1[1], 161 | b: lab1[2], 162 | alpha: 1.0, 163 | }; 164 | 165 | let color2 = Lab { 166 | l: lab2[0], 167 | a: lab2[1], 168 | b: lab2[2], 169 | alpha: 1.0, 170 | }; 171 | 172 | assert_eq!(round(ciede2000(&color1, &color2)), expected); 173 | } 174 | 175 | // Tests taken from Table 1: "CIEDE2000 total color difference test data" of 176 | // "The CIEDE2000 Color-Difference Formula: Implementation Notes, 177 | // Supplementary Test Data, and Mathematical Observations" by Gaurav Sharma, 178 | // Wencheng Wu and Edul N. Dalal. 179 | // 180 | // http://www.ece.rochester.edu/~gsharma/papers/CIEDE2000CRNAFeb05.pdf 181 | 182 | #[test] 183 | fn tests() { 184 | assert_delta_e(0.0, &[0.0, 0.0, 0.0], &[0.0, 0.0, 0.0]); 185 | assert_delta_e(0.0, &[99.5, 0.005, -0.010], &[99.5, 0.005, -0.010]); 186 | assert_delta_e(100.0, &[100.0, 0.005, -0.010], &[0.0, 0.0, 0.0]); 187 | assert_delta_e( 188 | 2.0425, 189 | &[50.0000, 2.6772, -79.7751], 190 | &[50.0000, 0.0000, -82.7485], 191 | ); 192 | assert_delta_e( 193 | 2.8615, 194 | &[50.0000, 3.1571, -77.2803], 195 | &[50.0000, 0.0000, -82.7485], 196 | ); 197 | assert_delta_e( 198 | 3.4412, 199 | &[50.0000, 2.8361, -74.0200], 200 | &[50.0000, 0.0000, -82.7485], 201 | ); 202 | assert_delta_e( 203 | 1.0000, 204 | &[50.0000, -1.3802, -84.2814], 205 | &[50.0000, 0.0000, -82.7485], 206 | ); 207 | assert_delta_e( 208 | 1.0000, 209 | &[50.0000, -1.1848, -84.8006], 210 | &[50.0000, 0.0000, -82.7485], 211 | ); 212 | assert_delta_e( 213 | 1.0000, 214 | &[50.0000, -0.9009, -85.5211], 215 | &[50.0000, 0.0000, -82.7485], 216 | ); 217 | assert_delta_e( 218 | 2.3669, 219 | &[50.0000, 0.0000, 0.0000], 220 | &[50.0000, -1.0000, 2.0000], 221 | ); 222 | assert_delta_e( 223 | 2.3669, 224 | &[50.0000, -1.0000, 2.0000], 225 | &[50.0000, 0.0000, 0.0000], 226 | ); 227 | assert_delta_e( 228 | 7.1792, 229 | &[50.0000, 2.4900, -0.0010], 230 | &[50.0000, -2.4900, 0.0009], 231 | ); 232 | assert_delta_e( 233 | 7.1792, 234 | &[50.0000, 2.4900, -0.0010], 235 | &[50.0000, -2.4900, 0.0010], 236 | ); 237 | assert_delta_e( 238 | 7.2195, 239 | &[50.0000, 2.4900, -0.0010], 240 | &[50.0000, -2.4900, 0.0011], 241 | ); 242 | assert_delta_e( 243 | 7.2195, 244 | &[50.0000, 2.4900, -0.0010], 245 | &[50.0000, -2.4900, 0.0012], 246 | ); 247 | assert_delta_e( 248 | 4.8045, 249 | &[50.0000, -0.0010, 2.4900], 250 | &[50.0000, 0.0009, -2.4900], 251 | ); 252 | assert_delta_e( 253 | 4.7461, 254 | &[50.0000, -0.0010, 2.4900], 255 | &[50.0000, 0.0011, -2.4900], 256 | ); 257 | assert_delta_e( 258 | 4.3065, 259 | &[50.0000, 2.5000, 0.0000], 260 | &[50.0000, 0.0000, -2.5000], 261 | ); 262 | assert_delta_e( 263 | 27.1492, 264 | &[50.0000, 2.5000, 0.0000], 265 | &[73.0000, 25.0000, -18.0000], 266 | ); 267 | assert_delta_e( 268 | 22.8977, 269 | &[50.0000, 2.5000, 0.0000], 270 | &[61.0000, -5.0000, 29.0000], 271 | ); 272 | assert_delta_e( 273 | 31.9030, 274 | &[50.0000, 2.5000, 0.0000], 275 | &[56.0000, -27.0000, -3.0000], 276 | ); 277 | assert_delta_e( 278 | 19.4535, 279 | &[50.0000, 2.5000, 0.0000], 280 | &[58.0000, 24.0000, 15.0000], 281 | ); 282 | assert_delta_e( 283 | 1.0000, 284 | &[50.0000, 2.5000, 0.0000], 285 | &[50.0000, 3.1736, 0.5854], 286 | ); 287 | assert_delta_e( 288 | 1.0000, 289 | &[50.0000, 2.5000, 0.0000], 290 | &[50.0000, 3.2972, 0.0000], 291 | ); 292 | assert_delta_e( 293 | 1.0000, 294 | &[50.0000, 2.5000, 0.0000], 295 | &[50.0000, 1.8634, 0.5757], 296 | ); 297 | assert_delta_e( 298 | 1.0000, 299 | &[50.0000, 2.5000, 0.0000], 300 | &[50.0000, 3.2592, 0.3350], 301 | ); 302 | assert_delta_e( 303 | 1.2644, 304 | &[60.2574, -34.0099, 36.2677], 305 | &[60.4626, -34.1751, 39.4387], 306 | ); 307 | assert_delta_e( 308 | 1.2630, 309 | &[63.0109, -31.0961, -5.8663], 310 | &[62.8187, -29.7946, -4.0864], 311 | ); 312 | assert_delta_e( 313 | 1.8731, 314 | &[61.2901, 3.7196, -5.3901], 315 | &[61.4292, 2.2480, -4.9620], 316 | ); 317 | assert_delta_e( 318 | 1.8645, 319 | &[35.0831, -44.1164, 3.7933], 320 | &[35.0232, -40.0716, 1.5901], 321 | ); 322 | assert_delta_e( 323 | 2.0373, 324 | &[22.7233, 20.0904, -46.6940], 325 | &[23.0331, 14.9730, -42.5619], 326 | ); 327 | assert_delta_e( 328 | 1.4146, 329 | &[36.4612, 47.8580, 18.3852], 330 | &[36.2715, 50.5065, 21.2231], 331 | ); 332 | assert_delta_e( 333 | 1.4441, 334 | &[90.8027, -2.0831, 1.4410], 335 | &[91.1528, -1.6435, 0.0447], 336 | ); 337 | assert_delta_e( 338 | 1.5381, 339 | &[90.9257, -0.5406, -0.9208], 340 | &[88.6381, -0.8985, -0.7239], 341 | ); 342 | assert_delta_e( 343 | 0.6377, 344 | &[6.7747, -0.2908, -2.4247], 345 | &[5.8714, -0.0985, -2.2286], 346 | ); 347 | assert_delta_e( 348 | 0.9082, 349 | &[2.0776, 0.0795, -1.1350], 350 | &[0.9033, -0.0636, -0.5514], 351 | ); 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/ansi.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | 3 | pub use atty::Stream; 4 | use once_cell::sync::Lazy; 5 | 6 | use crate::delta_e::ciede2000; 7 | use crate::{Color, Lab}; 8 | 9 | static ANSI_LAB_REPRESENTATIONS: Lazy> = Lazy::new(|| { 10 | (16..255) 11 | .map(|code| (code, Color::from_ansi_8bit(code).to_lab())) 12 | .collect() 13 | }); 14 | 15 | #[derive(Debug, Clone, Copy, PartialEq)] 16 | pub enum Mode { 17 | Ansi8Bit, 18 | TrueColor, 19 | } 20 | 21 | #[derive(Debug)] 22 | pub struct UnknownColorModeError(pub String); 23 | 24 | impl Mode { 25 | pub fn from_mode_str(mode_str: &str) -> Result, UnknownColorModeError> { 26 | match mode_str { 27 | "24bit" | "truecolor" => Ok(Some(Mode::TrueColor)), 28 | "8bit" => Ok(Some(Mode::Ansi8Bit)), 29 | "off" => Ok(None), 30 | value => Err(UnknownColorModeError(value.into())), 31 | } 32 | } 33 | } 34 | 35 | fn cube_to_8bit(code: u8) -> u8 { 36 | assert!(code < 6); 37 | match code { 38 | 0 => 0, 39 | _ => 55 + 40 * code, 40 | } 41 | } 42 | 43 | pub trait AnsiColor { 44 | fn from_ansi_8bit(code: u8) -> Self; 45 | fn to_ansi_8bit(&self) -> u8; 46 | 47 | fn to_ansi_sequence(&self, mode: Mode) -> String; 48 | } 49 | 50 | impl AnsiColor for Color { 51 | /// Create a color from an 8-bit ANSI escape code 52 | /// 53 | /// See: 54 | fn from_ansi_8bit(code: u8) -> Color { 55 | match code { 56 | 0 => Color::black(), 57 | 1 => Color::maroon(), 58 | 2 => Color::green(), 59 | 3 => Color::olive(), 60 | 4 => Color::navy(), 61 | 5 => Color::purple(), 62 | 6 => Color::teal(), 63 | 7 => Color::silver(), 64 | 8 => Color::gray(), 65 | 9 => Color::red(), 66 | 10 => Color::lime(), 67 | 11 => Color::yellow(), 68 | 12 => Color::blue(), 69 | 13 => Color::fuchsia(), 70 | 14 => Color::aqua(), 71 | 15 => Color::white(), 72 | 16..=231 => { 73 | // 6 x 6 x 6 cube of 216 colors. We need to decode from 74 | // 75 | // code = 16 + 36 × r + 6 × g + b 76 | 77 | let code_rgb = code - 16; 78 | let blue = code_rgb % 6; 79 | 80 | let code_rg = (code_rgb - blue) / 6; 81 | let green = code_rg % 6; 82 | 83 | let red = (code_rg - green) / 6; 84 | 85 | Color::from_rgb(cube_to_8bit(red), cube_to_8bit(green), cube_to_8bit(blue)) 86 | } 87 | 232..=255 => { 88 | // grayscale from (almost) black to (almost) white in 24 steps 89 | 90 | let gray_value = 10 * (code - 232) + 8; 91 | Color::from_rgb(gray_value, gray_value, gray_value) 92 | } 93 | } 94 | } 95 | 96 | /// Approximate a color by its closest 8-bit ANSI color (as measured by the perceived 97 | /// color distance). 98 | /// 99 | /// See: 100 | fn to_ansi_8bit(&self) -> u8 { 101 | let self_lab = self.to_lab(); 102 | ANSI_LAB_REPRESENTATIONS 103 | .iter() 104 | .min_by_key(|(_, lab)| ciede2000(&self_lab, lab) as i32) 105 | .expect("list of codes can not be empty") 106 | .0 107 | } 108 | 109 | /// Return an ANSI escape sequence in 8-bit or 24-bit representation: 110 | /// * 8-bit: `ESC[38;5;CODEm`, where CODE represents the color. 111 | /// * 24-bit: `ESC[38;2;R;G;Bm`, where R, G, B represent 8-bit RGB values 112 | fn to_ansi_sequence(&self, mode: Mode) -> String { 113 | match mode { 114 | Mode::Ansi8Bit => format!("\x1b[38;5;{}m", self.to_ansi_8bit()), 115 | Mode::TrueColor => { 116 | let rgba = self.to_rgba(); 117 | format!("\x1b[38;2;{r};{g};{b}m", r = rgba.r, g = rgba.g, b = rgba.b) 118 | } 119 | } 120 | } 121 | } 122 | 123 | #[derive(Debug, Default, Clone, PartialEq)] 124 | pub struct Style { 125 | foreground: Option, 126 | background: Option, 127 | bold: bool, 128 | italic: bool, 129 | underline: bool, 130 | } 131 | 132 | impl Style { 133 | pub fn foreground(&mut self, color: &Color) -> &mut Self { 134 | self.foreground = Some(color.clone()); 135 | self 136 | } 137 | 138 | pub fn on>(&mut self, color: C) -> &mut Self { 139 | self.background = Some(color.borrow().clone()); 140 | self 141 | } 142 | 143 | pub fn bold(&mut self, on: bool) -> &mut Self { 144 | self.bold = on; 145 | self 146 | } 147 | 148 | pub fn italic(&mut self, on: bool) -> &mut Self { 149 | self.italic = on; 150 | self 151 | } 152 | 153 | pub fn underline(&mut self, on: bool) -> &mut Self { 154 | self.underline = on; 155 | self 156 | } 157 | 158 | pub fn escape_sequence(&self, mode: Mode) -> String { 159 | let mut codes: Vec = vec![]; 160 | 161 | if let Some(ref fg) = self.foreground { 162 | match mode { 163 | Mode::Ansi8Bit => codes.extend_from_slice(&[38, 5, fg.to_ansi_8bit()]), 164 | Mode::TrueColor => { 165 | let rgb = fg.to_rgba(); 166 | codes.extend_from_slice(&[38, 2, rgb.r, rgb.g, rgb.b]); 167 | } 168 | } 169 | } 170 | if let Some(ref bg) = self.background { 171 | match mode { 172 | Mode::Ansi8Bit => codes.extend_from_slice(&[48, 5, bg.to_ansi_8bit()]), 173 | Mode::TrueColor => { 174 | let rgb = bg.to_rgba(); 175 | codes.extend_from_slice(&[48, 2, rgb.r, rgb.g, rgb.b]); 176 | } 177 | } 178 | } 179 | 180 | if self.bold { 181 | codes.push(1); 182 | } 183 | 184 | if self.italic { 185 | codes.push(3); 186 | } 187 | 188 | if self.underline { 189 | codes.push(4); 190 | } 191 | 192 | if codes.is_empty() { 193 | codes.push(0); 194 | } 195 | 196 | format!( 197 | "\x1b[{codes}m", 198 | codes = codes 199 | .iter() 200 | .map(|c| c.to_string()) 201 | .collect::>() 202 | .join(";") 203 | ) 204 | } 205 | } 206 | 207 | impl From for Style { 208 | fn from(color: Color) -> Style { 209 | Style { 210 | foreground: Some(color), 211 | background: None, 212 | bold: false, 213 | italic: false, 214 | underline: false, 215 | } 216 | } 217 | } 218 | 219 | impl From<&Color> for Style { 220 | fn from(color: &Color) -> Style { 221 | color.clone().into() 222 | } 223 | } 224 | 225 | impl From<&Style> for Style { 226 | fn from(style: &Style) -> Style { 227 | style.clone() 228 | } 229 | } 230 | 231 | impl From<&mut Style> for Style { 232 | fn from(style: &mut Style) -> Style { 233 | style.clone() 234 | } 235 | } 236 | 237 | pub trait ToAnsiStyle { 238 | fn ansi_style(&self) -> Style; 239 | } 240 | 241 | impl ToAnsiStyle for Color { 242 | fn ansi_style(&self) -> Style { 243 | self.clone().into() 244 | } 245 | } 246 | 247 | #[cfg(not(windows))] 248 | pub fn get_colormode() -> Option { 249 | use std::env; 250 | let env_nocolor = env::var_os("NO_COLOR"); 251 | if env_nocolor.is_some() { 252 | return None; 253 | } 254 | 255 | let env_colorterm = env::var("COLORTERM").ok(); 256 | match env_colorterm.as_deref() { 257 | Some("truecolor") | Some("24bit") => Some(Mode::TrueColor), 258 | _ => Some(Mode::Ansi8Bit), 259 | } 260 | } 261 | 262 | #[cfg(windows)] 263 | pub fn get_colormode() -> Option { 264 | use std::env; 265 | let env_nocolor = env::var_os("NO_COLOR"); 266 | match env_nocolor { 267 | Some(_) => None, 268 | // Assume 24bit support on Windows 269 | None => Some(Mode::TrueColor), 270 | } 271 | } 272 | 273 | #[derive(Default, Debug, Clone, Copy)] 274 | pub struct Brush { 275 | mode: Option, 276 | } 277 | 278 | impl Brush { 279 | pub fn from_mode(mode: Option) -> Self { 280 | Brush { mode } 281 | } 282 | 283 | pub fn from_environment(stream: Stream) -> Result { 284 | let mode = if atty::is(stream) { 285 | let env_color_mode = std::env::var("PASTEL_COLOR_MODE").ok(); 286 | match env_color_mode.as_deref() { 287 | Some(mode_str) => Mode::from_mode_str(mode_str)?, 288 | None => get_colormode(), 289 | } 290 | } else { 291 | None 292 | }; 293 | Ok(Brush { mode }) 294 | } 295 | 296 | pub fn paint(self, text: S, style: impl Into