deltae-0.3.2/.cargo_vcs_info.json0000644000000001360000000000100122740ustar { "git": { "sha1": "3caea55fe378157802187cf66d07169aa5f1c06e" }, "path_in_vcs": "" }deltae-0.3.2/.gitignore000064400000000000000000000000551046102023000130540ustar 00000000000000/target **/*.rs.bk /docs/debug /docs/release deltae-0.3.2/.gitlab-ci.yml000064400000000000000000000023261046102023000135230ustar 00000000000000# This file is a template, and might need editing before it works on your project. # To contribute improvements to CI/CD templates, please follow the Development guide at: # https://docs.gitlab.com/ee/development/cicd/templates.html # This specific template is located at: # https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Rust.gitlab-ci.yml # Official language image. Look for the different tagged releases at: # https://hub.docker.com/r/library/rust/tags/ image: "rust:latest" # Optional: Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. # Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service # services: # - mysql:latest # - redis:latest # - postgres:latest # Optional: Install a C compiler, cmake and git into the container. # You will often need this when you (or any of your dependencies) depends on C code. # before_script: # - apt-get update -yqq # - apt-get install -yqq --no-install-recommends build-essential # Use cargo to test the project test:cargo: script: - rustc --version && cargo --version # Print version info for debugging - cargo test --workspace --verbose deltae-0.3.2/Cargo.lock0000644000000220020000000000100102430ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "anstream" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "342258dd14006105c2b75ab1bd7543a03bdf0cfc94383303ac212a04939dff6f" dependencies = [ "anstyle", "anstyle-parse", "anstyle-wincon", "concolor-override", "concolor-query", "is-terminal", "utf8parse", ] [[package]] name = "anstyle" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23ea9e81bd02e310c216d080f6223c179012256e5151c41db88d12c88a1684d2" [[package]] name = "anstyle-parse" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7d1bb534e9efed14f3e5f44e7dd1a4f709384023a4165199a4241e18dff0116" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-wincon" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3127af6145b149f3287bb9a0d10ad9c5692dba8c53ad48285e5bec4063834fa" dependencies = [ "anstyle", "windows-sys 0.45.0", ] [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "cc" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" [[package]] name = "clap" version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046ae530c528f252094e4a77886ee1374437744b2bff1497aa898bbddbbb29b3" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "223163f58c9a40c3b0a43e1c4b50a9ce09f007ea2cb1ec258a687945b4b7929f" dependencies = [ "anstream", "anstyle", "bitflags", "clap_lex", "strsim", ] [[package]] name = "clap_lex" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" [[package]] name = "concolor-override" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a855d4a1978dc52fb0536a04d384c2c0c1aa273597f08b77c8c4d3b2eec6037f" [[package]] name = "concolor-query" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d11d52c3d7ca2e6d0040212be9e4dbbcd78b6447f535b6b561f449427944cf" dependencies = [ "windows-sys 0.45.0", ] [[package]] name = "deltae" version = "0.3.2" dependencies = [ "clap", ] [[package]] name = "errno" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", "windows-sys 0.48.0", ] [[package]] name = "errno-dragonfly" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" dependencies = [ "cc", "libc", ] [[package]] name = "hermit-abi" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" [[package]] name = "io-lifetimes" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" dependencies = [ "hermit-abi", "libc", "windows-sys 0.48.0", ] [[package]] name = "is-terminal" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ "hermit-abi", "io-lifetimes", "rustix", "windows-sys 0.48.0", ] [[package]] name = "libc" version = "0.2.141" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" [[package]] name = "linux-raw-sys" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" [[package]] name = "rustix" version = "0.37.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", "windows-sys 0.48.0", ] [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "windows-sys" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ "windows-targets 0.42.2", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.0", ] [[package]] name = "windows-targets" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-targets" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ "windows_aarch64_gnullvm 0.48.0", "windows_aarch64_msvc 0.48.0", "windows_i686_gnu 0.48.0", "windows_i686_msvc 0.48.0", "windows_x86_64_gnu 0.48.0", "windows_x86_64_gnullvm 0.48.0", "windows_x86_64_msvc 0.48.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" deltae-0.3.2/Cargo.toml0000644000000015660000000000100103020ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "deltae" version = "0.3.2" authors = ["Ryan O'Beirne "] description = "Calculate Delta E between two colors in CIE Lab space." homepage = "https://gitlab.com/ryanobeirne/deltae" documentation = "https://docs.rs/deltae" readme = "README.md" license = "MIT" repository = "https://gitlab.com/ryanobeirne/deltae.git" [dev-dependencies.clap] version = "4.2.1" deltae-0.3.2/Cargo.toml.orig0000644000000010400000000000100112240ustar [package] name = "deltae" version = "0.3.2" authors = ["Ryan O'Beirne "] description = "Calculate Delta E between two colors in CIE Lab space." edition = "2018" license = "MIT" documentation = "https://docs.rs/deltae" homepage = "https://gitlab.com/ryanobeirne/deltae" repository = "https://gitlab.com/ryanobeirne/deltae.git" readme = "README.md" # [[example]] # name = "deltae" # path = "examples/deltae/deltae.rs" # [[example]] # name = "readme" # path = "examples/readme.rs" [dev-dependencies] clap = "4.2.1" deltae-0.3.2/Cargo.toml.orig000064400000000000000000000010401046102023000137460ustar 00000000000000[package] name = "deltae" version = "0.3.2" authors = ["Ryan O'Beirne "] description = "Calculate Delta E between two colors in CIE Lab space." edition = "2018" license = "MIT" documentation = "https://docs.rs/deltae" homepage = "https://gitlab.com/ryanobeirne/deltae" repository = "https://gitlab.com/ryanobeirne/deltae.git" readme = "README.md" # [[example]] # name = "deltae" # path = "examples/deltae/deltae.rs" # [[example]] # name = "readme" # path = "examples/readme.rs" [dev-dependencies] clap = "4.2.1" deltae-0.3.2/LICENSE000064400000000000000000000020521046102023000120700ustar 00000000000000MIT License Copyright 2019 Ryan O'Beirne Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. deltae-0.3.2/README.md000064400000000000000000000077111046102023000123510ustar 00000000000000# deltae [![Gitlab](https://gitlab.com/ryanobeirne/deltae/badges/master/pipeline.svg?ignore_skipped=true)](https://gitlab.com/ryanobeirne/deltae/pipelines) [![Documentation](https://docs.rs/deltae/badge.svg)](https://docs.rs/deltae) [![Crates.io](https://img.shields.io/crates/v/deltae.svg)](https://crates.io/crates/deltae) [![License](https://img.shields.io/crates/l/deltae.svg)](https://gitlab.com/ryanobeirne/deltae/blob/master/LICENSE) ## Library A rust library for converting colors and calculating [DeltaE](http://www.colorwiki.com/wiki/Delta_E:_The_Color_Difference) (color difference). Check out the documentation here: [Rust API Documentation](https://docs.rs/deltae) ...or compile it yourself: ```sh cargo doc --open ``` ### Examples ```rust use std::error::Error; use deltae::*; fn main() -> Result<(), Box>{ // Lab from a string let lab0: LabValue = "89.73, 1.88, -6.96".parse()?; // Lab directly from values let lab1 = LabValue { l: 95.08, a: -0.17, b: -10.81, }.validate()?; // Validate that the values are in range // Create your own Lab type #[derive(Clone, Copy)] struct MyLab(f32, f32, f32); // Types that implement Into also implement the Delta trait impl From for LabValue { fn from(mylab: MyLab) -> Self { LabValue { l: mylab.0, a: mylab.1, b: mylab.2 } } } let mylab = MyLab(95.08, -0.17, -10.81); // Implement DeltaEq for your own types impl DeltaEq for MyLab {} // Assert that colors are equivalent within a tolerance assert_delta_eq!(mylab, lab1, DE2000, 0.0, "mylab is not equal to lab1!"); // Calculate DeltaE between two lab values let de0 = DeltaE::new(lab0, lab1, DE2000); // Use the Delta trait let de1 = lab0.delta(lab1, DE2000); assert_eq!(de0, de1); // Convert to other color types let lch0 = LchValue::from(lab0); let xyz0 = XyzValue::from(lab1); // If DE2000 is less than 1.0, the colors are considered equivalent assert!(lch0.delta_eq(lab0, DE2000, 1.0)); assert!(xyz0.delta_eq(lab1, DE2000, 1.0)); // Calculate DeltaE between different color types let de2 = lch0.delta(xyz0, DE2000); assert_eq!(de2.round_to(4), de0.round_to(4)); // There is some loss of accuracy in the conversion. // Usually rounding to 4 decimal places is more than enough. // Recalculate DeltaE with different method let de3 = de2.with_method(DE1976); println!("{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n", lab0, // [L:89.73, a:1.88, b:-6.96] lab1, // [L:95.08, a:-0.17, b:-10.81] lch0, // [L:89.73, c:7.2094383, h:285.11572] xyz0, // [X:0.84574246, Y:0.8780792, Z:0.8542397] de0, // 5.316941 de1, // 5.316941 de2, // 5.316937 de3, // 6.902717 ); Ok(()) } ``` --- ## Example The example binary included with this library is a command line application that calculates Delta E between to Lab colors. ### Usage ```txt deltae 0.2.1 Ryan O'Beirne Calculate Delta E between two colors in CIE Lab space. USAGE: deltae [OPTIONS] FLAGS: -h, --help Prints help information -V, --version Prints version information OPTIONS: -c, --color-type Set color type [default: lab] [possible values: lab, lch, xyz] -m, --method Set DeltaE method [default: 2000] [possible values: 2000, 1994, 1994T, CMC1, CMC2, 1976] ARGS: Reference color values Sample color values ``` ### Example ```sh deltae --method=de1976 "89.73, 1.88, -6.96" "95.08, -0.17, -10.81" ``` ### Install ```sh git clone https://gitlab.com/ryanobeirne/deltae cd deltae cargo install --example=deltae --path=. --force ``` ### Notes Calculates DE2000, DE1994 (Graphic Arts and Textiles), DECMC (with a tolerance for lightness and chroma), and DE1976. The Default is DE2000. deltae-0.3.2/examples/deltae/cli.rs000064400000000000000000000020621046102023000152550ustar 00000000000000use clap::{Command, Arg, ArgAction}; use deltae::DEMethod; use std::str::FromStr; pub fn command() -> Command { Command::new("deltae") .version(env!("CARGO_PKG_VERSION")) .about(env!("CARGO_PKG_DESCRIPTION")) .author(env!("CARGO_PKG_AUTHORS")) .arg(Arg::new("METHOD") .help("Set DeltaE method") .long("method") .short('m') .value_parser(DEMethod::from_str) .ignore_case(true) .default_value("2000") .action(ArgAction::Set)) .arg(Arg::new("REFERENCE") .help("Reference color values") .required(true)) .arg(Arg::new("SAMPLE") .help("Sample color values") .required(true)) .arg(Arg::new("COLORTYPE") .help("Set color type") .short('c') .long("color-type") .aliases(["color", "type"]) .default_value("lab") .value_names(["lab", "lch", "xyz"]) .ignore_case(true) .action(ArgAction::Set)) } deltae-0.3.2/examples/deltae/main.rs000064400000000000000000000015711046102023000154360ustar 00000000000000use deltae::*; use std::error::Error; mod cli; fn main() -> Result<(), Box> { //Parse command line arguments with clap let matches = cli::command().get_matches(); let method = matches.get_one::("METHOD").unwrap().to_owned(); let color_type = matches.get_one::("COLORTYPE").unwrap(); let color0 = matches.get_one::("REFERENCE").unwrap(); let color1 = matches.get_one::("SAMPLE").unwrap(); let delta = match color_type.as_str() { "lab" => color0.parse::()?.delta(color1.parse::()?, method), "lch" => color0.parse::()?.delta(color1.parse::()?, method), "xyz" => color0.parse::()?.delta(color1.parse::()?, method), _ => unreachable!("COLORTYPE"), }; println!("{}: {}", delta.method(), delta.value()); Ok(()) } deltae-0.3.2/examples/readme.rs000064400000000000000000000041221046102023000145040ustar 00000000000000use std::error::Error; use deltae::*; fn main() -> Result<(), Box>{ // Lab from a string let lab0: LabValue = "89.73, 1.88, -6.96".parse()?; // Lab directly from values let lab1 = LabValue { l: 95.08, a: -0.17, b: -10.81, }.validate()?; // Validate that the values are in range // Create your own Lab type #[derive(Clone, Copy)] struct MyLab(f32, f32, f32); // Types that implement Into also implement the Delta trait impl From for LabValue { fn from(mylab: MyLab) -> Self { LabValue { l: mylab.0, a: mylab.1, b: mylab.2 } } } let mylab = MyLab(95.08, -0.17, -10.81); // Implement DeltaEq for your own types impl DeltaEq for MyLab {} // Assert that colors are equivalent within a tolerance assert_delta_eq!(mylab, lab1, DE2000, 0.0, "mylab is not equal to lab1!"); // Calculate DeltaE between two lab values let de0 = DeltaE::new(lab0, lab1, DE2000); // Use the Delta trait let de1 = lab0.delta(lab1, DE2000); assert_eq!(de0, de1); // Convert to other color types let lch0 = LchValue::from(lab0); let xyz0 = XyzValue::from(lab1); // If DE2000 is less than 1.0, the colors are considered equivalent assert!(lch0.delta_eq(lab0, DE2000, 1.0)); assert!(xyz0.delta_eq(lab1, DE2000, 1.0)); // Calculate DeltaE between different color types let de2 = lch0.delta(xyz0, DE2000); assert_eq!(de2.round_to(4), de0.round_to(4)); // There is some loss of accuracy in the conversion. // Usually rounding to 4 decimal places is more than enough. // Recalculate DeltaE with different method let de3 = de2.with_method(DE1976); println!("{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n", lab0, // [L:89.73, a:1.88, b:-6.96] lab1, // [L:95.08, a:-0.17, b:-10.81] lch0, // [L:89.73, c:7.2094383, h:285.11572] xyz0, // [X:0.84574246, Y:0.8780792, Z:0.8542397] de0, // 5.316941 de1, // 5.316941 de2, // 5.316937 de3, // 6.902717 ); Ok(()) } deltae-0.3.2/src/color.rs000064400000000000000000000111461046102023000133420ustar 00000000000000//! Manipulate and convert CIE L\*a\*b\* and Lch colors. //! //! # Examples //! //! ``` //! use deltae::*; //! //! let lab0: LabValue = "95.08, -0.17, -10.81".parse().unwrap(); //! let lch0 = LchValue { //! l: 95.08, //! c: 10.811337, //! h: 269.09903, //! }; //! //! assert!(lab0.delta_eq(&lch0, DE2000, 0.01)); //! //! let lch0 = LchValue::from(lab0); //! let lab2 = LabValue::from(lch0); //! //! println!("{}", lch0); // [L:89.73, c:7.2094, h:285.1157] //! //! assert_eq!(lab0.round_to(4), lab2.round_to(4)); //! ``` use std::fmt; use std::error::Error; use crate::*; /// # CIEL\*a\*b\* /// /// The [`LabValue`] is the key component in calculating [`DeltaE`] /// /// | `Value` | `Color` | `Range` | /// |:-------:|:---------------------:|:--------------------:| /// | `L*` | `Light <---> Dark` | `0.0 <---> 100.0` | /// | `a*` | `Green <---> Magenta` | `-128.0 <---> 128.0` | /// | `b*` | `Blue <---> Yellow` | `-128.0 <---> 128.0` | /// #[derive(Debug, Clone, Copy, PartialEq)] pub struct LabValue { /// Lightness pub l: f32, /// Green - Magenta pub a: f32, /// Blue - Yellow pub b: f32, } impl LabValue { /// Returns a result of a LabValue from 3 `f32`s. /// Will return `Err()` if the values are out of range as determined by the [`Validate`] trait. pub fn new(l: f32, a: f32, b: f32) -> ValueResult { LabValue {l, a, b}.validate() } } impl Default for LabValue { fn default() -> LabValue { LabValue { l: 0.0, a: 0.0, b: 0.0 } } } impl fmt::Display for LabValue { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[L:{}, a:{}, b:{}]", self.l, self.a, self.b) } } /// # Lch: Luminance, Chroma, Hue /// /// | `Value` | `Color` | `Range` | /// |:-------:|:--------------------------:|:----------------------:| /// | `L*` | `Light <---> Dark` | `0.0 <---> 100.0` | /// | `c` | `Chroma (Amount of color)` | `0.0 <---> 181.0139` | /// | `h` | `Hue (Degrees)` | `0.0 <---> 360.0°` | /// #[derive(Debug, Clone, Copy, PartialEq)] pub struct LchValue { /// Lightness pub l: f32, /// Chroma pub c: f32, /// Hue (in degrees) pub h: f32, } impl LchValue { /// Returns a result of an LchValue from 3 `f32`s. /// Will return `Err()` if the values are out of range as determined by the [`Validate`] trait. pub fn new(l: f32, c: f32, h: f32) -> ValueResult { LchValue { l, c, h }.validate() } /// Returns the Hue as radians rather than degrees pub fn hue_radians(&self) -> f32 { self.h.to_radians() } } impl Default for LchValue { fn default() -> LchValue { LchValue { l: 0.0, c: 0.0, h: 0.0 } } } impl fmt::Display for LchValue { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[L:{}, c:{}, h:{}]", self.l, self.c, self.h) } } /// # CIE 1931 XYZ /// /// Further Reading: [https://en.wikipedia.org/wiki/CIE_1931_color_space](https://en.wikipedia.org/wiki/CIE_1931_color_space) /// /// | `Value` | `Color` | `Range` | /// |:-------:|:-----------:|:---------------:| /// | `X` | `RGB` | `0.0 <---> 1.0` | /// | `Y` | `Luminance` | `0.0 <---> 1.0` | /// | `Z` | `Blue` | `0.0 <---> 1.0` | /// #[derive(Debug, Clone, Copy, PartialEq)] pub struct XyzValue { /// X Value pub x: f32, /// Y Value pub y: f32, /// Z Value pub z: f32, } impl XyzValue { /// Returns a result of an XyzValue from 3 `f32`s. /// Will return `Err()` if the values are out of range as determined by the [`Validate`] trait. pub fn new(x: f32, y: f32, z:f32) -> ValueResult { XyzValue {x, y, z}.validate() } } impl Default for XyzValue { fn default() -> XyzValue { XyzValue { x: 0.0, y: 0.0, z: 0.0 } } } impl fmt::Display for XyzValue { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[X:{}, Y:{}, Z:{}]", self.x, self.y, self.z) } } #[derive(Debug)] /// Value validation Error type pub enum ValueError { /// The value is outside the acceptable range OutOfBounds, /// The value is formatted incorrectly BadFormat, } impl fmt::Display for ValueError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.description()) } } impl ValueError { fn description(&self) -> &str { match self { ValueError::OutOfBounds => "Value is out of range!", ValueError::BadFormat => "Value is malformed!", } } } impl Error for ValueError {} deltae-0.3.2/src/convert.rs000064400000000000000000000231761046102023000137120ustar 00000000000000use super::*; use std::convert::TryFrom; use std::error::Error; use std::num::ParseFloatError; use std::str::FromStr; // To Lab ///////////////////////////////////////////////////////////////////// impl From for LabValue { fn from(lch: LchValue) -> LabValue { LabValue { l: lch.l, a: lch.c * lch.h.to_radians().cos(), b: lch.c * lch.h.to_radians().sin(), } } } impl From<&LchValue> for LabValue { fn from(lch: &LchValue) -> LabValue { LabValue::from(*lch) } } impl From<&LabValue> for LabValue { fn from(lab: &LabValue) -> LabValue { *lab } } impl From for LabValue { fn from(xyz: XyzValue) -> LabValue { let x = xyz_to_lab_map(xyz.x / 0.9642); let y = xyz_to_lab_map(xyz.y); let z = xyz_to_lab_map(xyz.z / 0.8251); LabValue { l: (116.0 * y) - 16.0, a: 500.0 * (x - y), b: 200.0 * (y - z), } } } impl From<&XyzValue> for LabValue { fn from(xyz: &XyzValue) -> LabValue { LabValue::from(*xyz) } } impl TryFrom<&[f32; 3]> for LabValue { type Error = ValueError; fn try_from(slice: &[f32; 3]) -> ValueResult { LabValue { l: slice[0], a: slice[1], b: slice[2] }.validate() } } impl TryFrom<(f32, f32, f32)> for LabValue { type Error = ValueError; fn try_from(tuple: (f32, f32, f32)) -> ValueResult { LabValue { l: tuple.0, a: tuple.1, b: tuple.2, }.validate() } } impl TryFrom<&(f32, f32, f32)> for LabValue { type Error = ValueError; fn try_from(tuple: &(f32, f32, f32)) -> ValueResult { LabValue { l: tuple.0, a: tuple.1, b: tuple.2, }.validate() } } // To Lch ///////////////////////////////////////////////////////////////////// impl From for LchValue { fn from(lab: LabValue) -> LchValue { LchValue { l: lab.l, c: ( lab.a.powi(2) + lab.b.powi(2) ).sqrt(), h: get_h_prime(lab.a, lab.b), } } } impl From<&LabValue> for LchValue { fn from(lab: &LabValue) -> LchValue { LchValue::from(*lab) } } impl From for LchValue { fn from(xyz: XyzValue) -> LchValue { LchValue::from(LabValue::from(xyz)) } } impl From<&XyzValue> for LchValue { fn from(xyz: &XyzValue) -> LchValue { LchValue::from(*xyz) } } impl TryFrom<&[f32; 3]> for LchValue { type Error = ValueError; fn try_from(slice: &[f32; 3]) -> ValueResult { LchValue { l: slice[0], c: slice[1], h: slice[2] }.validate() } } impl TryFrom<(f32, f32, f32)> for LchValue { type Error = ValueError; fn try_from(tuple: (f32, f32, f32)) -> ValueResult { LchValue { l: tuple.0, c: tuple.1, h: tuple.2, }.validate() } } impl TryFrom<&(f32, f32, f32)> for LchValue { type Error = ValueError; fn try_from(tuple: &(f32, f32, f32)) -> ValueResult { LchValue { l: tuple.0, c: tuple.1, h: tuple.2, }.validate() } } // To Xyz ///////////////////////////////////////////////////////////////////// impl From for XyzValue { fn from(lab: LabValue) -> XyzValue { let fy = (lab.l + 16.0) / 116.0; let fx = (lab.a / 500.0) + fy; let fz = fy - (lab.b / 200.0); let xr = if fx > CBRT_EPSILON as f32 { fx.powi(3) } else { ((fx * 116.0) - 16.0) / KAPPA }; let yr = if lab.l > EPSILON * KAPPA { fy.powi(3) } else { lab.l / KAPPA }; let zr = if fz > CBRT_EPSILON as f32 { fz.powi(3) } else { ((fz * 116.0) - 16.0) / KAPPA }; XyzValue { x: xr * 0.9642, y: yr, z: zr * 0.8251, } } } impl From<&LabValue> for XyzValue { fn from(lab: &LabValue) -> XyzValue { XyzValue::from(*lab) } } impl From for XyzValue { fn from(lch: LchValue) -> XyzValue { XyzValue::from(LabValue::from(lch)) } } impl From<&LchValue> for XyzValue { fn from(lch: &LchValue) -> XyzValue { XyzValue::from(*lch) } } impl TryFrom<&[f32; 3]> for XyzValue { type Error = ValueError; fn try_from(slice: &[f32; 3]) -> ValueResult { XyzValue { x: slice[0], y: slice[1], z: slice[2] }.validate() } } impl TryFrom<(f32, f32, f32)> for XyzValue { type Error = ValueError; fn try_from(tuple: (f32, f32, f32)) -> ValueResult { XyzValue { x: tuple.0, y: tuple.1, z: tuple.2, }.validate() } } impl TryFrom<&(f32, f32, f32)> for XyzValue { type Error = ValueError; fn try_from(tuple: &(f32, f32, f32)) -> ValueResult { XyzValue { x: tuple.0, y: tuple.1, z: tuple.2, }.validate() } } // FromStr //////////////////////////////////////////////////////////////////// impl FromStr for DEMethod { type Err = ParseDEMethodError; fn from_str(s: &str) -> Result { Ok(match s.to_lowercase().trim() { "de2000" | "de00" | "2000" | "00" => DEMethod::DE2000, "de1976" | "de76" | "1976" | "76" => DEMethod::DE1976, "de1994" | "de94" | "1994" | "94" | "de1994g" | "de94g" | "1994g" | "94g" => DEMethod::DE1994G, "de1994t" | "de94t" | "1994t" | "94t" => DEMethod::DE1994T, "decmc" | "decmc1"| "cmc1" | "cmc" => DEMethod::DECMC(1.0, 1.0), "decmc2" | "cmc2" => DEMethod::DECMC(2.0, 1.0), method => if method.contains("cmc") { let mut split = method .split('c') .nth(2) .ok_or(Self::Err::Other)? .split(':') .map(|word| word.replace(['(', ')'], "")); let tl = split.next() .ok_or(Self::Err::MissingTlValue)? .parse() .map_err(Self::Err::InvalidValue)?; let tc = split.next() .ok_or(Self::Err::MissingTcValue)? .parse() .map_err(Self::Err::InvalidValue)?; DECMC(tl, tc) } else { return Err(Self::Err::Other); } }) } } #[test] fn parse_cmc_method() { let cmc_methods = [ "decmc", "decmc1", "decmc2", "cmc1", "cmc2", "cmc", "cmc1:1", "cmc2:1", "cmc(2.0:2.0)", "cmc(1:2)", "cmc(1.5:2.5)", ]; for i in cmc_methods { let method = i.parse().unwrap(); matches!(method, DEMethod::DECMC(_,_)); } } #[derive(Debug)] pub enum ParseDEMethodError { MissingTlValue, MissingTcValue, InvalidValue(ParseFloatError), Other, } impl Error for ParseDEMethodError {} impl fmt::Display for ParseDEMethodError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::MissingTlValue => write!(f, "CMC missing lightness tolerance"), Self::MissingTcValue => write!(f, "CMC missing chroma tolerance"), Self::InvalidValue(e) => write!(f, "invalid value: {e}"), Self::Other => write!(f, "unable to parse DEMethod"), } } } impl FromStr for LabValue { type Err = ValueError; fn from_str(s: &str) -> ValueResult { let split = parse_str_to_vecf32(s, 3)?; LabValue { l: split[0], a: split[1], b: split[2], }.validate() } } impl FromStr for LchValue { type Err = ValueError; fn from_str(s: &str) -> ValueResult { let split = parse_str_to_vecf32(s, 3)?; LchValue { l: split[0], c: split[1], h: split[2], }.validate() } } impl FromStr for XyzValue { type Err = ValueError; fn from_str(s: &str) -> ValueResult { let split = parse_str_to_vecf32(s, 3)?; XyzValue { x: split[0], y: split[1], z: split[2], }.validate() } } // Helper Functions //////////////////////////////////////////////////////////// const KAPPA: f32 = 24389.0 / 27.0; // CIE Standard: 903.3 const EPSILON: f32 = 216.0 / 24389.0; // CIE Standard: 0.008856 const CBRT_EPSILON: f64 = 0.20689655172413796; pub fn get_h_prime(a: f32, b: f32) -> f32 { let h_prime = b.atan2(a).to_degrees(); if h_prime < 0.0 { h_prime + 360.0 } else { h_prime } } // Validate and convert strings to `LabValue`. // Split string by comma (92.5,33.5,-18.8). fn parse_str_to_vecf32(s: &str, length: usize) -> ValueResult> { let collection: Vec<&str> = s.split(',').collect(); // Allow extraneous whitespace ("92.5, 33.5, -18.8") let mut v: Vec<&str> = Vec::new(); for item in collection.iter() { if !item.is_empty() { v.push(item.trim()); } } // Parse the f32's into a Vec let split: Vec = v.iter().filter_map(|s| s.parse().ok()).collect(); // Check if it's the right number of items if v.len() != length || split.len() != length { return Err(ValueError::BadFormat); } Ok(split) } #[inline] fn xyz_to_lab_map(c: f32) -> f32 { if c > EPSILON { c.powf(1.0/3.0) } else { (KAPPA * c + 16.0) / 116.0 } } deltae-0.3.2/src/delta.rs000064400000000000000000000136311046102023000133160ustar 00000000000000use super::*; /// Trait to determine color difference between various types. /// As long as the type can be converted to Lab, we can calculate DeltaE. pub trait Delta: Into { /// Calculate DeltaE between 2 types /// ``` /// use deltae::*; /// /// let lch = LchValue::new(60.3, 89.2, 270.0).unwrap(); /// let xyz = XyzValue::new(0.347, 0.912, 0.446).unwrap(); /// let de = lch.delta(xyz, DE1976); /// assert_eq!(de, 180.18364); /// ``` #[inline] fn delta>(self, other: L, method: DEMethod) -> DeltaE { let reference: LabValue = self.into(); let sample: LabValue = other.into(); let value = match method { DEMethod::DE1976 => delta_e_1976(&reference, &sample), DEMethod::DE1994T => delta_e_1994(&reference, &sample, true), DEMethod::DE1994G => delta_e_1994(&reference, &sample, false), DEMethod::DE2000 => delta_e_2000(&reference, &sample), DEMethod::DECMC(t_l, t_c) => delta_e_cmc(&reference, &sample, t_l, t_c), }; DeltaE { value, method, reference, sample } } } impl> Delta for T {} /// DeltaE 1976. Basic euclidian distance formula. #[inline] fn delta_e_1976(lab_0: &LabValue, lab_1: &LabValue) -> f32 { ( (lab_0.l - lab_1.l).powi(2) + (lab_0.a - lab_1.a).powi(2) + (lab_0.b - lab_1.b).powi(2) ).sqrt() } /// DeltaE 1994. Weighted for textiles (`true`) or graphics (`false`) #[inline] fn delta_e_1994(lab_0: &LabValue, lab_1: &LabValue, textiles: bool) -> f32 { let delta_l = lab_0.l - lab_1.l; let chroma_0 = (lab_0.a.powi(2) + lab_0.b.powi(2)).sqrt(); let chroma_1 = (lab_1.a.powi(2) + lab_1.b.powi(2)).sqrt(); let delta_chroma = chroma_0 - chroma_1; let delta_a = lab_0.a - lab_1.a; let delta_b = lab_0.b - lab_1.b; let delta_hue = (delta_a.powi(2) + delta_b.powi(2) - delta_chroma.powi(2)).sqrt(); let (kl, k1, k2) = match textiles { true => (2.0, 0.048, 0.014), false => (1.0, 0.045, 0.015), }; let s_l = 1.0; let s_c = 1.0 + k1 * chroma_0; let s_h = 1.0 + k2 * chroma_0; ((delta_l / kl * s_l).powi(2) + (delta_chroma / s_c).powi(2) + (delta_hue / s_h).powi(2)).sqrt() } /// DeltaE 2000. This is a ridiculously complicated formula. #[inline] fn delta_e_2000(lab_0: &LabValue, lab_1: &LabValue) -> f32 { let chroma_0 = (lab_0.a.powi(2) + lab_0.b.powi(2)).sqrt(); let chroma_1 = (lab_1.a.powi(2) + lab_1.b.powi(2)).sqrt(); let c_bar = (chroma_0 + chroma_1) / 2.0; let g = 0.5 * (1.0 - ( c_bar.powi(7) / (c_bar.powi(7) + 25_f32.powi(7)) ).sqrt()); let a_prime_0 = lab_0.a * (1.0 + g); let a_prime_1 = lab_1.a * (1.0 + g); let c_prime_0 = (a_prime_0.powi(2) + lab_0.b.powi(2)).sqrt(); let c_prime_1 = (a_prime_1.powi(2) + lab_1.b.powi(2)).sqrt(); let l_bar_prime = (lab_0.l + lab_1.l)/2.0; let c_bar_prime = (c_prime_0 + c_prime_1) / 2.0; let h_prime_0 = convert::get_h_prime(a_prime_0, lab_0.b); let h_prime_1 = convert::get_h_prime(a_prime_1, lab_1.b); let h_bar_prime = if (h_prime_0 - h_prime_1).abs() > 180.0 { if (h_prime_0 - h_prime_1) < 360.0 { (h_prime_0 + h_prime_1 + 360.0) / 2.0 } else { (h_prime_0 + h_prime_1 - 360.0) / 2.0 } } else { (h_prime_0 + h_prime_1) / 2.0 }; let t = 1.0 - 0.17 * (( h_bar_prime - 30.0).to_radians()).cos() + 0.24 * ((2.0 * h_bar_prime ).to_radians()).cos() + 0.32 * ((3.0 * h_bar_prime + 6.0).to_radians()).cos() - 0.20 * ((4.0 * h_bar_prime - 63.0).to_radians()).cos(); let mut delta_h = h_prime_1 - h_prime_0; if delta_h > 180.0 && h_prime_1 <= h_prime_0 { delta_h += 360.0; } else if delta_h > 180.0 { delta_h -= 360.0; }; let delta_l_prime = lab_1.l - lab_0.l; let delta_c_prime = c_prime_1 - c_prime_0; let delta_h_prime = 2.0 * (c_prime_0 * c_prime_1).sqrt() * (delta_h.to_radians() / 2.0).sin(); let s_l = 1.0 + ( (0.015 * (l_bar_prime - 50.0).powi(2)) / (20.00 + (l_bar_prime - 50.0).powi(2)).sqrt() ); let s_c = 1.0 + 0.045 * c_bar_prime; let s_h = 1.0 + 0.015 * c_bar_prime * t; let delta_theta = 30.0 * (-((h_bar_prime - 275.0)/25.0).powi(2)).exp(); let r_c = 2.0 * (c_bar_prime.powi(7)/(c_bar_prime.powi(7) + 25_f32.powi(7))).sqrt(); let r_t = -(r_c * (2.0 * delta_theta.to_radians()).sin()); let k_l = 1.0; let k_c = 1.0; let k_h = 1.0; ( (delta_l_prime/(k_l*s_l)).powi(2) + (delta_c_prime/(k_c*s_c)).powi(2) + (delta_h_prime/(k_h*s_h)).powi(2) + (r_t * (delta_c_prime/(k_c*s_c)) * (delta_h_prime/(k_h*s_h))) ).sqrt() } /// Custom weighted DeltaE formula #[inline] fn delta_e_cmc(lab0: &LabValue, lab1: &LabValue, tolerance_l: f32, tolerance_c: f32) -> f32 { let chroma_0 = (lab0.a.powi(2) + lab0.b.powi(2)).sqrt(); let chroma_1 = (lab1.a.powi(2) + lab1.b.powi(2)).sqrt(); let delta_c = chroma_0 - chroma_1; let delta_l = lab0.l - lab1.l; let delta_a = lab0.a - lab1.a; let delta_b = lab0.b - lab1.b; let delta_h = (delta_a.powi(2) + delta_b.powi(2) - delta_c.powi(2)).sqrt(); let s_l = if lab0.l < 16.0 { 0.511 } else { (0.040975 * lab0.l) / (1.0 + (0.01765 * lab0.l)) }; let s_c = ((0.0638 * chroma_0) / (1.0 + (0.0131 * chroma_0))) + 0.638; let h = lab0.b.atan2(lab0.a).to_degrees(); let h_1 = if h >= 0.0 { h } else { h + 360.0 }; let f = (chroma_0.powi(4) / (chroma_0.powi(4) + 1900.0)).sqrt(); let t = if (164.0..345.0).contains(&h_1) { 0.56 + (0.2 * (h_1 + 168.0).to_radians().cos()).abs() } else { 0.36 + (0.4 * (h_1 + 35.0).to_radians().cos()).abs() }; let s_h = s_c * (f * t + 1.0 - f); ( (delta_l / (tolerance_l * s_l)).powi(2) + (delta_c / (tolerance_c * s_c)).powi(2) + (delta_h / s_h).powi(2) ) .sqrt() } deltae-0.3.2/src/eq.rs000064400000000000000000000106021046102023000126250ustar 00000000000000//! ## `Tolerance` and `DeltaEq` traits //! //! This module deals with comparing two colors by [`DeltaE`] within a certain [`Tolerance`]. //! //! See also: [`assert_delta_eq`] //! //! ### Implementing `Tolerance` and `DeltaEq` //! //! ``` //! use deltae::*; //! //! struct MyTolerance(f32); //! //! impl Tolerance for MyTolerance { //! fn tolerance(self) -> f32 { //! self.0 //! } //! } //! //! #[derive(Copy, Clone)] //! struct MyLab(f32, f32, f32); //! //! // Types that implement Into also implement the Delta trait //! impl From for LabValue { //! fn from(mylab: MyLab) -> LabValue { //! LabValue { //! l: mylab.0, //! a: mylab.1, //! b: mylab.2, //! } //! } //! } //! //! impl DeltaEq for MyLab {} //! //! let mylab = MyLab(89.73, 1.88, -6.96); //! let lab = LabValue::new(89.73, 1.88, -6.96).unwrap(); //! let de2000 = mylab.delta(lab, DEMethod::DE2000); //! assert!(mylab.delta_eq(&lab, DE1976, 0.0)); //! ``` use crate::*; /// Trait to determine whether two values are within a certain tolerance of [`DeltaE`]. Types that /// implement Into<[`LabValue`]> implicitly implement [`Delta`]. Types that implement [`Delta`] and /// [`Copy`] may also implement DeltaEq for other types that also implement [`Delta`] and [`Copy`]. /// ``` /// use deltae::*; /// /// #[derive(Copy, Clone)] /// struct MyLab(f32, f32, f32); /// /// // Types that implement Into implicitly implement the Delta trait /// impl From for LabValue { /// fn from(mylab: MyLab) -> LabValue { /// LabValue { /// l: mylab.0, /// a: mylab.1, /// b: mylab.2, /// } /// } /// } /// /// // Types that implement Delta and Copy may also implement DeltaEq for other types that also /// // implement Delta and Copy /// impl DeltaEq for MyLab {} /// /// let mylab = MyLab(89.73, 1.88, -6.96); /// let lab = LabValue::new(89.73, 1.88, -6.96).unwrap(); /// let de2000 = mylab.delta(lab, DEMethod::DE2000); /// assert!(mylab.delta_eq(&lab, DE1976, 0.0)); /// ``` pub trait DeltaEq: Delta + Copy { /// Return true if the value is less than or equal to the [`Tolerance`] fn delta_eq(&self, other: D, method: DEMethod, tolerance: T) -> bool { self.delta(other, method).value() <= &tolerance.tolerance() } } /// Convenience macro for asserting two values are equivalent within a tolerance /// ``` /// use deltae::*; /// /// let lab0 = LabValue::new(50.0, 0.0, 0.0).unwrap(); /// let lab1 = LabValue::new(50.1, 0.1, 0.1).unwrap(); /// /// // Assert that the difference between lab0 and lab1 is less than 1.0 DE2000 /// assert_delta_eq!(lab0, lab1, DE2000, 1.0); /// ``` #[macro_export] macro_rules! assert_delta_eq { ($reference:expr, $sample:expr, $method:expr, $tolerance:expr) => { assert!($reference.delta_eq($sample, $method, $tolerance)) }; ($reference:expr, $sample:expr, $method:expr, $tolerance:expr, $($message:tt)*) => { assert!($reference.delta_eq($sample, $method, $tolerance), $($message)*) }; } /// Convenience macro for asserting two values are not equivalent within a tolerance /// ``` /// use deltae::*; /// /// let lab0 = LabValue::new(50.0, 0.0, 0.0).unwrap(); /// let lab1 = LabValue::new(50.1, 1.0, 1.0).unwrap(); /// /// // Assert that the difference between lab0 and lab1 is greater than 1.0 DE2000 /// assert_delta_ne!(lab0, lab1, DE2000, 1.0); /// ``` #[macro_export] macro_rules! assert_delta_ne { ($reference:expr, $sample:expr, $method:expr, $tolerance:expr) => { assert!(!$reference.delta_eq($sample, $method, $tolerance)) }; ($reference:expr, $sample:expr, $method:expr, $tolerance:expr, $($message:tt)*) => { assert!(!$reference.delta_eq($sample, $method, $tolerance), $($message)*) }; } /// Trait to define a tolerance value for the [`DeltaEq`] trait pub trait Tolerance { /// Return a tolerance value fn tolerance(self) -> f32; } impl Tolerance for f32 { fn tolerance(self) -> f32 { self } } impl Tolerance for f64 { fn tolerance(self) -> f32 { self as f32 } } impl Tolerance for DeltaE { fn tolerance(self) -> f32 { self.value } } macro_rules! impl_delta_eq { ($t:ty) => { impl DeltaEq for $t {} } } impl_delta_eq!(LabValue); impl_delta_eq!(LchValue); impl_delta_eq!(XyzValue); deltae-0.3.2/src/lib.rs000064400000000000000000000163671046102023000130040ustar 00000000000000#![warn(missing_docs)] //! Calculate [Delta E](http://www.colorwiki.com/wiki/Delta_E:_The_Color_Difference) //! (color difference) between two colors in CIE Lab space. //! //! # Examples //! //! ``` //! use std::error::Error; //! use deltae::*; //! //! fn main() -> Result<(), Box>{ //! // Lab from a string //! let lab0: LabValue = "89.73, 1.88, -6.96".parse()?; //! // Lab directly from values //! let lab1 = LabValue { //! l: 95.08, //! a: -0.17, //! b: -10.81, //! }.validate()?; // Validate that the values are in range //! //! // Create your own Lab type //! #[derive(Clone, Copy)] //! struct MyLab(f32, f32, f32); //! //! // Types that implement Into also implement the Delta trait //! impl From for LabValue { //! fn from(mylab: MyLab) -> Self { //! LabValue { l: mylab.0, a: mylab.1, b: mylab.2 } //! } //! } //! let mylab = MyLab(95.08, -0.17, -10.81); //! //! // Implement DeltaEq for your own types //! impl DeltaEq for MyLab {} //! //! // Assert that colors are equivalent within a tolerance //! assert_delta_eq!(mylab, lab1, DE2000, 0.0, "mylab is not equal to lab1!"); //! //! // Calculate DeltaE between two lab values //! let de0 = DeltaE::new(lab0, lab1, DE2000); //! // Use the Delta trait //! let de1 = lab0.delta(lab1, DE2000); //! assert_eq!(de0, de1); //! //! // Convert to other color types //! let lch0 = LchValue::from(lab0); //! let xyz0 = XyzValue::from(lab1); //! // If DE2000 is less than 1.0, the colors are considered equivalent //! assert!(lch0.delta_eq(lab0, DE2000, 1.0)); //! assert!(xyz0.delta_eq(lab1, DE2000, 1.0)); //! //! // Calculate DeltaE between different color types //! let de2 = lch0.delta(xyz0, DE2000); //! assert_eq!(de2.round_to(4), de0.round_to(4)); //! // There is some loss of accuracy in the conversion. //! // Usually rounding to 4 decimal places is more than enough. //! //! // Recalculate DeltaE with different method //! let de3 = de2.with_method(DE1976); //! //! println!("{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n", //! lab0, // [L:89.73, a:1.88, b:-6.96] //! lab1, // [L:95.08, a:-0.17, b:-10.81] //! lch0, // [L:89.73, c:7.2094383, h:285.11572] //! xyz0, // [X:0.84574246, Y:0.8780792, Z:0.8542397] //! de0, // 5.316941 //! de1, // 5.316941 //! de2, // 5.316937 //! de3, // 6.902717 //! ); //! //! Ok(()) //! } //! ``` pub mod color; mod convert; mod delta; pub mod eq; mod round; mod validate; #[cfg(test)] mod tests; pub use DEMethod::*; pub use color::*; pub use delta::*; pub use eq::*; pub use round::*; pub use validate::*; use std::fmt; pub(crate) type ValueResult = Result; /// ## The measured difference between two colors /// /// There are many different methods of calculating color difference. Different methods have a /// specific purpose, mainly in determining the level of tolerance for describing the difference /// between two colors. Regardless of the [`DEMethod`] used, [`DeltaE`] is always calculated based on the /// [`LabValue`]s of the two colors. #[derive(Debug, Clone, Copy)] pub struct DeltaE { /// The mathematical method used for calculating color difference method: DEMethod, /// The calculated Delta E value value: f32, /// The reference color reference: LabValue, /// The sample color sample: LabValue, } impl DeltaE { /// New [`DeltaE`] from two colors and a [`DEMethod`]. /// ``` /// use deltae::{LabValue, DeltaE, DEMethod::DE2000}; /// /// let lab0 = LabValue::new(89.73, 1.88, -6.96).unwrap(); /// let lab1 = LabValue::new(95.08, -0.17, -10.81).unwrap(); /// let de0 = DeltaE::new(&lab0, &lab1, DE2000); /// assert_eq!(de0, 5.316941); /// ``` #[inline] pub fn new(a: A, b: B, method: DEMethod) -> DeltaE where A: Delta, B: Delta { a.delta(b, method) } /// Recalculate [`DeltaE`] with another [`DEMethod`] /// ``` /// use deltae::{Delta, DeltaE, LabValue, DEMethod}; /// /// let lab0 = LabValue::new(89.73, 1.88, -6.96).unwrap(); /// let lab1 = LabValue::new(95.08, -0.17, -10.81).unwrap(); /// let de2000 = lab0.delta(lab1, DEMethod::DE2000); /// let de1976 = de2000.with_method(DEMethod::DE1976); /// assert_eq!(de1976, 6.902716); /// ``` #[inline] pub fn with_method(self, method: DEMethod) -> Self { self.reference.delta(self.sample, method) } /// Return a reference to the [`DeltaE`] method used in the calculation pub fn method(&self) -> &DEMethod { &self.method } /// Return a reference to the [`DeltaE`] value pub fn value(&self) -> &f32 { &self.value } /// Return a reference to the reference [`LabValue`] used in the calculation. A reference color /// is the base color to which the sample color is being compared. pub fn reference(&self) -> &LabValue { &self.reference } /// Return a reference to the sample [`LabValue`] used in the calculation. A sample color is /// the color being compared to the reference color. pub fn sample(&self) -> &LabValue { &self.sample } } impl fmt::Display for DeltaE { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", &self.value) } } impl PartialEq for DeltaE { fn eq(&self, f: &f32) -> bool { &self.value == f } } impl PartialEq for DeltaE { fn eq(&self, other: &Self) -> bool { self.value == other.value } } /// One should be careful when ordering DeltaE. A `DE2000:1.0` value is not /// necessarily the same amount of color difference as a amount of color /// difference `DE1976:1.0` value. impl PartialOrd for DeltaE { fn partial_cmp(&self, other: &Self) -> Option { self.value.partial_cmp(&other.value) } } /// The most common DeltaE methods #[derive(Debug, PartialEq, Clone, Copy, Default)] pub enum DEMethod{ /// The defacto standard DeltaE method, weighted to account for human color vision tolerances #[default] DE2000, /// An implementation of DeltaE with separate tolerances for Lightness and Chroma DECMC( /// Lightness tolerance f32, /// Chroma tolerance f32, ), /// CIE94 DeltaE implementation, weighted with a tolerance for graphics DE1994G, /// CIE94 DeltaE implementation, weighted with a tolerance for textiles DE1994T, /// The original DeltaE implementation, a basic euclidian distance formula DE1976, } /// DeltaE CMC (1:1) pub const DECMC1: DEMethod = DECMC(1.0, 1.0); /// DeltaE CMC (2:1) pub const DECMC2: DEMethod = DECMC(2.0, 1.0); impl Eq for DEMethod {} impl fmt::Display for DEMethod { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { DECMC(tl, tc) => { if (tl, tc) == (&1.0, &1.0) { write!(f, "DECMC1") } else if (tl, tc) == (&2.0, &1.0) { write!(f, "DECMC2") } else { write!(f, "DECMC({:0.2}:{:0.2})", tl, tc) } } _ => write!(f, "{:?}", self) } } } deltae-0.3.2/src/round.rs000064400000000000000000000025261046102023000133550ustar 00000000000000use super::*; /// Trait for rounding values to a number of decimal places pub trait Round { /// Rounds the value to a number of decimal places fn round_to(self, places: i32) -> Self; } // Round an f32 to a number of decimal places fn round_to(val: f32, places: i32) -> f32 { let mult = 10_f32.powi(places); (val * mult).round() / mult } impl Round for DeltaE { fn round_to(self, places: i32) -> Self { Self { value: round_to(self.value, places), ..self } } } impl Round for LabValue { fn round_to(self, places: i32) -> LabValue { Self { l: round_to(self.l, places), a: round_to(self.a, places), b: round_to(self.b, places), } } } impl Round for LchValue { fn round_to(self, places: i32) -> LchValue { Self { l: round_to(self.l, places), c: round_to(self.c, places), h: round_to(self.h, places), } } } impl Round for XyzValue { fn round_to(self, places: i32) -> XyzValue { Self { x: round_to(self.x, places), y: round_to(self.y, places), z: round_to(self.z, places), } } } #[test] fn round() { let val = 1.2345679; let rnd = round::round_to(val, 4); assert_eq!(rnd, 1.2346); assert_ne!(rnd, val); } deltae-0.3.2/src/tests.rs000064400000000000000000000162531046102023000133720ustar 00000000000000use super::*; use color::{LabValue, LchValue, XyzValue}; use std::convert::TryFrom; #[test] fn lab_to_lch() { let lab = LabValue { l: 30.0, a: 40.0, b: 50.0, }; let lch = LchValue::from(lab); let lab2 = LabValue::from(lch); assert_eq!(lab.round_to(4), lab2.round_to(4)); } #[test] fn lch_to_lab() { let lch = LchValue { l: 30.0, c: 40.0, h: 50.0, }; let lab = LabValue::from(lch); let lch2 = LchValue::from(lab); assert_eq!(lch.round_to(4), lch2.round_to(4)); } #[test] fn lab_to_xyz() { let lab = LabValue { l: 30.0, a: 40.0, b: 50.0, }; let xyz = XyzValue::from(lab); let lab2 = LabValue::from(xyz); assert_eq!(lab.round_to(4), lab2.round_to(4)); } #[test] fn lab_string() { let good = &[ "100,128,-128", "100,-128,128", "100, -128, 128", "0,0,0", "0,1,-1", "50,-1,-1", "99.9999,127.9999,-127.9999", ]; for i in good { assert!(i.parse::().is_ok()); } let bad = &[ "100,128,-129", "101,129,129", "101, 129, 129", "derp", "1,2,three,4", "", "1,2,3,4", "1,2", "1", "1,2,3,derp" ]; for i in bad { assert!(i.parse::().is_err()); } } #[test] fn lch_string() { let good = &[ "100,181.0193,360", "100, 181.0193, 360", "100,129,129", "0,0,0", "99.9999,181.0193,359.9999", ]; for i in good { assert!(i.parse::().is_ok()); } let bad = &[ "100,128,-129", "100,181.0194,360", "100, 181.0194, 360", "0,-0.01,-0.01", "derp", "1,2,three,4", "", "1,2,3,4", "1,2", "1", "1,2,3,derp" ]; for i in bad { assert!(i.parse::().is_err()); } } #[test] fn xyz_string() { let good = &[ "0, 0, 0", "1, 1, 1", "0.5, 0.5, 0.5" ]; for i in good { assert!(i.parse::().is_ok()); } let bad = &[ "-0.01, 0, 0", "0, 1.01, 0", "0, 0, 1.01", "derp", "0, 0, 0, derp", "0, 0, derp" ]; for i in bad { assert!(i.parse::().is_err()); } } fn compare_de(method: DEMethod, expected: f32, reference: &[f32; 3], sample: &[f32; 3]) -> ValueResult<()> { let lab0 = LabValue::try_from(reference)?; let lab1 = LabValue::try_from(sample)?; let de = lab0.delta(lab1, method).round_to(4).value; assert_eq!(expected, de); Ok(()) } #[test] fn decmc1() { assert!(compare_de(DEMethod::DECMC(1.0, 1.0), 17.4901, &[20.0, 30.0, 40.0], &[30.0, 40.0, 50.0]).is_ok()); } #[test] fn decmc2() { assert!(compare_de(DEMethod::DECMC(2.0, 1.0), 10.0731, &[20.0, 30.0, 40.0], &[30.0, 40.0, 50.0]).is_ok()); } #[test] fn de1976_test_set() { let set = &[ (0.0000, &[0.0000, 0.0000, 0.0000 ], &[0.0000, 0.0000, 0.0000 ]), (5.0000, &[0.0000, 0.0000, 0.0000 ], &[0.0000, 3.0000, 4.0000 ]), (5.0000, &[0.0000, 0.0000, 0.0000 ], &[0.0000, -3.0000, -4.0000 ]), (50.0000, &[0.0000, 0.0000, 0.0000 ], &[0.0000, -30.0000, -40.0000 ]), (181.0193, &[0.0000, 0.0000, 0.0000 ], &[0.0000, 128.0000, 128.0000]), (362.0387, &[0.0000, -128.0000, -128.0000], &[0.0000, 128.0000, 128.0000]), (375.5955, &[0.0000, -128.0000, -128.0000], &[100.0000, 128.0000, 128.0000]) ]; for (expected, reference, sample) in set.iter() { assert!(compare_de(DEMethod::DE1976, *expected, reference, sample).is_ok()); } } // Tests taken from Table 1: "CIEDE2000 total color difference test data" of // "The CIEDE2000 Color-Difference Formula: Implementation Notes, // Supplementary Test Data, and Mathematical Observations" by Gaurav Sharma, // Wencheng Wu and Edul N. Dalal. // // http://www.ece.rochester.edu/~gsharma/papers/CIEDE2000CRNAFeb05.pdf #[test] fn de2000_test_set() { let set = &[ (0.0000, &[0.0000, 0.0000, 0.0000 ], &[0.0000, 0.0000, 0.0000 ]), (0.0000, &[99.5000, 0.0050, -0.0100 ], &[99.5000, 0.0050, -0.0100 ]), (100.0000, &[100.0000, 0.0050, -0.0100 ], &[0.0000, 0.0000, 0.0000 ]), (2.0425, &[50.0000, 2.6772, -79.7751], &[50.0000, 0.0000, -82.7485]), (2.8615, &[50.0000, 3.1571, -77.2803], &[50.0000, 0.0000, -82.7485]), (3.4412, &[50.0000, 2.8361, -74.0200], &[50.0000, 0.0000, -82.7485]), (1.0000, &[50.0000, -1.3802, -84.2814], &[50.0000, 0.0000, -82.7485]), (1.0000, &[50.0000, -1.1848, -84.8006], &[50.0000, 0.0000, -82.7485]), (1.0000, &[50.0000, -0.9009, -85.5211], &[50.0000, 0.0000, -82.7485]), (2.3669, &[50.0000, 0.0000, 0.0000 ], &[50.0000, -1.0000, 2.0000 ]), (2.3669, &[50.0000, -1.0000, 2.0000 ], &[50.0000, 0.0000, 0.0000 ]), (7.1792, &[50.0000, 2.4900, -0.0010 ], &[50.0000, -2.4900, 0.0009 ]), (7.1792, &[50.0000, 2.4900, -0.0010 ], &[50.0000, -2.4900, 0.0010 ]), (7.2195, &[50.0000, 2.4900, -0.0010 ], &[50.0000, -2.4900, 0.0011 ]), (7.2195, &[50.0000, 2.4900, -0.0010 ], &[50.0000, -2.4900, 0.0012 ]), (4.8045, &[50.0000, -0.0010, 2.4900 ], &[50.0000, 0.0009, -2.4900 ]), (4.7461, &[50.0000, -0.0010, 2.4900 ], &[50.0000, 0.0011, -2.4900 ]), (4.3065, &[50.0000, 2.5000, 0.0000 ], &[50.0000, 0.0000, -2.5000 ]), (27.1492, &[50.0000, 2.5000, 0.0000 ], &[73.0000, 25.0000, -18.0000]), (22.8977, &[50.0000, 2.5000, 0.0000 ], &[61.0000, -5.0000, 29.0000]), (31.9030, &[50.0000, 2.5000, 0.0000 ], &[56.0000, -27.0000, -3.0000 ]), (19.4535, &[50.0000, 2.5000, 0.0000 ], &[58.0000, 24.0000, 15.0000]), (1.0000, &[50.0000, 2.5000, 0.0000 ], &[50.0000, 3.1736, 0.5854 ]), (1.0000, &[50.0000, 2.5000, 0.0000 ], &[50.0000, 3.2972, 0.0000 ]), (1.0000, &[50.0000, 2.5000, 0.0000 ], &[50.0000, 1.8634, 0.5757 ]), (1.0000, &[50.0000, 2.5000, 0.0000 ], &[50.0000, 3.2592, 0.3350 ]), (1.2644, &[60.2574, -34.0099, 36.2677], &[60.4626, -34.1751, 39.4387]), (1.2630, &[63.0109, -31.0961, -5.8663 ], &[62.8187, -29.7946, -4.0864 ]), (1.8731, &[61.2901, 3.7196, -5.3901 ], &[61.4292, 2.2480, -4.9620 ]), (1.8645, &[35.0830, -44.1164, 3.7933 ], &[35.0232, -40.0716, 1.5901 ]), (2.0373, &[22.7233, 20.0904, -46.6940], &[23.0331, 14.9730, -42.5619]), (1.4146, &[36.4612, 47.8580, 18.3852], &[36.2715, 50.5065, 21.2231]), (1.4441, &[90.8027, -2.0831, 1.4410 ], &[91.1528, -1.6435, 0.0447 ]), (1.5381, &[90.9257, -0.5406, -0.9208 ], &[88.6381, -0.8985, -0.7239]), (0.6377, &[6.7747, -0.2908, -2.4247 ], &[5.8714, -0.0985, -2.2286]), (0.9082, &[2.0776, 0.0795, -1.1350 ], &[0.9033, -0.0636, -0.5514]) ]; for (expected, reference, sample) in set.iter() { assert!(compare_de(DEMethod::DE2000, *expected, reference, sample).is_ok()) } } deltae-0.3.2/src/validate.rs000064400000000000000000000026451046102023000140210ustar 00000000000000use super::*; /// Trait to validate whether a type has appropriate values pub trait Validate where Self: Sized { /// Return `Err()` if the values are invalid fn validate(self) -> ValueResult; } const RANGE_PCT: std::ops::RangeInclusive = 0.0..=100.0; const RANGE_I8: std::ops::RangeInclusive = -128.0..=128.0; const RANGE_CHROMA: std::ops::RangeInclusive = 0.0..=181.01933; const RANGE_360: std::ops::RangeInclusive = 0.0..=360.0; const RANGE_01: std::ops::RangeInclusive = 0.0..=1.0; impl Validate for LabValue { fn validate(self) -> ValueResult { if RANGE_PCT.contains(&self.l) && RANGE_I8.contains(&self.a) && RANGE_I8.contains(&self.b) { Ok(self) } else { Err(ValueError::OutOfBounds) } } } impl Validate for LchValue { fn validate(self) -> ValueResult { if RANGE_PCT.contains(&self.l) && RANGE_CHROMA.contains(&self.c) && RANGE_360.contains(&self.h) { Ok(self) } else { Err(ValueError::OutOfBounds) } } } impl Validate for XyzValue { fn validate(self) -> ValueResult { if RANGE_01.contains(&self.x) && RANGE_01.contains(&self.y) && RANGE_01.contains(&self.z) { Ok(self) } else { Err(ValueError::OutOfBounds) } } }