├── images ├── chefs_kiss.gif ├── chefs_kiss.png ├── fidget_spinner.gif └── fidget_spinner_rainbow.gif ├── src ├── lib.rs ├── buffer.rs ├── error_utils.rs ├── bin │ ├── print_colors.rs │ └── generate_gradient.rs ├── color │ ├── quantize.rs │ ├── gradient.rs │ └── mod.rs ├── codec │ ├── mod.rs │ ├── image.rs │ └── gif.rs ├── commandline.rs └── main.rs ├── go ├── go.mod ├── go.sum ├── blend.go ├── static_image.go ├── blend_test.go ├── gradient.go ├── main.go ├── README.md ├── quantize.go └── gradient_test.go ├── README.md ├── .gitignore ├── Cargo.toml ├── LICENSE └── Cargo.lock /images/chefs_kiss.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwoos/rainbowgif/HEAD/images/chefs_kiss.gif -------------------------------------------------------------------------------- /images/chefs_kiss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwoos/rainbowgif/HEAD/images/chefs_kiss.png -------------------------------------------------------------------------------- /images/fidget_spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwoos/rainbowgif/HEAD/images/fidget_spinner.gif -------------------------------------------------------------------------------- /images/fidget_spinner_rainbow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwoos/rainbowgif/HEAD/images/fidget_spinner_rainbow.gif -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod buffer; 2 | pub mod codec; 3 | pub mod color; 4 | pub mod commandline; 5 | pub mod error_utils; 6 | -------------------------------------------------------------------------------- /go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jwoos/rainbowgif 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/lucasb-eyer/go-colorful v1.0.3 7 | ) 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rainbow GIF 2 | ## What? 3 | This is a program to read in images and overlay colors over the frames to create a rainbow effect. 4 | -------------------------------------------------------------------------------- /go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= 2 | github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | rainbowgif 15 | 16 | 17 | # Added by cargo 18 | 19 | /target 20 | 21 | .DS_Store 22 | 23 | images/ 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rainbowgif" 3 | version = "0.1.0" 4 | edition = "2024" 5 | default-run = "rainbowgif" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | clap = { version = "4.0.26", features = ["cargo"] } 11 | palette = "0.6.1" 12 | gif = "0.12.0" 13 | image = "0.24.5" 14 | imagequant = "4.0.4" 15 | 16 | [features] 17 | default = [] 18 | -------------------------------------------------------------------------------- /src/buffer.rs: -------------------------------------------------------------------------------- 1 | use std::error; 2 | use std::fs; 3 | use std::io; 4 | use std::io::Read; 5 | use std::path; 6 | use std::vec; 7 | 8 | pub type Buffer = io::Cursor>; 9 | 10 | pub struct Data { 11 | pub buffer: Buffer, 12 | } 13 | 14 | impl Data { 15 | pub fn new() -> Self { 16 | return Data { 17 | buffer: io::Cursor::new(vec::Vec::new()), 18 | }; 19 | } 20 | 21 | pub fn from_path>(p: P) -> Result> { 22 | let file = fs::File::open(p)?; 23 | return Self::from_file(file); 24 | } 25 | 26 | pub fn from_file(mut file: fs::File) -> Result> { 27 | let mut data = Self::new(); 28 | 29 | file.read_to_end(data.buffer.get_mut())?; 30 | 31 | return Ok(data); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/error_utils.rs: -------------------------------------------------------------------------------- 1 | macro_rules! define_error { 2 | ($x:ident, { $($y:ident : $z:literal),* $(,)? }) => { 3 | #[derive(Debug)] 4 | #[allow(dead_code)] 5 | pub enum $x { 6 | $( 7 | $y(Option>, String), 8 | )* 9 | } 10 | 11 | impl std::fmt::Display for $x { 12 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 13 | return match self { 14 | $( 15 | Self::$y(_, desc) => f.write_str(format!("{}: {desc}", $z).as_str()), 16 | )* 17 | }; 18 | } 19 | } 20 | 21 | impl std::error::Error for $x { 22 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 23 | return match self { 24 | $( 25 | Self::$y(src, _) => src.as_ref().map(|e| e.as_ref()), 26 | )* 27 | }; 28 | } 29 | } 30 | }; 31 | } 32 | 33 | pub(crate) use define_error; 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jun Woo Shin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /go/blend.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* Functions for different blend modes 4 | */ 5 | 6 | import ( 7 | "github.com/lucasb-eyer/go-colorful" 8 | ) 9 | 10 | // top layer over bottom - most common 11 | func blendNormal(top colorful.Color, topAlpha float64, bottom colorful.Color, bottomAlpha float64) (colorful.Color, float64) { 12 | alphaDelta := (1 - topAlpha) * bottomAlpha 13 | 14 | alpha := alphaDelta + topAlpha 15 | red := (alphaDelta*bottom.R + topAlpha*top.R) / alpha 16 | green := (alphaDelta*bottom.G + topAlpha*top.G) / alpha 17 | blue := (alphaDelta*bottom.B + topAlpha*top.B) / alpha 18 | 19 | result := colorful.Color{R: red / 255, G: green / 255, B: blue / 255} 20 | 21 | return result.Clamped(), alpha 22 | } 23 | 24 | /* color blend 25 | * preserves the luma of the bottom 26 | * adopts the hue and chroma of the top 27 | */ 28 | func blendColor(top colorful.Color, bottom colorful.Color) colorful.Color { 29 | topHue, topChroma, _ := top.Hcl() 30 | _, _, bottomLuma := bottom.Hcl() 31 | 32 | result := colorful.Hcl(topHue, topChroma, bottomLuma) 33 | 34 | return result.Clamped() 35 | } 36 | 37 | /* color blend 38 | * preserves the chroma and luma of the bottom 39 | * adopts the hue of the top 40 | */ 41 | func blendHue(top colorful.Color, bottom colorful.Color) colorful.Color { 42 | topHue, _, _ := top.Hcl() 43 | _, bottomChroma, bottomLuma := bottom.Hcl() 44 | 45 | result := colorful.Hcl(topHue, bottomChroma, bottomLuma) 46 | 47 | return result.Clamped() 48 | } 49 | -------------------------------------------------------------------------------- /go/static_image.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | "image/gif" 8 | ) 9 | 10 | func staticTransform(img image.Image, format string, quantizer string, delay uint) (*gif.GIF, error) { 11 | transform := img.ColorModel() != color.RGBAModel 12 | 13 | bounds := img.Bounds() 14 | 15 | colors := make([]color.RGBA, (bounds.Max.Y-bounds.Min.Y)*(bounds.Max.X-bounds.Min.X)) 16 | stride := bounds.Max.X - bounds.Min.X 17 | for y := bounds.Min.Y; y < bounds.Max.Y; y++ { 18 | for x := bounds.Min.X; x < bounds.Max.X; x++ { 19 | c := img.At(x, y) 20 | if transform { 21 | colors[y*stride+x] = color.RGBAModel.Convert(c).(color.RGBA) 22 | } else { 23 | colors[y*stride+x] = c.(color.RGBA) 24 | } 25 | } 26 | } 27 | 28 | q := newQuantizer(256) 29 | newColorsPtr, indexMap, err := q.quantize(quantizer, colors) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | pix := make([]uint8, len(colors)) 35 | for i, paletteIndex := range indexMap { 36 | pix[i] = uint8(paletteIndex) 37 | } 38 | 39 | newColors := make([]color.Color, len(newColorsPtr)) 40 | for i, c := range newColorsPtr { 41 | newColors[i] = *c 42 | } 43 | 44 | pi := image.NewPaletted(bounds, newColors) 45 | // dithering 46 | draw.FloydSteinberg.Draw(pi, bounds, img, image.Point{}) 47 | pi.Stride = stride 48 | pi.Pix = pix 49 | 50 | gifImg := gif.GIF{ 51 | Image: []*image.Paletted{pi}, 52 | Delay: []int{int(delay)}, 53 | LoopCount: 0, 54 | } 55 | 56 | return &gifImg, nil 57 | } 58 | -------------------------------------------------------------------------------- /src/bin/print_colors.rs: -------------------------------------------------------------------------------- 1 | use std::error; 2 | 3 | use clap::{arg, command}; 4 | use palette::{self, FromColor}; 5 | 6 | fn main() -> Result<(), Box> { 7 | let matches = command!() 8 | .arg(arg!(color: "Color to view")) 9 | .get_matches(); 10 | 11 | let color = matches.get_one::("color").unwrap(); 12 | 13 | { 14 | let rgba_color: palette::rgb::Rgba = rainbowgif::color::from_hex(color)?; 15 | println!("RGBA: {:?}", rgba_color); 16 | 17 | let lcha_color = palette::Lcha::from_color(rgba_color); 18 | println!("LCHA: {:?}", lcha_color); 19 | 20 | let laba_color = palette::Laba::from_color(rgba_color); 21 | println!("LABA: {:?}", laba_color); 22 | 23 | let hsla_color = palette::Hsla::from_color(rgba_color); 24 | println!("HSLA: {:?}", hsla_color); 25 | 26 | let hsva_color = palette::Hsva::from_color(rgba_color); 27 | println!("HSVA: {:?}", hsva_color); 28 | } 29 | 30 | println!("--------------"); 31 | 32 | { 33 | let lin_rgba_color: palette::rgb::LinSrgba = rainbowgif::color::from_hex(color)?; 34 | println!("RGBA: {:?}", lin_rgba_color); 35 | 36 | let lcha_color = palette::Lcha::from_color(lin_rgba_color); 37 | println!("LCHA: {:?}", lcha_color); 38 | 39 | let laba_color = palette::Laba::from_color(lin_rgba_color); 40 | println!("LABA: {:?}", laba_color); 41 | 42 | let hsla_color = palette::Hsla::from_color(lin_rgba_color); 43 | println!("HSLA: {:?}", hsla_color); 44 | 45 | let hsva_color = palette::Hsva::from_color(lin_rgba_color); 46 | println!("HSVA: {:?}", hsva_color); 47 | } 48 | 49 | println!("--------------"); 50 | 51 | { 52 | let gamma_rgba_color: palette::rgb::GammaSrgba = rainbowgif::color::from_hex(color)?; 53 | println!("RGBA: {:?}", gamma_rgba_color); 54 | 55 | let lcha_color = palette::Lcha::from_color(gamma_rgba_color); 56 | println!("LCHA: {:?}", lcha_color); 57 | 58 | let laba_color = palette::Laba::from_color(gamma_rgba_color); 59 | println!("LABA: {:?}", laba_color); 60 | 61 | let hsla_color = palette::Hsla::from_color(gamma_rgba_color); 62 | println!("HSLA: {:?}", hsla_color); 63 | 64 | let hsva_color = palette::Hsva::from_color(gamma_rgba_color); 65 | println!("HSVA: {:?}", hsva_color); 66 | } 67 | 68 | return Ok(()); 69 | } 70 | -------------------------------------------------------------------------------- /go/blend_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/lucasb-eyer/go-colorful" 7 | ) 8 | 9 | func TestBlendNormal(t *testing.T) { 10 | t.Run( 11 | "Top alpha 1 - bottom alpha 1", 12 | func(innerT *testing.T) { 13 | topColor := colorful.Color{0, 0, 0} 14 | topAlpha := 1.0 15 | bottomColor := colorful.Color{1, 1, 1} 16 | bottomAlpha := 1.0 17 | 18 | color, alpha := blendNormal( 19 | topColor, 20 | topAlpha, 21 | bottomColor, 22 | bottomAlpha, 23 | ) 24 | 25 | if color != topColor { 26 | innerT.Errorf("Expected %v but got %v", topColor, color) 27 | } 28 | 29 | if alpha != 1 { 30 | innerT.Errorf("Expected %v but got %v", topAlpha, alpha) 31 | } 32 | }, 33 | ) 34 | 35 | t.Run( 36 | "Top alpha 0 - bottom alpha 1", 37 | func(innerT *testing.T) { 38 | topColor := colorful.Color{0, 0, 0} 39 | topAlpha := 0.0 40 | bottomColor := colorful.Color{1, 1, 1} 41 | bottomAlpha := 1.0 42 | 43 | color, alpha := blendNormal( 44 | topColor, 45 | topAlpha, 46 | bottomColor, 47 | bottomAlpha, 48 | ) 49 | 50 | if color != bottomColor { 51 | innerT.Errorf("Expected %v but got %v", bottomColor, color) 52 | } 53 | 54 | if alpha != bottomAlpha { 55 | innerT.Errorf("Expected %v but got %v", bottomAlpha, alpha) 56 | } 57 | }, 58 | ) 59 | 60 | t.Run( 61 | "Top alpha 0.5 - bottom alpha 0.5", 62 | func(innerT *testing.T) { 63 | topColor := colorful.Color{0, 0, 0} 64 | topAlpha := 0.5 65 | bottomColor := colorful.Color{1, 1, 1} 66 | bottomAlpha := 0.5 67 | 68 | color, alpha := blendNormal( 69 | topColor, 70 | topAlpha, 71 | bottomColor, 72 | bottomAlpha, 73 | ) 74 | 75 | if color == bottomColor || color == topColor { 76 | innerT.Errorf("Expected %v but got %v", nil, color) 77 | } 78 | 79 | if alpha != 0.75 { 80 | innerT.Errorf("Expected %v but got %v", 1, alpha) 81 | } 82 | }, 83 | ) 84 | 85 | t.Run( 86 | "Top alpha 0.5 - bottom alpha 1.0", 87 | func(innerT *testing.T) { 88 | topColor := colorful.Color{0, 0, 0} 89 | topAlpha := 0.5 90 | bottomColor := colorful.Color{1, 1, 1} 91 | bottomAlpha := 1.0 92 | 93 | color, alpha := blendNormal( 94 | topColor, 95 | topAlpha, 96 | bottomColor, 97 | bottomAlpha, 98 | ) 99 | 100 | if color == bottomColor || color == topColor { 101 | innerT.Errorf("Expected %v but got %v", nil, color) 102 | } 103 | 104 | if alpha != 1.0 { 105 | innerT.Errorf("Expected %v but got %v", 1, alpha) 106 | } 107 | }, 108 | ) 109 | } 110 | -------------------------------------------------------------------------------- /go/gradient.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/lucasb-eyer/go-colorful" 7 | ) 8 | 9 | type Gradient struct { 10 | colors []colorful.Color 11 | positions []float64 12 | } 13 | 14 | type GradientKeyFrame struct { 15 | color colorful.Color 16 | position float64 17 | index int 18 | } 19 | 20 | func newGradient(colors []colorful.Color, wrap bool) Gradient { 21 | var gradient Gradient 22 | 23 | if wrap && len(colors) > 1 { 24 | // wrap around 25 | gradient = Gradient{ 26 | colors: make([]colorful.Color, len(colors)+1), 27 | positions: make([]float64, len(colors)+1), 28 | } 29 | copy(gradient.colors, colors) 30 | gradient.colors[len(colors)] = colors[0] 31 | } else { 32 | gradient = Gradient{ 33 | colors: make([]colorful.Color, len(colors)), 34 | positions: make([]float64, len(colors)), 35 | } 36 | copy(gradient.colors, colors) 37 | } 38 | 39 | colorCount := len(gradient.colors) - 1 40 | 41 | if len(gradient.colors) == 1 { 42 | gradient.positions[0] = 0.0 43 | } else { 44 | // Distribute the colors evenly 45 | for i := 0; i <= colorCount; i++ { 46 | gradient.positions[i] = float64(i) / float64(colorCount) 47 | } 48 | } 49 | 50 | return gradient 51 | } 52 | 53 | func (gradient Gradient) generate(frameCount uint) []colorful.Color { 54 | generated := make([]colorful.Color, frameCount) 55 | 56 | for i := uint(0); i < frameCount; i++ { 57 | position := float64(i) / float64(frameCount) 58 | keyframes := gradient.positionSearch(position) 59 | 60 | if len(keyframes) == 1 { 61 | generated[i] = keyframes[0].color.Clamped() 62 | } else { 63 | relativePosition := (position - keyframes[0].position) / (keyframes[1].position - keyframes[0].position) 64 | generated[i] = keyframes[0].color.BlendHcl(keyframes[1].color, relativePosition).Clamped() 65 | } 66 | } 67 | 68 | return generated 69 | } 70 | 71 | func (gradient Gradient) positionSearch(position float64) []GradientKeyFrame { 72 | length := len(gradient.colors) - 1 73 | base := 1.0 / float64(length) 74 | lowerIndex := int(math.Floor(position / base)) 75 | 76 | sliced := gradient.colors[lowerIndex:] 77 | 78 | if len(sliced) >= 2 { 79 | sliced = sliced[:2] 80 | 81 | return []GradientKeyFrame{ 82 | { 83 | color: sliced[0], 84 | position: gradient.positions[lowerIndex], 85 | index: lowerIndex, 86 | }, 87 | { 88 | color: sliced[1], 89 | position: gradient.positions[lowerIndex+1], 90 | index: lowerIndex + 1, 91 | }, 92 | } 93 | } 94 | 95 | return []GradientKeyFrame{ 96 | { 97 | color: sliced[0], 98 | position: gradient.positions[lowerIndex], 99 | index: lowerIndex, 100 | }, 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/color/quantize.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map; 2 | use std::error; 3 | use std::vec; 4 | 5 | use imagequant; 6 | 7 | crate::error_utils::define_error!( 8 | QuantizeError, { 9 | InvalidType: "The given quantizer is not a valid one", 10 | } 11 | ); 12 | 13 | #[derive(Debug)] 14 | pub enum QuantizerType { 15 | IDENTITY, 16 | IMAGEQUANT, 17 | SCALAR, 18 | } 19 | 20 | pub struct Quantizer { 21 | pub max_color_count: usize, 22 | pub quantizer_type: QuantizerType, 23 | } 24 | 25 | impl Quantizer { 26 | pub fn new(max_color_count: usize, quantizer_type: QuantizerType) -> Self { 27 | return Quantizer { 28 | max_color_count, 29 | quantizer_type, 30 | }; 31 | } 32 | 33 | pub fn run( 34 | &self, 35 | img: vec::Vec<(u8, u8, u8, u8)>, 36 | dimensions: (usize, usize), 37 | ) -> Result<(vec::Vec<(u8, u8, u8, u8)>, vec::Vec), Box> { 38 | return match self.quantizer_type { 39 | QuantizerType::IDENTITY => quantize_identity(img, dimensions), 40 | QuantizerType::IMAGEQUANT => quantize_image_quant(img, dimensions), 41 | _ => Err(Box::new(QuantizeError::InvalidType( 42 | None, 43 | format!("Invalid quantizer {:?}", self.quantizer_type), 44 | ))), 45 | }; 46 | } 47 | } 48 | 49 | /* helper method to just transform input to the appropriate output. 50 | * Generates a proper palette from any given input. size is not taken into consideration and it's 51 | * the caller's responsibility to adjust as needed. 52 | */ 53 | pub fn quantize_identity( 54 | img: vec::Vec<(u8, u8, u8, u8)>, 55 | dimensions: (usize, usize), 56 | ) -> Result<(vec::Vec<(u8, u8, u8, u8)>, vec::Vec), Box> { 57 | let mut color_map: hash_map::HashMap<(u8, u8, u8, u8), usize> = hash_map::HashMap::new(); 58 | let mut palette_list = vec::Vec::new(); 59 | let mut indexed_pixels = vec::Vec::new(); 60 | indexed_pixels.resize(dimensions.0 * dimensions.1, 0); 61 | for (i, color) in img.into_iter().enumerate() { 62 | if !color_map.contains_key(&color) { 63 | palette_list.push(color.clone()); 64 | color_map.insert(color, palette_list.len() - 1); 65 | } 66 | 67 | indexed_pixels[i] = *color_map.get(&color).unwrap(); 68 | } 69 | 70 | return Ok((palette_list, indexed_pixels)); 71 | } 72 | 73 | // pub fn quantize_scalar( 74 | // img: vec::Vec<(u8, u8, u8, u8)> 75 | // ) 76 | 77 | pub fn quantize_image_quant( 78 | img: vec::Vec<(u8, u8, u8, u8)>, 79 | dimensions: (usize, usize), 80 | ) -> Result<(vec::Vec<(u8, u8, u8, u8)>, vec::Vec), Box> { 81 | let mut liq = imagequant::new(); 82 | liq.set_speed(5)?; 83 | liq.set_quality(0, 100)?; 84 | 85 | let ref mut img = liq.new_image( 86 | img.into_iter().map(|e| e.into()).collect::>(), 87 | dimensions.0, 88 | dimensions.1, 89 | 0.0, 90 | )?; 91 | 92 | let mut res = liq.quantize(img)?; 93 | res.set_dithering_level(1.0)?; 94 | 95 | let (palette, pixels) = res.remapped(img).unwrap(); 96 | 97 | Ok(( 98 | palette 99 | .into_iter() 100 | .map(|pixel| { 101 | return (pixel.r, pixel.g, pixel.b, pixel.a); 102 | }) 103 | .collect(), 104 | pixels, 105 | )) 106 | } 107 | -------------------------------------------------------------------------------- /src/bin/generate_gradient.rs: -------------------------------------------------------------------------------- 1 | use std::error; 2 | 3 | use clap::{arg, command}; 4 | use image::{self, GenericImage, GenericImageView}; 5 | use palette::{self, FromColor}; 6 | 7 | fn main_impl(matches: clap::ArgMatches) -> Result<(), Box> 8 | where 9 | C: rainbowgif::color::Color, 10 | { 11 | let input_colors = rainbowgif::commandline::get_colors::(&matches)?; 12 | 13 | let steps = matches.get_one::("count").unwrap(); 14 | let colors = rainbowgif::commandline::get_gradient(&matches, input_colors, *steps as usize, 1); 15 | 16 | let original_width = matches.get_one::("width").unwrap().to_owned(); 17 | let increment = original_width / steps; 18 | let width = increment * steps; 19 | 20 | let mut image = 21 | image::ImageBuffer::new(width, matches.get_one::("height").unwrap().to_owned()); 22 | 23 | for (i, color) in colors.into_iter().enumerate() { 24 | let srgb_color = rainbowgif::color::ColorType::from_color(color); 25 | println!( 26 | "({}, {}), ({}, {}) - {:?}", 27 | (i as u32) * increment, 28 | 0, 29 | ((i as u32) + 1) * increment, 30 | image.height(), 31 | srgb_color, 32 | ); 33 | let mut sub_image = image.sub_image((i as u32) * increment, 0, increment, image.height()); 34 | let (width, height) = sub_image.dimensions(); 35 | for x in 0..width { 36 | for y in 0..height { 37 | sub_image.put_pixel( 38 | x, 39 | y, 40 | image::Rgba::from([ 41 | (srgb_color.red * 255.) as u8, 42 | (srgb_color.green * 255.) as u8, 43 | (srgb_color.blue * 255.) as u8, 44 | 255, 45 | ]), 46 | ); 47 | } 48 | } 49 | } 50 | 51 | match image.save(matches.get_one::("output_file").unwrap()) { 52 | Ok(_) => Ok(()), 53 | Err(e) => { 54 | println!("Error encoding image: {}", e); 55 | Err(Box::new(e)) 56 | } 57 | } 58 | } 59 | 60 | fn main() -> Result<(), Box> { 61 | let matches = command!() 62 | .arg(arg!(colors: "Colors to generate gradient for").value_delimiter(',')) 63 | .arg(arg!(output_file: "The path to the output file")) 64 | .arg( 65 | arg!(count: "Number of colors to display") 66 | .value_parser(clap::value_parser!(u32).range(1..)), 67 | ) 68 | .arg( 69 | arg!(width: --width [WIDTH] "Width of the image") 70 | .value_parser(clap::value_parser!(u32).range(1..)) 71 | .default_value("512"), 72 | ) 73 | .arg( 74 | arg!( 75 | height: --height [HEIGHT] "Height of the image" 76 | ) 77 | .value_parser(clap::value_parser!(u32).range(1..)) 78 | .default_value("512"), 79 | ) 80 | .arg( 81 | arg!(color_space: -s --color_space [COLOR_SPACE] "The color space to use") 82 | .value_parser(clap::value_parser!(rainbowgif::color::ColorSpace)) 83 | .default_value("lch"), 84 | ) 85 | .arg( 86 | arg!(generator: -g --generator [GENERATOR] "The type generator to use") 87 | .value_parser(clap::value_parser!( 88 | rainbowgif::color::gradient::GradientGeneratorType 89 | )) 90 | .default_value("discrete"), 91 | ) 92 | .get_matches(); 93 | 94 | let color_space = matches 95 | .get_one::("color_space") 96 | .unwrap(); 97 | 98 | match color_space { 99 | rainbowgif::color::ColorSpace::HSL => main_impl::< 100 | palette::Hsla, 101 | >(matches), 102 | 103 | rainbowgif::color::ColorSpace::HSV => main_impl::< 104 | palette::Hsva, 105 | >(matches), 106 | 107 | rainbowgif::color::ColorSpace::LAB => main_impl::< 108 | palette::Laba, 109 | >(matches), 110 | 111 | rainbowgif::color::ColorSpace::LCH => main_impl::< 112 | palette::Lcha, 113 | >(matches), 114 | 115 | _ => Err(Box::new( 116 | rainbowgif::commandline::CommandlineError::IncompatibleValue( 117 | None, 118 | "Only HSL, HSV, LAB, and LCH are supported".to_owned(), 119 | ), 120 | )), 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/codec/mod.rs: -------------------------------------------------------------------------------- 1 | use std::error; 2 | use std::vec; 3 | 4 | use ::gif as gif_lib; 5 | use palette::FromColor; 6 | 7 | use crate::{color, error_utils}; 8 | 9 | pub mod gif; 10 | pub mod image; 11 | 12 | error_utils::define_error!(DecodeError, { 13 | Init: "Error initializing decoder", 14 | Read: "Error reading data", 15 | FrameRead: "Error reading frame", 16 | InvalidData: "Invalid data", 17 | }); 18 | 19 | error_utils::define_error!(EncodeError, { 20 | Init: "Error initializing encoder", 21 | Write: "Error write data", 22 | FrameWrite: "Error write frame", 23 | }); 24 | 25 | // TODO make private after iterable 26 | #[derive(Clone)] 27 | pub struct Palette { 28 | pub colors: vec::Vec, 29 | } 30 | 31 | // TODO implement IntoIterator and Iterator trait 32 | impl Palette 33 | where 34 | C: color::Color, 35 | palette::rgb::Rgb: 36 | palette::convert::FromColorUnclamped<>::Color>, 37 | { 38 | pub fn new(colors: vec::Vec) -> Self { 39 | assert!( 40 | colors.len() <= 256, 41 | "Expected <= 255 but got {}", 42 | colors.len() 43 | ); 44 | 45 | return Palette { colors }; 46 | } 47 | 48 | pub fn from_gif_format(colors: &[u8]) -> Self { 49 | return Palette { 50 | colors: colors[..] 51 | .chunks(3) 52 | .map(|chunk| { 53 | let [r, g, b]: [u8; 3] = chunk.try_into().unwrap(); 54 | return C::from_color(color::ColorType::new( 55 | r as color::ScalarType / 255., 56 | g as color::ScalarType / 255., 57 | b as color::ScalarType / 255., 58 | 1., 59 | )); 60 | }) 61 | .collect(), 62 | }; 63 | } 64 | 65 | #[allow(dead_code)] 66 | pub fn into_gif_format(self) -> vec::Vec { 67 | return self 68 | .colors 69 | .into_iter() 70 | .flat_map(|c| { 71 | let c_prime = color::ColorType::from_color(c); 72 | return [ 73 | (c_prime.red * 255.) as u8, 74 | (c_prime.green * 255.) as u8, 75 | (c_prime.blue * 255.) as u8, 76 | ]; 77 | }) 78 | .collect(); 79 | } 80 | } 81 | 82 | #[derive(Clone)] 83 | pub struct Frame 84 | where 85 | C: color::Color, 86 | { 87 | pub delay: u16, 88 | pub dispose: gif_lib::DisposalMethod, 89 | 90 | // sometimes the frame doesn't fill the whole screen 91 | pub origin: (u16, u16), 92 | pub dimensions: (u16, u16), 93 | 94 | // local palette, limited to 255 colors 95 | // while GIFs technically can have a global palette, this is easier 96 | // if it has no local palette, it'll take the global palette 97 | // TODO look into maybe only using global palette 98 | pub palette: Palette, 99 | 100 | // pixels indexing into palette (local if present, otherwise global) 101 | pub pixels_indexed: vec::Vec, 102 | 103 | // transparent pixel index, if available 104 | pub transparent_index: Option, 105 | 106 | // these are rarely used 107 | pub interlaced: bool, 108 | pub needs_input: bool, 109 | } 110 | 111 | // TODO: figure this out 112 | // impl Frame 113 | // where 114 | // C: palette::FromColor + palette::IntoColor, 115 | // T: palette::FromColor + palette::IntoColor, 116 | // { 117 | // pub fn to_colorspace(self, color_space: color::ColorSpace) -> Frame { 118 | // return Frame { 119 | // pixels: self.pixels.into_iter().map(|pixel| { return T::from_color(pixel);}).collect(), 120 | // delay: self.delay, 121 | // dispose: self.dispose, 122 | // interlaced: self.interlaced, 123 | // }; 124 | // } 125 | // } 126 | 127 | pub trait Decodable 128 | where 129 | ::OutputColor: color::Color, 130 | { 131 | type OutputColor; 132 | 133 | fn decode(&mut self) -> Result>, Box>; 134 | 135 | fn decode_all( 136 | &mut self, 137 | ) -> Result>>, Box>; 138 | 139 | fn get_dimensions(&self) -> (u16, u16); 140 | } 141 | 142 | // can't use FromIterator as a super trait, as it requires more than just an iterator to encode all 143 | // the data 144 | pub trait Encodable 145 | where 146 | ::InputColor: color::Color, 147 | { 148 | type InputColor; 149 | 150 | fn encode(&self, frame: Frame) -> Result<(), Box>; 151 | 152 | fn encode_all( 153 | &self, 154 | frames: vec::Vec>, 155 | ) -> Result<(), Box>; 156 | } 157 | -------------------------------------------------------------------------------- /src/color/gradient.rs: -------------------------------------------------------------------------------- 1 | use std::vec; 2 | 3 | use clap::{builder::PossibleValue, ValueEnum}; 4 | use palette::gradient; 5 | use palette::Mix; 6 | 7 | use super::{Color, ScalarType}; 8 | use crate::commandline; 9 | 10 | commandline::define_cli_enum!(GradientGeneratorType, { 11 | Discrete: ("discrete", "Where the colors are calculated by global and local position"), 12 | Continuous: ("continuous", "where palette generates it, taking into account all colors"), 13 | }); 14 | 15 | struct GradientKeyFrame<'a, C> 16 | where 17 | C: Mix + Sized, 18 | { 19 | color: &'a C, 20 | position: ScalarType, 21 | } 22 | 23 | pub struct GradientDescriptor { 24 | pub colors: vec::Vec, 25 | pub positions: vec::Vec, 26 | } 27 | 28 | impl GradientDescriptor 29 | where 30 | C: Mix + Color, 31 | palette::rgb::Rgb: 32 | palette::convert::FromColorUnclamped<>::Color>, 33 | { 34 | pub fn new(mut colors: vec::Vec) -> GradientDescriptor { 35 | colors.push(colors[0].clone()); 36 | let rng = 0..colors.len(); 37 | let length = std::cmp::max(colors.len() - 1, 1); 38 | return GradientDescriptor { 39 | colors, 40 | positions: rng 41 | .map(|i| (i as ScalarType) / (length as ScalarType)) 42 | .collect(), 43 | }; 44 | } 45 | 46 | pub fn generate( 47 | &self, 48 | frame_count: usize, 49 | generator_type: GradientGeneratorType, 50 | ) -> vec::Vec { 51 | return match generator_type { 52 | GradientGeneratorType::Continuous => self.generate_continuous(frame_count), 53 | GradientGeneratorType::Discrete => self.generate_discrete(frame_count), 54 | }; 55 | } 56 | 57 | fn generate_continuous(&self, frame_count: usize) -> vec::Vec { 58 | let grad = gradient::Gradient::new(self.colors.clone()); 59 | return grad.take(frame_count + 1).take(frame_count).collect(); 60 | } 61 | 62 | fn generate_discrete(&self, frame_count: usize) -> vec::Vec { 63 | let mut gen = vec::Vec::::new(); 64 | 65 | for i in 0..frame_count { 66 | let global_position = (i as ScalarType) / (frame_count as ScalarType); 67 | 68 | let (key_frame_src, key_frame_dest) = self.position_search(global_position); 69 | let local_position = (global_position - key_frame_src.position) 70 | / (key_frame_dest.position - key_frame_src.position); 71 | 72 | let src = key_frame_src.color; 73 | let dest = key_frame_dest.color; 74 | 75 | gen.push(src.mix(&dest, local_position)); 76 | } 77 | 78 | return gen; 79 | } 80 | 81 | fn position_search<'a>( 82 | &'a self, 83 | position: ScalarType, 84 | ) -> (GradientKeyFrame<'a, C>, GradientKeyFrame<'a, C>) { 85 | let length = self.colors.len() - 1; 86 | let base = 1.0 / (length as ScalarType); 87 | let lower_index = (position / base).floor() as usize; 88 | 89 | if lower_index == length { 90 | return ( 91 | GradientKeyFrame { 92 | color: &self.colors[lower_index], 93 | position: self.positions[lower_index], 94 | }, 95 | GradientKeyFrame { 96 | color: &self.colors[0], 97 | position: self.positions[0], 98 | }, 99 | ); 100 | } 101 | 102 | return ( 103 | GradientKeyFrame { 104 | color: &self.colors[lower_index], 105 | position: self.positions[lower_index], 106 | }, 107 | GradientKeyFrame { 108 | color: &self.colors[lower_index + 1], 109 | position: self.positions[lower_index + 1], 110 | }, 111 | ); 112 | } 113 | } 114 | 115 | #[cfg(test)] 116 | mod tests { 117 | use palette::{FromColor, Lcha}; 118 | 119 | use crate::color; 120 | 121 | #[test] 122 | fn test_generate_discrete() { 123 | let grad_desc = color::gradient::GradientDescriptor::new(vec![ 124 | Lcha::from_color(color::ColorType::new(0., 0., 0., 1.)), 125 | Lcha::from_color(color::ColorType::new(0.5, 0.5, 0.5, 1.)), 126 | Lcha::from_color(color::ColorType::new(1., 1., 1., 1.)), 127 | ]); 128 | 129 | let colors = grad_desc.generate(12, color::gradient::GradientGeneratorType::Discrete); 130 | assert_eq!(colors.len(), 12); 131 | 132 | assert_eq!(colors[0].chroma, 0.0); 133 | assert_eq!(colors[4].chroma, 0.0); 134 | assert_eq!(colors[8].chroma, 0.0); 135 | } 136 | 137 | #[test] 138 | fn test_generate_continuous() { 139 | let grad_desc = color::gradient::GradientDescriptor::new(vec![ 140 | Lcha::from_color(color::ColorType::new(0., 0., 0., 1.)), 141 | Lcha::from_color(color::ColorType::new(0.5, 0.5, 0.5, 1.)), 142 | Lcha::from_color(color::ColorType::new(1., 1., 1., 1.)), 143 | ]); 144 | 145 | let colors = grad_desc.generate(12, color::gradient::GradientGeneratorType::Continuous); 146 | assert_eq!(colors.len(), 12); 147 | 148 | assert_eq!(colors[0].chroma, 0.0); 149 | assert_eq!(colors[4].chroma, 0.0); 150 | assert_eq!(colors[8].chroma, 0.0); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/color/mod.rs: -------------------------------------------------------------------------------- 1 | use clap::{builder::PossibleValue, ValueEnum}; 2 | use palette::{Clamp, FromColor, Hsla, Hsva, LabHue, Lcha, RgbHue}; 3 | 4 | use crate::commandline; 5 | 6 | pub mod gradient; 7 | pub mod quantize; 8 | 9 | pub type ScalarType = f32; 10 | // TODO put behind a feature 11 | // pub type ScalarType = f64; 12 | 13 | // TODO put behind a feature 14 | // pub type ColorType = palette::rgb::GammaSrgba; 15 | // pub type ColorType = palette::rgb::LinSrgba; 16 | pub type ColorType = palette::rgb::Srgba; 17 | 18 | // pub type EncodingType = palette::encoding::Gamma; 19 | // pub type EncodingType = palette::encoding::linear::Linear; 20 | pub type EncodingType = palette::encoding::Srgb; 21 | 22 | pub type WhitePoint = palette::white_point::D65; 23 | 24 | pub trait Color: 25 | palette::FromColor 26 | + palette::convert::FromColorUnclamped 27 | + palette::IntoColor 28 | + palette::convert::IntoColorUnclamped 29 | + palette::WithAlpha 30 | + palette::Mix 31 | + Clone 32 | + Sized 33 | { 34 | } 35 | 36 | impl Color for T where 37 | T: palette::FromColor 38 | + palette::convert::FromColorUnclamped 39 | + palette::IntoColor 40 | + palette::convert::IntoColorUnclamped 41 | + palette::WithAlpha 42 | + palette::Mix 43 | + Clone 44 | + Sized 45 | { 46 | } 47 | 48 | commandline::define_cli_enum!(MixingMode, { 49 | None: ("none", "Doesn't actually mix and returns the original colors"), 50 | Custom: ("custom", "Mixes the color by taking the hue component of the other color, keeping the base luma and chroma"), 51 | Lab: ("lab", "Mixes the color by taking the color components of the other color, keeping the base lightness"), 52 | Linear: ("linear", "Uses palettee for linear mixing"), 53 | BlendOverlay: ("blend_overlay", "Uses blending: overlay"), 54 | }); 55 | 56 | commandline::define_cli_enum!(ColorSpace, { 57 | HSL: ( 58 | "hsl", 59 | "The HSL color space can be seen as a cylindrical version of RGB, where the hue is the angle around the color cylinder, the saturation is the distance from the center, and the lightness is the height from the bottom." 60 | ), 61 | HSV: ( 62 | "hsv", 63 | "HSV is a cylindrical version of RGB and it’s very similar to HSL. The difference is that the value component in HSV determines the brightness of the color, and not the lightness." 64 | ), 65 | LCH: ( 66 | "lch", 67 | "L*C*h° shares its range and perceptual uniformity with L*a*b*, but it’s a cylindrical color space, like HSL and HSV. This gives it the same ability to directly change the hue and colorfulness of a color, while preserving other visual aspects." 68 | ), 69 | RGB: ( 70 | "rgb", 71 | "RGB" 72 | ), 73 | LAB: ( 74 | "lab", 75 | "The CIE L*a*b* (CIELAB) color space" 76 | ) 77 | }); 78 | 79 | pub fn from_hex(color_string: &str) -> Result 80 | where 81 | C: FromColor, 82 | { 83 | let r = u8::from_str_radix(&color_string[0..2], 16)? as ScalarType; 84 | let g = u8::from_str_radix(&color_string[2..4], 16)? as ScalarType; 85 | let b = u8::from_str_radix(&color_string[4..6], 16)? as ScalarType; 86 | 87 | // expects values in (0, 1) 88 | let temp_color = ColorType::new(r / 255.0, g / 255.0, b / 255.0, 1.0); 89 | return Ok(C::from_color(temp_color)); 90 | } 91 | 92 | pub trait Componentize { 93 | fn get_components(&self) -> (H, C, L, A); 94 | 95 | fn from_components(h: H, c: C, l: L, a: A) -> Self; 96 | } 97 | 98 | impl Componentize, ScalarType, ScalarType, ScalarType> 99 | for Lcha 100 | { 101 | fn get_components(&self) -> (LabHue, ScalarType, ScalarType, ScalarType) { 102 | let (l, c, h, a) = self.into_components(); 103 | return (h, c, l, a); 104 | } 105 | 106 | fn from_components(h: LabHue, c: ScalarType, l: ScalarType, a: ScalarType) -> Self { 107 | return Lcha::from_components((l, c, h, a)).clamp(); 108 | } 109 | } 110 | 111 | impl Componentize, ScalarType, ScalarType, ScalarType> 112 | for Hsla 113 | { 114 | fn get_components(&self) -> (RgbHue, ScalarType, ScalarType, ScalarType) { 115 | return self.into_components(); 116 | } 117 | 118 | fn from_components(h: RgbHue, c: ScalarType, l: ScalarType, a: ScalarType) -> Self { 119 | return Hsla::from_components((h, c, l, a)).clamp(); 120 | } 121 | } 122 | 123 | impl Componentize, ScalarType, ScalarType, ScalarType> 124 | for Hsva 125 | { 126 | fn get_components(&self) -> (RgbHue, ScalarType, ScalarType, ScalarType) { 127 | return self.into_components(); 128 | } 129 | 130 | fn from_components(h: RgbHue, c: ScalarType, l: ScalarType, a: ScalarType) -> Self { 131 | return Hsva::from_components((h, c, l, a)).clamp(); 132 | } 133 | } 134 | 135 | pub fn blend_colors>( 136 | bottom: &Color, 137 | top: &Color, 138 | include_chroma: bool, 139 | ) -> Color { 140 | if include_chroma { 141 | let (top_h, top_c, _, _) = top.get_components(); 142 | let (_, _, bottom_l, bottom_a) = bottom.get_components(); 143 | 144 | return Color::from_components(top_h, top_c, bottom_l, bottom_a); 145 | } else { 146 | let (top_h, _, _, _) = top.get_components(); 147 | let (_, bottom_c, bottom_l, bottom_a) = bottom.get_components(); 148 | 149 | return Color::from_components(top_h, bottom_c, bottom_l, bottom_a); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/codec/image.rs: -------------------------------------------------------------------------------- 1 | use std::collections; 2 | use std::error; 3 | use std::io; 4 | use std::marker; 5 | use std::vec; 6 | 7 | use ::gif as gif_lib; 8 | use image::io::Reader; 9 | use image::{DynamicImage, ImageFormat}; 10 | use palette; 11 | 12 | use super::{Decodable, DecodeError, Frame}; 13 | use crate::codec; 14 | use crate::color; 15 | 16 | pub struct ImageDecoder { 17 | phantom: marker::PhantomData, 18 | image: DynamicImage, 19 | decoded: bool, 20 | } 21 | 22 | impl ImageDecoder 23 | where 24 | C: color::Color, 25 | { 26 | fn new_impl( 27 | dec_impl: Reader, 28 | ) -> Result> { 29 | if dec_impl.format().is_none() { 30 | return Err(Box::new(DecodeError::Read( 31 | None, 32 | "Passed in Read doesn't yield valid format data".to_owned(), 33 | ))); 34 | } 35 | 36 | let decoded = match dec_impl.decode() { 37 | Ok(frame) => frame, 38 | Err(e) => { 39 | return Err(Box::new(DecodeError::Read( 40 | Some(Box::new(e)), 41 | "Unable to decode properly".to_owned(), 42 | ))) 43 | } 44 | }; 45 | 46 | return Ok(ImageDecoder { 47 | phantom: marker::PhantomData, 48 | image: decoded, 49 | decoded: false, 50 | }); 51 | } 52 | 53 | pub fn new( 54 | read: R, 55 | format: Option, 56 | ) -> Result> { 57 | let dec_impl = { 58 | if let Some(image_format) = format { 59 | Reader::with_format(read, image_format) 60 | } else { 61 | Reader::new(read).with_guessed_format()? 62 | } 63 | }; 64 | return Self::new_impl(dec_impl); 65 | } 66 | } 67 | 68 | impl Decodable for ImageDecoder 69 | where 70 | C: color::Color, 71 | palette::rgb::Rgb: 72 | palette::convert::FromColorUnclamped<>::Color>, 73 | { 74 | type OutputColor = C; 75 | 76 | fn decode(&mut self) -> Result>, Box> { 77 | if self.decoded { 78 | return Ok(None); 79 | } 80 | 81 | let mut buf = self.image.to_rgba8(); 82 | let mut transparent_indices = collections::hash_set::HashSet::new(); 83 | 84 | // in a gif if a value isn't fully opaque, it's considered transparent 85 | for (i, mut pixel) in buf.pixels_mut().enumerate() { 86 | if pixel.0[3] != 255 { 87 | transparent_indices.insert(i); 88 | pixel.0[0] = 0; 89 | pixel.0[1] = 0; 90 | pixel.0[2] = 0; 91 | pixel.0[3] = 0; 92 | } 93 | } 94 | 95 | let quantizer = 96 | color::quantize::Quantizer::new(256, color::quantize::QuantizerType::IMAGEQUANT); 97 | let (pixels, indices) = quantizer.run( 98 | buf.into_vec()[..] 99 | .chunks(4) 100 | .map(|chunk| { 101 | let [r, g, b, a]: [u8; 4] = chunk.try_into().unwrap(); 102 | return (r, g, b, a); 103 | }) 104 | .collect(), 105 | (self.image.width() as usize, self.image.height() as usize), 106 | )?; 107 | 108 | let transparent_index = pixels.iter().position(|&x| x.3 == 0).map(|i| i as u8); 109 | 110 | let quantized_pixels = pixels 111 | .into_iter() 112 | .map(|pixel| { 113 | return C::from_color(color::ColorType::new( 114 | (pixel.0 as color::ScalarType) / 255., 115 | (pixel.1 as color::ScalarType) / 255., 116 | (pixel.2 as color::ScalarType) / 255., 117 | (pixel.3 as color::ScalarType) / 255., 118 | )); 119 | }) 120 | .collect::>(); 121 | 122 | let pal = codec::Palette::new(quantized_pixels); 123 | 124 | self.decoded = true; 125 | 126 | return Ok(Some(Frame { 127 | delay: 0, // TODO #35 128 | dispose: gif_lib::DisposalMethod::Keep, 129 | origin: (0, 0), 130 | dimensions: self.get_dimensions(), 131 | palette: pal, 132 | pixels_indexed: indices, 133 | transparent_index, 134 | interlaced: false, 135 | needs_input: false, 136 | })); 137 | } 138 | 139 | fn decode_all( 140 | &mut self, 141 | ) -> Result>>, Box> { 142 | if self.decoded { 143 | return Ok(None); 144 | } 145 | 146 | if let Some(frame) = self.decode()? { 147 | return Ok(Some(vec![frame])); 148 | } 149 | 150 | return Ok(None); 151 | } 152 | 153 | fn get_dimensions(&self) -> (u16, u16) { 154 | return (self.image.width() as u16, self.image.height() as u16); 155 | } 156 | } 157 | 158 | impl IntoIterator for ImageDecoder 159 | where 160 | C: color::Color, 161 | palette::rgb::Rgb: 162 | palette::convert::FromColorUnclamped<>::Color>, 163 | { 164 | type Item = Frame; 165 | type IntoIter = ImageDecoderIter; 166 | 167 | fn into_iter(self) -> Self::IntoIter { 168 | return ImageDecoderIter { decoder: self }; 169 | } 170 | } 171 | 172 | pub struct ImageDecoderIter { 173 | decoder: ImageDecoder, 174 | } 175 | 176 | impl Iterator for ImageDecoderIter 177 | where 178 | C: color::Color, 179 | palette::rgb::Rgb: 180 | palette::convert::FromColorUnclamped<>::Color>, 181 | { 182 | type Item = Frame; 183 | 184 | fn next(&mut self) -> Option { 185 | if let Ok(res) = self.decoder.decode() { 186 | return res; 187 | } 188 | 189 | return None; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "image" 8 | "image/color" 9 | "image/gif" 10 | "image/jpeg" 11 | "image/png" 12 | "os" 13 | "runtime" 14 | "strings" 15 | 16 | "github.com/lucasb-eyer/go-colorful" 17 | ) 18 | 19 | func prepareFrame(src *image.Paletted, dst *image.Paletted, overlayColor colorful.Color) { 20 | dst.Pix = src.Pix 21 | dst.Stride = src.Stride 22 | 23 | for pixelIndex, pixel := range src.Palette { 24 | _, _, _, alpha := pixel.RGBA() 25 | convertedPixel, ok := colorful.MakeColor(pixel) 26 | 27 | if alpha == 0 || !ok { 28 | dst.Palette[pixelIndex] = pixel 29 | continue 30 | } 31 | 32 | convertedPixel = convertedPixel.Clamped() 33 | 34 | blendedPixel := blendColor(overlayColor, convertedPixel) 35 | 36 | blendedR, blendedG, blendedB := blendedPixel.RGB255() 37 | dst.Palette[pixelIndex] = color.NRGBA{ 38 | blendedR, 39 | blendedG, 40 | blendedB, 41 | 255, 42 | } 43 | } 44 | } 45 | 46 | func parseGradientColors(gradientColors string) ([]colorful.Color, error) { 47 | var colors []colorful.Color 48 | 49 | if len(gradientColors) != 0 { 50 | colorHexes := strings.Split(gradientColors, ",") 51 | colors = make([]colorful.Color, len(colorHexes)) 52 | for i, hex := range colorHexes { 53 | color, err := colorful.Hex("#" + hex) 54 | if err != nil { 55 | return nil, errors.New(fmt.Sprintf("Invalid color: %s", hex)) 56 | } 57 | colors[i] = color 58 | } 59 | } else { 60 | // ROYGBV 61 | colors = []colorful.Color{ 62 | {1, 0, 0}, 63 | {1, 127.0 / 255.0, 0}, 64 | {1, 1, 0}, 65 | {0, 1, 0}, 66 | {0, 0, 1}, 67 | {139.0 / 255.0, 0, 1}, 68 | } 69 | } 70 | 71 | return colors, nil 72 | } 73 | 74 | func main() { 75 | // register image formats 76 | image.RegisterFormat("jpg", "\xFF\xD8", jpeg.Decode, jpeg.DecodeConfig) 77 | image.RegisterFormat("png", "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A", png.Decode, png.DecodeConfig) 78 | 79 | var threads uint 80 | flag.UintVar(&threads, "threads", uint(runtime.NumCPU())/2, "The number of go threads to use") 81 | 82 | var gradientColors string 83 | flag.StringVar(&gradientColors, "gradient", "", "A list of colors in hex without # separated by comma to use as the gradient") 84 | 85 | var loopCount uint 86 | flag.UintVar(&loopCount, "loop_count", 1, "The number of times ot loop through thr GIF or the number of frames to show") 87 | 88 | var static bool 89 | flag.BoolVar(&static, "static", false, "Whether it's a static image (JPG/PNG) or not") 90 | 91 | var delay uint 92 | flag.UintVar(&delay, "delay", 0, "The delay between frames") 93 | 94 | var quantizer string 95 | flag.StringVar(&quantizer, "quantizer", "populosity", "quantizer algorithm to use") 96 | 97 | flag.Parse() 98 | 99 | if threads < 1 { 100 | fmt.Println("Thread count must be at least 1") 101 | os.Exit(1) 102 | } 103 | 104 | colors, err := parseGradientColors(gradientColors) 105 | if err != nil { 106 | fmt.Println(err.Error()) 107 | os.Exit(1) 108 | } 109 | 110 | if loopCount < 1 { 111 | fmt.Println("Loop count must be at least 1") 112 | os.Exit(1) 113 | } 114 | 115 | positionalArgs := flag.Args() 116 | 117 | if len(positionalArgs) != 2 { 118 | fmt.Println("Expected two positional arguments: input and output") 119 | os.Exit(1) 120 | } 121 | 122 | input := positionalArgs[0] 123 | output := positionalArgs[1] 124 | 125 | file, err := os.Open(input) 126 | if err != nil { 127 | fmt.Println("Error opening file: ", err) 128 | os.Exit(1) 129 | } 130 | 131 | var img *gif.GIF 132 | 133 | if static { 134 | staticImg, format, err := image.Decode(file) 135 | if err != nil { 136 | fmt.Println("Error decoding static image: ", err) 137 | os.Exit(1) 138 | } 139 | img, err = staticTransform(staticImg, format, quantizer, delay) 140 | } else { 141 | img, err = gif.DecodeAll(file) 142 | } 143 | 144 | if err != nil { 145 | fmt.Println("Error decoding: ", err) 146 | os.Exit(1) 147 | } 148 | file.Close() 149 | 150 | frameCount := uint(len(img.Image)) * loopCount 151 | newFrames := make([]*image.Paletted, frameCount) 152 | for i := range newFrames { 153 | originalFrame := img.Image[i%len(img.Image)] 154 | newPalette := make([]color.Color, len(originalFrame.Palette)) 155 | copy(newPalette, originalFrame.Palette) 156 | newFrames[i] = image.NewPaletted(originalFrame.Bounds(), newPalette) 157 | } 158 | 159 | gradient := newGradient(colors, true) 160 | overlayColors := gradient.generate(frameCount) 161 | 162 | framesPerThread := frameCount/threads + 1 163 | ch := make(chan uint) 164 | barrier := uint(0) 165 | 166 | for i := 0; i < int(threads); i++ { 167 | go func(base int, normalizedFrameIndex int) { 168 | for processed := 0; processed < int(framesPerThread); processed++ { 169 | frameIndex := base*int(framesPerThread) + processed 170 | 171 | if frameIndex >= int(frameCount) { 172 | break 173 | } 174 | 175 | if normalizedFrameIndex >= len(img.Image) { 176 | normalizedFrameIndex = 0 177 | } 178 | 179 | // do actual work in here 180 | prepareFrame( 181 | img.Image[normalizedFrameIndex], 182 | newFrames[frameIndex], 183 | overlayColors[frameIndex], 184 | ) 185 | normalizedFrameIndex++ 186 | } 187 | 188 | // thread is done 189 | ch <- 1 190 | }(i, i*int(framesPerThread)%len(img.Image)) 191 | } 192 | 193 | // wait for all threads to synchronize 194 | for barrier != threads { 195 | barrier += <-ch 196 | } 197 | 198 | newDelay := make([]int, len(newFrames)) 199 | // overwrite the delay if one is provided, otherwise use default 200 | for i := range newDelay { 201 | if delay == 0 { 202 | newDelay[i] = img.Delay[i%len(img.Delay)] 203 | } else { 204 | newDelay[i] = int(delay) 205 | } 206 | } 207 | 208 | newDisposal := make([]byte, len(newFrames)) 209 | if len(img.Disposal) > 0 { 210 | for i := range newDelay { 211 | newDisposal[i] = img.Disposal[i%len(img.Disposal)] 212 | } 213 | } 214 | 215 | img.Image = newFrames 216 | img.Delay = newDelay 217 | img.Disposal = newDisposal 218 | 219 | file, err = os.OpenFile(output, os.O_RDWR|os.O_CREATE, 0644) 220 | if err != nil { 221 | fmt.Println("Error opening file: ", err) 222 | os.Exit(1) 223 | } 224 | 225 | img.Config.ColorModel = nil 226 | img.BackgroundIndex = 0 227 | 228 | err = gif.EncodeAll(file, img) 229 | if err != nil { 230 | fmt.Println("Error encoding image: ", err) 231 | os.Exit(1) 232 | } 233 | file.Close() 234 | } 235 | -------------------------------------------------------------------------------- /go/README.md: -------------------------------------------------------------------------------- 1 | # Rainbow GIF 2 | ## What? 3 | This is a program to read in images and overlay colors over the frames to create a rainbow effect. The tool currently supports GIFs as well as JPGs and PNGs. 4 | 5 | | Before | After | 6 | | ------ | ------ | 7 | | ![Before](../images/fidget_spinner.gif) | ![After](../images/fidget_spinner_rainbow.gif) | 8 | | ![Before](../images/chefs_kiss.png) | ![After](../images/chefs_kiss.gif) | 9 | 10 | - first one was created with `rainbowgif images/fidget_spinner.gif images/fidget_spinner_rainbow.gif`. 11 | - second one was created with `rainbowgif --static --threads=1 --loop_count=18 --quantizer=populosity images/chefs_kiss.png images/chefs_kiss.gif` 12 | 13 | ## Usage 14 | Clone it and assuming you have Go a version greater than or equal to 1.3, you should just be able to do a `go mod download` to download all the modules and then `go build`. This should output a binary in the directory. Run it with by doing `./rainbowgif `. 15 | 16 | ### Options 17 | - `threads`: The number of goroutines to use when processing the GIF 18 | - `gradient`: The list of colors to use as the overlay. When omitted, it will default to ROYGBV. 19 | - `loop_count`: Defaults to 1. 20 | - For GIF: The number of times to loop over the GIF. The output GIF will be `loop_count` times longer. 21 | - For static images (JPG, PNG): The number of frames to create for the resulting GIF. The output will be `loop_count` frames long. 22 | - `static`: Indicate whether the input is a static image. Defaults to false. Hopefully this can be removed in the future. 23 | - `quantizer`: Only used with `static` on. This will choose which quantizer to use. 24 | - `delay`: This sets the delay between frames in 100ths of a second 25 | 26 | ## Technical Detail 27 | This makes use of https://github.com/lucasb-eyer/go-colorful - this library saved me a lot of travel since the standard color library doesn't cover all this. 28 | 29 | I'll be explaining how the colors are generated and used below. 30 | 31 | Before we begin, I'll just explain some variables even though I'll be describing and naming them as I go. 32 | 33 | | Prefix | Meaning | 34 | | ------ | ------- | 35 | | `n_` | Number of or count | 36 | | `i_` | Index | 37 | | `p_` | Position | 38 | | `dp_` | Delta position | 39 | 40 | | Base | Meaning | 41 | | ------ | ------- | 42 | | `f` | Frame of GIF | 43 | | `ci` | Input color | 44 | | `cg` | Gradient generated color | 45 | 46 | ### Gradient 47 | The gradient can be initialized with varying number of colors (`n_ci`), but for the purposes of this script I chose seven: ROYGBV and a seventh color close to red. Without this last color, the looping won't be as smooth as there will be a suddent transition from the last color to the first color. 48 | 49 | This is what the input colors would look like: 50 | ``` 51 | | R | O | Y | G | B | V | R' | 52 | ``` 53 | 54 | However regardless of `n_ci`, there can be any number of generated colors (`n_cg`). `n_cg` will depend only on how many frames the input GIF has (`n_f`). Given a 10 frame GIF, the program will generate 10 different colors to apply to each frame - in short, `n_cg == n_f`. All output colors will be either an input color or two input colors blended in HCL mode. 55 | 56 | Colors are calculated using `n_f`, the index of the frame (`i_f`), positions of the frame (`p_f`), and position of the generated color (`p_cg`). Position is represented as a number between 0 and 1. It's meant to show where a color is in the gradient. 57 | 58 | Given a 10 frame GIF, the position would look like the following (top is index and bottom is position): 59 | 60 | | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 61 | | - | - | - | - | - | - | - | - | - | - | 62 | | 0 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 1 | 63 | 64 | Given the previous input colors, the positions would look like the following (top is index and bottom is position): 65 | 66 | | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 67 | | - | - | - | - | - | - | - | 68 | | 0 | 0.1667 | 0.3333 | 0.5 | 0.6667 | 0.8333 | 1 | 69 | 70 | Positions for frames are found by doing `i_f / (n_f - 1)` - the subtraction by one is important due to zero indexing. It's clear here that the delta between the positions (`dp_f`) is `1 / (n_f - 1)`. Similarly, positions for the input colors are found by doing `i_ci / (n_ci - 1)` and the delta (`dp_ci`) is `1 / (n_ci - 1)`. In some cases, the frame's position will line up with the color's position. However in the majority of the cases, it won't. For those cases, the relative position can be found between the colors. 71 | 72 | Before moving on, we can intuite this. Using the previous examples, we know that frame 0 has `p_f = 0` and that correlates directly to `p_cg = 0` so frame 0 will be using colors 0. Frame 1's position is 0.1 and that's nowhere to be found on the gradient color position list. However we can see that it's between 0 and 0.1667, so we can say that it's between colors 0 and 1. 73 | 74 | The actual calculation is basically just that - first `p_f` is divided by `dp_ci`. This number is then floored and this can be considered the lower index. It is then incremented and considered the higher index. There we have our two colors using the same idea as described above. Of course, this won't work if the lower index happens to be last index. In that case, the higher index will be equal to the lower index. 75 | 76 | Using frame 1 again as an example we know that its position is 0.1 and `dp_ci` is `0.1667`. Doing `floor(0.1 / 0.1667)` yields 0, so we know that colors 0 and 1 are the two colors to blend. 77 | 78 | Now that we know which two colors to blend - we just blend them right? Not quite - since we care about just how close to each color the position really is. Let's call the two colors c1 and c2. To use c1's position as the base and c2's position as the end, we simply have to subtract both by c1's position. This will leave c1's position as 0 and c2's position as some number (it doesn't matter which). We also subtract the input position by c1's original position to get the relative position. This is then divided by c2's new position, which yields a number between zero and one. This number is then used to get the color at that position using the colorful library's HCL blending function. 79 | 80 | This step is repeated for all of the frames, yield `n_cg` colors. 81 | 82 | ### Blend 83 | The blending I'm talking about here is blending mode as used in image editors such as PhotoShop. I opted to use PhotoShop's color blending mode, which works in HCL colorspace. HCL is hue, chroma, and luma. The color blending mode preserves the bottom layer's luma while adopting the top layer's hue and chroma. This deals with the issue of having a solid color just being overlayed on top. In addition, any 0 alpha pixel will be ignored preserving the original transparency. 84 | -------------------------------------------------------------------------------- /src/commandline.rs: -------------------------------------------------------------------------------- 1 | use std::error; 2 | use std::vec; 3 | 4 | use crate::color; 5 | 6 | macro_rules! define_cli_enum { 7 | ($enum_name:ident, { $($enum_val:ident : ($enum_val_name:literal, $enum_help:literal)),* $(,)? }) => { 8 | #[derive(Clone, Copy)] 9 | pub enum $enum_name { 10 | $( 11 | $enum_val, 12 | )* 13 | } 14 | 15 | impl ValueEnum for $enum_name { 16 | fn value_variants<'a>() -> &'a [Self] { 17 | return &[ 18 | $( 19 | Self::$enum_val, 20 | )* 21 | ]; 22 | } 23 | 24 | fn to_possible_value<'a>(&self) -> Option { 25 | return Some(match self { 26 | $( 27 | Self::$enum_val => PossibleValue::new($enum_val_name).help($enum_help), 28 | )* 29 | }); 30 | } 31 | } 32 | 33 | impl std::fmt::Display for $enum_name { 34 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 35 | return self.to_possible_value() 36 | .expect("no values are skipped") 37 | .get_name() 38 | .fmt(f); 39 | } 40 | } 41 | 42 | impl std::str::FromStr for $enum_name { 43 | type Err = String; 44 | 45 | fn from_str(s: &str) -> Result { 46 | for variant in Self::value_variants() { 47 | if variant.to_possible_value().unwrap().matches(s, false) { 48 | return Ok(*variant); 49 | } 50 | } 51 | 52 | return Err(format!("Invalid variant: {}", s)); 53 | } 54 | } 55 | }; 56 | } 57 | 58 | pub(crate) use define_cli_enum; 59 | 60 | crate::error_utils::define_error!( 61 | CommandlineError, { 62 | IncompatibleValue: "The given value is incompatible with the configurations", 63 | NotImplemented: "The given option is not implemented", 64 | InvalidValue: "The given value is invalid", 65 | } 66 | ); 67 | 68 | pub fn get_colors(matches: &clap::ArgMatches) -> Result, Box> 69 | where 70 | C: color::Color, 71 | { 72 | let color_strings = matches.get_many::("colors").unwrap(); 73 | let mut color_vec: vec::Vec = vec::Vec::new(); 74 | for color_string in color_strings { 75 | if color_string.len() != 6 { 76 | return Err(Box::new(CommandlineError::InvalidValue( 77 | None, 78 | format!("Invalid color format {}", &color_string), 79 | ))); 80 | } 81 | 82 | match color::from_hex::(&color_string) { 83 | Ok(c) => color_vec.push(c), 84 | Err(e) => { 85 | return Err(Box::new(CommandlineError::InvalidValue( 86 | Some(Box::new(e)), 87 | format!("Could not parse {} as color", color_string).into(), 88 | ))) 89 | } 90 | } 91 | } 92 | 93 | return Ok(color_vec); 94 | } 95 | 96 | pub fn get_gradient( 97 | matches: &clap::ArgMatches, 98 | colors: vec::Vec, 99 | frames_len: usize, 100 | loop_count: usize, 101 | ) -> vec::Vec 102 | where 103 | C: color::Color + palette::Mix + Clone, 104 | palette::rgb::Rgb: 105 | palette::convert::FromColorUnclamped<>::Color>, 106 | { 107 | let gradient_desc = color::gradient::GradientDescriptor::new(colors); 108 | let generator_type = matches 109 | .get_one::("generator") 110 | .unwrap() 111 | .to_owned(); 112 | return gradient_desc.generate(frames_len * loop_count, generator_type); 113 | } 114 | 115 | #[allow(dead_code, unused_variables)] 116 | pub fn get_gradient_2( 117 | matches: &clap::ArgMatches, 118 | colors: vec::Vec, 119 | frames_len: usize, 120 | loop_count: usize, 121 | ) -> vec::Vec 122 | where 123 | C: color::Color + palette::Mix + Clone, 124 | { 125 | println!("HELLO"); 126 | // hard coded gradient from the go version for fidget spinner 127 | return vec![ 128 | C::from_color(color::ColorType::new( 129 | 0.999999918662032, 130 | 1.703028093156283e-06, 131 | 0., 132 | 1., 133 | )), 134 | C::from_color(color::ColorType::new( 135 | 0.9922556125011065, 136 | 0.2849531521554511, 137 | 0., 138 | 1., 139 | )), 140 | C::from_color(color::ColorType::new( 141 | 0.9627980942031773, 142 | 0.42782030813630506, 143 | 0., 144 | 1., 145 | )), 146 | C::from_color(color::ColorType::new( 147 | 0.9125083887754324, 148 | 0.5468183165433307, 149 | 0., 150 | 1., 151 | )), 152 | C::from_color(color::ColorType::new( 153 | 0.841537229795374, 154 | 0.6529456392653107, 155 | 0., 156 | 1., 157 | )), 158 | C::from_color(color::ColorType::new( 159 | 0.7488661564705226, 160 | 0.749785992742367, 161 | 0., 162 | 1., 163 | )), 164 | C::from_color(color::ColorType::new( 165 | 0.6289970827379517, 166 | 0.8391317023531399, 167 | 0., 168 | 1., 169 | )), 170 | C::from_color(color::ColorType::new( 171 | 0.46107468248846606, 172 | 0.9222048643508922, 173 | 0., 174 | 1., 175 | )), 176 | C::from_color(color::ColorType::new(5.226930676345863e-07, 1., 0., 1.)), 177 | C::from_color(color::ColorType::new( 178 | 0., 179 | 0.950319894289878, 180 | 0.4129452019482706, 181 | 1., 182 | )), 183 | C::from_color(color::ColorType::new( 184 | 0., 185 | 0.884209508605179, 186 | 0.6780196090054769, 187 | 1., 188 | )), 189 | C::from_color(color::ColorType::new( 190 | 0., 191 | 0.8095729216795732, 192 | 0.9308077481287376, 193 | 1., 194 | )), 195 | C::from_color(color::ColorType::new(0., 0.7297272229698234, 1., 1.)), 196 | C::from_color(color::ColorType::new(0., 0.6407381119030577, 1., 1.)), 197 | C::from_color(color::ColorType::new(0., 0.5304552900441599, 1., 1.)), 198 | C::from_color(color::ColorType::new(0., 0.3754240144898441, 1., 1.)), 199 | C::from_color(color::ColorType::new(6.469852725499018e-07, 0., 1., 1.)), 200 | C::from_color(color::ColorType::new( 201 | 0.5712265300199242, 202 | 0., 203 | 0.8888992006071924, 204 | 1., 205 | )), 206 | C::from_color(color::ColorType::new( 207 | 0.7730113881799562, 208 | 0., 209 | 0.7635102796640795, 210 | 1., 211 | )), 212 | C::from_color(color::ColorType::new( 213 | 0.9014103746953062, 214 | 0., 215 | 0.632485491998479, 216 | 1., 217 | )), 218 | C::from_color(color::ColorType::new( 219 | 0.9822822089152776, 220 | 0., 221 | 0.5034234435986551, 222 | 1., 223 | )), 224 | C::from_color(color::ColorType::new(1., 0., 0.3818031186680415, 1.)), 225 | C::from_color(color::ColorType::new(1., 0., 0.26939468411722584, 1.)), 226 | C::from_color(color::ColorType::new(1., 0., 0.16015913371298365, 1.)), 227 | ]; 228 | } 229 | -------------------------------------------------------------------------------- /go/quantize.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "image/color" 6 | "math" 7 | "sort" 8 | ) 9 | 10 | type Quantizer struct { 11 | count int 12 | } 13 | 14 | func newQuantizer(count int) Quantizer { 15 | return Quantizer{ 16 | count: count, 17 | } 18 | } 19 | 20 | func (q Quantizer) quantize(algo string, colors []color.RGBA) ([]*color.RGBA, []int, error) { 21 | var palette []*color.RGBA 22 | var mapping []int 23 | 24 | switch algo { 25 | case "scalar": 26 | palette, mapping = q.scalar(colors) 27 | case "populosity": 28 | palette, mapping = q.populosity(colors) 29 | case "mediancut": 30 | palette, mapping = q.medianCut(colors) 31 | case "octree": 32 | palette, mapping = q.octree(colors) 33 | case "kmeans": 34 | palette, mapping = q.kmeans(colors) 35 | default: 36 | return nil, nil, errors.New("Invalid quantizer") 37 | } 38 | 39 | return palette, mapping, nil 40 | } 41 | 42 | /* helper method to just transform input to the appropriate output. 43 | * Generates a proper palette from any given input. 44 | */ 45 | func (q Quantizer) identity(colors []color.RGBA) ([]*color.RGBA, []int) { 46 | palette := make(map[color.RGBA]struct { 47 | addr *color.RGBA 48 | index int 49 | }) 50 | paletteSlice := make([]*color.RGBA, 0) 51 | indexMapping := make([]int, len(colors)) 52 | 53 | for i, c := range colors { 54 | colorInfo, okay := palette[c] 55 | if !okay { 56 | colorInfo = struct { 57 | addr *color.RGBA 58 | index int 59 | }{ 60 | addr: &c, 61 | index: len(paletteSlice), 62 | } 63 | palette[c] = colorInfo 64 | paletteSlice = append(paletteSlice, colorInfo.addr) 65 | } 66 | 67 | indexMapping[i] = colorInfo.index 68 | } 69 | 70 | return paletteSlice, indexMapping 71 | } 72 | 73 | func (q Quantizer) scalar(colors []color.RGBA) ([]*color.RGBA, []int) { 74 | if len(colors) < q.count { 75 | return q.identity(colors) 76 | } 77 | 78 | palette := make(map[color.RGBA]struct { 79 | addr *color.RGBA 80 | index int 81 | }) 82 | paletteSlice := make([]*color.RGBA, 0) 83 | indexMapping := make([]int, len(colors)) 84 | for i, c := range colors { 85 | // pack R into 3 bits, G into 3 bits, and B into 2 bits 86 | newColor := &color.RGBA{ 87 | R: c.R & (0b11100000), 88 | G: c.G & (0b11100000), 89 | B: c.B & (0b11000000), 90 | A: c.A, 91 | } 92 | 93 | colorInfo, okay := palette[*newColor] 94 | if !okay { 95 | colorInfo = struct { 96 | addr *color.RGBA 97 | index int 98 | }{ 99 | addr: newColor, 100 | index: len(paletteSlice), 101 | } 102 | palette[*newColor] = colorInfo 103 | paletteSlice = append(paletteSlice, colorInfo.addr) 104 | } 105 | 106 | indexMapping[i] = colorInfo.index 107 | } 108 | 109 | return paletteSlice, indexMapping 110 | } 111 | 112 | func (q Quantizer) populosity(colors []color.RGBA) ([]*color.RGBA, []int) { 113 | if len(colors) < q.count { 114 | return q.identity(colors) 115 | } 116 | 117 | palette := make(map[color.RGBA]struct { 118 | index int 119 | count int 120 | }) 121 | 122 | for _, c := range colors { 123 | v, okay := palette[c] 124 | if okay { 125 | v.count++ 126 | palette[c] = v 127 | } else { 128 | palette[c] = struct { 129 | index int 130 | count int 131 | }{ 132 | index: -1, 133 | count: 1, 134 | } 135 | } 136 | } 137 | 138 | // no need to do any extra work, we have all the colors we need 139 | if len(palette) <= q.count { 140 | return q.identity(colors) 141 | } 142 | 143 | sorted := make([]struct { 144 | color color.RGBA 145 | count int 146 | }, len(palette)) 147 | index := 0 148 | for k, v := range palette { 149 | sorted[index] = struct { 150 | color color.RGBA 151 | count int 152 | }{color: k, count: v.count} 153 | index++ 154 | } 155 | 156 | sort.Slice(sorted, func(i int, j int) bool { 157 | return sorted[i].count > sorted[j].count 158 | }) 159 | 160 | // lose any extra colors 161 | if len(sorted) > q.count { 162 | sorted = sorted[:q.count] 163 | } 164 | 165 | tempPalette := make(color.Palette, len(sorted)) 166 | for i, c := range sorted { 167 | tempPalette[i] = c.color 168 | } 169 | 170 | paletteSlice := make([]*color.RGBA, 0) 171 | indexMapping := make([]int, len(colors)) 172 | 173 | // TODO toss this into a goroutine - `Convert` is essentially a linear search 174 | for i, originalColor := range colors { 175 | converted := tempPalette.Convert(originalColor).(color.RGBA) 176 | 177 | colorInfo := palette[converted] 178 | if colorInfo.index == -1 { 179 | colorInfo.index = len(paletteSlice) 180 | paletteSlice = append(paletteSlice, &color.RGBA{R: converted.R, G: converted.G, B: converted.B, A: originalColor.A}) 181 | palette[converted] = colorInfo 182 | } 183 | 184 | indexMapping[i] = colorInfo.index 185 | } 186 | 187 | return paletteSlice, indexMapping 188 | } 189 | 190 | func (q Quantizer) medianCut(colors []color.RGBA) ([]*color.RGBA, []int) { 191 | if len(colors) < q.count { 192 | return q.identity(colors) 193 | } 194 | 195 | // for use later on 196 | indices := make(map[color.RGBA][]int) 197 | uniqueColors := make([]*color.RGBA, 0) 198 | 199 | for i, c := range colors { 200 | _, okay := indices[c] 201 | if okay { 202 | indices[c] = append(indices[c], i) 203 | } else { 204 | indices[c] = []int{i} 205 | uniqueColors = append(uniqueColors, &color.RGBA{R: c.R, G: c.G, B: c.B, A: c.A}) 206 | } 207 | } 208 | 209 | depth := int(math.Round(math.Log2(float64(q.count)))) 210 | actualCount := int(math.Pow(2, float64(depth))) 211 | palette := make([]*color.RGBA, actualCount) 212 | indexMapping := make([]int, len(colors)) 213 | 214 | uniqueColors, mappedColors := q.medianCutSplit(uniqueColors, depth) 215 | 216 | temp := make(map[*color.RGBA]int) 217 | index := 0 218 | for _, c := range mappedColors { 219 | _, okay := temp[c] 220 | if !okay { 221 | palette[index] = c 222 | temp[c] = index 223 | index++ 224 | } 225 | } 226 | 227 | for i := range uniqueColors { 228 | colorPtr := uniqueColors[i] 229 | mappedColor := mappedColors[i] 230 | originalIndices := indices[*colorPtr] 231 | 232 | for _, x := range originalIndices { 233 | indexMapping[x] = temp[mappedColor] 234 | } 235 | } 236 | 237 | return palette, indexMapping 238 | } 239 | 240 | func (q Quantizer) medianCutSplit(bucket []*color.RGBA, depth int) ([]*color.RGBA, []*color.RGBA) { 241 | if len(bucket) == 0 { 242 | return nil, nil 243 | } 244 | 245 | if depth == 0 { 246 | // average all the pixels in the bucket and return 247 | sum := []uint{0, 0, 0, 1} 248 | for _, c := range bucket { 249 | sum[0] += uint(c.R) 250 | sum[1] += uint(c.G) 251 | sum[2] += uint(c.B) 252 | sum[3] *= uint(c.A) 253 | } 254 | 255 | if sum[3] != 0 { 256 | sum[3] = 255 257 | } 258 | 259 | avg := &color.RGBA{ 260 | R: uint8(sum[0] / uint(len(bucket))), 261 | G: uint8(sum[1] / uint(len(bucket))), 262 | B: uint8(sum[2] / uint(len(bucket))), 263 | A: uint8(sum[3]), 264 | } 265 | 266 | palette := make([]*color.RGBA, len(bucket)) 267 | for i := range palette { 268 | palette[i] = avg 269 | } 270 | 271 | return bucket, palette 272 | } 273 | 274 | r := []int{-1, 0} 275 | g := []int{-1, 0} 276 | b := []int{-1, 0} 277 | 278 | for _, c := range bucket { 279 | cR := int(c.R) 280 | if r[0] == -1 || cR < r[0] { 281 | r[0] = cR 282 | } 283 | if cR > r[1] { 284 | r[1] = cR 285 | } 286 | 287 | cG := int(c.G) 288 | if g[0] == -1 || cG < g[0] { 289 | g[0] = cG 290 | } 291 | if cG > g[1] { 292 | g[1] = cG 293 | } 294 | 295 | cB := int(c.B) 296 | if b[0] == -1 || cB < b[0] { 297 | b[0] = cB 298 | } 299 | if cB > b[1] { 300 | b[1] = cB 301 | } 302 | } 303 | 304 | rDiff := r[1] - r[0] 305 | gDiff := g[1] - g[0] 306 | bDiff := b[1] - b[0] 307 | 308 | if rDiff > gDiff && rDiff > bDiff { 309 | // red 310 | sort.Slice(bucket, func(i int, j int) bool { 311 | return bucket[i].R < bucket[j].R 312 | }) 313 | } else if gDiff > bDiff { 314 | // green 315 | sort.Slice(bucket, func(i int, j int) bool { 316 | return bucket[i].G < bucket[j].G 317 | }) 318 | } else { 319 | // blue 320 | sort.Slice(bucket, func(i int, j int) bool { 321 | return bucket[i].B < bucket[j].B 322 | }) 323 | } 324 | 325 | aPalette, aMappedcolor := q.medianCutSplit(bucket[:len(bucket)/2+1], depth-1) 326 | bPalette, bMappedColor := q.medianCutSplit(bucket[len(bucket)/2+1:], depth-1) 327 | 328 | palette := append(aPalette, bPalette...) 329 | mappedColor := append(aMappedcolor, bMappedColor...) 330 | 331 | return palette, mappedColor 332 | } 333 | 334 | func (q Quantizer) octree([]color.RGBA) ([]*color.RGBA, []int) { 335 | panic("Not implemented") 336 | } 337 | 338 | func (q Quantizer) kmeans([]color.RGBA) ([]*color.RGBA, []int) { 339 | panic("Not implemented") 340 | } 341 | -------------------------------------------------------------------------------- /src/codec/gif.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::error; 3 | use std::io; 4 | use std::marker::PhantomData; 5 | use std::vec; 6 | 7 | use palette::FromColor; 8 | 9 | use super::{Decodable, DecodeError, EncodeError, Frame, Palette}; 10 | use crate::color; 11 | 12 | pub struct GifDecoder { 13 | phantom: PhantomData, 14 | decoder: gif::Decoder, 15 | } 16 | 17 | impl GifDecoder 18 | where 19 | R: io::Read, 20 | C: color::Color, 21 | { 22 | pub fn new(read: R) -> Result> { 23 | let mut decoder_options = gif::DecodeOptions::new(); 24 | decoder_options.set_color_output(gif::ColorOutput::Indexed); 25 | 26 | let decoder = match decoder_options.read_info(read) { 27 | Ok(dec) => dec, 28 | Err(e) => { 29 | return Err(Box::new(DecodeError::Read( 30 | Some(Box::new(e)), 31 | "Could not read image".to_owned(), 32 | ))); 33 | } 34 | }; 35 | 36 | return Ok(GifDecoder { 37 | phantom: PhantomData, 38 | decoder, 39 | }); 40 | } 41 | 42 | #[allow(dead_code)] 43 | pub fn get_width(&self) -> u16 { 44 | return self.decoder.width(); 45 | } 46 | 47 | #[allow(dead_code)] 48 | pub fn get_height(&self) -> u16 { 49 | return self.decoder.height(); 50 | } 51 | } 52 | 53 | impl IntoIterator for GifDecoder 54 | where 55 | R: io::Read, 56 | C: color::Color, 57 | palette::rgb::Rgb: 58 | palette::convert::FromColorUnclamped<>::Color>, 59 | { 60 | type Item = Frame; 61 | type IntoIter = GifDecoderImpl; 62 | 63 | fn into_iter(self) -> Self::IntoIter { 64 | return GifDecoderImpl { decoder: self }; 65 | } 66 | } 67 | 68 | impl super::Decodable for GifDecoder 69 | where 70 | R: io::Read, 71 | C: color::Color, 72 | palette::rgb::Rgb: 73 | palette::convert::FromColorUnclamped<>::Color>, 74 | { 75 | type OutputColor = C; 76 | 77 | fn decode(&mut self) -> Result>, Box> { 78 | let global_pal = self 79 | .decoder 80 | .global_palette() 81 | .map(|e| e.to_vec()) 82 | .unwrap_or(vec::Vec::new()); 83 | let frame = match self.decoder.read_next_frame() { 84 | Ok(f) => { 85 | if let Some(f_result) = f { 86 | f_result 87 | } else { 88 | return Ok(None); 89 | } 90 | } 91 | Err(e) => return Err(Box::new(e)), 92 | }; 93 | 94 | let pal = if let Some(pal) = &frame.palette { 95 | Palette::::from_gif_format(&pal[..]) 96 | } else { 97 | if global_pal.len() == 0 { 98 | return Err(Box::new(DecodeError::InvalidData( 99 | None, 100 | "Frame had no valid global palette to fall back to".to_owned(), 101 | ))); 102 | } 103 | 104 | Palette::from_gif_format(&global_pal[..]) 105 | }; 106 | 107 | // We don't need to deal with disposal and such since it'll just be encoded back into a gif 108 | return Ok(Some(Frame { 109 | delay: frame.delay, 110 | dispose: frame.dispose, 111 | origin: (frame.left, frame.top), 112 | dimensions: (frame.width, frame.height), 113 | palette: (pal), 114 | pixels_indexed: frame.buffer.to_vec(), 115 | transparent_index: frame.transparent, 116 | interlaced: frame.interlaced, 117 | needs_input: frame.needs_user_input, 118 | })); 119 | } 120 | 121 | fn decode_all(&mut self) -> Result>>, Box> { 122 | let mut frames = vec::Vec::new(); 123 | 124 | loop { 125 | let opt = match self.decode() { 126 | Ok(opt) => opt, 127 | Err(e) => return Err(e), 128 | }; 129 | 130 | if let Some(frame) = opt { 131 | frames.push(frame); 132 | } else { 133 | break; 134 | } 135 | } 136 | 137 | if frames.len() != 0 { 138 | return Ok(Some(frames)); 139 | } 140 | 141 | return Ok(None); 142 | } 143 | 144 | fn get_dimensions(&self) -> (u16, u16) { 145 | let dec_ref = &self.decoder; 146 | return (dec_ref.width(), dec_ref.height()); 147 | } 148 | } 149 | 150 | pub struct GifDecoderImpl { 151 | decoder: GifDecoder, 152 | } 153 | 154 | impl Iterator for GifDecoderImpl 155 | where 156 | R: io::Read, 157 | C: color::Color, 158 | palette::rgb::Rgb: 159 | palette::convert::FromColorUnclamped<>::Color>, 160 | { 161 | type Item = Frame; 162 | 163 | fn next(&mut self) -> Option { 164 | if let Ok(res) = self.decoder.decode() { 165 | return res; 166 | } 167 | 168 | return None; 169 | } 170 | } 171 | 172 | #[allow(dead_code)] 173 | pub struct GifEncoder { 174 | phantom: PhantomData, 175 | encoder: RefCell>, 176 | width: u16, 177 | height: u16, 178 | } 179 | 180 | impl<'a, W, C> GifEncoder 181 | where 182 | W: io::Write, 183 | C: color::Color, 184 | { 185 | pub fn new(w: W, (width, height): (u16, u16)) -> Result { 186 | let encoder_impl = match gif::Encoder::new(w, width, height, &[]) { 187 | Ok(enc) => RefCell::new(enc), 188 | Err(e) => return Err(e.to_string()), 189 | }; 190 | 191 | if let Err(e) = encoder_impl.borrow_mut().set_repeat(gif::Repeat::Infinite) { 192 | return Err(e.to_string()); 193 | } 194 | 195 | return Ok(GifEncoder { 196 | phantom: PhantomData, 197 | encoder: encoder_impl, 198 | width, 199 | height, 200 | }); 201 | } 202 | 203 | pub fn write(&self, frame: Frame) -> Result<(), Box> 204 | where 205 | C: color::Color, 206 | palette::rgb::Rgb: 207 | palette::convert::FromColorUnclamped< 208 | >::Color, 209 | >, 210 | { 211 | let pal = frame 212 | .palette 213 | .colors 214 | .into_iter() 215 | .flat_map(|c| { 216 | let temp_color = color::ColorType::from_color(c); 217 | return [ 218 | (temp_color.color.red * 255.) as u8, 219 | (temp_color.color.green * 255.) as u8, 220 | (temp_color.color.blue * 255.) as u8, 221 | ]; 222 | }) 223 | .collect::>(); 224 | 225 | let mut new_frame = gif::Frame::from_palette_pixels( 226 | frame.dimensions.0, 227 | frame.dimensions.1, 228 | &&frame.pixels_indexed[..], 229 | &pal[..], 230 | frame.transparent_index, 231 | ); 232 | new_frame.left = frame.origin.0; 233 | new_frame.top = frame.origin.1; 234 | new_frame.delay = frame.delay; 235 | new_frame.dispose = frame.dispose; 236 | new_frame.interlaced = frame.interlaced; 237 | new_frame.needs_user_input = frame.needs_input; 238 | 239 | if let Err(e) = self.encoder.borrow_mut().write_frame(&new_frame) { 240 | return Err(Box::new(EncodeError::FrameWrite( 241 | Some(Box::new(e)), 242 | "write_frame errored".to_owned(), 243 | ))); 244 | } 245 | 246 | return Ok(()); 247 | } 248 | 249 | pub fn into_inner(self) -> Result { 250 | return self.encoder.into_inner().into_inner(); 251 | } 252 | } 253 | 254 | impl super::Encodable for GifEncoder 255 | where 256 | W: io::Write, 257 | C: color::Color, 258 | palette::rgb::Rgb: 259 | palette::convert::FromColorUnclamped<>::Color>, 260 | { 261 | type InputColor = C; 262 | 263 | fn encode(&self, frame: Frame) -> Result<(), Box> { 264 | // TODO probably should have own error type 265 | return self.write(frame); 266 | } 267 | 268 | fn encode_all(&self, frames: vec::Vec>) -> Result<(), Box> { 269 | for frame in frames { 270 | self.write(frame)?; 271 | } 272 | return Ok(()); 273 | } 274 | } 275 | 276 | /* impl FromIterator> for GifEncoder 277 | where 278 | C: color::Color + palette::WithAlpha, 279 | palette::rgb::Rgb: 280 | palette::convert::FromColorUnclamped<>::Color>, 281 | { 282 | fn from_iter(iter: T) -> Self 283 | where 284 | T: IntoIterator>, 285 | { 286 | return GifEncoder {}; 287 | } 288 | } */ 289 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | use std::error; 3 | use std::fmt; 4 | use std::fs; 5 | 6 | use clap::{arg, command, value_parser, ArgMatches}; 7 | use palette; 8 | 9 | use rainbowgif::{buffer, codec, color, commandline}; 10 | 11 | fn mix_impl(matches: ArgMatches, mix_fn: fn(&C, &C) -> C) -> Result<(), Box> 12 | where 13 | C: color::Color + palette::Clamp + fmt::Debug, 14 | palette::rgb::Rgb: 15 | palette::convert::FromColorUnclamped<>::Color>, 16 | { 17 | let src_image_path = matches.get_one::("input_file").unwrap(); 18 | let src_data = buffer::Data::from_path(src_image_path)?; 19 | let mut decoder: Box> = { 20 | if matches.get_flag("static") { 21 | Box::new(codec::image::ImageDecoder::new(src_data.buffer, None)?) 22 | } else { 23 | Box::new(codec::gif::GifDecoder::new(src_data.buffer)?) 24 | } 25 | }; 26 | 27 | let dest_image_path = matches.get_one::("output_file").unwrap(); 28 | let mut dest_data = buffer::Data::new(); 29 | let encoder = codec::gif::GifEncoder::new(dest_data.buffer, decoder.get_dimensions())?; 30 | 31 | let loop_count = cmp::max( 32 | matches.get_one::("loop_count").unwrap().to_owned() as usize, 33 | 1usize, 34 | ); 35 | // TODO: figure out either how to generate colors without knowing the frame count OR figure out 36 | // how to get the frame count while streaming the decoding process (not decoding everything at 37 | // once) 38 | 39 | // automatically transform to the specified color space in the decoder 40 | let frames = decoder.decode_all()?.unwrap(); 41 | let frames_len = frames.len(); 42 | let input_colors = commandline::get_colors::(&matches)?; 43 | let colors = commandline::get_gradient(&matches, input_colors, frames_len, loop_count); 44 | 45 | // let rgba_colors: vec::Vec = colors.iter().map(|e| color::ColorType::from_color(e.clone())).collect(); 46 | // println!("{:#?}", rgba_colors[20]); 47 | 48 | // TODO find a better way to do this, rather than cloning every loop 49 | for l in 0usize..loop_count { 50 | for (i, frame) in frames.iter().enumerate() { 51 | let new_color = &colors[i + (frames_len * l)]; 52 | 53 | let mut cloned = frame.clone(); 54 | cloned 55 | .palette 56 | .colors 57 | .iter_mut() 58 | .for_each(|c| *c = mix_fn(c, new_color)); 59 | 60 | encoder.write(cloned)?; 61 | } 62 | } 63 | 64 | dest_data.buffer = encoder.into_inner()?; 65 | let _ = fs::write(dest_image_path, dest_data.buffer.get_ref())?; 66 | 67 | return Ok(()); 68 | } 69 | 70 | fn mix_none(matches: ArgMatches) -> Result<(), Box> 71 | where 72 | C: color::Color + palette::Clamp + fmt::Debug, 73 | palette::rgb::Rgb: 74 | palette::convert::FromColorUnclamped<>::Color>, 75 | { 76 | return mix_impl(matches, |a: &C, _: &C| { 77 | return a.clone(); 78 | }); 79 | } 80 | 81 | fn mix_custom(matches: ArgMatches) -> Result<(), Box> 82 | where 83 | C: color::Color 84 | + palette::Clamp 85 | + color::Componentize 86 | + fmt::Debug, 87 | palette::rgb::Rgb: 88 | palette::convert::FromColorUnclamped<>::Color>, 89 | { 90 | return mix_impl(matches, |a, b| { 91 | return color::blend_colors::( 92 | a, b, true, 93 | ); 94 | }); 95 | } 96 | 97 | fn mix_lab(matches: ArgMatches) -> Result<(), Box> { 98 | return mix_impl( 99 | matches, 100 | |a: &palette::Laba, 101 | b: &palette::Laba| { 102 | return palette::Laba::from_components((a.l, b.a, b.b, a.alpha)); 103 | }, 104 | ); 105 | } 106 | 107 | fn mix_linear(matches: ArgMatches) -> Result<(), Box> 108 | where 109 | C: color::Color + palette::Clamp + fmt::Debug, 110 | palette::rgb::Rgb: 111 | palette::convert::FromColorUnclamped<>::Color>, 112 | { 113 | // this isn't quite right, but again linear mixing might just not be ever 114 | return mix_impl(matches, |a: &C, b: &C| { 115 | let (_, a_alpha) = a.clone().split(); 116 | if a_alpha <= 0.5 { 117 | return a.clone(); 118 | } 119 | 120 | return a.mix(b, 0.2); 121 | }); 122 | } 123 | 124 | fn main() -> Result<(), Box> { 125 | let matches = command!() 126 | .arg(arg!(input_file: "The path to the input file")) 127 | .arg(arg!(output_file: "The path to the output file")) 128 | .arg(arg!(static: --static "Whether the input is static or not")) 129 | .arg( 130 | arg!(loop_count: --loop_count [LOOP_COUNT] "Number of times to loop for a GIF and for a static input, the resulting number of frames") 131 | .value_parser(clap::value_parser!(u64) 132 | .range(1..)) 133 | .default_value("1") 134 | ) 135 | .arg( 136 | arg!(colors: -c --colors [COLORS] "The colors to use in the gradient") 137 | .value_delimiter(',') 138 | .default_value("FF0000,00FF00,0000FF") 139 | ) 140 | .arg( 141 | arg!(generator: -g --generator [GENERATOR] "The type generator to use") 142 | .value_parser(value_parser!(color::gradient::GradientGeneratorType)) 143 | .default_value("discrete") 144 | ) 145 | .arg( 146 | arg!(color_space: -s --color_space [COLOR_SPACE] "The color space to use") 147 | .value_parser(value_parser!(color::ColorSpace)) 148 | .default_value("lch") 149 | ) 150 | .arg( 151 | arg!(mixing_mode: -m --mixing_mode [MIXING_MODE] "What kind of mixing to use") 152 | .value_parser(value_parser!(color::MixingMode)) 153 | .default_value("custom") 154 | ) 155 | .get_matches(); 156 | 157 | let color_space = matches.get_one::("color_space").unwrap(); 158 | 159 | match matches.get_one::("mixing_mode").unwrap() { 160 | color::MixingMode::None => match color_space { 161 | color::ColorSpace::HSL => { 162 | mix_none::>(matches) 163 | } 164 | 165 | color::ColorSpace::HSV => { 166 | mix_none::>(matches) 167 | } 168 | 169 | color::ColorSpace::LAB => { 170 | mix_none::>(matches) 171 | } 172 | 173 | color::ColorSpace::LCH => { 174 | mix_none::>(matches) 175 | } 176 | 177 | _ => Err(Box::new(commandline::CommandlineError::IncompatibleValue( 178 | None, 179 | "Only HSL, HSV, LAB, and LCH are supported for custom mixing mode".to_owned(), 180 | ))), 181 | }, 182 | 183 | color::MixingMode::Custom => match color_space { 184 | color::ColorSpace::HSL => mix_custom::< 185 | palette::RgbHue, 186 | palette::Hsla, 187 | >(matches), 188 | 189 | color::ColorSpace::HSV => mix_custom::< 190 | palette::RgbHue, 191 | palette::Hsva, 192 | >(matches), 193 | 194 | color::ColorSpace::LCH => mix_custom::< 195 | palette::LabHue, 196 | palette::Lcha, 197 | >(matches), 198 | 199 | _ => Err(Box::new(commandline::CommandlineError::IncompatibleValue( 200 | None, 201 | "Only HSL, HSV, and LCH are supported for custom mixing mode".to_owned(), 202 | ))), 203 | }, 204 | 205 | color::MixingMode::Lab => match color_space { 206 | color::ColorSpace::LAB => mix_lab(matches), 207 | 208 | _ => Err(Box::new(commandline::CommandlineError::IncompatibleValue( 209 | None, 210 | "Only LAB is supported for lab mixing mode".to_owned(), 211 | ))), 212 | }, 213 | 214 | color::MixingMode::Linear => match color_space { 215 | color::ColorSpace::HSL => { 216 | mix_linear::>(matches) 217 | } 218 | 219 | color::ColorSpace::HSV => { 220 | mix_linear::>(matches) 221 | } 222 | 223 | color::ColorSpace::LAB => { 224 | mix_linear::>(matches) 225 | } 226 | 227 | color::ColorSpace::LCH => { 228 | mix_linear::>(matches) 229 | } 230 | 231 | _ => Err(Box::new(commandline::CommandlineError::IncompatibleValue( 232 | None, 233 | "Only HSL, HSV, LAB, and LCH are supported for custom mixing mode".to_owned(), 234 | ))), 235 | }, 236 | 237 | color::MixingMode::BlendOverlay => { 238 | Err(Box::new(commandline::CommandlineError::NotImplemented( 239 | None, 240 | "Blend overlay mixing is not implemented".to_owned(), 241 | ))) 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /go/gradient_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/lucasb-eyer/go-colorful" 7 | ) 8 | 9 | func TestNewGradient(t *testing.T) { 10 | t.Run( 11 | "Zero color", 12 | func(innerT *testing.T) { 13 | gradient := newGradient( 14 | []colorful.Color{}, 15 | ) 16 | 17 | if len(gradient.positions) != 0 { 18 | innerT.Errorf("Expected %v but got %v", 0, len(gradient.positions)) 19 | } 20 | }, 21 | ) 22 | 23 | t.Run( 24 | "One color", 25 | func(innerT *testing.T) { 26 | gradient := newGradient( 27 | []colorful.Color{ 28 | {0, 0, 0}, 29 | }, 30 | ) 31 | 32 | if len(gradient.positions) != 1 { 33 | innerT.Errorf("Expected %v but got %v", 1, len(gradient.positions)) 34 | } 35 | 36 | if gradient.positions[0] != 0.0 { 37 | innerT.Errorf("Expected %v but got %v", 0.0, gradient.positions[0]) 38 | } 39 | }, 40 | ) 41 | 42 | t.Run( 43 | "Two colors", 44 | func(innerT *testing.T) { 45 | gradient := newGradient( 46 | []colorful.Color{ 47 | {0, 0, 0}, 48 | {1, 1, 1}, 49 | }, 50 | ) 51 | 52 | if len(gradient.positions) != 2 { 53 | innerT.Errorf("Expected %v but got %v", 2, len(gradient.positions)) 54 | } 55 | 56 | if gradient.positions[0] != 0.0 { 57 | innerT.Errorf("Expected %v but got %v", 0.0, gradient.positions[0]) 58 | } 59 | 60 | if gradient.positions[1] != 1.0 { 61 | innerT.Errorf("%v", gradient.colors) 62 | innerT.Errorf("%v", gradient.positions) 63 | innerT.Errorf("Expected %v but got %v", 1.0, gradient.positions[1]) 64 | } 65 | }, 66 | ) 67 | } 68 | 69 | func TestPositionSearch(t *testing.T) { 70 | t.Run( 71 | "One color", 72 | func(innerT *testing.T) { 73 | colors := []colorful.Color{ 74 | {0, 0, 0}, 75 | } 76 | gradient := newGradient(colors) 77 | 78 | for i := 0.0; i <= 1.0; i += 0.1 { 79 | returnedKeyFrames := gradient.positionSearch(i) 80 | 81 | if returnedKeyFrames[0].color != colors[0] { 82 | innerT.Errorf( 83 | "Expected %v but got %v", 84 | colors[0], 85 | returnedKeyFrames[0].color, 86 | ) 87 | } 88 | } 89 | }, 90 | ) 91 | 92 | t.Run( 93 | "Two colors", 94 | func(innerT *testing.T) { 95 | colors := []colorful.Color{ 96 | {0, 0, 0}, 97 | {1, 1, 1}, 98 | } 99 | gradient := newGradient(colors) 100 | 101 | for i := 0.0; i <= 1.0; i += 0.1 { 102 | returnedKeyFrames := gradient.positionSearch(i) 103 | 104 | if returnedKeyFrames[0].color != colors[0] { 105 | innerT.Errorf( 106 | "Expected %v but got %v", 107 | colors[0], 108 | returnedKeyFrames[0].color, 109 | ) 110 | } 111 | 112 | if returnedKeyFrames[1].color != colors[1] { 113 | innerT.Errorf( 114 | "Expected %v but got %v", 115 | colors[1], 116 | returnedKeyFrames[1].color, 117 | ) 118 | } 119 | } 120 | }, 121 | ) 122 | 123 | t.Run( 124 | "Three colors", 125 | func(innerT *testing.T) { 126 | colors := []colorful.Color{ 127 | {0, 0, 0}, 128 | {0.5, 0.5, 0.5}, 129 | {1, 1, 1}, 130 | } 131 | gradient := newGradient(colors) 132 | 133 | for i := 0.0; i < 0.5; i += 0.1 { 134 | returnedKeyFrames := gradient.positionSearch(i) 135 | 136 | if returnedKeyFrames[0].color != colors[0] { 137 | innerT.Errorf( 138 | "positionSearch(%f)[0] - expected %v but got %v", 139 | i, 140 | colors[0], 141 | returnedKeyFrames[0].color, 142 | ) 143 | } 144 | 145 | if returnedKeyFrames[1].color != colors[1] { 146 | innerT.Errorf( 147 | "positionSearch(%f)[1] - expected %v but got %v", 148 | i, 149 | colors[1], 150 | returnedKeyFrames[1].color, 151 | ) 152 | } 153 | } 154 | 155 | for i := 0.5; i <= 1.0; i += 0.1 { 156 | returnedKeyFrames := gradient.positionSearch(i) 157 | 158 | if returnedKeyFrames[0].color != colors[1] { 159 | innerT.Errorf( 160 | "Expected %v but got %v", 161 | colors[1], 162 | returnedKeyFrames[0].color, 163 | ) 164 | } 165 | 166 | if returnedKeyFrames[1].color != colors[2] { 167 | innerT.Errorf( 168 | "Expected %v but got %v", 169 | colors[2], 170 | returnedKeyFrames[1].color, 171 | ) 172 | } 173 | } 174 | }, 175 | ) 176 | 177 | t.Run( 178 | "Three colors", 179 | func(innerT *testing.T) { 180 | colors := []colorful.Color{ 181 | {0, 0, 0}, 182 | {0.33, 0.33, 0.33}, 183 | {0.66, 0.66, 0.66}, 184 | {1, 1, 1}, 185 | } 186 | gradient := newGradient(colors) 187 | 188 | for i := 0.0; i <= 0.33; i += 0.03 { 189 | returnedKeyFrames := gradient.positionSearch(i) 190 | 191 | if returnedKeyFrames[0].color != colors[0] { 192 | innerT.Errorf( 193 | "positionSearch(%f)[0] - expected %v but got %v", 194 | i, 195 | colors[0], 196 | returnedKeyFrames[0].color, 197 | ) 198 | } 199 | 200 | if returnedKeyFrames[1].color != colors[1] { 201 | innerT.Errorf( 202 | "positionSearch(%f)[1] - expected %v but got %v", 203 | i, 204 | colors[1], 205 | returnedKeyFrames[1].color, 206 | ) 207 | } 208 | } 209 | 210 | for i := 0.34; i < 0.66; i += 0.03 { 211 | returnedKeyFrames := gradient.positionSearch(i) 212 | 213 | if returnedKeyFrames[0].color != colors[1] { 214 | innerT.Errorf( 215 | "positionSearch(%f)[0] - expected %v but got %v", 216 | i, 217 | colors[1], 218 | returnedKeyFrames[0].color, 219 | ) 220 | } 221 | 222 | if returnedKeyFrames[1].color != colors[2] { 223 | innerT.Errorf( 224 | "positionSearch(%f)[1] - expected %v but got %v", 225 | i, 226 | colors[2], 227 | returnedKeyFrames[1].color, 228 | ) 229 | } 230 | } 231 | 232 | for i := 0.67; i <= 1; i += 0.03 { 233 | returnedKeyFrames := gradient.positionSearch(i) 234 | 235 | if returnedKeyFrames[0].color != colors[2] { 236 | innerT.Errorf( 237 | "positionSearch(%f)[0] - expected %v but got %v", 238 | i, 239 | colors[2], 240 | returnedKeyFrames[0].color, 241 | ) 242 | } 243 | 244 | if returnedKeyFrames[1].color != colors[3] { 245 | innerT.Errorf( 246 | "positionSearch(%f)[1] - expected %v but got %v", 247 | i, 248 | colors[3], 249 | returnedKeyFrames[0].color, 250 | ) 251 | } 252 | } 253 | }, 254 | ) 255 | 256 | t.Run( 257 | "Four colors", 258 | func(innerT *testing.T) { 259 | colors := []colorful.Color{ 260 | {0, 0, 0}, 261 | {0.25, 0.25, 0.25}, 262 | {0.5, 0.5, 0.5}, 263 | {0.75, 0.75, 0.75}, 264 | {1, 1, 1}, 265 | } 266 | gradient := newGradient(colors) 267 | 268 | for i := 0.0; i < 0.25; i += 0.05 { 269 | returnedKeyFrames := gradient.positionSearch(i) 270 | 271 | if returnedKeyFrames[0].color != colors[0] { 272 | innerT.Errorf( 273 | "positionSearch(%f)[0] - expected %v but got %v", 274 | i, 275 | colors[0], 276 | returnedKeyFrames[0].color, 277 | ) 278 | } 279 | 280 | if returnedKeyFrames[1].color != colors[1] { 281 | innerT.Errorf( 282 | "positionSearch(%f)[1] - expected %v but got %v", 283 | i, 284 | colors[1], 285 | returnedKeyFrames[1].color, 286 | ) 287 | } 288 | } 289 | 290 | for i := 0.26; i < 0.5; i += 0.05 { 291 | returnedKeyFrames := gradient.positionSearch(i) 292 | 293 | if returnedKeyFrames[0].color != colors[1] { 294 | innerT.Errorf( 295 | "positionSearch(%f)[0] - expected %v but got %v", 296 | i, 297 | colors[1], 298 | returnedKeyFrames[0].color, 299 | ) 300 | } 301 | 302 | if returnedKeyFrames[1].color != colors[2] { 303 | innerT.Errorf( 304 | "positionSearch(%f)[1] - expected %v but got %v", 305 | i, 306 | colors[2], 307 | returnedKeyFrames[1].color, 308 | ) 309 | } 310 | } 311 | 312 | for i := 0.51; i < 0.75; i += 0.05 { 313 | returnedKeyFrames := gradient.positionSearch(i) 314 | 315 | if returnedKeyFrames[0].color != colors[2] { 316 | innerT.Errorf( 317 | "positionSearch(%f)[0] - expected %v but got %v", 318 | i, 319 | colors[2], 320 | returnedKeyFrames[0].color, 321 | ) 322 | } 323 | 324 | if returnedKeyFrames[1].color != colors[3] { 325 | innerT.Errorf( 326 | "positionSearch(%f)[1] - expected %v but got %v", 327 | i, 328 | colors[3], 329 | returnedKeyFrames[0].color, 330 | ) 331 | } 332 | } 333 | 334 | for i := 0.76; i <= 1; i += 0.05 { 335 | returnedKeyFrames := gradient.positionSearch(i) 336 | 337 | if returnedKeyFrames[0].color != colors[3] { 338 | innerT.Errorf( 339 | "positionSearch(%f)[0] - expected %v but got %v", 340 | i, 341 | colors[3], 342 | returnedKeyFrames[0].color, 343 | ) 344 | } 345 | 346 | if returnedKeyFrames[1].color != colors[4] { 347 | innerT.Errorf( 348 | "positionSearch(%f)[1] - expected %v but got %v", 349 | i, 350 | colors[4], 351 | returnedKeyFrames[0].color, 352 | ) 353 | } 354 | } 355 | }, 356 | ) 357 | } 358 | 359 | func TestGenerate(t *testing.T) { 360 | t.Run( 361 | "Two colors - two frames", 362 | func(innerT *testing.T) { 363 | colors := []colorful.Color{ 364 | {0, 0, 0}, 365 | {1, 1, 1}, 366 | } 367 | gradient := newGradient(colors) 368 | generated := gradient.generate(2) 369 | 370 | if len(generated) != 2 { 371 | innerT.Errorf("Expected %v but got %v", 2, len(generated)) 372 | } 373 | 374 | if generated[0] != colors[0] { 375 | innerT.Errorf("Expected %v but got %v", colors[0], generated[0]) 376 | } 377 | 378 | if generated[1] != colors[1] { 379 | innerT.Errorf("Expected %v but got %v", colors[1], generated[1]) 380 | } 381 | }, 382 | ) 383 | 384 | t.Run( 385 | "Two colors - three frames", 386 | func(innerT *testing.T) { 387 | colors := []colorful.Color{ 388 | {0, 0, 0}, 389 | {1, 1, 1}, 390 | } 391 | gradient := newGradient(colors) 392 | generated := gradient.generate(3) 393 | 394 | if len(generated) != 3 { 395 | innerT.Errorf("Expected %v but got %v", 2, len(generated)) 396 | } 397 | 398 | if generated[0] != colors[0] { 399 | innerT.Errorf("Expected %v but got %v", colors[0], generated[0]) 400 | } 401 | 402 | if generated[1] == colors[0] || generated[1] == colors[1] { 403 | innerT.Errorf("Expected %v but got %v", nil, generated[0]) 404 | } 405 | 406 | if generated[2] != colors[1] { 407 | innerT.Errorf("Expected %v but got %v", colors[1], generated[2]) 408 | } 409 | }, 410 | ) 411 | 412 | t.Run( 413 | "Two colors - four frames", 414 | func(innerT *testing.T) { 415 | colors := []colorful.Color{ 416 | {0, 0, 0}, 417 | {1, 1, 1}, 418 | } 419 | gradient := newGradient(colors) 420 | generated := gradient.generate(4) 421 | 422 | if len(generated) != 4 { 423 | innerT.Errorf("Expected %v but got %v", 2, len(generated)) 424 | } 425 | 426 | if generated[0] != colors[0] { 427 | innerT.Errorf("Expected %v but got %v", colors[0], generated[0]) 428 | } 429 | 430 | if generated[1] == colors[0] || generated[1] == colors[1] { 431 | innerT.Errorf("Expected %v but got %v", nil, generated[1]) 432 | } 433 | 434 | if generated[2] == colors[0] || generated[2] == colors[1] { 435 | innerT.Errorf("Expected %v but got %v", nil, generated[2]) 436 | } 437 | 438 | if generated[3] != colors[1] { 439 | innerT.Errorf("Expected %v but got %v", colors[1], generated[3]) 440 | } 441 | }, 442 | ) 443 | 444 | t.Run( 445 | "Three colors - two frames", 446 | func(innerT *testing.T) { 447 | colors := []colorful.Color{ 448 | {0, 0, 0}, 449 | {0.5, 0.5, 0.5}, 450 | {1, 1, 1}, 451 | } 452 | gradient := newGradient(colors) 453 | generated := gradient.generate(2) 454 | 455 | if len(generated) != 2 { 456 | innerT.Errorf("Expected %v but got %v", 2, len(generated)) 457 | } 458 | 459 | if generated[0] != colors[0] { 460 | innerT.Errorf("Expected %v but got %v", colors[0], generated[0]) 461 | } 462 | 463 | if generated[1] != colors[2] { 464 | innerT.Errorf("Expected %v but got %v", colors[2], generated[1]) 465 | } 466 | }, 467 | ) 468 | 469 | t.Run( 470 | "Three colors - three frames", 471 | func(innerT *testing.T) { 472 | colors := []colorful.Color{ 473 | {0, 0, 0}, 474 | {0.5, 0.5, 0.5}, 475 | {1, 1, 1}, 476 | } 477 | gradient := newGradient(colors) 478 | generated := gradient.generate(3) 479 | 480 | if len(generated) != 3 { 481 | innerT.Errorf("Expected %v but got %v", 3, len(generated)) 482 | } 483 | 484 | if generated[0] != colors[0] { 485 | innerT.Errorf("Expected %v but got %v", colors[0], generated[0]) 486 | } 487 | 488 | /* 489 | *if generated[1] != colors[1] { 490 | * innerT.Errorf("Expected %v but got %v", colors[1], generated[1]) 491 | *} 492 | */ 493 | 494 | if generated[2] != colors[2] { 495 | innerT.Errorf("Expected %v but got %v", colors[2], generated[2]) 496 | } 497 | }, 498 | ) 499 | 500 | t.Run( 501 | "Three colors - four frames", 502 | func(innerT *testing.T) { 503 | colors := []colorful.Color{ 504 | {0, 0, 0}, 505 | {0.5, 0.5, 0.5}, 506 | {1, 1, 1}, 507 | } 508 | gradient := newGradient(colors) 509 | generated := gradient.generate(4) 510 | 511 | if len(generated) != 4 { 512 | innerT.Errorf("Expected %v but got %v", 3, len(generated)) 513 | } 514 | 515 | /* 516 | *if generated[0] != colors[0] { 517 | * innerT.Errorf("Expected %v but got %v", colors[0], generated[0]) 518 | *} 519 | */ 520 | 521 | if generated[0] != colors[0] { 522 | innerT.Errorf("Expected %v but got %v", colors[0], generated[0]) 523 | } 524 | 525 | if generated[3] != colors[2] { 526 | innerT.Errorf("Expected %v but got %v", colors[2], generated[3]) 527 | } 528 | }, 529 | ) 530 | } 531 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "adler2" 7 | version = "2.0.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 10 | 11 | [[package]] 12 | name = "anstream" 13 | version = "0.6.21" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" 16 | dependencies = [ 17 | "anstyle", 18 | "anstyle-parse", 19 | "anstyle-query", 20 | "anstyle-wincon", 21 | "colorchoice", 22 | "is_terminal_polyfill", 23 | "utf8parse", 24 | ] 25 | 26 | [[package]] 27 | name = "anstyle" 28 | version = "1.0.13" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 31 | 32 | [[package]] 33 | name = "anstyle-parse" 34 | version = "0.2.7" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 37 | dependencies = [ 38 | "utf8parse", 39 | ] 40 | 41 | [[package]] 42 | name = "anstyle-query" 43 | version = "1.1.4" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 46 | dependencies = [ 47 | "windows-sys", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-wincon" 52 | version = "3.0.10" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 55 | dependencies = [ 56 | "anstyle", 57 | "once_cell_polyfill", 58 | "windows-sys", 59 | ] 60 | 61 | [[package]] 62 | name = "approx" 63 | version = "0.5.1" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" 66 | dependencies = [ 67 | "num-traits", 68 | ] 69 | 70 | [[package]] 71 | name = "arrayvec" 72 | version = "0.7.6" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 75 | 76 | [[package]] 77 | name = "autocfg" 78 | version = "1.5.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 81 | 82 | [[package]] 83 | name = "bit_field" 84 | version = "0.10.3" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" 87 | 88 | [[package]] 89 | name = "bitflags" 90 | version = "1.3.2" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 93 | 94 | [[package]] 95 | name = "bytemuck" 96 | version = "1.24.0" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" 99 | 100 | [[package]] 101 | name = "byteorder" 102 | version = "1.5.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 105 | 106 | [[package]] 107 | name = "cfg-if" 108 | version = "1.0.3" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 111 | 112 | [[package]] 113 | name = "clap" 114 | version = "4.5.48" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" 117 | dependencies = [ 118 | "clap_builder", 119 | ] 120 | 121 | [[package]] 122 | name = "clap_builder" 123 | version = "4.5.48" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" 126 | dependencies = [ 127 | "anstream", 128 | "anstyle", 129 | "clap_lex", 130 | "strsim", 131 | ] 132 | 133 | [[package]] 134 | name = "clap_lex" 135 | version = "0.7.5" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 138 | 139 | [[package]] 140 | name = "color_quant" 141 | version = "1.1.0" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 144 | 145 | [[package]] 146 | name = "colorchoice" 147 | version = "1.0.4" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 150 | 151 | [[package]] 152 | name = "crc32fast" 153 | version = "1.5.0" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" 156 | dependencies = [ 157 | "cfg-if", 158 | ] 159 | 160 | [[package]] 161 | name = "crossbeam-deque" 162 | version = "0.8.6" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 165 | dependencies = [ 166 | "crossbeam-epoch", 167 | "crossbeam-utils", 168 | ] 169 | 170 | [[package]] 171 | name = "crossbeam-epoch" 172 | version = "0.9.18" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 175 | dependencies = [ 176 | "crossbeam-utils", 177 | ] 178 | 179 | [[package]] 180 | name = "crossbeam-utils" 181 | version = "0.8.21" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 184 | 185 | [[package]] 186 | name = "crunchy" 187 | version = "0.2.4" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" 190 | 191 | [[package]] 192 | name = "either" 193 | version = "1.15.0" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 196 | 197 | [[package]] 198 | name = "exr" 199 | version = "1.73.0" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" 202 | dependencies = [ 203 | "bit_field", 204 | "half", 205 | "lebe", 206 | "miniz_oxide", 207 | "rayon-core", 208 | "smallvec", 209 | "zune-inflate", 210 | ] 211 | 212 | [[package]] 213 | name = "fdeflate" 214 | version = "0.3.7" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 217 | dependencies = [ 218 | "simd-adler32", 219 | ] 220 | 221 | [[package]] 222 | name = "find-crate" 223 | version = "0.6.3" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" 226 | dependencies = [ 227 | "toml", 228 | ] 229 | 230 | [[package]] 231 | name = "flate2" 232 | version = "1.1.2" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" 235 | dependencies = [ 236 | "crc32fast", 237 | "miniz_oxide", 238 | ] 239 | 240 | [[package]] 241 | name = "gif" 242 | version = "0.12.0" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" 245 | dependencies = [ 246 | "color_quant", 247 | "weezl", 248 | ] 249 | 250 | [[package]] 251 | name = "gif" 252 | version = "0.13.3" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" 255 | dependencies = [ 256 | "color_quant", 257 | "weezl", 258 | ] 259 | 260 | [[package]] 261 | name = "half" 262 | version = "2.6.0" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" 265 | dependencies = [ 266 | "cfg-if", 267 | "crunchy", 268 | ] 269 | 270 | [[package]] 271 | name = "image" 272 | version = "0.24.9" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" 275 | dependencies = [ 276 | "bytemuck", 277 | "byteorder", 278 | "color_quant", 279 | "exr", 280 | "gif 0.13.3", 281 | "jpeg-decoder", 282 | "num-traits", 283 | "png", 284 | "qoi", 285 | "tiff", 286 | ] 287 | 288 | [[package]] 289 | name = "imagequant" 290 | version = "4.4.1" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "caf5d73b959dfbe5d6b5cd3ca8de5265c7bc58297f20560a60a1d2ba6a19991f" 293 | dependencies = [ 294 | "arrayvec", 295 | "once_cell", 296 | "rayon", 297 | "rgb", 298 | "thread_local", 299 | ] 300 | 301 | [[package]] 302 | name = "is_terminal_polyfill" 303 | version = "1.70.1" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 306 | 307 | [[package]] 308 | name = "jpeg-decoder" 309 | version = "0.3.2" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" 312 | dependencies = [ 313 | "rayon", 314 | ] 315 | 316 | [[package]] 317 | name = "lebe" 318 | version = "0.5.3" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" 321 | 322 | [[package]] 323 | name = "miniz_oxide" 324 | version = "0.8.9" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 327 | dependencies = [ 328 | "adler2", 329 | "simd-adler32", 330 | ] 331 | 332 | [[package]] 333 | name = "num-traits" 334 | version = "0.2.19" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 337 | dependencies = [ 338 | "autocfg", 339 | ] 340 | 341 | [[package]] 342 | name = "once_cell" 343 | version = "1.21.3" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 346 | 347 | [[package]] 348 | name = "once_cell_polyfill" 349 | version = "1.70.1" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 352 | 353 | [[package]] 354 | name = "palette" 355 | version = "0.6.1" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "8f9cd68f7112581033f157e56c77ac4a5538ec5836a2e39284e65bd7d7275e49" 358 | dependencies = [ 359 | "approx", 360 | "num-traits", 361 | "palette_derive", 362 | "phf", 363 | ] 364 | 365 | [[package]] 366 | name = "palette_derive" 367 | version = "0.6.1" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "05eedf46a8e7c27f74af0c9cfcdb004ceca158cb1b918c6f68f8d7a549b3e427" 370 | dependencies = [ 371 | "find-crate", 372 | "proc-macro2", 373 | "quote", 374 | "syn 1.0.109", 375 | ] 376 | 377 | [[package]] 378 | name = "phf" 379 | version = "0.11.3" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" 382 | dependencies = [ 383 | "phf_macros", 384 | "phf_shared", 385 | ] 386 | 387 | [[package]] 388 | name = "phf_generator" 389 | version = "0.11.3" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" 392 | dependencies = [ 393 | "phf_shared", 394 | "rand", 395 | ] 396 | 397 | [[package]] 398 | name = "phf_macros" 399 | version = "0.11.3" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" 402 | dependencies = [ 403 | "phf_generator", 404 | "phf_shared", 405 | "proc-macro2", 406 | "quote", 407 | "syn 2.0.106", 408 | ] 409 | 410 | [[package]] 411 | name = "phf_shared" 412 | version = "0.11.3" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" 415 | dependencies = [ 416 | "siphasher", 417 | ] 418 | 419 | [[package]] 420 | name = "png" 421 | version = "0.17.16" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" 424 | dependencies = [ 425 | "bitflags", 426 | "crc32fast", 427 | "fdeflate", 428 | "flate2", 429 | "miniz_oxide", 430 | ] 431 | 432 | [[package]] 433 | name = "proc-macro2" 434 | version = "1.0.101" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 437 | dependencies = [ 438 | "unicode-ident", 439 | ] 440 | 441 | [[package]] 442 | name = "qoi" 443 | version = "0.4.1" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" 446 | dependencies = [ 447 | "bytemuck", 448 | ] 449 | 450 | [[package]] 451 | name = "quote" 452 | version = "1.0.41" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" 455 | dependencies = [ 456 | "proc-macro2", 457 | ] 458 | 459 | [[package]] 460 | name = "rainbowgif" 461 | version = "0.1.0" 462 | dependencies = [ 463 | "clap", 464 | "gif 0.12.0", 465 | "image", 466 | "imagequant", 467 | "palette", 468 | ] 469 | 470 | [[package]] 471 | name = "rand" 472 | version = "0.8.5" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 475 | dependencies = [ 476 | "rand_core", 477 | ] 478 | 479 | [[package]] 480 | name = "rand_core" 481 | version = "0.6.4" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 484 | 485 | [[package]] 486 | name = "rayon" 487 | version = "1.11.0" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" 490 | dependencies = [ 491 | "either", 492 | "rayon-core", 493 | ] 494 | 495 | [[package]] 496 | name = "rayon-core" 497 | version = "1.13.0" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" 500 | dependencies = [ 501 | "crossbeam-deque", 502 | "crossbeam-utils", 503 | ] 504 | 505 | [[package]] 506 | name = "rgb" 507 | version = "0.8.52" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" 510 | dependencies = [ 511 | "bytemuck", 512 | ] 513 | 514 | [[package]] 515 | name = "serde" 516 | version = "1.0.228" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 519 | dependencies = [ 520 | "serde_core", 521 | ] 522 | 523 | [[package]] 524 | name = "serde_core" 525 | version = "1.0.228" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 528 | dependencies = [ 529 | "serde_derive", 530 | ] 531 | 532 | [[package]] 533 | name = "serde_derive" 534 | version = "1.0.228" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 537 | dependencies = [ 538 | "proc-macro2", 539 | "quote", 540 | "syn 2.0.106", 541 | ] 542 | 543 | [[package]] 544 | name = "simd-adler32" 545 | version = "0.3.7" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 548 | 549 | [[package]] 550 | name = "siphasher" 551 | version = "1.0.1" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" 554 | 555 | [[package]] 556 | name = "smallvec" 557 | version = "1.15.1" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 560 | 561 | [[package]] 562 | name = "strsim" 563 | version = "0.11.1" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 566 | 567 | [[package]] 568 | name = "syn" 569 | version = "1.0.109" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 572 | dependencies = [ 573 | "proc-macro2", 574 | "quote", 575 | "unicode-ident", 576 | ] 577 | 578 | [[package]] 579 | name = "syn" 580 | version = "2.0.106" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 583 | dependencies = [ 584 | "proc-macro2", 585 | "quote", 586 | "unicode-ident", 587 | ] 588 | 589 | [[package]] 590 | name = "thread_local" 591 | version = "1.1.9" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 594 | dependencies = [ 595 | "cfg-if", 596 | ] 597 | 598 | [[package]] 599 | name = "tiff" 600 | version = "0.9.1" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" 603 | dependencies = [ 604 | "flate2", 605 | "jpeg-decoder", 606 | "weezl", 607 | ] 608 | 609 | [[package]] 610 | name = "toml" 611 | version = "0.5.11" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" 614 | dependencies = [ 615 | "serde", 616 | ] 617 | 618 | [[package]] 619 | name = "unicode-ident" 620 | version = "1.0.19" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" 623 | 624 | [[package]] 625 | name = "utf8parse" 626 | version = "0.2.2" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 629 | 630 | [[package]] 631 | name = "weezl" 632 | version = "0.1.10" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" 635 | 636 | [[package]] 637 | name = "windows-link" 638 | version = "0.2.0" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" 641 | 642 | [[package]] 643 | name = "windows-sys" 644 | version = "0.60.2" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 647 | dependencies = [ 648 | "windows-targets", 649 | ] 650 | 651 | [[package]] 652 | name = "windows-targets" 653 | version = "0.53.4" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" 656 | dependencies = [ 657 | "windows-link", 658 | "windows_aarch64_gnullvm", 659 | "windows_aarch64_msvc", 660 | "windows_i686_gnu", 661 | "windows_i686_gnullvm", 662 | "windows_i686_msvc", 663 | "windows_x86_64_gnu", 664 | "windows_x86_64_gnullvm", 665 | "windows_x86_64_msvc", 666 | ] 667 | 668 | [[package]] 669 | name = "windows_aarch64_gnullvm" 670 | version = "0.53.0" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 673 | 674 | [[package]] 675 | name = "windows_aarch64_msvc" 676 | version = "0.53.0" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 679 | 680 | [[package]] 681 | name = "windows_i686_gnu" 682 | version = "0.53.0" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 685 | 686 | [[package]] 687 | name = "windows_i686_gnullvm" 688 | version = "0.53.0" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 691 | 692 | [[package]] 693 | name = "windows_i686_msvc" 694 | version = "0.53.0" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 697 | 698 | [[package]] 699 | name = "windows_x86_64_gnu" 700 | version = "0.53.0" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 703 | 704 | [[package]] 705 | name = "windows_x86_64_gnullvm" 706 | version = "0.53.0" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 709 | 710 | [[package]] 711 | name = "windows_x86_64_msvc" 712 | version = "0.53.0" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 715 | 716 | [[package]] 717 | name = "zune-inflate" 718 | version = "0.2.54" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" 721 | dependencies = [ 722 | "simd-adler32", 723 | ] 724 | --------------------------------------------------------------------------------