hexrgba 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hexrgba-0.2.1/.gitignore +16 -0
- hexrgba-0.2.1/CHANGELOG.md +42 -0
- hexrgba-0.2.1/GUIDE.md +145 -0
- hexrgba-0.2.1/LICENSE +21 -0
- hexrgba-0.2.1/PKG-INFO +109 -0
- hexrgba-0.2.1/README.md +82 -0
- hexrgba-0.2.1/pyproject.toml +72 -0
- hexrgba-0.2.1/scripts/e2e_wheel.py +106 -0
- hexrgba-0.2.1/scripts/fetch_upstream.py +37 -0
- hexrgba-0.2.1/src/hexrgba/__init__.py +36 -0
- hexrgba-0.2.1/src/hexrgba/__main__.py +10 -0
- hexrgba-0.2.1/src/hexrgba/_alpha.py +56 -0
- hexrgba-0.2.1/src/hexrgba/_blend.py +62 -0
- hexrgba-0.2.1/src/hexrgba/_cli.py +125 -0
- hexrgba-0.2.1/src/hexrgba/_color.py +141 -0
- hexrgba-0.2.1/src/hexrgba/_errors.py +32 -0
- hexrgba-0.2.1/src/hexrgba/_hex.py +89 -0
- hexrgba-0.2.1/src/hexrgba/_names.py +164 -0
- hexrgba-0.2.1/src/hexrgba/_wcag.py +70 -0
- hexrgba-0.2.1/src/hexrgba/py.typed +0 -0
- hexrgba-0.2.1/tests/conftest.py +41 -0
- hexrgba-0.2.1/tests/fixtures/css-color-4-named-colors.json +156 -0
- hexrgba-0.2.1/tests/test_alpha.py +76 -0
- hexrgba-0.2.1/tests/test_blend.py +56 -0
- hexrgba-0.2.1/tests/test_browser_cases.py +45 -0
- hexrgba-0.2.1/tests/test_cli.py +128 -0
- hexrgba-0.2.1/tests/test_color.py +108 -0
- hexrgba-0.2.1/tests/test_contract.py +46 -0
- hexrgba-0.2.1/tests/test_docs.py +35 -0
- hexrgba-0.2.1/tests/test_e2e_wheel.py +25 -0
- hexrgba-0.2.1/tests/test_fuzz.py +107 -0
- hexrgba-0.2.1/tests/test_hex.py +119 -0
- hexrgba-0.2.1/tests/test_names.py +82 -0
- hexrgba-0.2.1/tests/test_wcag.py +86 -0
- hexrgba-0.2.1/tests/test_wpt.py +82 -0
- hexrgba-0.2.1/tests/upstream/MANIFEST.json +47 -0
hexrgba-0.2.1/.gitignore
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
.venv/
|
|
4
|
+
venv/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.mypy_cache/
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
htmlcov/
|
|
13
|
+
|
|
14
|
+
# Third-party test files, fetched locally by scripts/fetch_upstream.py; only the manifest is tracked.
|
|
15
|
+
tests/upstream/*
|
|
16
|
+
!tests/upstream/MANIFEST.json
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.1] - 2026-10-13
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- The Web Platform Tests color parsing pages are no longer committed or shipped in the
|
|
14
|
+
sdist: `scripts/fetch_upstream.py` fetches them at a pinned commit, checked against
|
|
15
|
+
`tests/upstream/MANIFEST.json`, and hand-written cases keep the default suite meaningful
|
|
16
|
+
without them.
|
|
17
|
+
|
|
18
|
+
## [0.2.0] - Unreleased
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `parse_color` and `is_color`: hex colors plus all 148 CSS named colors and
|
|
23
|
+
`transparent`, matched ASCII case-insensitively, with a `strict` mode that follows the
|
|
24
|
+
CSS grammar exactly.
|
|
25
|
+
- `Color`, a frozen, hashable value type with `hex`, `to_hex(short=...)`, `to_css()`,
|
|
26
|
+
`name`, `alpha`, `is_opaque`, `with_alpha()`, `from_rgba()` and `as_tuple()`.
|
|
27
|
+
- `NAMED_COLORS`, the read-only CSS Color 4 named color table.
|
|
28
|
+
- `alpha_to_byte` and `css_alpha`: exact alpha conversion and the CSS legacy alpha
|
|
29
|
+
serialization; every alpha byte round-trips through both.
|
|
30
|
+
- `shorten` and `normalize(..., short=True)` for the lossless `#rgb` / `#rgba` form.
|
|
31
|
+
- `relative_luminance`, `contrast_ratio` and `passes_contrast` (WCAG 2.x, AA and AAA).
|
|
32
|
+
- `blend`, simple alpha compositing of one color over another.
|
|
33
|
+
- `ColorError`, `ColorNameError` and `ChannelError`. `HexColorError` is now a
|
|
34
|
+
`ColorError`, still a `ValueError`.
|
|
35
|
+
- The `hexrgba` command (`normalize`, `convert`, `contrast`, `blend`) and `python -m hexrgba`.
|
|
36
|
+
- `GUIDE.md`, with the exact rules and their sources.
|
|
37
|
+
|
|
38
|
+
## [0.1.0]
|
|
39
|
+
|
|
40
|
+
- First release: `is_hex`, `parse`, `normalize`, `to_hex` and `HexColorError`.
|
|
41
|
+
- Support for `#rgb`, `#rgba`, `#rrggbb` and `#rrggbbaa`, with optional leading hash,
|
|
42
|
+
any case, shorthand expansion and alpha round-tripping.
|
hexrgba-0.2.1/GUIDE.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# hexrgba guide
|
|
2
|
+
|
|
3
|
+
This guide states the exact rules `hexrgba` follows and where each one comes from. All
|
|
4
|
+
sources were fetched on 2026-10-12.
|
|
5
|
+
|
|
6
|
+
| Topic | Source |
|
|
7
|
+
| --- | --- |
|
|
8
|
+
| Hex syntax, named colors, `transparent`, serialization | [CSS Color Module Level 4](https://www.w3.org/TR/css-color-4/), Candidate Recommendation Draft of 9 October 2026, sections 5.2, 6.1, 6.3 and 16 |
|
|
9
|
+
| Golden parsing vectors | [Web Platform Tests `css/css-color/parsing`](https://github.com/web-platform-tests/wpt/tree/786385c76ca1c01d40eba0234dc59384e2eb73d4/css/css-color/parsing), commit `786385c` |
|
|
10
|
+
| Luminance and contrast | [WCAG 2.2](https://www.w3.org/TR/WCAG22/), Recommendation of 12 December 2024 |
|
|
11
|
+
| Blending | [Compositing and Blending Level 1](https://www.w3.org/TR/compositing-1/), section 5.1 |
|
|
12
|
+
|
|
13
|
+
## Parsing
|
|
14
|
+
|
|
15
|
+
`parse_color(text, *, strict=False)` returns a `Color`. It accepts:
|
|
16
|
+
|
|
17
|
+
- `#rgb`, `#rgba`, `#rrggbb` and `#rrggbbaa`. Short forms expand by doubling each digit,
|
|
18
|
+
so `#123` is `#112233`. Without an alpha pair the color is opaque (alpha byte 255).
|
|
19
|
+
- The 148 keywords of the CSS named color table (it includes `rebeccapurple` and both
|
|
20
|
+
spellings of every gray), and `transparent`, which is `rgba(0, 0, 0, 0)`.
|
|
21
|
+
|
|
22
|
+
Hex digits and keywords are **ASCII** case-insensitive: only `A` to `Z` fold. That matters
|
|
23
|
+
because Python's `str.lower()` folds more: the Kelvin sign `U+212A` lowers to `k`, so a
|
|
24
|
+
naive parser would accept `blac` + Kelvin sign as `black`. CSS rejects it, the WPT suite
|
|
25
|
+
tests it, and so does `hexrgba`.
|
|
26
|
+
|
|
27
|
+
| Mode | Leading `#` | Surrounding whitespace | Upper case |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `strict=True` | required for hex | rejected | accepted (CSS is case-insensitive) |
|
|
30
|
+
| lenient (default) | optional | stripped with `str.strip()` (any Python whitespace) | accepted |
|
|
31
|
+
|
|
32
|
+
No keyword is made only of hex digits, so lenient mode can never confuse a name with a
|
|
33
|
+
hash-less hex color; a test pins that.
|
|
34
|
+
|
|
35
|
+
`currentcolor`, system colors and the functional notations (`rgb()`, `hsl()`, `color()`)
|
|
36
|
+
are not accepted as input: they need a cascade or a much larger grammar.
|
|
37
|
+
|
|
38
|
+
The `0.1.0` functions (`is_hex`, `parse`, `normalize`, `to_hex`) keep their exact
|
|
39
|
+
behaviour: hex only, lenient, `(r, g, b, a)` tuples.
|
|
40
|
+
|
|
41
|
+
## Output
|
|
42
|
+
|
|
43
|
+
- `Color.hex` and `normalize()` give canonical lower-case `#rrggbb` when opaque and
|
|
44
|
+
`#rrggbbaa` otherwise.
|
|
45
|
+
- `to_hex(short=True)`, `normalize(..., short=True)` and `shorten()` give `#rgb` or `#rgba`
|
|
46
|
+
only when that is lossless (`#aabbcc` to `#abc`, `#aabbcd` stays long).
|
|
47
|
+
- `Color.to_css()` follows "CSS serialization of sRGB values" (section 16.2.2): `rgb(r, g, b)`
|
|
48
|
+
when the alpha byte is 255, else `rgba(r, g, b, alpha)`, comma separated with one space.
|
|
49
|
+
The WPT expected strings are used as the test oracle.
|
|
50
|
+
- `Color.name` gives the keyword for an exact match, or `None`. Where two keywords share a
|
|
51
|
+
value (`aqua` and `cyan`, `fuchsia` and `magenta`, `gray` and `grey`) the first in the
|
|
52
|
+
spec's alphabetical table wins.
|
|
53
|
+
|
|
54
|
+
## Alpha: 0..255 versus 0..1
|
|
55
|
+
|
|
56
|
+
The alpha is stored as a byte, `Color.a`, from 0 to 255. There are two float views and
|
|
57
|
+
one way back:
|
|
58
|
+
|
|
59
|
+
- `Color.alpha` is exactly `a / 255`.
|
|
60
|
+
- `css_alpha(a)` is the CSS legacy alpha string (section 16.1.1). If a whole percentage
|
|
61
|
+
`n` satisfies `round(n * 2.55) == a`, the result is `n / 100` (237 gives `0.93`, 204
|
|
62
|
+
gives `0.8`, 128 gives `0.5`). Otherwise it is `a / 0.255` rounded and divided by 1000
|
|
63
|
+
(236 gives `0.925`). Halves round up. The arithmetic is exact, not binary floating point.
|
|
64
|
+
- `alpha_to_byte(x)` multiplies by 255 and rounds halves up. A float is read as its
|
|
65
|
+
shortest decimal form, the number you typed: `0.3` is 76.5 and gives 77, where the
|
|
66
|
+
binary value just below 0.3 would give 76.
|
|
67
|
+
|
|
68
|
+
**Round-trip contract**, tested for all 256 bytes:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
alpha_to_byte(a / 255) == a
|
|
72
|
+
alpha_to_byte(float(css_alpha(a))) == a
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
So `#rrggbbaa` to `Color.alpha` (or to the `rgba()` string) and back to hex is lossless.
|
|
76
|
+
The other direction cannot be: a byte has 256 values, so `alpha_to_byte(0.123)` gives 31,
|
|
77
|
+
and `css_alpha(31)` is `0.12`, which reads back as 31, not as 0.123.
|
|
78
|
+
|
|
79
|
+
One note on the spec: for byte 236 its prose example prints `0.92549`, but its own
|
|
80
|
+
numbered steps give `0.925`. `hexrgba` follows the numbered steps.
|
|
81
|
+
|
|
82
|
+
## Contrast
|
|
83
|
+
|
|
84
|
+
`relative_luminance(color)` is `0.2126 R + 0.7152 G + 0.0722 B` on linearized sRGB
|
|
85
|
+
channels, using the `0.04045` threshold of the current WCAG definition (older texts said
|
|
86
|
+
`0.03928`; no 8-bit value falls between the two, so results are identical). Alpha is
|
|
87
|
+
ignored.
|
|
88
|
+
|
|
89
|
+
`contrast_ratio(fg, bg)` is `(L1 + 0.05) / (L2 + 0.05)` with `L1` the lighter, from 1 to 21.
|
|
90
|
+
A translucent foreground is first blended over the background and rounded to 8-bit
|
|
91
|
+
channels, as a browser paints it; the ratio is measured on that painted color. The background must be
|
|
92
|
+
opaque, because what lies behind it is unknown.
|
|
93
|
+
|
|
94
|
+
`passes_contrast(fg, bg, level="AA", large=False)` compares without rounding, as WCAG
|
|
95
|
+
requires (4.499 fails 4.5):
|
|
96
|
+
|
|
97
|
+
| Level | Normal text | Large text |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| AA (1.4.3) | 4.5 | 3 |
|
|
100
|
+
| AAA (1.4.6) | 7 | 4.5 |
|
|
101
|
+
|
|
102
|
+
The test suite checks the worked examples published by WCAG (for example `#949494` on
|
|
103
|
+
white is 3:1, `#E3660E` on white is 3.4:1). WCAG prints those with one decimal and does not
|
|
104
|
+
round consistently, so each is checked to within 0.1.
|
|
105
|
+
|
|
106
|
+
## Blending
|
|
107
|
+
|
|
108
|
+
`blend(fg, bg)` is source-over simple alpha compositing:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
ao = as + ab (1 - as)
|
|
112
|
+
Co = (Cs as + Cb ab (1 - as)) / ao
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
It is computed with exact fractions and each channel is rounded to a byte once, halves
|
|
116
|
+
up. If both inputs are fully transparent the result is transparent black.
|
|
117
|
+
|
|
118
|
+
## Errors
|
|
119
|
+
|
|
120
|
+
All errors are `ValueError` subclasses, so `0.1.0` handlers keep working:
|
|
121
|
+
|
|
122
|
+
- `ColorError`: base class; also raised for a translucent contrast background.
|
|
123
|
+
- `HexColorError`: malformed hex, or a hash-less hex in strict mode.
|
|
124
|
+
- `ChannelError` (a `HexColorError`): a channel or alpha outside its range or of the
|
|
125
|
+
wrong type, including booleans.
|
|
126
|
+
- `ColorNameError`: neither hex nor a known keyword.
|
|
127
|
+
|
|
128
|
+
`parse_color` raises `TypeError` for a non-string. `is_color` never raises.
|
|
129
|
+
`is_hex` and `parse` keep their 0.1.0 behaviour and raise `AttributeError` for a non-string.
|
|
130
|
+
Error messages show at most the first 40 characters of the offending value.
|
|
131
|
+
|
|
132
|
+
## Command line
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
hexrgba normalize COLOR [--short] [--strict]
|
|
136
|
+
hexrgba convert COLOR [--to hex|short|css|channels|name] [--strict]
|
|
137
|
+
hexrgba contrast FOREGROUND BACKGROUND [--level AA|AAA] [--large]
|
|
138
|
+
hexrgba blend FOREGROUND BACKGROUND
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`contrast` prints the ratio truncated to two decimals (a failing 4.499 never shows as
|
|
142
|
+
4.50); with `--level` it exits 1 when the pair fails. `--large` is only valid with `--level`.
|
|
143
|
+
Bad usage and invalid colors print to stderr and exit 2, so a script can tell a failing
|
|
144
|
+
pair from bad input. Every example in `hexrgba --help` is executed by the
|
|
145
|
+
test suite and by the wheel end-to-end check.
|
hexrgba-0.2.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Javier Lianes
|
|
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.
|
hexrgba-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hexrgba
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: CSS hex and named colors: parse, normalize, exact alpha, rgba(), WCAG contrast and blending
|
|
5
|
+
Project-URL: Homepage, https://lianes8studio.com
|
|
6
|
+
Project-URL: Repository, https://github.com/JaviLianes8/hexrgba
|
|
7
|
+
Project-URL: Issues, https://github.com/JaviLianes8/hexrgba/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/JaviLianes8/hexrgba/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: LinkedIn, https://www.linkedin.com/in/jlianes/
|
|
10
|
+
Project-URL: Funding, https://buymeacoffee.com/jlianesglrs
|
|
11
|
+
Author-email: Javier Lianes <javier@lianes8studio.com>
|
|
12
|
+
License-Expression: MIT
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Keywords: blend,color,contrast,css,hex,named-colors,normalize,parse,rgba,wcag
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: build; extra == 'dev'
|
|
22
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
25
|
+
Requires-Dist: twine; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# hexrgba
|
|
29
|
+
|
|
30
|
+
Parse, validate and normalize CSS hex colors and named colors, with exact alpha handling,
|
|
31
|
+
CSS `rgb()` / `rgba()` output, WCAG contrast and alpha blending. Typed, zero dependencies.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
pip install hexrgba
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from hexrgba import Color, contrast_ratio, is_hex, normalize, parse, parse_color, to_hex
|
|
43
|
+
|
|
44
|
+
is_hex("#f00") # True
|
|
45
|
+
parse("#ff000080") # (255, 0, 0, 128)
|
|
46
|
+
normalize("#F00") # "#ff0000"
|
|
47
|
+
normalize("#AABBCC", short=True) # "#abc"
|
|
48
|
+
to_hex(255, 0, 128, 64) # "#ff008040"
|
|
49
|
+
|
|
50
|
+
color = parse_color("RebeccaPurple") # Color(r=102, g=51, b=153, a=255)
|
|
51
|
+
color.hex # "#663399"
|
|
52
|
+
color.to_css() # "rgb(102, 51, 153)"
|
|
53
|
+
parse_color("#ff00ffed").to_css() # "rgba(255, 0, 255, 0.93)"
|
|
54
|
+
parse_color("#00ffff").name # "aqua"
|
|
55
|
+
Color.from_rgba(255, 0, 0, 0.5).hex # "#ff000080"
|
|
56
|
+
|
|
57
|
+
contrast_ratio("#767676", "white") # 4.54...
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
What it covers:
|
|
61
|
+
|
|
62
|
+
- **All four hex forms** (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`) and **all 148 CSS named
|
|
63
|
+
colors** plus `transparent`, matched ASCII case-insensitively as CSS requires.
|
|
64
|
+
- **Strict or lenient.** `strict=True` follows the CSS grammar (leading `#`, no surrounding
|
|
65
|
+
whitespace). The default also accepts a missing `#` and strips surrounding
|
|
66
|
+
Python whitespace (`str.strip()`).
|
|
67
|
+
- **A frozen `Color`** with `hex`, `to_hex(short=True)`, `to_css()`, `name`, `alpha`,
|
|
68
|
+
`with_alpha()` and `from_rgba()`.
|
|
69
|
+
- **Exact alpha.** `alpha_to_byte` and `css_alpha` follow CSS Color 4; every alpha byte
|
|
70
|
+
round-trips through its 0..1 float and through its CSS string.
|
|
71
|
+
- **WCAG 2.x** `relative_luminance`, `contrast_ratio` and `passes_contrast` (AA / AAA).
|
|
72
|
+
- **Blending** of a translucent color over a background with `blend`.
|
|
73
|
+
- **Typed errors**, all `ValueError`: `ColorError`, `HexColorError`, `ColorNameError`,
|
|
74
|
+
`ChannelError`.
|
|
75
|
+
|
|
76
|
+
The full guide, with the exact rules and the sources they come from, is in
|
|
77
|
+
[GUIDE.md](GUIDE.md).
|
|
78
|
+
|
|
79
|
+
## Command line
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
hexrgba normalize "#FFAA00" # #ffaa00
|
|
83
|
+
hexrgba normalize "#aabbcc" --short # #abc
|
|
84
|
+
hexrgba convert "#ff00ffed" --to css # rgba(255, 0, 255, 0.93)
|
|
85
|
+
hexrgba convert "#00ffff" --to name # aqua
|
|
86
|
+
hexrgba contrast "#767676" white # 4.54:1
|
|
87
|
+
hexrgba contrast "#777" white --level AA # prints 4.47:1, exits 1
|
|
88
|
+
hexrgba blend "#0000ff80" red # #7f0080
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Quote a leading `#`: most shells read it as the start of a comment. Errors go to stderr.
|
|
92
|
+
Exit codes: 0 success, 1 the pair fails `--level`, 2 bad usage or an invalid color. `python -m hexrgba` works too.
|
|
93
|
+
|
|
94
|
+
## Why
|
|
95
|
+
|
|
96
|
+
`webcolors` is built around the CSS named-color tables, and most other color packages pull
|
|
97
|
+
in a wider model of color spaces. `hexrgba` stays small and exact: the hex and keyword
|
|
98
|
+
syntax of CSS, checked against the spec tables and the Web Platform Tests vectors, an
|
|
99
|
+
alpha conversion that never drifts, and the WCAG contrast math, with no dependencies.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT. See [LICENSE](LICENSE).
|
|
104
|
+
|
|
105
|
+
## Links
|
|
106
|
+
|
|
107
|
+
- Website: https://lianes8studio.com
|
|
108
|
+
- GitHub: https://github.com/JaviLianes8
|
|
109
|
+
- LinkedIn: https://www.linkedin.com/in/jlianes/
|
hexrgba-0.2.1/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# hexrgba
|
|
2
|
+
|
|
3
|
+
Parse, validate and normalize CSS hex colors and named colors, with exact alpha handling,
|
|
4
|
+
CSS `rgb()` / `rgba()` output, WCAG contrast and alpha blending. Typed, zero dependencies.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
pip install hexrgba
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from hexrgba import Color, contrast_ratio, is_hex, normalize, parse, parse_color, to_hex
|
|
16
|
+
|
|
17
|
+
is_hex("#f00") # True
|
|
18
|
+
parse("#ff000080") # (255, 0, 0, 128)
|
|
19
|
+
normalize("#F00") # "#ff0000"
|
|
20
|
+
normalize("#AABBCC", short=True) # "#abc"
|
|
21
|
+
to_hex(255, 0, 128, 64) # "#ff008040"
|
|
22
|
+
|
|
23
|
+
color = parse_color("RebeccaPurple") # Color(r=102, g=51, b=153, a=255)
|
|
24
|
+
color.hex # "#663399"
|
|
25
|
+
color.to_css() # "rgb(102, 51, 153)"
|
|
26
|
+
parse_color("#ff00ffed").to_css() # "rgba(255, 0, 255, 0.93)"
|
|
27
|
+
parse_color("#00ffff").name # "aqua"
|
|
28
|
+
Color.from_rgba(255, 0, 0, 0.5).hex # "#ff000080"
|
|
29
|
+
|
|
30
|
+
contrast_ratio("#767676", "white") # 4.54...
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
What it covers:
|
|
34
|
+
|
|
35
|
+
- **All four hex forms** (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`) and **all 148 CSS named
|
|
36
|
+
colors** plus `transparent`, matched ASCII case-insensitively as CSS requires.
|
|
37
|
+
- **Strict or lenient.** `strict=True` follows the CSS grammar (leading `#`, no surrounding
|
|
38
|
+
whitespace). The default also accepts a missing `#` and strips surrounding
|
|
39
|
+
Python whitespace (`str.strip()`).
|
|
40
|
+
- **A frozen `Color`** with `hex`, `to_hex(short=True)`, `to_css()`, `name`, `alpha`,
|
|
41
|
+
`with_alpha()` and `from_rgba()`.
|
|
42
|
+
- **Exact alpha.** `alpha_to_byte` and `css_alpha` follow CSS Color 4; every alpha byte
|
|
43
|
+
round-trips through its 0..1 float and through its CSS string.
|
|
44
|
+
- **WCAG 2.x** `relative_luminance`, `contrast_ratio` and `passes_contrast` (AA / AAA).
|
|
45
|
+
- **Blending** of a translucent color over a background with `blend`.
|
|
46
|
+
- **Typed errors**, all `ValueError`: `ColorError`, `HexColorError`, `ColorNameError`,
|
|
47
|
+
`ChannelError`.
|
|
48
|
+
|
|
49
|
+
The full guide, with the exact rules and the sources they come from, is in
|
|
50
|
+
[GUIDE.md](GUIDE.md).
|
|
51
|
+
|
|
52
|
+
## Command line
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
hexrgba normalize "#FFAA00" # #ffaa00
|
|
56
|
+
hexrgba normalize "#aabbcc" --short # #abc
|
|
57
|
+
hexrgba convert "#ff00ffed" --to css # rgba(255, 0, 255, 0.93)
|
|
58
|
+
hexrgba convert "#00ffff" --to name # aqua
|
|
59
|
+
hexrgba contrast "#767676" white # 4.54:1
|
|
60
|
+
hexrgba contrast "#777" white --level AA # prints 4.47:1, exits 1
|
|
61
|
+
hexrgba blend "#0000ff80" red # #7f0080
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Quote a leading `#`: most shells read it as the start of a comment. Errors go to stderr.
|
|
65
|
+
Exit codes: 0 success, 1 the pair fails `--level`, 2 bad usage or an invalid color. `python -m hexrgba` works too.
|
|
66
|
+
|
|
67
|
+
## Why
|
|
68
|
+
|
|
69
|
+
`webcolors` is built around the CSS named-color tables, and most other color packages pull
|
|
70
|
+
in a wider model of color spaces. `hexrgba` stays small and exact: the hex and keyword
|
|
71
|
+
syntax of CSS, checked against the spec tables and the Web Platform Tests vectors, an
|
|
72
|
+
alpha conversion that never drifts, and the WCAG contrast math, with no dependencies.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT. See [LICENSE](LICENSE).
|
|
77
|
+
|
|
78
|
+
## Links
|
|
79
|
+
|
|
80
|
+
- Website: https://lianes8studio.com
|
|
81
|
+
- GitHub: https://github.com/JaviLianes8
|
|
82
|
+
- LinkedIn: https://www.linkedin.com/in/jlianes/
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hexrgba"
|
|
7
|
+
version = "0.2.1"
|
|
8
|
+
description = "CSS hex and named colors: parse, normalize, exact alpha, rgba(), WCAG contrast and blending"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Javier Lianes", email = "javier@lianes8studio.com" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"hex", "color", "css", "rgba", "parse", "normalize", "named-colors", "wcag", "contrast", "blend",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
hexrgba = "hexrgba._cli:main"
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = ["ruff", "mypy", "pytest", "build", "twine"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://lianes8studio.com"
|
|
33
|
+
Repository = "https://github.com/JaviLianes8/hexrgba"
|
|
34
|
+
Issues = "https://github.com/JaviLianes8/hexrgba/issues"
|
|
35
|
+
Changelog = "https://github.com/JaviLianes8/hexrgba/blob/main/CHANGELOG.md"
|
|
36
|
+
LinkedIn = "https://www.linkedin.com/in/jlianes/"
|
|
37
|
+
Funding = "https://buymeacoffee.com/jlianesglrs"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.sdist]
|
|
40
|
+
# Third-party test files are fetched locally and never shipped; only their manifest is.
|
|
41
|
+
include = [
|
|
42
|
+
"src/hexrgba",
|
|
43
|
+
"tests/*.py",
|
|
44
|
+
"tests/fixtures/css-color-4-named-colors.json",
|
|
45
|
+
"tests/upstream/MANIFEST.json",
|
|
46
|
+
"scripts/fetch_upstream.py",
|
|
47
|
+
"scripts/e2e_wheel.py",
|
|
48
|
+
"CHANGELOG.md",
|
|
49
|
+
"GUIDE.md",
|
|
50
|
+
"README.md",
|
|
51
|
+
"LICENSE",
|
|
52
|
+
"pyproject.toml",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.wheel]
|
|
56
|
+
packages = ["src/hexrgba"]
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
line-length = 100
|
|
60
|
+
target-version = "py310"
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint]
|
|
63
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
64
|
+
|
|
65
|
+
[tool.mypy]
|
|
66
|
+
python_version = "3.10"
|
|
67
|
+
strict = true
|
|
68
|
+
|
|
69
|
+
[tool.pytest.ini_options]
|
|
70
|
+
testpaths = ["tests"]
|
|
71
|
+
pythonpath = ["src"]
|
|
72
|
+
addopts = "-q"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Build the wheel, install it in a throwaway venv and drive the installed command.
|
|
2
|
+
|
|
3
|
+
Run from anywhere: ``python scripts/e2e_wheel.py``. Exits 0 when every check passes.
|
|
4
|
+
Needs pip and network access for the build backend. Every example printed by
|
|
5
|
+
``hexrgba --help`` is executed against the installed script and must hold.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import shlex
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import tarfile
|
|
15
|
+
import tempfile
|
|
16
|
+
import zipfile
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run(cmd: list[str], *, cwd: Path, env: dict[str, str] | None = None) -> tuple[int, str, str]:
|
|
23
|
+
done = subprocess.run(cmd, capture_output=True, cwd=cwd, env=env, check=False, timeout=600)
|
|
24
|
+
return done.returncode, done.stdout.decode("utf-8"), done.stderr.decode("utf-8")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check(label: str, condition: bool, detail: object = "") -> None:
|
|
28
|
+
print(("ok " if condition else "FAIL ") + label + ("" if condition else f": {detail!r}"))
|
|
29
|
+
if not condition:
|
|
30
|
+
raise SystemExit(1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def help_examples(help_text: str) -> list[tuple[list[str], int, str]]:
|
|
34
|
+
"""Return (argv, exit code, stdout) for every ``hexrgba ... -> ...`` help line."""
|
|
35
|
+
examples = []
|
|
36
|
+
for line in help_text.splitlines():
|
|
37
|
+
if not line.startswith(" hexrgba ") or "->" not in line:
|
|
38
|
+
continue
|
|
39
|
+
command, expected = (part.strip() for part in line.split("->", 1))
|
|
40
|
+
code = 0
|
|
41
|
+
if expected.startswith("exits 1:"):
|
|
42
|
+
code, expected = 1, expected.removeprefix("exits 1:").split()[0]
|
|
43
|
+
examples.append((shlex.split(command)[1:], code, expected))
|
|
44
|
+
return examples
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main() -> int:
|
|
48
|
+
with tempfile.TemporaryDirectory(prefix="hexrgba-e2e-") as raw:
|
|
49
|
+
work = Path(raw)
|
|
50
|
+
dist = work / "dist"
|
|
51
|
+
code, _, err = run(
|
|
52
|
+
[sys.executable, "-m", "pip", "wheel", "--no-deps", "-w", str(dist), str(ROOT)],
|
|
53
|
+
cwd=work,
|
|
54
|
+
)
|
|
55
|
+
check("wheel builds", code == 0, err[-2000:])
|
|
56
|
+
wheel = next(dist.glob("hexrgba-*.whl"))
|
|
57
|
+
names = zipfile.ZipFile(wheel).namelist()
|
|
58
|
+
check("wheel ships py.typed", "hexrgba/py.typed" in names, names)
|
|
59
|
+
check("wheel has no tests", not any(n.startswith(("tests/", "scripts/")) for n in names))
|
|
60
|
+
entry = [n for n in names if n.endswith("entry_points.txt")]
|
|
61
|
+
text = zipfile.ZipFile(wheel).read(entry[0]).decode() if entry else ""
|
|
62
|
+
check("console script declared", "hexrgba = hexrgba._cli:main" in text, text)
|
|
63
|
+
|
|
64
|
+
code, _, err = run(
|
|
65
|
+
[sys.executable, "-m", "build", "--sdist", "-o", str(dist), "."], cwd=ROOT
|
|
66
|
+
)
|
|
67
|
+
check("sdist builds", code == 0, err[-2000:])
|
|
68
|
+
with tarfile.open(next(dist.glob("hexrgba-*.tar.gz"))) as sdist:
|
|
69
|
+
members = [n.split("/", 1)[-1] for n in sdist.getnames()]
|
|
70
|
+
check("sdist ships the manifest", "tests/upstream/MANIFEST.json" in members, members)
|
|
71
|
+
check("sdist has no WPT page", not any(n.endswith(".html") for n in members), members)
|
|
72
|
+
|
|
73
|
+
venv = work / "venv"
|
|
74
|
+
check("venv created", run([sys.executable, "-m", "venv", str(venv)], cwd=work)[0] == 0)
|
|
75
|
+
bindir = venv / ("Scripts" if os.name == "nt" else "bin")
|
|
76
|
+
python = str(bindir / "python")
|
|
77
|
+
code, _, err = run(
|
|
78
|
+
[python, "-m", "pip", "install", "--no-index", "--no-deps", str(wheel)], cwd=work
|
|
79
|
+
)
|
|
80
|
+
check("wheel installs", code == 0, err[-2000:])
|
|
81
|
+
tool = str(bindir / "hexrgba")
|
|
82
|
+
env = {"PATH": os.environ.get("PATH", ""), "SYSTEMROOT": os.environ.get("SYSTEMROOT", "")}
|
|
83
|
+
|
|
84
|
+
code, out, _ = run([tool, "--version"], cwd=work, env=env)
|
|
85
|
+
check("--version", code == 0 and out.strip().startswith("hexrgba 0.2"), out)
|
|
86
|
+
code, out, _ = run([tool, "--help"], cwd=work, env=env)
|
|
87
|
+
check("--help shows examples", code == 0 and "examples" in out, out)
|
|
88
|
+
examples = help_examples(out)
|
|
89
|
+
check("help has examples", len(examples) >= 8, examples)
|
|
90
|
+
for argv, want_code, want_out in examples:
|
|
91
|
+
code, got, _ = run([tool, *argv], cwd=work, env=env)
|
|
92
|
+
check(f"example {' '.join(argv)}", (code, got.strip()) == (want_code, want_out), got)
|
|
93
|
+
|
|
94
|
+
code, out, err = run([tool, "normalize", "nope"], cwd=work, env=env)
|
|
95
|
+
check("bad input exits 2 on stderr", (code, out) == (2, "") and "error" in err, err)
|
|
96
|
+
code, out, _ = run([python, "-m", "hexrgba", "normalize", "BLACK"], cwd=work, env=env)
|
|
97
|
+
check("python -m hexrgba", (code, out.strip()) == (0, "#000000"), out)
|
|
98
|
+
probe = "import hexrgba; print(hexrgba.Color.parse('rebeccapurple').to_css())"
|
|
99
|
+
code, out, _ = run([python, "-c", probe], cwd=work, env=env)
|
|
100
|
+
check("library import", out.strip() == "rgb(102, 51, 153)", out)
|
|
101
|
+
print("e2e: all checks passed")
|
|
102
|
+
return 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
sys.exit(main())
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Fetch the third-party test files listed in tests/upstream/MANIFEST.json.
|
|
2
|
+
|
|
3
|
+
The files are not part of the repository. Their tests skip until this script has fetched
|
|
4
|
+
them; each download is checked against the size and sha256 in the manifest.
|
|
5
|
+
|
|
6
|
+
python scripts/fetch_upstream.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
import urllib.request
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
FOLDER = Path(__file__).resolve().parents[1] / "tests" / "upstream"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> int:
|
|
21
|
+
manifest = json.loads((FOLDER / "MANIFEST.json").read_text(encoding="utf-8"))
|
|
22
|
+
failed = 0
|
|
23
|
+
for name, entry in manifest.items():
|
|
24
|
+
with urllib.request.urlopen(entry["url"], timeout=60) as response:
|
|
25
|
+
body = response.read()
|
|
26
|
+
digest = hashlib.sha256(body).hexdigest()
|
|
27
|
+
if len(body) != entry["size"] or digest != entry["sha256"]:
|
|
28
|
+
print(f"{name}: got {len(body)} bytes, sha256 {digest}; manifest differs")
|
|
29
|
+
failed += 1
|
|
30
|
+
continue
|
|
31
|
+
(FOLDER / name).write_bytes(body)
|
|
32
|
+
print(f"{name}: ok")
|
|
33
|
+
return 1 if failed else 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
sys.exit(main())
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Parse, validate and normalize CSS hex colors and named colors, with exact alpha,
|
|
2
|
+
CSS serialization, WCAG contrast and alpha blending."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from ._alpha import alpha_to_byte, css_alpha
|
|
7
|
+
from ._blend import blend
|
|
8
|
+
from ._color import Color, is_color, parse_color
|
|
9
|
+
from ._errors import ChannelError, ColorError, ColorNameError, HexColorError
|
|
10
|
+
from ._hex import is_hex, normalize, parse, shorten, to_hex
|
|
11
|
+
from ._names import NAMED_COLORS
|
|
12
|
+
from ._wcag import contrast_ratio, passes_contrast, relative_luminance
|
|
13
|
+
|
|
14
|
+
__version__ = "0.2.1"
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"NAMED_COLORS",
|
|
18
|
+
"ChannelError",
|
|
19
|
+
"Color",
|
|
20
|
+
"ColorError",
|
|
21
|
+
"ColorNameError",
|
|
22
|
+
"HexColorError",
|
|
23
|
+
"alpha_to_byte",
|
|
24
|
+
"blend",
|
|
25
|
+
"contrast_ratio",
|
|
26
|
+
"css_alpha",
|
|
27
|
+
"is_color",
|
|
28
|
+
"is_hex",
|
|
29
|
+
"normalize",
|
|
30
|
+
"parse",
|
|
31
|
+
"parse_color",
|
|
32
|
+
"passes_contrast",
|
|
33
|
+
"relative_luminance",
|
|
34
|
+
"shorten",
|
|
35
|
+
"to_hex",
|
|
36
|
+
]
|