├── .clippy.toml ├── .github ├── check_autogen.sh ├── copyright.sh └── workflows │ └── ci.yml ├── .gitignore ├── .taplo.toml ├── .typos.toml ├── AUTHORS ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── color ├── .clippy.toml ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples │ ├── gradient.rs │ ├── interp.rs │ └── parse.rs ├── make_x11_colors.py └── src │ ├── cache_key.rs │ ├── chromaticity.rs │ ├── color.rs │ ├── colorspace.rs │ ├── dynamic.rs │ ├── flags.rs │ ├── floatfuncs.rs │ ├── gradient.rs │ ├── impl_bytemuck.rs │ ├── lib.rs │ ├── palette │ ├── css.rs │ └── mod.rs │ ├── parse.rs │ ├── rgba8.rs │ ├── serialize.rs │ ├── tag.rs │ └── x11_colors.rs └── color_operations ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md └── src └── lib.rs /.clippy.toml: -------------------------------------------------------------------------------- 1 | # LINEBENDER LINT SET - .clippy.toml - v1 2 | # See https://linebender.org/wiki/canonical-lints/ 3 | 4 | # The default Clippy value is capped at 8 bytes, which was chosen to improve performance on 32-bit. 5 | # Given that we are building for the future and even low-end mobile phones have 64-bit CPUs, 6 | # it makes sense to optimize for 64-bit and accept the performance hits on 32-bit. 7 | # 16 bytes is the number of bytes that fits into two 64-bit CPU registers. 8 | trivial-copy-size-limit = 16 9 | 10 | # END LINEBENDER LINT SET 11 | -------------------------------------------------------------------------------- /.github/check_autogen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check that the autogenerated file matches the repo (protect against hand-editing). 4 | 5 | set -e 6 | 7 | python3 color/make_x11_colors.py > x11_colors.rs 8 | diff color/src/x11_colors.rs x11_colors.rs 9 | 10 | rm x11_colors.rs 11 | echo "Autogenerated file matches." 12 | exit 0 13 | -------------------------------------------------------------------------------- /.github/copyright.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # If there are new files with headers that can't match the conditions here, 4 | # then the files can be ignored by an additional glob argument via the -g flag. 5 | # For example: 6 | # -g "!src/special_file.rs" 7 | # -g "!src/special_directory" 8 | 9 | # Check all the standard Rust source files 10 | output=$(rg "^// Copyright (19|20)[\d]{2} (.+ and )?the Color Authors( and .+)?$\n^// SPDX-License-Identifier: Apache-2\.0 OR MIT$\n\n" --files-without-match --multiline -g "*.rs" .) 11 | 12 | if [ -n "$output" ]; then 13 | echo -e "The following files lack the correct copyright header:\n" 14 | echo $output 15 | echo -e "\n\nPlease add the following header:\n" 16 | echo "// Copyright $(date +%Y) the Color Authors" 17 | echo "// SPDX-License-Identifier: Apache-2.0 OR MIT" 18 | echo -e "\n... rest of the file ...\n" 19 | exit 1 20 | fi 21 | 22 | echo "All files have correct copyright headers." 23 | exit 0 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | env: 2 | # We aim to always test with the latest stable Rust toolchain, however we pin to a specific 3 | # version like 1.70. Note that we only specify MAJOR.MINOR and not PATCH so that bugfixes still 4 | # come automatically. If the version specified here is no longer the latest stable version, 5 | # then please feel free to submit a PR that adjusts it along with the potential clippy fixes. 6 | RUST_STABLE_VER: "1.87" # In quotes because otherwise (e.g.) 1.70 would be interpreted as 1.7 7 | # The purpose of checking with the minimum supported Rust toolchain is to detect its staleness. 8 | # If the compilation fails, then the version specified here needs to be bumped up to reality. 9 | # Be sure to also update the rust-version property in the workspace Cargo.toml file, 10 | # plus all the README.md files of the affected packages. 11 | RUST_MIN_VER: "1.82" 12 | # List of packages that will be checked with the minimum supported Rust version. 13 | # This should be limited to packages that are intended for publishing. 14 | RUST_MIN_VER_PKGS: "-p color -p color_operations" 15 | # List of features that depend on the standard library and will be excluded from no_std checks. 16 | FEATURES_DEPENDING_ON_STD: "std,default" 17 | 18 | 19 | # Rationale 20 | # 21 | # We don't run clippy with --all-targets because then even --lib and --bins are compiled with 22 | # dev dependencies enabled, which does not match how they would be compiled by users. 23 | # A dev dependency might enable a feature that we need for a regular dependency, 24 | # and checking with --all-targets would not find our feature requirements lacking. 25 | # This problem still applies to cargo resolver version 2. 26 | # Thus we split all the targets into two steps, one with --lib --bins 27 | # and another with --tests --benches --examples. 28 | # Also, we can't give --lib --bins explicitly because then cargo will error on binary-only packages. 29 | # Luckily the default behavior of cargo with no explicit targets is the same but without the error. 30 | # 31 | # We use cargo-hack for a similar reason. Cargo's --workspace will do feature unification across 32 | # the whole workspace. While cargo-hack will instead check each workspace package separately. 33 | # 34 | # Using cargo-hack also allows us to more easily test the feature matrix of our packages. 35 | # We use --each-feature & --optional-deps which will run a separate check for every feature. 36 | # 37 | # We use cargo-nextest, which has a faster concurrency model for running tests. 38 | # However cargo-nextest does not support running doc tests, so we also have a cargo test --doc step. 39 | # For more information see https://github.com/nextest-rs/nextest/issues/16 40 | # 41 | # The MSRV jobs run only cargo check because different clippy versions can disagree on goals and 42 | # running tests introduces dev dependencies which may require a higher MSRV than the bare package. 43 | # 44 | # For no_std checks we target x86_64-unknown-none, because this target doesn't support std 45 | # and as such will error out if our dependency tree accidentally tries to use std. 46 | # https://doc.rust-lang.org/stable/rustc/platform-support/x86_64-unknown-none.html 47 | # 48 | # We don't save caches in the merge-group cases, because those caches will never be re-used (apart 49 | # from the very rare cases where there are multiple PRs in the merge queue). 50 | # This is because GitHub doesn't share caches between merge queues and the main branch. 51 | 52 | name: CI 53 | 54 | on: 55 | pull_request: 56 | merge_group: 57 | # We run on push, even though the commit is the same as when we ran in merge_group. 58 | # This allows the cache to be primed. 59 | # See https://github.com/orgs/community/discussions/66430 60 | push: 61 | branches: 62 | - main 63 | 64 | jobs: 65 | fmt: 66 | name: formatting 67 | runs-on: ubuntu-latest 68 | steps: 69 | - uses: actions/checkout@v4 70 | 71 | - name: install stable toolchain 72 | uses: dtolnay/rust-toolchain@master 73 | with: 74 | toolchain: ${{ env.RUST_STABLE_VER }} 75 | components: rustfmt 76 | 77 | - name: cargo fmt 78 | run: cargo fmt --all --check 79 | 80 | - name: Install Taplo 81 | uses: uncenter/setup-taplo@09968a8ae38d66ddd3d23802c44bf6122d7aa991 # v1 82 | with: 83 | version: "0.9.3" 84 | 85 | - name: Run taplo fmt 86 | run: taplo fmt --check --diff 87 | 88 | - name: install ripgrep 89 | run: | 90 | sudo apt update 91 | sudo apt install ripgrep 92 | 93 | - name: check copyright headers 94 | run: bash .github/copyright.sh 95 | 96 | - name: check autogenerated files 97 | run: bash .github/check_autogen.sh 98 | 99 | - name: install cargo-rdme 100 | uses: taiki-e/install-action@v2 101 | with: 102 | tool: cargo-rdme 103 | 104 | - name: cargo rdme 105 | # Ideally, we'd set heading-base-level=0 in the config file for cargo rdme, but that only 106 | # supports being placed at the top level, which we want to avoid cluttering. 107 | run: cargo rdme --workspace-project=color --heading-base-level=0 --check 108 | 109 | clippy-stable: 110 | name: cargo clippy 111 | runs-on: ${{ matrix.os }} 112 | strategy: 113 | matrix: 114 | os: [windows-latest, macos-latest, ubuntu-latest] 115 | steps: 116 | - uses: actions/checkout@v4 117 | 118 | - name: install stable toolchain 119 | uses: dtolnay/rust-toolchain@master 120 | with: 121 | toolchain: ${{ env.RUST_STABLE_VER }} 122 | targets: x86_64-unknown-none 123 | components: clippy 124 | 125 | - name: install cargo-hack 126 | uses: taiki-e/install-action@v2 127 | with: 128 | tool: cargo-hack 129 | 130 | - name: restore cache 131 | uses: Swatinem/rust-cache@v2 132 | with: 133 | save-if: ${{ github.event_name != 'merge_group' }} 134 | 135 | - name: cargo clippy (no_std) 136 | run: cargo hack clippy --workspace --locked --optional-deps --each-feature --ignore-unknown-features --features libm --exclude-features ${{ env.FEATURES_DEPENDING_ON_STD }} --target x86_64-unknown-none -- -D warnings 137 | 138 | - name: cargo clippy 139 | run: cargo hack clippy --workspace --locked --optional-deps --each-feature --ignore-unknown-features --features std -- -D warnings 140 | 141 | - name: cargo clippy (auxiliary) 142 | run: cargo hack clippy --workspace --locked --optional-deps --each-feature --ignore-unknown-features --features std --tests --benches --examples -- -D warnings 143 | 144 | clippy-stable-wasm: 145 | name: cargo clippy (wasm32) 146 | runs-on: ubuntu-latest 147 | steps: 148 | - uses: actions/checkout@v4 149 | 150 | - name: install stable toolchain 151 | uses: dtolnay/rust-toolchain@master 152 | with: 153 | toolchain: ${{ env.RUST_STABLE_VER }} 154 | targets: wasm32-unknown-unknown 155 | components: clippy 156 | 157 | - name: install cargo-hack 158 | uses: taiki-e/install-action@v2 159 | with: 160 | tool: cargo-hack 161 | 162 | - name: restore cache 163 | uses: Swatinem/rust-cache@v2 164 | with: 165 | save-if: ${{ github.event_name != 'merge_group' }} 166 | 167 | - name: cargo clippy 168 | run: cargo hack clippy --workspace --locked --target wasm32-unknown-unknown --optional-deps --each-feature --ignore-unknown-features --features std -- -D warnings 169 | 170 | - name: cargo clippy (auxiliary) 171 | run: cargo hack clippy --workspace --locked --target wasm32-unknown-unknown --optional-deps --each-feature --ignore-unknown-features --features std --tests --benches --examples -- -D warnings 172 | 173 | test-stable: 174 | name: cargo test 175 | runs-on: ${{ matrix.os }} 176 | strategy: 177 | matrix: 178 | os: [windows-latest, macos-latest, ubuntu-latest] 179 | steps: 180 | - uses: actions/checkout@v4 181 | 182 | - name: install stable toolchain 183 | uses: dtolnay/rust-toolchain@master 184 | with: 185 | toolchain: ${{ env.RUST_STABLE_VER }} 186 | 187 | - name: install cargo-nextest 188 | uses: taiki-e/install-action@v2 189 | with: 190 | tool: cargo-nextest 191 | 192 | - name: restore cache 193 | uses: Swatinem/rust-cache@v2 194 | with: 195 | save-if: ${{ github.event_name != 'merge_group' }} 196 | 197 | - name: cargo nextest 198 | run: cargo nextest run --workspace --locked --all-features --no-fail-fast 199 | 200 | - name: cargo test --doc 201 | run: cargo test --doc --workspace --locked --all-features --no-fail-fast 202 | 203 | test-stable-wasm: 204 | name: cargo test (wasm32) 205 | runs-on: ubuntu-latest 206 | steps: 207 | - uses: actions/checkout@v4 208 | 209 | - name: install stable toolchain 210 | uses: dtolnay/rust-toolchain@master 211 | with: 212 | toolchain: ${{ env.RUST_STABLE_VER }} 213 | targets: wasm32-unknown-unknown 214 | 215 | - name: restore cache 216 | uses: Swatinem/rust-cache@v2 217 | with: 218 | save-if: ${{ github.event_name != 'merge_group' }} 219 | 220 | # TODO: Find a way to make tests work. Until then the tests are merely compiled. 221 | - name: cargo test compile 222 | run: cargo test --workspace --locked --target wasm32-unknown-unknown --all-features --no-run 223 | 224 | check-msrv: 225 | name: cargo check (msrv) 226 | runs-on: ${{ matrix.os }} 227 | strategy: 228 | matrix: 229 | os: [windows-latest, macos-latest, ubuntu-latest] 230 | steps: 231 | - uses: actions/checkout@v4 232 | 233 | - name: install msrv toolchain 234 | uses: dtolnay/rust-toolchain@master 235 | with: 236 | toolchain: ${{ env.RUST_MIN_VER }} 237 | targets: x86_64-unknown-none 238 | 239 | - name: install cargo-hack 240 | uses: taiki-e/install-action@v2 241 | with: 242 | tool: cargo-hack 243 | 244 | - name: restore cache 245 | uses: Swatinem/rust-cache@v2 246 | with: 247 | save-if: ${{ github.event_name != 'merge_group' }} 248 | 249 | - name: cargo check (no_std) 250 | run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} --locked --optional-deps --each-feature --ignore-unknown-features --features libm --exclude-features ${{ env.FEATURES_DEPENDING_ON_STD }} --target x86_64-unknown-none 251 | 252 | - name: cargo check 253 | run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} --locked --optional-deps --each-feature --ignore-unknown-features --features std 254 | 255 | check-msrv-wasm: 256 | name: cargo check (msrv) (wasm32) 257 | runs-on: ubuntu-latest 258 | steps: 259 | - uses: actions/checkout@v4 260 | 261 | - name: install msrv toolchain 262 | uses: dtolnay/rust-toolchain@master 263 | with: 264 | toolchain: ${{ env.RUST_MIN_VER }} 265 | targets: wasm32-unknown-unknown 266 | 267 | - name: install cargo-hack 268 | uses: taiki-e/install-action@v2 269 | with: 270 | tool: cargo-hack 271 | 272 | - name: restore cache 273 | uses: Swatinem/rust-cache@v2 274 | with: 275 | save-if: ${{ github.event_name != 'merge_group' }} 276 | 277 | - name: cargo check 278 | run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} --locked --target wasm32-unknown-unknown --optional-deps --each-feature --ignore-unknown-features --features std 279 | 280 | miri: 281 | name: cargo miri 282 | runs-on: ubuntu-latest 283 | steps: 284 | - uses: actions/checkout@v4 285 | 286 | - name: install nightly toolchain 287 | uses: dtolnay/rust-toolchain@master 288 | with: 289 | toolchain: nightly 290 | components: miri 291 | targets: s390x-unknown-linux-gnu 292 | 293 | - name: install cargo-hack 294 | uses: taiki-e/install-action@v2 295 | with: 296 | tool: cargo-hack 297 | 298 | - name: restore cache 299 | uses: Swatinem/rust-cache@v2 300 | with: 301 | save-if: ${{ github.event_name != 'merge_group' }} 302 | 303 | - name: cargo miri 304 | run: cargo miri test --workspace --locked --all-features --target s390x-unknown-linux-gnu --no-fail-fast 305 | 306 | doc: 307 | name: cargo doc 308 | # NOTE: We don't have any platform specific docs in this workspace, so we only run on Ubuntu. 309 | # If we get per-platform docs (win/macos/linux/wasm32/..) then doc jobs should match that. 310 | runs-on: ubuntu-latest 311 | steps: 312 | - uses: actions/checkout@v4 313 | 314 | - name: install nightly toolchain 315 | uses: dtolnay/rust-toolchain@nightly 316 | 317 | - name: restore cache 318 | uses: Swatinem/rust-cache@v2 319 | with: 320 | save-if: ${{ github.event_name != 'merge_group' }} 321 | 322 | # We test documentation using nightly to match docs.rs. 323 | - name: cargo doc 324 | run: cargo doc --workspace --locked --all-features --no-deps --document-private-items 325 | env: 326 | RUSTDOCFLAGS: '--cfg docsrs -D warnings' 327 | 328 | # If this fails, consider changing your text or adding something to .typos.toml. 329 | typos: 330 | runs-on: ubuntu-latest 331 | steps: 332 | - uses: actions/checkout@v4 333 | 334 | - name: check typos 335 | uses: crate-ci/typos@v1.32.0 336 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | # See https://taplo.tamasfe.dev/configuration/file.html 2 | # and https://taplo.tamasfe.dev/configuration/formatter-options.html 3 | 4 | [formatting] 5 | # Aligning comments with the largest line creates 6 | # diff noise when neighboring lines are changed. 7 | align_comments = false 8 | 9 | # Matches how rustfmt formats Rust code 10 | column_width = 100 11 | indent_string = " " 12 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | # See the configuration reference at 2 | # https://github.com/crate-ci/typos/blob/master/docs/reference.md 3 | 4 | [default] 5 | extend-ignore-re = ["colour-science"] 6 | 7 | # Corrections take the form of a key/value pair. The key is the incorrect word 8 | # and the value is the correct word. If the key and value are the same, the 9 | # word is treated as always correct. If the value is an empty string, the word 10 | # is treated as always incorrect. 11 | 12 | # Match Identifier - Case Sensitive 13 | [default.extend-identifiers] 14 | colour = "color" 15 | 16 | # Match Inside a Word - Case Insensitive 17 | [default.extend-words] 18 | 19 | [files] 20 | # Include .github, .cargo, etc. 21 | ignore-hidden = false 22 | extend-exclude = [ 23 | # /.git isn't in .gitignore, because git never tracks it. 24 | # Typos doesn't know that, though. 25 | "/.git", 26 | ] 27 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of Color's significant contributors. 2 | # 3 | # This does not necessarily list everyone who has contributed code, 4 | # especially since many employees of one corporation may be contributing. 5 | # To see the full list of contributors, see the revision history in 6 | # source control. 7 | Raph Levien 8 | Bruce Mitchener, Jr. 9 | Tom Churchman 10 | Jordan Johnson 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | # Changelog 10 | 11 | The latest published Color release is [0.3.1](#031-2025-05-19) which was released on 2025-05-19. 12 | You can find its changes [documented below](#031-2025-05-19). 13 | 14 | ## [Unreleased] 15 | 16 | This release has an [MSRV][] of 1.82. 17 | 18 | ## [0.3.1][] (2025-05-19) 19 | 20 | This release has an [MSRV][] of 1.82. 21 | 22 | ### Fixed 23 | 24 | * Compilation failure with nightly Rust due to `core_float_math` changes. ([#175][] by [@ajakubowicz-canva][]) 25 | 26 | ## [0.3.0][] (2025-04-30) 27 | 28 | This release has an [MSRV][] of 1.82. 29 | 30 | ### Added 31 | 32 | * Support converting between color spaces without chromatic adaptation, thereby representing the same absolute color in the destination color space as in the source color space. ([#139][], [#153][] by [@tomcur][]) 33 | * Add absolute color conversion matrices for ProPhoto RGB, ACES2065-1 and ACEScg for faster conversion without chromatic adaptation to and from these color spaces. ([#156][], [#164][], [#165][] by [@tomcur][]) 34 | 35 | **Note to `ColorSpace` implementers:** the `WHITE_POINT` associated constant is added to `ColorSpace`, defaulting to D65. 36 | Implementations with a non-D65 white point should set this constant to get correct default absolute conversion behavior. 37 | * Support manual chromatic adaptation of colors between arbitrary white point chromaticities. ([#139][] by [@tomcur][]) 38 | * Add `Missing::EMPTY` to allow getting an empty `Missing` set in `const` contexts. ([#149][] by [@tomcur][]) 39 | * Add `From> for DynamicColor` conversions for all color spaces that have a direct runtime representation in `ColorSpaceTag`. ([#155][] by [@LaurenzV][]) 40 | * Add usage examples for `DynamicColor::interpolate` and `gradient`. ([#158][], [#159][] by [@tomcur][]) 41 | 42 | ### Changed 43 | 44 | * Breaking change: the deprecated conversion `From for PremulColor` has been removed. Use `From for PremulColor` instead. ([#157][] by [@tomcur][]) 45 | * Improve `no_std` support. ([#146][] by [@waywardmonkeys][]) 46 | * Make `{AlphaColor, OpaqueColor, PremulColor}::to_rgba8` faster. ([#166][] by [@tomcur][]) 47 | 48 | ### Fixed 49 | 50 | * Correctly determine analogous components between ACES2065-1 and other color spaces when converting, 51 | to carry missing components forward when interpolating colors with missing components in the ACES 2065-1 colorspace. ([#144][] by [@tomcur][]) 52 | * Fixed powerless hue component calculation for the HWB color space. ([#145][] by [@tomcur][]) 53 | 54 | ## [0.2.4][] (2025-05-19) 55 | 56 | This release has an [MSRV][] of 1.82. 57 | 58 | ### Fixed 59 | 60 | * Compilation failure with nightly Rust due to `core_float_math` changes. ([#175][] by [@ajakubowicz-canva][]) 61 | 62 | ## [0.2.3][] (2025-01-20) 63 | 64 | This release has an [MSRV][] of 1.82. 65 | 66 | ### Added 67 | 68 | * Support for the ACES2065-1 color space. ([#124][] by [@tomcur][]) 69 | * A documentation example implementing `ColorSpace`. ([#130][] by [@tomcur][]) 70 | * Conversions of `[u8; 4]` and packed `u32` into `Rgba8` and `PremulRgba8` are now provided. ([#135][] by [@tomcur][]) 71 | * Support construction of `AlphaColor`, `OpaqueColor` and `PremulColor` from rgb8 values. ([#136][] by [@waywardmonkeys][]) 72 | 73 | ### Fixed 74 | 75 | * Specify some `ColorSpace::WHITE_COMPONENTS` to higher precision. ([#128][], [#129][] by [@tomcur][]) 76 | 77 | ## [0.2.2][] (2025-01-03) 78 | 79 | This release has an [MSRV][] of 1.82. 80 | 81 | ### Fixed 82 | 83 | * Colors in `XyzD65` are serialized as `xyz-d65` rather than `xyz`. ([#118][] by [@waywardmonkeys][]) 84 | * Alpha values are clamped at parse time. ([#119][] by [@waywardmonkeys][]) 85 | 86 | ## [0.2.1][] (2024-12-27) 87 | 88 | This release has an [MSRV][] of 1.82. 89 | 90 | ### Added 91 | 92 | * Add `FromStr` impl for `AlphaColor`, `DynamicColor`, `OpaqueColor`, `PremulColor`. ([#111][] by [@waywardmonkeys][]) 93 | 94 | ### Changed 95 | 96 | * Don't enable `serde`'s `std` feature when enabling our `std` feature. ([#108][] by [@waywardmonkeys][]) 97 | * `From` for `PremulColor` is deprecated and replaced by `From`. ([#113][] by [@waywardmonkeys][]) 98 | 99 | ### Fixed 100 | 101 | * Make color parsing case insensitive. ([#109][] by [@raphlinus][]) 102 | 103 | ## [0.2.0][] (2024-12-17) 104 | 105 | This release has an [MSRV][] of 1.82. 106 | 107 | ### Added 108 | 109 | * Add `BLACK`, `WHITE`, and `TRANSPARENT` constants to the color types. ([#64][] by [@waywardmonkeys][]) 110 | * The `serde` feature enables using `serde` with `AlphaColor`, `DynamicColor`, `HueDirection`, `OpaqueColor`, `PremulColor`, and `Rgba8`. ([#61][], [#70][], [#80][] by [@waywardmonkeys][]) 111 | * Conversion of a `Rgba8` to a `u32` is now provided. ([#66][], [#77][] by [@waywardmonkeys][], [#100][] by [@tomcur][]) 112 | * A new `PremulRgba8` type mirrors `Rgba8`, but for `PremulColor`. ([#66][] by [@waywardmonkeys][]) 113 | * `AlphaColor::with_alpha` allows setting the alpha channel. ([#67][] by [@waywardmonkeys][]) 114 | * Support for the `ACEScg` color space. ([#54][] by [@MightyBurger][]) 115 | * `DynamicColor` gets `with_alpha` and `multiply_alpha`. ([#71][] by [@waywardmonkeys][]) 116 | * `DynamicColor` now impls `PartialEq`. ([#75][] by [@waywardmonkeys][]) 117 | * `AlphaColor`, `OpaqueColor`, and `PremulColor` now impl `PartialEq`. ([#76][], [#86][] by [@waywardmonkeys][]) 118 | * `HueDirection` now impls `PartialEq`. ([#79][] by [@waywardmonkeys][]) 119 | * `ColorSpaceTag` and `HueDirection` now have bytemuck support. ([#81][] by [@waywardmonkeys][]) 120 | * A `DynamicColor` parsed from a named color or named color space function now serializes back to that name, as per the CSS Color Level 4 spec ([#39][] by [@tomcur][]). 121 | * `CacheKey` to allow using colors as keys for resource caching. ([#92][] by [@DJMcNab][]) 122 | 123 | ### Changed 124 | 125 | * The `mul_alpha` method was renamed to `multiply_alpha`. ([#65][] by [@waywardmonkeys][]) 126 | 127 | ### Fixed 128 | 129 | * Stray parenthesis in hex serialization of `Rgba8` fixed. ([#78][] by [@raphlinus][]) 130 | 131 | ## [0.1.0][] (2024-11-20) 132 | 133 | This release has an [MSRV][] of 1.82. 134 | 135 | This is the initial release. 136 | 137 | [@ajakubowicz-canva]: https://github.com/ajakubowicz-canva 138 | [@DJMcNab]: https://github.com/DJMcNab 139 | [@LaurenzV]: https://github.com/LaurenzV 140 | [@MightyBurger]: https://github.com/MightyBurger 141 | [@raphlinus]: https://github.com/raphlinus 142 | [@tomcur]: https://github.com/tomcur 143 | [@waywardmonkeys]: https://github.com/waywardmonkeys 144 | 145 | [#39]: https://github.com/linebender/color/pull/39 146 | [#54]: https://github.com/linebender/color/pull/54 147 | [#61]: https://github.com/linebender/color/pull/61 148 | [#64]: https://github.com/linebender/color/pull/64 149 | [#65]: https://github.com/linebender/color/pull/65 150 | [#66]: https://github.com/linebender/color/pull/66 151 | [#67]: https://github.com/linebender/color/pull/67 152 | [#70]: https://github.com/linebender/color/pull/70 153 | [#71]: https://github.com/linebender/color/pull/71 154 | [#75]: https://github.com/linebender/color/pull/75 155 | [#76]: https://github.com/linebender/color/pull/76 156 | [#77]: https://github.com/linebender/color/pull/77 157 | [#78]: https://github.com/linebender/color/pull/78 158 | [#79]: https://github.com/linebender/color/pull/79 159 | [#80]: https://github.com/linebender/color/pull/80 160 | [#81]: https://github.com/linebender/color/pull/81 161 | [#86]: https://github.com/linebender/color/pull/86 162 | [#92]: https://github.com/linebender/color/pull/92 163 | [#100]: https://github.com/linebender/color/pull/100 164 | [#108]: https://github.com/linebender/color/pull/108 165 | [#109]: https://github.com/linebender/color/pull/109 166 | [#111]: https://github.com/linebender/color/pull/111 167 | [#113]: https://github.com/linebender/color/pull/113 168 | [#118]: https://github.com/linebender/color/pull/118 169 | [#119]: https://github.com/linebender/color/pull/119 170 | [#124]: https://github.com/linebender/color/pull/124 171 | [#128]: https://github.com/linebender/color/pull/128 172 | [#129]: https://github.com/linebender/color/pull/129 173 | [#130]: https://github.com/linebender/color/pull/130 174 | [#135]: https://github.com/linebender/color/pull/135 175 | [#136]: https://github.com/linebender/color/pull/136 176 | [#139]: https://github.com/linebender/color/pull/139 177 | [#144]: https://github.com/linebender/color/pull/144 178 | [#145]: https://github.com/linebender/color/pull/145 179 | [#146]: https://github.com/linebender/color/pull/146 180 | [#149]: https://github.com/linebender/color/pull/149 181 | [#153]: https://github.com/linebender/color/pull/153 182 | [#155]: https://github.com/linebender/color/pull/155 183 | [#156]: https://github.com/linebender/color/pull/156 184 | [#157]: https://github.com/linebender/color/pull/157 185 | [#158]: https://github.com/linebender/color/pull/158 186 | [#159]: https://github.com/linebender/color/pull/159 187 | [#164]: https://github.com/linebender/color/pull/164 188 | [#165]: https://github.com/linebender/color/pull/165 189 | [#166]: https://github.com/linebender/color/pull/166 190 | [#175]: https://github.com/linebender/color/pull/175 191 | 192 | [Unreleased]: https://github.com/linebender/color/compare/v0.3.1...HEAD 193 | [0.3.1]: https://github.com/linebender/color/releases/tag/v0.3.1 194 | [0.3.0]: https://github.com/linebender/color/releases/tag/v0.3.0 195 | [0.2.4]: https://github.com/linebender/color/releases/tag/v0.2.4 196 | [0.2.3]: https://github.com/linebender/color/releases/tag/v0.2.3 197 | [0.2.2]: https://github.com/linebender/color/releases/tag/v0.2.2 198 | [0.2.1]: https://github.com/linebender/color/releases/tag/v0.2.1 199 | [0.2.0]: https://github.com/linebender/color/releases/tag/v0.2.0 200 | [0.1.0]: https://github.com/linebender/color/releases/tag/v0.1.0 201 | 202 | [MSRV]: README.md#minimum-supported-rust-version-msrv 203 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "bytemuck" 7 | version = "1.23.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" 10 | 11 | [[package]] 12 | name = "color" 13 | version = "0.3.1" 14 | dependencies = [ 15 | "bytemuck", 16 | "libm", 17 | "serde", 18 | ] 19 | 20 | [[package]] 21 | name = "color_operations" 22 | version = "0.3.1" 23 | dependencies = [ 24 | "color", 25 | ] 26 | 27 | [[package]] 28 | name = "libm" 29 | version = "0.2.15" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" 32 | 33 | [[package]] 34 | name = "proc-macro2" 35 | version = "1.0.95" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 38 | dependencies = [ 39 | "unicode-ident", 40 | ] 41 | 42 | [[package]] 43 | name = "quote" 44 | version = "1.0.40" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 47 | dependencies = [ 48 | "proc-macro2", 49 | ] 50 | 51 | [[package]] 52 | name = "serde" 53 | version = "1.0.219" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 56 | dependencies = [ 57 | "serde_derive", 58 | ] 59 | 60 | [[package]] 61 | name = "serde_derive" 62 | version = "1.0.219" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 65 | dependencies = [ 66 | "proc-macro2", 67 | "quote", 68 | "syn", 69 | ] 70 | 71 | [[package]] 72 | name = "syn" 73 | version = "2.0.101" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 76 | dependencies = [ 77 | "proc-macro2", 78 | "quote", 79 | "unicode-ident", 80 | ] 81 | 82 | [[package]] 83 | name = "unicode-ident" 84 | version = "1.0.18" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 87 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["color", "color_operations"] 4 | 5 | [workspace.package] 6 | # Color version, also used by other packages which want to mimic Color's version. 7 | # Right now those packages include color_operations. 8 | # 9 | # NOTE: When bumping this, remember to also bump the aforementioned other packages' 10 | # version in the `workspace.dependencies` section in this file. 11 | version = "0.3.1" 12 | 13 | edition = "2021" 14 | # Keep in sync with RUST_MIN_VER in .github/workflows/ci.yml, with the relevant README.md files 15 | # and with the MSRV in the `Unreleased` section of CHANGELOG.md. 16 | # When updating to 1.83 or later, update `color/src/flags.rs` and remove this note. 17 | # When updating to 1.84 or later, update `color/src/floatfuncs.rs` and remove this note. 18 | rust-version = "1.82" 19 | license = "Apache-2.0 OR MIT" 20 | repository = "https://github.com/linebender/color" 21 | 22 | [workspace.dependencies] 23 | color = { version = "0.3.1", path = "color", default-features = false } 24 | color_operations = { version = "0.3.1", path = "color_operations" } 25 | 26 | [workspace.lints] 27 | rust.unsafe_code = "deny" 28 | 29 | # LINEBENDER LINT SET - Cargo.toml - v2 30 | # See https://linebender.org/wiki/canonical-lints/ 31 | rust.keyword_idents_2024 = "forbid" 32 | rust.non_ascii_idents = "forbid" 33 | rust.non_local_definitions = "forbid" 34 | rust.unsafe_op_in_unsafe_fn = "forbid" 35 | 36 | rust.elided_lifetimes_in_paths = "warn" 37 | rust.let_underscore_drop = "warn" 38 | rust.missing_debug_implementations = "warn" 39 | rust.missing_docs = "warn" 40 | rust.single_use_lifetimes = "warn" 41 | rust.trivial_numeric_casts = "warn" 42 | rust.unexpected_cfgs = "warn" 43 | rust.unit_bindings = "warn" 44 | rust.unnameable_types = "warn" 45 | rust.unreachable_pub = "warn" 46 | rust.unused_import_braces = "warn" 47 | rust.unused_lifetimes = "warn" 48 | rust.unused_macro_rules = "warn" 49 | rust.unused_qualifications = "warn" 50 | rust.variant_size_differences = "warn" 51 | 52 | clippy.allow_attributes = "warn" 53 | clippy.allow_attributes_without_reason = "warn" 54 | clippy.cast_possible_truncation = "warn" 55 | clippy.collection_is_never_read = "warn" 56 | clippy.dbg_macro = "warn" 57 | clippy.debug_assert_with_mut_call = "warn" 58 | clippy.doc_markdown = "warn" 59 | clippy.exhaustive_enums = "warn" 60 | clippy.fn_to_numeric_cast_any = "forbid" 61 | clippy.infinite_loop = "warn" 62 | clippy.large_include_file = "warn" 63 | clippy.large_stack_arrays = "warn" 64 | clippy.match_same_arms = "warn" 65 | clippy.mismatching_type_param_order = "warn" 66 | clippy.missing_assert_message = "warn" 67 | clippy.missing_errors_doc = "warn" 68 | clippy.missing_fields_in_debug = "warn" 69 | clippy.missing_panics_doc = "warn" 70 | clippy.partial_pub_fields = "warn" 71 | clippy.return_self_not_must_use = "warn" 72 | clippy.same_functions_in_if_condition = "warn" 73 | clippy.semicolon_if_nothing_returned = "warn" 74 | clippy.shadow_unrelated = "warn" 75 | clippy.should_panic_without_expect = "warn" 76 | clippy.todo = "warn" 77 | clippy.trivially_copy_pass_by_ref = "warn" 78 | clippy.unseparated_literal_suffix = "warn" 79 | clippy.use_self = "warn" 80 | clippy.wildcard_imports = "warn" 81 | 82 | clippy.cargo_common_metadata = "warn" 83 | clippy.negative_feature_names = "warn" 84 | clippy.redundant_feature_names = "warn" 85 | clippy.wildcard_dependencies = "warn" 86 | # END LINEBENDER LINT SET 87 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Color 4 | 5 | A library for representing and manipulating colors 6 | 7 | [![Linebender Zulip, #color channel](https://img.shields.io/badge/Linebender-%23color-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/channel/466849-color) 8 | [![dependency status](https://deps.rs/repo/github/linebender/color/status.svg)](https://deps.rs/repo/github/linebender/color) 9 | [![Apache 2.0 or MIT license.](https://img.shields.io/badge/license-Apache--2.0_OR_MIT-blue.svg)](#license) 10 | [![Build status](https://github.com/linebender/color/workflows/CI/badge.svg)](https://github.com/linebender/color/actions) 11 | [![Crates.io](https://img.shields.io/crates/v/color.svg)](https://crates.io/crates/color) 12 | [![Docs](https://docs.rs/color/badge.svg)](https://docs.rs/color) 13 | 14 |
15 | 16 | The Color library provides functionality for representing, converting, parsing, serializing, and manipulating colors in a variety of color spaces. 17 | It closely follows the [CSS Color Level 4] draft spec. 18 | 19 | ## Minimum supported Rust Version (MSRV) 20 | 21 | This version of Color has been verified to compile with **Rust 1.82** and later. 22 | 23 | Future versions of Color might increase the Rust version requirement. 24 | It will not be treated as a breaking change and as such can even happen with small patch releases. 25 | 26 |
27 | Click here if compiling fails. 28 | 29 | As time has passed, some of Color's dependencies could have released versions with a higher Rust requirement. 30 | If you encounter a compilation issue due to a dependency and don't want to upgrade your Rust toolchain, then you could downgrade the dependency. 31 | 32 | ```sh 33 | # Use the problematic dependency's name and version 34 | cargo update -p package_name --precise 0.1.1 35 | ``` 36 |
37 | 38 | ## Community 39 | 40 | [![Linebender Zulip](https://img.shields.io/badge/Xi%20Zulip-%23color-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/channel/466849-color) 41 | 42 | Discussion of Color development happens in the [Linebender Zulip](https://xi.zulipchat.com/), specifically the [#color channel](https://xi.zulipchat.com/#narrow/channel/466849-color). 43 | All public content can be read without logging in. 44 | 45 | ## License 46 | 47 | Licensed under either of 48 | 49 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) 50 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 51 | 52 | at your option. 53 | 54 | ## Contribution 55 | 56 | Contributions are welcome by pull request. The [Rust code of conduct] applies. 57 | Please feel free to add your name to the [AUTHORS] file in any substantive pull request. 58 | 59 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions. 60 | 61 | [Rust Code of Conduct]: https://www.rust-lang.org/policies/code-of-conduct 62 | [AUTHORS]: ./AUTHORS 63 | [CSS Color Level 4]: https://www.w3.org/TR/css-color-4/ 64 | -------------------------------------------------------------------------------- /color/.clippy.toml: -------------------------------------------------------------------------------- 1 | doc-valid-idents = ["AArch64", "ACEScg", "ACEScc", "ACEScct", "ProPhoto", ".."] 2 | -------------------------------------------------------------------------------- /color/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "color" 3 | version.workspace = true 4 | license.workspace = true 5 | edition.workspace = true 6 | description = "A library for representing and manipulating colors" 7 | keywords = ["color", "css", "rgb"] 8 | categories = ["graphics"] 9 | repository.workspace = true 10 | rust-version.workspace = true 11 | 12 | [package.metadata.docs.rs] 13 | all-features = true 14 | # There are no platform specific docs. 15 | default-target = "x86_64-unknown-linux-gnu" 16 | targets = [] 17 | 18 | [features] 19 | default = ["std"] 20 | std = [] 21 | libm = ["dep:libm"] 22 | bytemuck = ["dep:bytemuck"] 23 | serde = ["dep:serde"] 24 | 25 | [dependencies] 26 | 27 | [dependencies.bytemuck] 28 | version = "1.23.0" 29 | optional = true 30 | default-features = false 31 | 32 | [dependencies.libm] 33 | version = "0.2.15" 34 | optional = true 35 | 36 | [dependencies.serde] 37 | version = "1.0.219" 38 | optional = true 39 | default-features = false 40 | features = ["derive"] 41 | 42 | [lints] 43 | workspace = true 44 | -------------------------------------------------------------------------------- /color/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /color/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /color/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Color 4 | 5 | A library for representing and manipulating colors 6 | 7 | [![Linebender Zulip, #color channel](https://img.shields.io/badge/Linebender-%23color-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/channel/466849-color) 8 | [![dependency status](https://deps.rs/repo/github/linebender/color/status.svg)](https://deps.rs/repo/github/linebender/color) 9 | [![Apache 2.0 or MIT license.](https://img.shields.io/badge/license-Apache--2.0_OR_MIT-blue.svg)](#license) 10 | [![Build status](https://github.com/linebender/color/workflows/CI/badge.svg)](https://github.com/linebender/color/actions) 11 | [![Crates.io](https://img.shields.io/crates/v/color.svg)](https://crates.io/crates/color) 12 | [![Docs](https://docs.rs/color/badge.svg)](https://docs.rs/color) 13 | 14 |
15 | 16 | 20 | 21 | 23 | [libm]: https://crates.io/crates/libm 24 | 25 | [`DynamicColor`]: https://docs.rs/color/latest/color/struct.DynamicColor.html 26 | [`AlphaColor`]: https://docs.rs/color/latest/color/struct.AlphaColor.html 27 | [`ColorSpace`]: https://docs.rs/color/latest/color/trait.ColorSpace.html 28 | [`ColorSpaceTag`]: https://docs.rs/color/latest/color/enum.ColorSpaceTag.html 29 | [`HueDirection`]: https://docs.rs/color/latest/color/enum.HueDirection.html 30 | [`OpaqueColor`]: https://docs.rs/color/latest/color/struct.OpaqueColor.html 31 | [`PremulColor`]: https://docs.rs/color/latest/color/struct.PremulColor.html 32 | [`PremulRgba8`]: https://docs.rs/color/latest/color/struct.PremulRgba8.html 33 | [`Rgba8`]: https://docs.rs/color/latest/color/struct.Rgba8.html 34 | 35 | 36 | Color is a Rust crate which implements color space conversions, targeting at least 37 | [CSS Color Level 4]. 38 | 39 | ## Main types 40 | 41 | The crate has two approaches to representing color in the Rust type system: a set of 42 | types with static color space as part of the types, and [`DynamicColor`] 43 | in which the color space is represented at runtime. 44 | 45 | The static color types come in three variants: [`OpaqueColor`] without an 46 | alpha channel, [`AlphaColor`] with a separate alpha channel, and [`PremulColor`] with 47 | premultiplied alpha. The last type is particularly useful for making interpolation and 48 | compositing more efficient. These have a marker type parameter, indicating which 49 | [`ColorSpace`] they are in. Conversion to another color space uses the `convert` method 50 | on each of these types. The static types are open-ended, as it's possible to implement 51 | this trait for new color spaces. 52 | 53 | ## Scope and goals 54 | 55 | Color in its entirety is an extremely deep and complex topic. It is completely impractical 56 | for a single crate to meet all color needs. The goal of this one is to strike a balance, 57 | providing color capabilities while also keeping things simple and efficient. 58 | 59 | The main purpose of this crate is to provide a good set of types for representing colors, 60 | along with conversions between them and basic manipulations, especially interpolation. A 61 | major inspiration is the [CSS Color Level 4] draft spec; we implement most of the operations 62 | and strive for correctness. 63 | 64 | A primary use case is rendering, including color conversions and methods for preparing 65 | gradients. The crate should also be suitable for document authoring and editing, as it 66 | contains methods for parsing and serializing colors with CSS Color 4 compatible syntax. 67 | 68 | Simplifications include: 69 | * Always using `f32` to represent component values. 70 | * Only handling 3-component color spaces (plus optional alpha). 71 | * Choosing a fixed, curated set of color spaces for dynamic color types. 72 | * Choosing linear sRGB as the central color space. 73 | * Keeping white point implicit in the general conversion operations. 74 | 75 | A number of other tasks are out of scope for this crate: 76 | * Print color spaces (CMYK). 77 | * Spectral colors. 78 | * Color spaces with more than 3 components generally. 79 | * [ICC] color profiles. 80 | * [ACES] color transforms. 81 | * Appearance models and other color science not needed for rendering. 82 | * Quantizing and packing to lower bit depths. 83 | 84 | The [`Rgba8`] and [`PremulRgba8`] types are a partial exception to this last item, as 85 | those representation are ubiquitous and requires special logic for serializing to 86 | maximize compatibility. 87 | 88 | Some of these capabilities may be added as other crates within the `color` repository, 89 | and we will also facilitate interoperability with other color crates in the Rust 90 | ecosystem as needed. 91 | 92 | ## Features 93 | 94 | - `std` (enabled by default): Get floating point functions from the standard library 95 | (likely using your target's libc). 96 | - `libm`: Use floating point implementations from [libm][]. 97 | - `bytemuck`: Implement traits from `bytemuck` on [`AlphaColor`], [`ColorSpaceTag`], 98 | [`HueDirection`], [`OpaqueColor`], [`PremulColor`], [`PremulRgba8`], and [`Rgba8`]. 99 | - `serde`: Implement `serde::Deserialize` and `serde::Serialize` on [`AlphaColor`], 100 | [`DynamicColor`], [`OpaqueColor`], [`PremulColor`], [`PremulRgba8`], and [`Rgba8`]. 101 | 102 | At least one of `std` and `libm` is required; `std` overrides `libm`. 103 | 104 | [CSS Color Level 4]: https://www.w3.org/TR/css-color-4/ 105 | [ICC]: https://color.org/ 106 | [ACES]: https://acescentral.com/ 107 | 108 | 109 | 110 | ## Minimum supported Rust Version (MSRV) 111 | 112 | This version of Color has been verified to compile with **Rust 1.82** and later. 113 | 114 | Future versions of Color might increase the Rust version requirement. 115 | It will not be treated as a breaking change and as such can even happen with small patch releases. 116 | 117 |
118 | Click here if compiling fails. 119 | 120 | As time has passed, some of Color's dependencies could have released versions with a higher Rust requirement. 121 | If you encounter a compilation issue due to a dependency and don't want to upgrade your Rust toolchain, then you could downgrade the dependency. 122 | 123 | ```sh 124 | # Use the problematic dependency's name and version 125 | cargo update -p package_name --precise 0.1.1 126 | ``` 127 | 128 |
129 | 130 | ## Community 131 | 132 | [![Linebender Zulip](https://img.shields.io/badge/Xi%20Zulip-%23color-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/channel/466849-color) 133 | 134 | Discussion of Color development happens in the [Linebender Zulip](https://xi.zulipchat.com/), specifically the [#color channel](https://xi.zulipchat.com/#narrow/channel/466849-color). 135 | All public content can be read without logging in. 136 | 137 | ## License 138 | 139 | Licensed under either of 140 | 141 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) 142 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 143 | 144 | at your option. 145 | 146 | ## Contribution 147 | 148 | Contributions are welcome by pull request. The [Rust code of conduct] applies. 149 | Please feel free to add your name to the [AUTHORS] file in any substantive pull request. 150 | 151 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions. 152 | 153 | [Rust Code of Conduct]: https://www.rust-lang.org/policies/code-of-conduct 154 | [AUTHORS]: ./AUTHORS 155 | -------------------------------------------------------------------------------- /color/examples/gradient.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Color Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | //! Gradient example 5 | //! 6 | //! Outputs a test page to stdout. 7 | //! 8 | //! Typical usage: 9 | //! 10 | //! ```sh 11 | //! cargo run --example gradient 'oklab(0.5 0.2 0)' 'rgb(0, 200, 0, 0.8)' oklab 12 | //! ``` 13 | 14 | use color::{gradient, ColorSpaceTag, DynamicColor, GradientIter, HueDirection, Srgb}; 15 | 16 | fn main() { 17 | let mut args = std::env::args().skip(1); 18 | let c1_s = args.next().expect("give color as arg"); 19 | let c1 = color::parse_color(&c1_s).expect("error parsing color 1"); 20 | let c2_s = args.next().expect("give 2 colors as arg"); 21 | let c2 = color::parse_color(&c2_s).expect("error parsing color 2"); 22 | let cs_s_raw = args.next(); 23 | let cs_s = cs_s_raw.as_deref().unwrap_or("srgb"); 24 | let cs: ColorSpaceTag = cs_s.parse().expect("error parsing color space"); 25 | let gradient: GradientIter = gradient(c1, c2, cs, HueDirection::default(), 0.02); 26 | println!(""); 27 | println!(""); 28 | println!(""); 29 | println!(""); 42 | println!(""); 43 | println!(""); 44 | println!("
{c1_s} {c2_s} {cs_s}
"); 45 | println!("
"); 46 | println!("
"); 47 | println!(""); 48 | println!(""); 49 | } 50 | -------------------------------------------------------------------------------- /color/examples/interp.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Color Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | //! Interpolation example 5 | //! 6 | //! Outputs a test page to stdout. 7 | //! 8 | //! Typical usage: 9 | //! 10 | //! ```sh 11 | //! cargo run --example interp 'oklab(0.5 0.2 0)' 'rgb(0, 200, 0, 0.8)' oklab 12 | //! ``` 13 | 14 | use color::{ColorSpaceTag, HueDirection}; 15 | 16 | fn main() { 17 | let mut args = std::env::args().skip(1); 18 | let c1_s = args.next().expect("give color as arg"); 19 | let c1 = color::parse_color(&c1_s).expect("error parsing color 1"); 20 | let c2_s = args.next().expect("give 2 colors as arg"); 21 | let c2 = color::parse_color(&c2_s).expect("error parsing color 2"); 22 | let cs_s_raw = args.next(); 23 | let cs_s = cs_s_raw.as_deref().unwrap_or("srgb"); 24 | let cs: ColorSpaceTag = cs_s.parse().expect("error parsing color space"); 25 | const N: usize = 20; 26 | println!(""); 27 | println!(""); 28 | println!(""); 29 | println!(""); 45 | println!(""); 46 | println!(""); 47 | println!("
{c1_s} {c2_s} {cs_s}
"); 48 | println!("
"); 49 | println!("
"); 50 | for i in 0..=N { 51 | print!(""); 52 | } 53 | println!(); 54 | println!("
"); 55 | println!(""); 56 | println!(""); 57 | } 58 | -------------------------------------------------------------------------------- /color/examples/parse.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Color Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | //! Parsing example 5 | //! 6 | //! Outputs debug strings for the parse to stdout 7 | //! 8 | //! Typical usage: 9 | //! 10 | //! ```sh 11 | //! cargo run --example parse 'oklab(0.5 0.2 0)' 12 | //! ``` 13 | 14 | use color::{AlphaColor, Hwb, Lab, Srgb}; 15 | 16 | fn main() { 17 | let arg = std::env::args().nth(1).expect("give color as arg"); 18 | match color::parse_color(&arg) { 19 | Ok(color) => { 20 | println!("display: {color}"); 21 | println!("debug: {color:?}"); 22 | let srgba: AlphaColor = color.to_alpha_color(); 23 | println!("{srgba:?}"); 24 | let lab: AlphaColor = color.to_alpha_color(); 25 | println!("{lab:?}"); 26 | let hwb: AlphaColor = color.to_alpha_color(); 27 | println!("{hwb:?}"); 28 | } 29 | Err(e) => println!("error: {e}"), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /color/make_x11_colors.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 the Color Authors 2 | # SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | # A utility to create a minimal perfect hash lookup table for 5 | # the x11 palette colors. 6 | # 7 | # This utility has been adapted from . 8 | # 9 | # See Steve Hanov's blog 10 | # [Throw away the keys: Easy, Minimal Perfect Hashing](https://stevehanov.ca/blog/?id=119) 11 | # for the basic technique. 12 | 13 | colors = [ 14 | ("aliceblue", (240, 248, 255, 255)), 15 | ("antiquewhite", (250, 235, 215, 255)), 16 | ("aqua", (0, 255, 255, 255)), 17 | ("aquamarine", (127, 255, 212, 255)), 18 | ("azure", (240, 255, 255, 255)), 19 | ("beige", (245, 245, 220, 255)), 20 | ("bisque", (255, 228, 196, 255)), 21 | ("black", (0, 0, 0, 255)), 22 | ("blanchedalmond", (255, 235, 205, 255)), 23 | ("blue", (0, 0, 255, 255)), 24 | ("blueviolet", (138, 43, 226, 255)), 25 | ("brown", (165, 42, 42, 255)), 26 | ("burlywood", (222, 184, 135, 255)), 27 | ("cadetblue", (95, 158, 160, 255)), 28 | ("chartreuse", (127, 255, 0, 255)), 29 | ("chocolate", (210, 105, 30, 255)), 30 | ("coral", (255, 127, 80, 255)), 31 | ("cornflowerblue", (100, 149, 237, 255)), 32 | ("cornsilk", (255, 248, 220, 255)), 33 | ("crimson", (220, 20, 60, 255)), 34 | ("cyan", (0, 255, 255, 255)), 35 | ("darkblue", (0, 0, 139, 255)), 36 | ("darkcyan", (0, 139, 139, 255)), 37 | ("darkgoldenrod", (184, 134, 11, 255)), 38 | ("darkgray", (169, 169, 169, 255)), 39 | ("darkgreen", (0, 100, 0, 255)), 40 | ("darkkhaki", (189, 183, 107, 255)), 41 | ("darkmagenta", (139, 0, 139, 255)), 42 | ("darkolivegreen", (85, 107, 47, 255)), 43 | ("darkorange", (255, 140, 0, 255)), 44 | ("darkorchid", (153, 50, 204, 255)), 45 | ("darkred", (139, 0, 0, 255)), 46 | ("darksalmon", (233, 150, 122, 255)), 47 | ("darkseagreen", (143, 188, 143, 255)), 48 | ("darkslateblue", (72, 61, 139, 255)), 49 | ("darkslategray", (47, 79, 79, 255)), 50 | ("darkturquoise", (0, 206, 209, 255)), 51 | ("darkviolet", (148, 0, 211, 255)), 52 | ("deeppink", (255, 20, 147, 255)), 53 | ("deepskyblue", (0, 191, 255, 255)), 54 | ("dimgray", (105, 105, 105, 255)), 55 | ("dodgerblue", (30, 144, 255, 255)), 56 | ("firebrick", (178, 34, 34, 255)), 57 | ("floralwhite", (255, 250, 240, 255)), 58 | ("forestgreen", (34, 139, 34, 255)), 59 | ("fuchsia", (255, 0, 255, 255)), 60 | ("gainsboro", (220, 220, 220, 255)), 61 | ("ghostwhite", (248, 248, 255, 255)), 62 | ("gold", (255, 215, 0, 255)), 63 | ("goldenrod", (218, 165, 32, 255)), 64 | ("gray", (128, 128, 128, 255)), 65 | ("green", (0, 128, 0, 255)), 66 | ("greenyellow", (173, 255, 47, 255)), 67 | ("honeydew", (240, 255, 240, 255)), 68 | ("hotpink", (255, 105, 180, 255)), 69 | ("indianred", (205, 92, 92, 255)), 70 | ("indigo", (75, 0, 130, 255)), 71 | ("ivory", (255, 255, 240, 255)), 72 | ("khaki", (240, 230, 140, 255)), 73 | ("lavender", (230, 230, 250, 255)), 74 | ("lavenderblush", (255, 240, 245, 255)), 75 | ("lawngreen", (124, 252, 0, 255)), 76 | ("lemonchiffon", (255, 250, 205, 255)), 77 | ("lightblue", (173, 216, 230, 255)), 78 | ("lightcoral", (240, 128, 128, 255)), 79 | ("lightcyan", (224, 255, 255, 255)), 80 | ("lightgoldenrodyellow", (250, 250, 210, 255)), 81 | ("lightgray", (211, 211, 211, 255)), 82 | ("lightgreen", (144, 238, 144, 255)), 83 | ("lightpink", (255, 182, 193, 255)), 84 | ("lightsalmon", (255, 160, 122, 255)), 85 | ("lightseagreen", (32, 178, 170, 255)), 86 | ("lightskyblue", (135, 206, 250, 255)), 87 | ("lightslategray", (119, 136, 153, 255)), 88 | ("lightsteelblue", (176, 196, 222, 255)), 89 | ("lightyellow", (255, 255, 224, 255)), 90 | ("lime", (0, 255, 0, 255)), 91 | ("limegreen", (50, 205, 50, 255)), 92 | ("linen", (250, 240, 230, 255)), 93 | ("magenta", (255, 0, 255, 255)), 94 | ("maroon", (128, 0, 0, 255)), 95 | ("mediumaquamarine", (102, 205, 170, 255)), 96 | ("mediumblue", (0, 0, 205, 255)), 97 | ("mediumorchid", (186, 85, 211, 255)), 98 | ("mediumpurple", (147, 112, 219, 255)), 99 | ("mediumseagreen", (60, 179, 113, 255)), 100 | ("mediumslateblue", (123, 104, 238, 255)), 101 | ("mediumspringgreen", (0, 250, 154, 255)), 102 | ("mediumturquoise", (72, 209, 204, 255)), 103 | ("mediumvioletred", (199, 21, 133, 255)), 104 | ("midnightblue", (25, 25, 112, 255)), 105 | ("mintcream", (245, 255, 250, 255)), 106 | ("mistyrose", (255, 228, 225, 255)), 107 | ("moccasin", (255, 228, 181, 255)), 108 | ("navajowhite", (255, 222, 173, 255)), 109 | ("navy", (0, 0, 128, 255)), 110 | ("oldlace", (253, 245, 230, 255)), 111 | ("olive", (128, 128, 0, 255)), 112 | ("olivedrab", (107, 142, 35, 255)), 113 | ("orange", (255, 165, 0, 255)), 114 | ("orangered", (255, 69, 0, 255)), 115 | ("orchid", (218, 112, 214, 255)), 116 | ("palegoldenrod", (238, 232, 170, 255)), 117 | ("palegreen", (152, 251, 152, 255)), 118 | ("paleturquoise", (175, 238, 238, 255)), 119 | ("palevioletred", (219, 112, 147, 255)), 120 | ("papayawhip", (255, 239, 213, 255)), 121 | ("peachpuff", (255, 218, 185, 255)), 122 | ("peru", (205, 133, 63, 255)), 123 | ("pink", (255, 192, 203, 255)), 124 | ("plum", (221, 160, 221, 255)), 125 | ("powderblue", (176, 224, 230, 255)), 126 | ("purple", (128, 0, 128, 255)), 127 | ("rebeccapurple", (102, 51, 153, 255)), 128 | ("red", (255, 0, 0, 255)), 129 | ("rosybrown", (188, 143, 143, 255)), 130 | ("royalblue", (65, 105, 225, 255)), 131 | ("saddlebrown", (139, 69, 19, 255)), 132 | ("salmon", (250, 128, 114, 255)), 133 | ("sandybrown", (244, 164, 96, 255)), 134 | ("seagreen", (46, 139, 87, 255)), 135 | ("seashell", (255, 245, 238, 255)), 136 | ("sienna", (160, 82, 45, 255)), 137 | ("silver", (192, 192, 192, 255)), 138 | ("skyblue", (135, 206, 235, 255)), 139 | ("slateblue", (106, 90, 205, 255)), 140 | ("slategray", (112, 128, 144, 255)), 141 | ("snow", (255, 250, 250, 255)), 142 | ("springgreen", (0, 255, 127, 255)), 143 | ("steelblue", (70, 130, 180, 255)), 144 | ("tan", (210, 180, 140, 255)), 145 | ("teal", (0, 128, 128, 255)), 146 | ("thistle", (216, 191, 216, 255)), 147 | ("tomato", (255, 99, 71, 255)), 148 | ("transparent", (0, 0, 0, 0)), 149 | ("turquoise", (64, 224, 208, 255)), 150 | ("violet", (238, 130, 238, 255)), 151 | ("wheat", (245, 222, 179, 255)), 152 | ("white", (255, 255, 255, 255)), 153 | ("whitesmoke", (245, 245, 245, 255)), 154 | ("yellow", (255, 255, 0, 255)), 155 | ("yellowgreen", (154, 205, 50, 255)), 156 | ] 157 | 158 | def weak_hash_string(s): 159 | mask_32 = 0xffffffff 160 | h = 0 161 | for char in s: 162 | h = (9 * h + ord(char)) & mask_32 163 | return h 164 | 165 | # Guaranteed to be less than n. 166 | def weak_hash(s, salt, n): 167 | x = weak_hash_string(s[0]) 168 | # This is hash based on the theory that multiplication is efficient 169 | mask_32 = 0xffffffff 170 | y = ((x + salt) * 2654435769) & mask_32 171 | y ^= x 172 | return (y * n) >> 32 173 | 174 | # Compute minimal perfect hash function, d can be either a dict or list of keys. 175 | def minimal_perfect_hash(d): 176 | n = len(d) 177 | buckets = dict((h, []) for h in range(n)) 178 | for key in d: 179 | h = weak_hash(key, 0, n) 180 | buckets[h].append(key) 181 | bsorted = [(len(buckets[h]), h) for h in range(n)] 182 | bsorted.sort(reverse = True) 183 | claimed = [False] * n 184 | salts = [0] * n 185 | keys = [0] * n 186 | for (bucket_size, h) in bsorted: 187 | # Note: the traditional perfect hashing approach would also special-case 188 | # bucket_size == 1 here and assign any empty slot, rather than iterating 189 | # until rehash finds an empty slot. But we're not doing that so we can 190 | # avoid the branch. 191 | if bucket_size == 0: 192 | break 193 | else: 194 | for salt in range(1, 32768): 195 | rehashes = [weak_hash(key, salt, n) for key in buckets[h]] 196 | # Make sure there are no rehash collisions within this bucket. 197 | if all(not claimed[hash] for hash in rehashes): 198 | if len(set(rehashes)) < bucket_size: 199 | continue 200 | salts[h] = salt 201 | for key in buckets[h]: 202 | rehash = weak_hash(key, salt, n) 203 | claimed[rehash] = True 204 | keys[rehash] = key 205 | break 206 | if salts[h] == 0: 207 | print("minimal perfect hashing failed") 208 | # Note: if this happens (because of unfortunate data), then there are 209 | # a few things that could be done. First, the hash function could be 210 | # tweaked. Second, the bucket order could be scrambled (especially the 211 | # singletons). Right now, the buckets are sorted, which has the advantage 212 | # of being deterministic. 213 | # 214 | # As a more extreme approach, the singleton bucket optimization could be 215 | # applied (give the direct address for singleton buckets, rather than 216 | # relying on a rehash). That is definitely the more standard approach in 217 | # the minimal perfect hashing literature, but in testing the branch was a 218 | # significant slowdown. 219 | exit(1) 220 | return (salts, keys) 221 | 222 | (salts, keys) = minimal_perfect_hash(colors) 223 | n = len(colors) 224 | print("""// Copyright 2024 the Color Authors 225 | // SPDX-License-Identifier: Apache-2.0 OR MIT 226 | 227 | // This file was auto-generated by make_x11_colors.py. Do not hand-edit. 228 | """) 229 | print(f"const SALTS: [u8; {n}] = [") 230 | obuf = " " 231 | for salt in salts: 232 | word = f" {salt}," 233 | if len(obuf) + len(word) >= 100: 234 | print(obuf) 235 | obuf = " " 236 | obuf += word 237 | if len(obuf) > 3: 238 | print(obuf) 239 | print("];") 240 | print() 241 | 242 | print(f"pub(crate) const NAMES: [&str; {n}] = [") 243 | for (name, rgba) in keys: 244 | print(f' "{name}",') 245 | print("];") 246 | print(f""" 247 | /// RGBA8 color components of the named X11 colors, in the same order as [`NAMES`]. 248 | /// 249 | /// Use [`lookup_palette_index`] to efficiently find the color components for a given color name 250 | /// string. 251 | pub(crate) const COLORS: [[u8; 4]; {n}] = [""") 252 | for (name, rgba) in keys: 253 | print(f' {list(rgba)},') 254 | print("];") 255 | print(""" 256 | /// Hash the 32 bit key into a value less than `n`, adding salt. 257 | /// 258 | /// This is basically the weakest hash we can get away with that 259 | /// still distinguishes all the values. 260 | #[inline] 261 | fn weak_hash(key: u32, salt: u32, n: usize) -> usize { 262 | let y = key.wrapping_add(salt).wrapping_mul(2654435769); 263 | let y = y ^ key; 264 | (((y as u64) * (n as u64)) >> 32) as usize 265 | } 266 | 267 | /// Given a named color (e.g., "red", "mediumorchid"), returns the index of that color into 268 | /// [`COLORS`] and [`NAMES`]. 269 | pub(crate) fn lookup_palette_index(s: &str) -> Option { 270 | let mut key = 0_u32; 271 | for b in s.as_bytes() { 272 | key = key.wrapping_mul(9).wrapping_add(*b as u32); 273 | } 274 | let salt = SALTS[weak_hash(key, 0, SALTS.len())] as u32; 275 | let ix = weak_hash(key, salt, SALTS.len()); 276 | if s == NAMES[ix] { 277 | Some(ix) 278 | } else { 279 | None 280 | } 281 | }""") 282 | -------------------------------------------------------------------------------- /color/src/cache_key.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Color Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | //! Hashing and other caching utilities for Color types. 5 | //! 6 | //! In this crate, colors are implemented using `f32`. 7 | //! This means that color types aren't `Hash` and `Eq` for good reasons: 8 | //! 9 | //! - Equality on these types is not reflexive (consider [NaN](f32::NAN)). 10 | //! - Certain values have two representations (`-0` and `+0` are both zero). 11 | //! 12 | //! However, it is still useful to create caches which key off these values. 13 | //! These are caches which don't have any semantic meaning, but instead 14 | //! are used to avoid redundant calculations or storage. 15 | //! 16 | //! Color supports creating these caches by using [`CacheKey`] as the key in 17 | //! your cache. 18 | //! `T` is the key type (i.e. a color) which you want to use as the key. 19 | //! This `T` must implement both [`BitHash`] and [`BitEq`], which are 20 | //! versions of the standard `Hash` and `Eq` traits which support implementations 21 | //! for floating point numbers which might be unexpected outside of a caching context. 22 | 23 | use core::hash::{Hash, Hasher}; 24 | 25 | /// A key usable in a hashmap to compare the bit representation 26 | /// types containing colors. 27 | /// 28 | /// See the [module level docs](self) for more information. 29 | #[derive(Debug, Copy, Clone)] 30 | #[repr(transparent)] 31 | pub struct CacheKey(pub T); 32 | 33 | impl CacheKey { 34 | /// Create a new `CacheKey`. 35 | /// 36 | /// All fields are public, so the struct constructor can also be used. 37 | pub fn new(value: T) -> Self { 38 | Self(value) 39 | } 40 | 41 | /// Get the inner value. 42 | pub fn into_inner(self) -> T { 43 | self.0 44 | } 45 | } 46 | 47 | // This module exists for these implementations: 48 | 49 | // `BitEq` is an equivalence relation, just maybe not the one you'd expect. 50 | impl Eq for CacheKey {} 51 | impl PartialEq for CacheKey { 52 | fn eq(&self, other: &Self) -> bool { 53 | self.0.bit_eq(&other.0) 54 | } 55 | } 56 | // If we implement Eq, BitEq's implementation matches that of the hash. 57 | impl Hash for CacheKey { 58 | fn hash(&self, state: &mut H) { 59 | self.0.bit_hash(state); 60 | } 61 | } 62 | 63 | /// A hash implementation for types which normally wouldn't have one, 64 | /// implemented using a hash of the bitwise equivalent types when needed. 65 | /// 66 | /// If a type is `BitHash` and `BitEq`, then it is important that the following property holds: 67 | /// 68 | /// ```text 69 | /// k1 biteq k2 -> bithash(k1) == bithash(k2) 70 | /// ``` 71 | /// 72 | /// See the docs on [`Hash`] for more information. 73 | /// 74 | /// Useful for creating caches based on exact values. 75 | /// See the [module level docs](self) for more information. 76 | pub trait BitHash { 77 | /// Feeds this value into the given [`Hasher`]. 78 | fn bit_hash(&self, state: &mut H); 79 | // Intentionally no hash_slice for simplicity. 80 | } 81 | 82 | impl BitHash for f32 { 83 | fn bit_hash(&self, state: &mut H) { 84 | self.to_bits().hash(state); 85 | } 86 | } 87 | impl BitHash for [T; N] { 88 | fn bit_hash(&self, state: &mut H) { 89 | self[..].bit_hash(state); 90 | } 91 | } 92 | 93 | impl BitHash for [T] { 94 | fn bit_hash(&self, state: &mut H) { 95 | // In theory, we should use `write_length_prefix`, which is unstable: 96 | // https://github.com/rust-lang/rust/issues/96762 97 | // We could do that by (unsafely) casting to `[CacheKey]`, then 98 | // using `Hash::hash` on the resulting slice. 99 | state.write_usize(self.len()); 100 | for piece in self { 101 | piece.bit_hash(state); 102 | } 103 | } 104 | } 105 | 106 | impl BitHash for &T { 107 | fn bit_hash(&self, state: &mut H) { 108 | T::bit_hash(*self, state); 109 | } 110 | } 111 | 112 | // Don't BitHash tuples, not that important 113 | 114 | /// An equivalence relation for types which normally wouldn't have 115 | /// one, implemented using a bitwise comparison for floating point 116 | /// values. 117 | /// 118 | /// See the docs on [`Eq`] for more information. 119 | /// 120 | /// Useful for creating caches based on exact values. 121 | /// See the [module level docs](self) for more information. 122 | pub trait BitEq { 123 | /// Returns `true` if `self` is equal to `other`. 124 | /// 125 | /// This need not use the semantically natural comparison operation 126 | /// for the type; indeed floating point types should implement this 127 | /// by comparing bit values. 128 | fn bit_eq(&self, other: &Self) -> bool; 129 | // Intentionally no bit_ne as would be added complexity for little gain 130 | } 131 | 132 | impl BitEq for f32 { 133 | fn bit_eq(&self, other: &Self) -> bool { 134 | self.to_bits() == other.to_bits() 135 | } 136 | } 137 | 138 | impl BitEq for [T; N] { 139 | fn bit_eq(&self, other: &Self) -> bool { 140 | for i in 0..N { 141 | if !self[i].bit_eq(&other[i]) { 142 | return false; 143 | } 144 | } 145 | true 146 | } 147 | } 148 | 149 | impl BitEq for [T] { 150 | fn bit_eq(&self, other: &Self) -> bool { 151 | if self.len() != other.len() { 152 | return false; 153 | } 154 | for (a, b) in self.iter().zip(other) { 155 | if !a.bit_eq(b) { 156 | return false; 157 | } 158 | } 159 | true 160 | } 161 | } 162 | 163 | impl BitEq for &T { 164 | fn bit_eq(&self, other: &Self) -> bool { 165 | T::bit_eq(*self, *other) 166 | } 167 | } 168 | 169 | // Don't BitEq tuples, not that important 170 | 171 | // Ideally we'd also have these implementations, but they cause conflicts 172 | // (in case std ever went mad and implemented Eq for f32, for example). 173 | // impl BitHash for T {...} 174 | // impl BitEq for T {...} 175 | 176 | #[cfg(test)] 177 | mod tests { 178 | extern crate std; 179 | use super::CacheKey; 180 | use crate::{parse_color, DynamicColor}; 181 | use std::collections::HashMap; 182 | 183 | #[test] 184 | fn bit_eq_hashmap() { 185 | let mut map: HashMap, i32> = HashMap::new(); 186 | // The implementation for f32 is the base case. 187 | assert!(map.insert(CacheKey(0.0), 0).is_none()); 188 | assert!(map.insert(CacheKey(-0.0), -1).is_none()); 189 | assert!(map.insert(CacheKey(1.0), 1).is_none()); 190 | assert!(map.insert(CacheKey(0.5), 5).is_none()); 191 | 192 | assert_eq!(map.get(&CacheKey(1.0)).unwrap(), &1); 193 | assert_eq!(map.get(&CacheKey(0.0)).unwrap(), &0); 194 | assert_eq!(map.remove(&CacheKey(-0.0)).unwrap(), -1); 195 | assert!(!map.contains_key(&CacheKey(-0.0))); 196 | assert_eq!(map.get(&CacheKey(0.5)).unwrap(), &5); 197 | } 198 | #[test] 199 | fn bit_eq_color_hashmap() { 200 | let mut map: HashMap, i32> = HashMap::new(); 201 | 202 | let red = parse_color("red").unwrap(); 203 | let red2 = parse_color("red").unwrap(); 204 | let other = parse_color("oklab(0.4 0.2 0.6)").unwrap(); 205 | assert!(map.insert(CacheKey(red), 10).is_none()); 206 | assert_eq!(map.insert(CacheKey(red2), 5).unwrap(), 10); 207 | assert!(map.insert(CacheKey(other), 15).is_none()); 208 | assert_eq!(map.get(&CacheKey(other)).unwrap(), &15); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /color/src/chromaticity.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Color Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use crate::{matdiagmatmul, matmatmul, matvecmul}; 5 | 6 | /// CIE `xy` chromaticity, specifying a color in the XYZ color space, but not its luminosity. 7 | /// 8 | /// An absolute color can be specified by adding a luminosity coordinate `Y` as in `xyY`. An `XYZ` 9 | /// color can be calculated from `xyY` as follows. 10 | /// 11 | /// ```text 12 | /// X = Y/y * x 13 | /// Y = Y 14 | /// Z = Y/y * (1 - x - y) 15 | /// ``` 16 | #[derive(Clone, Copy, Debug, PartialEq)] 17 | pub struct Chromaticity { 18 | /// The x-coordinate of the CIE `xy` chromaticity. 19 | pub x: f32, 20 | 21 | /// The y-coordinate of the CIE `xy` chromaticity. 22 | pub y: f32, 23 | } 24 | 25 | impl Chromaticity { 26 | /// The CIE D65 white point under the standard 2° observer. 27 | /// 28 | /// This is a common white point for color spaces targeting monitors. 29 | /// 30 | /// The white point's chromaticities are truncated to four digits here, as specified by the 31 | /// CSS Color 4 specification, and following most color spaces using this white point. 32 | pub const D65: Self = Self { 33 | x: 0.3127, 34 | y: 0.3290, 35 | }; 36 | 37 | /// The CIE D50 white point under the standard 2° observer. 38 | /// 39 | /// The white point's chromaticities are truncated to four digits here, as specified by the 40 | /// CSS Color 4 specification, and following most color spaces using this white point. 41 | pub const D50: Self = Self { 42 | x: 0.3457, 43 | y: 0.3585, 44 | }; 45 | 46 | /// The [ACES white point][aceswp]. 47 | /// 48 | /// This is the reference white of [ACEScg](crate::AcesCg) and [ACES2065-1](crate::Aces2065_1). 49 | /// The white point is near the D60 white point under the standard 2° observer. 50 | /// 51 | /// [aceswp]: https://docs.acescentral.com/tb/white-point 52 | pub const ACES: Self = Self { 53 | x: 0.32168, 54 | y: 0.33767, 55 | }; 56 | 57 | /// Convert the `xy` chromaticities to XYZ, assuming `xyY` with `Y=1`. 58 | pub(crate) const fn to_xyz(self) -> [f32; 3] { 59 | let y_recip = 1. / self.y; 60 | [self.x * y_recip, 1., (1. - self.x - self.y) * y_recip] 61 | } 62 | 63 | /// Calculate the 3x3 linear Bradford chromatic adaptation matrix from linear sRGB space. 64 | /// 65 | /// This calculates the matrix going from a reference white of `self` to a reference white of 66 | /// `to`. 67 | pub(crate) const fn linear_srgb_chromatic_adaptation_matrix(self, to: Self) -> [[f32; 3]; 3] { 68 | let bradford_source = matvecmul(&Self::XYZ_TO_BRADFORD, self.to_xyz()); 69 | let bradford_dest = matvecmul(&Self::XYZ_TO_BRADFORD, to.to_xyz()); 70 | 71 | matmatmul( 72 | &matdiagmatmul( 73 | &Self::BRADFORD_TO_SRGB, 74 | [ 75 | bradford_dest[0] / bradford_source[0], 76 | bradford_dest[1] / bradford_source[1], 77 | bradford_dest[2] / bradford_source[2], 78 | ], 79 | ), 80 | &Self::SRGB_TO_BRADFORD, 81 | ) 82 | } 83 | 84 | /// `XYZ_to_Bradford * lin_sRGB_to_XYZ` 85 | const SRGB_TO_BRADFORD: [[f32; 3]; 3] = [ 86 | [ 87 | 1_298_421_353. / 3_072_037_500., 88 | 172_510_403. / 351_090_000., 89 | 32_024_671. / 1_170_300_000., 90 | ], 91 | [ 92 | 85_542_113. / 1_536_018_750., 93 | 7_089_448_151. / 7_372_890_000., 94 | 244_246_729. / 10_532_700_000., 95 | ], 96 | [ 97 | 131_355_661. / 6_144_075_000., 98 | 71_798_777. / 819_210_000., 99 | 3_443_292_119. / 3_510_900_000., 100 | ], 101 | ]; 102 | 103 | /// `XYZ_to_lin_sRGB * Bradford_to_XYZ` 104 | const BRADFORD_TO_SRGB: [[f32; 3]; 3] = [ 105 | [ 106 | 3_597_831_250_055_000. / 1_417_335_035_684_489., 107 | -1_833_298_161_702_000. / 1_417_335_035_684_489., 108 | -57_038_163_791_000. / 1_417_335_035_684_489., 109 | ], 110 | [ 111 | -4_593_417_841_453_000. / 31_461_687_363_220_151., 112 | 35_130_825_086_032_200. / 31_461_687_363_220_151., 113 | -702_492_905_752_400. / 31_461_687_363_220_151., 114 | ], 115 | [ 116 | -191_861_334_350_000. / 4_536_975_728_019_583., 117 | -324_802_409_790_000. / 4_536_975_728_019_583., 118 | 4_639_090_845_380_000. / 4_536_975_728_019_583., 119 | ], 120 | ]; 121 | 122 | const XYZ_TO_BRADFORD: [[f32; 3]; 3] = [ 123 | [0.8951, 0.2664, -0.1614], 124 | [-0.7502, 1.7135, 0.0367], 125 | [0.0389, -0.0685, 1.0296], 126 | ]; 127 | } 128 | -------------------------------------------------------------------------------- /color/src/dynamic.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Color Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | //! CSS colors and syntax. 5 | 6 | use crate::{ 7 | cache_key::{BitEq, BitHash}, 8 | color::{add_alpha, fixup_hues_for_interpolate, split_alpha}, 9 | AlphaColor, Chromaticity, ColorSpace, ColorSpaceLayout, ColorSpaceTag, Flags, HueDirection, 10 | LinearSrgb, Missing, 11 | }; 12 | use core::hash::{Hash, Hasher}; 13 | 14 | /// A color with a [color space tag] decided at runtime. 15 | /// 16 | /// This type is roughly equivalent to [`AlphaColor`] except with a tag 17 | /// for color space as opposed being determined at compile time. It can 18 | /// also represent missing components, which are a feature of the CSS 19 | /// Color 4 spec. 20 | /// 21 | /// Missing components are mostly useful for interpolation, and in that 22 | /// context take the value of the other color being interpolated. For 23 | /// example, interpolating a color in [Oklch] with `oklch(none 0 none)` 24 | /// fades the color saturation, ending in a gray with the same lightness. 25 | /// 26 | /// In other contexts, missing colors are interpreted as a zero value. 27 | /// When manipulating components directly, setting them nonzero when the 28 | /// corresponding missing flag is set may yield unexpected results. 29 | /// 30 | /// [color space tag]: ColorSpaceTag 31 | /// [Oklch]: crate::Oklch 32 | #[derive(Clone, Copy, Debug)] 33 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 34 | pub struct DynamicColor { 35 | /// The color space. 36 | pub cs: ColorSpaceTag, 37 | /// The state of this color, tracking whether it has missing components and how it was 38 | /// constructed. See the documentation of [`Flags`] for more information. 39 | pub flags: Flags, 40 | /// The components. 41 | /// 42 | /// The first three components are interpreted according to the 43 | /// color space tag. The fourth component is alpha, interpreted 44 | /// as separate alpha. 45 | pub components: [f32; 4], 46 | } 47 | 48 | /// An intermediate struct used for interpolating between colors. 49 | /// 50 | /// This is the return value of [`DynamicColor::interpolate`]. 51 | #[derive(Clone, Copy)] 52 | #[expect( 53 | missing_debug_implementations, 54 | reason = "it's an intermediate struct, only used for eval" 55 | )] 56 | pub struct Interpolator { 57 | premul1: [f32; 3], 58 | alpha1: f32, 59 | delta_premul: [f32; 3], 60 | delta_alpha: f32, 61 | cs: ColorSpaceTag, 62 | missing: Missing, 63 | } 64 | 65 | impl DynamicColor { 66 | /// Convert to `AlphaColor` with a static color space. 67 | /// 68 | /// Missing components are interpreted as 0. 69 | #[must_use] 70 | pub fn to_alpha_color(self) -> AlphaColor { 71 | if let Some(cs) = CS::TAG { 72 | AlphaColor::new(self.convert(cs).components) 73 | } else { 74 | self.to_alpha_color::().convert() 75 | } 76 | } 77 | 78 | /// Convert from `AlphaColor`. 79 | #[must_use] 80 | pub fn from_alpha_color(color: AlphaColor) -> Self { 81 | if let Some(cs) = CS::TAG { 82 | Self { 83 | cs, 84 | flags: Flags::default(), 85 | components: color.components, 86 | } 87 | } else { 88 | Self::from_alpha_color(color.convert::()) 89 | } 90 | } 91 | 92 | /// The const-generic parameter `ABSOLUTE` indicates whether the conversion performs chromatic 93 | /// adaptation. When `ABSOLUTE` is `true`, no chromatic adaptation is performed. 94 | fn convert_impl(self, cs: ColorSpaceTag) -> Self { 95 | if self.cs == cs { 96 | // Note: §12 suggests that changing powerless to missing happens 97 | // even when the color is already in the interpolation color space, 98 | // but Chrome and color.js don't seem do to that. 99 | self 100 | } else { 101 | let (opaque, alpha) = split_alpha(self.components); 102 | let mut components = if ABSOLUTE { 103 | add_alpha(self.cs.convert_absolute(cs, opaque), alpha) 104 | } else { 105 | add_alpha(self.cs.convert(cs, opaque), alpha) 106 | }; 107 | // Reference: §12.2 of Color 4 spec 108 | let missing = if !self.flags.missing().is_empty() { 109 | if self.cs.same_analogous(cs) { 110 | for (i, component) in components.iter_mut().enumerate() { 111 | if self.flags.missing().contains(i) { 112 | *component = 0.0; 113 | } 114 | } 115 | self.flags.missing() 116 | } else { 117 | let mut missing = self.flags.missing() & Missing::single(3); 118 | if self.cs.h_missing(self.flags.missing()) { 119 | cs.set_h_missing(&mut missing, &mut components); 120 | } 121 | if self.cs.c_missing(self.flags.missing()) { 122 | cs.set_c_missing(&mut missing, &mut components); 123 | } 124 | if self.cs.l_missing(self.flags.missing()) { 125 | cs.set_l_missing(&mut missing, &mut components); 126 | } 127 | missing 128 | } 129 | } else { 130 | Missing::default() 131 | }; 132 | let mut result = Self { 133 | cs, 134 | flags: Flags::from_missing(missing), 135 | components, 136 | }; 137 | result.powerless_to_missing(); 138 | result 139 | } 140 | } 141 | 142 | #[must_use] 143 | /// Convert to a different color space. 144 | pub fn convert(self, cs: ColorSpaceTag) -> Self { 145 | self.convert_impl::(cs) 146 | } 147 | 148 | #[must_use] 149 | /// Convert to a different color space, without chromatic adaptation. 150 | /// 151 | /// For most use-cases you should consider using the chromatically-adapting 152 | /// [`DynamicColor::convert`] instead. See the documentation on 153 | /// [`ColorSpace::convert_absolute`] for more information. 154 | pub fn convert_absolute(self, cs: ColorSpaceTag) -> Self { 155 | self.convert_impl::(cs) 156 | } 157 | 158 | #[must_use] 159 | /// Chromatically adapt the color between the given white point chromaticities. 160 | /// 161 | /// The color is assumed to be under a reference white point of `from` and is chromatically 162 | /// adapted to the given white point `to`. The linear Bradford transform is used to perform the 163 | /// chromatic adaptation. 164 | pub fn chromatically_adapt(self, from: Chromaticity, to: Chromaticity) -> Self { 165 | if from == to { 166 | return self; 167 | } 168 | 169 | // Treat missing components as zero, as per CSS Color Module Level 4 § 4.4. 170 | let (opaque, alpha) = split_alpha(self.zero_missing_components().components); 171 | let components = add_alpha(self.cs.chromatically_adapt(opaque, from, to), alpha); 172 | Self { 173 | cs: self.cs, 174 | // After chromatically adapting the color, components may no longer be missing. Don't 175 | // forward the flags. 176 | flags: Flags::default(), 177 | components, 178 | } 179 | } 180 | 181 | /// Set any missing components to zero. 182 | /// 183 | /// We have a soft invariant that any bit set in the missing bitflag has 184 | /// a corresponding component which is 0. This method restores that 185 | /// invariant after manipulation which might invalidate it. 186 | fn zero_missing_components(mut self) -> Self { 187 | if !self.flags.missing().is_empty() { 188 | for (i, component) in self.components.iter_mut().enumerate() { 189 | if self.flags.missing().contains(i) { 190 | *component = 0.0; 191 | } 192 | } 193 | } 194 | self 195 | } 196 | 197 | /// Multiply alpha by the given factor. 198 | /// 199 | /// If the alpha channel is missing, then the new alpha channel 200 | /// will be ignored and the color returned unchanged. 201 | #[must_use] 202 | pub const fn multiply_alpha(self, rhs: f32) -> Self { 203 | if self.flags.missing().contains(3) { 204 | self 205 | } else { 206 | let (opaque, alpha) = split_alpha(self.components); 207 | Self { 208 | cs: self.cs, 209 | flags: Flags::from_missing(self.flags.missing()), 210 | components: add_alpha(opaque, alpha * rhs), 211 | } 212 | } 213 | } 214 | 215 | /// Set the alpha channel. 216 | /// 217 | /// This replaces the existing alpha channel. To scale or 218 | /// or otherwise modify the existing alpha channel, use 219 | /// [`DynamicColor::multiply_alpha`] or [`DynamicColor::map`]. 220 | /// 221 | /// If the alpha channel is missing, then the new alpha channel 222 | /// will be ignored and the color returned unchanged. 223 | /// 224 | /// ``` 225 | /// # use color::{parse_color, Srgb}; 226 | /// let c = parse_color("lavenderblush").unwrap().with_alpha(0.7); 227 | /// assert_eq!(0.7, c.to_alpha_color::().split().1); 228 | /// ``` 229 | #[must_use] 230 | pub const fn with_alpha(self, alpha: f32) -> Self { 231 | if self.flags.missing().contains(3) { 232 | self 233 | } else { 234 | let (opaque, _alpha) = split_alpha(self.components); 235 | Self { 236 | cs: self.cs, 237 | flags: Flags::from_missing(self.flags.missing()), 238 | components: add_alpha(opaque, alpha), 239 | } 240 | } 241 | } 242 | 243 | /// Scale the chroma by the given amount. 244 | /// 245 | /// See [`ColorSpace::scale_chroma`] for more details. 246 | #[must_use] 247 | pub fn scale_chroma(self, scale: f32) -> Self { 248 | let (opaque, alpha) = split_alpha(self.components); 249 | let components = self.cs.scale_chroma(opaque, scale); 250 | 251 | let mut flags = self.flags; 252 | flags.discard_name(); 253 | Self { 254 | cs: self.cs, 255 | flags, 256 | components: add_alpha(components, alpha), 257 | } 258 | .zero_missing_components() 259 | } 260 | 261 | /// Clip the color's components to fit within the natural gamut of the color space, and clamp 262 | /// the color's alpha to be in the range `[0, 1]`. 263 | /// 264 | /// See [`ColorSpace::clip`] for more details. 265 | #[must_use] 266 | pub fn clip(self) -> Self { 267 | let (opaque, alpha) = split_alpha(self.components); 268 | let components = self.cs.clip(opaque); 269 | let alpha = alpha.clamp(0., 1.); 270 | Self { 271 | cs: self.cs, 272 | flags: self.flags, 273 | components: add_alpha(components, alpha), 274 | } 275 | } 276 | 277 | fn premultiply_split(self) -> ([f32; 3], f32) { 278 | // Reference: §12.3 of Color 4 spec 279 | let (opaque, alpha) = split_alpha(self.components); 280 | let premul = if alpha == 1.0 || self.flags.missing().contains(3) { 281 | opaque 282 | } else { 283 | self.cs.layout().scale(opaque, alpha) 284 | }; 285 | (premul, alpha) 286 | } 287 | 288 | fn powerless_to_missing(&mut self) { 289 | // Note: the spec seems vague on the details of what this should do, 290 | // and there is some controversy in discussion threads. For example, 291 | // in Lab-like spaces, if L is 0 do the other components become powerless? 292 | 293 | // Note: we use hard-coded epsilons to check for approximate equality here, but these do 294 | // not account for the normal value range of components. It might be somewhat more correct 295 | // to, e.g., consider `0.000_01` approximately equal to `0` for a component with the 296 | // natural range `0-100`, but not for a component with the natural range `0-0.5`. 297 | 298 | match self.cs { 299 | // See CSS Color Module level 4 § 7, § 9.3, and § 9.4 (HSL, LCH, Oklch). 300 | ColorSpaceTag::Hsl | ColorSpaceTag::Lch | ColorSpaceTag::Oklch 301 | if self.components[1] < 1e-6 => 302 | { 303 | let mut missing = self.flags.missing(); 304 | self.cs.set_h_missing(&mut missing, &mut self.components); 305 | self.flags.set_missing(missing); 306 | } 307 | 308 | // See CSS Color Module level 4 § 8 (HWB). 309 | ColorSpaceTag::Hwb if self.components[1] + self.components[2] > 100. - 1e-4 => { 310 | let mut missing = self.flags.missing(); 311 | self.cs.set_h_missing(&mut missing, &mut self.components); 312 | self.flags.set_missing(missing); 313 | } 314 | _ => {} 315 | } 316 | } 317 | 318 | /// Interpolate two colors. 319 | /// 320 | /// The colors are interpolated linearly from `self` to `other` in the color space given by 321 | /// `cs`. When interpolating in a cylindrical color space, the hue can be interpolated in 322 | /// multiple ways. The [`direction`](`HueDirection`) parameter controls the way in which the 323 | /// hue is interpolated. 324 | /// 325 | /// The interpolation proceeds according to [CSS Color Module Level 4 § 12][css-sec]. 326 | /// 327 | /// This method does a bunch of precomputation, resulting in an [`Interpolator`] object that 328 | /// can be evaluated at various `t` values. 329 | /// 330 | /// [css-sec]: https://www.w3.org/TR/css-color-4/#interpolation 331 | /// 332 | /// # Example 333 | /// 334 | /// ```rust 335 | /// use color::{AlphaColor, ColorSpaceTag, DynamicColor, HueDirection, Srgb}; 336 | /// 337 | /// let start = DynamicColor::from_alpha_color(AlphaColor::::new([1., 0., 0., 1.])); 338 | /// let end = DynamicColor::from_alpha_color(AlphaColor::::new([0., 1., 0., 1.])); 339 | /// 340 | /// let interp = start.interpolate(end, ColorSpaceTag::Hsl, HueDirection::Increasing); 341 | /// let mid = interp.eval(0.5); 342 | /// assert_eq!(mid.cs, ColorSpaceTag::Hsl); 343 | /// assert!((mid.components[0] - 60.).abs() < 0.01); 344 | /// ``` 345 | pub fn interpolate( 346 | self, 347 | other: Self, 348 | cs: ColorSpaceTag, 349 | direction: HueDirection, 350 | ) -> Interpolator { 351 | let mut a = self.convert(cs); 352 | let mut b = other.convert(cs); 353 | let a_missing = a.flags.missing(); 354 | let b_missing = b.flags.missing(); 355 | let missing = a_missing & b_missing; 356 | if a_missing != b_missing { 357 | for i in 0..4 { 358 | if (a_missing & !b_missing).contains(i) { 359 | a.components[i] = b.components[i]; 360 | } else if (!a_missing & b_missing).contains(i) { 361 | b.components[i] = a.components[i]; 362 | } 363 | } 364 | } 365 | let (premul1, alpha1) = a.premultiply_split(); 366 | let (mut premul2, alpha2) = b.premultiply_split(); 367 | fixup_hues_for_interpolate(premul1, &mut premul2, cs.layout(), direction); 368 | let delta_premul = [ 369 | premul2[0] - premul1[0], 370 | premul2[1] - premul1[1], 371 | premul2[2] - premul1[2], 372 | ]; 373 | Interpolator { 374 | premul1, 375 | alpha1, 376 | delta_premul, 377 | delta_alpha: alpha2 - alpha1, 378 | cs, 379 | missing, 380 | } 381 | } 382 | 383 | /// Compute the relative luminance of the color. 384 | /// 385 | /// This can be useful for choosing contrasting colors, and follows the 386 | /// [WCAG 2.1 spec]. 387 | /// 388 | /// Note that this method only considers the opaque color, not the alpha. 389 | /// Blending semi-transparent colors will reduce contrast, and that 390 | /// should also be taken into account. 391 | /// 392 | /// [WCAG 2.1 spec]: https://www.w3.org/TR/WCAG21/#dfn-relative-luminance 393 | #[must_use] 394 | pub fn relative_luminance(self) -> f32 { 395 | let [r, g, b, _] = self.convert(ColorSpaceTag::LinearSrgb).components; 396 | 0.2126 * r + 0.7152 * g + 0.0722 * b 397 | } 398 | 399 | /// Map components. 400 | #[must_use] 401 | pub fn map(self, f: impl Fn(f32, f32, f32, f32) -> [f32; 4]) -> Self { 402 | let [x, y, z, a] = self.components; 403 | 404 | let mut flags = self.flags; 405 | flags.discard_name(); 406 | Self { 407 | cs: self.cs, 408 | flags, 409 | components: f(x, y, z, a), 410 | } 411 | .zero_missing_components() 412 | } 413 | 414 | /// Map components in a given color space. 415 | #[must_use] 416 | pub fn map_in(self, cs: ColorSpaceTag, f: impl Fn(f32, f32, f32, f32) -> [f32; 4]) -> Self { 417 | self.convert(cs).map(f).convert(self.cs) 418 | } 419 | 420 | /// Map the lightness of the color. 421 | /// 422 | /// In a color space that naturally has a lightness component, map that value. 423 | /// Otherwise, do the mapping in [Oklab]. The lightness range is normalized so 424 | /// that 1.0 is white. That is the normal range for Oklab but differs from the 425 | /// range in [Lab], [Lch], and [Hsl]. 426 | /// 427 | /// [Oklab]: crate::Oklab 428 | /// [Lab]: crate::Lab 429 | /// [Lch]: crate::Lch 430 | /// [Hsl]: crate::Hsl 431 | #[must_use] 432 | pub fn map_lightness(self, f: impl Fn(f32) -> f32) -> Self { 433 | match self.cs { 434 | ColorSpaceTag::Lab | ColorSpaceTag::Lch => { 435 | self.map(|l, c1, c2, a| [100.0 * f(l * 0.01), c1, c2, a]) 436 | } 437 | ColorSpaceTag::Oklab | ColorSpaceTag::Oklch => { 438 | self.map(|l, c1, c2, a| [f(l), c1, c2, a]) 439 | } 440 | ColorSpaceTag::Hsl => self.map(|h, s, l, a| [h, s, 100.0 * f(l * 0.01), a]), 441 | _ => self.map_in(ColorSpaceTag::Oklab, |l, a, b, alpha| [f(l), a, b, alpha]), 442 | } 443 | } 444 | 445 | /// Map the hue of the color. 446 | /// 447 | /// In a color space that naturally has a hue component, map that value. 448 | /// Otherwise, do the mapping in [Oklch]. The hue is in degrees. 449 | /// 450 | /// [Oklch]: crate::Oklch 451 | #[must_use] 452 | pub fn map_hue(self, f: impl Fn(f32) -> f32) -> Self { 453 | match self.cs.layout() { 454 | ColorSpaceLayout::HueFirst => self.map(|h, c1, c2, a| [f(h), c1, c2, a]), 455 | ColorSpaceLayout::HueThird => self.map(|c0, c1, h, a| [c0, c1, f(h), a]), 456 | _ => self.map_in(ColorSpaceTag::Oklch, |l, c, h, a| [l, c, f(h), a]), 457 | } 458 | } 459 | } 460 | 461 | impl PartialEq for DynamicColor { 462 | /// Equality is not perceptual, but requires the component values to be equal. 463 | /// 464 | /// See also [`CacheKey`](crate::cache_key::CacheKey). 465 | fn eq(&self, other: &Self) -> bool { 466 | // Same as the derive implementation, but we want a doc comment. 467 | self.cs == other.cs && self.flags == other.flags && self.components == other.components 468 | } 469 | } 470 | 471 | impl BitEq for DynamicColor { 472 | fn bit_eq(&self, other: &Self) -> bool { 473 | self.cs == other.cs 474 | && self.flags == other.flags 475 | && self.components.bit_eq(&other.components) 476 | } 477 | } 478 | 479 | impl BitHash for DynamicColor { 480 | fn bit_hash(&self, state: &mut H) { 481 | self.cs.hash(state); 482 | self.flags.hash(state); 483 | self.components.bit_hash(state); 484 | } 485 | } 486 | 487 | /// Note that the conversion is only lossless for color spaces that have a corresponding [tag](ColorSpaceTag). 488 | /// This is why we have this additional trait bound. See also 489 | /// for more discussion. 490 | impl From> for DynamicColor 491 | where 492 | ColorSpaceTag: From, 493 | { 494 | fn from(value: AlphaColor) -> Self { 495 | const { 496 | assert!( 497 | CS::TAG.is_some(), 498 | "this trait can only be implemented for colors with a tag" 499 | ); 500 | } 501 | 502 | Self::from_alpha_color(value) 503 | } 504 | } 505 | 506 | impl Interpolator { 507 | /// Evaluate the color ramp at the given point. 508 | /// 509 | /// Typically `t` ranges between 0 and 1, but that is not enforced, 510 | /// so extrapolation is also possible. 511 | pub fn eval(&self, t: f32) -> DynamicColor { 512 | let premul = [ 513 | self.premul1[0] + t * self.delta_premul[0], 514 | self.premul1[1] + t * self.delta_premul[1], 515 | self.premul1[2] + t * self.delta_premul[2], 516 | ]; 517 | let alpha = self.alpha1 + t * self.delta_alpha; 518 | let opaque = if alpha == 0.0 || alpha == 1.0 { 519 | premul 520 | } else { 521 | self.cs.layout().scale(premul, 1.0 / alpha) 522 | }; 523 | let components = add_alpha(opaque, alpha); 524 | DynamicColor { 525 | cs: self.cs, 526 | flags: Flags::from_missing(self.missing), 527 | components, 528 | } 529 | } 530 | } 531 | 532 | #[cfg(test)] 533 | mod tests { 534 | use crate::{parse_color, ColorSpaceTag, DynamicColor, Missing}; 535 | 536 | // `DynamicColor` was carefully packed. Ensure its size doesn't accidentally change. 537 | const _: () = if size_of::() != 20 { 538 | panic!("`DynamicColor` size changed"); 539 | }; 540 | 541 | #[test] 542 | fn missing_alpha() { 543 | let c = parse_color("oklab(0.5 0.2 0 / none)").unwrap(); 544 | assert_eq!(0., c.components[3]); 545 | assert_eq!(Missing::single(3), c.flags.missing()); 546 | 547 | // Alpha is missing, so we shouldn't be able to get an alpha added. 548 | let c2 = c.with_alpha(0.5); 549 | assert_eq!(0., c2.components[3]); 550 | assert_eq!(Missing::single(3), c2.flags.missing()); 551 | 552 | let c3 = c.multiply_alpha(0.2); 553 | assert_eq!(0., c3.components[3]); 554 | assert_eq!(Missing::single(3), c3.flags.missing()); 555 | } 556 | 557 | #[test] 558 | fn preserves_rgb_missingness() { 559 | let c = parse_color("color(srgb 0.5 none 0)").unwrap(); 560 | assert_eq!( 561 | c.convert(ColorSpaceTag::XyzD65).flags.missing(), 562 | Missing::single(1) 563 | ); 564 | } 565 | 566 | #[test] 567 | fn drops_missingness_when_not_analogous() { 568 | let c = parse_color("oklab(none 0.2 -0.3)").unwrap(); 569 | assert!(c.convert(ColorSpaceTag::Srgb).flags.missing().is_empty()); 570 | } 571 | 572 | #[test] 573 | fn preserves_hue_missingness() { 574 | let c = parse_color("oklch(0.2 0.3 none)").unwrap(); 575 | assert_eq!( 576 | c.convert(ColorSpaceTag::Hsl).flags.missing(), 577 | Missing::single(0) 578 | ); 579 | } 580 | 581 | #[test] 582 | fn preserves_lightness_missingness() { 583 | let c = parse_color("oklab(none 0.2 -0.3)").unwrap(); 584 | assert_eq!( 585 | c.convert(ColorSpaceTag::Hsl).flags.missing(), 586 | Missing::single(2) 587 | ); 588 | } 589 | 590 | #[test] 591 | fn preserves_saturation_missingness() { 592 | let c = parse_color("oklch(0.2 none 240)").unwrap(); 593 | assert_eq!(c.flags.missing(), Missing::single(1)); 594 | 595 | // As saturation is missing, it is effectively 0, meaning the color is achromatic and hue 596 | // is powerless. § 4.4.1 says hue must be set missing after conversion. 597 | assert_eq!( 598 | c.convert(ColorSpaceTag::Hsl).flags.missing(), 599 | Missing::single(0) | Missing::single(1) 600 | ); 601 | } 602 | 603 | #[test] 604 | fn achromatic_sets_hue_powerless() { 605 | let c = parse_color("oklab(0.2 0 0)").unwrap(); 606 | 607 | // As the color is achromatic, the hue is powerless. § 4.4.1 says hue must be set missing 608 | // after conversion. 609 | assert_eq!( 610 | c.convert(ColorSpaceTag::Hsl).flags.missing(), 611 | Missing::single(0) 612 | ); 613 | } 614 | 615 | #[test] 616 | fn powerless_components() { 617 | static COLORS_AND_POWERLESS: &[(&str, &[usize])] = &[ 618 | // Grayscale HWB results in powerless hue... 619 | ("hwb(240 80 20)", &[0]), 620 | ("hwb(240 79.9999999 19.9999999)", &[0]), 621 | // ... also if the grayscale is specified out of gamut... 622 | ("hwb(240 120 200)", &[0]), 623 | // ... but near-grayscale HWB does not result in powerless hue... 624 | ("hwb(240 79.99 20)", &[]), 625 | // ... and colorful colors don't either. 626 | ("hwb(240 20 15)", &[]), 627 | // Unsaturated hue-saturation-lightness-like colors result in powerless hue... 628 | ("hsl(240 0 50)", &[0]), 629 | ("hsl(240 0.0000001 50)", &[0]), 630 | // ... also if the saturation is negative... 631 | ("hsl(240 -0.2 50)", &[0]), 632 | // ... but near-unsaturated hue-saturation-lightness-like colors do not result 633 | // in powerless hue... 634 | ("hsl(240 0.01 50)", &[]), 635 | // ... and colorful colors don't either. 636 | ("hsl(240 0.6 50)", &[]), 637 | // In lab-like spaces, zero lightness does not (currently) result in powerless 638 | // components. 639 | ("lab(0 0.4 -0.3)", &[]), 640 | ("oklab(0 0.4 -0.3)", &[]), 641 | // sRGB (and in other rectangular spaces) never have powerless components. 642 | ("color(srgb 0 0 0)", &[]), 643 | ("color(srgb 1 1 1)", &[]), 644 | ("color(srgb 500 -200 20)", &[]), 645 | ]; 646 | 647 | for (color, powerless) in COLORS_AND_POWERLESS { 648 | let mut c = parse_color(color).unwrap(); 649 | c.powerless_to_missing(); 650 | for idx in *powerless { 651 | assert!( 652 | c.flags.missing().contains(*idx), 653 | "Expected color `{color}` to have the following powerless components: {powerless:?}" 654 | ); 655 | } 656 | } 657 | } 658 | } 659 | -------------------------------------------------------------------------------- /color/src/flags.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Color Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | //! Types for tracking [`DynamicColor`](crate::DynamicColor) state. 5 | 6 | use crate::x11_colors; 7 | 8 | /// Flags indicating [`DynamicColor`](crate::DynamicColor) state. 9 | /// 10 | /// The "missing" flags indicate whether a specific color component is missing (either the three 11 | /// color channels or the alpha channel). 12 | /// 13 | /// The "named" flag represents whether the dynamic color was parsed from one of the named colors 14 | /// in [CSS Color Module Level 4 § 6.1][css-named-colors] or named color space functions in [CSS 15 | /// Color Module Level 4 § 4.1][css-named-color-spaces]. 16 | /// 17 | /// The latter is primarily useful for serializing to a CSS-compliant string format. 18 | /// 19 | /// [css-named-colors]: https://www.w3.org/TR/css-color-4/#named-colors 20 | /// [css-named-color-spaces]: https://www.w3.org/TR/css-color-4/#color-syntax 21 | #[derive(Default, Clone, Copy, PartialEq, Eq, Hash)] 22 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 23 | pub struct Flags { 24 | /// A bitset of missing color components. 25 | missing: Missing, 26 | 27 | /// The named source a [`crate::DynamicColor`] was constructed from. Meanings: 28 | /// - 0 - not constructed from a named source; 29 | /// - 255 - constructed from a named color space function; 30 | /// - otherwise - the 1-based index into [`crate::x11_colors::NAMES`]. 31 | name: u8, 32 | } 33 | 34 | // Ensure the amount of colors fits into the `Flags::name` packing. 35 | #[cfg(test)] 36 | const _: () = const { 37 | if x11_colors::NAMES.len() > 253 { 38 | panic!("There are more X11 color names than can be packed into Flags."); 39 | } 40 | }; 41 | 42 | /// Missing color components, extracted from [`Flags`]. 43 | /// 44 | /// Some bitwise operations are implemented on this type, making certain manipulations more 45 | /// ergonomic. 46 | #[derive(Default, Clone, Copy, PartialEq, Eq, Hash)] 47 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 48 | pub struct Missing(u8); 49 | 50 | impl Flags { 51 | /// Construct flags with the given missing components. 52 | #[inline] 53 | pub const fn from_missing(missing: Missing) -> Self { 54 | Self { missing, name: 0 } 55 | } 56 | 57 | /// Set the missing components. 58 | #[inline] 59 | #[warn( 60 | clippy::missing_const_for_fn, 61 | reason = "can be made const with MSRV 1.83" 62 | )] 63 | pub fn set_missing(&mut self, missing: Missing) { 64 | self.missing = missing; 65 | } 66 | 67 | /// Returns the missing components from the flags. 68 | #[inline] 69 | pub const fn missing(self) -> Missing { 70 | self.missing 71 | } 72 | 73 | /// Set the flags to indicate the color was specified as one of the named colors. `name_ix` is 74 | /// the index into [`crate::x11_colors::NAMES`]. 75 | pub(crate) fn set_named_color(&mut self, name_ix: usize) { 76 | debug_assert!( 77 | name_ix < x11_colors::NAMES.len(), 78 | "Expected an X11 color name index no larger than: {}. Got: {}.", 79 | x11_colors::NAMES.len(), 80 | name_ix 81 | ); 82 | 83 | #[expect( 84 | clippy::cast_possible_truncation, 85 | reason = "name_ix is guaranteed to small enough by the above condition and by the test on the length of `x11_colors::NAMES`" 86 | )] 87 | { 88 | self.name = name_ix as u8 + 1; 89 | } 90 | } 91 | 92 | /// Set the flags to indicate the color was specified using one of the named color space 93 | /// functions. 94 | #[warn( 95 | clippy::missing_const_for_fn, 96 | reason = "can be made const with MSRV 1.83" 97 | )] 98 | pub(crate) fn set_named_color_space(&mut self) { 99 | self.name = 255; 100 | } 101 | 102 | /// Returns `true` if the flags indicate the color was generated from a named color or named 103 | /// color space function. 104 | #[inline] 105 | pub const fn named(self) -> bool { 106 | self.name != 0 107 | } 108 | 109 | /// If the color was constructed from a named color, returns that name. 110 | /// 111 | /// See also [`parse_color`][crate::parse_color]. 112 | pub const fn color_name(self) -> Option<&'static str> { 113 | let name_ix = self.name; 114 | if name_ix == 0 || name_ix == 255 { 115 | None 116 | } else { 117 | Some(x11_colors::NAMES[name_ix as usize - 1]) 118 | } 119 | } 120 | 121 | /// Discard the color name or color space name from the flags. 122 | #[inline] 123 | #[warn( 124 | clippy::missing_const_for_fn, 125 | reason = "can be made const with MSRV 1.83" 126 | )] 127 | pub fn discard_name(&mut self) { 128 | self.name = 0; 129 | } 130 | } 131 | 132 | impl core::fmt::Debug for Flags { 133 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 134 | f.debug_struct("Flags") 135 | .field("missing", &self.missing) 136 | .field("name", &self.name) 137 | .field("named", &self.named()) 138 | .field("color_name", &self.color_name()) 139 | .finish() 140 | } 141 | } 142 | 143 | impl Missing { 144 | /// The set containing no missing components. 145 | pub const EMPTY: Self = Self(0); 146 | 147 | /// The set containing a single component index. 148 | #[inline] 149 | pub const fn single(ix: usize) -> Self { 150 | debug_assert!(ix <= 3, "color component index must be 0, 1, 2 or 3"); 151 | Self(1 << ix) 152 | } 153 | 154 | /// Returns `true` if the set contains the component index. 155 | #[inline] 156 | pub const fn contains(self, ix: usize) -> bool { 157 | (self.0 & Self::single(ix).0) != 0 158 | } 159 | 160 | /// Add a missing component index to the set. 161 | #[inline] 162 | #[warn( 163 | clippy::missing_const_for_fn, 164 | reason = "can be made const with MSRV 1.83" 165 | )] 166 | pub fn insert(&mut self, ix: usize) { 167 | self.0 |= Self::single(ix).0; 168 | } 169 | 170 | /// Returns `true` if the set contains no indices. 171 | #[inline] 172 | pub const fn is_empty(self) -> bool { 173 | self.0 == 0 174 | } 175 | } 176 | 177 | impl core::ops::BitAnd for Missing { 178 | type Output = Self; 179 | 180 | #[inline] 181 | fn bitand(self, rhs: Self) -> Self { 182 | Self(self.0 & rhs.0) 183 | } 184 | } 185 | 186 | impl core::ops::BitOr for Missing { 187 | type Output = Self; 188 | 189 | #[inline] 190 | fn bitor(self, rhs: Self) -> Self { 191 | Self(self.0 | rhs.0) 192 | } 193 | } 194 | 195 | impl core::ops::Not for Missing { 196 | type Output = Self; 197 | 198 | #[inline] 199 | fn not(self) -> Self::Output { 200 | Self(!self.0) 201 | } 202 | } 203 | 204 | impl core::fmt::Debug for Missing { 205 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 206 | f.debug_tuple("Missing") 207 | .field(&format_args!("{:#010b}", self.0)) 208 | .finish() 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /color/src/floatfuncs.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Color Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | // In Rust 1.84 (https://github.com/rust-lang/rust/pull/131304), `abs` and 5 | // `copysign` were added to `core`, so we no longer need these forwarded to 6 | // libm. 7 | #![cfg_attr( 8 | not(feature = "std"), 9 | allow(dead_code, reason = "abs and copysign were added to core in 1.84") 10 | )] 11 | 12 | //! Shims for math functions that ordinarily come from std. 13 | 14 | /// Defines a trait that chooses between libstd or libm implementations of float methods. 15 | macro_rules! define_float_funcs { 16 | ($( 17 | fn $name:ident(self $(,$arg:ident: $arg_ty:ty)*) -> $ret:ty 18 | => $lfname:ident; 19 | )+) => { 20 | 21 | /// Since core doesn't depend upon libm, this provides libm implementations 22 | /// of float functions which are typically provided by the std library, when 23 | /// the `std` feature is not enabled. 24 | /// 25 | /// For documentation see the respective functions in the std library. 26 | #[cfg(not(feature = "std"))] 27 | pub(crate) trait FloatFuncs : Sized { 28 | $(fn $name(self $(,$arg: $arg_ty)*) -> $ret;)+ 29 | } 30 | 31 | #[cfg(not(feature = "std"))] 32 | impl FloatFuncs for f32 { 33 | $(fn $name(self $(,$arg: $arg_ty)*) -> $ret { 34 | #[cfg(feature = "libm")] 35 | return libm::$lfname(self $(,$arg)*); 36 | 37 | #[cfg(not(feature = "libm"))] 38 | compile_error!("color requires either the `std` or `libm` feature") 39 | })+ 40 | } 41 | 42 | } 43 | } 44 | 45 | define_float_funcs! { 46 | // This is not needed once the MSRV is 1.84 or later. 47 | fn abs(self) -> Self => fabsf; 48 | fn atan2(self, other: Self) -> Self => atan2f; 49 | fn cbrt(self) -> Self => cbrtf; 50 | fn ceil(self) -> Self => ceilf; 51 | // This is not needed once the MSRV is 1.84 or later. 52 | fn copysign(self, sign: Self) -> Self => copysignf; 53 | fn floor(self) -> Self => floorf; 54 | fn hypot(self, other: Self) -> Self => hypotf; 55 | // Note: powi is missing because its libm implementation is not efficient 56 | fn powf(self, n: Self) -> Self => powf; 57 | fn round(self) -> Self => roundf; 58 | fn sin_cos(self) -> (Self, Self) => sincosf; 59 | fn sqrt(self) -> Self => sqrtf; 60 | } 61 | -------------------------------------------------------------------------------- /color/src/gradient.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Color Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use crate::{ 5 | ColorSpace, ColorSpaceTag, DynamicColor, HueDirection, Interpolator, Oklab, PremulColor, 6 | }; 7 | 8 | /// The iterator for gradient approximation. 9 | /// 10 | /// This will yield a value for each gradient stop, including `t` values 11 | /// of 0 and 1 at the endpoints. 12 | /// 13 | /// Use the [`gradient`] function to generate this iterator. 14 | #[expect(missing_debug_implementations, reason = "it's an iterator")] 15 | pub struct GradientIter { 16 | interpolator: Interpolator, 17 | // This is in deltaEOK units 18 | tolerance: f32, 19 | // The adaptive subdivision logic is lifted from the stroke expansion paper. 20 | t0: u32, 21 | dt: f32, 22 | target0: PremulColor, 23 | target1: PremulColor, 24 | end_color: PremulColor, 25 | } 26 | 27 | /// Generate a piecewise linear approximation to a gradient ramp. 28 | /// 29 | /// The target gradient ramp is the linear interpolation from `color0` to `color1` in the target 30 | /// color space specified by `interp_cs`. For efficiency, this function returns an 31 | /// [iterator over color stops](GradientIter) in the `CS` color space, such that the gradient ramp 32 | /// created by linearly interpolating between those stops in the `CS` color space is equal within 33 | /// the specified `tolerance` to the target gradient ramp. 34 | /// 35 | /// When the target interpolation color space is cylindrical, the hue can be interpolated in 36 | /// multiple ways. The [`direction`](`HueDirection`) parameter controls the way in which the hue is 37 | /// interpolated. 38 | /// 39 | /// The given `tolerance` value specifies the maximum perceptual error in the approximation 40 | /// measured as the [Euclidean distance][euclidean-distance] in the [Oklab] color space (see also 41 | /// [`PremulColor::difference`][crate::PremulColor::difference]). This metric is known as 42 | /// [deltaEOK][delta-eok]. A reasonable value is 0.01, which in testing is nearly indistinguishable 43 | /// from the exact ramp. The number of stops scales roughly as the inverse square root of the 44 | /// tolerance. 45 | /// 46 | /// The error is measured at the midpoint of each segment, which in some cases may underestimate 47 | /// the error. 48 | /// 49 | /// For regular interpolation between two colors, see [`DynamicColor::interpolate`]. 50 | /// 51 | /// [euclidean-distance]: https://en.wikipedia.org/wiki/Euclidean_distance 52 | /// [delta-eok]: https://www.w3.org/TR/css-color-4/#color-difference-OK 53 | /// 54 | /// # Motivation 55 | /// 56 | /// A major feature of CSS Color 4 is the ability to specify color interpolation in any 57 | /// interpolation color space [CSS Color Module Level 4 § 12.1][css-sec], which may be quite a bit 58 | /// better than simple linear interpolation in sRGB (for example). 59 | /// 60 | /// One strategy for implementing these gradients is to interpolate in the appropriate 61 | /// (premultiplied) space, then map each resulting color to the space used for compositing. That 62 | /// can be expensive. An alternative strategy is to precompute a piecewise linear ramp that closely 63 | /// approximates the desired ramp, then render that using high performance techniques. This method 64 | /// computes such an approximation. 65 | /// 66 | /// [css-sec]: https://www.w3.org/TR/css-color-4/#interpolation-space 67 | /// 68 | /// # Example 69 | /// 70 | /// The following compares interpolating in the target color space Oklab with interpolating 71 | /// piecewise in the color space sRGB. 72 | /// 73 | /// ```rust 74 | /// use color::{AlphaColor, ColorSpaceTag, DynamicColor, HueDirection, Oklab, Srgb}; 75 | /// 76 | /// let start = DynamicColor::from_alpha_color(AlphaColor::::new([1., 0., 0., 1.])); 77 | /// let end = DynamicColor::from_alpha_color(AlphaColor::::new([0., 1., 0., 1.])); 78 | /// 79 | /// // Interpolation in a target interpolation color space. 80 | /// let interp = start.interpolate(end, ColorSpaceTag::Oklab, HueDirection::default()); 81 | /// // Piecewise-approximated interpolation in a compositing color space. 82 | /// let mut gradient = color::gradient::( 83 | /// start, 84 | /// end, 85 | /// ColorSpaceTag::Oklab, 86 | /// HueDirection::default(), 87 | /// 0.01, 88 | /// ); 89 | /// 90 | /// let (mut t0, mut stop0) = gradient.next().unwrap(); 91 | /// for (t1, stop1) in gradient { 92 | /// // Compare a few points between the piecewise stops. 93 | /// for point in [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9] { 94 | /// let interpolated_point = interp 95 | /// .eval(t0 + (t1 - t0) * point) 96 | /// .to_alpha_color::() 97 | /// .discard_alpha(); 98 | /// let approximated_point = stop0.lerp_rect(stop1, point).discard_alpha(); 99 | /// 100 | /// // The perceptual deltaEOK between the two is lower than the tolerance. 101 | /// assert!( 102 | /// approximated_point 103 | /// .convert::() 104 | /// .difference(interpolated_point.convert::()) 105 | /// < 0.01 106 | /// ); 107 | /// } 108 | /// 109 | /// t0 = t1; 110 | /// stop0 = stop1; 111 | /// } 112 | /// ``` 113 | pub fn gradient( 114 | mut color0: DynamicColor, 115 | mut color1: DynamicColor, 116 | interp_cs: ColorSpaceTag, 117 | direction: HueDirection, 118 | tolerance: f32, 119 | ) -> GradientIter { 120 | let interpolator = color0.interpolate(color1, interp_cs, direction); 121 | if !color0.flags.missing().is_empty() { 122 | color0 = interpolator.eval(0.0); 123 | } 124 | let target0 = color0.to_alpha_color().premultiply(); 125 | if !color1.flags.missing().is_empty() { 126 | color1 = interpolator.eval(1.0); 127 | } 128 | let target1 = color1.to_alpha_color().premultiply(); 129 | let end_color = target1; 130 | GradientIter { 131 | interpolator, 132 | tolerance, 133 | t0: 0, 134 | dt: 0.0, 135 | target0, 136 | target1, 137 | end_color, 138 | } 139 | } 140 | 141 | impl Iterator for GradientIter { 142 | type Item = (f32, PremulColor); 143 | 144 | fn next(&mut self) -> Option { 145 | if self.dt == 0.0 { 146 | self.dt = 1.0; 147 | return Some((0.0, self.target0)); 148 | } 149 | let t0 = self.t0 as f32 * self.dt; 150 | if t0 == 1.0 { 151 | return None; 152 | } 153 | loop { 154 | // compute midpoint color 155 | let midpoint = self.interpolator.eval(t0 + 0.5 * self.dt); 156 | let midpoint_oklab: PremulColor = midpoint.to_alpha_color().premultiply(); 157 | let approx = self.target0.lerp_rect(self.target1, 0.5); 158 | let error = midpoint_oklab.difference(approx.convert()); 159 | if error <= self.tolerance { 160 | let t1 = t0 + self.dt; 161 | self.t0 += 1; 162 | let shift = self.t0.trailing_zeros(); 163 | self.t0 >>= shift; 164 | self.dt *= (1 << shift) as f32; 165 | self.target0 = self.target1; 166 | let new_t1 = t1 + self.dt; 167 | if new_t1 < 1.0 { 168 | self.target1 = self 169 | .interpolator 170 | .eval(new_t1) 171 | .to_alpha_color() 172 | .premultiply(); 173 | } else { 174 | self.target1 = self.end_color; 175 | } 176 | return Some((t1, self.target0)); 177 | } 178 | self.t0 *= 2; 179 | self.dt *= 0.5; 180 | self.target1 = midpoint.to_alpha_color().premultiply(); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /color/src/impl_bytemuck.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Color Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | #![allow(unsafe_code, reason = "unsafe is required for bytemuck unsafe impls")] 5 | 6 | use crate::{ 7 | cache_key::CacheKey, AlphaColor, ColorSpace, ColorSpaceTag, HueDirection, OpaqueColor, 8 | PremulColor, PremulRgba8, Rgba8, 9 | }; 10 | 11 | // Safety: The struct is `repr(transparent)` and the data member is bytemuck::Pod. 12 | unsafe impl bytemuck::Pod for AlphaColor {} 13 | 14 | // Safety: The struct is `repr(transparent)`. 15 | unsafe impl bytemuck::TransparentWrapper<[f32; 4]> for AlphaColor {} 16 | 17 | // Safety: The struct is `repr(transparent)` and the data member is bytemuck::Zeroable. 18 | unsafe impl bytemuck::Zeroable for AlphaColor {} 19 | 20 | // Safety: The struct is `repr(transparent)` and the data member is bytemuck::Pod. 21 | unsafe impl bytemuck::Pod for OpaqueColor {} 22 | 23 | // Safety: The struct is `repr(transparent)`. 24 | unsafe impl bytemuck::TransparentWrapper<[f32; 3]> for OpaqueColor {} 25 | 26 | // Safety: The struct is `repr(transparent)` and the data member is bytemuck::Zeroable. 27 | unsafe impl bytemuck::Zeroable for OpaqueColor {} 28 | 29 | // Safety: The struct is `repr(transparent)` and the data member is bytemuck::Pod. 30 | unsafe impl bytemuck::Pod for PremulColor {} 31 | 32 | // Safety: The struct is `repr(transparent)`. 33 | unsafe impl bytemuck::TransparentWrapper<[f32; 4]> for PremulColor {} 34 | 35 | // Safety: The struct is `repr(transparent)` and the data member is bytemuck::Zeroable. 36 | unsafe impl bytemuck::Zeroable for PremulColor {} 37 | 38 | // Safety: The struct is `repr(C)` and all members are bytemuck::Pod. 39 | unsafe impl bytemuck::Pod for PremulRgba8 {} 40 | 41 | // Safety: The struct is `repr(C)` and all members are bytemuck::Zeroable. 42 | unsafe impl bytemuck::Zeroable for PremulRgba8 {} 43 | 44 | // Safety: The struct is `repr(C)` and all members are bytemuck::Pod. 45 | unsafe impl bytemuck::Pod for Rgba8 {} 46 | 47 | // Safety: The struct is `repr(C)` and all members are bytemuck::Zeroable. 48 | unsafe impl bytemuck::Zeroable for Rgba8 {} 49 | 50 | // Safety: The enum is `repr(u8)` and has only fieldless variants. 51 | unsafe impl bytemuck::NoUninit for ColorSpaceTag {} 52 | 53 | // Safety: The enum is `repr(u8)` and `0` is a valid value. 54 | unsafe impl bytemuck::Zeroable for ColorSpaceTag {} 55 | 56 | // Safety: The enum is `repr(u8)`. 57 | unsafe impl bytemuck::checked::CheckedBitPattern for ColorSpaceTag { 58 | type Bits = u8; 59 | 60 | fn is_valid_bit_pattern(bits: &u8) -> bool { 61 | use bytemuck::Contiguous; 62 | // Don't need to compare against MIN_VALUE as this is u8 and 0 is the MIN_VALUE. 63 | *bits <= Self::MAX_VALUE 64 | } 65 | } 66 | 67 | // Safety: The enum is `repr(u8)`. All values are `u8` and fall within 68 | // the min and max values. 69 | unsafe impl bytemuck::Contiguous for ColorSpaceTag { 70 | type Int = u8; 71 | const MIN_VALUE: u8 = Self::Srgb as u8; 72 | const MAX_VALUE: u8 = Self::Aces2065_1 as u8; 73 | } 74 | 75 | // Safety: The enum is `repr(u8)` and has only fieldless variants. 76 | unsafe impl bytemuck::NoUninit for HueDirection {} 77 | 78 | // Safety: The enum is `repr(u8)` and `0` is a valid value. 79 | unsafe impl bytemuck::Zeroable for HueDirection {} 80 | 81 | // Safety: The enum is `repr(u8)`. 82 | unsafe impl bytemuck::checked::CheckedBitPattern for HueDirection { 83 | type Bits = u8; 84 | 85 | fn is_valid_bit_pattern(bits: &u8) -> bool { 86 | use bytemuck::Contiguous; 87 | // Don't need to compare against MIN_VALUE as this is u8 and 0 is the MIN_VALUE. 88 | *bits <= Self::MAX_VALUE 89 | } 90 | } 91 | 92 | // Safety: The enum is `repr(u8)`. All values are `u8` and fall within 93 | // the min and max values. 94 | unsafe impl bytemuck::Contiguous for HueDirection { 95 | type Int = u8; 96 | const MIN_VALUE: u8 = Self::Shorter as u8; 97 | const MAX_VALUE: u8 = Self::Decreasing as u8; 98 | } 99 | 100 | // Safety: The struct is `repr(transparent)`. 101 | unsafe impl bytemuck::TransparentWrapper for CacheKey {} 102 | 103 | #[cfg(test)] 104 | mod tests { 105 | use crate::{ 106 | cache_key::CacheKey, AlphaColor, ColorSpaceTag, HueDirection, OpaqueColor, PremulColor, 107 | PremulRgba8, Rgba8, Srgb, 108 | }; 109 | use bytemuck::{checked::try_from_bytes, Contiguous, TransparentWrapper, Zeroable}; 110 | use core::{marker::PhantomData, ptr}; 111 | 112 | fn assert_is_pod(_pod: impl bytemuck::Pod) {} 113 | 114 | #[test] 115 | fn alphacolor_is_pod() { 116 | let AlphaColor { 117 | components, 118 | cs: PhantomData, 119 | } = AlphaColor::::new([1., 2., 3., 0.]); 120 | assert_is_pod(components); 121 | } 122 | 123 | #[test] 124 | fn opaquecolor_is_pod() { 125 | let OpaqueColor { 126 | components, 127 | cs: PhantomData, 128 | } = OpaqueColor::::new([1., 2., 3.]); 129 | assert_is_pod(components); 130 | } 131 | 132 | #[test] 133 | fn premulcolor_is_pod() { 134 | let PremulColor { 135 | components, 136 | cs: PhantomData, 137 | } = PremulColor::::new([1., 2., 3., 0.]); 138 | assert_is_pod(components); 139 | } 140 | 141 | #[test] 142 | fn premulrgba8_is_pod() { 143 | let rgba8 = PremulRgba8 { 144 | r: 0, 145 | b: 0, 146 | g: 0, 147 | a: 0, 148 | }; 149 | let PremulRgba8 { r, g, b, a } = rgba8; 150 | assert_is_pod(r); 151 | assert_is_pod(g); 152 | assert_is_pod(b); 153 | assert_is_pod(a); 154 | } 155 | 156 | #[test] 157 | fn rgba8_is_pod() { 158 | let rgba8 = Rgba8 { 159 | r: 0, 160 | b: 0, 161 | g: 0, 162 | a: 0, 163 | }; 164 | let Rgba8 { r, g, b, a } = rgba8; 165 | assert_is_pod(r); 166 | assert_is_pod(g); 167 | assert_is_pod(b); 168 | assert_is_pod(a); 169 | } 170 | 171 | #[test] 172 | fn checked_bit_pattern() { 173 | let valid = bytemuck::bytes_of(&2_u8); 174 | let invalid = bytemuck::bytes_of(&200_u8); 175 | 176 | assert_eq!( 177 | Ok(&ColorSpaceTag::Lab), 178 | try_from_bytes::(valid) 179 | ); 180 | 181 | assert!(try_from_bytes::(invalid).is_err()); 182 | 183 | assert_eq!( 184 | Ok(&HueDirection::Increasing), 185 | try_from_bytes::(valid) 186 | ); 187 | 188 | assert!(try_from_bytes::(invalid).is_err()); 189 | } 190 | 191 | #[test] 192 | fn contiguous() { 193 | let cst1 = ColorSpaceTag::LinearSrgb; 194 | let cst2 = ColorSpaceTag::from_integer(cst1.into_integer()); 195 | assert_eq!(Some(cst1), cst2); 196 | 197 | assert_eq!(None, ColorSpaceTag::from_integer(255)); 198 | 199 | let hd1 = HueDirection::Decreasing; 200 | let hd2 = HueDirection::from_integer(hd1.into_integer()); 201 | assert_eq!(Some(hd1), hd2); 202 | 203 | assert_eq!(None, HueDirection::from_integer(255)); 204 | } 205 | 206 | // If the inner type is wrong in the unsafe impl above, 207 | // that will result in failures here due to assertions 208 | // within bytemuck. 209 | #[test] 210 | fn transparent_wrapper() { 211 | let ac = AlphaColor::::new([1., 2., 3., 0.]); 212 | let ai: [f32; 4] = AlphaColor::::peel(ac); 213 | assert_eq!(ai, [1., 2., 3., 0.]); 214 | 215 | let oc = OpaqueColor::::new([1., 2., 3.]); 216 | let oi: [f32; 3] = OpaqueColor::::peel(oc); 217 | assert_eq!(oi, [1., 2., 3.]); 218 | 219 | let pc = PremulColor::::new([1., 2., 3., 0.]); 220 | let pi: [f32; 4] = PremulColor::::peel(pc); 221 | assert_eq!(pi, [1., 2., 3., 0.]); 222 | 223 | let ck = CacheKey::::new(1.); 224 | let ci: f32 = CacheKey::::peel(ck); 225 | assert_eq!(ci, 1.); 226 | } 227 | 228 | #[test] 229 | fn zeroable() { 230 | let ac = AlphaColor::::zeroed(); 231 | assert_eq!(ac.components, [0., 0., 0., 0.]); 232 | 233 | let oc = OpaqueColor::::zeroed(); 234 | assert_eq!(oc.components, [0., 0., 0.]); 235 | 236 | let pc = PremulColor::::zeroed(); 237 | assert_eq!(pc.components, [0., 0., 0., 0.]); 238 | 239 | let rgba8 = Rgba8::zeroed(); 240 | assert_eq!( 241 | rgba8, 242 | Rgba8 { 243 | r: 0, 244 | g: 0, 245 | b: 0, 246 | a: 0 247 | } 248 | ); 249 | 250 | let cst = ColorSpaceTag::zeroed(); 251 | assert_eq!(cst, ColorSpaceTag::Srgb); 252 | 253 | let hd = HueDirection::zeroed(); 254 | assert_eq!(hd, HueDirection::Shorter); 255 | } 256 | 257 | /// Tests that the [`Contiguous`] impl for [`HueDirection`] is not trivially incorrect. 258 | const _: () = { 259 | let mut value = 0; 260 | while value <= HueDirection::MAX_VALUE { 261 | // Safety: In a const context, therefore if this makes an invalid HueDirection, that will be detected. 262 | let it: HueDirection = unsafe { ptr::read((&raw const value).cast()) }; 263 | // Evaluate the enum value to ensure it actually has a valid tag 264 | if it as u8 != value { 265 | unreachable!(); 266 | } 267 | value += 1; 268 | } 269 | }; 270 | 271 | /// Tests that the [`Contiguous`] impl for [`ColorSpaceTag`] is not trivially incorrect. 272 | const _: () = { 273 | let mut value = 0; 274 | while value <= ColorSpaceTag::MAX_VALUE { 275 | // Safety: In a const context, therefore if this makes an invalid ColorSpaceTag, that will be detected. 276 | let it: ColorSpaceTag = unsafe { ptr::read((&raw const value).cast()) }; 277 | // Evaluate the enum value to ensure it actually has a valid tag 278 | if it as u8 != value { 279 | unreachable!(); 280 | } 281 | value += 1; 282 | } 283 | }; 284 | } 285 | 286 | #[cfg(doctest)] 287 | /// Doctests aren't collected under `cfg(test)`; we can use `cfg(doctest)` instead 288 | mod doctests { 289 | /// Validates that any new variants in `HueDirection` has led to a change in the `Contiguous` impl. 290 | /// Note that to test this robustly, we'd need 256 tests, which is impractical. 291 | /// We make the assumption that all new variants will maintain contiguousness. 292 | /// 293 | /// ```compile_fail,E0080 294 | /// use bytemuck::Contiguous; 295 | /// use color::HueDirection; 296 | /// const { 297 | /// let value = HueDirection::MAX_VALUE + 1; 298 | /// // Safety: In a const context, therefore if this makes an invalid HueDirection, that will be detected. 299 | /// // (Indeed, we rely upon that) 300 | /// let it: HueDirection = unsafe { core::ptr::read((&raw const value).cast()) }; 301 | /// // Evaluate the enum value to ensure it actually has an invalid tag 302 | /// if it as u8 != value { 303 | /// unreachable!(); 304 | /// } 305 | /// } 306 | /// ``` 307 | const _HUE_DIRECTION: () = {}; 308 | 309 | /// Validates that any new variants in `ColorSpaceTag` has led to a change in the `Contiguous` impl. 310 | /// Note that to test this robustly, we'd need 256 tests, which is impractical. 311 | /// We make the assumption that all new variants will maintain contiguousness. 312 | /// 313 | /// ```compile_fail,E0080 314 | /// use bytemuck::Contiguous; 315 | /// use color::ColorSpaceTag; 316 | /// const { 317 | /// let value = ColorSpaceTag::MAX_VALUE + 1; 318 | /// let it: ColorSpaceTag = unsafe { core::ptr::read((&raw const value).cast()) }; 319 | /// // Evaluate the enum value to ensure it actually has an invalid tag 320 | /// if it as u8 != value { 321 | /// unreachable!(); 322 | /// } 323 | /// } 324 | /// ``` 325 | const _COLOR_SPACE_TAG: () = {}; 326 | } 327 | -------------------------------------------------------------------------------- /color/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Color Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | //! Color is a Rust crate which implements color space conversions, targeting at least 5 | //! [CSS Color Level 4]. 6 | //! 7 | //! ## Main types 8 | //! 9 | //! The crate has two approaches to representing color in the Rust type system: a set of 10 | //! types with static color space as part of the types, and [`DynamicColor`] 11 | //! in which the color space is represented at runtime. 12 | //! 13 | //! The static color types come in three variants: [`OpaqueColor`] without an 14 | //! alpha channel, [`AlphaColor`] with a separate alpha channel, and [`PremulColor`] with 15 | //! premultiplied alpha. The last type is particularly useful for making interpolation and 16 | //! compositing more efficient. These have a marker type parameter, indicating which 17 | //! [`ColorSpace`] they are in. Conversion to another color space uses the `convert` method 18 | //! on each of these types. The static types are open-ended, as it's possible to implement 19 | //! this trait for new color spaces. 20 | //! 21 | //! ## Scope and goals 22 | //! 23 | //! Color in its entirety is an extremely deep and complex topic. It is completely impractical 24 | //! for a single crate to meet all color needs. The goal of this one is to strike a balance, 25 | //! providing color capabilities while also keeping things simple and efficient. 26 | //! 27 | //! The main purpose of this crate is to provide a good set of types for representing colors, 28 | //! along with conversions between them and basic manipulations, especially interpolation. A 29 | //! major inspiration is the [CSS Color Level 4] draft spec; we implement most of the operations 30 | //! and strive for correctness. 31 | //! 32 | //! A primary use case is rendering, including color conversions and methods for preparing 33 | //! gradients. The crate should also be suitable for document authoring and editing, as it 34 | //! contains methods for parsing and serializing colors with CSS Color 4 compatible syntax. 35 | //! 36 | //! Simplifications include: 37 | //! * Always using `f32` to represent component values. 38 | //! * Only handling 3-component color spaces (plus optional alpha). 39 | //! * Choosing a fixed, curated set of color spaces for dynamic color types. 40 | //! * Choosing linear sRGB as the central color space. 41 | //! * Keeping white point implicit in the general conversion operations. 42 | //! 43 | //! A number of other tasks are out of scope for this crate: 44 | //! * Print color spaces (CMYK). 45 | //! * Spectral colors. 46 | //! * Color spaces with more than 3 components generally. 47 | //! * [ICC] color profiles. 48 | //! * [ACES] color transforms. 49 | //! * Appearance models and other color science not needed for rendering. 50 | //! * Quantizing and packing to lower bit depths. 51 | //! 52 | //! The [`Rgba8`] and [`PremulRgba8`] types are a partial exception to this last item, as 53 | //! those representation are ubiquitous and requires special logic for serializing to 54 | //! maximize compatibility. 55 | //! 56 | //! Some of these capabilities may be added as other crates within the `color` repository, 57 | //! and we will also facilitate interoperability with other color crates in the Rust 58 | //! ecosystem as needed. 59 | //! 60 | //! ## Features 61 | //! 62 | //! - `std` (enabled by default): Get floating point functions from the standard library 63 | //! (likely using your target's libc). 64 | //! - `libm`: Use floating point implementations from [libm][]. 65 | //! - `bytemuck`: Implement traits from `bytemuck` on [`AlphaColor`], [`ColorSpaceTag`], 66 | //! [`HueDirection`], [`OpaqueColor`], [`PremulColor`], [`PremulRgba8`], and [`Rgba8`]. 67 | //! - `serde`: Implement `serde::Deserialize` and `serde::Serialize` on [`AlphaColor`], 68 | //! [`DynamicColor`], [`OpaqueColor`], [`PremulColor`], [`PremulRgba8`], and [`Rgba8`]. 69 | //! 70 | //! At least one of `std` and `libm` is required; `std` overrides `libm`. 71 | //! 72 | //! [CSS Color Level 4]: https://www.w3.org/TR/css-color-4/ 73 | //! [ICC]: https://color.org/ 74 | //! [ACES]: https://acescentral.com/ 75 | #![cfg_attr(feature = "libm", doc = "[libm]: libm")] 76 | #![cfg_attr(not(feature = "libm"), doc = "[libm]: https://crates.io/crates/libm")] 77 | // LINEBENDER LINT SET - lib.rs - v1 78 | // See https://linebender.org/wiki/canonical-lints/ 79 | // These lints aren't included in Cargo.toml because they 80 | // shouldn't apply to examples and tests 81 | #![warn(unused_crate_dependencies)] 82 | #![warn(clippy::print_stdout, clippy::print_stderr)] 83 | // END LINEBENDER LINT SET 84 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 85 | #![no_std] 86 | 87 | pub mod cache_key; 88 | mod chromaticity; 89 | mod color; 90 | mod colorspace; 91 | mod dynamic; 92 | mod flags; 93 | mod gradient; 94 | pub mod palette; 95 | mod rgba8; 96 | mod serialize; 97 | mod tag; 98 | mod x11_colors; 99 | 100 | // Note: this may become feature-gated; we'll decide this soon 101 | // (This line is isolated so that the comment binds to it with import ordering) 102 | mod parse; 103 | 104 | #[cfg(feature = "bytemuck")] 105 | mod impl_bytemuck; 106 | 107 | #[cfg(all(not(feature = "std"), not(test)))] 108 | mod floatfuncs; 109 | 110 | pub use chromaticity::Chromaticity; 111 | pub use color::{AlphaColor, HueDirection, OpaqueColor, PremulColor}; 112 | pub use colorspace::{ 113 | A98Rgb, Aces2065_1, AcesCg, ColorSpace, ColorSpaceLayout, DisplayP3, Hsl, Hwb, Lab, Lch, 114 | LinearSrgb, Oklab, Oklch, ProphotoRgb, Rec2020, Srgb, XyzD50, XyzD65, 115 | }; 116 | pub use dynamic::{DynamicColor, Interpolator}; 117 | pub use flags::{Flags, Missing}; 118 | pub use gradient::{gradient, GradientIter}; 119 | pub use parse::{parse_color, parse_color_prefix, ParseError}; 120 | pub use rgba8::{PremulRgba8, Rgba8}; 121 | pub use tag::ColorSpaceTag; 122 | 123 | const fn u8_to_f32(x: u8) -> f32 { 124 | x as f32 * (1.0 / 255.0) 125 | } 126 | 127 | /// Multiplication `m * x` of a 3x3-matrix `m` and a 3-vector `x`. 128 | const fn matvecmul(m: &[[f32; 3]; 3], x: [f32; 3]) -> [f32; 3] { 129 | [ 130 | m[0][0] * x[0] + m[0][1] * x[1] + m[0][2] * x[2], 131 | m[1][0] * x[0] + m[1][1] * x[1] + m[1][2] * x[2], 132 | m[2][0] * x[0] + m[2][1] * x[1] + m[2][2] * x[2], 133 | ] 134 | } 135 | 136 | /// Multiplication `ma * mb` of two 3x3-matrices `ma` and `mb`. 137 | const fn matmatmul(ma: &[[f32; 3]; 3], mb: &[[f32; 3]; 3]) -> [[f32; 3]; 3] { 138 | [ 139 | [ 140 | ma[0][0] * mb[0][0] + ma[0][1] * mb[1][0] + ma[0][2] * mb[2][0], 141 | ma[0][0] * mb[0][1] + ma[0][1] * mb[1][1] + ma[0][2] * mb[2][1], 142 | ma[0][0] * mb[0][2] + ma[0][1] * mb[1][2] + ma[0][2] * mb[2][2], 143 | ], 144 | [ 145 | ma[1][0] * mb[0][0] + ma[1][1] * mb[1][0] + ma[1][2] * mb[2][0], 146 | ma[1][0] * mb[0][1] + ma[1][1] * mb[1][1] + ma[1][2] * mb[2][1], 147 | ma[1][0] * mb[0][2] + ma[1][1] * mb[1][2] + ma[1][2] * mb[2][2], 148 | ], 149 | [ 150 | ma[2][0] * mb[0][0] + ma[2][1] * mb[1][0] + ma[2][2] * mb[2][0], 151 | ma[2][0] * mb[0][1] + ma[2][1] * mb[1][1] + ma[2][2] * mb[2][1], 152 | ma[2][0] * mb[0][2] + ma[2][1] * mb[1][2] + ma[2][2] * mb[2][2], 153 | ], 154 | ] 155 | } 156 | 157 | /// Multiplication `ma * mb` of a 3x3-matrix `ma` by a 3x3-diagonal matrix `mb`. 158 | /// 159 | /// Diagonal matrix `mb` is given by 160 | /// 161 | /// ```text 162 | /// [ mb[0] 0 0 ] 163 | /// [ 0 mb[1] 0 ] 164 | /// [ 0 0 mb[2] ] 165 | /// ``` 166 | const fn matdiagmatmul(ma: &[[f32; 3]; 3], mb: [f32; 3]) -> [[f32; 3]; 3] { 167 | [ 168 | [ma[0][0] * mb[0], ma[0][1] * mb[1], ma[0][2] * mb[2]], 169 | [ma[1][0] * mb[0], ma[1][1] * mb[1], ma[1][2] * mb[2]], 170 | [ma[2][0] * mb[0], ma[2][1] * mb[1], ma[2][2] * mb[2]], 171 | ] 172 | } 173 | 174 | impl AlphaColor { 175 | /// Create a color from 8-bit rgba values. 176 | /// 177 | /// Note: for conversion from the [`Rgba8`] type, just use the `From` trait. 178 | pub const fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self { 179 | let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), u8_to_f32(a)]; 180 | Self::new(components) 181 | } 182 | 183 | /// Create a color from 8-bit rgb values with an opaque alpha. 184 | /// 185 | /// Note: for conversion from the [`Rgba8`] type, just use the `From` trait. 186 | pub const fn from_rgb8(r: u8, g: u8, b: u8) -> Self { 187 | let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), 1.]; 188 | Self::new(components) 189 | } 190 | } 191 | 192 | impl OpaqueColor { 193 | /// Create a color from 8-bit rgb values. 194 | pub const fn from_rgb8(r: u8, g: u8, b: u8) -> Self { 195 | let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b)]; 196 | Self::new(components) 197 | } 198 | } 199 | 200 | impl PremulColor { 201 | /// Create a color from pre-multiplied 8-bit rgba values. 202 | /// 203 | /// Note: for conversion from the [`PremulRgba8`] type, just use the `From` trait. 204 | pub const fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self { 205 | let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), u8_to_f32(a)]; 206 | Self::new(components) 207 | } 208 | 209 | /// Create a color from 8-bit rgb values with an opaque alpha. 210 | /// 211 | /// Note: for conversion from the [`Rgba8`] type, just use the `From` trait. 212 | pub const fn from_rgb8(r: u8, g: u8, b: u8) -> Self { 213 | let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), 1.]; 214 | Self::new(components) 215 | } 216 | } 217 | 218 | // Keep clippy from complaining about unused libm in nostd test case. 219 | #[cfg(feature = "libm")] 220 | #[expect(unused, reason = "keep clippy happy")] 221 | fn ensure_libm_dependency_used() -> f32 { 222 | libm::sqrtf(4_f32) 223 | } 224 | -------------------------------------------------------------------------------- /color/src/palette/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Color Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | //! Palettes with predefined colors. 5 | 6 | pub mod css; 7 | -------------------------------------------------------------------------------- /color/src/rgba8.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Color Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use crate::{AlphaColor, PremulColor, Srgb}; 5 | 6 | /// A packed representation of sRGB colors. 7 | /// 8 | /// Encoding sRGB with 8 bits per component is extremely common, as 9 | /// it is efficient and convenient, even if limited in accuracy and 10 | /// gamut. 11 | /// 12 | /// This is not meant to be a general purpose color type and is 13 | /// intended for use with [`AlphaColor::to_rgba8`] and [`OpaqueColor::to_rgba8`]. 14 | /// 15 | /// For a pre-multiplied packed representation, see [`PremulRgba8`]. 16 | /// 17 | /// [`AlphaColor::to_rgba8`]: crate::AlphaColor::to_rgba8 18 | /// [`OpaqueColor::to_rgba8`]: crate::OpaqueColor::to_rgba8 19 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 20 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 21 | #[repr(C)] 22 | pub struct Rgba8 { 23 | /// Red component. 24 | pub r: u8, 25 | /// Green component. 26 | pub g: u8, 27 | /// Blue component. 28 | pub b: u8, 29 | /// Alpha component. 30 | /// 31 | /// Alpha is interpreted as separated alpha. 32 | pub a: u8, 33 | } 34 | 35 | impl Rgba8 { 36 | /// Returns the color as a `[u8; 4]`. 37 | /// 38 | /// The color values will be in the order `[r, g, b, a]`. 39 | #[must_use] 40 | pub const fn to_u8_array(self) -> [u8; 4] { 41 | [self.r, self.g, self.b, self.a] 42 | } 43 | 44 | /// Convert the `[u8; 4]` byte array into an `Rgba8` color. 45 | /// 46 | /// The color values must be given in the order `[r, g, b, a]`. 47 | #[must_use] 48 | pub const fn from_u8_array([r, g, b, a]: [u8; 4]) -> Self { 49 | Self { r, g, b, a } 50 | } 51 | 52 | /// Returns the color as a little-endian packed value, with `r` the least significant byte and 53 | /// `a` the most significant. 54 | #[must_use] 55 | pub const fn to_u32(self) -> u32 { 56 | u32::from_ne_bytes(self.to_u8_array()) 57 | } 58 | 59 | /// Interpret the little-endian packed value as a color, with `r` the least significant byte 60 | /// and `a` the most significant. 61 | #[must_use] 62 | pub const fn from_u32(packed_bytes: u32) -> Self { 63 | Self::from_u8_array(u32::to_ne_bytes(packed_bytes)) 64 | } 65 | } 66 | 67 | impl From for AlphaColor { 68 | fn from(value: Rgba8) -> Self { 69 | Self::from_rgba8(value.r, value.g, value.b, value.a) 70 | } 71 | } 72 | 73 | /// A packed representation of pre-multiplied sRGB colors. 74 | /// 75 | /// Encoding sRGB with 8 bits per component is extremely common, as 76 | /// it is efficient and convenient, even if limited in accuracy and 77 | /// gamut. 78 | /// 79 | /// This is not meant to be a general purpose color type and is 80 | /// intended for use with [`PremulColor::to_rgba8`]. 81 | /// 82 | /// For a non-pre-multiplied packed representation, see [`Rgba8`]. 83 | /// 84 | /// [`PremulColor::to_rgba8`]: crate::PremulColor::to_rgba8 85 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 86 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 87 | #[repr(C)] 88 | pub struct PremulRgba8 { 89 | /// Red component. 90 | pub r: u8, 91 | /// Green component. 92 | pub g: u8, 93 | /// Blue component. 94 | pub b: u8, 95 | /// Alpha component. 96 | pub a: u8, 97 | } 98 | 99 | impl PremulRgba8 { 100 | /// Returns the color as a `[u8; 4]`. 101 | /// 102 | /// The color values will be in the order `[r, g, b, a]`. 103 | #[must_use] 104 | pub const fn to_u8_array(self) -> [u8; 4] { 105 | [self.r, self.g, self.b, self.a] 106 | } 107 | 108 | /// Convert the `[u8; 4]` byte array into a `PremulRgba8` color. 109 | /// 110 | /// The color values must be given in the order `[r, g, b, a]`. 111 | #[must_use] 112 | pub const fn from_u8_array([r, g, b, a]: [u8; 4]) -> Self { 113 | Self { r, g, b, a } 114 | } 115 | 116 | /// Returns the color as a little-endian packed value, with `r` the least significant byte and 117 | /// `a` the most significant. 118 | #[must_use] 119 | pub const fn to_u32(self) -> u32 { 120 | u32::from_ne_bytes(self.to_u8_array()) 121 | } 122 | 123 | /// Interpret the little-endian packed value as a color, with `r` the least significant byte 124 | /// and `a` the most significant. 125 | #[must_use] 126 | pub const fn from_u32(packed_bytes: u32) -> Self { 127 | Self::from_u8_array(u32::to_ne_bytes(packed_bytes)) 128 | } 129 | } 130 | 131 | impl From for PremulColor { 132 | fn from(value: PremulRgba8) -> Self { 133 | Self::from_rgba8(value.r, value.g, value.b, value.a) 134 | } 135 | } 136 | 137 | #[cfg(test)] 138 | mod tests { 139 | use super::{PremulRgba8, Rgba8}; 140 | 141 | #[test] 142 | fn to_u32() { 143 | let c = Rgba8 { 144 | r: 1, 145 | g: 2, 146 | b: 3, 147 | a: 4, 148 | }; 149 | assert_eq!(0x04030201_u32.to_le(), c.to_u32()); 150 | 151 | let p = PremulRgba8 { 152 | r: 0xaa, 153 | g: 0xbb, 154 | b: 0xcc, 155 | a: 0xff, 156 | }; 157 | assert_eq!(0xffccbbaa_u32.to_le(), p.to_u32()); 158 | } 159 | 160 | #[test] 161 | fn from_u32() { 162 | let c = Rgba8 { 163 | r: 1, 164 | g: 2, 165 | b: 3, 166 | a: 4, 167 | }; 168 | assert_eq!(Rgba8::from_u32(0x04030201_u32.to_le()), c); 169 | 170 | let p = PremulRgba8 { 171 | r: 0xaa, 172 | g: 0xbb, 173 | b: 0xcc, 174 | a: 0xff, 175 | }; 176 | assert_eq!(PremulRgba8::from_u32(0xffccbbaa_u32.to_le()), p); 177 | } 178 | 179 | #[test] 180 | fn to_u8_array() { 181 | let c = Rgba8 { 182 | r: 1, 183 | g: 2, 184 | b: 3, 185 | a: 4, 186 | }; 187 | assert_eq!([1, 2, 3, 4], c.to_u8_array()); 188 | 189 | let p = PremulRgba8 { 190 | r: 0xaa, 191 | g: 0xbb, 192 | b: 0xcc, 193 | a: 0xff, 194 | }; 195 | assert_eq!([0xaa, 0xbb, 0xcc, 0xff], p.to_u8_array()); 196 | } 197 | 198 | #[test] 199 | fn from_u8_array() { 200 | let c = Rgba8 { 201 | r: 1, 202 | g: 2, 203 | b: 3, 204 | a: 4, 205 | }; 206 | assert_eq!(Rgba8::from_u8_array([1, 2, 3, 4]), c); 207 | 208 | let p = PremulRgba8 { 209 | r: 0xaa, 210 | g: 0xbb, 211 | b: 0xcc, 212 | a: 0xff, 213 | }; 214 | assert_eq!(PremulRgba8::from_u8_array([0xaa, 0xbb, 0xcc, 0xff]), p); 215 | } 216 | 217 | #[test] 218 | #[cfg(feature = "bytemuck")] 219 | fn bytemuck_to_u32() { 220 | let c = Rgba8 { 221 | r: 1, 222 | g: 2, 223 | b: 3, 224 | a: 4, 225 | }; 226 | assert_eq!(c.to_u32(), bytemuck::cast(c)); 227 | 228 | let p = PremulRgba8 { 229 | r: 0xaa, 230 | g: 0xbb, 231 | b: 0xcc, 232 | a: 0xff, 233 | }; 234 | assert_eq!(p.to_u32(), bytemuck::cast(p)); 235 | } 236 | 237 | #[test] 238 | #[cfg(feature = "bytemuck")] 239 | fn bytemuck_from_u32() { 240 | let c = 0x04030201_u32.to_le(); 241 | assert_eq!(Rgba8::from_u32(c), bytemuck::cast(c)); 242 | 243 | let p = 0xffccbbaa_u32.to_le(); 244 | assert_eq!(PremulRgba8::from_u32(p), bytemuck::cast(p)); 245 | } 246 | 247 | #[test] 248 | #[cfg(feature = "bytemuck")] 249 | fn bytemuck_to_u8_array() { 250 | let c = Rgba8 { 251 | r: 1, 252 | g: 2, 253 | b: 3, 254 | a: 4, 255 | }; 256 | assert_eq!(c.to_u8_array(), bytemuck::cast::<_, [u8; 4]>(c)); 257 | assert_eq!(c.to_u8_array(), bytemuck::cast::<_, [u8; 4]>(c.to_u32())); 258 | 259 | let p = PremulRgba8 { 260 | r: 0xaa, 261 | g: 0xbb, 262 | b: 0xcc, 263 | a: 0xff, 264 | }; 265 | assert_eq!(p.to_u8_array(), bytemuck::cast::<_, [u8; 4]>(p)); 266 | assert_eq!(p.to_u8_array(), bytemuck::cast::<_, [u8; 4]>(p.to_u32())); 267 | } 268 | 269 | #[test] 270 | #[cfg(feature = "bytemuck")] 271 | fn bytemuck_from_u8_array() { 272 | let c = [1, 2, 3, 4]; 273 | assert_eq!(Rgba8::from_u8_array(c), bytemuck::cast(c)); 274 | 275 | let p = [0xaa, 0xbb, 0xcc, 0xff]; 276 | assert_eq!(PremulRgba8::from_u8_array(p), bytemuck::cast(p)); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /color/src/serialize.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Color Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | //! CSS-compatible string serializations of colors. 5 | 6 | use core::fmt::{Formatter, Result}; 7 | 8 | use crate::{ColorSpaceTag, DynamicColor, Rgba8}; 9 | 10 | fn write_scaled_component( 11 | color: &DynamicColor, 12 | ix: usize, 13 | f: &mut Formatter<'_>, 14 | scale: f32, 15 | ) -> Result { 16 | if color.flags.missing().contains(ix) { 17 | // According to the serialization rules (§15.2), missing should be converted to 0. 18 | // However, it seems useful to preserve these. Perhaps we want to talk about whether 19 | // we want string formatting to strictly follow the serialization spec. 20 | 21 | write!(f, "none") 22 | } else { 23 | write!(f, "{}", color.components[ix] * scale) 24 | } 25 | } 26 | 27 | fn write_modern_function(color: &DynamicColor, name: &str, f: &mut Formatter<'_>) -> Result { 28 | write!(f, "{name}(")?; 29 | write_scaled_component(color, 0, f, 1.0)?; 30 | write!(f, " ")?; 31 | write_scaled_component(color, 1, f, 1.0)?; 32 | write!(f, " ")?; 33 | write_scaled_component(color, 2, f, 1.0)?; 34 | if color.components[3] < 1.0 { 35 | write!(f, " / ")?; 36 | // TODO: clamp negative values 37 | write_scaled_component(color, 3, f, 1.0)?; 38 | } 39 | write!(f, ")") 40 | } 41 | 42 | fn write_color_function(color: &DynamicColor, name: &str, f: &mut Formatter<'_>) -> Result { 43 | write!(f, "color({name} ")?; 44 | write_scaled_component(color, 0, f, 1.0)?; 45 | write!(f, " ")?; 46 | write_scaled_component(color, 1, f, 1.0)?; 47 | write!(f, " ")?; 48 | write_scaled_component(color, 2, f, 1.0)?; 49 | if color.components[3] < 1.0 { 50 | write!(f, " / ")?; 51 | // TODO: clamp negative values 52 | write_scaled_component(color, 3, f, 1.0)?; 53 | } 54 | write!(f, ")") 55 | } 56 | 57 | fn write_legacy_function( 58 | color: &DynamicColor, 59 | name: &str, 60 | scale: f32, 61 | f: &mut Formatter<'_>, 62 | ) -> Result { 63 | let opt_a = if color.components[3] < 1.0 { "a" } else { "" }; 64 | write!(f, "{name}{opt_a}(")?; 65 | write_scaled_component(color, 0, f, scale)?; 66 | write!(f, ", ")?; 67 | write_scaled_component(color, 1, f, scale)?; 68 | write!(f, ", ")?; 69 | write_scaled_component(color, 2, f, scale)?; 70 | if color.components[3] < 1.0 { 71 | write!(f, ", ")?; 72 | // TODO: clamp negative values 73 | write_scaled_component(color, 3, f, 1.0)?; 74 | } 75 | write!(f, ")") 76 | } 77 | 78 | impl core::fmt::Display for DynamicColor { 79 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 80 | if let Some(color_name) = self.flags.color_name() { 81 | return write!(f, "{color_name}"); 82 | } 83 | 84 | match self.cs { 85 | ColorSpaceTag::Srgb if self.flags.named() => { 86 | write_legacy_function(self, "rgb", 255.0, f) 87 | } 88 | ColorSpaceTag::Hsl | ColorSpaceTag::Hwb if self.flags.named() => { 89 | let srgb = self.convert(ColorSpaceTag::Srgb); 90 | write_legacy_function(&srgb, "rgb", 255.0, f) 91 | } 92 | ColorSpaceTag::Srgb => write_color_function(self, "srgb", f), 93 | ColorSpaceTag::LinearSrgb => write_color_function(self, "srgb-linear", f), 94 | ColorSpaceTag::DisplayP3 => write_color_function(self, "display-p3", f), 95 | ColorSpaceTag::A98Rgb => write_color_function(self, "a98-rgb", f), 96 | ColorSpaceTag::ProphotoRgb => write_color_function(self, "prophoto-rgb", f), 97 | ColorSpaceTag::Rec2020 => write_color_function(self, "rec2020", f), 98 | ColorSpaceTag::Aces2065_1 => write_color_function(self, "--aces2065-1", f), 99 | ColorSpaceTag::AcesCg => write_color_function(self, "--acescg", f), 100 | ColorSpaceTag::Hsl => write_legacy_function(self, "hsl", 1.0, f), 101 | ColorSpaceTag::Hwb => write_modern_function(self, "hwb", f), 102 | ColorSpaceTag::XyzD50 => write_color_function(self, "xyz-d50", f), 103 | ColorSpaceTag::XyzD65 => write_color_function(self, "xyz-d65", f), 104 | ColorSpaceTag::Lab => write_modern_function(self, "lab", f), 105 | ColorSpaceTag::Lch => write_modern_function(self, "lch", f), 106 | ColorSpaceTag::Oklab => write_modern_function(self, "oklab", f), 107 | ColorSpaceTag::Oklch => write_modern_function(self, "oklch", f), 108 | } 109 | } 110 | } 111 | 112 | impl core::fmt::Display for Rgba8 { 113 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 114 | if self.a == 255 { 115 | write!(f, "rgb({}, {}, {})", self.r, self.g, self.b) 116 | } else { 117 | let a = self.a as f32 * (1.0 / 255.0); 118 | write!(f, "rgba({}, {}, {}, {a})", self.r, self.g, self.b) 119 | } 120 | } 121 | } 122 | 123 | impl core::fmt::LowerHex for Rgba8 { 124 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 125 | if self.a == 255 { 126 | write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b) 127 | } else { 128 | write!( 129 | f, 130 | "#{:02x}{:02x}{:02x}{:02x}", 131 | self.r, self.g, self.b, self.a 132 | ) 133 | } 134 | } 135 | } 136 | 137 | impl core::fmt::UpperHex for Rgba8 { 138 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 139 | if self.a == 255 { 140 | write!(f, "#{:02X}{:02X}{:02X}", self.r, self.g, self.b) 141 | } else { 142 | write!( 143 | f, 144 | "#{:02X}{:02X}{:02X}{:02X}", 145 | self.r, self.g, self.b, self.a 146 | ) 147 | } 148 | } 149 | } 150 | 151 | #[cfg(test)] 152 | mod tests { 153 | extern crate alloc; 154 | 155 | use crate::{parse_color, AlphaColor, DynamicColor, Hsl, Oklab, Srgb, XyzD65}; 156 | use alloc::format; 157 | 158 | #[test] 159 | fn rgb8() { 160 | let c = parse_color("#abcdef").unwrap().to_alpha_color::(); 161 | assert_eq!(format!("{:x}", c.to_rgba8()), "#abcdef"); 162 | assert_eq!(format!("{:X}", c.to_rgba8()), "#ABCDEF"); 163 | let c_alpha = c.with_alpha(1. / 3.); 164 | assert_eq!(format!("{:x}", c_alpha.to_rgba8()), "#abcdef55"); 165 | assert_eq!(format!("{:X}", c_alpha.to_rgba8()), "#ABCDEF55"); 166 | } 167 | 168 | #[test] 169 | fn specified_to_serialized() { 170 | for (specified, expected) in [ 171 | ("#ff0000", "rgb(255, 0, 0)"), 172 | ("rgb(255,0,0)", "rgb(255, 0, 0)"), 173 | ("rgba(255,0,0,50%)", "rgba(255, 0, 0, 0.5)"), 174 | ("rgb(255 0 0 / 95%)", "rgba(255, 0, 0, 0.95)"), 175 | // TODO: output rounding? Otherwise the tests should check for approximate equality 176 | // (and not string equality) for these conversion cases 177 | // ( 178 | // "hwb(740deg 20% 30% / 50%)", 179 | // "rgba(178.5, 93.50008, 50.999996, 0.5)", 180 | // ), 181 | ("ReD", "red"), 182 | ("RgB(1,1,1)", "rgb(1, 1, 1)"), 183 | ("rgb(257,-2,50)", "rgb(255, 0, 50)"), 184 | ("color(srgb 1.0 1.0 1.0)", "color(srgb 1 1 1)"), 185 | ("oklab(0.4 0.2 -0.2)", "oklab(0.4 0.2 -0.2)"), 186 | ("lab(20% 0 60)", "lab(20 0 60)"), 187 | ] { 188 | let result = format!("{}", parse_color(specified).unwrap()); 189 | assert_eq!( 190 | result, 191 | expected, 192 | "Failed serializing specified color `{specified}`. Expected: `{expected}`. Got: `{result}`." 193 | ); 194 | } 195 | 196 | // TODO: this can be removed when the "output rounding" TODO above is resolved. Here we 197 | // just check the prefix is as expected. 198 | for (specified, expected_prefix) in [ 199 | ("hwb(740deg 20% 30%)", "rgb("), 200 | ("hwb(740deg 20% 30% / 50%)", "rgba("), 201 | ("hsl(120deg 50% 25%)", "rgb("), 202 | ("hsla(0.4turn 50% 25% / 50%)", "rgba("), 203 | ] { 204 | let result = format!("{}", parse_color(specified).unwrap()); 205 | assert!( 206 | result.starts_with(expected_prefix), 207 | "Failed serializing specified color `{specified}`. Expected the serialization to start with: `{expected_prefix}`. Got: `{result}`." 208 | ); 209 | } 210 | } 211 | 212 | #[test] 213 | fn generated_to_serialized() { 214 | for (color, expected) in [ 215 | ( 216 | DynamicColor::from_alpha_color(AlphaColor::::new([0.5, 0.2, 1.1, 0.5])), 217 | "color(srgb 0.5 0.2 1.1 / 0.5)", 218 | ), 219 | ( 220 | DynamicColor::from_alpha_color(AlphaColor::::new([0.4, 0.2, -0.2, 1.])), 221 | "oklab(0.4 0.2 -0.2)", 222 | ), 223 | ( 224 | DynamicColor::from_alpha_color(AlphaColor::::new([ 225 | 0.472, 0.372, 0.131, 1., 226 | ])), 227 | "color(xyz-d65 0.472 0.372 0.131)", 228 | ), 229 | // Perhaps this should actually serialize to `rgb(...)`. 230 | ( 231 | DynamicColor::from_alpha_color(AlphaColor::::new([120., 50., 25., 1.])), 232 | "hsl(120, 50, 25)", 233 | ), 234 | ] { 235 | let result = format!("{color}"); 236 | assert_eq!( 237 | result, 238 | expected, 239 | "Failed serializing specified color `{color}`. Expected: `{expected}`. Got: `{result}`." 240 | ); 241 | } 242 | } 243 | 244 | #[test] 245 | fn roundtrip_named_colors() { 246 | for name in crate::x11_colors::NAMES { 247 | let result = format!("{}", parse_color(name).unwrap()); 248 | assert_eq!( 249 | result, 250 | name, 251 | "Failed serializing specified named color `{name}`. Expected it to roundtrip. Got: `{result}`." 252 | ); 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /color/src/tag.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Color Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | //! The color space tag enum. 5 | 6 | use crate::{ 7 | A98Rgb, Aces2065_1, AcesCg, Chromaticity, ColorSpace, ColorSpaceLayout, DisplayP3, Hsl, Hwb, 8 | Lab, Lch, LinearSrgb, Missing, Oklab, Oklch, ProphotoRgb, Rec2020, Srgb, XyzD50, XyzD65, 9 | }; 10 | 11 | /// The color space tag for [dynamic colors]. 12 | /// 13 | /// This represents a fixed set of known color spaces. The set contains all 14 | /// color spaces in the CSS Color 4 spec and includes some other color spaces 15 | /// useful for computer graphics. 16 | /// 17 | /// The integer values of these variants can change in breaking releases. 18 | /// 19 | /// [dynamic colors]: crate::DynamicColor 20 | // 21 | // Note: when adding an RGB-like color space, add to `same_analogous`. 22 | #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] 23 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 24 | #[non_exhaustive] 25 | #[repr(u8)] 26 | pub enum ColorSpaceTag { 27 | /// The [`Srgb`] color space. 28 | Srgb = 0, 29 | /// The [`LinearSrgb`] color space. 30 | LinearSrgb = 1, 31 | /// The [`Lab`] color space. 32 | Lab = 2, 33 | /// The [`Lch`] color space. 34 | Lch = 3, 35 | /// The [`Hsl`] color space. 36 | Hsl = 4, 37 | /// The [`Hwb`] color space. 38 | Hwb = 5, 39 | /// The [`Oklab`] color space. 40 | Oklab = 6, 41 | /// The [`Oklch`] color space. 42 | Oklch = 7, 43 | /// The [`DisplayP3`] color space. 44 | DisplayP3 = 8, 45 | /// The [`A98Rgb`] color space. 46 | A98Rgb = 9, 47 | /// The [`ProphotoRgb`] color space. 48 | ProphotoRgb = 10, 49 | /// The [`Rec2020`] color space. 50 | Rec2020 = 11, 51 | /// The [`Aces2065_1`] color space. 52 | Aces2065_1 = 15, 53 | /// The [`AcesCg`] color space. 54 | AcesCg = 12, 55 | /// The [`XyzD50`] color space. 56 | XyzD50 = 13, 57 | /// The [`XyzD65`] color space. 58 | XyzD65 = 14, 59 | // NOTICE: If a new value is added, be sure to modify `MAX_VALUE` in the bytemuck impl. Also 60 | // note the variants' integer values are not necessarily in order, allowing newly added color 61 | // space tags to be grouped with related color spaces. 62 | } 63 | 64 | impl ColorSpaceTag { 65 | pub(crate) fn layout(self) -> ColorSpaceLayout { 66 | match self { 67 | Self::Lch | Self::Oklch => ColorSpaceLayout::HueThird, 68 | Self::Hsl | Self::Hwb => ColorSpaceLayout::HueFirst, 69 | _ => ColorSpaceLayout::Rectangular, 70 | } 71 | } 72 | 73 | /// Whether all components of the two color spaces are analogous. See also 74 | /// Section 12.2 of CSS Color 4, defining which components are analogous: 75 | /// . 76 | /// 77 | /// Note: if color spaces are the same, then they're also analogous, but 78 | /// in that case we wouldn't do the conversion, so this function is not 79 | /// guaranteed to return the correct answer in those cases. 80 | pub(crate) fn same_analogous(self, other: Self) -> bool { 81 | use ColorSpaceTag::*; 82 | matches!( 83 | (self, other), 84 | ( 85 | Srgb | LinearSrgb 86 | | DisplayP3 87 | | A98Rgb 88 | | ProphotoRgb 89 | | Rec2020 90 | | Aces2065_1 91 | | AcesCg 92 | | XyzD50 93 | | XyzD65, 94 | Srgb | LinearSrgb 95 | | DisplayP3 96 | | A98Rgb 97 | | ProphotoRgb 98 | | Rec2020 99 | | Aces2065_1 100 | | AcesCg 101 | | XyzD50 102 | | XyzD65 103 | ) | (Lab | Oklab, Lab | Oklab) 104 | | (Lch | Oklch, Lch | Oklch) 105 | ) 106 | } 107 | 108 | pub(crate) fn l_missing(self, missing: Missing) -> bool { 109 | use ColorSpaceTag::*; 110 | match self { 111 | Lab | Lch | Oklab | Oklch => missing.contains(0), 112 | Hsl => missing.contains(2), 113 | _ => false, 114 | } 115 | } 116 | 117 | pub(crate) fn set_l_missing(self, missing: &mut Missing, components: &mut [f32; 4]) { 118 | use ColorSpaceTag::*; 119 | match self { 120 | Lab | Lch | Oklab | Oklch => { 121 | missing.insert(0); 122 | components[0] = 0.0; 123 | } 124 | Hsl => { 125 | missing.insert(2); 126 | components[2] = 0.0; 127 | } 128 | _ => (), 129 | } 130 | } 131 | 132 | pub(crate) fn c_missing(self, missing: Missing) -> bool { 133 | use ColorSpaceTag::*; 134 | match self { 135 | Lab | Lch | Oklab | Oklch | Hsl => missing.contains(1), 136 | _ => false, 137 | } 138 | } 139 | 140 | pub(crate) fn set_c_missing(self, missing: &mut Missing, components: &mut [f32; 4]) { 141 | use ColorSpaceTag::*; 142 | match self { 143 | Lab | Lch | Oklab | Oklch | Hsl => { 144 | missing.insert(1); 145 | components[1] = 0.0; 146 | } 147 | _ => (), 148 | } 149 | } 150 | 151 | pub(crate) fn h_missing(self, missing: Missing) -> bool { 152 | self.layout() 153 | .hue_channel() 154 | .is_some_and(|ix| missing.contains(ix)) 155 | } 156 | 157 | pub(crate) fn set_h_missing(self, missing: &mut Missing, components: &mut [f32; 4]) { 158 | if let Some(ix) = self.layout().hue_channel() { 159 | missing.insert(ix); 160 | components[ix] = 0.0; 161 | } 162 | } 163 | 164 | /// Convert an opaque color from linear sRGB. 165 | /// 166 | /// This is the tagged counterpart of [`ColorSpace::from_linear_srgb`]. 167 | pub fn from_linear_srgb(self, rgb: [f32; 3]) -> [f32; 3] { 168 | match self { 169 | Self::Srgb => Srgb::from_linear_srgb(rgb), 170 | Self::LinearSrgb => rgb, 171 | Self::Lab => Lab::from_linear_srgb(rgb), 172 | Self::Lch => Lch::from_linear_srgb(rgb), 173 | Self::Oklab => Oklab::from_linear_srgb(rgb), 174 | Self::Oklch => Oklch::from_linear_srgb(rgb), 175 | Self::DisplayP3 => DisplayP3::from_linear_srgb(rgb), 176 | Self::A98Rgb => A98Rgb::from_linear_srgb(rgb), 177 | Self::ProphotoRgb => ProphotoRgb::from_linear_srgb(rgb), 178 | Self::Rec2020 => Rec2020::from_linear_srgb(rgb), 179 | Self::Aces2065_1 => Aces2065_1::from_linear_srgb(rgb), 180 | Self::AcesCg => AcesCg::from_linear_srgb(rgb), 181 | Self::XyzD50 => XyzD50::from_linear_srgb(rgb), 182 | Self::XyzD65 => XyzD65::from_linear_srgb(rgb), 183 | Self::Hsl => Hsl::from_linear_srgb(rgb), 184 | Self::Hwb => Hwb::from_linear_srgb(rgb), 185 | } 186 | } 187 | 188 | /// Convert an opaque color to linear sRGB. 189 | /// 190 | /// This is the tagged counterpart of [`ColorSpace::to_linear_srgb`]. 191 | pub fn to_linear_srgb(self, src: [f32; 3]) -> [f32; 3] { 192 | match self { 193 | Self::Srgb => Srgb::to_linear_srgb(src), 194 | Self::LinearSrgb => src, 195 | Self::Lab => Lab::to_linear_srgb(src), 196 | Self::Lch => Lch::to_linear_srgb(src), 197 | Self::Oklab => Oklab::to_linear_srgb(src), 198 | Self::Oklch => Oklch::to_linear_srgb(src), 199 | Self::DisplayP3 => DisplayP3::to_linear_srgb(src), 200 | Self::A98Rgb => A98Rgb::to_linear_srgb(src), 201 | Self::ProphotoRgb => ProphotoRgb::to_linear_srgb(src), 202 | Self::Rec2020 => Rec2020::to_linear_srgb(src), 203 | Self::Aces2065_1 => Aces2065_1::to_linear_srgb(src), 204 | Self::AcesCg => AcesCg::to_linear_srgb(src), 205 | Self::XyzD50 => XyzD50::to_linear_srgb(src), 206 | Self::XyzD65 => XyzD65::to_linear_srgb(src), 207 | Self::Hsl => Hsl::to_linear_srgb(src), 208 | Self::Hwb => Hwb::to_linear_srgb(src), 209 | } 210 | } 211 | 212 | /// Convert the color components into the target color space. 213 | /// 214 | /// This is the tagged counterpart of [`ColorSpace::convert`]. 215 | pub fn convert(self, target: Self, src: [f32; 3]) -> [f32; 3] { 216 | match (self, target) { 217 | _ if self == target => src, 218 | (Self::Oklab, Self::Oklch) | (Self::Lab, Self::Lch) => Oklab::convert::(src), 219 | (Self::Oklch, Self::Oklab) | (Self::Lch, Self::Lab) => Oklch::convert::(src), 220 | (Self::Srgb, Self::Hsl) => Srgb::convert::(src), 221 | (Self::Hsl, Self::Srgb) => Hsl::convert::(src), 222 | (Self::Srgb, Self::Hwb) => Srgb::convert::(src), 223 | (Self::Hwb, Self::Srgb) => Hwb::convert::(src), 224 | (Self::Hsl, Self::Hwb) => Hsl::convert::(src), 225 | (Self::Hwb, Self::Hsl) => Hwb::convert::(src), 226 | _ => target.from_linear_srgb(self.to_linear_srgb(src)), 227 | } 228 | } 229 | 230 | /// Convert an opaque color from linear sRGB, without chromatic adaptation. 231 | /// 232 | /// For most use-cases you should consider using the chromatically-adapting 233 | /// [`ColorSpaceTag::from_linear_srgb`] instead. 234 | /// 235 | /// This is the tagged counterpart of [`ColorSpace::from_linear_srgb_absolute`]. 236 | pub fn from_linear_srgb_absolute(self, rgb: [f32; 3]) -> [f32; 3] { 237 | match self { 238 | Self::Srgb => Srgb::from_linear_srgb_absolute(rgb), 239 | Self::LinearSrgb => rgb, 240 | Self::Lab => Lab::from_linear_srgb_absolute(rgb), 241 | Self::Lch => Lch::from_linear_srgb_absolute(rgb), 242 | Self::Oklab => Oklab::from_linear_srgb_absolute(rgb), 243 | Self::Oklch => Oklch::from_linear_srgb_absolute(rgb), 244 | Self::DisplayP3 => DisplayP3::from_linear_srgb_absolute(rgb), 245 | Self::A98Rgb => A98Rgb::from_linear_srgb_absolute(rgb), 246 | Self::ProphotoRgb => ProphotoRgb::from_linear_srgb_absolute(rgb), 247 | Self::Rec2020 => Rec2020::from_linear_srgb_absolute(rgb), 248 | Self::Aces2065_1 => Aces2065_1::from_linear_srgb_absolute(rgb), 249 | Self::AcesCg => AcesCg::from_linear_srgb_absolute(rgb), 250 | Self::XyzD50 => XyzD50::from_linear_srgb_absolute(rgb), 251 | Self::XyzD65 => XyzD65::from_linear_srgb_absolute(rgb), 252 | Self::Hsl => Hsl::from_linear_srgb_absolute(rgb), 253 | Self::Hwb => Hwb::from_linear_srgb_absolute(rgb), 254 | } 255 | } 256 | 257 | /// Convert an opaque color to linear sRGB, without chromatic adaptation. 258 | /// 259 | /// For most use-cases you should consider using the chromatically-adapting 260 | /// [`ColorSpaceTag::to_linear_srgb`] instead. 261 | /// 262 | /// This is the tagged counterpart of [`ColorSpace::to_linear_srgb_absolute`]. 263 | pub fn to_linear_srgb_absolute(self, src: [f32; 3]) -> [f32; 3] { 264 | match self { 265 | Self::Srgb => Srgb::to_linear_srgb_absolute(src), 266 | Self::LinearSrgb => src, 267 | Self::Lab => Lab::to_linear_srgb_absolute(src), 268 | Self::Lch => Lch::to_linear_srgb_absolute(src), 269 | Self::Oklab => Oklab::to_linear_srgb_absolute(src), 270 | Self::Oklch => Oklch::to_linear_srgb_absolute(src), 271 | Self::DisplayP3 => DisplayP3::to_linear_srgb_absolute(src), 272 | Self::A98Rgb => A98Rgb::to_linear_srgb_absolute(src), 273 | Self::ProphotoRgb => ProphotoRgb::to_linear_srgb_absolute(src), 274 | Self::Rec2020 => Rec2020::to_linear_srgb_absolute(src), 275 | Self::Aces2065_1 => Aces2065_1::to_linear_srgb_absolute(src), 276 | Self::AcesCg => AcesCg::to_linear_srgb_absolute(src), 277 | Self::XyzD50 => XyzD50::to_linear_srgb_absolute(src), 278 | Self::XyzD65 => XyzD65::to_linear_srgb_absolute(src), 279 | Self::Hsl => Hsl::to_linear_srgb_absolute(src), 280 | Self::Hwb => Hwb::to_linear_srgb_absolute(src), 281 | } 282 | } 283 | 284 | /// Convert the color components into the target color space, without chromatic adaptation. 285 | /// 286 | /// For most use-cases you should consider using the chromatically-adapting 287 | /// [`ColorSpaceTag::convert`] instead. 288 | /// 289 | /// This is the tagged counterpart of [`ColorSpace::convert_absolute`]. See the documentation 290 | /// on [`ColorSpace::convert_absolute`] for more information. 291 | pub fn convert_absolute(self, target: Self, src: [f32; 3]) -> [f32; 3] { 292 | match (self, target) { 293 | _ if self == target => src, 294 | (Self::Oklab, Self::Oklch) | (Self::Lab, Self::Lch) => { 295 | Oklab::convert_absolute::(src) 296 | } 297 | (Self::Oklch, Self::Oklab) | (Self::Lch, Self::Lab) => { 298 | Oklch::convert_absolute::(src) 299 | } 300 | (Self::Srgb, Self::Hsl) => Srgb::convert_absolute::(src), 301 | (Self::Hsl, Self::Srgb) => Hsl::convert_absolute::(src), 302 | (Self::Srgb, Self::Hwb) => Srgb::convert_absolute::(src), 303 | (Self::Hwb, Self::Srgb) => Hwb::convert_absolute::(src), 304 | (Self::Hsl, Self::Hwb) => Hsl::convert_absolute::(src), 305 | (Self::Hwb, Self::Hsl) => Hwb::convert_absolute::(src), 306 | _ => target.from_linear_srgb_absolute(self.to_linear_srgb_absolute(src)), 307 | } 308 | } 309 | 310 | /// Chromatically adapt the color between the given white point chromaticities. 311 | /// 312 | /// This is the tagged counterpart of [`ColorSpace::chromatically_adapt`]. 313 | /// 314 | /// The color is assumed to be under a reference white point of `from` and is chromatically 315 | /// adapted to the given white point `to`. The linear Bradford transform is used to perform the 316 | /// chromatic adaptation. 317 | pub fn chromatically_adapt( 318 | self, 319 | src: [f32; 3], 320 | from: Chromaticity, 321 | to: Chromaticity, 322 | ) -> [f32; 3] { 323 | match self { 324 | Self::Srgb => Srgb::chromatically_adapt(src, from, to), 325 | Self::LinearSrgb => LinearSrgb::chromatically_adapt(src, from, to), 326 | Self::Lab => Lab::chromatically_adapt(src, from, to), 327 | Self::Lch => Lch::chromatically_adapt(src, from, to), 328 | Self::Oklab => Oklab::chromatically_adapt(src, from, to), 329 | Self::Oklch => Oklch::chromatically_adapt(src, from, to), 330 | Self::DisplayP3 => DisplayP3::chromatically_adapt(src, from, to), 331 | Self::A98Rgb => A98Rgb::chromatically_adapt(src, from, to), 332 | Self::ProphotoRgb => ProphotoRgb::chromatically_adapt(src, from, to), 333 | Self::Rec2020 => Rec2020::chromatically_adapt(src, from, to), 334 | Self::Aces2065_1 => Aces2065_1::chromatically_adapt(src, from, to), 335 | Self::AcesCg => AcesCg::chromatically_adapt(src, from, to), 336 | Self::XyzD50 => XyzD50::chromatically_adapt(src, from, to), 337 | Self::XyzD65 => XyzD65::chromatically_adapt(src, from, to), 338 | Self::Hsl => Hsl::chromatically_adapt(src, from, to), 339 | Self::Hwb => Hwb::chromatically_adapt(src, from, to), 340 | } 341 | } 342 | 343 | /// Scale the chroma by the given amount. 344 | /// 345 | /// This is the tagged counterpart of [`ColorSpace::scale_chroma`]. 346 | pub fn scale_chroma(self, src: [f32; 3], scale: f32) -> [f32; 3] { 347 | match self { 348 | Self::LinearSrgb => LinearSrgb::scale_chroma(src, scale), 349 | Self::Oklab | Self::Lab => Oklab::scale_chroma(src, scale), 350 | Self::Oklch | Self::Lch | Self::Hsl => Oklch::scale_chroma(src, scale), 351 | _ => { 352 | let rgb = self.to_linear_srgb(src); 353 | let scaled = LinearSrgb::scale_chroma(rgb, scale); 354 | self.from_linear_srgb(scaled) 355 | } 356 | } 357 | } 358 | 359 | /// Clip the color's components to fit within the natural gamut of the color space. 360 | /// 361 | /// See [`ColorSpace::clip`] for more details. 362 | pub fn clip(self, src: [f32; 3]) -> [f32; 3] { 363 | match self { 364 | Self::Srgb => Srgb::clip(src), 365 | Self::LinearSrgb => LinearSrgb::clip(src), 366 | Self::Lab => Lab::clip(src), 367 | Self::Lch => Lch::clip(src), 368 | Self::Oklab => Oklab::clip(src), 369 | Self::Oklch => Oklch::clip(src), 370 | Self::DisplayP3 => DisplayP3::clip(src), 371 | Self::A98Rgb => A98Rgb::clip(src), 372 | Self::ProphotoRgb => ProphotoRgb::clip(src), 373 | Self::Rec2020 => Rec2020::clip(src), 374 | Self::Aces2065_1 => Aces2065_1::clip(src), 375 | Self::AcesCg => AcesCg::clip(src), 376 | Self::XyzD50 => XyzD50::clip(src), 377 | Self::XyzD65 => XyzD65::clip(src), 378 | Self::Hsl => Hsl::clip(src), 379 | Self::Hwb => Hwb::clip(src), 380 | } 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /color/src/x11_colors.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Color Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | // This file was auto-generated by make_x11_colors.py. Do not hand-edit. 5 | 6 | const SALTS: [u8; 142] = [ 7 | 3, 8, 0, 5, 0, 0, 0, 0, 3, 0, 1, 0, 9, 0, 112, 7, 3, 0, 1, 19, 1, 0, 0, 0, 7, 3, 5, 2, 23, 38, 8 | 9, 0, 4, 28, 5, 3, 0, 0, 1, 0, 0, 18, 7, 0, 3, 2, 8, 2, 13, 16, 12, 1, 3, 0, 11, 0, 14, 0, 1, 9 | 0, 2, 4, 1, 0, 3, 6, 1, 0, 3, 3, 0, 0, 3, 0, 6, 17, 0, 10, 0, 0, 1, 1, 7, 12, 1, 7, 0, 2, 0, 0, 10 | 1, 1, 1, 2, 1, 0, 1, 0, 3, 6, 5, 1, 0, 1, 5, 0, 2, 0, 0, 1, 0, 1, 0, 1, 0, 1, 6, 6, 2, 2, 0, 5, 11 | 0, 0, 1, 4, 2, 0, 0, 2, 2, 0, 1, 0, 0, 1, 5, 0, 0, 1, 0, 0, 12 | ]; 13 | 14 | pub(crate) const NAMES: [&str; 142] = [ 15 | "rosybrown", 16 | "forestgreen", 17 | "cornsilk", 18 | "silver", 19 | "purple", 20 | "thistle", 21 | "antiquewhite", 22 | "mediumspringgreen", 23 | "dimgray", 24 | "olivedrab", 25 | "wheat", 26 | "mediumturquoise", 27 | "darkseagreen", 28 | "burlywood", 29 | "magenta", 30 | "lemonchiffon", 31 | "darkkhaki", 32 | "lightyellow", 33 | "mediumslateblue", 34 | "mediumorchid", 35 | "rebeccapurple", 36 | "transparent", 37 | "springgreen", 38 | "maroon", 39 | "lightseagreen", 40 | "yellow", 41 | "gray", 42 | "tan", 43 | "gainsboro", 44 | "chartreuse", 45 | "gold", 46 | "orchid", 47 | "red", 48 | "snow", 49 | "greenyellow", 50 | "limegreen", 51 | "aqua", 52 | "salmon", 53 | "sienna", 54 | "slateblue", 55 | "aquamarine", 56 | "azure", 57 | "saddlebrown", 58 | "lightskyblue", 59 | "cornflowerblue", 60 | "darkorange", 61 | "dodgerblue", 62 | "midnightblue", 63 | "mediumpurple", 64 | "white", 65 | "darkslateblue", 66 | "darkturquoise", 67 | "paleturquoise", 68 | "linen", 69 | "slategray", 70 | "lightpink", 71 | "lightcyan", 72 | "peru", 73 | "beige", 74 | "indianred", 75 | "navajowhite", 76 | "green", 77 | "lightblue", 78 | "mediumaquamarine", 79 | "palegreen", 80 | "skyblue", 81 | "darkviolet", 82 | "deeppink", 83 | "darkcyan", 84 | "peachpuff", 85 | "navy", 86 | "darkolivegreen", 87 | "lavender", 88 | "lightgoldenrodyellow", 89 | "moccasin", 90 | "deepskyblue", 91 | "firebrick", 92 | "ghostwhite", 93 | "honeydew", 94 | "turquoise", 95 | "darkorchid", 96 | "plum", 97 | "lightgray", 98 | "chocolate", 99 | "lightsteelblue", 100 | "bisque", 101 | "mediumblue", 102 | "aliceblue", 103 | "lightcoral", 104 | "lightslategray", 105 | "darkgreen", 106 | "mintcream", 107 | "coral", 108 | "darkmagenta", 109 | "mistyrose", 110 | "whitesmoke", 111 | "lightsalmon", 112 | "mediumvioletred", 113 | "tomato", 114 | "indigo", 115 | "brown", 116 | "lawngreen", 117 | "cadetblue", 118 | "violet", 119 | "fuchsia", 120 | "darkblue", 121 | "orange", 122 | "mediumseagreen", 123 | "lime", 124 | "blue", 125 | "darkred", 126 | "blueviolet", 127 | "papayawhip", 128 | "blanchedalmond", 129 | "seagreen", 130 | "steelblue", 131 | "seashell", 132 | "lavenderblush", 133 | "floralwhite", 134 | "lightgreen", 135 | "darksalmon", 136 | "black", 137 | "crimson", 138 | "hotpink", 139 | "darkslategray", 140 | "palevioletred", 141 | "oldlace", 142 | "darkgray", 143 | "olive", 144 | "palegoldenrod", 145 | "powderblue", 146 | "pink", 147 | "orangered", 148 | "ivory", 149 | "teal", 150 | "cyan", 151 | "darkgoldenrod", 152 | "royalblue", 153 | "sandybrown", 154 | "khaki", 155 | "goldenrod", 156 | "yellowgreen", 157 | ]; 158 | 159 | /// RGBA8 color components of the named X11 colors, in the same order as [`NAMES`]. 160 | /// 161 | /// Use [`lookup_palette_index`] to efficiently find the color components for a given color name 162 | /// string. 163 | pub(crate) const COLORS: [[u8; 4]; 142] = [ 164 | [188, 143, 143, 255], 165 | [34, 139, 34, 255], 166 | [255, 248, 220, 255], 167 | [192, 192, 192, 255], 168 | [128, 0, 128, 255], 169 | [216, 191, 216, 255], 170 | [250, 235, 215, 255], 171 | [0, 250, 154, 255], 172 | [105, 105, 105, 255], 173 | [107, 142, 35, 255], 174 | [245, 222, 179, 255], 175 | [72, 209, 204, 255], 176 | [143, 188, 143, 255], 177 | [222, 184, 135, 255], 178 | [255, 0, 255, 255], 179 | [255, 250, 205, 255], 180 | [189, 183, 107, 255], 181 | [255, 255, 224, 255], 182 | [123, 104, 238, 255], 183 | [186, 85, 211, 255], 184 | [102, 51, 153, 255], 185 | [0, 0, 0, 0], 186 | [0, 255, 127, 255], 187 | [128, 0, 0, 255], 188 | [32, 178, 170, 255], 189 | [255, 255, 0, 255], 190 | [128, 128, 128, 255], 191 | [210, 180, 140, 255], 192 | [220, 220, 220, 255], 193 | [127, 255, 0, 255], 194 | [255, 215, 0, 255], 195 | [218, 112, 214, 255], 196 | [255, 0, 0, 255], 197 | [255, 250, 250, 255], 198 | [173, 255, 47, 255], 199 | [50, 205, 50, 255], 200 | [0, 255, 255, 255], 201 | [250, 128, 114, 255], 202 | [160, 82, 45, 255], 203 | [106, 90, 205, 255], 204 | [127, 255, 212, 255], 205 | [240, 255, 255, 255], 206 | [139, 69, 19, 255], 207 | [135, 206, 250, 255], 208 | [100, 149, 237, 255], 209 | [255, 140, 0, 255], 210 | [30, 144, 255, 255], 211 | [25, 25, 112, 255], 212 | [147, 112, 219, 255], 213 | [255, 255, 255, 255], 214 | [72, 61, 139, 255], 215 | [0, 206, 209, 255], 216 | [175, 238, 238, 255], 217 | [250, 240, 230, 255], 218 | [112, 128, 144, 255], 219 | [255, 182, 193, 255], 220 | [224, 255, 255, 255], 221 | [205, 133, 63, 255], 222 | [245, 245, 220, 255], 223 | [205, 92, 92, 255], 224 | [255, 222, 173, 255], 225 | [0, 128, 0, 255], 226 | [173, 216, 230, 255], 227 | [102, 205, 170, 255], 228 | [152, 251, 152, 255], 229 | [135, 206, 235, 255], 230 | [148, 0, 211, 255], 231 | [255, 20, 147, 255], 232 | [0, 139, 139, 255], 233 | [255, 218, 185, 255], 234 | [0, 0, 128, 255], 235 | [85, 107, 47, 255], 236 | [230, 230, 250, 255], 237 | [250, 250, 210, 255], 238 | [255, 228, 181, 255], 239 | [0, 191, 255, 255], 240 | [178, 34, 34, 255], 241 | [248, 248, 255, 255], 242 | [240, 255, 240, 255], 243 | [64, 224, 208, 255], 244 | [153, 50, 204, 255], 245 | [221, 160, 221, 255], 246 | [211, 211, 211, 255], 247 | [210, 105, 30, 255], 248 | [176, 196, 222, 255], 249 | [255, 228, 196, 255], 250 | [0, 0, 205, 255], 251 | [240, 248, 255, 255], 252 | [240, 128, 128, 255], 253 | [119, 136, 153, 255], 254 | [0, 100, 0, 255], 255 | [245, 255, 250, 255], 256 | [255, 127, 80, 255], 257 | [139, 0, 139, 255], 258 | [255, 228, 225, 255], 259 | [245, 245, 245, 255], 260 | [255, 160, 122, 255], 261 | [199, 21, 133, 255], 262 | [255, 99, 71, 255], 263 | [75, 0, 130, 255], 264 | [165, 42, 42, 255], 265 | [124, 252, 0, 255], 266 | [95, 158, 160, 255], 267 | [238, 130, 238, 255], 268 | [255, 0, 255, 255], 269 | [0, 0, 139, 255], 270 | [255, 165, 0, 255], 271 | [60, 179, 113, 255], 272 | [0, 255, 0, 255], 273 | [0, 0, 255, 255], 274 | [139, 0, 0, 255], 275 | [138, 43, 226, 255], 276 | [255, 239, 213, 255], 277 | [255, 235, 205, 255], 278 | [46, 139, 87, 255], 279 | [70, 130, 180, 255], 280 | [255, 245, 238, 255], 281 | [255, 240, 245, 255], 282 | [255, 250, 240, 255], 283 | [144, 238, 144, 255], 284 | [233, 150, 122, 255], 285 | [0, 0, 0, 255], 286 | [220, 20, 60, 255], 287 | [255, 105, 180, 255], 288 | [47, 79, 79, 255], 289 | [219, 112, 147, 255], 290 | [253, 245, 230, 255], 291 | [169, 169, 169, 255], 292 | [128, 128, 0, 255], 293 | [238, 232, 170, 255], 294 | [176, 224, 230, 255], 295 | [255, 192, 203, 255], 296 | [255, 69, 0, 255], 297 | [255, 255, 240, 255], 298 | [0, 128, 128, 255], 299 | [0, 255, 255, 255], 300 | [184, 134, 11, 255], 301 | [65, 105, 225, 255], 302 | [244, 164, 96, 255], 303 | [240, 230, 140, 255], 304 | [218, 165, 32, 255], 305 | [154, 205, 50, 255], 306 | ]; 307 | 308 | /// Hash the 32 bit key into a value less than `n`, adding salt. 309 | /// 310 | /// This is basically the weakest hash we can get away with that 311 | /// still distinguishes all the values. 312 | #[inline] 313 | fn weak_hash(key: u32, salt: u32, n: usize) -> usize { 314 | let y = key.wrapping_add(salt).wrapping_mul(2654435769); 315 | let y = y ^ key; 316 | (((y as u64) * (n as u64)) >> 32) as usize 317 | } 318 | 319 | /// Given a named color (e.g., "red", "mediumorchid"), returns the index of that color into 320 | /// [`COLORS`] and [`NAMES`]. 321 | pub(crate) fn lookup_palette_index(s: &str) -> Option { 322 | let mut key = 0_u32; 323 | for b in s.as_bytes() { 324 | key = key.wrapping_mul(9).wrapping_add(*b as u32); 325 | } 326 | let salt = SALTS[weak_hash(key, 0, SALTS.len())] as u32; 327 | let ix = weak_hash(key, salt, SALTS.len()); 328 | if s == NAMES[ix] { 329 | Some(ix) 330 | } else { 331 | None 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /color_operations/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "color_operations" 3 | version.workspace = true 4 | license.workspace = true 5 | edition.workspace = true 6 | description = "" 7 | keywords = [] 8 | categories = [] 9 | repository.workspace = true 10 | rust-version.workspace = true 11 | 12 | # Whilst we prepare the initial release 13 | publish = false 14 | 15 | [package.metadata.docs.rs] 16 | all-features = true 17 | # There are no platform specific docs. 18 | default-target = "x86_64-unknown-linux-gnu" 19 | targets = [] 20 | 21 | [features] 22 | default = ["std"] 23 | std = ["color/std"] 24 | libm = ["color/libm"] 25 | 26 | [dependencies] 27 | color = { workspace = true, default-features = false } 28 | 29 | [lints] 30 | workspace = true 31 | -------------------------------------------------------------------------------- /color_operations/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /color_operations/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /color_operations/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Color Operations 4 | 5 | **TODO: Add tagline** 6 | 7 | [![Linebender Zulip, #color channel](https://img.shields.io/badge/Linebender-%23color-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/channel/466849-color) 8 | [![dependency status](https://deps.rs/repo/github/linebender/color/status.svg)](https://deps.rs/repo/github/linebender/color) 9 | [![Apache 2.0 or MIT license.](https://img.shields.io/badge/license-Apache--2.0_OR_MIT-blue.svg)](#license) 10 | [![Build status](https://github.com/linebender/color/workflows/CI/badge.svg)](https://github.com/linebender/color/actions) 11 | [![Crates.io](https://img.shields.io/crates/v/color.svg)](https://crates.io/crates/color) 12 | [![Docs](https://docs.rs/color/badge.svg)](https://docs.rs/color) 13 | 14 |
15 | 16 | The Color Operations library provides functionality for ... 17 | 18 | ## Minimum supported Rust Version (MSRV) 19 | 20 | This version of Color Operations has been verified to compile with **Rust 1.82** and later. 21 | 22 | Future versions of Color Operations might increase the Rust version requirement. 23 | It will not be treated as a breaking change and as such can even happen with small patch releases. 24 | 25 |
26 | Click here if compiling fails. 27 | 28 | As time has passed, some of Color Operations' dependencies could have released versions with a higher Rust requirement. 29 | If you encounter a compilation issue due to a dependency and don't want to upgrade your Rust toolchain, then you could downgrade the dependency. 30 | 31 | ```sh 32 | # Use the problematic dependency's name and version 33 | cargo update -p package_name --precise 0.1.1 34 | ``` 35 |
36 | 37 | ## Community 38 | 39 | [![Linebender Zulip](https://img.shields.io/badge/Xi%20Zulip-%23color-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/channel/466849-color) 40 | 41 | Discussion of Color development happens in the [Linebender Zulip](https://xi.zulipchat.com/), specifically the [#color channel](https://xi.zulipchat.com/#narrow/channel/466849-color). 42 | All public content can be read without logging in. 43 | 44 | ## License 45 | 46 | Licensed under either of 47 | 48 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) 49 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 50 | 51 | at your option. 52 | 53 | ## Contribution 54 | 55 | Contributions are welcome by pull request. The [Rust code of conduct] applies. 56 | Please feel free to add your name to the [AUTHORS] file in any substantive pull request. 57 | 58 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions. 59 | 60 | [Rust Code of Conduct]: https://www.rust-lang.org/policies/code-of-conduct 61 | [AUTHORS]: ./AUTHORS 62 | -------------------------------------------------------------------------------- /color_operations/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Color Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | //! # Color Operations 5 | 6 | // LINEBENDER LINT SET - lib.rs - v1 7 | // See https://linebender.org/wiki/canonical-lints/ 8 | // These lints aren't included in Cargo.toml because they 9 | // shouldn't apply to examples and tests 10 | #![warn(unused_crate_dependencies)] 11 | #![warn(clippy::print_stdout, clippy::print_stderr)] 12 | // END LINEBENDER LINT SET 13 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 14 | #![cfg_attr(all(not(feature = "std"), not(test)), no_std)] 15 | 16 | // Temporary to suppress warning. 17 | use color as _; 18 | --------------------------------------------------------------------------------