├── .github ├── FUNDING.yml └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── Makefile ├── README.md ├── examples └── named-colors.rs ├── src ├── cint.rs ├── color.rs ├── color2.rs ├── error.rs ├── lab.rs ├── lib.rs ├── named_colors.rs ├── parser.rs └── utils.rs └── tests ├── chrome_android.rs ├── chromium.rs ├── color.rs ├── firefox.rs ├── named_colors.rs ├── parser.rs └── parser2.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mazznoer 4 | ko_fi: mazznoer 5 | liberapay: mazznoer 6 | custom: "https://paypal.me/mazznoer" 7 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - "**.md" 7 | pull_request: 8 | paths-ignore: 9 | - "**.md" 10 | schedule: 11 | - cron: '0 0 */5 * *' 12 | 13 | env: 14 | CARGO_TERM_COLOR: always 15 | 16 | jobs: 17 | build: 18 | strategy: 19 | matrix: 20 | os: [macos-latest, windows-latest, ubuntu-latest] 21 | runs-on: ${{ matrix.os }} 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Build 27 | run: | 28 | cargo build --verbose --no-default-features 29 | cargo build --verbose --all-features 30 | cargo build --examples --all-features 31 | - uses: taiki-e/install-action@cargo-hack 32 | - run: | 33 | cargo hack build --no-private --feature-powerset --no-dev-deps --optional-deps=cint,lab,serde 34 | 35 | - name: Run tests 36 | run: | 37 | cargo test --verbose --no-default-features 38 | cargo test --verbose --all-features 39 | 40 | - name: Run cargo clippy 41 | run: | 42 | cargo clippy --no-default-features -- -D warnings 43 | cargo clippy --all-features -- -D warnings 44 | 45 | - name: Run cargo fmt 46 | run: | 47 | cargo fmt --all -- --check 48 | 49 | #- name: Tarpaulin code coverage 50 | # id: coverage 51 | # if: matrix.os == 'ubuntu-latest' 52 | # run: > 53 | # cargo install cargo-tarpaulin && 54 | # cargo tarpaulin -o xml 55 | 56 | #- name: Upload to codecov.io 57 | # if: matrix.os == 'ubuntu-latest' 58 | # uses: codecov/codecov-action@v3 59 | 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased](https://github.com/mazznoer/csscolorparser-rs/compare/v0.7.2...HEAD) 4 | 5 | ## [0.7.2](https://github.com/mazznoer/csscolorparser-rs/compare/v0.7.1...v0.7.2) 6 | 7 | ### Added 8 | 9 | - `Color::to_oklcha()` 10 | - `Color::to_css_hex()` 11 | - `Color::to_css_rgb()` 12 | - `Color::to_css_hsl()` 13 | - `Color::to_css_hwb()` 14 | - `Color::to_css_lab()` 15 | - `Color::to_css_lch()` 16 | - `Color::to_css_oklab()` 17 | - `Color::to_css_oklch()` 18 | 19 | ### Changed 20 | 21 | - Deprecate `Color::to_hex_string()` and `Color::to_rgb_string()` 22 | 23 | ## [0.7.1](https://github.com/mazznoer/csscolorparser-rs/compare/v0.7.0...v0.7.1) 24 | 25 | ### Changed 26 | 27 | - Remove some unnecessary allocations on parser code. 28 | 29 | ## [0.7.0](https://github.com/mazznoer/csscolorparser-rs/compare/v0.6.2...v0.7.0) 30 | 31 | ### Added 32 | 33 | - `Color::from_oklcha()` 34 | - Support parsing `oklab()` and `oklch()` color format. 35 | - `Color::{from,to}_{laba,lcha}()` 36 | 37 | ### Changed 38 | 39 | - `f64` -> `f32` 40 | - Return type for `Color::to_{hsva,hsla,hwba,lab,lch,oklaba,linear_rgba}()` changed from tuple to array. 41 | - Deprecate `Color::{from,to}_{lab,lch}()`, use `Color::{from,to}_{laba,lcha}()` instead. 42 | - `NAMED_COLORS` is now public 43 | 44 | ### Removed 45 | 46 | ### Fixed 47 | 48 | - Fix parsing `lab()` and `lch()` color format. 49 | - Update `oklab` formula. 50 | 51 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "csscolorparser" 3 | version = "0.7.2" 4 | authors = ["Nor Khasyatillah "] 5 | edition = "2018" 6 | description = "CSS color parser library" 7 | readme = "README.md" 8 | repository = "https://github.com/mazznoer/csscolorparser-rs" 9 | documentation = "https://docs.rs/csscolorparser/" 10 | license = "MIT OR Apache-2.0" 11 | keywords = ["color", "colour", "css", "parser"] 12 | categories = ["graphics", "parser-implementations"] 13 | exclude = [ 14 | ".github/*", 15 | ] 16 | 17 | [package.metadata.docs.rs] 18 | features = ["named-colors", "lab", "rust-rgb", "cint", "serde"] 19 | 20 | [features] 21 | default = ["named-colors"] 22 | lab = [] 23 | named-colors = ["phf"] 24 | rust-rgb = ["rgb"] 25 | 26 | [dependencies] 27 | cint = { version = "^0.3.1", optional = true } 28 | phf = { version = "0.11.0", optional = true, features = ["macros"] } 29 | rgb = { version = "0.8.33", optional = true } 30 | serde = { version = "1.0.139", optional = true, features = ["derive"] } 31 | 32 | [dev-dependencies] 33 | serde_test = "1.0.139" 34 | 35 | [[test]] 36 | name = "named_colors" 37 | required-features = ["named-colors"] 38 | 39 | [[example]] 40 | name = "named-colors" 41 | required-features = ["named-colors"] 42 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Nor Khasyatillah 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nor Khasyatillah 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | .PHONY: all check test 4 | 5 | all: check test 6 | 7 | check: 8 | cargo build --no-default-features && \ 9 | cargo clippy --no-default-features -- -D warnings && \ 10 | cargo build --all-features && \ 11 | cargo clippy --all-features -- -D warnings && \ 12 | cargo fmt --all -- --check 13 | 14 | test: 15 | cargo test --no-default-features && \ 16 | cargo test --all-features 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Rust CSS Color Parser Library 3 |

4 | 5 |

6 | License 7 | crates.io 8 | Documentation 9 | Build Status 10 | Total Downloads 11 |

12 | 13 |

14 | 15 | DocumentationChangelogFeatures 16 | 17 |

18 | 19 |
20 | 21 | [Rust](https://www.rust-lang.org/) library for parsing CSS color string as defined in the W3C's [CSS Color Module Level 4](https://www.w3.org/TR/css-color-4/). 22 | 23 | ## Supported Color Format 24 | 25 | * [Named colors](https://www.w3.org/TR/css-color-4/#named-colors) 26 | * RGB hexadecimal (with and without `#` prefix) 27 | + Short format `#rgb` 28 | + Short format with alpha `#rgba` 29 | + Long format `#rrggbb` 30 | + Long format with alpha `#rrggbbaa` 31 | * `rgb()` and `rgba()` 32 | * `hsl()` and `hsla()` 33 | * `hwb()` 34 | * `lab()` 35 | * `lch()` 36 | * `oklab()` 37 | * `oklch()` 38 | * `hwba()`, `hsv()`, `hsva()` - not in CSS standard. 39 | 40 | ## Usage 41 | 42 | Add this to your `Cargo.toml` 43 | 44 | ```toml 45 | csscolorparser = "0.7" 46 | ``` 47 | 48 | ## Examples 49 | 50 | Using `csscolorparser::parse()` function. 51 | 52 | ```rust 53 | let c = csscolorparser::parse("rgb(100%,0%,0%)")?; 54 | 55 | assert_eq!(c.to_array(), [1.0, 0.0, 0.0, 1.0]); 56 | assert_eq!(c.to_rgba8(), [255, 0, 0, 255]); 57 | assert_eq!(c.to_css_hex(), "#ff0000"); 58 | assert_eq!(c.to_css_rgb(), "rgb(255 0 0)"); 59 | assert_eq!(c.name(), Some("red")); 60 | ``` 61 | 62 | Using `parse()` method on `&str`. 63 | 64 | ```rust 65 | use csscolorparser::Color; 66 | 67 | let c: Color = "#ff00007f".parse()?; 68 | 69 | assert_eq!(c.to_rgba8(), [255, 0, 0, 127]); 70 | assert_eq!(c.to_css_hex(), "#ff00007f"); 71 | ``` 72 | 73 | ## Features 74 | 75 | ### Default 76 | 77 | * __named-colors__: Enables parsing from [named colors](https://www.w3.org/TR/css-color-4/#named-colors). Requires [`phf`](https://crates.io/crates/phf). Can be disabled using `default-features = false`. 78 | 79 | ### Optional 80 | 81 | * __lab__: Enables parsing `lab()` and `lch()` color format. 82 | * __rust-rgb__: Enables converting from [`rgb`](https://crates.io/crates/rgb) crate types into `Color`. 83 | * __cint__: Enables converting [`cint`](https://crates.io/crates/cint) crate types to and from `Color`. 84 | * __serde__: Enables serializing (into HEX string) and deserializing (from any supported string color format) using [`serde`](https://serde.rs/) framework. 85 | 86 | ## Similar Projects 87 | 88 | * [csscolorparser](https://github.com/mazznoer/csscolorparser) (Go) 89 | * [csscolorparser](https://github.com/deanm/css-color-parser-js) (Javascript) 90 | 91 | -------------------------------------------------------------------------------- /examples/named-colors.rs: -------------------------------------------------------------------------------- 1 | use csscolorparser::NAMED_COLORS; 2 | 3 | fn main() { 4 | for (name, rgb) in &NAMED_COLORS { 5 | let [r, g, b] = rgb; 6 | println!("\x1B[48;2;{r};{g};{b}m \x1B[49m {name} {rgb:?}"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/cint.rs: -------------------------------------------------------------------------------- 1 | use crate::Color; 2 | use cint::{Alpha, ColorInterop, EncodedSrgb}; 3 | 4 | impl ColorInterop for Color { 5 | type CintTy = Alpha>; 6 | } 7 | 8 | impl From for EncodedSrgb { 9 | fn from(c: Color) -> Self { 10 | let Color { r, g, b, a: _ } = c; 11 | EncodedSrgb { r, g, b } 12 | } 13 | } 14 | 15 | impl From> for Color { 16 | fn from(c: EncodedSrgb) -> Self { 17 | let EncodedSrgb { r, g, b } = c; 18 | Self::new(r, g, b, 1.0) 19 | } 20 | } 21 | 22 | impl From for EncodedSrgb { 23 | fn from(c: Color) -> Self { 24 | let Color { r, g, b, a: _ } = c; 25 | let (r, g, b) = (r as f64, g as f64, b as f64); 26 | EncodedSrgb { r, g, b } 27 | } 28 | } 29 | 30 | impl From> for Color { 31 | fn from(c: EncodedSrgb) -> Self { 32 | let EncodedSrgb { r, g, b } = c; 33 | let (r, g, b) = (r as f32, g as f32, b as f32); 34 | Self::new(r, g, b, 1.0) 35 | } 36 | } 37 | 38 | impl From for Alpha> { 39 | fn from(c: Color) -> Self { 40 | let Color { r, g, b, a } = c; 41 | Alpha { 42 | color: EncodedSrgb { r, g, b }, 43 | alpha: a, 44 | } 45 | } 46 | } 47 | 48 | impl From>> for Color { 49 | fn from(c: Alpha>) -> Self { 50 | let Alpha { 51 | color: EncodedSrgb { r, g, b }, 52 | alpha, 53 | } = c; 54 | Self::new(r, g, b, alpha) 55 | } 56 | } 57 | 58 | impl From for Alpha> { 59 | fn from(c: Color) -> Self { 60 | let Color { r, g, b, a } = c; 61 | let (r, g, b, alpha) = (r as f64, g as f64, b as f64, a as f64); 62 | Alpha { 63 | color: EncodedSrgb { r, g, b }, 64 | alpha, 65 | } 66 | } 67 | } 68 | 69 | impl From>> for Color { 70 | fn from(c: Alpha>) -> Self { 71 | let Alpha { 72 | color: EncodedSrgb { r, g, b }, 73 | alpha, 74 | } = c; 75 | let (r, g, b, alpha) = (r as f32, g as f32, b as f32, alpha as f32); 76 | Self::new(r, g, b, alpha) 77 | } 78 | } 79 | 80 | impl From for EncodedSrgb { 81 | fn from(c: Color) -> Self { 82 | let [r, g, b, _] = c.to_rgba8(); 83 | EncodedSrgb { r, g, b } 84 | } 85 | } 86 | 87 | impl From> for Color { 88 | fn from(c: EncodedSrgb) -> Self { 89 | let EncodedSrgb { r, g, b } = c; 90 | Self::from_rgba8(r, g, b, 255) 91 | } 92 | } 93 | 94 | impl From for Alpha> { 95 | fn from(c: Color) -> Self { 96 | let [r, g, b, alpha] = c.to_rgba8(); 97 | Alpha { 98 | color: EncodedSrgb { r, g, b }, 99 | alpha, 100 | } 101 | } 102 | } 103 | 104 | impl From>> for Color { 105 | fn from(c: Alpha>) -> Self { 106 | let Alpha { 107 | color: EncodedSrgb { r, g, b }, 108 | alpha, 109 | } = c; 110 | Self::from_rgba8(r, g, b, alpha) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/color.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use std::fmt; 3 | use std::str::FromStr; 4 | 5 | #[cfg(feature = "rust-rgb")] 6 | use rgb::{RGB, RGBA}; 7 | 8 | #[cfg(feature = "serde")] 9 | use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; 10 | 11 | #[cfg(feature = "lab")] 12 | use crate::lab::{lab_to_linear_rgb, linear_rgb_to_lab}; 13 | 14 | use crate::utils::*; 15 | use crate::{parse, ParseColorError}; 16 | 17 | #[cfg(feature = "named-colors")] 18 | use crate::NAMED_COLORS; 19 | 20 | #[derive(Debug, Clone, PartialEq, PartialOrd)] 21 | /// The color 22 | pub struct Color { 23 | /// Red 24 | pub r: f32, 25 | /// Green 26 | pub g: f32, 27 | /// Blue 28 | pub b: f32, 29 | /// Alpha 30 | pub a: f32, 31 | } 32 | 33 | impl Color { 34 | /// Arguments: 35 | /// 36 | /// * `r`: Red value [0..1] 37 | /// * `g`: Green value [0..1] 38 | /// * `b`: Blue value [0..1] 39 | /// * `a`: Alpha value [0..1] 40 | pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self { 41 | Self { r, g, b, a } 42 | } 43 | 44 | /// Arguments: 45 | /// 46 | /// * `r`: Red value [0..255] 47 | /// * `g`: Green value [0..255] 48 | /// * `b`: Blue value [0..255] 49 | /// * `a`: Alpha value [0..255] 50 | pub const fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self { 51 | Self { 52 | r: r as f32 / 255.0, 53 | g: g as f32 / 255.0, 54 | b: b as f32 / 255.0, 55 | a: a as f32 / 255.0, 56 | } 57 | } 58 | 59 | /// Arguments: 60 | /// 61 | /// * `r`: Red value [0..1] 62 | /// * `g`: Green value [0..1] 63 | /// * `b`: Blue value [0..1] 64 | /// * `a`: Alpha value [0..1] 65 | pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self { 66 | fn from_linear(x: f32) -> f32 { 67 | if x >= 0.0031308 { 68 | return 1.055 * x.powf(1.0 / 2.4) - 0.055; 69 | } 70 | 12.92 * x 71 | } 72 | Self::new(from_linear(r), from_linear(g), from_linear(b), a) 73 | } 74 | 75 | /// Arguments: 76 | /// 77 | /// * `r`: Red value [0..255] 78 | /// * `g`: Green value [0..255] 79 | /// * `b`: Blue value [0..255] 80 | /// * `a`: Alpha value [0..255] 81 | pub fn from_linear_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self { 82 | Self::from_linear_rgba( 83 | r as f32 / 255.0, 84 | g as f32 / 255.0, 85 | b as f32 / 255.0, 86 | a as f32 / 255.0, 87 | ) 88 | } 89 | 90 | /// Arguments: 91 | /// 92 | /// * `h`: Hue angle [0..360] 93 | /// * `s`: Saturation [0..1] 94 | /// * `v`: Value [0..1] 95 | /// * `a`: Alpha [0..1] 96 | pub const fn from_hsva(h: f32, s: f32, v: f32, a: f32) -> Self { 97 | let [r, g, b] = hsv_to_rgb(normalize_angle(h), s.clamp(0.0, 1.0), v.clamp(0.0, 1.0)); 98 | Self::new(r, g, b, a) 99 | } 100 | 101 | /// Arguments: 102 | /// 103 | /// * `h`: Hue angle [0..360] 104 | /// * `s`: Saturation [0..1] 105 | /// * `l`: Lightness [0..1] 106 | /// * `a`: Alpha [0..1] 107 | pub const fn from_hsla(h: f32, s: f32, l: f32, a: f32) -> Self { 108 | let [r, g, b] = hsl_to_rgb(normalize_angle(h), s.clamp(0.0, 1.0), l.clamp(0.0, 1.0)); 109 | Self::new(r, g, b, a) 110 | } 111 | 112 | /// Arguments: 113 | /// 114 | /// * `h`: Hue angle [0..360] 115 | /// * `w`: Whiteness [0..1] 116 | /// * `b`: Blackness [0..1] 117 | /// * `a`: Alpha [0..1] 118 | pub const fn from_hwba(h: f32, w: f32, b: f32, a: f32) -> Self { 119 | let [r, g, b] = hwb_to_rgb(normalize_angle(h), w.clamp(0.0, 1.0), b.clamp(0.0, 1.0)); 120 | Self::new(r, g, b, a) 121 | } 122 | 123 | /// Arguments: 124 | /// 125 | /// * `l`: Perceived lightness 126 | /// * `a`: How green/red the color is 127 | /// * `b`: How blue/yellow the color is 128 | /// * `alpha`: Alpha [0..1] 129 | pub fn from_oklaba(l: f32, a: f32, b: f32, alpha: f32) -> Self { 130 | let [r, g, b] = oklab_to_linear_rgb(l, a, b); 131 | Self::from_linear_rgba(r, g, b, alpha) 132 | } 133 | 134 | /// Arguments: 135 | /// 136 | /// * `l`: Perceived lightness 137 | /// * `c`: Chroma 138 | /// * `h`: Hue angle in radians 139 | /// * `alpha`: Alpha [0..1] 140 | pub fn from_oklcha(l: f32, c: f32, h: f32, alpha: f32) -> Self { 141 | Self::from_oklaba(l, c * h.cos(), c * h.sin(), alpha) 142 | } 143 | 144 | #[cfg(feature = "lab")] 145 | /// Arguments: 146 | /// 147 | /// * `l`: Lightness 148 | /// * `a`: Distance along the `a` axis 149 | /// * `b`: Distance along the `b` axis 150 | /// * `alpha`: Alpha [0..1] 151 | pub fn from_laba(l: f32, a: f32, b: f32, alpha: f32) -> Self { 152 | let [r, g, b] = lab_to_linear_rgb(l, a, b); 153 | Self::from_linear_rgba(r, g, b, alpha) 154 | } 155 | 156 | #[cfg(feature = "lab")] 157 | /// Arguments: 158 | /// 159 | /// * `l`: Lightness 160 | /// * `c`: Chroma 161 | /// * `h`: Hue angle in radians 162 | /// * `alpha`: Alpha [0..1] 163 | pub fn from_lcha(l: f32, c: f32, h: f32, alpha: f32) -> Self { 164 | Self::from_laba(l, c * h.cos(), c * h.sin(), alpha) 165 | } 166 | 167 | /// Create color from CSS color string. 168 | /// 169 | /// # Examples 170 | /// ``` 171 | /// use csscolorparser::Color; 172 | /// # use std::error::Error; 173 | /// # fn main() -> Result<(), Box> { 174 | /// 175 | /// let c = Color::from_html("rgb(255,0,0)")?; 176 | /// 177 | /// assert_eq!(c.to_array(), [1.0, 0.0, 0.0, 1.0]); 178 | /// assert_eq!(c.to_rgba8(), [255, 0, 0, 255]); 179 | /// assert_eq!(c.to_css_hex(), "#ff0000"); 180 | /// assert_eq!(c.to_css_rgb(), "rgb(255 0 0)"); 181 | /// # Ok(()) 182 | /// # } 183 | /// ``` 184 | pub fn from_html>(s: S) -> Result { 185 | parse(s.as_ref()) 186 | } 187 | 188 | /// Restricts R, G, B, A values to the range [0..1]. 189 | #[must_use = "method returns a new Color and does not mutate the original Color"] 190 | pub const fn clamp(&self) -> Self { 191 | Self { 192 | r: self.r.clamp(0.0, 1.0), 193 | g: self.g.clamp(0.0, 1.0), 194 | b: self.b.clamp(0.0, 1.0), 195 | a: self.a.clamp(0.0, 1.0), 196 | } 197 | } 198 | 199 | /// Returns name if there is a name for this color. 200 | /// 201 | /// **Note:** It ignores transparency (alpha value). 202 | /// 203 | /// ``` 204 | /// use csscolorparser::Color; 205 | /// 206 | /// assert_eq!(Color::from_rgba8(255, 0, 0, 255).name(), Some("red")); 207 | /// assert_eq!(Color::from_rgba8(238, 130, 238, 255).name(), Some("violet")); 208 | /// assert_eq!(Color::from_rgba8(90, 150, 200, 255).name(), None); 209 | /// ``` 210 | #[cfg(feature = "named-colors")] 211 | pub fn name(&self) -> Option<&'static str> { 212 | let rgb = &self.to_rgba8()[0..3]; 213 | for (&k, &v) in NAMED_COLORS.entries() { 214 | if v == rgb { 215 | return Some(k); 216 | } 217 | } 218 | None 219 | } 220 | 221 | /// Returns: `[r, g, b, a]` 222 | /// 223 | /// * Red, green, blue and alpha in the range [0..1] 224 | pub const fn to_array(&self) -> [f32; 4] { 225 | [ 226 | self.r.clamp(0.0, 1.0), 227 | self.g.clamp(0.0, 1.0), 228 | self.b.clamp(0.0, 1.0), 229 | self.a.clamp(0.0, 1.0), 230 | ] 231 | } 232 | 233 | /// Returns: `[r, g, b, a]` 234 | /// 235 | /// * Red, green, blue and alpha in the range [0..255] 236 | pub const fn to_rgba8(&self) -> [u8; 4] { 237 | [ 238 | (self.r * 255.0 + 0.5) as u8, 239 | (self.g * 255.0 + 0.5) as u8, 240 | (self.b * 255.0 + 0.5) as u8, 241 | (self.a * 255.0 + 0.5) as u8, 242 | ] 243 | } 244 | 245 | /// Returns: `[r, g, b, a]` 246 | /// 247 | /// * Red, green, blue and alpha in the range [0..65535] 248 | pub const fn to_rgba16(&self) -> [u16; 4] { 249 | [ 250 | (self.r * 65535.0 + 0.5) as u16, 251 | (self.g * 65535.0 + 0.5) as u16, 252 | (self.b * 65535.0 + 0.5) as u16, 253 | (self.a * 65535.0 + 0.5) as u16, 254 | ] 255 | } 256 | 257 | /// Returns: `[h, s, v, a]` 258 | /// 259 | /// * `h`: Hue angle [0..360] 260 | /// * `s`: Saturation [0..1] 261 | /// * `v`: Value [0..1] 262 | /// * `a`: Alpha [0..1] 263 | pub const fn to_hsva(&self) -> [f32; 4] { 264 | let [h, s, v] = rgb_to_hsv( 265 | self.r.clamp(0.0, 1.0), 266 | self.g.clamp(0.0, 1.0), 267 | self.b.clamp(0.0, 1.0), 268 | ); 269 | [ 270 | h, 271 | s.clamp(0.0, 1.0), 272 | v.clamp(0.0, 1.0), 273 | self.a.clamp(0.0, 1.0), 274 | ] 275 | } 276 | 277 | /// Returns: `[h, s, l, a]` 278 | /// 279 | /// * `h`: Hue angle [0..360] 280 | /// * `s`: Saturation [0..1] 281 | /// * `l`: Lightness [0..1] 282 | /// * `a`: Alpha [0..1] 283 | pub const fn to_hsla(&self) -> [f32; 4] { 284 | let [h, s, l] = rgb_to_hsl( 285 | self.r.clamp(0.0, 1.0), 286 | self.g.clamp(0.0, 1.0), 287 | self.b.clamp(0.0, 1.0), 288 | ); 289 | [ 290 | h, 291 | s.clamp(0.0, 1.0), 292 | l.clamp(0.0, 1.0), 293 | self.a.clamp(0.0, 1.0), 294 | ] 295 | } 296 | 297 | /// Returns: `[h, w, b, a]` 298 | /// 299 | /// * `h`: Hue angle [0..360] 300 | /// * `w`: Whiteness [0..1] 301 | /// * `b`: Blackness [0..1] 302 | /// * `a`: Alpha [0..1] 303 | pub const fn to_hwba(&self) -> [f32; 4] { 304 | let [h, w, b] = rgb_to_hwb( 305 | self.r.clamp(0.0, 1.0), 306 | self.g.clamp(0.0, 1.0), 307 | self.b.clamp(0.0, 1.0), 308 | ); 309 | [ 310 | h, 311 | w.clamp(0.0, 1.0), 312 | b.clamp(0.0, 1.0), 313 | self.a.clamp(0.0, 1.0), 314 | ] 315 | } 316 | 317 | /// Returns: `[r, g, b, a]` 318 | /// 319 | /// * Red, green, blue and alpha in the range [0..1] 320 | pub fn to_linear_rgba(&self) -> [f32; 4] { 321 | fn to_linear(x: f32) -> f32 { 322 | if x >= 0.04045 { 323 | return ((x + 0.055) / 1.055).powf(2.4); 324 | } 325 | x / 12.92 326 | } 327 | [ 328 | to_linear(self.r), 329 | to_linear(self.g), 330 | to_linear(self.b), 331 | self.a, 332 | ] 333 | } 334 | 335 | /// Returns: `[r, g, b, a]` 336 | /// 337 | /// * Red, green, blue and alpha in the range [0..255] 338 | pub fn to_linear_rgba_u8(&self) -> [u8; 4] { 339 | let [r, g, b, a] = self.to_linear_rgba(); 340 | [ 341 | (r * 255.0).round() as u8, 342 | (g * 255.0).round() as u8, 343 | (b * 255.0).round() as u8, 344 | (a * 255.0).round() as u8, 345 | ] 346 | } 347 | 348 | /// Returns: `[l, a, b, alpha]` 349 | pub fn to_oklaba(&self) -> [f32; 4] { 350 | let [r, g, b, _] = self.to_linear_rgba(); 351 | let [l, a, b] = linear_rgb_to_oklab(r, g, b); 352 | [l, a, b, self.a.clamp(0.0, 1.0)] 353 | } 354 | 355 | /// Returns: `[l, c, h, alpha]` 356 | pub fn to_oklcha(&self) -> [f32; 4] { 357 | let [l, a, b, alpha] = self.to_oklaba(); 358 | let c = (a * a + b * b).sqrt(); 359 | let h = b.atan2(a); 360 | [l, c, h, alpha] 361 | } 362 | 363 | #[cfg(feature = "lab")] 364 | /// Returns: `[l, a, b, alpha]` 365 | pub fn to_laba(&self) -> [f32; 4] { 366 | let [r, g, b, alpha] = self.to_linear_rgba(); 367 | let [l, a, b] = linear_rgb_to_lab(r, g, b); 368 | [l, a, b, alpha.clamp(0.0, 1.0)] 369 | } 370 | 371 | #[cfg(feature = "lab")] 372 | /// Returns: `[l, c, h, alpha]` 373 | pub fn to_lcha(&self) -> [f32; 4] { 374 | let [l, a, b, alpha] = self.to_laba(); 375 | let c = (a * a + b * b).sqrt(); 376 | let h = b.atan2(a); 377 | [l, c, h, alpha.clamp(0.0, 1.0)] 378 | } 379 | 380 | /// Get CSS RGB hexadecimal color representation 381 | pub fn to_css_hex(&self) -> String { 382 | let [r, g, b, a] = self.to_rgba8(); 383 | if a < 255 { 384 | format!("#{r:02x}{g:02x}{b:02x}{a:02x}") 385 | } else { 386 | format!("#{r:02x}{g:02x}{b:02x}") 387 | } 388 | } 389 | 390 | /// Get CSS `rgb()` color representation 391 | pub fn to_css_rgb(&self) -> String { 392 | let [r, g, b, _] = self.to_rgba8(); 393 | format!("rgb({r} {g} {b}{})", fmt_alpha(self.a)) 394 | } 395 | 396 | /// Get CSS `hsl()` color representation 397 | pub fn to_css_hsl(&self) -> String { 398 | let [h, s, l, alpha] = self.to_hsla(); 399 | let h = if h.is_nan() { 400 | "none".into() 401 | } else { 402 | fmt_float(h, 2) 403 | }; 404 | let s = (s * 100.0 + 0.5).floor(); 405 | let l = (l * 100.0 + 0.5).floor(); 406 | format!("hsl({h} {s}% {l}%{})", fmt_alpha(alpha)) 407 | } 408 | 409 | /// Get CSS `hwb()` color representation 410 | pub fn to_css_hwb(&self) -> String { 411 | let [h, w, b, alpha] = self.to_hwba(); 412 | let h = if h.is_nan() { 413 | "none".into() 414 | } else { 415 | fmt_float(h, 2) 416 | }; 417 | let w = (w * 100.0 + 0.5).floor(); 418 | let b = (b * 100.0 + 0.5).floor(); 419 | format!("hwb({h} {w}% {b}%{})", fmt_alpha(alpha)) 420 | } 421 | 422 | /// Get CSS `oklab()` color representation 423 | pub fn to_css_oklab(&self) -> String { 424 | let [l, a, b, alpha] = self.to_oklaba(); 425 | let l = fmt_float(l, 3); 426 | let a = fmt_float(a, 3); 427 | let b = fmt_float(b, 3); 428 | format!("oklab({l} {a} {b}{})", fmt_alpha(alpha)) 429 | } 430 | 431 | /// Get CSS `oklch()` color representation 432 | pub fn to_css_oklch(&self) -> String { 433 | let [l, c, h, alpha] = self.to_oklcha(); 434 | let l = fmt_float(l, 3); 435 | let c = fmt_float(c, 3); 436 | let h = fmt_float(normalize_angle(h.to_degrees()), 2); 437 | format!("oklch({l} {c} {h}{})", fmt_alpha(alpha)) 438 | } 439 | 440 | #[cfg(feature = "lab")] 441 | /// Get CSS `lab()` color representation 442 | pub fn to_css_lab(&self) -> String { 443 | let [l, a, b, alpha] = self.to_laba(); 444 | let l = fmt_float(l, 2); 445 | let a = fmt_float(a, 2); 446 | let b = fmt_float(b, 2); 447 | format!("lab({l} {a} {b}{})", fmt_alpha(alpha)) 448 | } 449 | 450 | #[cfg(feature = "lab")] 451 | /// Get CSS `lch()` color representation 452 | pub fn to_css_lch(&self) -> String { 453 | use std::f32::consts::PI; 454 | 455 | fn to_degrees(t: f32) -> f32 { 456 | if t > 0.0 { 457 | t / PI * 180.0 458 | } else { 459 | 360.0 - (t.abs() / PI) * 180.0 460 | } 461 | } 462 | 463 | let [l, c, h, alpha] = self.to_lcha(); 464 | let l = fmt_float(l, 2); 465 | let c = fmt_float(c, 2); 466 | let h = fmt_float(to_degrees(h), 2); 467 | format!("lch({l} {c} {h}{})", fmt_alpha(alpha)) 468 | } 469 | 470 | /// Blend this color with the other one, in the RGB color-space. `t` in the range [0..1]. 471 | pub const fn interpolate_rgb(&self, other: &Color, t: f32) -> Self { 472 | Self { 473 | r: self.r + t * (other.r - self.r), 474 | g: self.g + t * (other.g - self.g), 475 | b: self.b + t * (other.b - self.b), 476 | a: self.a + t * (other.a - self.a), 477 | } 478 | } 479 | 480 | /// Blend this color with the other one, in the linear RGB color-space. `t` in the range [0..1]. 481 | pub fn interpolate_linear_rgb(&self, other: &Color, t: f32) -> Self { 482 | let [r1, g1, b1, a1] = self.to_linear_rgba(); 483 | let [r2, g2, b2, a2] = other.to_linear_rgba(); 484 | Self::from_linear_rgba( 485 | r1 + t * (r2 - r1), 486 | g1 + t * (g2 - g1), 487 | b1 + t * (b2 - b1), 488 | a1 + t * (a2 - a1), 489 | ) 490 | } 491 | 492 | /// Blend this color with the other one, in the HSV color-space. `t` in the range [0..1]. 493 | pub const fn interpolate_hsv(&self, other: &Color, t: f32) -> Self { 494 | let [h1, s1, v1, a1] = self.to_hsva(); 495 | let [h2, s2, v2, a2] = other.to_hsva(); 496 | Self::from_hsva( 497 | interp_angle(h1, h2, t), 498 | s1 + t * (s2 - s1), 499 | v1 + t * (v2 - v1), 500 | a1 + t * (a2 - a1), 501 | ) 502 | } 503 | 504 | /// Blend this color with the other one, in the [Oklab](https://bottosson.github.io/posts/oklab/) color-space. `t` in the range [0..1]. 505 | pub fn interpolate_oklab(&self, other: &Color, t: f32) -> Self { 506 | let [l1, a1, b1, alpha1] = self.to_oklaba(); 507 | let [l2, a2, b2, alpha2] = other.to_oklaba(); 508 | Self::from_oklaba( 509 | l1 + t * (l2 - l1), 510 | a1 + t * (a2 - a1), 511 | b1 + t * (b2 - b1), 512 | alpha1 + t * (alpha2 - alpha1), 513 | ) 514 | } 515 | 516 | #[cfg(feature = "lab")] 517 | /// Blend this color with the other one, in the Lab color-space. `t` in the range [0..1]. 518 | pub fn interpolate_lab(&self, other: &Color, t: f32) -> Self { 519 | let [l1, a1, b1, alpha1] = self.to_laba(); 520 | let [l2, a2, b2, alpha2] = other.to_laba(); 521 | Self::from_laba( 522 | l1 + t * (l2 - l1), 523 | a1 + t * (a2 - a1), 524 | b1 + t * (b2 - b1), 525 | alpha1 + t * (alpha2 - alpha1), 526 | ) 527 | } 528 | 529 | #[cfg(feature = "lab")] 530 | /// Blend this color with the other one, in the LCH color-space. `t` in the range [0..1]. 531 | pub fn interpolate_lch(&self, other: &Color, t: f32) -> Self { 532 | let [l1, c1, h1, alpha1] = self.to_lcha(); 533 | let [l2, c2, h2, alpha2] = other.to_lcha(); 534 | Self::from_lcha( 535 | l1 + t * (l2 - l1), 536 | c1 + t * (c2 - c1), 537 | interp_angle_rad(h1, h2, t), 538 | alpha1 + t * (alpha2 - alpha1), 539 | ) 540 | } 541 | } 542 | 543 | impl Default for Color { 544 | fn default() -> Self { 545 | Self { 546 | r: 0.0, 547 | g: 0.0, 548 | b: 0.0, 549 | a: 1.0, 550 | } 551 | } 552 | } 553 | 554 | impl fmt::Display for Color { 555 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 556 | write!(f, "RGBA({},{},{},{})", self.r, self.g, self.b, self.a) 557 | } 558 | } 559 | 560 | impl FromStr for Color { 561 | type Err = ParseColorError; 562 | 563 | fn from_str(s: &str) -> Result { 564 | parse(s) 565 | } 566 | } 567 | 568 | impl TryFrom<&str> for Color { 569 | type Error = ParseColorError; 570 | 571 | fn try_from(s: &str) -> Result { 572 | parse(s) 573 | } 574 | } 575 | 576 | impl TryFrom for Color { 577 | type Error = ParseColorError; 578 | 579 | fn try_from(s: String) -> Result { 580 | parse(s.as_ref()) 581 | } 582 | } 583 | 584 | impl From<(f32, f32, f32, f32)> for Color { 585 | fn from((r, g, b, a): (f32, f32, f32, f32)) -> Self { 586 | Self { r, g, b, a } 587 | } 588 | } 589 | 590 | impl From<(f32, f32, f32)> for Color { 591 | fn from((r, g, b): (f32, f32, f32)) -> Self { 592 | Self { r, g, b, a: 1.0 } 593 | } 594 | } 595 | 596 | impl From<[f32; 4]> for Color { 597 | fn from([r, g, b, a]: [f32; 4]) -> Self { 598 | Self { r, g, b, a } 599 | } 600 | } 601 | 602 | impl From<[f32; 3]> for Color { 603 | fn from([r, g, b]: [f32; 3]) -> Self { 604 | Self { r, g, b, a: 1.0 } 605 | } 606 | } 607 | 608 | impl From<[f64; 4]> for Color { 609 | fn from([r, g, b, a]: [f64; 4]) -> Self { 610 | Self { 611 | r: r as f32, 612 | g: g as f32, 613 | b: b as f32, 614 | a: a as f32, 615 | } 616 | } 617 | } 618 | 619 | impl From<[f64; 3]> for Color { 620 | fn from([r, g, b]: [f64; 3]) -> Self { 621 | Self { 622 | r: r as f32, 623 | g: g as f32, 624 | b: b as f32, 625 | a: 1.0, 626 | } 627 | } 628 | } 629 | 630 | impl From<(u8, u8, u8, u8)> for Color { 631 | fn from((r, g, b, a): (u8, u8, u8, u8)) -> Self { 632 | Self::from_rgba8(r, g, b, a) 633 | } 634 | } 635 | 636 | impl From<(u8, u8, u8)> for Color { 637 | fn from((r, g, b): (u8, u8, u8)) -> Self { 638 | Self::from_rgba8(r, g, b, 255) 639 | } 640 | } 641 | 642 | impl From<[u8; 4]> for Color { 643 | fn from([r, g, b, a]: [u8; 4]) -> Self { 644 | Self::from_rgba8(r, g, b, a) 645 | } 646 | } 647 | 648 | impl From<[u8; 3]> for Color { 649 | fn from([r, g, b]: [u8; 3]) -> Self { 650 | Self::from_rgba8(r, g, b, 255) 651 | } 652 | } 653 | 654 | /// Convert rust-rgb's `RGB` type into `Color`. 655 | #[cfg(feature = "rust-rgb")] 656 | impl From> for Color { 657 | fn from(item: RGB) -> Self { 658 | Self::new(item.r, item.g, item.b, 1.0) 659 | } 660 | } 661 | 662 | /// Convert rust-rgb's `RGBA` type into `Color`. 663 | #[cfg(feature = "rust-rgb")] 664 | impl From> for Color { 665 | fn from(item: RGBA) -> Self { 666 | Self::new(item.r, item.g, item.b, item.a) 667 | } 668 | } 669 | 670 | /// Implement Serde serialization into HEX string 671 | #[cfg(feature = "serde")] 672 | impl Serialize for Color { 673 | fn serialize(&self, serializer: S) -> Result { 674 | serializer.serialize_str(&self.to_css_hex()) 675 | } 676 | } 677 | 678 | /// Implement Serde deserialization from string 679 | #[cfg(feature = "serde")] 680 | impl<'de> Deserialize<'de> for Color { 681 | fn deserialize>(deserializer: D) -> Result { 682 | deserializer.deserialize_str(ColorVisitor) 683 | } 684 | } 685 | 686 | #[cfg(feature = "serde")] 687 | struct ColorVisitor; 688 | 689 | #[cfg(feature = "serde")] 690 | impl Visitor<'_> for ColorVisitor { 691 | type Value = Color; 692 | 693 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 694 | f.write_str("a valid css color") 695 | } 696 | 697 | fn visit_str(self, v: &str) -> Result 698 | where 699 | E: serde::de::Error, 700 | { 701 | Color::from_str(v).map_err(serde::de::Error::custom) 702 | } 703 | } 704 | 705 | fn fmt_float(t: f32, precision: usize) -> String { 706 | let s = format!("{t:.precision$}"); 707 | s.trim_end_matches('0').trim_end_matches('.').to_string() 708 | } 709 | 710 | fn fmt_alpha(alpha: f32) -> String { 711 | if alpha < 1.0 { 712 | format!(" / {}%", (alpha.max(0.0) * 100.0 + 0.5).floor()) 713 | } else { 714 | "".into() 715 | } 716 | } 717 | 718 | #[cfg(test)] 719 | mod tests { 720 | #[cfg(any(feature = "serde", feature = "rust-rgb"))] 721 | use super::*; 722 | 723 | #[cfg(feature = "rust-rgb")] 724 | #[test] 725 | fn test_convert_rust_rgb_to_color() { 726 | let rgb = RGB::new(0.0, 0.5, 1.0); 727 | assert_eq!(Color::new(0.0, 0.5, 1.0, 1.0), Color::from(rgb)); 728 | 729 | let rgba = RGBA::new(1.0, 0.5, 0.0, 0.5); 730 | assert_eq!(Color::new(1.0, 0.5, 0.0, 0.5), Color::from(rgba)); 731 | } 732 | 733 | #[cfg(feature = "serde")] 734 | #[test] 735 | fn test_serde_serialize_to_hex() { 736 | let color = Color::new(1.0, 1.0, 0.5, 0.5); 737 | serde_test::assert_ser_tokens(&color, &[serde_test::Token::Str("#ffff8080")]); 738 | } 739 | 740 | #[cfg(all(feature = "serde", feature = "named-colors"))] 741 | #[test] 742 | fn test_serde_deserialize_from_string() { 743 | let named = Color::new(1.0, 1.0, 0.0, 1.0); 744 | serde_test::assert_de_tokens(&named, &[serde_test::Token::Str("yellow")]); 745 | 746 | let hex = Color::new(0.0, 1.0, 0.0, 1.0); 747 | serde_test::assert_de_tokens(&hex, &[serde_test::Token::Str("#00ff00ff")]); 748 | 749 | let rgb = Color::new(0.0, 1.0, 0.0, 1.0); 750 | serde_test::assert_de_tokens(&rgb, &[serde_test::Token::Str("rgba(0,255,0,1)")]); 751 | } 752 | } 753 | -------------------------------------------------------------------------------- /src/color2.rs: -------------------------------------------------------------------------------- 1 | // Color deprecated methods 2 | 3 | use crate::Color; 4 | 5 | impl Color { 6 | #[deprecated = "Use [new](#method.new) instead."] 7 | /// Arguments: 8 | /// 9 | /// * `r`: Red value [0..1] 10 | /// * `g`: Green value [0..1] 11 | /// * `b`: Blue value [0..1] 12 | pub fn from_rgb(r: f32, g: f32, b: f32) -> Self { 13 | Self { r, g, b, a: 1.0 } 14 | } 15 | 16 | #[deprecated = "Use [new](#method.new) instead."] 17 | /// Arguments: 18 | /// 19 | /// * `r`: Red value [0..1] 20 | /// * `g`: Green value [0..1] 21 | /// * `b`: Blue value [0..1] 22 | /// * `a`: Alpha value [0..1] 23 | pub fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Self { 24 | Self { r, g, b, a } 25 | } 26 | 27 | #[deprecated = "Use [from_rgba8](#method.from_rgba8) instead."] 28 | /// Arguments: 29 | /// 30 | /// * `r`: Red value [0..255] 31 | /// * `g`: Green value [0..255] 32 | /// * `b`: Blue value [0..255] 33 | pub fn from_rgb_u8(r: u8, g: u8, b: u8) -> Self { 34 | Self { 35 | r: r as f32 / 255.0, 36 | g: g as f32 / 255.0, 37 | b: b as f32 / 255.0, 38 | a: 1.0, 39 | } 40 | } 41 | 42 | #[deprecated = "Use [from_rgba8](#method.from_rgba8) instead."] 43 | /// Arguments: 44 | /// 45 | /// * `r`: Red value [0..255] 46 | /// * `g`: Green value [0..255] 47 | /// * `b`: Blue value [0..255] 48 | /// * `a`: Alpha value [0..255] 49 | pub fn from_rgba_u8(r: u8, g: u8, b: u8, a: u8) -> Self { 50 | Self { 51 | r: r as f32 / 255.0, 52 | g: g as f32 / 255.0, 53 | b: b as f32 / 255.0, 54 | a: a as f32 / 255.0, 55 | } 56 | } 57 | 58 | #[deprecated = "Use [from_linear_rgba](#method.from_linear_rgba) instead."] 59 | /// Arguments: 60 | /// 61 | /// * `r`: Red value [0..1] 62 | /// * `g`: Green value [0..1] 63 | /// * `b`: Blue value [0..1] 64 | pub fn from_linear_rgb(r: f32, g: f32, b: f32) -> Self { 65 | Self::from_linear_rgba(r, g, b, 1.0) 66 | } 67 | 68 | #[deprecated = "Use [from_linear_rgba8](#method.from_linear_rgba8) instead."] 69 | /// Arguments: 70 | /// 71 | /// * `r`: Red value [0..255] 72 | /// * `g`: Green value [0..255] 73 | /// * `b`: Blue value [0..255] 74 | pub fn from_linear_rgb_u8(r: u8, g: u8, b: u8) -> Self { 75 | Self::from_linear_rgba(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0) 76 | } 77 | 78 | #[deprecated = "Use [from_linear_rgba8](#method.from_linear_rgba8) instead."] 79 | /// Arguments: 80 | /// 81 | /// * `r`: Red value [0..255] 82 | /// * `g`: Green value [0..255] 83 | /// * `b`: Blue value [0..255] 84 | /// * `a`: Alpha value [0..255] 85 | pub fn from_linear_rgba_u8(r: u8, g: u8, b: u8, a: u8) -> Self { 86 | Self::from_linear_rgba( 87 | r as f32 / 255.0, 88 | g as f32 / 255.0, 89 | b as f32 / 255.0, 90 | a as f32 / 255.0, 91 | ) 92 | } 93 | 94 | #[deprecated = "Use [from_hsva](#method.from_hsva) instead."] 95 | /// Arguments: 96 | /// 97 | /// * `h`: Hue angle [0..360] 98 | /// * `s`: Saturation [0..1] 99 | /// * `v`: Value [0..1] 100 | pub fn from_hsv(h: f32, s: f32, v: f32) -> Self { 101 | Self::from_hsva(h, s, v, 1.0) 102 | } 103 | 104 | #[deprecated = "Use [from_hsla](#method.from_hsla) instead."] 105 | /// Arguments: 106 | /// 107 | /// * `h`: Hue angle [0..360] 108 | /// * `s`: Saturation [0..1] 109 | /// * `l`: Lightness [0..1] 110 | pub fn from_hsl(h: f32, s: f32, l: f32) -> Self { 111 | Self::from_hsla(h, s, l, 1.0) 112 | } 113 | 114 | #[deprecated = "Use [from_hwba](#method.from_hwba) instead."] 115 | /// Arguments: 116 | /// 117 | /// * `h`: Hue angle [0..360] 118 | /// * `w`: Whiteness [0..1] 119 | /// * `b`: Blackness [0..1] 120 | pub fn from_hwb(h: f32, w: f32, b: f32) -> Self { 121 | Self::from_hwba(h, w, b, 1.0) 122 | } 123 | 124 | #[deprecated = "Use [from_oklaba](#method.from_oklaba) instead."] 125 | /// Arguments: 126 | /// 127 | /// * `l`: Perceived lightness 128 | /// * `a`: How green/red the color is 129 | /// * `b`: How blue/yellow the color is 130 | pub fn from_oklab(l: f32, a: f32, b: f32) -> Self { 131 | Self::from_oklaba(l, a, b, 1.0) 132 | } 133 | 134 | #[cfg(feature = "lab")] 135 | #[deprecated = "Use [from_laba](#method.from_laba) instead."] 136 | /// Arguments: 137 | /// 138 | /// * `l`: Lightness 139 | /// * `a`: Distance along the `a` axis 140 | /// * `b`: Distance along the `b` axis 141 | /// * `alpha`: Alpha [0..1] 142 | pub fn from_lab(l: f32, a: f32, b: f32, alpha: f32) -> Self { 143 | Self::from_laba(l, a, b, alpha) 144 | } 145 | 146 | #[cfg(feature = "lab")] 147 | #[deprecated = "Use [to_laba](#method.to_laba) instead."] 148 | /// Returns: `[l, a, b, alpha]` 149 | pub fn to_lab(&self) -> [f32; 4] { 150 | self.to_laba() 151 | } 152 | 153 | #[cfg(feature = "lab")] 154 | #[deprecated = "Use [from_lcha](#method.from_lcha) instead."] 155 | /// Arguments: 156 | /// 157 | /// * `l`: Lightness 158 | /// * `c`: Chroma 159 | /// * `h`: Hue angle in radians 160 | /// * `alpha`: Alpha [0..1] 161 | pub fn from_lch(l: f32, c: f32, h: f32, alpha: f32) -> Self { 162 | Self::from_lcha(l, c, h, alpha) 163 | } 164 | 165 | #[cfg(feature = "lab")] 166 | #[deprecated = "Use [to_lcha](#method.to_lcha) instead."] 167 | /// Returns: `[l, c, h, alpha]` 168 | pub fn to_lch(&self) -> [f32; 4] { 169 | self.to_lcha() 170 | } 171 | 172 | #[deprecated] 173 | /// Returns: `(r, g, b, a)` 174 | /// 175 | /// * Red, green, blue and alpha in the range [0..1] 176 | pub fn rgba(&self) -> (f32, f32, f32, f32) { 177 | (self.r, self.g, self.b, self.a) 178 | } 179 | 180 | #[deprecated = "Use [to_rgba8](#method.to_rgba8) instead."] 181 | /// Returns: `(r, g, b, a)` 182 | /// 183 | /// * Red, green, blue and alpha in the range [0..255] 184 | pub fn rgba_u8(&self) -> (u8, u8, u8, u8) { 185 | ( 186 | (self.r * 255.0).round() as u8, 187 | (self.g * 255.0).round() as u8, 188 | (self.b * 255.0).round() as u8, 189 | (self.a * 255.0).round() as u8, 190 | ) 191 | } 192 | 193 | // --- Since version 0.7.2 194 | 195 | #[deprecated = "Use [to_css_hex](#method.to_css_hex) instead."] 196 | /// Get the RGB hexadecimal color string. 197 | pub fn to_hex_string(&self) -> String { 198 | self.to_css_hex() 199 | } 200 | 201 | #[deprecated = "Use [to_css_rgb](#method.to_css_rgb) instead."] 202 | /// Get the CSS `rgb()` format string. 203 | pub fn to_rgb_string(&self) -> String { 204 | self.to_css_rgb() 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt; 3 | 4 | /// An error which can be returned when parsing a CSS color string. 5 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 6 | pub enum ParseColorError { 7 | /// A CSS color string was invalid hex format. 8 | InvalidHex, 9 | /// A CSS color string was invalid rgb format. 10 | InvalidRgb, 11 | /// A CSS color string was invalid hsl format. 12 | InvalidHsl, 13 | /// A CSS color string was invalid hwb format. 14 | InvalidHwb, 15 | /// A CSS color string was invalid hsv format. 16 | InvalidHsv, 17 | /// A CSS color string was invalid lab format. 18 | #[cfg(feature = "lab")] 19 | InvalidLab, 20 | /// A CSS color string was invalid lch format. 21 | #[cfg(feature = "lab")] 22 | InvalidLch, 23 | /// A CSS color string was invalid oklab format. 24 | InvalidOklab, 25 | /// A CSS color string was invalid oklch format. 26 | InvalidOklch, 27 | /// A CSS color string was invalid color function. 28 | InvalidFunction, 29 | /// A CSS color string was invalid unknown format. 30 | InvalidUnknown, 31 | } 32 | 33 | impl fmt::Display for ParseColorError { 34 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 35 | match *self { 36 | Self::InvalidHex => f.write_str("invalid hex format"), 37 | Self::InvalidRgb => f.write_str("invalid rgb format"), 38 | Self::InvalidHsl => f.write_str("invalid hsl format"), 39 | Self::InvalidHwb => f.write_str("invalid hwb format"), 40 | Self::InvalidHsv => f.write_str("invalid hsv format"), 41 | #[cfg(feature = "lab")] 42 | Self::InvalidLab => f.write_str("invalid lab format"), 43 | #[cfg(feature = "lab")] 44 | Self::InvalidLch => f.write_str("invalid lch format"), 45 | Self::InvalidOklab => f.write_str("invalid oklab format"), 46 | Self::InvalidOklch => f.write_str("invalid oklch format"), 47 | Self::InvalidFunction => f.write_str("invalid color function"), 48 | Self::InvalidUnknown => f.write_str("invalid unknown format"), 49 | } 50 | } 51 | } 52 | 53 | impl Error for ParseColorError {} 54 | -------------------------------------------------------------------------------- /src/lab.rs: -------------------------------------------------------------------------------- 1 | // Constants for D65 white point (normalized to Y=1.0) 2 | const D65_X: f32 = 0.95047; 3 | const D65_Y: f32 = 1.0; 4 | const D65_Z: f32 = 1.08883; 5 | 6 | // Helper function for LAB to XYZ conversion 7 | fn lab_to_xyz(l: f32, a: f32, b: f32) -> [f32; 3] { 8 | let fy = (l + 16.0) / 116.0; 9 | let fx = fy + a / 500.0; 10 | let fz = fy - b / 200.0; 11 | 12 | let delta = 6.0 / 29.0; 13 | 14 | let lab_f = |t: f32| -> f32 { 15 | if t > delta { 16 | t * t * t 17 | } else { 18 | (t - 16.0 / 116.0) * 3.0 * delta * delta 19 | } 20 | }; 21 | 22 | let x = D65_X * lab_f(fx); 23 | let y = D65_Y * lab_f(fy); 24 | let z = D65_Z * lab_f(fz); 25 | [x, y, z] 26 | } 27 | 28 | #[allow(clippy::excessive_precision)] 29 | // Helper function for XYZ to linear RGB conversion 30 | fn xyz_to_linear_rgb(x: f32, y: f32, z: f32) -> [f32; 3] { 31 | // sRGB matrix (D65) 32 | let r = 3.2404542 * x - 1.5371385 * y - 0.4985314 * z; 33 | let g = -0.9692660 * x + 1.8760108 * y + 0.0415560 * z; 34 | let b = 0.0556434 * x - 0.2040259 * y + 1.0572252 * z; 35 | [r, g, b] 36 | } 37 | 38 | #[allow(clippy::excessive_precision)] 39 | // Helper function for linear RGB to XYZ conversion 40 | fn linear_rgb_to_xyz(r: f32, g: f32, b: f32) -> [f32; 3] { 41 | // Inverse sRGB matrix (D65) 42 | let x = 0.4124564 * r + 0.3575761 * g + 0.1804375 * b; 43 | let y = 0.2126729 * r + 0.7151522 * g + 0.0721750 * b; 44 | let z = 0.0193339 * r + 0.1191920 * g + 0.9503041 * b; 45 | [x, y, z] 46 | } 47 | 48 | // Helper function for XYZ to LAB conversion 49 | fn xyz_to_lab(x: f32, y: f32, z: f32) -> [f32; 3] { 50 | let delta = 6.0 / 29.0; 51 | let delta_cubed = delta * delta * delta; 52 | 53 | let lab_f = |t: f32| -> f32 { 54 | if t > delta_cubed { 55 | t.cbrt() 56 | } else { 57 | (t / (3.0 * delta * delta)) + (4.0 / 29.0) 58 | } 59 | }; 60 | 61 | let fx = lab_f(x / D65_X); 62 | let fy = lab_f(y / D65_Y); 63 | let fz = lab_f(z / D65_Z); 64 | 65 | let l = 116.0 * fy - 16.0; 66 | let a = 500.0 * (fx - fy); 67 | let b = 200.0 * (fy - fz); 68 | 69 | [l, a, b] 70 | } 71 | 72 | // Convert CIELAB (L*a*b*) to linear RGB 73 | // L: [0, 100], a: [-128, 127], b: [-128, 127] 74 | // Returns RGB in [0, 1] range 75 | pub(crate) fn lab_to_linear_rgb(l: f32, a: f32, b: f32) -> [f32; 3] { 76 | let [x, y, z] = lab_to_xyz(l, a, b); 77 | xyz_to_linear_rgb(x, y, z) 78 | } 79 | 80 | // Convert linear RGB to CIELAB (L*a*b*) 81 | // RGB components in [0, 1] range 82 | // Returns [L, a, b] with L: [0, 100], a: [-128, 127], b: [-128, 127] 83 | pub(crate) fn linear_rgb_to_lab(r: f32, g: f32, b: f32) -> [f32; 3] { 84 | let [x, y, z] = linear_rgb_to_xyz(r, g, b); 85 | xyz_to_lab(x, y, z) 86 | } 87 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Overview 2 | //! 3 | //! Rust library for parsing CSS color string as defined in the W3C's [CSS Color Module Level 4](https://www.w3.org/TR/css-color-4/). 4 | //! 5 | //! ## Supported Color Format 6 | //! 7 | //! * [Named colors](https://www.w3.org/TR/css-color-4/#named-colors) 8 | //! * RGB hexadecimal (with and without `#` prefix) 9 | //! + Short format `#rgb` 10 | //! + Short format with alpha `#rgba` 11 | //! + Long format `#rrggbb` 12 | //! + Long format with alpha `#rrggbbaa` 13 | //! * `rgb()` and `rgba()` 14 | //! * `hsl()` and `hsla()` 15 | //! * `hwb()` 16 | //! * `lab()` 17 | //! * `lch()` 18 | //! * `oklab()` 19 | //! * `oklch()` 20 | //! * `hwba()`, `hsv()`, `hsva()` - not in CSS standard. 21 | //! 22 | //! ## Usage 23 | //! 24 | //! Add this to your `Cargo.toml` 25 | //! 26 | //! ```toml 27 | //! csscolorparser = "0.7" 28 | //! ``` 29 | //! 30 | //! ## Examples 31 | //! 32 | //! Using [`csscolorparser::parse()`](fn.parse.html) function. 33 | //! 34 | //! ```rust 35 | //! # fn main() -> Result<(), Box> { 36 | //! let c = csscolorparser::parse("rgb(100%,0%,0%)")?; 37 | //! 38 | //! assert_eq!(c.to_array(), [1.0, 0.0, 0.0, 1.0]); 39 | //! assert_eq!(c.to_rgba8(), [255, 0, 0, 255]); 40 | //! assert_eq!(c.to_css_hex(), "#ff0000"); 41 | //! assert_eq!(c.to_css_rgb(), "rgb(255 0 0)"); 42 | //! # Ok(()) 43 | //! # } 44 | //! ``` 45 | //! 46 | //! Using `parse()` method on `&str`. 47 | //! 48 | //! ```rust 49 | //! use csscolorparser::Color; 50 | //! # fn main() -> Result<(), Box> { 51 | //! 52 | //! let c: Color = "#ff00007f".parse()?; 53 | //! 54 | //! assert_eq!(c.to_rgba8(), [255, 0, 0, 127]); 55 | //! assert_eq!(c.to_css_hex(), "#ff00007f"); 56 | //! # Ok(()) 57 | //! # } 58 | //! ``` 59 | //! 60 | //! ## Default Feature 61 | //! 62 | //! * `named-colors`: Enables parsing from [named colors](https://www.w3.org/TR/css-color-4/#named-colors). Requires [`phf`](https://crates.io/crates/phf). 63 | //! 64 | //! ## Optional Features 65 | //! 66 | //! * `lab`: Enables parsing `lab()` and `lch()` color format. 67 | //! * `rust-rgb`: Enables converting from [`rgb`](https://crates.io/crates/rgb) crate types into `Color`. 68 | //! * `cint`: Enables converting [`cint`](https://crates.io/crates/cint) crate types to and from `Color`. 69 | //! * `serde`: Enables serializing (into HEX string) and deserializing (from any supported string color format) using [`serde`](https://serde.rs/) framework. 70 | 71 | #![forbid(unsafe_code)] 72 | #![warn(missing_docs)] 73 | 74 | mod color; 75 | mod color2; 76 | pub use color::Color; 77 | 78 | mod error; 79 | pub use error::ParseColorError; 80 | 81 | mod parser; 82 | pub use parser::parse; 83 | 84 | #[cfg(feature = "named-colors")] 85 | mod named_colors; 86 | #[cfg(feature = "named-colors")] 87 | pub use named_colors::NAMED_COLORS; 88 | 89 | #[cfg(feature = "cint")] 90 | mod cint; 91 | 92 | #[cfg(feature = "lab")] 93 | mod lab; 94 | 95 | mod utils; 96 | -------------------------------------------------------------------------------- /src/named_colors.rs: -------------------------------------------------------------------------------- 1 | /// Named colors defined in . 2 | pub static NAMED_COLORS: phf::Map<&'static str, [u8; 3]> = phf::phf_map! { 3 | "aliceblue" => [240, 248, 255], 4 | "antiquewhite" => [250, 235, 215], 5 | "aqua" => [0, 255, 255], 6 | "aquamarine" => [127, 255, 212], 7 | "azure" => [240, 255, 255], 8 | "beige" => [245, 245, 220], 9 | "bisque" => [255, 228, 196], 10 | "black" => [0, 0, 0], 11 | "blanchedalmond" => [255, 235, 205], 12 | "blue" => [0, 0, 255], 13 | "blueviolet" => [138, 43, 226], 14 | "brown" => [165, 42, 42], 15 | "burlywood" => [222, 184, 135], 16 | "cadetblue" => [95, 158, 160], 17 | "chartreuse" => [127, 255, 0], 18 | "chocolate" => [210, 105, 30], 19 | "coral" => [255, 127, 80], 20 | "cornflowerblue" => [100, 149, 237], 21 | "cornsilk" => [255, 248, 220], 22 | "crimson" => [220, 20, 60], 23 | "cyan" => [0, 255, 255], 24 | "darkblue" => [0, 0, 139], 25 | "darkcyan" => [0, 139, 139], 26 | "darkgoldenrod" => [184, 134, 11], 27 | "darkgray" => [169, 169, 169], 28 | "darkgreen" => [0, 100, 0], 29 | "darkgrey" => [169, 169, 169], 30 | "darkkhaki" => [189, 183, 107], 31 | "darkmagenta" => [139, 0, 139], 32 | "darkolivegreen" => [85, 107, 47], 33 | "darkorange" => [255, 140, 0], 34 | "darkorchid" => [153, 50, 204], 35 | "darkred" => [139, 0, 0], 36 | "darksalmon" => [233, 150, 122], 37 | "darkseagreen" => [143, 188, 143], 38 | "darkslateblue" => [72, 61, 139], 39 | "darkslategray" => [47, 79, 79], 40 | "darkslategrey" => [47, 79, 79], 41 | "darkturquoise" => [0, 206, 209], 42 | "darkviolet" => [148, 0, 211], 43 | "deeppink" => [255, 20, 147], 44 | "deepskyblue" => [0, 191, 255], 45 | "dimgray" => [105, 105, 105], 46 | "dimgrey" => [105, 105, 105], 47 | "dodgerblue" => [30, 144, 255], 48 | "firebrick" => [178, 34, 34], 49 | "floralwhite" => [255, 250, 240], 50 | "forestgreen" => [34, 139, 34], 51 | "fuchsia" => [255, 0, 255], 52 | "gainsboro" => [220, 220, 220], 53 | "ghostwhite" => [248, 248, 255], 54 | "gold" => [255, 215, 0], 55 | "goldenrod" => [218, 165, 32], 56 | "gray" => [128, 128, 128], 57 | "green" => [0, 128, 0], 58 | "greenyellow" => [173, 255, 47], 59 | "grey" => [128, 128, 128], 60 | "honeydew" => [240, 255, 240], 61 | "hotpink" => [255, 105, 180], 62 | "indianred" => [205, 92, 92], 63 | "indigo" => [75, 0, 130], 64 | "ivory" => [255, 255, 240], 65 | "khaki" => [240, 230, 140], 66 | "lavender" => [230, 230, 250], 67 | "lavenderblush" => [255, 240, 245], 68 | "lawngreen" => [124, 252, 0], 69 | "lemonchiffon" => [255, 250, 205], 70 | "lightblue" => [173, 216, 230], 71 | "lightcoral" => [240, 128, 128], 72 | "lightcyan" => [224, 255, 255], 73 | "lightgoldenrodyellow" => [250, 250, 210], 74 | "lightgray" => [211, 211, 211], 75 | "lightgreen" => [144, 238, 144], 76 | "lightgrey" => [211, 211, 211], 77 | "lightpink" => [255, 182, 193], 78 | "lightsalmon" => [255, 160, 122], 79 | "lightseagreen" => [32, 178, 170], 80 | "lightskyblue" => [135, 206, 250], 81 | "lightslategray" => [119, 136, 153], 82 | "lightslategrey" => [119, 136, 153], 83 | "lightsteelblue" => [176, 196, 222], 84 | "lightyellow" => [255, 255, 224], 85 | "lime" => [0, 255, 0], 86 | "limegreen" => [50, 205, 50], 87 | "linen" => [250, 240, 230], 88 | "magenta" => [255, 0, 255], 89 | "maroon" => [128, 0, 0], 90 | "mediumaquamarine" => [102, 205, 170], 91 | "mediumblue" => [0, 0, 205], 92 | "mediumorchid" => [186, 85, 211], 93 | "mediumpurple" => [147, 112, 219], 94 | "mediumseagreen" => [60, 179, 113], 95 | "mediumslateblue" => [123, 104, 238], 96 | "mediumspringgreen" => [0, 250, 154], 97 | "mediumturquoise" => [72, 209, 204], 98 | "mediumvioletred" => [199, 21, 133], 99 | "midnightblue" => [25, 25, 112], 100 | "mintcream" => [245, 255, 250], 101 | "mistyrose" => [255, 228, 225], 102 | "moccasin" => [255, 228, 181], 103 | "navajowhite" => [255, 222, 173], 104 | "navy" => [0, 0, 128], 105 | "oldlace" => [253, 245, 230], 106 | "olive" => [128, 128, 0], 107 | "olivedrab" => [107, 142, 35], 108 | "orange" => [255, 165, 0], 109 | "orangered" => [255, 69, 0], 110 | "orchid" => [218, 112, 214], 111 | "palegoldenrod" => [238, 232, 170], 112 | "palegreen" => [152, 251, 152], 113 | "paleturquoise" => [175, 238, 238], 114 | "palevioletred" => [219, 112, 147], 115 | "papayawhip" => [255, 239, 213], 116 | "peachpuff" => [255, 218, 185], 117 | "peru" => [205, 133, 63], 118 | "pink" => [255, 192, 203], 119 | "plum" => [221, 160, 221], 120 | "powderblue" => [176, 224, 230], 121 | "purple" => [128, 0, 128], 122 | "rebeccapurple" => [102, 51, 153], 123 | "red" => [255, 0, 0], 124 | "rosybrown" => [188, 143, 143], 125 | "royalblue" => [65, 105, 225], 126 | "saddlebrown" => [139, 69, 19], 127 | "salmon" => [250, 128, 114], 128 | "sandybrown" => [244, 164, 96], 129 | "seagreen" => [46, 139, 87], 130 | "seashell" => [255, 245, 238], 131 | "sienna" => [160, 82, 45], 132 | "silver" => [192, 192, 192], 133 | "skyblue" => [135, 206, 235], 134 | "slateblue" => [106, 90, 205], 135 | "slategray" => [112, 128, 144], 136 | "slategrey" => [112, 128, 144], 137 | "snow" => [255, 250, 250], 138 | "springgreen" => [0, 255, 127], 139 | "steelblue" => [70, 130, 180], 140 | "tan" => [210, 180, 140], 141 | "teal" => [0, 128, 128], 142 | "thistle" => [216, 191, 216], 143 | "tomato" => [255, 99, 71], 144 | "turquoise" => [64, 224, 208], 145 | "violet" => [238, 130, 238], 146 | "wheat" => [245, 222, 179], 147 | "white" => [255, 255, 255], 148 | "whitesmoke" => [245, 245, 245], 149 | "yellow" => [255, 255, 0], 150 | "yellowgreen" => [154, 205, 50], 151 | }; 152 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::remap; 2 | use crate::{Color, ParseColorError}; 3 | 4 | #[cfg(feature = "named-colors")] 5 | use crate::NAMED_COLORS; 6 | 7 | /// Parse CSS color string 8 | /// 9 | /// # Examples 10 | /// 11 | /// ``` 12 | /// # use std::error::Error; 13 | /// # fn main() -> Result<(), Box> { 14 | /// let c = csscolorparser::parse("#ff0")?; 15 | /// 16 | /// assert_eq!(c.to_array(), [1.0, 1.0, 0.0, 1.0]); 17 | /// assert_eq!(c.to_rgba8(), [255, 255, 0, 255]); 18 | /// assert_eq!(c.to_css_hex(), "#ffff00"); 19 | /// assert_eq!(c.to_css_rgb(), "rgb(255 255 0)"); 20 | /// # Ok(()) 21 | /// # } 22 | /// ``` 23 | /// 24 | /// ``` 25 | /// # use std::error::Error; 26 | /// # fn main() -> Result<(), Box> { 27 | /// let c = csscolorparser::parse("hsl(360deg,100%,50%)")?; 28 | /// 29 | /// assert_eq!(c.to_array(), [1.0, 0.0, 0.0, 1.0]); 30 | /// assert_eq!(c.to_rgba8(), [255, 0, 0, 255]); 31 | /// assert_eq!(c.to_css_hex(), "#ff0000"); 32 | /// assert_eq!(c.to_css_rgb(), "rgb(255 0 0)"); 33 | /// # Ok(()) 34 | /// # } 35 | /// ``` 36 | #[inline(never)] 37 | pub fn parse(s: &str) -> Result { 38 | let s = s.trim(); 39 | 40 | if s.eq_ignore_ascii_case("transparent") { 41 | return Ok(Color::new(0.0, 0.0, 0.0, 0.0)); 42 | } 43 | 44 | // Hex format 45 | if let Some(s) = s.strip_prefix('#') { 46 | return parse_hex(s); 47 | } 48 | 49 | if let (Some(idx), Some(s)) = (s.find('('), s.strip_suffix(')')) { 50 | let fname = &s[..idx].trim_end(); 51 | let mut params = s[idx + 1..] 52 | .split(&[',', '/']) 53 | .flat_map(str::split_ascii_whitespace); 54 | 55 | let (Some(val0), Some(val1), Some(val2)) = (params.next(), params.next(), params.next()) 56 | else { 57 | return Err(ParseColorError::InvalidFunction); 58 | }; 59 | 60 | let alpha = if let Some(a) = params.next() { 61 | if let Some((v, _)) = parse_percent_or_float(a) { 62 | v.clamp(0.0, 1.0) 63 | } else { 64 | return Err(ParseColorError::InvalidFunction); 65 | } 66 | } else { 67 | 1.0 68 | }; 69 | 70 | if params.next().is_some() { 71 | return Err(ParseColorError::InvalidFunction); 72 | } 73 | 74 | if fname.eq_ignore_ascii_case("rgb") || fname.eq_ignore_ascii_case("rgba") { 75 | if let (Some((r, r_fmt)), Some((g, g_fmt)), Some((b, b_fmt))) = ( 76 | // red 77 | parse_percent_or_255(val0), 78 | // green 79 | parse_percent_or_255(val1), 80 | // blue 81 | parse_percent_or_255(val2), 82 | ) { 83 | if r_fmt == g_fmt && g_fmt == b_fmt { 84 | return Ok(Color { 85 | r: r.clamp(0.0, 1.0), 86 | g: g.clamp(0.0, 1.0), 87 | b: b.clamp(0.0, 1.0), 88 | a: alpha, 89 | }); 90 | } 91 | } 92 | 93 | return Err(ParseColorError::InvalidRgb); 94 | } else if fname.eq_ignore_ascii_case("hsl") || fname.eq_ignore_ascii_case("hsla") { 95 | if let (Some(h), Some((s, s_fmt)), Some((l, l_fmt))) = ( 96 | // hue 97 | parse_angle(val0), 98 | // saturation 99 | parse_percent_or_float(val1), 100 | // lightness 101 | parse_percent_or_float(val2), 102 | ) { 103 | if s_fmt == l_fmt { 104 | return Ok(Color::from_hsla(h, s, l, alpha)); 105 | } 106 | } 107 | 108 | return Err(ParseColorError::InvalidHsl); 109 | } else if fname.eq_ignore_ascii_case("hwb") || fname.eq_ignore_ascii_case("hwba") { 110 | if let (Some(h), Some((w, w_fmt)), Some((b, b_fmt))) = ( 111 | // hue 112 | parse_angle(val0), 113 | // whiteness 114 | parse_percent_or_float(val1), 115 | // blackness 116 | parse_percent_or_float(val2), 117 | ) { 118 | if w_fmt == b_fmt { 119 | return Ok(Color::from_hwba(h, w, b, alpha)); 120 | } 121 | } 122 | 123 | return Err(ParseColorError::InvalidHwb); 124 | } else if fname.eq_ignore_ascii_case("hsv") || fname.eq_ignore_ascii_case("hsva") { 125 | if let (Some(h), Some((s, s_fmt)), Some((v, v_fmt))) = ( 126 | // hue 127 | parse_angle(val0), 128 | // saturation 129 | parse_percent_or_float(val1), 130 | // value 131 | parse_percent_or_float(val2), 132 | ) { 133 | if s_fmt == v_fmt { 134 | return Ok(Color::from_hsva(h, s, v, alpha)); 135 | } 136 | } 137 | 138 | return Err(ParseColorError::InvalidHsv); 139 | } else if fname.eq_ignore_ascii_case("lab") { 140 | #[cfg(feature = "lab")] 141 | if let (Some((l, l_fmt)), Some((a, a_fmt)), Some((b, b_fmt))) = ( 142 | // lightness 143 | parse_percent_or_float(val0), 144 | // a 145 | parse_percent_or_float(val1), 146 | // b 147 | parse_percent_or_float(val2), 148 | ) { 149 | let l = if l_fmt { l * 100.0 } else { l }; 150 | let a = if a_fmt { 151 | remap(a, -1.0, 1.0, -125.0, 125.0) 152 | } else { 153 | a 154 | }; 155 | let b = if b_fmt { 156 | remap(b, -1.0, 1.0, -125.0, 125.0) 157 | } else { 158 | b 159 | }; 160 | return Ok(Color::from_laba(l.max(0.0), a, b, alpha)); 161 | } else { 162 | return Err(ParseColorError::InvalidLab); 163 | } 164 | } else if fname.eq_ignore_ascii_case("lch") { 165 | #[cfg(feature = "lab")] 166 | if let (Some((l, l_fmt)), Some((c, c_fmt)), Some(h)) = ( 167 | // lightness 168 | parse_percent_or_float(val0), 169 | // chroma 170 | parse_percent_or_float(val1), 171 | // hue 172 | parse_angle(val2), 173 | ) { 174 | let l = if l_fmt { l * 100.0 } else { l }; 175 | let c = if c_fmt { c * 150.0 } else { c }; 176 | return Ok(Color::from_lcha( 177 | l.max(0.0), 178 | c.max(0.0), 179 | h.to_radians(), 180 | alpha, 181 | )); 182 | } else { 183 | return Err(ParseColorError::InvalidLch); 184 | } 185 | } else if fname.eq_ignore_ascii_case("oklab") { 186 | if let (Some((l, _)), Some((a, a_fmt)), Some((b, b_fmt))) = ( 187 | // lightness 188 | parse_percent_or_float(val0), 189 | // a 190 | parse_percent_or_float(val1), 191 | // b 192 | parse_percent_or_float(val2), 193 | ) { 194 | let a = if a_fmt { 195 | remap(a, -1.0, 1.0, -0.4, 0.4) 196 | } else { 197 | a 198 | }; 199 | let b = if b_fmt { 200 | remap(b, -1.0, 1.0, -0.4, 0.4) 201 | } else { 202 | b 203 | }; 204 | return Ok(Color::from_oklaba(l.max(0.0), a, b, alpha)); 205 | } 206 | 207 | return Err(ParseColorError::InvalidOklab); 208 | } else if fname.eq_ignore_ascii_case("oklch") { 209 | if let (Some((l, _)), Some((c, c_fmt)), Some(h)) = ( 210 | // lightness 211 | parse_percent_or_float(val0), 212 | // chroma 213 | parse_percent_or_float(val1), 214 | // hue 215 | parse_angle(val2), 216 | ) { 217 | let c = if c_fmt { c * 0.4 } else { c }; 218 | return Ok(Color::from_oklcha( 219 | l.max(0.0), 220 | c.max(0.0), 221 | h.to_radians(), 222 | alpha, 223 | )); 224 | } 225 | 226 | return Err(ParseColorError::InvalidOklch); 227 | } 228 | 229 | return Err(ParseColorError::InvalidFunction); 230 | } 231 | 232 | // Hex format without prefix '#' 233 | if let Ok(c) = parse_hex(s) { 234 | return Ok(c); 235 | } 236 | 237 | // Named colors 238 | #[cfg(feature = "named-colors")] 239 | if s.len() > 2 && s.len() < 21 { 240 | let s = s.to_ascii_lowercase(); 241 | if let Some([r, g, b]) = NAMED_COLORS.get(&s) { 242 | return Ok(Color::from_rgba8(*r, *g, *b, 255)); 243 | } 244 | } 245 | 246 | Err(ParseColorError::InvalidUnknown) 247 | } 248 | 249 | fn parse_hex(s: &str) -> Result { 250 | if !s.is_ascii() { 251 | return Err(ParseColorError::InvalidHex); 252 | } 253 | 254 | let n = s.len(); 255 | 256 | fn parse_single_digit(digit: &str) -> Result { 257 | u8::from_str_radix(digit, 16) 258 | .map(|n| (n << 4) | n) 259 | .map_err(|_| ParseColorError::InvalidHex) 260 | } 261 | 262 | if n == 3 || n == 4 { 263 | let r = parse_single_digit(&s[0..1])?; 264 | let g = parse_single_digit(&s[1..2])?; 265 | let b = parse_single_digit(&s[2..3])?; 266 | 267 | let a = if n == 4 { 268 | parse_single_digit(&s[3..4])? 269 | } else { 270 | 255 271 | }; 272 | 273 | Ok(Color::from_rgba8(r, g, b, a)) 274 | } else if n == 6 || n == 8 { 275 | let r = u8::from_str_radix(&s[0..2], 16).map_err(|_| ParseColorError::InvalidHex)?; 276 | let g = u8::from_str_radix(&s[2..4], 16).map_err(|_| ParseColorError::InvalidHex)?; 277 | let b = u8::from_str_radix(&s[4..6], 16).map_err(|_| ParseColorError::InvalidHex)?; 278 | 279 | let a = if n == 8 { 280 | u8::from_str_radix(&s[6..8], 16).map_err(|_| ParseColorError::InvalidHex)? 281 | } else { 282 | 255 283 | }; 284 | 285 | Ok(Color::from_rgba8(r, g, b, a)) 286 | } else { 287 | Err(ParseColorError::InvalidHex) 288 | } 289 | } 290 | 291 | // strip suffix ignore case 292 | fn strip_suffix<'a>(s: &'a str, suffix: &str) -> Option<&'a str> { 293 | if suffix.len() > s.len() { 294 | return None; 295 | } 296 | let s_end = &s[s.len() - suffix.len()..]; 297 | if s_end.eq_ignore_ascii_case(suffix) { 298 | Some(&s[..s.len() - suffix.len()]) 299 | } else { 300 | None 301 | } 302 | } 303 | 304 | fn parse_percent_or_float(s: &str) -> Option<(f32, bool)> { 305 | s.strip_suffix('%') 306 | .and_then(|s| s.parse().ok().map(|t: f32| (t / 100.0, true))) 307 | .or_else(|| s.parse().ok().map(|t| (t, false))) 308 | } 309 | 310 | fn parse_percent_or_255(s: &str) -> Option<(f32, bool)> { 311 | s.strip_suffix('%') 312 | .and_then(|s| s.parse().ok().map(|t: f32| (t / 100.0, true))) 313 | .or_else(|| s.parse().ok().map(|t: f32| (t / 255.0, false))) 314 | } 315 | 316 | fn parse_angle(s: &str) -> Option { 317 | strip_suffix(s, "deg") 318 | .and_then(|s| s.parse().ok()) 319 | .or_else(|| { 320 | strip_suffix(s, "grad") 321 | .and_then(|s| s.parse().ok()) 322 | .map(|t: f32| t * 360.0 / 400.0) 323 | }) 324 | .or_else(|| { 325 | strip_suffix(s, "rad") 326 | .and_then(|s| s.parse().ok()) 327 | .map(|t: f32| t.to_degrees()) 328 | }) 329 | .or_else(|| { 330 | strip_suffix(s, "turn") 331 | .and_then(|s| s.parse().ok()) 332 | .map(|t: f32| t * 360.0) 333 | }) 334 | .or_else(|| s.parse().ok()) 335 | } 336 | 337 | #[cfg(test)] 338 | mod tests { 339 | use super::*; 340 | 341 | #[test] 342 | fn test_strip_suffix() { 343 | assert_eq!(strip_suffix("45deg", "deg"), Some("45")); 344 | assert_eq!(strip_suffix("90DEG", "deg"), Some("90")); 345 | assert_eq!(strip_suffix("0.25turn", "turn"), Some("0.25")); 346 | assert_eq!(strip_suffix("1.0Turn", "turn"), Some("1.0")); 347 | 348 | assert_eq!(strip_suffix("", "deg"), None); 349 | assert_eq!(strip_suffix("90", "deg"), None); 350 | } 351 | 352 | #[test] 353 | fn test_parse_percent_or_float() { 354 | let test_data = [ 355 | ("0%", Some((0.0, true))), 356 | ("100%", Some((1.0, true))), 357 | ("50%", Some((0.5, true))), 358 | ("0", Some((0.0, false))), 359 | ("1", Some((1.0, false))), 360 | ("0.5", Some((0.5, false))), 361 | ("100.0", Some((100.0, false))), 362 | ("-23.7", Some((-23.7, false))), 363 | ("%", None), 364 | ("1x", None), 365 | ]; 366 | for (s, expected) in test_data { 367 | assert_eq!(parse_percent_or_float(s), expected); 368 | } 369 | } 370 | 371 | #[test] 372 | fn test_parse_percent_or_255() { 373 | let test_data = [ 374 | ("0%", Some((0.0, true))), 375 | ("100%", Some((1.0, true))), 376 | ("50%", Some((0.5, true))), 377 | ("-100%", Some((-1.0, true))), 378 | ("0", Some((0.0, false))), 379 | ("255", Some((1.0, false))), 380 | ("127.5", Some((0.5, false))), 381 | ("%", None), 382 | ("255x", None), 383 | ]; 384 | for (s, expected) in test_data { 385 | assert_eq!(parse_percent_or_255(s), expected); 386 | } 387 | } 388 | 389 | #[test] 390 | fn test_parse_angle() { 391 | let test_data = [ 392 | ("360", Some(360.0)), 393 | ("127.356", Some(127.356)), 394 | ("+120deg", Some(120.0)), 395 | ("90deg", Some(90.0)), 396 | ("-127deg", Some(-127.0)), 397 | ("100grad", Some(90.0)), 398 | ("1.5707963267948966rad", Some(90.0)), 399 | ("0.25turn", Some(90.0)), 400 | ("-0.25turn", Some(-90.0)), 401 | ("O", None), 402 | ("Odeg", None), 403 | ("rad", None), 404 | ]; 405 | for (s, expected) in test_data { 406 | assert_eq!(parse_angle(s), expected); 407 | } 408 | } 409 | 410 | #[test] 411 | fn test_parse_hex() { 412 | // case-insensitive tests 413 | macro_rules! cmp { 414 | ($a:expr, $b:expr) => { 415 | assert_eq!( 416 | parse_hex($a).unwrap().to_rgba8(), 417 | parse_hex($b).unwrap().to_rgba8() 418 | ); 419 | }; 420 | } 421 | cmp!("abc", "ABC"); 422 | cmp!("DeF", "dEf"); 423 | cmp!("f0eB", "F0Eb"); 424 | cmp!("abcdef", "ABCDEF"); 425 | cmp!("Ff03E0cB", "fF03e0Cb"); 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "lab")] 2 | use std::f32::consts::{PI, TAU}; 3 | 4 | #[cfg(feature = "lab")] 5 | const PI_3: f32 = PI * 3.0; 6 | 7 | #[allow(clippy::excessive_precision)] 8 | pub(crate) fn oklab_to_linear_rgb(l: f32, a: f32, b: f32) -> [f32; 3] { 9 | let l_ = (l + 0.3963377774 * a + 0.2158037573 * b).powi(3); 10 | let m_ = (l - 0.1055613458 * a - 0.0638541728 * b).powi(3); 11 | let s_ = (l - 0.0894841775 * a - 1.2914855480 * b).powi(3); 12 | let r = 4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_; 13 | let g = -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_; 14 | let b = -0.0041960863 * l_ - 0.7034186147 * m_ + 1.7076147010 * s_; 15 | [r, g, b] 16 | } 17 | 18 | #[allow(clippy::excessive_precision)] 19 | pub(crate) fn linear_rgb_to_oklab(r: f32, g: f32, b: f32) -> [f32; 3] { 20 | let l_ = (0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b).cbrt(); 21 | let m_ = (0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b).cbrt(); 22 | let s_ = (0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b).cbrt(); 23 | let l = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_; 24 | let a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_; 25 | let b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_; 26 | [l, a, b] 27 | } 28 | 29 | pub(crate) const fn hue_to_rgb(n1: f32, n2: f32, h: f32) -> f32 { 30 | let h = modulo(h, 6.0); 31 | 32 | if h < 1.0 { 33 | return n1 + ((n2 - n1) * h); 34 | } 35 | 36 | if h < 3.0 { 37 | return n2; 38 | } 39 | 40 | if h < 4.0 { 41 | return n1 + ((n2 - n1) * (4.0 - h)); 42 | } 43 | 44 | n1 45 | } 46 | 47 | // h = 0..360 48 | // s, l = 0..1 49 | // r, g, b = 0..1 50 | pub(crate) const fn hsl_to_rgb(h: f32, s: f32, l: f32) -> [f32; 3] { 51 | if s == 0.0 { 52 | return [l, l, l]; 53 | } 54 | 55 | let n2 = if l < 0.5 { 56 | l * (1.0 + s) 57 | } else { 58 | l + s - (l * s) 59 | }; 60 | 61 | let n1 = 2.0 * l - n2; 62 | let h = h / 60.0; 63 | let r = hue_to_rgb(n1, n2, h + 2.0); 64 | let g = hue_to_rgb(n1, n2, h); 65 | let b = hue_to_rgb(n1, n2, h - 2.0); 66 | [r, g, b] 67 | } 68 | 69 | pub(crate) const fn hwb_to_rgb(hue: f32, white: f32, black: f32) -> [f32; 3] { 70 | if white + black >= 1.0 { 71 | let l = white / (white + black); 72 | return [l, l, l]; 73 | } 74 | 75 | let [r, g, b] = hsl_to_rgb(hue, 1.0, 0.5); 76 | let r = r * (1.0 - white - black) + white; 77 | let g = g * (1.0 - white - black) + white; 78 | let b = b * (1.0 - white - black) + white; 79 | [r, g, b] 80 | } 81 | 82 | #[allow(clippy::float_cmp)] 83 | pub(crate) const fn hsv_to_hsl(h: f32, s: f32, v: f32) -> [f32; 3] { 84 | let l = (2.0 - s) * v / 2.0; 85 | 86 | let s = if l != 0.0 { 87 | if l == 1.0 { 88 | 0.0 89 | } else if l < 0.5 { 90 | s * v / (l * 2.0) 91 | } else { 92 | s * v / (2.0 - l * 2.0) 93 | } 94 | } else { 95 | s 96 | }; 97 | 98 | [h, s, l] 99 | } 100 | 101 | pub(crate) const fn hsv_to_rgb(h: f32, s: f32, v: f32) -> [f32; 3] { 102 | let [h, s, l] = hsv_to_hsl(h, s, v); 103 | hsl_to_rgb(h, s, l) 104 | } 105 | 106 | #[allow(clippy::float_cmp)] 107 | pub(crate) const fn rgb_to_hsv(r: f32, g: f32, b: f32) -> [f32; 3] { 108 | let v = r.max(g.max(b)); 109 | let d = v - r.min(g.min(b)); 110 | 111 | if d == 0.0 { 112 | return [0.0, 0.0, v]; 113 | } 114 | 115 | let s = d / v; 116 | let dr = (v - r) / d; 117 | let dg = (v - g) / d; 118 | let db = (v - b) / d; 119 | 120 | let h = if r == v { 121 | db - dg 122 | } else if g == v { 123 | 2.0 + dr - db 124 | } else { 125 | 4.0 + dg - dr 126 | }; 127 | 128 | let h = (h * 60.0) % 360.0; 129 | [normalize_angle(h), s, v] 130 | } 131 | 132 | #[allow(clippy::float_cmp)] 133 | pub(crate) const fn rgb_to_hsl(r: f32, g: f32, b: f32) -> [f32; 3] { 134 | let min = r.min(g.min(b)); 135 | let max = r.max(g.max(b)); 136 | let l = (max + min) / 2.0; 137 | 138 | if min == max { 139 | return [0.0, 0.0, l]; 140 | } 141 | 142 | let d = max - min; 143 | 144 | let s = if l < 0.5 { 145 | d / (max + min) 146 | } else { 147 | d / (2.0 - max - min) 148 | }; 149 | 150 | let dr = (max - r) / d; 151 | let dg = (max - g) / d; 152 | let db = (max - b) / d; 153 | 154 | let h = if r == max { 155 | db - dg 156 | } else if g == max { 157 | 2.0 + dr - db 158 | } else { 159 | 4.0 + dg - dr 160 | }; 161 | 162 | let h = (h * 60.0) % 360.0; 163 | [normalize_angle(h), s, l] 164 | } 165 | 166 | pub(crate) const fn rgb_to_hwb(r: f32, g: f32, b: f32) -> [f32; 3] { 167 | let [hue, _, _] = rgb_to_hsl(r, g, b); 168 | let white = r.min(g.min(b)); 169 | let black = 1.0 - r.max(g.max(b)); 170 | [hue, white, black] 171 | } 172 | 173 | #[inline] 174 | pub(crate) const fn normalize_angle(t: f32) -> f32 { 175 | ((t % 360.0) + 360.0) % 360.0 176 | } 177 | 178 | #[inline] 179 | pub(crate) const fn interp_angle(a0: f32, a1: f32, t: f32) -> f32 { 180 | let delta = (((a1 - a0) % 360.0) + 540.0) % 360.0 - 180.0; 181 | (a0 + t * delta + 360.0) % 360.0 182 | } 183 | 184 | #[cfg(feature = "lab")] 185 | #[inline] 186 | pub(crate) const fn interp_angle_rad(a0: f32, a1: f32, t: f32) -> f32 { 187 | let delta = (((a1 - a0) % TAU) + PI_3) % TAU - PI; 188 | (a0 + t * delta + TAU) % TAU 189 | } 190 | 191 | #[inline] 192 | pub(crate) const fn modulo(x: f32, n: f32) -> f32 { 193 | (x % n + n) % n 194 | } 195 | 196 | // Map t from range [a, b] to range [c, d] 197 | #[inline] 198 | pub(crate) const fn remap(t: f32, a: f32, b: f32, c: f32, d: f32) -> f32 { 199 | (t - a) * ((d - c) / (b - a)) + c 200 | } 201 | 202 | #[cfg(test)] 203 | mod tests { 204 | use super::*; 205 | 206 | #[test] 207 | fn test_normalize_angle() { 208 | let data = vec![ 209 | (0.0, 0.0), 210 | (360.0, 0.0), 211 | (720.0, 0.0), 212 | (400.0, 40.0), 213 | (1155.0, 75.0), 214 | (-360.0, 0.0), 215 | (-90.0, 270.0), 216 | (-765.0, 315.0), 217 | ]; 218 | for (x, expected) in data { 219 | let c = normalize_angle(x); 220 | assert_eq!(expected, c); 221 | } 222 | } 223 | 224 | #[test] 225 | fn test_interp_angle() { 226 | let data = vec![ 227 | ((0.0, 360.0, 0.5), 0.0), 228 | ((360.0, 90.0, 0.0), 0.0), 229 | ((360.0, 90.0, 0.5), 45.0), 230 | ((360.0, 90.0, 1.0), 90.0), 231 | ]; 232 | for ((a, b, t), expected) in data { 233 | let v = interp_angle(a, b, t); 234 | assert_eq!(expected, v); 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /tests/chrome_android.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn random_colors() { 3 | // The color string is randomly generated, then parsed using Chrome 87.0 on Android 4 | let test_data = vec![ 5 | ("#6946", [102, 153, 68, 102]), 6 | ("#4F0", [68, 255, 0, 255]), 7 | ("#75C57A", [117, 197, 122, 255]), 8 | ("#4E0A70", [78, 10, 112, 255]), 9 | ("#A195", [170, 17, 153, 85]), 10 | ("#4A4786", [74, 71, 134, 255]), 11 | ("#FC0", [255, 204, 0, 255]), 12 | ("#517FE4", [81, 127, 228, 255]), 13 | ("#1594A4", [21, 148, 164, 255]), 14 | ("#1B574A", [27, 87, 74, 255]), 15 | ("#9445D48C", [148, 69, 212, 140]), 16 | ("#BC9", [187, 204, 153, 255]), 17 | ("#9E3C6B", [158, 60, 107, 255]), 18 | ("#E92C2E5B", [233, 44, 46, 91]), 19 | ("#563B", [85, 102, 51, 187]), 20 | ("#FAD87DAA", [250, 216, 125, 170]), 21 | ("#B11", [187, 17, 17, 255]), 22 | ("#817B", [136, 17, 119, 187]), 23 | ("#2DD", [34, 221, 221, 255]), 24 | ("#7403B61A", [116, 3, 182, 26]), 25 | ("#02ABBC2F", [2, 171, 188, 47]), 26 | ("#B9A1D823", [185, 161, 216, 35]), 27 | ("#180", [17, 136, 0, 255]), 28 | ("#AB8D15A1", [171, 141, 21, 161]), 29 | ("#B701", [187, 119, 0, 17]), 30 | ("#6DEBD9", [109, 235, 217, 255]), 31 | ("#A09", [170, 0, 153, 255]), 32 | ("#84A2CB", [132, 162, 203, 255]), 33 | ("#361501", [54, 21, 1, 255]), 34 | ("#8F20", [136, 255, 34, 0]), 35 | ("#9EC963", [158, 201, 99, 255]), 36 | ("#B2B50F", [178, 181, 15, 255]), 37 | ("#B038", [187, 0, 51, 136]), 38 | ("#E769EB", [231, 105, 235, 255]), 39 | ("#C4A", [204, 68, 170, 255]), 40 | ("#9E5", [153, 238, 85, 255]), 41 | ("#FF0", [255, 255, 0, 255]), 42 | ("#6DF", [102, 221, 255, 255]), 43 | ("#A41", [170, 68, 17, 255]), 44 | ("#596F0B", [89, 111, 11, 255]), 45 | ("#1A6", [17, 170, 102, 255]), 46 | ("#BB9B0EA9", [187, 155, 14, 169]), 47 | ("#85A", [136, 85, 170, 255]), 48 | ("#938B", [153, 51, 136, 187]), 49 | ("#F89", [255, 136, 153, 255]), 50 | ("#447591", [68, 117, 145, 255]), 51 | ("#8CE6", [136, 204, 238, 102]), 52 | ("#E1528097", [225, 82, 128, 151]), 53 | ("#7BCE11", [123, 206, 17, 255]), 54 | ("#40AECEB7", [64, 174, 206, 183]), 55 | ("rgb(93.170,64.269,73.499)", [93, 64, 73, 255]), 56 | ("rgb(7.929,61.287,229.755,-0.172)", [8, 61, 230, 0]), 57 | ("rgb(59.947,234.441,231.856)", [60, 234, 232, 255]), 58 | ("rgb(40.839,233.232,263.280)", [41, 233, 255, 255]), 59 | ("rgb(146.043,242.067,228.262,1.148)", [146, 242, 228, 255]), 60 | ("rgb(40.800,31.415,141.897,0.721)", [41, 31, 142, 184]), 61 | ("rgb(252.237,162.173,239.848,1.159)", [252, 162, 240, 255]), 62 | ("rgb(187.805,86.185,253.946,1.088)", [188, 86, 254, 255]), 63 | ("rgb(20.891,82.288,149.768)", [21, 82, 150, 255]), 64 | ("rgb(132.275,143.495,131.475)", [132, 143, 131, 255]), 65 | ("rgb(106.238,145.684,129.067)", [106, 146, 129, 255]), 66 | ("rgb(6.092,125.387,194.562,0.176)", [6, 125, 195, 45]), 67 | ("rgb(215.571,177.198,41.277)", [216, 177, 41, 255]), 68 | ("rgb(226.868,90.077,42.413,0.458)", [227, 90, 42, 117]), 69 | ("rgb(79.828,63.604,4.446,-0.071)", [80, 64, 4, 0]), 70 | ("rgb(182.752,129.166,77.250,0.955)", [183, 129, 77, 244]), 71 | ("rgb(214.852,23.610,245.474)", [215, 24, 245, 255]), 72 | ("rgb(132.858,192.196,34.316)", [133, 192, 34, 255]), 73 | ("rgb(222.322,136.431,72.692)", [222, 136, 73, 255]), 74 | ("rgb(263.504,94.097,7.834)", [255, 94, 8, 255]), 75 | ("rgb(254.248,34.996,26.785)", [254, 35, 27, 255]), 76 | ("rgb(154.676,75.369,247.392)", [155, 75, 247, 255]), 77 | ("rgb(98.048,245.136,84.628)", [98, 245, 85, 255]), 78 | ("rgb(257.612,4.746,247.485,0.894)", [255, 5, 247, 228]), 79 | ("rgb(108.478,219.046,146.111)", [108, 219, 146, 255]), 80 | ("rgb(87.414,185.873,26.154,0.536)", [87, 186, 26, 137]), 81 | ("rgb(91.980,117.789,56.497)", [92, 118, 56, 255]), 82 | ("rgb(134.494,228.709,63.294,0.649)", [134, 229, 63, 165]), 83 | ("rgb(44.674,131.163,35.602)", [45, 131, 36, 255]), 84 | ("rgb(127.390,73.029,27.948,0.963)", [127, 73, 28, 246]), 85 | ("rgb(187.426,125.312,219.397)", [187, 125, 219, 255]), 86 | ("rgb(254.713,264.153,258.329)", [255, 255, 255, 255]), 87 | ("rgb(244.510,207.326,178.902,0.339)", [245, 207, 179, 86]), 88 | ("rgb(26.199,-9.612,231.652,0.662)", [26, 0, 232, 169]), 89 | ("rgb(45.304,89.336,172.582)", [45, 89, 173, 255]), 90 | ("rgb(191.387,107.530,36.480)", [191, 108, 36, 255]), 91 | ("rgb(100.149,52.445,27.521,0.677)", [100, 52, 28, 173]), 92 | ("rgb(51.050,84.982,166.808)", [51, 85, 167, 255]), 93 | ("rgb(37.221,239.178,59.749)", [37, 239, 60, 255]), 94 | ("rgb(92.024,78.623,56.212,0.765)", [92, 79, 56, 195]), 95 | ("rgb(61.206,222.657,-6.863,0.526)", [61, 223, 0, 134]), 96 | ("rgb(233.507,205.865,171.023,0.637)", [234, 206, 171, 162]), 97 | ("rgb(135.288,90.821,193.045)", [135, 91, 193, 255]), 98 | ("rgb(18.590,144.907,124.470)", [19, 145, 124, 255]), 99 | ("rgb(241.835,158.227,224.189)", [242, 158, 224, 255]), 100 | ("rgb(184.917,157.120,206.682,0.283)", [185, 157, 207, 72]), 101 | ("rgb(185.019,101.395,60.572)", [185, 101, 61, 255]), 102 | ("rgb(67.718,1.945,198.884)", [68, 2, 199, 255]), 103 | ("rgb(129.479,142.689,149.387,0.186)", [129, 143, 149, 47]), 104 | ("rgb(253.673,-2.343,196.555,-0.144)", [254, 0, 197, 0]), 105 | ("rgb(98.168%,7.203%,51.225%)", [250, 18, 131, 255]), 106 | ("rgb(96.170%,77.147%,85.996%)", [245, 197, 219, 255]), 107 | ("rgb(44.834%,53.204%,-9.634%,0.188)", [114, 136, 0, 48]), 108 | ("rgb(-7.920%,77.363%,-5.832%)", [0, 197, 0, 255]), 109 | ("rgb(81.003%,65.363%,-9.457%)", [207, 167, 0, 255]), 110 | ("rgb(85.894%,7.216%,-7.468%)", [219, 18, 0, 255]), 111 | ("rgb(100.659%,-3.397%,-0.802%,0.330)", [255, 0, 0, 84]), 112 | ("rgb(32.614%,63.257%,55.861%,-0.179)", [83, 161, 142, 0]), 113 | ("rgb(17.267%,78.342%,106.706%)", [44, 200, 255, 255]), 114 | ("rgb(-2.465%,70.281%,64.300%)", [0, 179, 164, 255]), 115 | ("rgb(74.917%,92.472%,35.277%)", [191, 236, 90, 255]), 116 | ("rgb(25.250%,103.119%,9.820%,1.073)", [64, 255, 25, 255]), 117 | ("rgb(16.308%,73.992%,41.494%)", [42, 189, 106, 255]), 118 | ("rgb(33.416%,64.317%,2.900%)", [85, 164, 7, 255]), 119 | ("rgb(86.321%,-7.134%,95.066%,0.407)", [220, 0, 242, 104]), 120 | ("rgb(72.337%,85.660%,37.990%)", [184, 218, 97, 255]), 121 | ("rgb(60.830%,102.371%,64.532%)", [155, 255, 165, 255]), 122 | ("rgb(22.944%,45.301%,57.417%,0.233)", [59, 116, 146, 59]), 123 | ("rgb(-8.796%,77.305%,55.175%)", [0, 197, 141, 255]), 124 | ("rgb(62.273%,88.630%,40.361%,-0.133)", [159, 226, 103, 0]), 125 | ("rgb(9.002%,9.048%,55.050%)", [23, 23, 140, 255]), 126 | ("rgb(39.705%,64.215%,33.386%)", [101, 164, 85, 255]), 127 | ("rgb(79.062%,9.806%,-0.228%)", [202, 25, 0, 255]), 128 | ("rgb(12.557%,26.742%,81.062%,0.187)", [32, 68, 207, 48]), 129 | ("rgb(48.037%,44.658%,94.883%,0.104)", [122, 114, 242, 27]), 130 | ("rgb(70.643%,18.209%,5.847%)", [180, 46, 15, 255]), 131 | ("rgb(17.439%,107.090%,-4.975%,0.301)", [44, 255, 0, 77]), 132 | ("rgb(36.867%,63.947%,53.503%)", [94, 163, 136, 255]), 133 | ("rgb(58.049%,108.306%,41.125%,-0.083)", [148, 255, 105, 0]), 134 | ("rgb(53.613%,-2.382%,20.660%,0.375)", [137, 0, 53, 96]), 135 | ("rgb(17.281%,0.850%,86.809%)", [44, 2, 221, 255]), 136 | ("rgb(28.877%,108.291%,22.159%,0.048)", [74, 255, 57, 12]), 137 | ("rgb(67.469%,33.982%,29.863%)", [172, 87, 76, 255]), 138 | ("rgb(12.841%,42.108%,77.364%)", [33, 107, 197, 255]), 139 | ("rgb(-6.254%,104.573%,54.338%,0.326)", [0, 255, 139, 83]), 140 | ("rgb(23.335%,-7.262%,32.061%)", [60, 0, 82, 255]), 141 | ("rgb(33.559%,104.368%,82.602%)", [86, 255, 211, 255]), 142 | ("rgb(51.030%,84.331%,22.085%)", [130, 215, 56, 255]), 143 | ("rgb(-7.688%,-0.346%,109.870%,0.492)", [0, 0, 255, 125]), 144 | ("rgb(37.791%,66.140%,-2.511%)", [96, 169, 0, 255]), 145 | ("rgb(14.877%,-9.937%,98.026%,0.391)", [38, 0, 250, 100]), 146 | ("rgb(68.965%,54.114%,79.671%,0.786)", [176, 138, 203, 200]), 147 | ("rgb(32.699%,84.074%,12.438%)", [83, 214, 32, 255]), 148 | ("rgb(61.109%,37.962%,9.726%)", [156, 97, 25, 255]), 149 | ("rgb(50.551%,21.936%,91.162%,0.379)", [129, 56, 232, 97]), 150 | ("rgb(12.006%,50.391%,84.702%)", [31, 128, 216, 255]), 151 | ("rgb(58.866%,36.294%,44.703%)", [150, 93, 114, 255]), 152 | ("rgb(73.712%,35.422%,91.533%)", [188, 90, 233, 255]), 153 | ("rgb(35.268%,82.428%,99.633%)", [90, 210, 254, 255]), 154 | ("rgb(77.313%,92.131%,3.558%)", [197, 235, 9, 255]), 155 | ("hsl(191.596grad,43.986%,97.620%)", [246, 252, 251, 255]), 156 | ("hsl(79.060deg,8.776%,101.675%)", [255, 255, 255, 255]), 157 | ( 158 | "hsl(218.222deg,30.111%,58.967%,0.650)", 159 | [119, 142, 182, 166], 160 | ), 161 | ("hsl(-0.032turn,90.086%,72.510%)", [248, 122, 146, 255]), 162 | ("hsl(21.358deg,34.513%,70.780%)", [206, 173, 155, 255]), 163 | ("hsl(248.704deg,41.071%,94.868%,-0.194)", [238, 237, 247, 0]), 164 | ("hsl(0.315turn,88.238%,77.402%,0.394)", [158, 248, 147, 100]), 165 | ("hsl(1.126rad,23.412%,92.759%)", [240, 241, 232, 255]), 166 | ("hsl(221.926,39.531%,81.028%)", [187, 199, 226, 255]), 167 | ("hsl(275.876deg,77.777%,21.408%)", [63, 12, 97, 255]), 168 | ("hsl(0.223turn,6.827%,45.074%)", [117, 123, 107, 255]), 169 | ("hsl(88.794deg,91.390%,9.152%)", [24, 45, 2, 255]), 170 | ("hsl(322.793grad,7.388%,26.361%,0.283)", [71, 62, 72, 72]), 171 | ("hsl(320.912grad,89.287%,50.117%)", [199, 14, 241, 255]), 172 | ("hsl(89.001grad,-6.397%,14.927%)", [38, 38, 38, 255]), 173 | ("hsl(0.315turn,61.008%,39.837%)", [53, 164, 40, 255]), 174 | ("hsl(3.187rad,10.620%,1.837%,0.852)", [4, 5, 5, 217]), 175 | ("hsl(15.726,68.485%,92.900%,0.955)", [249, 231, 224, 244]), 176 | ("hsl(288.124deg,102.249%,26.487%)", [108, 0, 135, 255]), 177 | ( 178 | "hsl(66.360deg,17.120%,104.832%,0.867)", 179 | [255, 255, 255, 221], 180 | ), 181 | ("hsl(199.194deg,105.374%,33.202%)", [0, 115, 169, 255]), 182 | ("hsl(337.002grad,71.223%,56.730%)", [223, 66, 215, 255]), 183 | ("hsl(359.117,89.688%,25.416%,0.329)", [123, 7, 8, 84]), 184 | ("hsl(0.210rad,50.484%,31.972%)", [123, 57, 40, 255]), 185 | ("hsl(-3.642,101.576%,101.667%)", [255, 255, 255, 255]), 186 | ("hsl(0.904turn,97.186%,99.315%)", [255, 252, 254, 255]), 187 | ("hsl(0.123turn,64.031%,76.579%)", [234, 213, 157, 255]), 188 | ("hsl(165.918grad,2.876%,-4.821%,-0.128)", [0, 0, 0, 0]), 189 | ("hsl(47.128grad,86.109%,42.237%)", [200, 146, 15, 255]), 190 | ("hsl(357.805,66.392%,90.933%,0.084)", [247, 217, 218, 21]), 191 | ("hsl(59.582deg,72.571%,70.685%)", [234, 234, 126, 255]), 192 | ("hsl(219.957grad,88.355%,34.566%)", [10, 119, 166, 255]), 193 | ("hsl(184.649,78.952%,88.831%,0.234)", [204, 246, 249, 60]), 194 | ("hsl(243.323,11.432%,-6.552%)", [0, 0, 0, 255]), 195 | ("hsl(2.873rad,97.021%,7.421%)", [1, 37, 28, 255]), 196 | ("hsl(-5.237grad,32.508%,7.772%,0.700)", [26, 13, 14, 179]), 197 | ("hsl(0.065turn,74.685%,70.874%,-0.026)", [236, 169, 125, 0]), 198 | ("hsl(209.534grad,-3.497%,28.848%)", [74, 74, 74, 255]), 199 | ("hsl(120.884,48.350%,6.898%)", [9, 26, 9, 255]), 200 | ("hsl(327.151grad,8.405%,32.912%)", [90, 77, 91, 255]), 201 | ("hsl(3.470rad,38.504%,41.877%)", [66, 122, 148, 255]), 202 | ("hsl(5.489rad,88.555%,44.840%,0.832)", [216, 13, 167, 212]), 203 | ("hsl(0.295turn,34.256%,11.399%)", [24, 39, 19, 255]), 204 | ( 205 | "hsl(70.479deg,86.200%,102.072%,0.789)", 206 | [255, 255, 255, 201], 207 | ), 208 | ("hsl(77.957grad,53.345%,16.670%)", [58, 65, 20, 255]), 209 | ("hsl(5.562rad,11.968%,109.436%)", [255, 255, 255, 255]), 210 | ("hsl(404.677grad,103.003%,9.650%)", [49, 3, 0, 255]), 211 | ("hsl(2.715rad,89.884%,15.064%,-0.083)", [4, 73, 45, 0]), 212 | ("hsl(235.834,104.274%,12.569%)", [0, 4, 64, 255]), 213 | ("hsl(5.068rad,5.213%,83.391%)", [214, 210, 215, 255]), 214 | ]; 215 | for (s, expected) in test_data { 216 | let rgba = csscolorparser::parse(s).unwrap().to_rgba8(); 217 | assert_eq!(expected, rgba); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /tests/chromium.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn random_colors() { 3 | // The color string is randomly generated, then parsed using Chromium 87.0.4280.66 4 | let test_data = vec![ 5 | ("#9F2", [153, 255, 34, 255]), 6 | ("#919211", [145, 146, 17, 255]), 7 | ("#DF1A94", [223, 26, 148, 255]), 8 | ("#0E9665", [14, 150, 101, 255]), 9 | ("#D39", [221, 51, 153, 255]), 10 | ("#6EF1BB", [110, 241, 187, 255]), 11 | ("#5F2452", [95, 36, 82, 255]), 12 | ("#8E6", [136, 238, 102, 255]), 13 | ("#09BB", [0, 153, 187, 187]), 14 | ("#A51", [170, 85, 17, 255]), 15 | ("#E1FCF3", [225, 252, 243, 255]), 16 | ("#02E3", [0, 34, 238, 51]), 17 | ("#E6B2", [238, 102, 187, 34]), 18 | ("#8CEA3820", [140, 234, 56, 32]), 19 | ("#2B8", [34, 187, 136, 255]), 20 | ("#195", [17, 153, 85, 255]), 21 | ("#4B6", [68, 187, 102, 255]), 22 | ("#9AE", [153, 170, 238, 255]), 23 | ("#9D39E0", [157, 57, 224, 255]), 24 | ("#66E12CAC", [102, 225, 44, 172]), 25 | ("#27EB", [34, 119, 238, 187]), 26 | ("#9C12", [153, 204, 17, 34]), 27 | ("#86113903", [134, 17, 57, 3]), 28 | ("#B1C6ED", [177, 198, 237, 255]), 29 | ("#79D", [119, 153, 221, 255]), 30 | ("#D3F8B137", [211, 248, 177, 55]), 31 | ("#E2858DF2", [226, 133, 141, 242]), 32 | ("#B0DBA6", [176, 219, 166, 255]), 33 | ("#F7F", [255, 119, 255, 255]), 34 | ("#97C9", [153, 119, 204, 153]), 35 | ("#F06", [255, 0, 102, 255]), 36 | ("#243B", [34, 68, 51, 187]), 37 | ("#20DA", [34, 0, 221, 170]), 38 | ("#13C364B8", [19, 195, 100, 184]), 39 | ("#E515", [238, 85, 17, 85]), 40 | ("#A11C", [170, 17, 17, 204]), 41 | ("#488", [68, 136, 136, 255]), 42 | ("#5EE", [85, 238, 238, 255]), 43 | ("#5AB8A1", [90, 184, 161, 255]), 44 | ("#9A9AB8", [154, 154, 184, 255]), 45 | ("#EA9E", [238, 170, 153, 238]), 46 | ("#551AB14D", [85, 26, 177, 77]), 47 | ("#D1D5", [221, 17, 221, 85]), 48 | ("#3C4", [51, 204, 68, 255]), 49 | ("#B615FED3", [182, 21, 254, 211]), 50 | ("#FC7", [255, 204, 119, 255]), 51 | ("#4B10D2", [75, 16, 210, 255]), 52 | ("#2188EC", [33, 136, 236, 255]), 53 | ("#A68F5F9F", [166, 143, 95, 159]), 54 | ("#4670E4", [70, 112, 228, 255]), 55 | ("rgb(56.467,186.479,125.521)", [56, 186, 126, 255]), 56 | ("rgb(227.181,102.976,142.108)", [227, 103, 142, 255]), 57 | ("rgb(30.436,119.528,76.579)", [30, 120, 77, 255]), 58 | ("rgb(93.103,251.965,52.385)", [93, 252, 52, 255]), 59 | ("rgb(161.891,261.654,114.275)", [162, 255, 114, 255]), 60 | ("rgb(235.921,88.153,244.002)", [236, 88, 244, 255]), 61 | ("rgb(185.204,3.571,140.660,1.079)", [185, 4, 141, 255]), 62 | ("rgb(141.230,184.483,37.035)", [141, 184, 37, 255]), 63 | ("rgb(37.685,82.003,192.789,0.283)", [38, 82, 193, 72]), 64 | ("rgb(45.740,9.845,259.876)", [46, 10, 255, 255]), 65 | ("rgb(86.411,188.879,237.442,0.876)", [86, 189, 237, 223]), 66 | ("rgb(156.438,191.727,52.916)", [156, 192, 53, 255]), 67 | ("rgb(216.423,5.520,180.029,0.589)", [216, 6, 180, 150]), 68 | ("rgb(92.120,247.582,38.473)", [92, 248, 38, 255]), 69 | ("rgb(191.287,191.154,217.583)", [191, 191, 218, 255]), 70 | ("rgb(12.653,152.639,193.900)", [13, 153, 194, 255]), 71 | ("rgb(202.973,184.215,144.797)", [203, 184, 145, 255]), 72 | ("rgb(136.571,182.362,162.541,0.148)", [137, 182, 163, 38]), 73 | ("rgb(242.371,-9.883,94.380)", [242, 0, 94, 255]), 74 | ("rgb(225.423,48.220,245.090,0.771)", [225, 48, 245, 197]), 75 | ("rgb(120.844,198.795,84.888)", [121, 199, 85, 255]), 76 | ("rgb(164.768,213.433,79.101,0.592)", [165, 213, 79, 151]), 77 | ("rgb(38.678,179.795,181.240,0.668)", [39, 180, 181, 170]), 78 | ("rgb(20.568,-4.233,125.669)", [21, 0, 126, 255]), 79 | ("rgb(16.521,145.769,228.928,1.141)", [17, 146, 229, 255]), 80 | ("rgb(167.249,179.288,124.859,0.518)", [167, 179, 125, 132]), 81 | ("rgb(89.103,46.835,144.170)", [89, 47, 144, 255]), 82 | ("rgb(135.687,86.916,220.479,0.668)", [136, 87, 220, 170]), 83 | ("rgb(162.398,-6.010,110.044,0.753)", [162, 0, 110, 192]), 84 | ("rgb(232.790,116.598,58.135)", [233, 117, 58, 255]), 85 | ("rgb(151.480,198.087,129.953,0.205)", [151, 198, 130, 52]), 86 | ("rgb(220.157,183.680,73.321)", [220, 184, 73, 255]), 87 | ("rgb(182.469,204.897,82.469,0.901)", [182, 205, 82, 230]), 88 | ("rgb(4.167,34.557,42.727,0.701)", [4, 35, 43, 179]), 89 | ("rgb(43.038,100.728,14.607,1.052)", [43, 101, 15, 255]), 90 | ("rgb(0.045,150.822,2.053)", [0, 151, 2, 255]), 91 | ("rgb(252.789,220.105,226.842)", [253, 220, 227, 255]), 92 | ("rgb(158.071,262.551,21.134,1.191)", [158, 255, 21, 255]), 93 | ("rgb(233.762,23.326,169.395)", [234, 23, 169, 255]), 94 | ("rgb(231.402,104.070,-9.010,1.065)", [231, 104, 0, 255]), 95 | ("rgb(179.325,103.168,150.187,0.284)", [179, 103, 150, 72]), 96 | ("rgb(139.902,99.310,145.976)", [140, 99, 146, 255]), 97 | ("rgb(50.983,172.151,210.545,0.455)", [51, 172, 211, 116]), 98 | ("rgb(17.199,167.421,194.514,0.667)", [17, 167, 195, 170]), 99 | ("rgb(66.992,32.529,37.688,0.161)", [67, 33, 38, 41]), 100 | ("rgb(224.304,84.063,222.623)", [224, 84, 223, 255]), 101 | ("rgb(203.817,45.691,5.316)", [204, 46, 5, 255]), 102 | ("rgb(154.775,257.630,253.717)", [155, 255, 254, 255]), 103 | ("rgb(113.736,190.662,180.949)", [114, 191, 181, 255]), 104 | ("rgb(198.089,-9.253,145.769)", [198, 0, 146, 255]), 105 | ("rgb(36.705%,17.074%,105.740%,-0.093)", [94, 44, 255, 0]), 106 | ("rgb(36.517%,9.269%,20.384%,-0.136)", [93, 24, 52, 0]), 107 | ("rgb(43.466%,65.604%,55.038%)", [111, 167, 140, 255]), 108 | ("rgb(48.285%,63.684%,56.161%)", [123, 162, 143, 255]), 109 | ("rgb(-4.882%,-3.380%,43.553%)", [0, 0, 111, 255]), 110 | ("rgb(27.307%,75.448%,88.504%)", [70, 192, 226, 255]), 111 | ("rgb(69.694%,59.503%,75.063%)", [178, 152, 191, 255]), 112 | ("rgb(90.960%,12.457%,12.447%)", [232, 32, 32, 255]), 113 | ("rgb(-4.622%,36.627%,-8.178%)", [0, 93, 0, 255]), 114 | ("rgb(87.787%,20.447%,43.423%)", [224, 52, 111, 255]), 115 | ("rgb(67.308%,86.285%,72.392%,0.038)", [172, 220, 185, 10]), 116 | ("rgb(103.478%,75.442%,82.578%,0.746)", [255, 192, 211, 190]), 117 | ("rgb(94.011%,90.970%,79.718%)", [240, 232, 203, 255]), 118 | ("rgb(101.439%,74.849%,45.099%)", [255, 191, 115, 255]), 119 | ("rgb(89.327%,31.033%,44.547%)", [228, 79, 114, 255]), 120 | ("rgb(14.834%,42.672%,105.987%,0.244)", [38, 109, 255, 62]), 121 | ("rgb(21.958%,36.904%,78.661%)", [56, 94, 201, 255]), 122 | ("rgb(-6.690%,87.366%,71.478%)", [0, 223, 182, 255]), 123 | ("rgb(28.576%,39.953%,66.600%)", [73, 102, 170, 255]), 124 | ("rgb(40.806%,108.938%,47.153%,1.186)", [104, 255, 120, 255]), 125 | ("rgb(56.260%,70.797%,1.857%)", [143, 181, 5, 255]), 126 | ("rgb(83.108%,-8.247%,-6.553%,0.081)", [212, 0, 0, 21]), 127 | ("rgb(74.833%,49.474%,93.795%,0.274)", [191, 126, 239, 70]), 128 | ("rgb(68.953%,0.784%,100.538%,0.815)", [176, 2, 255, 208]), 129 | ("rgb(82.811%,4.286%,-7.748%)", [211, 11, 0, 255]), 130 | ("rgb(60.598%,108.140%,92.018%)", [155, 255, 235, 255]), 131 | ("rgb(43.708%,17.419%,-0.824%)", [111, 44, 0, 255]), 132 | ("rgb(25.794%,80.139%,104.639%)", [66, 204, 255, 255]), 133 | ("rgb(38.556%,11.224%,-1.615%)", [98, 29, 0, 255]), 134 | ("rgb(16.455%,48.941%,54.732%,0.187)", [42, 125, 140, 48]), 135 | ("rgb(80.390%,-9.653%,33.214%)", [205, 0, 85, 255]), 136 | ("rgb(79.032%,-1.482%,102.516%)", [202, 0, 255, 255]), 137 | ("rgb(84.366%,31.407%,87.727%)", [215, 80, 224, 255]), 138 | ("rgb(75.236%,101.526%,15.918%)", [192, 255, 41, 255]), 139 | ("rgb(41.238%,3.048%,1.219%)", [105, 8, 3, 255]), 140 | ("rgb(100.594%,5.613%,69.223%,0.190)", [255, 14, 177, 48]), 141 | ("rgb(50.523%,91.169%,-9.786%,-0.006)", [129, 232, 0, 0]), 142 | ("rgb(64.531%,35.250%,34.396%,0.566)", [165, 90, 88, 144]), 143 | ("rgb(29.348%,73.790%,37.770%)", [75, 188, 96, 255]), 144 | ("rgb(33.014%,25.582%,90.210%)", [84, 65, 230, 255]), 145 | ("rgb(-5.318%,-3.605%,13.564%)", [0, 0, 35, 255]), 146 | ("rgb(29.700%,52.430%,-9.889%)", [76, 134, 0, 255]), 147 | ("rgb(11.953%,5.255%,59.970%)", [30, 13, 153, 255]), 148 | ("rgb(76.719%,30.732%,61.515%,0.289)", [196, 78, 157, 74]), 149 | ("rgb(100.532%,96.866%,9.823%,0.475)", [255, 247, 25, 121]), 150 | ("rgb(25.194%,36.877%,67.434%,0.940)", [64, 94, 172, 240]), 151 | ("rgb(6.368%,90.828%,14.714%,0.304)", [16, 232, 38, 78]), 152 | ("rgb(90.081%,-4.819%,37.658%)", [230, 0, 96, 255]), 153 | ("rgb(-1.297%,9.539%,70.162%)", [0, 24, 179, 255]), 154 | ("rgb(47.324%,8.645%,-0.478%)", [121, 22, 0, 255]), 155 | ("hsl(0.069turn,21.307%,-3.478%,1.007)", [0, 0, 0, 255]), 156 | ("hsl(72.142,4.735%,-7.464%,1.042)", [0, 0, 0, 255]), 157 | ("hsl(0.890turn,49.391%,38.567%)", [147, 50, 114, 255]), 158 | ("hsl(3.845grad,68.172%,-6.573%)", [0, 0, 0, 255]), 159 | ("hsl(193.837,21.934%,21.649%)", [43, 62, 67, 255]), 160 | ("hsl(162.965deg,6.690%,44.810%)", [107, 122, 118, 255]), 161 | ("hsl(0.468turn,100.941%,37.582%)", [0, 192, 155, 255]), 162 | ("hsl(0.058turn,51.911%,48.034%,-0.122)", [186, 103, 59, 0]), 163 | ("hsl(120.939,66.051%,77.958%,0.145)", [162, 236, 163, 37]), 164 | ("hsl(72.452deg,63.319%,84.331%)", [230, 240, 190, 255]), 165 | ("hsl(155.120grad,106.379%,58.737%)", [45, 255, 113, 255]), 166 | ("hsl(5.240rad,-4.171%,32.487%)", [83, 83, 83, 255]), 167 | ("hsl(4.565rad,-3.467%,20.547%)", [52, 52, 52, 255]), 168 | ("hsl(295.291,7.475%,3.742%)", [10, 9, 10, 255]), 169 | ("hsl(4.287rad,96.686%,41.460%,1.012)", [23, 4, 208, 255]), 170 | ("hsl(0.283turn,95.539%,97.779%,1.064)", [247, 255, 244, 255]), 171 | ("hsl(2.638rad,37.279%,-9.931%)", [0, 0, 0, 255]), 172 | ("hsl(0.295turn,21.487%,35.668%,0.217)", [80, 110, 71, 55]), 173 | ("hsl(60.183deg,41.823%,22.086%)", [80, 80, 33, 255]), 174 | ("hsl(-0.021rad,64.948%,7.915%)", [33, 7, 8, 255]), 175 | ("hsl(121.677deg,15.914%,19.922%,-0.019)", [43, 59, 43, 0]), 176 | ("hsl(2.829rad,-9.457%,77.556%,-0.068)", [198, 198, 198, 0]), 177 | ("hsl(259.505grad,93.623%,67.416%)", [94, 111, 250, 255]), 178 | ("hsl(0.404rad,-4.398%,104.641%)", [255, 255, 255, 255]), 179 | ("hsl(0.840turn,76.343%,94.714%,0.063)", [252, 231, 251, 16]), 180 | ("hsl(49.836grad,83.982%,3.411%)", [16, 12, 1, 255]), 181 | ("hsl(0.018turn,107.944%,20.884%)", [107, 12, 0, 255]), 182 | ("hsl(0.375turn,22.837%,73.947%)", [173, 204, 181, 255]), 183 | ("hsl(331.324deg,84.224%,61.278%,0.830)", [239, 73, 153, 212]), 184 | ("hsl(2.062rad,60.636%,71.565%)", [141, 226, 139, 255]), 185 | ("hsl(91.718grad,48.490%,44.280%)", [127, 168, 58, 255]), 186 | ( 187 | "hsl(255.829deg,62.751%,107.894%,-0.045)", 188 | [255, 255, 255, 0], 189 | ), 190 | ("hsl(98.567grad,-6.159%,27.281%)", [70, 70, 70, 255]), 191 | ("hsl(1.517rad,83.487%,29.247%)", [81, 137, 12, 255]), 192 | ("hsl(83.494deg,99.500%,61.584%)", [178, 255, 60, 255]), 193 | ("hsl(400.564grad,42.181%,-6.001%,1.145)", [0, 0, 0, 255]), 194 | ("hsl(334.198deg,80.826%,90.951%,0.042)", [251, 213, 229, 11]), 195 | ("hsl(154.176,15.036%,62.757%,0.815)", [146, 174, 162, 208]), 196 | ("hsl(0.029rad,57.145%,72.419%)", [225, 147, 144, 255]), 197 | ("hsl(266.629,25.394%,-1.463%,0.584)", [0, 0, 0, 149]), 198 | ("hsl(5.873rad,105.561%,14.063%)", [72, 0, 28, 255]), 199 | ("hsl(5.731rad,38.296%,105.210%,0.659)", [255, 255, 255, 168]), 200 | ("hsl(327.273,65.108%,71.932%)", [230, 137, 188, 255]), 201 | ("hsl(89.405,36.983%,94.707%)", [242, 246, 237, 255]), 202 | ("hsl(0.925turn,78.035%,97.489%)", [254, 244, 248, 255]), 203 | ("hsl(0.000turn,56.799%,105.320%)", [255, 255, 255, 255]), 204 | ("hsl(2.276rad,107.756%,74.291%,0.290)", [124, 255, 147, 74]), 205 | ("hsl(215.619,25.313%,100.025%,-0.116)", [255, 255, 255, 0]), 206 | ("hsl(41.239,30.040%,7.348%)", [24, 21, 13, 255]), 207 | ("hsl(5.794rad,109.895%,71.423%)", [255, 109, 177, 255]), 208 | ]; 209 | for (s, expected) in test_data { 210 | let rgba = csscolorparser::parse(s).unwrap().to_rgba8(); 211 | assert_eq!(expected, rgba); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /tests/color.rs: -------------------------------------------------------------------------------- 1 | use csscolorparser::Color; 2 | use std::convert::TryFrom; 3 | 4 | #[test] 5 | fn basic() { 6 | let c = Color::new(1.0, 0.0, 0.0, 1.0); 7 | assert_eq!(c.to_array(), [1.0, 0.0, 0.0, 1.0]); 8 | assert_eq!(c.to_rgba8(), [255, 0, 0, 255]); 9 | assert_eq!(c.to_rgba16(), [65535, 0, 0, 65535]); 10 | assert_eq!(c.to_rgba8(), [255, 0, 0, 255]); 11 | assert_eq!(c.to_css_hex(), "#ff0000"); 12 | assert_eq!(c.to_css_rgb(), "rgb(255 0 0)"); 13 | assert_eq!(c.to_string(), "RGBA(1,0,0,1)"); 14 | assert_eq!(c.to_hsva(), [0.0, 1.0, 1.0, 1.0]); 15 | assert_eq!(c.to_hsla(), [0.0, 1.0, 0.5, 1.0]); 16 | assert_eq!(c.to_hwba(), [0.0, 0.0, 0.0, 1.0]); 17 | assert_eq!(c.to_linear_rgba(), [1.0, 0.0, 0.0, 1.0]); 18 | assert_eq!(c.to_linear_rgba_u8(), [255, 0, 0, 255]); 19 | 20 | let c = Color::new(1.0, 0.0, 0.0, 0.5); 21 | assert_eq!(c.to_rgba8(), [255, 0, 0, 128]); 22 | assert_eq!(c.to_css_hex(), "#ff000080"); 23 | assert_eq!(c.to_css_rgb(), "rgb(255 0 0 / 50%)"); 24 | assert_eq!(c.to_string(), "RGBA(1,0,0,0.5)"); 25 | 26 | let c = Color::new(0.0, 1.0, 0.0, 1.0); 27 | assert_eq!(c.to_hsva(), [120.0, 1.0, 1.0, 1.0]); 28 | assert_eq!(c.to_hsla(), [120.0, 1.0, 0.5, 1.0]); 29 | assert_eq!(c.to_hwba(), [120.0, 0.0, 0.0, 1.0]); 30 | 31 | let c = Color::new(0.0, 0.0, 1.0, 1.0); 32 | assert_eq!(c.to_hsva(), [240.0, 1.0, 1.0, 1.0]); 33 | assert_eq!(c.to_hsla(), [240.0, 1.0, 0.5, 1.0]); 34 | assert_eq!(c.to_hwba(), [240.0, 0.0, 0.0, 1.0]); 35 | 36 | let c = Color::new(0.0, 0.0, 0.6, 1.0); 37 | assert_eq!(c.to_hsva(), [240.0, 1.0, 0.6, 1.0]); 38 | assert_eq!(c.to_hsla(), [240.0, 1.0, 0.3, 1.0]); 39 | //assert_eq!(c.to_hwba(), [240.0, 0.0, 0.4, 1.0]); 40 | 41 | let c = Color::new(0.5, 0.5, 0.5, 1.0); 42 | assert_eq!(c.to_hsva(), [0.0, 0.0, 0.5, 1.0]); 43 | assert_eq!(c.to_hsla(), [0.0, 0.0, 0.5, 1.0]); 44 | assert_eq!(c.to_hwba(), [0.0, 0.5, 0.5, 1.0]); 45 | 46 | #[cfg(feature = "lab")] 47 | { 48 | let c = Color::from_laba(0.0, 0.0, 0.0, 1.0); 49 | assert_eq!(c.to_rgba8(), [0, 0, 0, 255]); 50 | 51 | let c = Color::from_laba(100.0, 0.0, 0.0, 1.0); 52 | assert_eq!(c.to_rgba8(), [255, 255, 255, 255]); 53 | 54 | let c = Color::from_lcha(0.0, 0.0, 0.0, 1.0); 55 | assert_eq!(c.to_rgba8(), [0, 0, 0, 255]); 56 | 57 | let c = Color::from_lcha(100.0, 0.0, 0.0, 1.0); 58 | assert_eq!(c.to_rgba8(), [255, 255, 255, 255]); 59 | } 60 | 61 | assert_eq!(Color::default().to_rgba8(), [0, 0, 0, 255]); 62 | 63 | assert_eq!( 64 | Color::try_from("#f00").unwrap().to_rgba8(), 65 | [255, 0, 0, 255] 66 | ); 67 | 68 | assert_eq!( 69 | Color::from((1.0, 0.0, 0.0, 0.5)).to_rgba8(), 70 | [255, 0, 0, 128] 71 | ); 72 | assert_eq!(Color::from((1.0, 0.0, 0.0)).to_rgba8(), [255, 0, 0, 255]); 73 | 74 | assert_eq!(Color::from((255, 0, 0, 128)).to_rgba8(), [255, 0, 0, 128]); 75 | assert_eq!(Color::from((255, 0, 0)).to_rgba8(), [255, 0, 0, 255]); 76 | 77 | assert_eq!( 78 | Color::from([1.0, 0.0, 0.0, 0.5]).to_rgba8(), 79 | [255, 0, 0, 128] 80 | ); 81 | assert_eq!(Color::from([1.0, 0.0, 0.0]).to_rgba8(), [255, 0, 0, 255]); 82 | 83 | assert_eq!(Color::from([255, 0, 0, 128]).to_rgba8(), [255, 0, 0, 128]); 84 | assert_eq!(Color::from([255, 0, 0]).to_rgba8(), [255, 0, 0, 255]); 85 | 86 | assert_eq!( 87 | Color::from([0.0_f32, 1.0, 0.5, 1.0]).to_rgba8(), 88 | [0, 255, 128, 255] 89 | ); 90 | assert_eq!( 91 | Color::from([0.0_f32, 1.0, 0.5]).to_rgba8(), 92 | [0, 255, 128, 255] 93 | ); 94 | 95 | // clamping 96 | 97 | let c = Color::new(1.23, 0.5, -0.01, 1.01); 98 | assert_eq!([c.r, c.g, c.b, c.a], [1.23, 0.5, -0.01, 1.01]); 99 | assert_eq!(c.to_array(), [1.0, 0.5, 0.0, 1.0]); 100 | assert_eq!(c.to_rgba8(), [255, 128, 0, 255]); 101 | assert_eq!(c.to_rgba16(), [65535, 32768, 0, 65535]); 102 | 103 | let c = Color::new(1.23, 0.5, -0.01, 1.01).clamp(); 104 | assert_eq!([c.r, c.g, c.b, c.a], [1.0, 0.5, 0.0, 1.0]); 105 | } 106 | 107 | #[test] 108 | fn parser() { 109 | use std::str::FromStr; 110 | let test_data = ["#71fe15", "#d6e3c9", "#2a7719", "#b53717", "#5b0b8d"]; 111 | for s in test_data { 112 | let c = Color::from_str(s).unwrap(); 113 | assert_eq!(c.to_css_hex(), s); 114 | 115 | let c = Color::try_from(s).unwrap(); 116 | assert_eq!(c.to_css_hex(), s); 117 | 118 | let c = Color::try_from(s.to_string()).unwrap(); 119 | assert_eq!(c.to_css_hex(), s); 120 | } 121 | } 122 | 123 | #[test] 124 | fn convert_colors() { 125 | let colors = &[ 126 | //Color::new(1.0, 0.7, 0.1, 1.0), // 127 | Color::from_rgba8(255, 179, 26, 255), 128 | Color::from_rgba8(10, 255, 125, 0), 129 | Color::from_linear_rgba(0.1, 0.9, 1.0, 1.0), 130 | Color::from_hwba(0.0, 0.0, 0.0, 1.0), 131 | Color::from_hwba(320.0, 0.1, 0.3, 1.0), 132 | Color::from_hsva(120.0, 0.3, 0.2, 0.1), 133 | Color::from_hsla(120.0, 0.3, 0.2, 1.0), 134 | ]; 135 | for (i, col) in colors.iter().enumerate() { 136 | println!("{i} -> {}, {}", &col.to_css_hex(), &col.to_css_rgb()); 137 | 138 | let [a, b, c, d] = col.to_linear_rgba(); 139 | let x = Color::from_linear_rgba(a, b, c, d); 140 | assert_eq!(&col.to_css_hex(), &x.to_css_hex()); 141 | 142 | let [a, b, c, d] = col.to_oklaba(); 143 | let x = Color::from_oklaba(a, b, c, d); 144 | assert_eq!(&col.to_css_hex(), &x.to_css_hex()); 145 | } 146 | 147 | let data = &[ 148 | "#000000", 149 | "#ffffff", 150 | "#999999", 151 | "#7f7f7f", 152 | "#ff0000", 153 | "#fa8072", 154 | "#87ceeb", 155 | "#ff6347", 156 | "#ee82ee", 157 | "#9acd32", 158 | "#0aff7d", 159 | "#09ff7d", 160 | "#ffb31a", 161 | "#0aff7d", 162 | "#09ff7d", 163 | "#825dfa6d", 164 | "#abc5679b", 165 | ]; 166 | for s in data { 167 | let col = csscolorparser::parse(s).unwrap(); 168 | assert_eq!(s, &col.to_css_hex()); 169 | 170 | let [a, b, c, d] = col.to_rgba8(); 171 | let x = Color::from_rgba8(a, b, c, d); 172 | assert_eq!(s, &x.to_css_hex()); 173 | 174 | let [a, b, c, d] = col.to_hsva(); 175 | let x = Color::from_hsva(a, b, c, d); 176 | assert_eq!(s, &x.to_css_hex()); 177 | 178 | let [a, b, c, d] = col.to_hsla(); 179 | let x = Color::from_hsla(a, b, c, d); 180 | assert_eq!(s, &x.to_css_hex()); 181 | 182 | let [a, b, c, d] = col.to_hwba(); 183 | let x = Color::from_hwba(a, b, c, d); 184 | assert_eq!(s, &x.to_css_hex()); 185 | 186 | let [a, b, c, d] = col.to_linear_rgba(); 187 | let x = Color::from_linear_rgba(a, b, c, d); 188 | assert_eq!(s, &x.to_css_hex()); 189 | 190 | let [a, b, c, d] = col.to_oklaba(); 191 | let x = Color::from_oklaba(a, b, c, d); 192 | assert_eq!(s, &x.to_css_hex()); 193 | 194 | #[cfg(feature = "lab")] 195 | { 196 | let [a, b, c, d] = col.to_laba(); 197 | let x = Color::from_laba(a, b, c, d); 198 | assert_eq!(s, &x.to_css_hex()); 199 | 200 | let [a, b, c, d] = col.to_lcha(); 201 | let x = Color::from_lcha(a, b, c, d); 202 | assert_eq!(s, &x.to_css_hex()); 203 | } 204 | } 205 | } 206 | 207 | #[test] 208 | fn red() { 209 | let data = &[ 210 | Color::new(1.0, 0.0, 0.0, 1.0), 211 | Color::from_rgba8(255, 0, 0, 255), 212 | Color::from_linear_rgba(1.0, 0.0, 0.0, 1.0), 213 | Color::from_linear_rgba8(255, 0, 0, 255), 214 | Color::from_hsva(0.0, 1.0, 1.0, 1.0), 215 | Color::from_hsla(360.0, 1.0, 0.5, 1.0), 216 | Color::from_hwba(0.0, 0.0, 0.0, 1.0), 217 | Color::from_oklaba( 218 | 0.6279151939969809, 219 | 0.2249032308661071, 220 | 0.12580287012451802, 221 | 1.0, 222 | ), 223 | Color::from_html("#f00").unwrap(), 224 | Color::from_html("hsv(360,100%,100%)").unwrap(), 225 | ]; 226 | for c in data { 227 | assert_eq!(c.to_rgba8(), [255, 0, 0, 255]); 228 | } 229 | } 230 | 231 | #[test] 232 | fn interpolate() { 233 | let a = Color::new(0.0, 1.0, 0.0, 1.0); 234 | let b = Color::new(0.0, 0.0, 1.0, 1.0); 235 | 236 | assert_eq!(a.interpolate_rgb(&b, 0.0).to_rgba8(), [0, 255, 0, 255]); 237 | assert_eq!(a.interpolate_rgb(&b, 0.5).to_rgba8(), [0, 128, 128, 255]); 238 | assert_eq!(a.interpolate_rgb(&b, 1.0).to_rgba8(), [0, 0, 255, 255]); 239 | 240 | assert_eq!(b.interpolate_rgb(&a, 0.0).to_rgba8(), [0, 0, 255, 255]); 241 | assert_eq!(b.interpolate_rgb(&a, 0.5).to_rgba8(), [0, 128, 128, 255]); 242 | assert_eq!(b.interpolate_rgb(&a, 1.0).to_rgba8(), [0, 255, 0, 255]); 243 | 244 | assert_eq!( 245 | a.interpolate_linear_rgb(&b, 0.0).to_rgba8(), 246 | [0, 255, 0, 255] 247 | ); 248 | assert_eq!( 249 | a.interpolate_linear_rgb(&b, 0.5).to_rgba8(), 250 | [0, 188, 188, 255] 251 | ); 252 | assert_eq!( 253 | a.interpolate_linear_rgb(&b, 1.0).to_rgba8(), 254 | [0, 0, 255, 255] 255 | ); 256 | 257 | assert_eq!(a.interpolate_hsv(&b, 0.0).to_rgba8(), [0, 255, 0, 255]); 258 | assert_eq!(a.interpolate_hsv(&b, 0.5).to_rgba8(), [0, 255, 255, 255]); 259 | assert_eq!(a.interpolate_hsv(&b, 1.0).to_rgba8(), [0, 0, 255, 255]); 260 | 261 | assert_eq!(a.interpolate_oklab(&b, 0.0).to_rgba8(), [0, 255, 0, 255]); 262 | assert_eq!(a.interpolate_oklab(&b, 0.5).to_rgba8(), [0, 170, 191, 255]); 263 | assert_eq!(a.interpolate_oklab(&b, 1.0).to_rgba8(), [0, 0, 255, 255]); 264 | 265 | #[cfg(feature = "lab")] 266 | { 267 | assert_eq!(a.interpolate_lab(&b, 0.0).to_rgba8(), [0, 255, 0, 255]); 268 | assert_eq!(a.interpolate_lab(&b, 1.0).to_rgba8(), [0, 0, 255, 255]); 269 | 270 | assert_eq!(a.interpolate_lch(&b, 0.0).to_rgba8(), [0, 255, 0, 255]); 271 | assert_eq!(a.interpolate_lch(&b, 1.0).to_rgba8(), [0, 0, 255, 255]); 272 | } 273 | } 274 | 275 | #[test] 276 | fn const_fn() { 277 | #![allow(dead_code)] 278 | 279 | const C: Color = Color::new(1.0, 0.0, 0.0, 1.0); 280 | const _: Color = Color::from_rgba8(255, 0, 0, 255); 281 | const _: Color = Color::from_hsla(0.0, 1.0, 1.0, 1.0); 282 | const _: Color = Color::from_hsva(0.0, 1.0, 1.0, 1.0); 283 | const _: Color = Color::from_hwba(0.0, 0.0, 0.3, 1.0); 284 | 285 | const _: Color = C.clamp(); 286 | 287 | const _: [f32; 4] = C.to_array(); 288 | const _: [u8; 4] = C.to_rgba8(); 289 | const _: [u16; 4] = C.to_rgba16(); 290 | const _: [f32; 4] = C.to_hsla(); 291 | const _: [f32; 4] = C.to_hsva(); 292 | const _: [f32; 4] = C.to_hwba(); 293 | 294 | const A: Color = Color::new(0.0, 0.0, 0.0, 1.0); 295 | 296 | const _: Color = A.interpolate_rgb(&C, 0.75); 297 | const _: Color = A.interpolate_hsv(&C, 0.75); 298 | } 299 | -------------------------------------------------------------------------------- /tests/firefox.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn random_colors() { 3 | // The color string is randomly generated, then parsed using Mozilla Firefox 84.0.2 4 | let test_data = vec![ 5 | ("#30758CE9", [48, 117, 140, 233]), 6 | ("#91F76A89", [145, 247, 106, 137]), 7 | ("#CF0206", [207, 2, 6, 255]), 8 | ("#5BD6AC", [91, 214, 172, 255]), 9 | ("#86AFB7FB", [134, 175, 183, 251]), 10 | ("#562", [85, 102, 34, 255]), 11 | ("#67030D", [103, 3, 13, 255]), 12 | ("#52DE306C", [82, 222, 48, 108]), 13 | ("#3425", [51, 68, 34, 85]), 14 | ("#3CF775", [60, 247, 117, 255]), 15 | ("#46D6DAC3", [70, 214, 218, 195]), 16 | ("#5AE", [85, 170, 238, 255]), 17 | ("#6900", [102, 153, 0, 0]), 18 | ("#266A6B69", [38, 106, 107, 105]), 19 | ("#D49C7F", [212, 156, 127, 255]), 20 | ("#29CEDDF2", [41, 206, 221, 242]), 21 | ("#E99", [238, 153, 153, 255]), 22 | ("#462FCA15", [70, 47, 202, 21]), 23 | ("#5ECAB8", [94, 202, 184, 255]), 24 | ("#663A77D6", [102, 58, 119, 214]), 25 | ("#4952", [68, 153, 85, 34]), 26 | ("#427273", [66, 114, 115, 255]), 27 | ("#9866", [153, 136, 102, 102]), 28 | ("#7206AC1C", [114, 6, 172, 28]), 29 | ("#03E9", [0, 51, 238, 153]), 30 | ("#8AB779", [138, 183, 121, 255]), 31 | ("#B6EFB9", [182, 239, 185, 255]), 32 | ("#735E8A", [115, 94, 138, 255]), 33 | ("#F45", [255, 68, 85, 255]), 34 | ("#E8E9", [238, 136, 238, 153]), 35 | ("#67B9FB", [103, 185, 251, 255]), 36 | ("#846A1C", [132, 106, 28, 255]), 37 | ("#DFF17C", [223, 241, 124, 255]), 38 | ("#5CB7", [85, 204, 187, 119]), 39 | ("#895191", [137, 81, 145, 255]), 40 | ("#B05ACA00", [176, 90, 202, 0]), 41 | ("#303B", [51, 0, 51, 187]), 42 | ("#E02D", [238, 0, 34, 221]), 43 | ("#BD202974", [189, 32, 41, 116]), 44 | ("#54F5D0", [84, 245, 208, 255]), 45 | ("#52F24795", [82, 242, 71, 149]), 46 | ("#2BC5", [34, 187, 204, 85]), 47 | ("#8AA7", [136, 170, 170, 119]), 48 | ("#53F55F", [83, 245, 95, 255]), 49 | ("#A4DA", [170, 68, 221, 170]), 50 | ("#CE3E", [204, 238, 51, 238]), 51 | ("#9F7", [153, 255, 119, 255]), 52 | ("#85418C", [133, 65, 140, 255]), 53 | ("#D467", [221, 68, 102, 119]), 54 | ("#D83", [221, 136, 51, 255]), 55 | ("rgb(144.894,221.944,15.338,1.042)", [145, 222, 15, 255]), 56 | ("rgb(172.519,104.262,195.385)", [173, 104, 195, 255]), 57 | ("rgb(197.660,22.336,220.613)", [198, 22, 221, 255]), 58 | ("rgb(172.049,149.391,83.459)", [172, 149, 83, 255]), 59 | ("rgb(15.808,118.246,48.177)", [16, 118, 48, 255]), 60 | ("rgb(110.224,15.299,118.110)", [110, 15, 118, 255]), 61 | ("rgb(61.657,132.868,241.810)", [62, 133, 242, 255]), 62 | ("rgb(109.629,130.088,177.988,0.263)", [110, 130, 178, 67]), 63 | ("rgb(230.646,136.749,248.448,0.049)", [231, 137, 248, 12]), 64 | ("rgb(-7.621,134.924,115.641)", [0, 135, 116, 255]), 65 | ("rgb(26.009,108.413,240.710)", [26, 108, 241, 255]), 66 | ("rgb(21.680,232.551,168.863)", [22, 233, 169, 255]), 67 | ("rgb(111.999,67.957,205.788,1.105)", [112, 68, 206, 255]), 68 | ("rgb(63.877,198.919,221.669,-0.010)", [64, 199, 222, 0]), 69 | ("rgb(142.226,135.238,260.621)", [142, 135, 255, 255]), 70 | ("rgb(59.970,110.510,182.595,-0.069)", [60, 111, 183, 0]), 71 | ("rgb(-2.286,254.610,235.853)", [0, 255, 236, 255]), 72 | ("rgb(-7.958,193.044,235.101)", [0, 193, 235, 255]), 73 | ("rgb(256.609,159.324,257.998)", [255, 159, 255, 255]), 74 | ("rgb(109.388,91.292,241.384)", [109, 91, 241, 255]), 75 | ("rgb(30.402,200.071,253.355)", [30, 200, 253, 255]), 76 | ("rgb(254.429,18.713,262.391)", [254, 19, 255, 255]), 77 | ("rgb(58.353,74.252,20.484,0.240)", [58, 74, 20, 61]), 78 | ("rgb(175.711,57.423,190.797)", [176, 57, 191, 255]), 79 | ("rgb(258.310,48.529,209.761)", [255, 49, 210, 255]), 80 | ("rgb(42.267,221.449,78.031)", [42, 221, 78, 255]), 81 | ("rgb(35.229,195.388,257.837)", [35, 195, 255, 255]), 82 | ("rgb(-1.089,245.407,-3.070,0.320)", [0, 245, 0, 82]), 83 | ("rgb(62.510,251.003,18.592)", [63, 251, 19, 255]), 84 | ("rgb(30.376,73.291,147.311)", [30, 73, 147, 255]), 85 | ("rgb(129.732,260.759,31.529)", [130, 255, 32, 255]), 86 | ("rgb(197.675,25.346,77.167,-0.005)", [198, 25, 77, 0]), 87 | ("rgb(132.191,40.666,142.643)", [132, 41, 143, 255]), 88 | ("rgb(72.049,262.173,213.300)", [72, 255, 213, 255]), 89 | ("rgb(168.366,214.806,197.234)", [168, 215, 197, 255]), 90 | ("rgb(233.079,33.094,211.204)", [233, 33, 211, 255]), 91 | ("rgb(241.575,256.514,243.212,0.915)", [242, 255, 243, 233]), 92 | ("rgb(192.030,70.554,5.442)", [192, 71, 5, 255]), 93 | ("rgb(214.761,181.138,1.594)", [215, 181, 2, 255]), 94 | ("rgb(137.246,159.092,175.243,0.412)", [137, 159, 175, 105]), 95 | ("rgb(-6.852,94.340,165.562,1.115)", [0, 94, 166, 255]), 96 | ("rgb(27.688,165.299,256.621)", [28, 165, 255, 255]), 97 | ("rgb(240.314,99.632,-3.909,0.214)", [240, 100, 0, 55]), 98 | ("rgb(62.626,96.898,45.764,0.580)", [63, 97, 46, 148]), 99 | ("rgb(101.295,61.291,109.920)", [101, 61, 110, 255]), 100 | ("rgb(250.755,243.673,73.583)", [251, 244, 74, 255]), 101 | ("rgb(87.908,173.653,236.399,0.100)", [88, 174, 236, 26]), 102 | ("rgb(1.276,63.625,76.657)", [1, 64, 77, 255]), 103 | ("rgb(182.622,81.426,107.665)", [183, 81, 108, 255]), 104 | ("rgb(97.666,126.808,202.343)", [98, 127, 202, 255]), 105 | ("rgb(19.077%,96.844%,107.049%)", [49, 247, 255, 255]), 106 | ("rgb(60.527%,20.266%,24.231%)", [154, 52, 62, 255]), 107 | ("rgb(-8.659%,45.149%,39.844%)", [0, 115, 102, 255]), 108 | ("rgb(72.862%,88.329%,41.681%)", [186, 225, 106, 255]), 109 | ("rgb(71.619%,55.033%,109.121%)", [183, 140, 255, 255]), 110 | ("rgb(8.320%,59.542%,86.342%,0.535)", [21, 152, 220, 136]), 111 | ("rgb(71.371%,30.476%,87.487%)", [182, 78, 223, 255]), 112 | ("rgb(72.932%,55.921%,36.855%)", [186, 143, 94, 255]), 113 | ("rgb(102.716%,16.386%,95.450%)", [255, 42, 243, 255]), 114 | ("rgb(31.319%,7.413%,83.436%,1.067)", [80, 19, 213, 255]), 115 | ("rgb(30.471%,106.110%,14.952%)", [78, 255, 38, 255]), 116 | ("rgb(73.746%,53.000%,70.974%)", [188, 135, 181, 255]), 117 | ("rgb(53.236%,76.453%,51.996%)", [136, 195, 133, 255]), 118 | ("rgb(74.430%,-9.053%,40.591%,0.372)", [190, 0, 104, 95]), 119 | ("rgb(63.771%,56.927%,61.386%,0.867)", [163, 145, 157, 221]), 120 | ("rgb(82.193%,76.039%,85.366%)", [210, 194, 218, 255]), 121 | ("rgb(-8.062%,84.997%,52.049%)", [0, 217, 133, 255]), 122 | ("rgb(87.040%,18.561%,51.447%)", [222, 47, 131, 255]), 123 | ("rgb(-0.383%,-7.852%,59.846%,0.606)", [0, 0, 153, 155]), 124 | ("rgb(-5.271%,29.630%,24.221%)", [0, 76, 62, 255]), 125 | ("rgb(37.412%,-9.639%,28.126%)", [95, 0, 72, 255]), 126 | ("rgb(49.021%,98.510%,38.101%,0.498)", [125, 251, 97, 127]), 127 | ("rgb(32.297%,10.427%,9.294%,1.078)", [82, 27, 24, 255]), 128 | ("rgb(66.507%,95.944%,5.022%,0.846)", [170, 245, 13, 216]), 129 | ("rgb(61.872%,37.070%,49.019%)", [158, 95, 125, 255]), 130 | ("rgb(71.738%,44.537%,91.829%)", [183, 114, 234, 255]), 131 | ("rgb(103.206%,32.133%,45.463%)", [255, 82, 116, 255]), 132 | ("rgb(72.780%,94.460%,32.319%)", [186, 241, 82, 255]), 133 | ("rgb(1.399%,0.124%,59.130%,0.668)", [4, 0, 151, 170]), 134 | ("rgb(14.442%,4.097%,14.401%,-0.092)", [37, 10, 37, 0]), 135 | ("rgb(104.800%,58.989%,13.037%)", [255, 150, 33, 255]), 136 | ("rgb(98.126%,78.825%,105.629%,0.673)", [250, 201, 255, 172]), 137 | ("rgb(53.607%,90.566%,100.766%,1.127)", [137, 231, 255, 255]), 138 | ("rgb(35.961%,2.549%,12.719%)", [92, 6, 32, 255]), 139 | ("rgb(47.649%,104.780%,20.425%,0.936)", [122, 255, 52, 239]), 140 | ("rgb(19.942%,54.172%,31.089%,1.077)", [51, 138, 79, 255]), 141 | ("rgb(70.314%,108.126%,96.525%,0.513)", [179, 255, 246, 131]), 142 | ("rgb(49.348%,-1.456%,15.051%)", [126, 0, 38, 255]), 143 | ("rgb(13.047%,34.342%,99.376%)", [33, 88, 253, 255]), 144 | ("rgb(21.879%,37.037%,34.846%)", [56, 94, 89, 255]), 145 | ("rgb(18.765%,3.997%,17.193%,0.598)", [48, 10, 44, 152]), 146 | ("rgb(54.629%,55.177%,51.436%,-0.123)", [139, 141, 131, 0]), 147 | ("rgb(8.356%,-0.583%,74.565%)", [21, 0, 190, 255]), 148 | ("rgb(2.435%,74.014%,16.351%,0.580)", [6, 189, 42, 148]), 149 | ("rgb(25.820%,80.885%,85.077%)", [66, 206, 217, 255]), 150 | ("rgb(-4.774%,54.466%,34.484%,1.139)", [0, 139, 88, 255]), 151 | ("rgb(24.928%,80.365%,48.506%)", [64, 205, 124, 255]), 152 | ("rgb(43.466%,12.333%,73.637%,0.170)", [111, 31, 188, 43]), 153 | ("rgb(26.659%,24.469%,51.155%,0.592)", [68, 62, 130, 151]), 154 | ("rgb(102.749%,16.226%,13.440%,0.542)", [255, 41, 34, 138]), 155 | ( 156 | "hsl(157.572grad,22.057%,82.352%,0.748)", 157 | [200, 220, 207, 191], 158 | ), 159 | ("hsl(359.534grad,99.302%,24.820%)", [126, 0, 77, 255]), 160 | ("hsl(89.381deg,0.820%,2.238%,1.184)", [6, 6, 6, 255]), 161 | ("hsl(3.435rad,9.931%,1.830%,0.688)", [4, 5, 5, 175]), 162 | ("hsl(269.781deg,72.977%,16.749%,0.919)", [42, 12, 74, 234]), 163 | ("hsl(0.231turn,25.156%,-8.804%)", [0, 0, 0, 255]), 164 | ("hsl(362.732grad,71.384%,89.611%)", [247, 210, 231, 255]), 165 | ("hsl(8.019deg,22.561%,21.216%)", [66, 45, 42, 255]), 166 | ("hsl(80.737grad,88.920%,-0.182%)", [0, 0, 0, 255]), 167 | ("hsl(3.901rad,104.403%,100.043%)", [255, 255, 255, 255]), 168 | ("hsl(127.668deg,38.344%,95.249%)", [238, 248, 239, 255]), 169 | ("hsl(276.662grad,32.442%,77.971%)", [186, 181, 217, 255]), 170 | ("hsl(5.491rad,65.601%,107.670%)", [255, 255, 255, 255]), 171 | ("hsl(92.909grad,102.247%,91.380%)", [238, 255, 211, 255]), 172 | ("hsl(236.020,98.201%,19.861%,-0.065)", [1, 8, 100, 0]), 173 | ("hsl(356.944grad,18.231%,70.131%)", [193, 165, 183, 255]), 174 | ("hsl(347.581grad,91.415%,35.258%,0.479)", [172, 8, 137, 122]), 175 | ("hsl(200.437deg,81.639%,56.948%,0.017)", [56, 174, 235, 4]), 176 | ("hsl(96.598,-1.219%,80.187%)", [204, 204, 204, 255]), 177 | ("hsl(162.463,35.025%,9.764%,1.000)", [16, 34, 29, 255]), 178 | ("hsl(97.785grad,42.878%,49.733%,0.540)", [130, 181, 72, 138]), 179 | ("hsl(0.243turn,61.733%,36.754%)", [99, 152, 36, 255]), 180 | ("hsl(81.865grad,93.755%,100.434%)", [255, 255, 255, 255]), 181 | ("hsl(0.846turn,12.520%,78.873%,-0.121)", [208, 194, 207, 0]), 182 | ("hsl(1.003turn,-3.924%,48.828%)", [125, 125, 125, 255]), 183 | ("hsl(262.728deg,25.894%,50.198%)", [120, 95, 161, 255]), 184 | ("hsl(187.679,4.225%,90.298%,0.974)", [229, 231, 231, 248]), 185 | ("hsl(0.301turn,-6.189%,34.167%)", [87, 87, 87, 255]), 186 | ("hsl(0.268turn,91.972%,89.764%)", [224, 253, 205, 255]), 187 | ("hsl(247.074grad,78.542%,14.539%)", [8, 25, 66, 255]), 188 | ("hsl(46.282grad,37.538%,92.244%)", [243, 238, 228, 255]), 189 | ("hsl(0.391turn,4.084%,67.843%)", [170, 176, 172, 255]), 190 | ("hsl(-0.088turn,61.468%,95.887%)", [251, 238, 245, 255]), 191 | ("hsl(0.386turn,59.601%,19.961%,-0.102)", [21, 81, 40, 0]), 192 | ("hsl(310.087,43.917%,93.640%)", [246, 232, 244, 255]), 193 | ("hsl(59.767grad,17.209%,-3.545%)", [0, 0, 0, 255]), 194 | ("hsl(-7.789grad,91.197%,40.446%)", [197, 9, 31, 255]), 195 | ("hsl(99.724deg,44.247%,91.630%,0.575)", [231, 243, 224, 147]), 196 | ("hsl(0.129rad,100.953%,88.130%,0.208)", [255, 202, 194, 53]), 197 | ("hsl(28.333grad,92.866%,94.350%,0.225)", [254, 239, 227, 57]), 198 | ("hsl(6.278rad,13.724%,109.460%,0.388)", [255, 255, 255, 99]), 199 | ("hsl(0.526turn,79.719%,57.535%)", [60, 206, 233, 255]), 200 | ("hsl(299.379,49.286%,95.788%)", [249, 239, 250, 255]), 201 | ("hsl(198.214,22.384%,26.807%,0.047)", [53, 74, 84, 12]), 202 | ("hsl(140.760,-9.660%,9.079%)", [23, 23, 23, 255]), 203 | ("hsl(0.342turn,38.150%,26.553%,0.570)", [42, 94, 45, 145]), 204 | ("hsl(235.559deg,-1.789%,81.781%)", [209, 209, 209, 255]), 205 | ("hsl(-0.400grad,100.928%,31.366%,-0.121)", [160, 0, 1, 0]), 206 | ("hsl(41.989grad,-8.040%,83.940%)", [214, 214, 214, 255]), 207 | ("hsl(327.089grad,41.748%,17.920%)", [61, 27, 65, 255]), 208 | ("hwb(5.093rad 16.228% 7.107% / 0.995)", [210, 41, 237, 254]), 209 | ("hwb(0.913turn 23.380% 28.693%)", [182, 60, 123, 255]), 210 | ( 211 | "hwb(0.083turn 48.957% 19.454% / -0.055)", 212 | [205, 165, 125, 0], 213 | ), 214 | ("hwb(223.305 46.995% 33.460% / -0.095)", [120, 134, 170, 0]), 215 | ("hwb(93.679grad 14.835% 5.257%)", [159, 242, 38, 255]), 216 | ( 217 | "hwb(208.953grad 43.974% 17.786% / 49%)", 218 | [112, 197, 210, 125], 219 | ), 220 | ("hwb(168.267 8.049% 1.527%)", [21, 251, 206, 255]), 221 | ("hwb(260.943 27.267% 43.541%)", [96, 70, 144, 255]), 222 | ("hwb(0.485turn 46.539% 10.643%)", [119, 228, 218, 255]), 223 | ("hwb(212.179deg 31.054% 9.752%)", [79, 149, 230, 255]), 224 | ("hwb(-0.070turn 14.978% 9.707%)", [230, 38, 119, 255]), 225 | ("hwb(0.073turn 36.215% 43.333% / -40%)", [145, 115, 92, 0]), 226 | ("hwb(0.162turn 49.627% 28.784%)", [182, 180, 127, 255]), 227 | ("hwb(0.243turn 20.723% 13.575%)", [144, 220, 53, 255]), 228 | ("hwb(24.098deg 10.817% 49.419%)", [129, 68, 28, 255]), 229 | ("hwb(231.600deg 9.597% 14.013% / 3%)", [24, 52, 219, 8]), 230 | ("hwb(203.254deg 18.262% 3.646%)", [47, 169, 246, 255]), 231 | ("hwb(153.756deg 48.303% 33.045%)", [123, 171, 150, 255]), 232 | ("hwb(298.912grad 22.529% 13.786%)", [136, 57, 220, 255]), 233 | ("hwb(185.717deg 5.163% 31.175%)", [13, 160, 176, 255]), 234 | ("hwb(211.980 1.733% 4.655% / -52%)", [4, 116, 243, 0]), 235 | ("hwb(204.276 30.754% 10.146%)", [78, 168, 229, 255]), 236 | ( 237 | "hwb(0.953turn 48.769% 4.335% / 0.684)", 238 | [244, 124, 158, 174], 239 | ), 240 | ("hwb(2.465rad 16.911% 11.363%)", [43, 226, 108, 255]), 241 | ("hwb(58.908deg 29.545% 12.057% / 7%)", [224, 222, 75, 18]), 242 | ("hwb(6.344rad 49.227% 42.409%)", [147, 127, 126, 255]), 243 | ("hwb(0.255turn 29.724% 27.689% / 45%)", [127, 184, 76, 115]), 244 | ("hwb(328.563deg 20.347% 38.896%)", [156, 52, 106, 255]), 245 | ("hwb(2.157deg 15.367% 13.797% / 0.761)", [220, 46, 39, 194]), 246 | ("hwb(290.068grad 13.739% 49.438%)", [68, 35, 129, 255]), 247 | ("hwb(3.523 0.171% 7.495%)", [236, 14, 0, 255]), 248 | ("hwb(0.228turn 7.261% 38.296% / -28%)", [106, 157, 19, 0]), 249 | ("hwb(114.298deg 2.263% 48.814% / 115%)", [18, 131, 6, 255]), 250 | ("hwb(6.000rad 29.569% 37.733% / -90%)", [159, 75, 98, 0]), 251 | ("hwb(293.975 48.728% 30.547%)", [172, 124, 177, 255]), 252 | ("hwb(3.861rad 5.194% 22.537%)", [13, 71, 198, 255]), 253 | ("hwb(363.051grad 39.733% 20.035%)", [204, 101, 158, 255]), 254 | ("hwb(0.046turn 4.758% 4.128%)", [244, 76, 12, 255]), 255 | ("hwb(198.156 40.979% 25.203% / 47%)", [104, 165, 191, 120]), 256 | ("hwb(169.283 15.477% 3.858% / -79%)", [39, 245, 208, 0]), 257 | ("hwb(6.282rad 36.718% 12.765% / 27%)", [222, 94, 94, 69]), 258 | ("hwb(4.590rad 20.571% 19.423%)", [111, 52, 205, 255]), 259 | ("hwb(252.979grad 3.471% 13.610% / 0.626)", [9, 52, 220, 160]), 260 | ( 261 | "hwb(0.738turn 12.619% 22.873% / 0.498)", 262 | [103, 32, 197, 127], 263 | ), 264 | ("hwb(210.399grad 1.847% 38.094%)", [5, 134, 158, 255]), 265 | ("hwb(143.863 35.042% 31.195% / 0.378)", [89, 175, 124, 96]), 266 | ( 267 | "hwb(21.631grad 41.743% 24.160% / 47%)", 268 | [193, 135, 106, 120], 269 | ), 270 | ("hwb(84.952grad 41.859% 39.539%)", [141, 154, 107, 255]), 271 | ("hwb(37.442 2.103% 9.857%)", [230, 145, 5, 255]), 272 | ("hwb(135.379grad 5.905% 8.483% / 76%)", [15, 233, 22, 194]), 273 | ]; 274 | for (s, expected) in test_data { 275 | let rgba = csscolorparser::parse(s).unwrap().to_rgba8(); 276 | assert_eq!(expected, rgba); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /tests/named_colors.rs: -------------------------------------------------------------------------------- 1 | use csscolorparser::{parse, Color, NAMED_COLORS}; 2 | 3 | #[test] 4 | fn named_colors() { 5 | let skip_list = ["aqua", "cyan", "fuchsia", "magenta"]; 6 | 7 | for (&name, &rgb) in NAMED_COLORS.entries() { 8 | let c = parse(name).unwrap(); 9 | assert_eq!(c.to_rgba8()[0..3], rgb); 10 | 11 | if skip_list.contains(&name) || name.contains("gray") || name.contains("grey") { 12 | continue; 13 | } 14 | assert_eq!(c.name(), Some(name)); 15 | 16 | let [r, g, b] = rgb; 17 | let c = Color::from_rgba8(r, g, b, 255); 18 | assert_eq!(c.name(), Some(name)); 19 | } 20 | 21 | // Case-insensitive tests 22 | 23 | macro_rules! cmp { 24 | ($a:expr, $b:expr) => { 25 | assert_eq!(parse($a).unwrap().to_rgba8(), parse($b).unwrap().to_rgba8()); 26 | }; 27 | } 28 | 29 | cmp!("red", "RED"); 30 | cmp!("red", "Red"); 31 | cmp!("skyblue", "SKYBLUE"); 32 | cmp!("skyblue", "SkyBlue"); 33 | 34 | // Hex 35 | 36 | #[rustfmt::skip] 37 | let test_data = [ 38 | ("aliceblue", "#f0f8ff"), 39 | ("bisque", "#ffe4c4"), 40 | ("black", "#000000"), 41 | ("chartreuse", "#7fff00"), 42 | ("coral", "#ff7f50"), 43 | ("crimson", "#dc143c"), 44 | ("dodgerblue", "#1e90ff"), 45 | ("firebrick", "#b22222"), 46 | ("gold", "#ffd700"), 47 | ("hotpink", "#ff69b4"), 48 | ("indigo", "#4b0082"), 49 | ("lavender", "#e6e6fa"), 50 | ("lime", "#00ff00"), 51 | ("plum", "#dda0dd"), 52 | ("red", "#ff0000"), 53 | ("salmon", "#fa8072"), 54 | ("skyblue", "#87ceeb"), 55 | ("tomato", "#ff6347"), 56 | ("violet", "#ee82ee"), 57 | ("yellowgreen", "#9acd32"), 58 | ]; 59 | 60 | for (name, hex) in test_data { 61 | let c = csscolorparser::parse(name).unwrap(); 62 | assert_eq!(c.to_css_hex(), hex); 63 | 64 | let c = csscolorparser::parse(hex).unwrap(); 65 | assert_eq!(c.name(), Some(name)); 66 | } 67 | 68 | // Colors without names 69 | 70 | let test_data = [ 71 | Color::new(0.7, 0.8, 0.9, 1.0), 72 | Color::new(1.0, 0.5, 0.0, 1.0), 73 | Color::from_rgba8(0, 50, 100, 255), 74 | ]; 75 | for c in test_data { 76 | assert!(c.name().is_none()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/parser.rs: -------------------------------------------------------------------------------- 1 | use csscolorparser::{parse, Color}; 2 | 3 | #[test] 4 | fn parser() { 5 | let test_data = [ 6 | ("transparent", [0, 0, 0, 0]), 7 | ("TRANSPARENT", [0, 0, 0, 0]), 8 | ("#ff00ff64", [255, 0, 255, 100]), 9 | ("ff00ff64", [255, 0, 255, 100]), 10 | ("rgb(247,179,99)", [247, 179, 99, 255]), 11 | ("rgb(50% 50% 50%)", [128, 128, 128, 255]), 12 | ("rgb(247,179,99,0.37)", [247, 179, 99, 94]), 13 | ("hsl(270 0% 50%)", [128, 128, 128, 255]), 14 | ("hwb(0 50% 50%)", [128, 128, 128, 255]), 15 | ("hsv(0 0% 50%)", [128, 128, 128, 255]), 16 | ("hsv(0 0% 100%)", [255, 255, 255, 255]), 17 | ("hsv(0 0% 19%)", [48, 48, 48, 255]), 18 | ]; 19 | 20 | for (s, expected) in test_data { 21 | let a = parse(s).unwrap().to_rgba8(); 22 | let b = s.parse::().unwrap().to_rgba8(); 23 | let c = Color::from_html(s).unwrap().to_rgba8(); 24 | assert_eq!(expected, a); 25 | assert_eq!(expected, b); 26 | assert_eq!(expected, c); 27 | } 28 | 29 | #[cfg(feature = "lab")] 30 | { 31 | let test_data = [ 32 | ("lab(0% 0 0)", [0, 0, 0, 255]), 33 | ("lab(100% 0 0)", [255, 255, 255, 255]), 34 | ("lab(0% 0 0 / 0.5)", [0, 0, 0, 128]), 35 | ("lch(0% 0 0)", [0, 0, 0, 255]), 36 | ("lch(100% 0 0)", [255, 255, 255, 255]), 37 | ("lch(0% 0 0 / 0.5)", [0, 0, 0, 128]), 38 | ]; 39 | 40 | for (s, expected) in test_data { 41 | assert_eq!(expected, parse(s).unwrap().to_rgba8()); 42 | } 43 | } 44 | } 45 | 46 | #[test] 47 | fn equal() { 48 | let test_data = [ 49 | ("transparent", "rgb(0,0,0,0%)"), 50 | ("#FF9900", "#f90"), 51 | ("#aabbccdd", "#ABCD"), 52 | ("#BAD455", "BAD455"), 53 | ("rgb(0 255 127 / 75%)", "rgb(0,255,127,0.75)"), 54 | ("hwb(180 0% 60%)", "hwb(180,0%,60%)"), 55 | ("hwb(290 30% 0%)", "hwb(290 0.3 0)"), 56 | ("hsl(180,50%,27%)", "hsl(180,0.5,0.27)"), 57 | ("rgb(255, 165, 0)", "hsl(38.824 100% 50%)"), 58 | ("#7654CD", "rgb(46.27% 32.94% 80.39%)"), 59 | //#[cfg(feature = "lab")] 60 | //("#7654CD", "lab(44.36% 36.05 -58.99)"), 61 | ]; 62 | 63 | for (a, b) in test_data { 64 | let c1 = parse(a).unwrap(); 65 | let c2 = parse(b).unwrap(); 66 | assert_eq!(c1.to_rgba8(), c2.to_rgba8(), "{:?}", [a, b]); 67 | } 68 | } 69 | 70 | #[test] 71 | fn black() { 72 | let data = [ 73 | "#000", 74 | "#000f", 75 | "#000000", 76 | "#000000ff", 77 | "000", 78 | "000f", 79 | "000000", 80 | "000000ff", 81 | "rgb(0,0,0)", 82 | "rgb(0% 0% 0%)", 83 | "rgb(0 0 0 100%)", 84 | "hsl(270,100%,0%)", 85 | "hwb(90 0% 100%)", 86 | "hwb(120deg 0% 100% 100%)", 87 | "hsv(120 100% 0%)", 88 | "oklab(0 0 0)", 89 | "oklch(0 0 180)", 90 | ]; 91 | 92 | let black = [0, 0, 0, 255]; 93 | 94 | for s in data { 95 | let c = parse(s).unwrap().to_rgba8(); 96 | assert_eq!(black, c); 97 | } 98 | } 99 | 100 | #[test] 101 | fn red() { 102 | let data = [ 103 | "#f00", 104 | "#f00f", 105 | "#ff0000", 106 | "#ff0000ff", 107 | "f00", 108 | "f00f", 109 | "ff0000", 110 | "ff0000ff", 111 | "rgb(255,0,0)", 112 | "rgb(255 0 0)", 113 | "rgb(700, -99, 0)", // clamp to 0..255 114 | "rgb(100% 0% 0%)", 115 | "rgb(200% -10% -100%)", // clamp to 0%..100% 116 | "rgb(255 0 0 100%)", 117 | " RGB ( 255 , 0 , 0 ) ", 118 | "RGB( 255 0 0 )", 119 | "hsl(0,100%,50%)", 120 | "hsl(360 100% 50%)", 121 | "hwb(0 0% 0%)", 122 | "hwb(360deg 0% 0% 100%)", 123 | "hwb(360DEG 0% 0% 100%)", 124 | "hsv(0 100% 100%)", 125 | "oklab(0.62796, 0.22486, 0.12585)", 126 | "oklch(0.62796, 0.25768, 29.23388)", 127 | ]; 128 | 129 | let red = [255, 0, 0, 255]; 130 | 131 | for s in data { 132 | let res = parse(s); 133 | assert!(res.is_ok(), "{:?}", s); 134 | let c = res.unwrap().to_rgba8(); 135 | assert_eq!(red, c); 136 | } 137 | } 138 | 139 | #[test] 140 | fn lime() { 141 | let data = [ 142 | "#0f0", 143 | "#0f0f", 144 | "#00ff00", 145 | "#00ff00ff", 146 | "0f0", 147 | "0f0f", 148 | "00ff00", 149 | "00ff00ff", 150 | "rgb(0,255,0)", 151 | "rgb(0% 100% 0%)", 152 | "rgb(0 255 0 / 100%)", 153 | "rgba(0,255,0,1)", 154 | "hsl(120,100%,50%)", 155 | "hsl(120deg 100% 50%)", 156 | "hsl(-240 100% 50%)", 157 | "hsl(-240deg 100% 50%)", 158 | "hsl(0.3333turn 100% 50%)", 159 | "hsl(0.3333TURN 100% 50%)", 160 | "hsl(133.333grad 100% 50%)", 161 | "hsl(133.333GRAD 100% 50%)", 162 | "hsl(2.0944rad 100% 50%)", 163 | "hsl(2.0944RAD 100% 50%)", 164 | "hsla(120,100%,50%,100%)", 165 | "hwb(120 0% 0%)", 166 | "hwb(480deg 0% 0% / 100%)", 167 | "hsv(120 100% 100%)", 168 | "oklab(0.86644, -0.23389, 0.1795)", 169 | "oklch(0.86644, 0.29483, 142.49535)", 170 | ]; 171 | 172 | let lime = [0, 255, 0, 255]; 173 | 174 | for s in data { 175 | let res = parse(s); 176 | assert!(res.is_ok(), "{:?}", s); 177 | let c = res.unwrap().to_rgba8(); 178 | assert_eq!(lime, c); 179 | } 180 | } 181 | 182 | #[test] 183 | fn lime_alpha() { 184 | let data = [ 185 | "#00ff0080", 186 | "00ff0080", 187 | "rgb(0,255,0,50%)", 188 | "rgb(0% 100% 0% / 0.5)", 189 | "rgba(0%,100%,0%,50%)", 190 | "hsl(120,100%,50%,0.5)", 191 | "hsl(120deg 100% 50% / 50%)", 192 | "hsla(120,100%,50%,0.5)", 193 | "hwb(120 0% 0% / 50%)", 194 | "hsv(120 100% 100% / 50%)", 195 | ]; 196 | 197 | let lime_alpha = [0, 255, 0, 128]; 198 | 199 | for s in data { 200 | let c = parse(s).unwrap().to_rgba8(); 201 | assert_eq!(lime_alpha, c); 202 | } 203 | } 204 | 205 | #[cfg(all(feature = "named-colors", feature = "lab"))] 206 | #[test] 207 | fn invalid_format() { 208 | let test_data = [ 209 | "", 210 | "bloodred", 211 | "#78afzd", 212 | "#fffff", 213 | "rgb(255,0,0", 214 | "rgb(0,255,8s)", 215 | "rgb(100%,z9%,75%)", 216 | "rgb(255,0,0%)", // mix format 217 | "rgb(70%,30%,0)", // mix format 218 | "cmyk(1 0 0)", 219 | "rgba(0 0)", 220 | "hsl(90',100%,50%)", 221 | "hsl(360,70%,50%,90%,100%)", 222 | "hsl(deg 100% 50%)", 223 | "hsl(Xturn 100% 50%)", 224 | "hsl(Zgrad 100% 50%)", 225 | "hsl(180 1 x%)", 226 | "hsl(360,0%,0)", // mix format 227 | "hsla(360)", 228 | "hwb(Xrad,50%,50%)", 229 | "hwb(270 0% 0% 0% 0%)", 230 | "hwb(360,0,20%)", // mix format 231 | "hsv(120 100% 100% 1 50%)", 232 | "hsv(120 XXX 100%)", 233 | "hsv(120,100%,0.5)", //mix format 234 | "lab(100%,0)", 235 | "lab(100% 0 X)", 236 | "lch(100%,0)", 237 | "lch(100% 0 X)", 238 | "oklab(0,0)", 239 | "oklab(0,0,x,0)", 240 | "oklch(0,0,0,0,0)", 241 | "oklch(0,0,0,x)", 242 | "æ", 243 | "#ß", 244 | "rgb(ß,0,0)", 245 | "\u{1F602}", 246 | "#\u{1F602}", 247 | "rgb(\u{1F602},\u{1F602},\u{1F602})", 248 | ]; 249 | 250 | for s in test_data { 251 | let c = parse(s); 252 | assert!(c.is_err(), "{:?}", s); 253 | } 254 | 255 | #[rustfmt::skip] 256 | let test_data = [ 257 | ("#78afzd", "invalid hex format"), 258 | ("rgb(xx,yy,xx)", "invalid rgb format"), 259 | ("rgb(255,0)", "invalid color function"), 260 | ("hsl(0,100%,2o%)", "invalid hsl format"), 261 | ("hsv(360)", "invalid color function"), 262 | ("hwb(270,0%,0%,x)", "invalid color function"), 263 | ("lab(0%)", "invalid color function"), 264 | ("lch(0%)", "invalid color function"), 265 | ("cmyk(0,0,0,0)", "invalid color function"), 266 | ("blood", "invalid unknown format"), 267 | ("rgb(255,0,0", "invalid unknown format"), 268 | ("x£", "invalid unknown format"), 269 | ("x£x", "invalid unknown format"), 270 | ("xxx£x", "invalid unknown format"), 271 | ("xxxxx£x", "invalid unknown format"), 272 | ("\u{1F602}", "invalid unknown format"), 273 | ]; 274 | 275 | for (s, err_msg) in test_data { 276 | let c = parse(s); 277 | assert_eq!(c.unwrap_err().to_string(), err_msg, "{:?}", s); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /tests/parser2.rs: -------------------------------------------------------------------------------- 1 | use csscolorparser::parse; 2 | 3 | #[test] 4 | fn hex() { 5 | let test_data = [ 6 | "#71fe15", 7 | "#d6e3c9", 8 | "#2a7719", 9 | "#b53717", 10 | "#5b0b8d", 11 | "#aff632", 12 | "#65ec8d", 13 | "#d35493", 14 | "#289e5f", 15 | "#b46152", 16 | "#e0afee", 17 | "#ac2be4", 18 | "#233490", 19 | "#1afbc5", 20 | "#e41755", 21 | "#e052ee", 22 | "#4d1b5e", 23 | "#230cde", 24 | "#f8a243", 25 | "#a130d1", 26 | "#b38373", 27 | "#6b9fa203", 28 | "#0e5e0be6", 29 | "#84f9a716", 30 | "#48651550", 31 | "#1adc2cf4", 32 | "#c191a31c", 33 | "#a25518c5", 34 | "#cb33f2c9", 35 | "#89b21d36", 36 | "#cbb97f3e", 37 | ]; 38 | for s in test_data { 39 | let c = parse(s).unwrap(); 40 | assert_eq!(s, c.to_css_hex()); 41 | } 42 | } 43 | 44 | #[test] 45 | fn rgb() { 46 | let test_data = [ 47 | "rgb(71 175 99)", 48 | "rgb(170 203 72)", 49 | "rgb(45 232 237)", 50 | "rgb(119 1 124)", 51 | "rgb(243 93 86)", 52 | "rgb(223 25 119)", 53 | "rgb(6 44 133)", 54 | "rgb(167 240 237)", 55 | "rgb(97 71 129)", 56 | "rgb(125 68 93)", 57 | "rgb(139 187 62)", 58 | "rgb(100 51 80)", 59 | "rgb(27 249 123)", 60 | "rgb(230 63 99)", 61 | "rgb(241 34 4)", 62 | "rgb(149 222 185)", 63 | "rgb(3 129 213)", 64 | "rgb(88 220 108)", 65 | "rgb(199 169 6)", 66 | "rgb(54 70 163)", 67 | "rgb(90 42 106)", 68 | ]; 69 | for s in test_data { 70 | let c = parse(s).unwrap(); 71 | assert_eq!(s, c.to_css_rgb()); 72 | } 73 | } 74 | 75 | #[test] 76 | fn hsl() { 77 | let test_data = [ 78 | "hsl(0 48% 83%)", 79 | "hsl(17 73% 13%)", 80 | "hsl(35 40% 84%)", 81 | "hsl(53 88% 21%)", 82 | "hsl(71 11% 45%)", 83 | "hsl(89 12% 89%)", 84 | "hsl(107 49% 68%)", 85 | "hsl(125 96% 72%)", 86 | "hsl(143 15% 92%)", 87 | "hsl(161 80% 93%)", 88 | "hsl(179 45% 76%)", 89 | "hsl(197 99% 84%)", 90 | "hsl(215 33% 15%)", 91 | "hsl(233 69% 59%)", 92 | "hsl(251 34% 46%)", 93 | "hsl(269 43% 18%)", 94 | "hsl(287 89% 69%)", 95 | "hsl(305 87% 36%)", 96 | "hsl(323 97% 26%)", 97 | "hsl(341 61% 66%)", 98 | "hsl(359 15% 74%)", 99 | ]; 100 | for s in test_data { 101 | let c = parse(s).unwrap(); 102 | assert_eq!(s, c.to_css_hsl()); 103 | } 104 | } 105 | 106 | #[test] 107 | fn hwb() { 108 | let test_data = [ 109 | "hwb(0 87% 0%)", 110 | "hwb(17 0% 23%)", 111 | "hwb(35 0% 7%)", 112 | "hwb(53 66% 0%)", 113 | "hwb(71 0% 66%)", 114 | "hwb(89 22% 0%)", 115 | "hwb(107 0% 2%)", 116 | "hwb(125 51% 0%)", 117 | "hwb(143 10% 0%)", 118 | "hwb(161 0% 76%)", 119 | "hwb(179 72% 0%)", 120 | "hwb(197 0% 60%)", 121 | "hwb(215 0% 39%)", 122 | "hwb(233 0% 18%)", 123 | "hwb(251 0% 3%)", 124 | "hwb(269 57% 0%)", 125 | "hwb(287 21% 0%)", 126 | "hwb(305 15% 0%)", 127 | "hwb(323 55% 0%)", 128 | "hwb(341 0% 72%)", 129 | "hwb(359 0% 2%)", 130 | ]; 131 | for s in test_data { 132 | let c = parse(s).unwrap(); 133 | assert_eq!(s, c.to_css_hwb()); 134 | } 135 | } 136 | 137 | #[test] 138 | fn oklab() { 139 | let test_data = [ 140 | "oklab(0.623 0.019 -0.359)", 141 | "oklab(0.362 -0.314 -0.035)", 142 | "oklab(0.804 0.166 -0.072)", 143 | "oklab(0.832 0.089 0.265)", 144 | "oklab(0.681 0.038 -0.3)", 145 | "oklab(0.117 -0.192 0.24)", 146 | "oklab(0.651 -0.241 -0.158)", 147 | "oklab(0.421 -0.248 0.053)", 148 | "oklab(0.923 -0.119 -0.288)", 149 | "oklab(0.811 -0.295 0.347)", 150 | "oklab(0.485 -0.368 0.066)", 151 | "oklab(0.905 0.13 -0.163)", 152 | "oklab(0.778 -0.001 0.4)", 153 | "oklab(0.672 0.136 -0.03)", 154 | "oklab(0.926 0.281 0.279)", 155 | "oklab(0.247 0.155 0.379)", 156 | "oklab(0.503 0.042 0.202)", 157 | "oklab(0.792 -0.34 -0.372)", 158 | "oklab(0.877 -0.13 0.222)", 159 | "oklab(0.898 -0.068 -0.239)", 160 | "oklab(0.725 -0.343 -0.352)", 161 | ]; 162 | for s in test_data { 163 | let c = parse(s).unwrap(); 164 | assert_eq!(s, c.to_css_oklab()); 165 | } 166 | } 167 | 168 | #[test] 169 | fn oklch() { 170 | let test_data = [ 171 | "oklch(0.284 0.132 0)", 172 | "oklch(0.314 0.136 17)", 173 | "oklch(0.935 0.398 35)", 174 | "oklch(0.729 0.175 53)", 175 | "oklch(0.157 0.29 71)", 176 | "oklch(0.266 0.365 89)", 177 | "oklch(0.12 0.225 107)", 178 | "oklch(0.532 0.274 125)", 179 | "oklch(0.571 0.201 143)", 180 | "oklch(0.948 0.217 161)", 181 | "oklch(0.501 0.2 179)", 182 | "oklch(0.184 0.308 197)", 183 | "oklch(0.308 0.273 215)", 184 | "oklch(0.874 0.143 233)", 185 | "oklch(0.544 0.186 251)", 186 | "oklch(0.144 0.255 269)", 187 | "oklch(0.997 0.327 287)", 188 | "oklch(0.544 0.22 305)", 189 | "oklch(0.578 0.203 323)", 190 | "oklch(0.819 0.343 341)", 191 | "oklch(0.497 0.188 359)", 192 | ]; 193 | for s in test_data { 194 | let c = parse(s).unwrap(); 195 | assert_eq!(s, c.to_css_oklch()); 196 | } 197 | } 198 | 199 | #[cfg(feature = "lab")] 200 | #[test] 201 | fn lab() { 202 | let test_data = [ 203 | "lab(57.32 70.93 101.73)", 204 | "lab(19.94 -109.64 -111.1)", 205 | "lab(54.5 -21.31 -64.68)", 206 | "lab(10.25 27.72 90.4)", 207 | "lab(33.83 105.64 37.89)", 208 | "lab(83.56 108.72 89.22)", 209 | "lab(40.01 105.35 -85.3)", 210 | "lab(30.1 21.78 -92.17)", 211 | "lab(99.19 0.44 93.11)", 212 | "lab(93.32 55.85 -9.14)", 213 | "lab(78.31 -23.69 -27.56)", 214 | "lab(42.64 -22.56 -45.68)", 215 | "lab(69.27 113.33 37.39)", 216 | "lab(24.6 -37.2 88.99)", 217 | "lab(72.6 -41.31 11.88)", 218 | "lab(12.44 -6.94 69.89)", 219 | "lab(71.23 -91.08 31.29)", 220 | "lab(5.18 65.67 63.53)", 221 | "lab(18.7 19.92 67.2)", 222 | "lab(14.62 71.89 57.13)", 223 | "lab(76.47 77.56 -107.61)", 224 | ]; 225 | for s in test_data { 226 | let c = parse(s).unwrap(); 227 | assert_eq!(s, c.to_css_lab()); 228 | } 229 | } 230 | 231 | #[cfg(feature = "lab")] 232 | #[test] 233 | fn lch() { 234 | let test_data = [ 235 | "lch(16.4 138.03 0)", 236 | "lch(7.88 52.89 17.95)", 237 | "lch(19.43 25.84 35.9)", 238 | "lch(73.85 45.9 53.85)", 239 | "lch(72.85 126.69 71.8)", 240 | "lch(42.26 71.09 89.75)", 241 | "lch(99.21 108.15 107.7)", 242 | "lch(13.05 38.12 125.65)", 243 | "lch(46.73 30.27 143.6)", 244 | "lch(33.88 90.43 161.55)", 245 | "lch(89.29 23.68 179.5)", 246 | "lch(20.69 14.49 197.45)", 247 | "lch(64.73 27.25 215.4)", 248 | "lch(61.08 70.57 233.35)", 249 | "lch(40.84 141.03 251.3)", 250 | "lch(7.45 91.95 269.25)", 251 | "lch(53.33 83.53 287.2)", 252 | "lch(26.3 52.41 305.15)", 253 | "lch(33.6 42.99 323.1)", 254 | "lch(90.17 91 341.05)", 255 | "lch(33.83 10.5 359)", 256 | ]; 257 | for s in test_data { 258 | let c = parse(s).unwrap(); 259 | assert_eq!(s, c.to_css_lch()); 260 | } 261 | } 262 | --------------------------------------------------------------------------------