aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--.python-version1
-rw-r--r--LICENSE.txt2
-rw-r--r--MANIFEST.in4
-rwxr-xr-xMakefile19
-rw-r--r--Pipfile16
-rw-r--r--Pipfile.lock297
-rw-r--r--README.rst45
-rw-r--r--docs/Makefile6
-rw-r--r--docs/angles.rst12
-rw-r--r--docs/api.rst13
-rw-r--r--docs/api/angles.rst24
-rw-r--r--docs/api/builtin.rst16
-rw-r--r--docs/api/colors.rst44
-rw-r--r--docs/api/decoders.rst21
-rw-r--r--docs/api/errors.rst10
-rw-r--r--docs/colors.rst16
-rw-r--r--docs/conf.py30
-rw-r--r--docs/discuss.rst11
-rw-r--r--docs/discuss/color-expressions.rst89
-rw-r--r--docs/discuss/colors.rst138
-rw-r--r--docs/expressions.rst89
-rw-r--r--docs/favicon.icobin0 -> 16958 bytes
-rw-r--r--docs/favicon.pngbin0 -> 8752 bytes
-rw-r--r--docs/guides.rst8
-rw-r--r--docs/guides/defining-decoders.rst216
-rw-r--r--docs/index.rst37
-rw-r--r--docs/onboarding.rst12
-rw-r--r--docs/onboarding/installing.rst36
-rw-r--r--docs/onboarding/trying.rst47
-rw-r--r--docs/requirements.txt4
-rw-r--r--requirements.txt0
-rw-r--r--setup.cfg42
-rwxr-xr-xsetup.py19
-rwxr-xr-xtests/__init__.py8
-rwxr-xr-xtests/test_angles.py57
-rwxr-xr-xtests/test_builtin.py74
-rwxr-xr-xtests/test_colors.py346
-rwxr-xr-xtests/test_decoders.py257
-rwxr-xr-xtests/test_text.py73
-rwxr-xr-xthcolor/__init__.py26
-rwxr-xr-xthcolor/_angle.py295
-rwxr-xr-xthcolor/_color.py1131
-rwxr-xr-xthcolor/_exc.py161
-rwxr-xr-xthcolor/_ref.py494
-rwxr-xr-xthcolor/_sys.py237
-rw-r--r--thcolor/angles.py280
-rw-r--r--thcolor/builtin.py528
-rwxr-xr-xthcolor/builtin/__init__.py15
-rwxr-xr-xthcolor/builtin/_css.py450
-rwxr-xr-xthcolor/builtin/_default.py232
-rw-r--r--thcolor/colors.py1424
-rw-r--r--thcolor/decoders.py1240
-rwxr-xr-xthcolor/errors.py65
-rw-r--r--thcolor/utils.py56
-rwxr-xr-xthcolor/version.py12
56 files changed, 5136 insertions, 3653 deletions
diff --git a/.gitignore b/.gitignore
index fceabbb..baae2cb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,10 +3,14 @@ __pycache__
/*.egg-info
/dist
/.spyproject
+/.tool-versions
+/.python-version
+/*.kdev4
/build
/docs/_build
/venv
/README.html
/.pytest_cache
+/.python-version
/docs/_build
diff --git a/.python-version b/.python-version
deleted file mode 100644
index 0b2eb36..0000000
--- a/.python-version
+++ /dev/null
@@ -1 +0,0 @@
-3.7.2
diff --git a/LICENSE.txt b/LICENSE.txt
index d1defab..bbb3793 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (C) 2018-2019 Thomas Touhey <thomas@touhey.fr>
+Copyright (C) 2018-2022 Thomas Touhey <thomas@touhey.fr>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
diff --git a/MANIFEST.in b/MANIFEST.in
index c0eb105..cd7c82e 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -3,8 +3,12 @@ include LICENSE.txt
include MANIFEST.in
include setup.py
include setup.cfg
+include requirements.txt
+include docs/*.txt
include docs/*.rst
+include docs/**/*.rst
+include docs/*.png
include docs/conf.py
include docs/Makefile
include docs/make.bat
diff --git a/Makefile b/Makefile
index 3d840bc..2dc851e 100755
--- a/Makefile
+++ b/Makefile
@@ -1,28 +1,19 @@
#!/usr/bin/make -f
- PE := pipenv run
- ST := $(PE) ./setup.py
- DNAME := dist/$(shell $(ST) --name)-$(shell $(ST) --version).tar.gz
+ DNAME := dist/$(shell ./setup.py --name)-$(shell ./setup.py --version).tar.gz
test tests:
- @$(PE) pytest -s -q
-
-prepare:
- @pipenv install --dev
-update:
- @pipenv update --dev
+ @pytest -s
docs:
- @$(ST) build_sphinx
-
-checkdocs:
- @$(ST) checkdocs
+ @./setup.py build_sphinx
dist: $(DNAME)
$(DNAME):
- @$(ST) sdist
+ @./setup.py sdist
upload: $(DNAME)
@twine upload $(DNAME)
.PHONY: test tests dist docs
+
# End of file.
diff --git a/Pipfile b/Pipfile
deleted file mode 100644
index 2b67cce..0000000
--- a/Pipfile
+++ /dev/null
@@ -1,16 +0,0 @@
-[[source]]
-url = 'https://pypi.python.org/simple'
-verify_ssl = true
-name = 'pypi'
-
-[requires]
-python_version = '3.7'
-
-[packages]
-regex = '*'
-
-[dev-packages]
-sphinx = '*'
-"collective.checkdocs" = '*'
-pudb = '*'
-pytest = '*'
diff --git a/Pipfile.lock b/Pipfile.lock
deleted file mode 100644
index 9758a34..0000000
--- a/Pipfile.lock
+++ /dev/null
@@ -1,297 +0,0 @@
-{
- "_meta": {
- "hash": {
- "sha256": "0376534457dcacd720d6a912bf87bdf10739752646bf34d5c06ef3de9863f538"
- },
- "pipfile-spec": 6,
- "requires": {
- "python_version": "3.7"
- },
- "sources": [
- {
- "name": "pypi",
- "url": "https://pypi.python.org/simple",
- "verify_ssl": true
- }
- ]
- },
- "default": {
- "regex": {
- "hashes": [
- "sha256:020429dcf9b76cc7648a99c81b3a70154e45afebc81e0b85364457fe83b525e4",
- "sha256:0552802b1c3f3c7e4fee8c85e904a13c48226020aa1a0593246888a1ac55aaaf",
- "sha256:308965a80b92e1fec263ac1e4f1094317809a72bc4d26be2ec8a5fd026301175",
- "sha256:4d627feef04eb626397aa7bdec772774f53d63a1dc7cc5ee4d1bd2786a769d19",
- "sha256:93d1f9fcb1d25e0b4bd622eeba95b080262e7f8f55e5b43c76b8a5677e67334c",
- "sha256:c3859bbf29b1345d694f069ddfe53d6907b0393fda5e3794c800ad02902d78e9",
- "sha256:d56ce4c7b1a189094b9bee3b81c4aeb3f1ba3e375e91627ec8561b6ab483d0a8",
- "sha256:ebc5ef4e10fa3312fa1967dc0a894e6bd985a046768171f042ac3974fadc9680",
- "sha256:f9cd39066048066a4abe4c18fb213bc541339728005e72263f023742fb912585"
- ],
- "index": "pypi",
- "version": "==2019.4.14"
- }
- },
- "develop": {
- "alabaster": {
- "hashes": [
- "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
- "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
- ],
- "version": "==0.7.12"
- },
- "atomicwrites": {
- "hashes": [
- "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
- "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
- ],
- "version": "==1.3.0"
- },
- "attrs": {
- "hashes": [
- "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
- "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
- ],
- "version": "==19.1.0"
- },
- "babel": {
- "hashes": [
- "sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
- "sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
- ],
- "version": "==2.6.0"
- },
- "certifi": {
- "hashes": [
- "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5",
- "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"
- ],
- "version": "==2019.3.9"
- },
- "chardet": {
- "hashes": [
- "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
- "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
- ],
- "version": "==3.0.4"
- },
- "collective.checkdocs": {
- "hashes": [
- "sha256:3a5328257c5224bc72753820c182910d7fb336bc1dba5e09113d48566655e46e"
- ],
- "index": "pypi",
- "version": "==0.2"
- },
- "docutils": {
- "hashes": [
- "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
- "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
- "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
- ],
- "version": "==0.14"
- },
- "idna": {
- "hashes": [
- "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
- "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
- ],
- "version": "==2.8"
- },
- "imagesize": {
- "hashes": [
- "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
- "sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
- ],
- "version": "==1.1.0"
- },
- "jinja2": {
- "hashes": [
- "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
- "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
- ],
- "version": "==2.10.1"
- },
- "markupsafe": {
- "hashes": [
- "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
- "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
- "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
- "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
- "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
- "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
- "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
- "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
- "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
- "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
- "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
- "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
- "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
- "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
- "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
- "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
- "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
- "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
- "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
- "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
- "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
- "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
- "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
- "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
- "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
- "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
- "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
- "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
- ],
- "version": "==1.1.1"
- },
- "more-itertools": {
- "hashes": [
- "sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7",
- "sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a"
- ],
- "markers": "python_version > '2.7'",
- "version": "==7.0.0"
- },
- "packaging": {
- "hashes": [
- "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af",
- "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3"
- ],
- "version": "==19.0"
- },
- "pluggy": {
- "hashes": [
- "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f",
- "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746"
- ],
- "version": "==0.9.0"
- },
- "pudb": {
- "hashes": [
- "sha256:ac30cfc64580958ab7265decb4cabb9141f08781ff072e9a336d5a7942ce35a6"
- ],
- "index": "pypi",
- "version": "==2019.1"
- },
- "py": {
- "hashes": [
- "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
- "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
- ],
- "version": "==1.8.0"
- },
- "pygments": {
- "hashes": [
- "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a",
- "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d"
- ],
- "version": "==2.3.1"
- },
- "pyparsing": {
- "hashes": [
- "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a",
- "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03"
- ],
- "version": "==2.4.0"
- },
- "pytest": {
- "hashes": [
- "sha256:3773f4c235918987d51daf1db66d51c99fac654c81d6f2f709a046ab446d5e5d",
- "sha256:b7802283b70ca24d7119b32915efa7c409982f59913c1a6c0640aacf118b95f5"
- ],
- "index": "pypi",
- "version": "==4.4.1"
- },
- "pytz": {
- "hashes": [
- "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda",
- "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"
- ],
- "version": "==2019.1"
- },
- "requests": {
- "hashes": [
- "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
- "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
- ],
- "version": "==2.21.0"
- },
- "six": {
- "hashes": [
- "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
- "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
- ],
- "version": "==1.12.0"
- },
- "snowballstemmer": {
- "hashes": [
- "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
- "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
- ],
- "version": "==1.2.1"
- },
- "sphinx": {
- "hashes": [
- "sha256:423280646fb37944dd3c85c58fb92a20d745793a9f6c511f59da82fa97cd404b",
- "sha256:de930f42600a4fef993587633984cc5027dedba2464bcf00ddace26b40f8d9ce"
- ],
- "index": "pypi",
- "version": "==2.0.1"
- },
- "sphinxcontrib-applehelp": {
- "hashes": [
- "sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897",
- "sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"
- ],
- "version": "==1.0.1"
- },
- "sphinxcontrib-devhelp": {
- "hashes": [
- "sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34",
- "sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"
- ],
- "version": "==1.0.1"
- },
- "sphinxcontrib-htmlhelp": {
- "hashes": [
- "sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422",
- "sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"
- ],
- "version": "==1.0.2"
- },
- "sphinxcontrib-jsmath": {
- "hashes": [
- "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
- "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
- ],
- "version": "==1.0.1"
- },
- "sphinxcontrib-qthelp": {
- "hashes": [
- "sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20",
- "sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"
- ],
- "version": "==1.0.2"
- },
- "sphinxcontrib-serializinghtml": {
- "hashes": [
- "sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227",
- "sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"
- ],
- "version": "==1.1.3"
- },
- "urllib3": {
- "hashes": [
- "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4",
- "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb"
- ],
- "version": "==1.24.3"
- },
- "urwid": {
- "hashes": [
- "sha256:644d3e3900867161a2fc9287a9762753d66bd194754679adb26aede559bcccbc"
- ],
- "version": "==2.0.1"
- }
- }
-}
diff --git a/README.rst b/README.rst
index 759e796..357b918 100644
--- a/README.rst
+++ b/README.rst
@@ -10,48 +10,11 @@ module. It provides the following features:
For more information, consult `the official website`_.
-Examples
---------
-
-Converting an RGB color to HSL:
-
-.. code-block:: python
-
- from thcolor import Color
-
- color = Color(Color.Type.RGB, 55, 23, 224)
- print(color.hsl())
-
-Converting a HSL color to RGB with an alpha value:
-
-.. code-block:: python
-
- from thcolor import Color, Angle
-
- alpha = 0.75
- color = Color(Color.Type.HSL, Angle(Angle.Type.DEG, 180), 0.5, 1.0, alpha)
- print(color.rgba())
-
-Converting a textual representation to the RGBA color components:
-
-.. code-block:: python
-
- from thcolor import Color
-
- color = Color.from_text("darker(10%, hsl(0, 1, 50.0%))")
- print(color.rgba())
-
-Getting the CSS color representations (with compatibility for earlier CSS
-versions) from a textual representation:
-
-.. code-block:: python
-
- from thcolor import Color
-
- color = Color.from_text("gray(red( #123456 )/0.2/)")
- for repres in color.css():
- print(f"color: {repres}")
+For getting started with the module, an onboarding is available on
+`the official documentation`_, as well as guides, discussion topics and
+references.
.. _Thomas Touhey: https://thomas.touhey.fr/
.. _textoutpc: https://textout.touhey.pro/
.. _the official website: https://thcolor.touhey.pro/
+.. _the official documentation: https://thcolor.touhey.pro/docs/
diff --git a/docs/Makefile b/docs/Makefile
index 28d11c9..26375a4 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -2,12 +2,13 @@
#
# You can set these variables from the command line.
+
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXWATCH = sphinx-autobuild
SOURCEDIR = .
BUILDDIR = _build
-WEBROOT = thcolor.touhey.pro:thcolor_doc
+WEBROOT = hercule:thcolor/docs
# Put it first so that "make" without argument is like "make help".
help:
@@ -22,7 +23,8 @@ help:
# Livehtml build.
livehtml:
- $(SPHINXWATCH) -b html -z ../thcolor $(SPHINXOPTS) . $(BUILDDIR)/html
+ $(SPHINXWATCH) -b html --watch ../thcolor --ignore "**/.*.kate-swp" \
+ $(SPHINXOPTS) . $(BUILDDIR)/html
.PHONY: livehtml
diff --git a/docs/angles.rst b/docs/angles.rst
deleted file mode 100644
index 71d52a9..0000000
--- a/docs/angles.rst
+++ /dev/null
@@ -1,12 +0,0 @@
-Angles
-======
-
-Some color representations use angles as some of their properties. Angles can
-have one of the following types:
-
-.. autoclass:: thcolor.Angle.Type
-
-Angles in ``thcolor`` are instances of the following class:
-
-.. autoclass:: thcolor.Angle
- :members: type, degrees, gradiants, radiants, turns
diff --git a/docs/api.rst b/docs/api.rst
new file mode 100644
index 0000000..3b41a3b
--- /dev/null
+++ b/docs/api.rst
@@ -0,0 +1,13 @@
+API Reference
+=============
+
+If you are looking for information on a specific function, class or method,
+this part of the documentation is for you.
+
+.. toctree::
+
+ api/errors
+ api/angles
+ api/colors
+ api/decoders
+ api/builtin
diff --git a/docs/api/angles.rst b/docs/api/angles.rst
new file mode 100644
index 0000000..67a37a2
--- /dev/null
+++ b/docs/api/angles.rst
@@ -0,0 +1,24 @@
+Angles API
+==========
+
+.. py:module:: thcolor.angles
+
+Some color representations use angles as some of their properties. The base
+class for angles is the following:
+
+.. autoclass:: Angle
+ :members: asdegrees, asgradians, asradians, asturns, asprincipal, fromtext
+
+Subclasses are the following:
+
+.. autoclass:: DegreesAngle
+ :members: degrees
+
+.. autoclass:: GradiansAngle
+ :members: gradians
+
+.. autoclass:: RadiansAngle
+ :members: radians
+
+.. autoclass:: TurnsAngle
+ :members: turns
diff --git a/docs/api/builtin.rst b/docs/api/builtin.rst
new file mode 100644
index 0000000..68c92a5
--- /dev/null
+++ b/docs/api/builtin.rst
@@ -0,0 +1,16 @@
+Builtin API
+===========
+
+.. py:module:: thcolor.builtin
+
+The following color decoders are defined:
+
+.. autoclass:: CSS1ColorDecoder
+
+.. autoclass:: CSS2ColorDecoder
+
+.. autoclass:: CSS3ColorDecoder
+
+.. autoclass:: CSS4ColorDecoder
+
+.. autoclass:: DefaultColorDecoder
diff --git a/docs/api/colors.rst b/docs/api/colors.rst
new file mode 100644
index 0000000..ac6e814
--- /dev/null
+++ b/docs/api/colors.rst
@@ -0,0 +1,44 @@
+Colors API
+==========
+
+.. py:module:: thcolor.colors
+
+The base class for colors is the following:
+
+.. autoclass:: Color
+ :members: alpha, assrgb, ashsl, ashsv, ashwb, ascmyk, aslab, aslch, asxyz,
+ asyiq, asyuv, fromtext, replace, darker, lighter, desaturate,
+ saturate, css
+
+Subclasses are the following:
+
+.. autoclass:: CMYKColor
+ :members: cyan, magenta, yellow, black, alpha
+
+.. autoclass:: HSLColor
+ :members: hue, saturation, lightness, alpha
+
+.. autoclass:: HSVColor
+ :members: hue, saturation, value, alpha
+
+.. autoclass:: HWBColor
+ :members: hue, whiteness, blackness, alpha
+
+.. autoclass:: LABColor
+ :members: lightness, a, b, alpha
+
+.. autoclass:: LCHColor
+ :members: lightness, chroma, hue, alpha
+
+.. autoclass:: SRGBColor
+ :members: red, green, blue, alpha, frombytes,
+ fromnetscapecolorname, asbytes
+
+.. autoclass:: XYZColor
+ :members: x, y, z, alpha
+
+.. autoclass:: YIQColor
+ :members: y, i, q, alpha
+
+.. autoclass:: YUVColor
+ :members: y, u, v, alpha
diff --git a/docs/api/decoders.rst b/docs/api/decoders.rst
new file mode 100644
index 0000000..abab8e7
--- /dev/null
+++ b/docs/api/decoders.rst
@@ -0,0 +1,21 @@
+Decoders API
+============
+
+.. py:module:: thcolor.decoders
+
+The ``thcolor.decoders`` module defines base utilities for decoding color
+expressions.
+
+.. autoclass:: ColorDecoder
+ :members: decode
+
+For convenience, the following class is defined to define color decoders:
+
+.. autoclass:: MetaColorDecoder
+
+.. autoclass:: alias
+
+.. autofunction:: fallback
+
+See :ref:`defining-decoders` for more information about how to use these
+classes.
diff --git a/docs/api/errors.rst b/docs/api/errors.rst
new file mode 100644
index 0000000..11a8cdc
--- /dev/null
+++ b/docs/api/errors.rst
@@ -0,0 +1,10 @@
+Errors API
+==========
+
+.. py:module:: thcolor.errors
+
+In this section, are defined all non-standard errors that are raised by
+all submodules.
+
+.. autoclass:: ColorExpressionSyntaxError
+ :members: text, column, func
diff --git a/docs/colors.rst b/docs/colors.rst
deleted file mode 100644
index 7bff8d1..0000000
--- a/docs/colors.rst
+++ /dev/null
@@ -1,16 +0,0 @@
-Colors
-======
-
-Colors can have one of the following types:
-
-.. autoclass:: thcolor.Color.Type
-
-RGB colors can have one of the following profiles:
-
-.. autoclass:: thcolor.Color.Profile
-
-Colors are represented in ``thcolor`` as instances of the following class:
-
-.. autoclass:: thcolor.Color
- :members: type, rgb, rgba, hls, hlsa, hwb, hwba, cmyk, cmyka, lab, laba,
- lch, lcha, xyz, xyza, css
diff --git a/docs/conf.py b/docs/conf.py
index 0627b7e..9b6148e 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -10,11 +10,13 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
+
def _add_paths():
- import os
- import sys
+ import os
+ import sys
+
+ sys.path.insert(0, os.path.abspath('..'))
- sys.path.insert(0, os.path.abspath('..'))
_add_paths()
@@ -26,13 +28,12 @@ author = 'Thomas Touhey'
# The full version, including alpha/beta/rc tags
+
def _get_release():
- from os.path import dirname, join
- from pkg_resources import find_distributions as find_dist
+ from thcolor.version import version
+
+ return version
- module_path = join(dirname(__file__), '..')
- dist = next(find_dist(module_path, True))
- return dist.version
release = _get_release()
@@ -42,16 +43,22 @@ release = _get_release()
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
- 'sphinx.ext.autodoc'
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.todo',
+ 'sphinx_autodoc_typehints',
]
+todo_include_todos = True
+
# Add any paths that contain templates here, relative to this directory.
templates_path = []
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
-exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '.goutput*']
+exclude_patterns = [
+ '_build', 'Thumbs.db', '.DS_Store', '.goutput*', 'requirements.txt',
+]
# -- Options for HTML output -------------------------------------------------
@@ -59,7 +66,8 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '.goutput*']
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
-html_theme = 'sphinx_rtd_theme'
+html_theme = 'bizstyle'
+html_favicon = 'favicon.png'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
diff --git a/docs/discuss.rst b/docs/discuss.rst
new file mode 100644
index 0000000..4e3a254
--- /dev/null
+++ b/docs/discuss.rst
@@ -0,0 +1,11 @@
+Discussion topics
+=================
+
+thcolor has a number of concepts necessary for a full understanding of its
+conception and an efficient use of it. You can find these concepts and
+discussion topics in the following sections.
+
+.. toctree::
+
+ discuss/colors
+ discuss/color-expressions
diff --git a/docs/discuss/color-expressions.rst b/docs/discuss/color-expressions.rst
new file mode 100644
index 0000000..f29578e
--- /dev/null
+++ b/docs/discuss/color-expressions.rst
@@ -0,0 +1,89 @@
+.. _expr:
+
+Color expressions
+=================
+
+One of the aims of thcolor was to decode text-based expressions
+representing colors with possibilities challenging and even outdo
+CSS color expression possibilities. In thcolor, these expressions are
+evaluated using :py:meth:`thcolor.decoders.ColorDecoder.decode`,
+:py:meth:`thcolor.colors.Color.fromtext` or
+:py:meth:`thcolor.angles.Angle.fromtext`.
+
+Concepts
+--------
+
+The goal of these expressions was to embrace and extend CSS syntax, so they
+are basically either basic expressions or function calls, with the following
+argument types:
+
+ * Numbers.
+ * Percentages.
+ * Angles.
+ * Colors.
+
+These elements are separated by separators (either commas, slashes, or
+spaces) and can be passed to functions, and the calls themselves can be passed
+to other functions. A function call is made in the following fashion:
+
+::
+
+ <function name>([<number | percentage | angle | color> [<separator> …]])
+
+If at least one separator (even spaces) are required between arguments,
+extraneous separators between and after the arguments are ignored. Other than
+if spaces are used as separators, spaces around the parenthesis or the
+separators (and "between" the separators as spaces are recognized as
+separators) are ignored.
+
+Here are some example calls:
+
+::
+
+ rgb(1, 2, 3)
+ rgb ( 1 22 //// 242 , 50.0% ,/,)
+ hsl (0 1 50 % / 22)
+ gray ( red( #123456 )/0.2/)
+
+.. _explain-decoders:
+
+Decoders
+--------
+
+Decoders are the tools within thcolor that handle the actual evaluation of
+expressions. They have two important components aside from the decoding
+function:
+
+* Options defining the behaviour of base parsing and evaluating.
+* A reference of functions, colors and other data indexed by name.
+
+The options currently available are the following:
+
+* Whether natural colors are enabled or not; see :ref:`natural-colors`.
+* Whether extended hexadecimal colors, i.e. 4 or 8 digit hexadecimal digits
+ preceded with a '#' sign, which includes transparency, are enabled or not.
+* Whether names that are not found should be resolved using Netscape-like
+ color parsing or not, what is also known as "quirks mode".
+
+The reference contains three kinds of elements, all indexed by name:
+
+* Constants, always to be used as values, e.g. the ``navy`` color.
+* Functions, always to be used by calling them, e.g. the ``rgb`` function.
+* Functions with fallbacks, which have a behaviour when called and a fallback
+ value when used as a value, e.g. the ``red`` function in the extended
+ color decoder which extracts the red channel value as a function and acts
+ as the color ``red`` if used as a value.
+
+Decoders can be defined using two base classes which act in a very different
+way:
+
+* **The manual way.**
+ You can define a decoder manually by making it inherit from
+ :py:class:`thcolor.decoders.ColorDecoder` and overriding
+ ``__mapping__`` to define the reference, and other double-underscore
+ properties for options.
+* **The user-friendly way.**
+ You can define a decoder in an easier way by making it inherit from
+ :py:class:`thcolor.decoders.MetaColorDecoder` and defining options,
+ functions and data by defining them directly in the class body;
+ see :ref:`defining-decoders` for more information.
diff --git a/docs/discuss/colors.rst b/docs/discuss/colors.rst
new file mode 100644
index 0000000..fb5c20d
--- /dev/null
+++ b/docs/discuss/colors.rst
@@ -0,0 +1,138 @@
+Color representations
+=====================
+
+thcolor allows you to represent colors in different colorspaces and using
+different kind of coordinates. This page regroups the information about these
+representations.
+
+Note that all color representations have an ``alpha`` component.
+
+sRGB colors
+-----------
+
+sRGB colors are colors represented using the red, green and blue channel
+intensities in the `standard RGB color space`_.
+
+In thcolor, such a color is represented by the
+:py:class:`thcolor.colors.SRGBColor` class, and other representations can be
+converted to this one using the :py:meth:`thcolor.colors.Color.assrgb`
+("as sRGB") method.
+
+CMYK colors
+-----------
+
+CMYK colors are colors represented using the cyan, magenta, yellow and
+black values in the `CMYK color model`_.
+
+In thcolor, such a color is represented by the
+:py:class:`thcolor.colors.CMYKColor` class, and other representations can be
+converted to this one using the :py:meth:`thcolor.colors.Color.ascmyk` method.
+
+HSL colors
+----------
+
+HSL colors are colors represented using the hue, saturation and lightness
+as polar coordinates in the sRGB color space. See `HSL and HSV`_ for more
+information.
+
+In thcolor, such a color is represented by the
+:py:class:`thcolor.colors.HSLColor` class, and other representations can be
+converted to this one using the :py:meth:`thcolor.colors.Color.ashsl` method.
+
+HSV colors
+----------
+
+HSV colors are colors represented using the hue, saturation and value
+as polar coordinates in the sRGB color space. See `HSL and HSV`_ for
+more information.
+
+In thcolor, such a color is represented by the
+:py:class:`thcolor.colors.HSVColor` class, and other representations can be
+converted to this one using the :py:meth:`thcolor.colors.Color.ashsv` method.
+
+.. _hwb-colors:
+
+HWB colors
+----------
+
+HWB colors are colors represented using the hue, whiteness and blackness
+in the `HWB color model`_.
+
+In thcolor, such a color is represented by the
+:py:class:`thcolor.colors.HWBColor` class, and other representations can be
+converted to this one using the :py:meth:`thcolor.colors.Color.ashwb` method.
+
+LAB colors
+----------
+
+LAB colors are colors represented using the lightness and A and B coordinates
+in the `CIELAB color space`_.
+
+In thcolor, such a color is represented by the
+:py:class:`thcolor.colors.LABColor` class, and other representations can be
+converted to this one using the :py:meth:`thcolor.colors.Color.aslab` method.
+
+LCH colors
+----------
+
+LCH colors are colors represented using the lightness, chroma and hue
+in the `CIELAB color space`_.
+
+In thcolor, such a color is represented by the
+:py:class:`thcolor.colors.LCHColor` class, and other representations can be
+converted to this one using the :py:meth:`thcolor.colors.Color.aslch` method.
+
+XYZ colors
+----------
+
+XYZ colors are colors represented using its x, y and z coordinates
+in the `CIE 1931 XYZ color space`_.
+
+In thcolor, such a color is represented by the
+:py:class:`thcolor.colors.XYZColor` class, and other representations can be
+converted to this one using the :py:meth:`thcolor.colors.Color.asxyz`
+method.
+
+YIQ colors
+----------
+
+YIQ colors are colors represented using its coordinates in the
+`YIQ color space`_.
+
+In thcolor, such a color is represented by the
+:py:class:`thcolor.colors.YIQColor` class, and other representations can be
+converted to this one using the :py:meth:`thcolor.colors.Color.asyiq` method.
+
+YUV colors
+----------
+
+YUV colors are colors represented using its coordinates in the
+`YUV color space`_.
+
+In thcolor, such a color is represented by the
+:py:class:`thcolor.colors.YUVColor` class, and other representations can be
+converted to this one using the :py:meth:`thcolor.colors.Color.asyuv`
+method.
+
+.. _natural-colors:
+
+Natural colors
+--------------
+
+Natural colors (NCol) are an initiative from W3Schools to make a color
+that is easily identifiable from reading its definition.
+
+In thcolor, no dedicated class exists for this representation since it is
+a light derivative from :ref:`HWB colors <hwb-colors>` with the angle
+expressed using a given format. It is, however, optionally supported
+in expressions behind the ``__ncol_support__`` option.
+
+.. _standard RGB color space: https://en.wikipedia.org/wiki/SRGB
+.. _CMYK color model: https://en.wikipedia.org/wiki/CMYK_color_model
+.. _HSL and HSV: https://en.wikipedia.org/wiki/HSL_and_HSV
+.. _HWB color model: https://en.wikipedia.org/wiki/HWB_color_model
+.. _CIELAB color space: https://en.wikipedia.org/wiki/CIELAB_color_space
+.. _CIE 1931 XYZ color space: https://en.wikipedia.org/wiki/CIE_1931_color_space
+.. _YIQ color space: https://en.wikipedia.org/wiki/YIQ
+.. _YUV color space: https://en.wikipedia.org/wiki/YUV
+.. _`natural colors`: https://www.w3schools.com/colors/colors_ncol.asp
diff --git a/docs/expressions.rst b/docs/expressions.rst
deleted file mode 100644
index 6f9e645..0000000
--- a/docs/expressions.rst
+++ /dev/null
@@ -1,89 +0,0 @@
-.. _expr:
-
-Expressions
-===========
-
-One of the aims of the ``thcolor`` module was to decode text-based expressions
-representing colors with possibilities challenging and even outdo
-CSS color expression possibilities. This is what the following static method
-is for:
-
-.. automethod:: thcolor.Color.from_text
-
-Expression concepts
--------------------
-
-The goal of these expressions was to embrace and extend CSS syntax, so they
-are basically either basic expressions or function calls, with the following
-argument types:
-
-.. autoclass:: thcolor.Reference.number
- :members:
-
-.. autoclass:: thcolor.Reference.percentage
- :members:
-
-.. autoclass:: thcolor.Reference.angle
- :members:
-
-.. autoclass:: thcolor.Reference.color
- :members:
-
-These elements are separated by separators (either commas, slashes, or simple
-spaces) and can be passed to functions, and the calls themselves can be passed
-to other functions. A function call is made in the following fashion:
-
-::
-
- <function name>(<number | percentage | angle | color> [<separator> …])
-
-If at least one separator (even simple spaces) are required between arguments,
-extraneous separators between and after the arguments are ignored. Other than
-if spaces are used as separators, spaces around the parenthesis or the
-separators (and "between" the separators as spaces are recognized as
-separators) are ignored.
-
-Here are some example calls:
-
-::
-
- rgb(1, 2, 3)
- rgb ( 1 22 //// 242 , 50.0% ,/,)
- hsl (0 1 50 % / 22)
- gray ( red( #123456 )/0.2/)
-
-In case of incorrectly formatted string, the following exception is returned:
-
-.. autoexception:: thcolor.ColorExpressionDecodingError
- :members:
-
-Defining a reference
---------------------
-
-Functions and color names are defined in a reference:
-
-- color names are defined behind an overload of
- :meth:`thcolor.Reference._color`.
-- functions are defined as reference class methods and use `type hints
- <https://www.python.org/dev/peps/pep-0484/>`_ to describe the types
- they are expecting.
-
-The reference must be a derivative of the following class:
-
-.. autoclass:: thcolor.Reference
- :members: _color, functions, colors, default
-
-Builtin references
-------------------
-
-The following references are defined:
-
-.. autoclass:: thcolor.CSS1Reference
-
-.. autoclass:: thcolor.CSS2Reference
-
-.. autoclass:: thcolor.CSS3Reference
-
-.. autoclass:: thcolor.CSS4Reference
-
-.. autoclass:: thcolor.DefaultReference
diff --git a/docs/favicon.ico b/docs/favicon.ico
new file mode 100644
index 0000000..2946f11
--- /dev/null
+++ b/docs/favicon.ico
Binary files differ
diff --git a/docs/favicon.png b/docs/favicon.png
new file mode 100644
index 0000000..4ebff7a
--- /dev/null
+++ b/docs/favicon.png
Binary files differ
diff --git a/docs/guides.rst b/docs/guides.rst
new file mode 100644
index 0000000..8f5a931
--- /dev/null
+++ b/docs/guides.rst
@@ -0,0 +1,8 @@
+Guides
+======
+
+This section consists of multiple guides for solving specific problems.
+
+.. toctree::
+
+ guides/defining-decoders
diff --git a/docs/guides/defining-decoders.rst b/docs/guides/defining-decoders.rst
new file mode 100644
index 0000000..c9c1da5
--- /dev/null
+++ b/docs/guides/defining-decoders.rst
@@ -0,0 +1,216 @@
+.. _defining-decoders:
+
+Defining a decoder
+==================
+
+The goal of this guide is to bootstrap you into making your own color decoder
+using tools from thcolor. For clarity, you should first read :ref:`expr`,
+especially the :ref:`explain-decoders` section.
+
+For this guide, we will want to do the following:
+
+* Create our ``TutorialColorDecoder`` with extended hexadecimal color
+ support.
+* Create a ``sum`` function which can take 2 to 5 numbers and sum them.
+* Create a ``diff`` function which takes 2 numbers and return the first one
+ minus the second one.
+* Create a ``reverse_diff`` which takes 2 numbers and return the second one
+ minus the first one.
+* Create an ``transparent`` function which returns the alpha value
+ as a byte between 0 and 255, and returns the transparent color when
+ used as a value.
+* Create a ``awesome-color`` constant which evaluates as blue.
+
+Setting up the framework
+------------------------
+
+First of, the elements you will want to import are the following:
+
+* :py:class:`thcolor.decoders.MetaColorDecoder`, which will be the base
+ class your decoder will inherit from.
+* :py:class:`thcolor.decoders.alias`, which will be useful when you want to
+ define an alias for an existing function without doing it manually.
+* :py:class:`thcolor.decoders.fallback`, which will be useful when you want
+ to define a fallback value for a given function.
+* Colors we might want to use from :py:mod:`thcolor.colors`.
+* Angles we might want to use from :py:mod:`thcolor.angles`.
+
+For our exercise, we are going to use :py:class:`thcolor.colors.SRGBColor`
+and no angles.
+
+Our base code is the following:
+
+.. code-block:: python
+
+ from thcolor.decoders import MetaColorDecoder, alias, fallback
+ from thcolor.colors import Color, SRGBColor
+
+ __all__ = ['TutorialColorDecoder']
+
+ class TutorialColorDecoder(MetaColorDecoder):
+ pass
+
+As we want extended hexadecimal colors to be read as well, we need to define
+the corresponding option. As described in
+:py:class:`thcolor.decoders.ColorDecoder`, the option for doing this is
+``__extended_hex_support__``, which gives the following class definition:
+
+.. code-block::
+
+ class TutorialColorDecoder(MetaColorDecoder):
+ __extended_hex_support__ = True
+
+Defining the ``sum`` function
+-----------------------------
+
+As described in the constraints, the ``sum`` function will sum 2 to 5
+numbers; the type of these numbers is not given, so we have three options:
+
+* If the hint on these numbers is ``int``, only integers will be accepted
+ by the function, and floats with decimal parts will throw an error.
+* If the hint on these numbers is ``float``, integers will be converted
+ to ``float`` before being passed on to the function.
+* If the hint on these numbers is ``typing.Union[int, float]``, the
+ function will receive both depending on what the syntax was in the
+ original expression.
+
+Here, we choose to use ``float``. The resulting code is the following:
+
+.. code-block::
+
+ class TutorialColorDecoder(MetaColorDecoder):
+ # ...
+
+ def sum(
+ a: float, b: float, c: float = 0,
+ d: float = 0, e: float = 0,
+ ) -> float:
+ return a + b + c + d + e
+
+Defining the ``diff`` and ``reverse_diff`` functions
+----------------------------------------------------
+
+As described in the constraints, the ``diff`` function will compute the
+difference between two numbers. Based on what we've learned in the previous
+function, we can do the following:
+
+.. code-block::
+
+ class TutorialColorDecoder(MetaColorDecoder):
+ # ...
+
+ def diff(a: float, b: float) -> float:
+ return a - b
+
+Now for the ``reverse_diff`` function, we could define it independently,
+but in order to save time, we will use :py:class:`thcolor.decoders.alias`.
+This function takes the following arguments:
+
+* The name of the function to alias.
+* The new order of the arguments, by name.
+
+Based on this information, the :py:class:`thcolor.decoders.MetaColorDecoder`
+will create the function out of the previous function (by calling it) and
+take all related annotations and default values (if possible and available).
+
+For our current case, we can define our function the following way:
+
+.. code-block::
+
+ class TutorialColorDecoder(MetaColorDecoder):
+ # ...
+
+ reverse_diff = alias('diff', args=('b', 'a'))
+
+Note that aliases can be defined anywhere within the class, even before
+the aliased function.
+
+Defining the ``transparent`` function with a fallback value
+-----------------------------------------------------------
+
+For the ``transparent`` function, we will get the alpha value, multiply it
+by 255 and round it to the nearest integer. However, how do we add a fallback
+value for the function? :py:class:`thcolor.decoders.fallback` comes to
+our rescue!
+
+Once our function is defined, we use :py:class:`thcolor.decoders.fallback`
+as a decorator by giving it the value the function should fall back on when
+used as a constant.
+
+The resulting code is the following:
+
+.. code-block::
+
+ class TutorialColorDecoder(MetaColorDecoder):
+ # ...
+
+ @fallback(SRGBColor(0, 0, 0, 0.0))
+ def transparent(color: Color) -> int:
+ return int(round(color.alpha * 255))
+
+Defining the ``awesome-color`` constant
+---------------------------------------
+
+For defining a constant, we can use affectations, as for aliases.
+But how do we define names with carets when they are illegal in Python names?
+We can replace it with ``_``; the color decoders in thcolor are not only
+case-insensitive, they also treat carets and underscores the same.
+
+We can thus do the following:
+
+.. code-block::
+
+ class TutorialColorDecoder(MetaColorDecoder):
+ # ...
+
+ awesome_color = SRGBColor.frombytes(0, 0, 255)
+
+Testing the resulting class
+---------------------------
+
+Putting all of our efforts together, we should have the following code:
+
+.. code-block:: python
+
+ from thcolor.decoders import MetaColorDecoder, alias, fallback
+ from thcolor.colors import Color, SRGBColor
+
+ __all__ = ['TutorialColorDecoder']
+
+ class TutorialColorDecoder(MetaColorDecoder):
+ __extended_hex_support__ = True
+
+ def sum(
+ a: float, b: float, c: float = 0,
+ d: float = 0, e: float = 0,
+ ) -> float:
+ return a + b + c + d + e
+
+ def diff(a: float, b: float) -> float:
+ return a - b
+
+ reverse_diff = alias('diff', args=('b', 'a'))
+
+ @fallback(SRGBColor(0, 0, 0, 0.0))
+ def transparent(color: Color) -> int:
+ return int(round(color.alpha * 255))
+
+ awesome_color = SRGBColor.frombytes(0, 0, 255)
+
+Now that our class is defined, we can instanciate and test our decoder:
+
+.. code-block:: python
+
+ decoder = TutorialColorDecoder()
+ results = decoder.decode(
+ 'awesome-color '
+ 'sum(sum(1, 2, 3, 4), reverse_diff(transparent(transparent), 4))',
+ )
+
+ print(results)
+
+And the result we obtain are the following:
+
+::
+
+ (SRGBColor(red=0.0, green=0.0, blue=1.0, alpha=1.0), 14.0)
diff --git a/docs/index.rst b/docs/index.rst
index 33e175d..a7089eb 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,31 +1,38 @@
Welcome to thcolor's documentation!
===================================
-This module is a color management module made by `Thomas Touhey`_ (``th``
-is for ``touhey``) for the `textoutpc`_ project, a BBCode to HTML translation
-module. It provides the following features:
+thcolor is a color management module made by `Thomas Touhey`_
+(``th`` is for ``touhey``).
-- color management and conversions between formats (RGB, HSL, HWB, NCol, …).
-- text-to-color using close-to-CSS format.
+It was originally made for the `textoutpc`_ project, a BBCode to HTML
+translation module.
-To install the module, use pip:
+thcolor allows you to:
-.. code-block:: bash
+* Represent colors using various colorspaces and coordinates.
+* Convert between these representations.
+* Parse colors and related elements using expressions inspired by (and
+ compatible with given the right options) CSS standards by the W3C.
+* Make CSS expressions out of given color representations.
- $ pip install thcolor
+For more information, you can consult:
-For more information and links, consult `the official website`_.
+* The project homepage at `thcolor.touhey.pro`_.
+* The PyPI project page at `pypi.org`_.
+* The source at `forge.touhey.org`_.
-Table of contents
------------------
+The documentation contents is the following:
.. toctree::
:maxdepth: 2
- angles
- colors
- expressions
+ onboarding
+ guides
+ discuss
+ api
.. _Thomas Touhey: https://thomas.touhey.fr/
.. _textoutpc: https://textout.touhey.pro/
-.. _the official website: https://thcolor.touhey.pro/
+.. _thcolor.touhey.pro: https://thcolor.touhey.pro/
+.. _pypi.org: https://pypi.org/project/thcolor/
+.. _forge.touhey.org: https://forge.touhey.org/thcolor.git/
diff --git a/docs/onboarding.rst b/docs/onboarding.rst
new file mode 100644
index 0000000..fb28fe0
--- /dev/null
+++ b/docs/onboarding.rst
@@ -0,0 +1,12 @@
+Onboarding
+==========
+
+You're a new user trying to figure out what you can and cannot do with
+thcolor, and you're willing to experiment? You're at the right place!
+In this section, you will be able to install, run and start tweaking
+thcolor to better suit your needs.
+
+.. toctree::
+
+ onboarding/installing
+ onboarding/trying
diff --git a/docs/onboarding/installing.rst b/docs/onboarding/installing.rst
new file mode 100644
index 0000000..3677dcf
--- /dev/null
+++ b/docs/onboarding/installing.rst
@@ -0,0 +1,36 @@
+Installing thcolor
+==================
+
+In order to run and tweak thcolor, you must first install it; this section
+will cover the need.
+
+Installing thcolor using pip
+----------------------------
+
+To install thcolor, you can use pip with the following command:
+
+.. code-block:: sh
+
+ python -m pip install thcolor
+
+Some notes on this command:
+
+ * On most Linux distributions, you can directly call ``pip`` (or ``pip3``
+ on those where Python 2.x is still the default); I personnally prefer
+ to call it through Python as a module.
+ * On Linux and other UNIX-like distributions where Python 2.x is still the
+ default, when Python 3.x is installed, you must usually call it using
+ ``python3`` instead of ``python``.
+ * On Microsoft Windows, the Python executable, when added to the PATH,
+ goes by the name ``py`` instead of ``python``.
+
+Installing thcolor from source
+------------------------------
+
+To install thcolor from source, you can use the following commands:
+
+.. code-block:: sh
+
+ python ./setup.py install --system # or --user
+
+.. _regex: https://pypi.org/project/regex/
diff --git a/docs/onboarding/trying.rst b/docs/onboarding/trying.rst
new file mode 100644
index 0000000..85ac502
--- /dev/null
+++ b/docs/onboarding/trying.rst
@@ -0,0 +1,47 @@
+Trying out thcolor
+==================
+
+Once thcolor is installed, it is time to put it to the test!
+Here are a few use cases for the library.
+
+Converting an RGB color to HSL:
+
+.. code-block:: python
+
+ from thcolor.colors import SRGBColor
+
+ color = SRGBColor.frombytes(55, 23, 224)
+ print(color.ashsl())
+
+Converting a HSL color to RGB with an alpha value:
+
+.. code-block:: python
+
+ from thcolor.colors import HSLColor
+ from thcolor.angles import DegreesAngle
+
+ color = HSLColor(DegreesAngle(180), 0.5, 1.0, 0.75)
+ print(color.assrgb())
+
+Converting a textual representation to the RGBA color components:
+
+.. code-block:: python
+
+ from thcolor.colors import Color
+
+ color = Color.fromtext('darker(10%, hsl(0, 1, 50.0%))')
+ print(color.assrgb())
+
+Getting the CSS color representations (with compatibility for earlier CSS
+versions) from a textual representation:
+
+.. code-block:: python
+
+ from thcolor.colors import Color
+
+ color = Color.fromtext('gray(red( #123456 )/0.2/)')
+ for repres in color.css():
+ print(f'color: {repres}')
+
+For more information, please consult the guides, discussion topics and
+API reference on the current documentation.
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..bf6bf6c
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,4 @@
+sphinx
+sphinx-autobuild
+sphinx_autodoc_typehints
+sphinx_rtd_theme
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/requirements.txt
diff --git a/setup.cfg b/setup.cfg
index 7ff488f..d002530 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,9 +1,9 @@
[metadata]
name = thcolor
-version = attr: thcolor.version
+version = attr: thcolor.version.version
url = https://thcolor.touhey.pro/
project_urls =
- Documentation = https://thcolor.touhey.pro/docs/
+ Documentation = https://thcolor.touhey.pro/docs/
author = Thomas Touhey
author_email = thomas@touhey.fr
description = color management module
@@ -11,20 +11,19 @@ long_description = file: README.rst
keywords = textout, color, parser, css
license = MIT
classifiers =
- Development Status :: 4 - Beta
- Natural Language :: English
- License :: OSI Approved :: MIT License
- Operating System :: OS Independent
- Programming Language :: Python :: 3
- Intended Audience :: Developers
- Topic :: Text Processing :: Markup :: HTML
+ Development Status :: 4 - Beta
+ Natural Language :: English
+ License :: OSI Approved :: MIT License
+ Operating System :: OS Independent
+ Programming Language :: Python :: 3
+ Intended Audience :: Developers
+ Topic :: Text Processing :: Markup :: HTML
[options]
zip_safe = False
include_package_data = True
-packages = thcolor, thcolor.builtin
-install_requires =
- regex
+packages = thcolor
+python_requires = >= 3.6
[options.package_data]
* = *.txt, *.rst
@@ -36,9 +35,16 @@ source-dir = docs
universal = True
[flake8]
-ignore = F401, F403, E128, E131, E241, E261, E265, E271, W191
-exclude = .git, __pycache__, build, dist, docs/conf.py, test.py, test
-
-[tool:pytest]
-python_files = tests.py test_*.py *_tests.py
-testpaths = tests
+ignore = D105,D107,D202,W503,T499
+exclude =
+ docs/conf.py
+ test.py
+per-file-ignores =
+ tests/*:F403,F405,S101,D102,D103,T484
+rst-roles =
+ py:class
+ py:attr
+ py:data
+ py:meth
+rst-directives =
+ py:data
diff --git a/setup.py b/setup.py
index 6bcf1c6..b4d2739 100755
--- a/setup.py
+++ b/setup.py
@@ -1,22 +1,21 @@
#!/usr/bin/env python3
-#******************************************************************************
-# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
-# This file is part of the thcolor Python 3.x module, which is MIT-licensed.
-#******************************************************************************
-""" Setup script for the thcolor Python package and script. """
+# *****************************************************************************
+# Copyright (C) 2019-2022 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# This file is part of the thcolor Python module, which is MIT-licensed.
+# *****************************************************************************
+"""Setup script for the thcolor Python package and script."""
from setuptools import setup as _setup
kwargs = {}
try:
- from sphinx.setup_command import BuildDoc as _BuildDoc
- kwargs['cmdclass'] = {'build_sphinx': _BuildDoc}
-except:
- pass
+ from sphinx.setup_command import BuildDoc as _BuildDoc
+ kwargs['cmdclass'] = {'build_sphinx': _BuildDoc}
+except ImportError:
+ pass
# Actually, most of the project's data is read from the `setup.cfg` file.
-
_setup(**kwargs)
# End of file.
diff --git a/tests/__init__.py b/tests/__init__.py
index 47268e6..9e1e6db 100755
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,9 +1,9 @@
#!/usr/bin/env python3
-#******************************************************************************
-# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# *****************************************************************************
+# Copyright (C) 2019-2021 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the thcolor project, which is MIT-licensed.
-#******************************************************************************
-""" Unit tests for the `thcolor` Python module. """
+# *****************************************************************************
+"""Unit tests for the `thcolor` Python module."""
# This file is only there to indicate that the folder is a module.
# It doesn't actually contain code.
diff --git a/tests/test_angles.py b/tests/test_angles.py
new file mode 100755
index 0000000..9438924
--- /dev/null
+++ b/tests/test_angles.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+# *****************************************************************************
+# Copyright (C) 2019-2021 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# This file is part of the thcolor project, which is MIT-licensed.
+# *****************************************************************************
+"""Unit tests for the thcolor color decoding and management module."""
+
+from math import pi
+
+import pytest
+from thcolor.angles import * # NOQA
+
+
+class TestAngles:
+ """Test angle definitions and conversions."""
+
+ @pytest.mark.parametrize('fst,snd', (
+ (GradiansAngle(.1), GradiansAngle(.2)),
+ ))
+ def test_not_equal(self, fst, snd):
+ assert fst != snd
+
+ @pytest.mark.parametrize('angle,expected', (
+ (DegreesAngle(400), DegreesAngle(40)),
+ (DegreesAngle(-10), DegreesAngle(350)),
+ (TurnsAngle(-1.5), TurnsAngle(1)),
+ (RadiansAngle(6 * pi), RadiansAngle(0)),
+ (RadiansAngle(7.5 * pi), RadiansAngle(1.5 * pi)),
+ (GradiansAngle(-975), GradiansAngle(225)),
+ ))
+ def test_principal_angles(self, angle, expected):
+ return angle.asprincipal() == expected
+
+ @pytest.mark.parametrize('test_input,expected', (
+ ('120deg', DegreesAngle(120)),
+ ('5rad', RadiansAngle(5)),
+ ('3grad', GradiansAngle(3)),
+ ('6.turns', TurnsAngle(6)),
+ ('355', DegreesAngle(355)),
+ ))
+ def test_expr(self, test_input, expected):
+ angle = Angle.fromtext(test_input)
+ assert isinstance(angle, type(expected))
+ assert angle == expected
+
+ @pytest.mark.parametrize('rad,deg', (
+ (0, 0),
+ (-pi / 2, 270),
+ (pi / 2, 90),
+ (3 * pi / 4, 135),
+ (-3 * pi / 4, 225),
+ ))
+ def test_radians_to_degrees(self, rad, deg):
+ return RadiansAngle(rad).asprincipal().asdegrees().degrees == deg
+
+
+# End of file.
diff --git a/tests/test_builtin.py b/tests/test_builtin.py
new file mode 100755
index 0000000..a43e724
--- /dev/null
+++ b/tests/test_builtin.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python3
+# *****************************************************************************
+# Copyright (C) 2019-2021 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# This file is part of the thcolor project, which is MIT-licensed.
+# *****************************************************************************
+"""Unit tests for the thcolor color decoding and management module."""
+
+import pytest
+from thcolor.angles import * # NOQA
+from thcolor.colors import * # NOQA
+from thcolor.decoders import * # NOQA
+from thcolor.builtin import * # NOQA
+from thcolor.errors import * # NOQA
+
+
+class TestDefaultDecoder:
+ """Test the default decoder with all features."""
+
+ @pytest.fixture
+ def decoder(self):
+ return DefaultColorDecoder()
+
+ @pytest.mark.parametrize('test_input,expected', (
+ ('blue', SRGBColor.frombytes(0, 0, 255, 1.00)),
+ ('rgb(110%, 0%, 0%)', SRGBColor(1.0, 0, 0, 1.00)),
+ ('rgb(1, 22,242)', SRGBColor.frombytes(1, 22, 242, 1.00)),
+ (' rgb (1,22, 242 , 50.0% )', SRGBColor.frombytes(1, 22, 242, 0.50)),
+ (' rgb (1 22/ 242,50.0%,/)', SRGBColor.frombytes(1, 22, 242, 0.50)),
+ ('rgba(1,22,242,0.500)', SRGBColor.frombytes(1, 22, 242, 0.50)),
+ ('rbga(5, 7)', SRGBColor.frombytes(5, 0, 7, 1.00)),
+ ('hsl(0, 1,50.0%)', HSLColor(DegreesAngle(0.0), 1.0, 0.5, 1.00)),
+ (
+ 'hls(0 / 1 0.5 , 0.2)',
+ HSLColor(DegreesAngle(0.0), 0.5, 1.0, 0.20),
+ ),
+ ('hwb(0 0% 0)', HWBColor(DegreesAngle(0), 0.0, 0.0, 1.00)),
+ ('hbw(127 .5)', HWBColor(DegreesAngle(127), 0.0, 0.5)),
+ ('gray(100)', SRGBColor.frombytes(100, 100, 100, 1.00)),
+ ('gray(100 / 55 %)', SRGBColor.frombytes(100, 100, 100, 0.55)),
+ ('gray(red( #123456 )/0.2/)', SRGBColor.frombytes(18, 18, 18, 0.20)),
+ ('B20 50% 32%', HWBColor(DegreesAngle(252), .5, .32, 1.00)),
+ ('ncol(B20 / 50% 32%)', HWBColor(DegreesAngle(252), .5, .32, 1.00)),
+ ('cmyk(0% 37% 0.13 .78)', CMYKColor(0, .37, .13, .78, 1.00)),
+ (
+ 'darker(10%, hsl(0, 1, 50.0%))',
+ HSLColor(DegreesAngle(0), 1.00, 0.40, 1.00),
+ ),
+ (
+ 'lighter(50%, hsl(0, 1, 60.0%))',
+ HSLColor(DegreesAngle(0), 1.00, 1.00, 1.00),
+ ),
+ (
+ 'saturate(10%, hls(0, 1, 85.0%))',
+ HSLColor(DegreesAngle(0), 0.95, 1.00, 1.00),
+ ),
+ (
+ 'desaturate(10%, hls(0turn, 1, 5%, 0.2))',
+ HSLColor(DegreesAngle(0), 0.00, 1.00, 0.20),
+ ),
+ (
+ 'rgba(255, 0, 0, 20 %)',
+ HSLColor(DegreesAngle(0), 1.00, 0.50, 0.20),
+ ),
+ (
+ 'Y40, 33%, 55%',
+ HWBColor(DegreesAngle(84), 0.33, 0.55, 1.00),
+ ),
+ ('cmyk(0% 37% 0.13 .78)', CMYKColor(0.00, 0.37, 0.13, 0.78, 1.00)),
+ ))
+ def test_decoding_colors(self, decoder, test_input, expected):
+ result = decoder.decode(test_input, prefer_colors=True)
+ assert result[0] == expected
+
+# End of file.
diff --git a/tests/test_colors.py b/tests/test_colors.py
new file mode 100755
index 0000000..7d0043d
--- /dev/null
+++ b/tests/test_colors.py
@@ -0,0 +1,346 @@
+#!/usr/bin/env python3
+# *****************************************************************************
+# Copyright (C) 2019-2021 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# This file is part of the thcolor project, which is MIT-licensed.
+# *****************************************************************************
+"""Unit tests for the thcolor color decoding and management module."""
+
+import pytest
+from thcolor.angles import * # NOQA
+from thcolor.colors import * # NOQA
+from thcolor.utils import round_half_up
+
+
+class TestSRGBColors:
+ """Test the sRGB color conversions."""
+
+ @pytest.mark.parametrize('args', (
+ (None, 0, 0),
+ (0, None, 0),
+ (0, 0, None),
+ ))
+ def test_invalid_values(self, args):
+ """Try to instanciate the class using invalid values."""
+
+ with pytest.raises(ValueError):
+ SRGBColor(*args)
+
+ @pytest.mark.parametrize('args,bytes_', (
+ ((0, 0, 0), (0, 0, 0)),
+ ((.0019, .002, .005), (0, 1, 1)),
+ ((.1, .2, .3), (26, 51, 77)),
+ ))
+ def test_to_bytes(self, args, bytes_):
+ """Try converting to bytes and test the rounding up."""
+
+ assert SRGBColor(*args).asbytes() == bytes_
+
+ @pytest.mark.parametrize('args,bytes_', (
+ ((0, .003921, .003925), (0, 1, 1)),
+ ))
+ def test_from_bytes(self, args, bytes_):
+ """Try converting from bytes and test the rounding up."""
+
+ assert SRGBColor(*args) == SRGBColor.frombytes(*bytes_)
+
+ @pytest.mark.parametrize('name,bytes_', (
+ # https://stackoverflow.com/q/8318911
+ ('chucknorris', (192, 0, 0)),
+ ('ninjaturtle', (0, 160, 0)),
+ ('crap', (192, 160, 0)),
+ ('grass', (0, 160, 0)),
+
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=121738
+ ('navyblue', (160, 176, 224)),
+
+ # http://web.archive.org/web/20050403162633/http://www.jgc.org:80/tsc/index.htm
+ # See the "Flex Hex" technique.
+ ('FxFxFx', (240, 240, 240)),
+ ('#FxFxFx', (240, 240, 240)),
+ ('FqFeFm', (240, 254, 240)),
+
+ # http://scrappy-do.blogspot.com/2004/08/little-rant-about-microsoft-internet.html
+ ('zqbttv', (0, 176, 0)),
+ ('6db6ec49efd278cd0bc92d1e5e072d680', (110, 205, 224)),
+ ))
+ def test_netscape_decoding(self, name, bytes_):
+ """Try decoding colors using Netscape color parsing."""
+
+ assert SRGBColor.fromnetscapecolorname(name).asbytes() == bytes_
+
+ @pytest.mark.parametrize('args,expected', (
+ ((0, 0, 255), ('#0000FF',)),
+ ((1, 22, 242, .5), (
+ '#0116F2',
+ 'rgba(1, 22, 242, 50%)',
+ )),
+ ))
+ def test_css(self, args, expected):
+ assert tuple(SRGBColor.frombytes(*args).css()) == expected
+
+ @pytest.mark.parametrize('args,hsl', (
+ ((0, 0, 0), (DegreesAngle(0), 0, 0)),
+ ((1, 2, 3), (DegreesAngle(210), .5, .01)),
+ ((50, 100, 150), (DegreesAngle(210), .5, .39)),
+ ((255, 255, 255), (DegreesAngle(0), 0, 1)),
+ ((82, 122, 122), (DegreesAngle(180), .2, .4)),
+ ))
+ def test_hsl(self, args, hsl):
+ hue, sat, lgt, *_ = SRGBColor.frombytes(*args).ashsl()
+ assert (hue, round(sat, 2), round(lgt, 2)) == hsl
+
+ @pytest.mark.parametrize('args,hsv', (
+ ((0, 0, 0), (DegreesAngle(0), 0, 0)),
+ ((255, 255, 255), (DegreesAngle(0), 0, 1)),
+ ((0, 255, 0), (DegreesAngle(120), 1, 1)),
+ ((0, 120, 0), (DegreesAngle(120), 1, .47)),
+ ((73, 33, 86), (DegreesAngle(285), .62, .34)),
+ ((10, 20, 30), (DegreesAngle(210), .67, .12)),
+ ))
+ def test_hsv(self, args, hsv):
+ hue, sat, val, *_ = SRGBColor.frombytes(*args).ashsv()
+ hue = DegreesAngle(int(hue.asdegrees().degrees))
+ assert (hue, round(sat, 2), round(val, 2)) == hsv
+
+ @pytest.mark.parametrize('args,hwb', (
+ ((82, 122, 122), (DegreesAngle(180), .32, .52)),
+ ((13, 157, 230), (DegreesAngle(200), .05, .1)),
+ ((212, 212, 212), (DegreesAngle(0), .83, .17)),
+ ))
+ def test_hwb(self, args, hwb):
+ hue, wht, blk, *_ = SRGBColor.frombytes(*args).ashwb()
+ hue = DegreesAngle(int(hue.asdegrees().degrees))
+ assert (hue, round(wht, 2), round(blk, 2)) == hwb
+
+ @pytest.mark.parametrize('args,yiq', (
+ ((0, 0, 0), (0, 0, 0)),
+ ((255, 255, 255), (1, 0, 0)),
+ ((0, 255, 0), (.587, -.275, -.523)),
+ ((0, 120, 0), (.276, -.129, -.246)),
+ ((73, 33, 86), (.2, .0271, .0983)),
+ ((10, 20, 30), (.07098, -.03611, .00389)),
+ ))
+ def test_yiq(self, args, yiq):
+ y, i, q, *_ = SRGBColor.frombytes(*args).asyiq()
+ print((y, i, q), yiq)
+
+ yiq2 = tuple(map(lambda x: round_half_up(x, 3), (y, i, q)))
+ yiq = tuple(map(lambda x: round_half_up(x, 3), yiq))
+ assert yiq == yiq2
+
+ @pytest.mark.parametrize('args,cmyk', (
+ ((0, 0, 0), (0, 0, 0, 1)),
+ ((255, 255, 255), (0, 0, 0, 0)),
+ ((0, 255, 0), (1, 0, 1, 0)),
+ ((0, 120, 0), (1, 0, 1, .53)),
+ ((73, 33, 86), (.15, .62, 0, .66)),
+ ((10, 20, 30), (.67, .33, 0, .88)),
+ ))
+ def test_cmyk(self, args, cmyk):
+ c, m, y, k, *_ = SRGBColor.frombytes(*args).ascmyk()
+ assert (
+ round_half_up(c, 2),
+ round_half_up(m, 2),
+ round_half_up(y, 2),
+ round_half_up(k, 2),
+ ) == cmyk
+
+
+class TestHSLColors:
+ """Test the HSL color conversions."""
+
+ @pytest.mark.parametrize('args', (
+ (0, 0, 0),
+ ('hello', 0, 0),
+ (DegreesAngle(0), -.5, 0),
+ (DegreesAngle(0), 1.5, 0),
+ (DegreesAngle(0), 0, -.5),
+ (DegreesAngle(0), 0, 1.5),
+ ))
+ def test_invalid_values(self, args):
+ """Try to instanciate the class using invalid values."""
+
+ with pytest.raises(ValueError):
+ HSLColor(*args)
+
+ @pytest.mark.parametrize('args,rgb', (
+ ((DegreesAngle(195), 1, .5), (0, 191, 255)),
+ ((DegreesAngle(240), 1, .25), (0, 0, 128)),
+ ((DegreesAngle(0), 1, .5), (255, 0, 0)),
+ ((DegreesAngle(0), 0, 0), (0, 0, 0)),
+ ((DegreesAngle(0), 0, 1), (255, 255, 255)),
+ ((DegreesAngle(0), 0, .01), (3, 3, 3)),
+ ((DegreesAngle(145), .30, .60), (122, 184, 148)),
+ ))
+ def test_srgb(self, args, rgb):
+ r, g, b = HSLColor(*args).assrgb().asbytes()
+ assert (r, g, b) == rgb
+
+ @pytest.mark.parametrize('args,expected', (
+ ((DegreesAngle(0), 1, .4), (
+ '#CC0000',
+ 'hsl(0deg, 100%, 40%)',
+ )),
+ ((DegreesAngle(0), .5, 1, .2), (
+ '#FFFFFF',
+ 'rgba(255, 255, 255, 20%)',
+ 'hsla(0deg, 50%, 100%, 20%)',
+ )),
+ ))
+ def test_css(self, args, expected):
+ assert tuple(HSLColor(*args).css()) == expected
+
+
+class TestHSVColor:
+ """Test the HSV color conversions."""
+
+ @pytest.mark.parametrize('args,rgb', (
+ ((DegreesAngle(120), 0, 0), (0, 0, 0)),
+ ((DegreesAngle(120), .5, .5), (64, 128, 64)),
+ ((DegreesAngle(120), 1, .5), (0, 128, 0)),
+ ((DegreesAngle(120), .5, 1), (128, 255, 128)),
+ ((DegreesAngle(355), .2, .8), (204, 163, 167)),
+ ))
+ def test_srgb(self, args, rgb):
+ assert HSVColor(*args).assrgb().asbytes() == rgb
+
+
+class TestHWBColors:
+ """Test the HWB color conversions."""
+
+ @pytest.mark.parametrize('args,rgb', (
+ ((DegreesAngle(145), .48, .28), (122, 184, 148)),
+ ))
+ def test_srgb(self, args, rgb):
+ r, g, b = HWBColor(*args).assrgb().asbytes()
+ assert (r, g, b) == rgb
+
+ @pytest.mark.parametrize('args,expected', (
+ ((DegreesAngle(127), 0, .5), (
+ '#00800F',
+ 'hwb(127deg, 0%, 50%)',
+ )),
+ ))
+ def test_css(self, args, expected):
+ assert tuple(HWBColor(*args).css()) == expected
+
+
+class TestCMYKColors:
+ """Test the CMYK color conversions."""
+
+ @pytest.mark.parametrize('args,rgb', (
+ ((0, 0, 0, 0), (255, 255, 255)),
+ ((0, 0, 0, 1), (0, 0, 0)),
+ ((0, 0, 0, .45), (140, 140, 140)),
+ ((0, .25, 0, .45), (140, 105, 140)),
+ ((0, .25, .77, .45), (140, 105, 32)),
+ ((.02, .04, .06, .08), (230, 225, 221)),
+ ))
+ def test_srgb(self, args, rgb):
+ assert CMYKColor(*args).assrgb().asbytes() == rgb
+
+
+class TestLABColors:
+ """Test the LAB color conversions."""
+
+ @pytest.mark.parametrize('args,lch', (
+ # Examples created using the following online converter:
+ # http://www.easyrgb.com/en/convert.php
+
+ ((0, 0, 0), (0, 0, DegreesAngle(0))),
+ ((.5, -128, 128), (.5, 181.019, DegreesAngle(135))),
+ ))
+ def test_lch(self, args, lch):
+ l, c, h, *_ = LABColor(*args).aslch()
+ assert (
+ round_half_up(l, 3),
+ round_half_up(c, 3),
+ h.asdegrees(),
+ ) == lch
+
+
+class TestLCHColors:
+ """Test the LCH color conversions."""
+
+ @pytest.mark.parametrize('args,lab', (
+ # Examples created using the following online converter:
+ # http://www.easyrgb.com/en/convert.php
+
+ ((0, 0, DegreesAngle(0)), (0, 0, 0)),
+ ((0, 50, DegreesAngle(0)), (0, 50, 0)),
+ ((.5, 50, DegreesAngle(0)), (.5, 50, 0)),
+ ((.5, 50, DegreesAngle(235)), (.5, -28.679, -40.958)),
+ ((.6, 200, DegreesAngle(146)), (.6, -165.808, 111.839)),
+ ((1, 200, DegreesAngle(0)), (1, 200, 0)),
+ ))
+ def test_lab(self, args, lab):
+ l, a, b, *_ = LCHColor(*args).aslab()
+ l2, a2, b2 = lab
+ assert (
+ round_half_up(l, 3),
+ round_half_up(a, 3),
+ round_half_up(b, 3),
+ ) == (
+ round_half_up(l2, 3),
+ round_half_up(a2, 3),
+ round_half_up(b2, 3),
+ )
+
+
+class TestXYZColors:
+ """Test the XYZ color conversions."""
+
+ @pytest.mark.parametrize('args', (
+ (-.5, 0, 0),
+ (1.5, 0, 0),
+ (0, -.5, 0),
+ (0, 1.5, 0),
+ (0, 0, -.5),
+ (0, 0, 1.5),
+ (None, 0, 0),
+ (0, None, 0),
+ (0, 0, None),
+ ))
+ def test_invalid_values(self, args):
+ """Try to instanciate the class using invalid values."""
+
+ with pytest.raises(ValueError):
+ XYZColor(*args)
+
+ @pytest.mark.parametrize('args,rgb', (
+ ((0, 0, 0), (0, 0, 0)),
+ ((1, 1, 1), (277, 249, 244)),
+ ((.1, .2, .3), (-438, 147, 145)),
+ ((1, .5, 0), (378, -103, -153)),
+ ))
+ def test_rgb(self, args, rgb):
+ assert XYZColor(*args).assrgb().asbytes() == rgb
+
+ @pytest.mark.parametrize('args,lab', (
+ ((1, .5, 0), (.76069, 111.688, 131.154)),
+ ((0, .5, 1), (.76069, -327.885, -35.666)),
+ ((.2, .3, .4), (.61654, -37.321, -9.353)),
+ ))
+ def test_lab(self, args, lab):
+ l, a, b, *_ = XYZColor(*args).aslab()
+ l2, a2, b2 = lab
+
+ l, a, b = (
+ round_half_up(l, 3),
+ round_half_up(a, 3),
+ round_half_up(b, 3),
+ )
+ l2, a2, b2 = (
+ round_half_up(l2, 3),
+ round_half_up(a2, 3),
+ round_half_up(b2, 3),
+ )
+
+ assert (l, a, b) == (l2, a2, b2)
+
+
+# TODO: add tests for invalid values in constructors.
+# TODO: test YIQ colors.
+# TODO: test YUV colors.
+
+# End of file.
diff --git a/tests/test_decoders.py b/tests/test_decoders.py
new file mode 100755
index 0000000..e3423aa
--- /dev/null
+++ b/tests/test_decoders.py
@@ -0,0 +1,257 @@
+#!/usr/bin/env python3
+# *****************************************************************************
+# Copyright (C) 2019-2021 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# This file is part of the thcolor project, which is MIT-licensed.
+# *****************************************************************************
+"""Unit tests for the thcolor color decoding and management module."""
+
+from typing import Any
+
+import pytest
+
+from thcolor.angles import *
+from thcolor.colors import *
+from thcolor.decoders import *
+from thcolor.errors import *
+
+
+class TestInvalidDecoders:
+ """Test the exceptions raised when an invalid decoder is created."""
+
+ def test_unknown_data_type(self):
+ with pytest.raises(TypeError, match=r'neither an alias'):
+ class BogusDecoder(MetaColorDecoder):
+ hello = 'hello'
+
+ def test_circular_aliases(self):
+ with pytest.raises(ValueError, match=r'cyclic dependency'):
+ class BogusDecoder(MetaColorDecoder):
+ a = alias('b')
+ b = alias('a')
+
+ def test_alias_unknown_argument(self):
+ with pytest.raises(ValueError, match=r'not an argument'):
+ class BogusDecoder(MetaColorDecoder):
+ def f(a: int) -> int:
+ return a
+
+ g = alias('f', args=('b',))
+
+ def test_alias_non_default_argument(self):
+ with pytest.raises(ValueError, match=r'left without value'):
+ class BogusDecoder(MetaColorDecoder):
+ def f(a: int, b: int = 0) -> int:
+ return a + b
+
+ g = alias('f', args=('b',))
+
+ def test_varargs(self):
+ with pytest.raises(ValueError, match=r'variable arg'):
+ class BogusDecoder(MetaColorDecoder):
+ def f(a: int, b: int = 0, *args) -> int:
+ return a + b + sum(args)
+
+
+class TestBaseDecoder:
+ """Test the base decoder with no options enabled."""
+
+ @pytest.fixture
+ def decoder(self):
+ class StrictDecoder(MetaColorDecoder):
+ B20 = 12.34
+ heLLo = 56.78
+
+ return StrictDecoder()
+
+ @pytest.mark.parametrize('test_input,expected', (
+ ('1 / 2 , 3', (
+ 1, 2, 3.0,
+ )),
+ ('20deg 4turn 3,, 3. 3.0 .2 50%', (
+ DegreesAngle(20), TurnsAngle(4), 3, None, 3.0, 3.0, 0.2, 0.5,
+ )),
+ ('#12345F', (
+ SRGBColor.frombytes(18, 52, 95, 1.00),
+ )),
+ ))
+ def test_syntax(self, decoder, test_input, expected):
+ """Test basic syntax.
+
+ Tests syntaxes that are always available, with separators.
+ """
+
+ result = decoder.decode(test_input)
+ assert result == expected
+
+ @pytest.mark.parametrize('test_input,expected', (
+ ('20.5, 20 / 19.5deg', (
+ DegreesAngle(20.5), DegreesAngle(20), DegreesAngle(19.5),
+ )),
+ ))
+ def test_coersce_into_angles(self, decoder, test_input, expected):
+ """Test the decoding while indicating that we want angles."""
+
+ result = decoder.decode(test_input, prefer_angles=True)
+ assert result == expected
+
+ def test_existing_variable(self, decoder):
+ """Test getting an existing variable."""
+
+ result = decoder.decode('hello')
+ assert result == (56.78,)
+
+ def test_non_existing_variable(self, decoder):
+ """Test getting a non-existing variable."""
+
+ with pytest.raises(ColorExpressionSyntaxError, match=r'unknown value'):
+ decoder.decode('nonexisting')
+
+ def test_existing_variable_ncol_format(self, decoder):
+ """Test decoding an expression using an NCol-like variable.
+
+ We want to avoid the NCol subsytem from shadowing
+ existing variables when NCol support is disabled.
+ """
+
+ result = decoder.decode('B20')
+ assert result == (12.34,)
+
+ def test_non_existing_variable_ncol_format(self, decoder):
+ """Test decoding an expression using an NCol-like variable.
+
+ We want to avoid the NCol subsystem intercepting errors
+ in case of non-existing variables.
+ """
+
+ with pytest.raises(ColorExpressionSyntaxError, match=r'unknown value'):
+ decoder.decode('Y40')
+
+ def test_disabled_extended_hex(self, decoder):
+ """Test decoding an expression using an extended hex color.
+
+ This should be disabled in this state.
+ """
+
+ with pytest.raises(ColorExpressionSyntaxError, match=r'extended hex'):
+ decoder.decode('#1234')
+
+
+class TestExtendedHexDecoder:
+ """Test base decoder with extended hex support."""
+
+ @pytest.fixture
+ def decoder(self):
+ class Decoder(MetaColorDecoder):
+ __extended_hex_support__ = True
+
+ return Decoder()
+
+ @pytest.mark.parametrize('test_input,expected', (
+ ('#0003', SRGBColor(0, 0, 0, alpha=.2)),
+ ('#000A', SRGBColor(0, 0, 0, alpha=.666667)),
+ ))
+ def test_extended_hex(self, decoder, test_input, expected):
+ assert decoder.decode(test_input) == (expected,)
+
+
+class TestNColDecoder:
+ """Test base decoder with ncol support."""
+
+ @pytest.fixture
+ def decoder(self):
+ class Decoder(MetaColorDecoder):
+ __ncol_support__ = True
+
+ def sum2(a: int, b: int) -> int:
+ return a + b
+
+ def sum5(a: int, b: int, c: int, d: int, e: int) -> int:
+ return a + b + c + d + e
+
+ def col(a) -> Any:
+ return a
+
+ return Decoder()
+
+ @pytest.mark.parametrize('test_input,expected', (
+ (
+ 'B20 / 50% 32%',
+ HWBColor(DegreesAngle(252), .5, .32, 1.00),
+ ),
+ (
+ 'B20 / / 16%',
+ HWBColor(DegreesAngle(252), 0, .16, 1.00),
+ ),
+ (
+ 'Y40, 33%, 55%',
+ HWBColor(DegreesAngle(84), 0.33, 0.55, 1.00),
+ ),
+ (
+ 'Y40, sum2(35, -2), 55%',
+ HWBColor(DegreesAngle(84), 0.33, 0.55, 1.00),
+ ),
+ (
+ 'Y40, sum5(1, 1, 1, 1, 29), 55%',
+ HWBColor(DegreesAngle(84), 0.33, 0.55, 1.00),
+ ),
+ (
+ 'Y40',
+ HWBColor(DegreesAngle(84), 0, 0),
+ ),
+ (
+ 'col(Y40)',
+ HWBColor(DegreesAngle(84), 0, 0),
+ ),
+ ))
+ def test_ncol(self, decoder, test_input, expected):
+ """Test natural colors."""
+
+ result = decoder.decode(test_input)
+ assert result == (expected,)
+
+ def test_invalid_ncol(self, decoder):
+ with pytest.raises(ColorExpressionSyntaxError, match=r'unknown value'):
+ assert decoder.decode('Y105, 50%, 50%')
+
+
+class TestNetscapeDecoder:
+ """Test base decoder with netscape color support."""
+
+ @pytest.fixture
+ def decoder(self):
+ class Decoder(MetaColorDecoder):
+ __ncol_support__ = False
+ __defaults_to_netscape_color__ = True
+
+ return Decoder()
+
+ @pytest.mark.parametrize('test_input,expected', (
+ ('chucknorris', SRGBColor.frombytes(192, 0, 0, 1.00)),
+ (
+ '6db6ec49efd278cd0bc92d1e5e072d680',
+ SRGBColor.frombytes(110, 205, 224),
+ ),
+ ))
+ def test_netscape_colors(self, decoder, test_input, expected):
+ """Test decoding using Netscape colors."""
+
+ result = decoder.decode(test_input, prefer_colors=True)
+ assert result == (expected,)
+
+ @pytest.mark.parametrize('test_input,expected', (
+ ('#123', SRGBColor.frombytes(17, 34, 51, 1.00)),
+ ('123', SRGBColor.frombytes(1, 2, 3, 1.00)),
+ ('123.0', SRGBColor.frombytes(18, 48, 0, 1.00)),
+ ))
+ def test_coersce_into_colors(self, decoder, test_input, expected):
+ """Test the decoding while indicating that we want colors.
+
+ It is expected from the decoder to use Netscape color name
+ style decoding in this scenario.
+ """
+
+ result = decoder.decode(test_input, prefer_colors=True)
+ assert result == (expected,)
+
+
+# End of file.
diff --git a/tests/test_text.py b/tests/test_text.py
deleted file mode 100755
index 069ef9c..0000000
--- a/tests/test_text.py
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/usr/bin/env python3
-#******************************************************************************
-# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
-# This file is part of the thcolor project, which is MIT-licensed.
-#******************************************************************************
-""" Unit tests for the thcolor color decoding and management module. """
-
-import pytest
-from thcolor import Color, Angle
-
-def _deg(value):
- return Angle(Angle.Type.DEG, value)
-
-@pytest.mark.parametrize('test_input,expected', (
- ('blue', ( 0, 0, 255, 1.00)),
- ('#12345F', ( 18, 52, 95, 1.00)),
- ('#123', ( 17, 34, 51, 1.00)),
- ('123', ( 1, 2, 3, 1.00)),
- ('123.0', ( 18, 48, 0, 1.00)),
- ('chucknorris', (192, 0, 0, 1.00)),
- ('rgb(1, 22,242)', ( 1, 22, 242, 1.00)),
- (' rgb (1,22, 242 , 50.0% )', ( 1, 22, 242, 0.50)),
- (' rgb (1 22/ /242,50.0%,/)', ( 1, 22, 242, 0.50)),
- ('rgba(1,22,242,0.500)', ( 1, 22, 242, 0.50)),
- ('rbga(5, 7)', ( 5, 0, 7, 1.00)),
- ('hsl(0, 1,50.0%)', (255, 0, 0, 1.00)),
- ('hls(0 / 1 0.5 , 0.2)', (255, 255, 255, 0.20)),
- ('hwb(0 0% 0)', (255, 0, 0, 1.00)),
- ('hbw(127 .5)', ( 0, 128, 15, 1.00)),
- ('gray(100)', (100, 100, 100, 1.00)),
- ('gray(100 / 55 %)', (100, 100, 100, 0.55)),
- ('gray(red( #123456 )/0.2/)', ( 18, 18, 18, 0.20)),
- ('B20 50% 32%', (137, 128, 173, 1.00)),
- ('ncol(B20 / 50% 32%)', (137, 128, 173, 1.00)),
- ('cmyk(0% 37% 0.13 .78)', ( 56, 35, 49, 1.00)),
- ('lab(50 50 0)', (193, 78, 121, 1.00)),
-))
-def test_rgba(test_input, expected):
- assert Color.from_text(test_input).rgba() == expected
-
-@pytest.mark.parametrize('test_input,expected', (
- ('darker(10%, hsl(0, 1, 50.0%))', (_deg( 0 ), 1.00, 0.40, 1.00)),
- ('lighter(50%, hsl(0, 1, 60.0%))', (_deg( 0 ), 1.00, 1.00, 1.00)),
- ('saturate(10%, hls(0, 1, 85.0%))', (_deg( 0 ), 0.95, 1.00, 1.00)),
- ('desaturate(10%, hls(0, 1, 5%, 0.2))', (_deg( 0 ), 0.00, 1.00, 0.20)),
- ('rgba(255, 0, 0, 20 %)', (_deg( 0 ), 1.00, 0.50, 0.20)),
- ('Y40, 33%, 55%', (_deg(83.23), 0.16, 0.39, 1.00)),
-))
-def test_hsla(test_input, expected):
- assert Color.from_text(test_input).hsla() == expected
-
-@pytest.mark.parametrize('test_input,expected', (
- ('cmyk(0% 37% 0.13 .78)', (0.00, 0.37, 0.13, 0.78, 1.00)),
-))
-def test_cmyka(test_input, expected):
- assert Color.from_text(test_input).cmyka() == expected
-
-@pytest.mark.parametrize('test_input,expected', (
- ('blue',
- ('#0000FF',)),
- (' rgb (1,22, 242 , 50.0% )',
- ('#0116F2', 'rgba(1, 22, 242, 50%)')),
- ('darker(10%, hsl(0, 1, 50.0%))',
- ('#CC0000', 'hsl(0deg, 100%, 40%)')),
- ('hls(0 / 1 0.5 , 0.2)',
- ('#FFFFFF', 'rgba(255, 255, 255, 20%)', 'hsla(0deg, 50%, 100%, 20%)')),
- ('hbw(127 .5)',
- ('#00800F', 'hwb(127deg, 0%, 50%)')),
-))
-def test_css(test_input, expected):
- assert Color.from_text(test_input).css() == expected
-
-# End of file.
diff --git a/thcolor/__init__.py b/thcolor/__init__.py
index 22c8a7e..4ee1693 100755
--- a/thcolor/__init__.py
+++ b/thcolor/__init__.py
@@ -1,24 +1,14 @@
#!/usr/bin/env python3
-#******************************************************************************
-# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# *****************************************************************************
+# Copyright (C) 2018-2022 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the thcolor project, which is MIT-licensed.
-#******************************************************************************
-""" HTML/CSS-like color parsing, mainly for the `[color]` tag.
- Defines the `get_color()` function which returns an rgba value.
+# *****************************************************************************
+"""HTML/CSS-like color parsing, mainly for the `[color]` tag.
- The functions in this module do not aim at being totally compliant with
- the W3C standards, although it is inspired from it.
-"""
-
-from ._color import Color
-from ._ref import Reference
-from ._angle import Angle
-
-from ._exc import ColorExpressionDecodingError
+Defines the `get_color()` function which returns an rgba value.
-__all__ = ["version", "Color", "Reference", "Angle",
- "ColorExpressionDecodingError"]
-
-version = "0.3.1"
+The functions in this module do not aim at being totally compliant with
+the W3C standards, although it is inspired from it.
+"""
# End of file.
diff --git a/thcolor/_angle.py b/thcolor/_angle.py
deleted file mode 100755
index ac3198d..0000000
--- a/thcolor/_angle.py
+++ /dev/null
@@ -1,295 +0,0 @@
-#!/usr/bin/env python3
-#******************************************************************************
-# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
-# This file is part of the thcolor project, which is MIT-licensed.
-#******************************************************************************
-""" Angle management. """
-
-from math import pi as _pi
-from enum import Enum as _Enum
-
-__all__ = ["Angle"]
-
-def _deg(name, value):
- value = float(value)
- return value # % 360.0
-
-def _grad(name, value):
- value = float(value)
- return value # % 400.0
-
-def _rad(name, value):
- value = float(value)
- return value # % (2 * _pi)
-
-def _turn(name, value):
- value = float(value)
- return value # % 1
-
-# ---
-# Angle definition.
-# ---
-
-class Angle:
- """ Class representing an angle within thcolor, used for some color
- representations (most notably hue). Its constructor depends on the
- first given argument, which represents the angle type as one of the
- :class:`Angle.Type` constants.
-
- .. function:: Angle(Angle.Type.DEG, degrees)
-
- Create an angle with a value in degrees (canonical values are
- between 0 and 360 excluded). For example, to create a 270° angle:
-
- .. code-block:: python
-
- angle = Angle(Angle.Type.DEG, 270)
-
- .. function:: Angle(Angle.Type.GRAD, gradiants)
-
- Create an angle with a value in gradiants (canonical values are
- between 0 and 400 excluded). For example, to create a 565.5
- gradiants angle:
-
- .. code-block:: python
-
- angle = Angle(Angle.Type.GRAD, 565.5)
-
- .. function:: Angle(Angle.Type.RAD, radiants)
-
- Create an angle with a value in radiants (canonical values are
- between 0 and 2π excluded). For example, to create a π
- radiants angle:
-
- .. code-block:: python
-
- from math import pi
- angle = Angle(Angle.Type.RAD, pi)
-
- .. function:: Angle(Angle.Type.TURN, turns)
-
- Create an angle with a value in turns (canonical values are
- between 0 and 1 excluded). For example, to create a 3.5 turns
- angle:
-
- .. code-block:: python
-
- angle = Angle(Angle.Type.TURN, 3.5) """
-
- # Properties to work with:
- #
- # `_type`: the type as one of the `Angle.Type` constants.
- # `_value`: the angle value.
-
- class Type(_Enum):
- """ Class representing the type of an angle, its unit really.
- The following types are available:
-
- .. data:: INVALID
-
- An invalid angle, for internal processing.
-
- .. data:: DEG
-
- An angle in degrees. A full circle is represented by
- 360 degrees.
-
- .. data:: GRAD
-
- An angle in gradiants. A full circle is represented by
- 400 gradiants.
-
- .. data:: RAD
-
- An angle in radiants. A full circle is represented by
- 2π radiants.
-
- .. data:: TURN
-
- An angle in turns. A full circle is represented by 1 turn. """
-
- INVALID = 0
- DEG = 1
- GRAD = 2
- RAD = 3
- TURN = 4
-
- def __init__(self, *args, **kwargs):
- self._type = Angle.Type.INVALID
- self.set(*args, **kwargs)
-
- def __eq__(self, other):
- if not isinstance(other, Angle):
- return super().__eq__(other)
-
- if self._type == Angle.Type.INVALID:
- return other.type == Angle.Type.INVALID
- elif self._type == Angle.Type.DEG:
- return other.degrees == self._value
- elif self._type == Angle.Type.GRAD:
- return other.gradiants == self._value
- elif self._type == Angle.Type.RAD:
- return other.radiants == self._value
- elif self._type == Angle.Type.TURN:
- return other.turns == self._value
-
- return False
-
- def __repr__(self):
- args = (('type', f'{self.__class__.__name__}.{str(self._type)}'),)
- if self._type in (Angle.Type.DEG, Angle.Type.GRAD, Angle.Type.RAD,
- Angle.Type.TURN):
- args += (('value', repr(self._value)),)
-
- argtext = ', '.join(f'{key} = {value}' for key, value in args)
- return f"{self.__class__.__name__}({argtext})"
-
- # ---
- # Management methods.
- # ---
-
- def set(self, *args, **kwargs):
- """ Set the angle. """
-
- args = list(args)
-
- def _decode_varargs(*keys):
- # Check for each key.
-
- results = ()
-
- for names, convert_func, *value in keys:
- for name in names:
- if name in kwargs:
- if args:
- raise TypeError(f"{self.__class__.__name__}() " \
- f"got multiple values for argument {name}")
-
- raw_result = kwargs.pop(name)
- break
- else:
- name = names[0]
- if args:
- raw_result = args.pop(0)
- elif value:
- raw_result = value[0] if len(value) == 1 else value
- else:
- raise TypeError(f"{self.__class__.__name__}() " \
- "missing a required positional argument: " \
- f"{name}")
-
- result = convert_func(name, raw_result)
- results += (result,)
-
- # Check for keyword arguments for which keys are not in the set.
-
- if kwargs:
- raise TypeError(f"{next(iter(kwargs.keys()))} is an invalid " \
- f"keyword argument for type {type}")
-
- return results
-
- # ---
- # Main function body.
- # ---
-
- # Check for the type.
-
- if args:
- try:
- type = kwargs.pop('type')
- except:
- type = args.pop(0)
- else:
- if isinstance(args[0], Color.Type):
- raise TypeError(f"{self.__class__.__name__}() got " \
- "multiple values for argument 'type'")
- else:
- try:
- type = kwargs.pop('type')
- except:
- type = self._type
- if type == Angle.Type.INVALID:
- raise TypeError(f"{self.__class__.__name__}() missing " \
- "required argument: 'type'")
-
- try:
- type = Angle.Type(type)
- except:
- type = Angle.Type.DEG
-
- # Initialize the properties.
-
- if type == Angle.Type.DEG:
- self._value, = _decode_varargs(\
- (('value', 'angle', 'degrees'), _deg))
- elif type == Angle.Type.GRAD:
- self._value, = _decode_varargs(\
- (('value', 'angle', 'gradiants'), _grad))
- elif type == Angle.Type.RAD:
- self._value, = _decode_varargs(\
- (('value', 'angle', 'radiants'), _rad))
- elif type == Angle.Type.TURN:
- self._value, = _decode_varargs(\
- (('value', 'angle', 'turns'), _turn))
- else:
- raise ValueError(f"invalid color type: {type}")
-
- # Once the arguments have been tested to be valid, we can set the
- # type.
-
- self._type = type
-
- # ---
- # Properties.
- # ---
-
- @property
- def type(self):
- """ The read-only angle type as one of the :class:`Angle.Type`
- constants. """
-
- return self._type
-
- @property
- def degrees(self):
- """ The read-only angle value in degrees. If the angle isn't in degrees
- already, it will be converted automatically. """
-
- if self._type == Angle.Type.DEG:
- return self._value
- return self.turns * 360
-
- @property
- def gradiants(self):
- """ The read-only angle value in gradiants. If the angle isn't in
- gradiants already, it will be converted automatically. """
-
- if self._type == Angle.Type.GRAD:
- return self._value
- return self.turns * 400
-
- @property
- def radiants(self):
- """ The read-only angle value in radiants. If the angle isn't in
- radiants already, it will be converted automatically. """
-
- if self._type == Angle.Type.RAD:
- return self._value
- return self.turns * (2 * _pi)
-
- @property
- def turns(self):
- """ The read-only angle value in turns. If the angle isn't in
- turns already, it will be converted automatically. """
-
- if self._type == Angle.Type.DEG:
- return self._value / 360
- elif self._type == Angle.Type.GRAD:
- return self._value / 400
- elif self._type == Angle.Type.RAD:
- return self._value / (2 * _pi)
- elif self._type == Angle.Type.TURN:
- return self._value
-
-# End of file.
diff --git a/thcolor/_color.py b/thcolor/_color.py
deleted file mode 100755
index 1bc8f6e..0000000
--- a/thcolor/_color.py
+++ /dev/null
@@ -1,1131 +0,0 @@
-#!/usr/bin/env python3
-#******************************************************************************
-# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
-# This file is part of the thcolor project, which is MIT-licensed.
-#******************************************************************************
-""" HTML/CSS like color parsing, mainly for the `[color]` tag.
- Defines the `get_color()` function which returns an rgba value. """
-
-from enum import Enum as _Enum
-from warnings import warn as _warn
-
-_gg_no_re = False
-
-try:
- import regex as _re
-except ImportError:
- _warn("could not import regex, text parsing is disabled.",
- RuntimeWarning)
- _gg_no_re = True
-
-from ._ref import Reference as _Reference
-from ._angle import Angle as _Angle
-from ._sys import (hls_to_rgb as _hls_to_rgb, rgb_to_hls as _rgb_to_hls,
- rgb_to_hwb as _rgb_to_hwb, hwb_to_rgb as _hwb_to_rgb,
- rgb_to_cmyk as _rgb_to_cmyk, cmyk_to_rgb as _cmyk_to_rgb,
- lab_to_rgb as _lab_to_rgb, rgb_to_lab as _rgb_to_lab,
- lab_to_lch as _lab_to_lch, lch_to_lab as _lch_to_lab,
- netscape_color as _netscape_color)
-from ._exc import (\
- ColorExpressionDecodingError as _ColorExpressionDecodingError,
- NotEnoughArgumentsError as _NotEnoughArgumentsError,
- TooManyArgumentsError as _TooManyArgumentsError,
- InvalidArgumentTypeError as _InvalidArgumentTypeError,
- InvalidArgumentValueError as _InvalidArgumentValueError)
-
-__all__ = ["Color"]
-
-# ---
-# Decoding utilities.
-# ---
-
-_color_pattern = None
-
-def _get_color_pattern():
- global _color_pattern
-
- if _color_pattern is None:
- if _gg_no_re:
- raise ImportError("text parsing is disabled until you install " \
- "the 'regex' module, e.g. via `pip install regex`.")
-
- _color_pattern = _re.compile(r"""
- (
- ((?P<agl_val>-? ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*
- (?P<agl_typ>deg|grad|rad|turn))
- | ((?P<per>[0-9]+(\.[0-9]*)? | \.[0-9]+) \s* \%)
- | (?P<num>[0-9]+(\.[0-9]*)? | \.[0-9]+)
- | (?P<ncol>[RYGCBM] [0-9]{0,2} (\.[0-9]*)?)
- | (\# (?P<hex>[0-9a-f]{3} | [0-9a-f]{4} | [0-9a-f]{6} | [0-9a-f]{8}))
- | ((?P<name>[a-z]([a-z0-9\s_-]*[a-z0-9_-])?)
- ( \s* \( \s* (?P<arg> (?0)? ) \s* \) )?)
- )
-
- \s* ((?P<sep>[,/\s])+ \s* (?P<nextargs> (?0))?)?
- """, _re.VERBOSE | _re.I | _re.M)
-
- return _color_pattern
-
-# ---
-# Color initialization varargs utilities.
-# ---
-
-def _color_profile(name, value):
- try:
- value = Color.Profile.from_value(value)
- except (TypeError, ValueError):
- raise ValueError(f"{name} is not a valid color profile " \
- f"(got {repr(value)}).")
-
- return value
-
-def _byte(name, value):
- try:
- assert value == int(value)
- assert 0 <= value < 256
- except (AssertionError, TypeError, ValueError):
- raise ValueError(f"{name} should be a byte between 0 and 255") \
- from None
-
- return value
-
-def _signed(name, value):
- try:
- value = float(value)
- except (AssertionError, TypeError, ValueError):
- raise ValueError(f"{name} should be a signed number")
-
- return round(value, 4)
-
-def _unsigned(name, value):
- try:
- value = float(value)
- assert value >= 0
- except (AssertionError, TypeError, ValueError):
- raise ValueError(f"{name} should be a positive number")
-
- return round(value, 4)
-
-def _unrestricted_percentage(name, value):
- try:
- value = float(value)
- assert 0.0 <= value
- except (AssertionError, TypeError, ValueError):
- raise ValueError(f"{name} should be a proportion starting from 0") \
- from None
-
- return round(value, 4)
-
-def _percentage(name, value):
- try:
- value = float(value)
- assert 0.0 <= value <= 1.0
- except (AssertionError, TypeError, ValueError):
- raise ValueError(f"{name} should be a proportion between 0 " \
- "and 1.0") from None
-
- return round(value, 4)
-
-def _hue(name, value):
- if isinstance(value, _Angle):
- pass
- else:
- try:
- value = _Angle(value)
- except:
- raise ValueError(f"{name} should be an Angle instance")
-
- return value
-
-# ---
-# Color class definition.
-# ---
-
-class Color:
- """ Class representing a color within thcolor. Its constructor depends
- on the first given argument, which represents the color type as one
- of the :class:`Color.Type` constants.
-
- .. function:: Color(Color.Type.RGB, red, green, blue, alpha = 1.0)
-
- Create a color using its red, green and blue components. Each is
- expressed as a byte value, from 0 (dark) to 255 (light).
-
- An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be
- appended to the base components.
-
- .. function:: Color(Color.Type.RGB, profile, red, green, blue, """ \
- """alpha = 1.0)
-
- Create a color using its profile, red, green and blue component.
- Each is expressed as a byte value, from 0 (dark) to 255 (light).
-
- The profile is one of the :class:`thcolor.Color.Profile`
- constants, and represents the RGB profile.
-
- An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be
- appended to the base components.
-
- .. function:: Color(Color.Type.HSL, hue, saturation, lightness, """ \
- """alpha = 1.0)
-
- Create a color using its hue, saturation and lightness components.
- The hue is represented by an :class:`Angle` object, and the
- saturation and lightness are values going from 0.0 to 1.0.
-
- An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be
- appended to the base components.
-
- .. function:: Color(Color.Type.HWB, hue, whiteness, blackness, """ \
- """alpha = 1.0)
-
- Create a color using its hue, whiteness and blackness components.
- The hue is represented by an :class:`Angle` object, and the
- whiteness and lightness are values going from 0.0 to 1.0.
-
- An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be
- appended to the base components.
-
- .. function:: Color(Color.Type.CMYK, cyan, magenta, yellow, """ \
- """black, alpha = 1.0)
-
- Create a color using its cyan, magenta, yellow and blackness
- components, which are all values going from 0.0 to 1.0.
-
- An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be
- appended to the base components.
-
- .. function:: Color(Color.Type.LAB, lightness, a, b, alpha = 1.0)
-
- Create a color using its CIE Lightness (similar to the lightness
- in the HSL representation) and the A and B axises in the Lab
- colorspace, represented by signed numbers.
-
- An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be
- appended to the base components.
-
- .. function:: Color(Color.Type.LCH, lightness, chroma, hue, """ \
- """alpha = 1.0)
-
- Create a color using its CIE Lightness (similar to the lightness
- in the HSL representation), its chroma (as a positive number
- theoretically unbounded) and its hue.
-
- An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be
- appended to the base components.
-
- .. function:: Color(Color.Type.XYZ, x, y, z, alpha = 1.0)
-
- Create a color using its CIE XYZ components (as numbers between
- 0 and 1).
-
- An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be
- appended to the base components. """
-
- class Type(_Enum):
- """ Class representing the type of a color, or how it is expressed.
- The following types are available:
-
- .. data:: INVALID
-
- An invalid color, for internal processing.
-
- .. data:: RGB
-
- A color expressed through its sRGB components: red, green
- and blue.
-
- .. data:: HSL
-
- A color expressed through its HSL components: hue, saturation
- and lightness.
-
- .. data:: HWB
-
- A color expressed through its HWB components: hue, whiteness
- and blackness.
-
- .. data:: CMYK
-
- A color expressed through its CMYK components: cyan, magenta,
- yellow and black.
-
- .. data:: LAB
-
- A color expressed through its lightness and Lab colorspace
- coordinates (A and B).
-
- .. data:: LCH
-
- A color expressed through its lightness, chroma and hue.
-
- .. data:: XYZ
-
- A color expressed through its CIE XYZ coordinates.
-
- An alpha component can be added to every single one of these
- types, so it is not included in the type names. """
-
- # Values start at 65600 in order not to infer with "normal" values
- # going up to 65535, just in case.
-
- INVALID = 65600
-
- RGB = 65601
- HSL = 65602
- HWB = 65603
- CMYK = 65604
- LAB = 65605
- LCH = 65606
- XYZ = 65607
-
- class Profile(_Enum):
- """ Class representing the profile of a color, or how it is expressed.
- The following profiles are available:
-
- .. data:: SRGB
-
- A basic sRGB profile.
-
- .. data:: IMAGE_P3
-
- See `the description in CSS Module Level 4
- <https://drafts.csswg.org/css-color/#valdef-color-image-p3>`_.
-
- .. data:: A98RGB
-
- The Adobe® RGB (1998) color profile. See `the description
- in CSS Module Level 4
- <https://drafts.csswg.org/css-color/#valdef-color-a98rgb>`_.
-
- .. data:: PROPHOTORGB
-
- The ProPHOTO RGB color profile. See `the description in
- CSS Module Level 4
- <https://drafts.csswg.org/css-color/#valdef-color-prophotorgb>`_.
-
- .. data:: REC2020
-
- The REC.2020 colorspace. See `the description in CSS Module
- Level 4
- <https://drafts.csswg.org/css-color/#valdef-color-rec2020>`_. """
-
- SRGB = 65700
- IMAGE_P3 = 65701
- A98RGB = 65702
- PROPHOTORGB = 65703
- REC2020 = 65704
-
- def from_value(value):
- _profiles = {
- 'srgb': 'SRGB',
- 'imagep3': 'IMAGE_P3',
- 'a98rgb': 'A98RGB',
- 'prophotorgb': 'PROPHOTORGB',
- 'rec2020': 'REC2020'}
-
- if type(value) == str:
- newval = ''.join(c for c in value.casefold() if c in \
- '0123456789abcdefghijklmnopqrstuvwxyz')
- try:
- value = _profiles[newval]
- except:
- pass
-
- return getattr(Color.Profile, value)
-
- return Color.Profile(value)
-
- # Properties to work with:
- #
- # `_type`: the type as one of the `Color.Type` constants.
- # `_alpha`: alpha value.
- #
- # RGB colors:
- # `_r`, `_g`, `_b`: rgb components, as bytes.
- # `_profile`: the color profile.
- #
- # HSL colors:
- # `_hue`: hue.
- # `_sat`, `_lgt`: saturation and light for HSL.
- #
- # HWB colors:
- # `_hue`: hue.
- # `_wht`, `_blk`: whiteness and blackness for HWB.
- #
- # CMYK colors:
- # `_cy`, `_ma`, `_ye`, `_bl`: CMYK components.
- #
- # LAB colors:
- # `_lgt`: lightness.
- # `_a`, `_b`: coordinates in the Lab colorspace.
- #
- # LCH colors:
- # `_lgt`: lightness.
- # `_hue`: the hue.
- # `_chr`: the chroma.
- #
- # XYZ colors:
- # `_x`, `_y`, `_z`: XYZ components.
-
- def __init__(self, *args, **kwargs):
- self._type = Color.Type.INVALID
- self.set(*args, **kwargs)
-
- def __repr__(self):
- args = (('type', f'{self.__class__.__name__}.{str(self._type)}'),)
- if self._type == Color.Type.RGB:
- args += (('profile',
- f'{self.__class__.__name__}.{str(self._profile)}'),
- ('red', repr(self._r)), ('green', repr(self._g)),
- ('blue', repr(self._b)))
- elif self._type == Color.Type.HSL:
- args += (('hue', repr(self._hue)), ('saturation', repr(self._sat)),
- ('lightness', repr(self._lgt)))
- elif self._type == Color.Type.HWB:
- args += (('hue', repr(self._hue)), ('whiteness', repr(self._wht)),
- ('blackness', repr(self._blk)))
- elif self._type == Color.Type.CMYK:
- args += (('cyan', repr(self._cy)), ('magenta', repr(self._ma)),
- ('yellow', repr(self._ye)), ('black', repr(self._bl)))
- elif self._type == Color.Type.LAB:
- args += (('lightness', repr(self._lgt)), ('a', repr(self._a)),
- ('b', repr(self._b)))
- elif self._type == Color.Type.LCH:
- args += (('lightness', repr(self._lgt)),
- ('chroma', repr(self._chr)), ('hue', repr(self._hue)))
- elif self._type == Color.Type.XYZ:
- args += (('x', repr(self._x)), ('y', repr(self._y)),
- ('z', repr(self._z)))
-
- args += (('alpha', self._alpha),)
-
- argtext = ', '.join(f'{key} = {value}' for key, value in args)
- return f"{self.__class__.__name__}({argtext})"
-
- def __eq__(self, other):
- if not isinstance(other, Color):
- return super().__eq__(other)
-
- if other.type == Color.Type.INVALID:
- return self._type == Color.Type.INVALID
- elif other.type == Color.Type.HSL:
- return self.hsla() == other.hsla()
- elif other.type == Color.Type.HWB:
- return self.hwba() == other.hwba()
- elif other.type == Color.Type.CMYK:
- return self.cmyka() == other.cmyka()
- elif other.type == Color.Type.LAB:
- return self.laba() == other.laba()
- elif other.type == Color.Type.LCH:
- return self.lcha() == other.lcha()
- elif other.type == Color.Type.XYZ:
- return self.xyza() == other.xyza()
-
- return self.rgba() == other.rgba()
-
- # ---
- # Management methods.
- # ---
-
- def set(self, *args, **kwargs):
- """ Set the color using its constructor arguments and keyword
- arguments. """
-
- args = list(args)
-
- def _decode_varargs(*keys):
- class _UNDEFINED_CLASS:
- pass
- _UNDEFINED = _UNDEFINED_CLASS()
-
- def _get_value(value_array):
- if not value_array:
- value = _UNDEFINED
- else:
- value = value_array[0] if len(value_array) == 1 \
- else value_array
-
- return value
-
- # Check for each key.
-
- results = ()
-
- left_args = len(keys)
-
- for names, convert_func, *value in keys:
- value = _get_value(value)
-
- for name in names:
- if name in kwargs:
- if value is _UNDEFINED and args:
- raise TypeError(f"{self.__class__.__name__}() " \
- f"got multiple values for argument {name}")
-
- raw_result = kwargs.pop(name)
- break
- else:
- name = names[0]
- if value is not _UNDEFINED and len(args) < left_args:
- raw_result = value
- elif args:
- raw_result = args.pop(0)
- else:
- raise TypeError(f"{self.__class__.__name__}() " \
- "missing a required positional argument: " \
- f"{name}")
-
- result = convert_func(name, raw_result)
- results += (result,)
-
- left_args -= 1
-
- # Check for keyword arguments for which keys are not in the set.
-
- if kwargs:
- raise TypeError(f"{next(iter(kwargs.keys()))} is an invalid " \
- f"keyword argument for type {type}")
-
- return results
-
- # ---
- # Main function body.
- # ---
-
- # Check for the type.
-
- if args:
- try:
- type = kwargs.pop('type')
- except:
- type = args.pop(0)
- else:
- if isinstance(args[0], Color.Type):
- raise TypeError(f"{self.__class__.__name__}() got " \
- "multiple values for argument 'type'")
- else:
- try:
- type = kwargs.pop('type')
- except:
- type = self._type
- if type == Color.Type.INVALID:
- raise TypeError(f"{self.__class__.__name__}() missing " \
- "required argument: 'type'")
-
- try:
- type = Color.Type(type)
- except:
- type = Color.Type.RGB
-
- # Initialize the properties.
-
- if type == Color.Type.RGB:
- self._profile, self._r, self._g, self._b, self._alpha = \
- _decode_varargs(\
- (('profile', 'p'), _color_profile, 'srgb'),
- (('red', 'r'), _byte),
- (('green', 'g'), _byte),
- (('blue', 'b'), _byte),
- (('alpha', 'a'), _percentage, 1.0))
-
- if self._profile != Color.Profile.SRGB:
- raise NotImplementedError("rgb profile " \
- f"{repr(self._profile)} isn't managed yet.")
- elif type == Color.Type.HSL:
- self._hue, self._sat, self._lgt, self._alpha = _decode_varargs(\
- (('hue', 'h'), _hue),
- (('saturation', 'sat', 's'), _percentage),
- (('lightness', 'light', 'lig', 'l'), _percentage),
- (('alpha', 'a'), _percentage, 1.0))
- elif type == Color.Type.HWB:
- self._hue, self._wht, self._blk, self._alpha = _decode_varargs(\
- (('hue', 'h'), _hue),
- (('whiteness', 'white', 'w'), _percentage),
- (('blackness', 'black', 'b'), _percentage),
- (('alpha', 'a'), _percentage, 1.0))
- elif type == Color.Type.CMYK:
- self._cy, self._ma, self._ye, self._bl, self._alpha = \
- _decode_varargs(\
- (('cyan', 'c'), _percentage),
- (('magenta', 'm'), _percentage),
- (('yellow', 'y'), _percentage),
- (('black', 'b'), _percentage),
- (('alpha', 'a'), _percentage, 1.0))
- elif type == Color.Type.LAB:
- self._lgt, self._a, self._b, self._alpha = _decode_varargs(\
- (('lightness', 'light', 'lig', 'l'), _unrestricted_percentage),
- (('a',), _signed),
- (('b',), _signed),
- (('alpha', 'a'), _percentage, 1.0))
- elif type == Color.Type.LCH:
- self._lgt, self._chr, self._hue, self._alpha = _decode_varargs(\
- (('lightness', 'light', 'lig', 'l'), _percentage),
- (('chroma', 'chr', 'c'), _unsigned),
- (('hue', 'h'), _hue),
- (('alpha', 'a'), _percentage, 1.0))
- elif type == Color.Type.XYZ:
- self._x, self._y, self._z, self._alpha = _decode_varargs(\
- (('x',), _percentage),
- (('y',), _percentage),
- (('z',), _percentage),
- (('alpha', 'a'), _percentage, 1.0))
- else:
- raise ValueError(f"invalid color type: {type}")
-
- # Once the arguments have been tested to be valid, we can set the
- # type.
-
- self._type = type
-
- # ---
- # Properties.
- # ---
-
- @property
- def type(self):
- """ The read-only angle type as one of the :class:`Color.Type`
- constants. """
-
- return self._type
-
- # ---
- # Conversion methods.
- # ---
-
- def rgb(self):
- """ Get the sRGB (red, green, blue) components of the color.
- For example:
-
- >>> Color.from_text("#876543").rgb()
- ... (135, 101, 67)
-
- If the color is not represented as sRGB internally, it will be
- converted. """
-
- if self._type == Color.Type.RGB:
- return (self._r, self._g, self._b)
- elif self._type == Color.Type.HSL:
- return _hls_to_rgb(self._hue, self._lgt, self._sat)
- elif self._type == Color.Type.HWB:
- return _hwb_to_rgb(self._hue, self._wht, self._blk)
- elif self._type == Color.Type.CMYK:
- return _cmyk_to_rgb(self._cy, self._ma, self._ye, self._bl)
- elif self._type == Color.Type.LAB:
- return _lab_to_rgb(self._lgt, self._a, self._b)
- elif self._type == Color.Type.LCH:
- return _lch_to_rgb(self._lgt, self._chr, self._hue)
-
- raise ValueError(f"color type {self._type} doesn't translate to rgb")
-
- def hsl(self):
- """ Get the HSL (hue, saturation, lightness) components of the color.
- For example:
-
- >>> Color.from_text("hsl(90turn 0% 5%)").hls()
- ... (Angle(type = Angle.Type.TURN, value = 90.0), 0.05, 0.0)
-
- If the color is not represented as HSL internally, it will be
- converted. """
-
- if self._type == Color.Type.HSL:
- return (self._hue, self._sat, self._lgt)
-
- try:
- rgb = self.rgb()
- except ValueError:
- raise ValueError(f"color type {self._type} doesn't translate " \
- "to hsl") from None
-
- return _rgb_to_hls(*rgb)
-
- def hwb(self):
- """ Get the HWB (hue, whiteness, blackness) components of the color.
- For example:
-
- >>> Color.from_text("hwb(.7 turn / 5% 10%)").hwb()
- ... (Angle(type = Angle.Type.TURN, value = 0.7), 0.05, 0.1)
-
- If the color is not represented as HSL internally, it will be
- converted. """
-
- if self._type == Color.Type.HWB:
- return (self._hue, self._wht, self._blk)
-
- try:
- rgb = self.rgb()
- except ValueError:
- raise ValueError(f"color type {self._type} doesn't translate " \
- "to hwb") from None
-
- return _rgb_to_hwb(*rgb)
-
- def cmyk(self):
- """ Get the CMYK (cyan, magenta, yellow, black) components of the
- color. For example:
-
- >>> Color.from_text("cmyk(.1 .2 .3 .4)").cmyk()
- ... (0.1, 0.2, 0.3, 0.4)
-
- If the color is not represented as CMYK internally, it will be
- converted naively. """
-
- if self._type == Color.Type.CMYK:
- return (self._cy, self._ma, self._ye, self._bl)
-
- try:
- rgb = self.rgb()
- except ValueError:
- raise ValueError(f"color type {self._type} doesn't translate " \
- "to cmyk") from None
-
- return _rgb_to_cmyk(*rgb)
-
- def lab(self):
- """ Get the LAB (lightness, Lab colorspace coordinates) components
- of the color. For example:
-
- >>> Color.from_text("lab(50 50 0)").lab()
- ... (0.5, 50, 0)
-
- If the color is not represented as LAB internally, it will be
- converted. """
-
- if self._type == Color.Type.LAB:
- return (self._lgt, self._a, self._b)
- elif self._type == Color.Type.LCH:
- return _lch_to_lab(self._lgt, self._chr, self._hue)
-
- try:
- rgb = self.rgb()
- except ValueError:
- raise ValueError(f"color type {self._type} doesn't translate " \
- "to lab") from None
-
- return _rgb_to_lab(*rgb)
-
- def lch(self):
- """ Get the LCH (lightness, chroma, hue) components of the color.
- For example:
-
- >>> Color.from_text("lch(50 230 0deg)").lch()
- ... (0.5, 230, Angle(Angle.Type.DEG, 0))
-
- If the color is not represented as LCH internally, it will be
- converted. """
-
- if self._type == Color.Type.LCH:
- return (self._lgt, self._chr, self._hue)
-
- try:
- lab = self.lab()
- except ValueError:
- raise ValueError(f"color type {self._type} doesn't translate " \
- "to lch") from None
-
- return _lab_to_lch(*lab)
-
- def xyz(self):
- """ Get the XYZ components of the color.
- For example:
-
- >>> Color.from_text("xyz(0.2, 0.4, 0.5)")
- ... (0.2, 0.4, 0.5)
-
- If the color is not represented as XYZ internally, it will be
- converted. """
-
- if self._type == Color.Type.XYZ:
- return (self._x, self._y, self._z)
-
- raise NotImplementedError # TODO
-
- def rgba(self):
- """ Get the sRGB (red, green, blue) and alpha components of the color.
- For example:
-
- >>> Color.from_text("#87654321").rgb()
- ... (135, 101, 67, 0.1294)
-
- If the color is not represented as sRGB internally, it will be
- converted. """
-
- r, g, b = self.rgb()
- alpha = self._alpha
-
- return (r, g, b, alpha)
-
- def hsla(self):
- """ Get the HSL (hue, saturation, lightness) and alpha components of
- the color. For example:
-
- >>> Color.from_text("hsl(90turn 0% 5% .8)").hlsa()
- ... (Angle(type = Angle.Type.TURN, value = 90.0), 0.05, 0.0, 0.8)
-
- If the color is not represented as HSL internally, it will be
- converted. """
-
- h, s, l = self.hsl()
- alpha = self._alpha
-
- return (h, s, l, alpha)
-
- def hls(self):
- """ Alias for :meth:`hsl` but reverses the lightness and
- saturation for commodity. """
-
- h, s, l = self.hsl()
- return (h, l, s)
-
- def hlsa(self):
- """ Alias for :meth:`hsla` but reverses the lightness and
- saturation for commodity. """
-
- h, s, l, a = self.hsla()
- return (h, l, s, a)
-
- def hwba(self):
- """ Get the HWB (hue, whiteness, blackness) and alpha components of
- the color. For example:
-
- >>> Color.from_text("hwb(.7 turn / 5% 10% .2)").hwba()
- ... (Angle(type = Angle.Type.TURN, value = 0.7), 0.05, 0.1, 0.2)
-
- If the color is not represented as HSL internally, it will be
- converted. """
-
- h, w, b = self.hwb()
- a = self._alpha
-
- return (h, w, b, a)
-
- def cmyka(self):
- """ Get the CMYK (cyan, magenta, yellow, black) and alpha components
- of the color. For example:
-
- >>> Color.from_text("cmyk(.1 .2 .3 .4 / 10%)").cmyka()
- ... (0.1, 0.2, 0.3, 0.4, 0.1)
-
- If the color is not represented as CMYK internally, it will be
- converted naively. """
-
- c, m, y, k = self.cmyk()
- a = self._alpha
-
- return (c, m, y, k, a)
-
- def laba(self):
- """ Get the LAB (lightness, Lab colorspace coordinates) and alpha
- components of the color. For example:
-
- >>> Color.from_text("lab(50 50 0 / 0.75)").laba()
- ... (0.5, 50, 0, 0.75)
-
- If the color is not represented as LAB internally, it will be
- converted naively. """
-
- l, a, b = self.lab()
- alpha = self._alpha
-
- return (l, a, b, alpha)
-
- def lcha(self):
- """ Get the LCH (lightness, chroma, hue) and alpha components
- of the color. For example:
-
- >>> Color.from_text("lch(50 230 0deg)").lcha()
- ... (0.5, 230, Angle(Angle.Type.DEG, 0), 1.0)
-
- If the color is not represented as LCH internally, it will be
- converted. """
-
- l, c, h = self.lch()
- alpha = self._alpha
-
- return (l, c, h, alpha)
-
- def xyza(self):
- """ Get the XYZ and alpha components of the color.
- For example:
-
- >>> Color.from_text("xyz(0.2, 0.4, 0.5 / 65%)")
- ... (0.2, 0.4, 0.5, 0.65)
-
- If the color is not represented as XYZ internally, it will be
- converted. """
-
- x, y, z = self.xyz()
- alpha = self._alpha
-
- return (x, y, z)
-
- def css(self):
- """ Get the CSS color descriptions, with older CSS specifications
- compatibility, as a list of strings.
-
- For example:
-
- >>> Color(Color.Type.RGB, 18, 52, 86, 0.82).css()
- ... ["#123456", "rgba(18, 52, 86, 82%)"] """
-
- def _percent(prop):
- per = round(prop, 4) * 100
- if per == int(per):
- per = int(per)
- return per
-
- def _deg(agl):
- agl = round(agl.degrees, 2)
- if agl == int(agl):
- agl = int(agl)
- return agl
-
- def statements():
- # Start by yelling a #RRGGBB color, compatible with most
- # web browsers around the world, followed by the rgba()
- # notation if the alpha value isn't 1.0.
-
- r, g, b, a = self.rgba()
- a = round(a, 3)
- yield f'#{r:02X}{g:02X}{b:02X}'
-
- if a < 1.0:
- yield f'rgba({r}, {g}, {b}, {_percent(a)}%)'
-
- # Then yield more specific CSS declarations in case
- # they're supported (which would be neat!).
-
- if self._type == Color.Type.HSL:
- args = f'{_deg(self._hue)}deg, ' \
- f'{_percent(self._sat)}%, {_percent(self._lgt)}%'
-
- if a < 1.0:
- yield f'hsla({args}, {_percent(a)}%)'
- else:
- yield f'hsl({args})'
- elif self._type == Color.Type.HWB:
- args = f'{_deg(self._hue)}deg, ' \
- f'{_percent(self._wht)}%, {_percent(self._blk)}%'
-
- if a < 1.0:
- yield f'hwba({args}, {_percent(a)}%)'
- else:
- yield f'hwb({args})'
-
- return tuple(statements())
-
- # ---
- # Static methods for decoding.
- # ---
-
- def from_str(*args, **kwargs):
- """ Alias for :meth:`from_text()`. """
-
- return Color.from_text(value)
-
- def from_string(*args, **kwargs):
- """ Alias for :meth:`from_text()`. """
-
- return Color.from_text(value)
-
- def from_text(expr, ref = None):
- """ Create a color from a string using a :class:`Reference` object.
- If the ``ref`` argument is ``None``, then the default reference
- is loaded.
-
- An example:
-
- >>> Color.from_text("#123456")
- ... Color(type = Color.Type.RGB, red = 18, green = 52, """ \
- """ blue = 86, alpha = 1.0) """
-
- if ref is None:
- ref = _Reference.default()
- if not isinstance(ref, _Reference):
- raise ValueError("ref is expected to be a subclass of Reference")
-
- class argument:
- def __init__(self, column, value):
- self._column = column
- self._value = value
-
- def __repr__(self):
- return f"{self.__class__.__name__}(column = {self._column}, " \
- f"value = {repr(self._value)})"
-
- @property
- def column(self):
- return self._column
-
- @property
- def value(self):
- return self._value
-
- def recurse(column, match):
- if not match:
- return ()
-
- if match['agl_val'] is not None:
- # The matched value is an angle.
-
- agl_typ = {
- 'deg': _Angle.Type.DEG,
- 'grad': _Angle.Type.GRAD,
- 'rad': _Angle.Type.RAD,
- 'turn': _Angle.Type.TURN}[match['agl_typ']]
-
- value = _Reference.angle(_Angle(agl_typ,
- float(match['agl_val'])))
- elif match['per'] is not None:
- # The matched value is a percentage.
-
- value = float(match['per'])
- value = _Reference.percentage(value)
- elif match['num'] is not None:
- # The matched value is a number.
-
- value = _Reference.number(match['num'])
- elif match['hex'] is not None:
- # The matched value is a hex color.
-
- name = match['hex']
-
- if len(name) <= 4:
- name = ''.join(map(lambda x: x + x, name))
-
- r = int(name[0:2], 16)
- g = int(name[2:4], 16)
- b = int(name[4:6], 16)
- a = int(name[6:8], 16) / 255.0 if len(name) == 8 else 1.0
-
- value = _Reference.color(Color(Color.Type.RGB, r, g, b, a))
- elif match['arg'] is not None:
- # The matched value is a function.
-
- name = match['name']
-
- # Get the arguments.
-
- args = recurse(column + match.start('arg'),
- _get_color_pattern().fullmatch(match['arg']))
-
- # Get the function and call it with the arguments.
-
- try:
- func = ref.functions[name]
- except KeyError:
- raise _ColorExpressionDecodingError("no such function " \
- f"{repr(name)}", column = column)
-
- try:
- value = func(*map(lambda x: x.value, args))
- except _NotEnoughArgumentsError as e:
- raise _ColorExpressionDecodingError("not enough " \
- f"arguments (expected at least {e.count} arguments)",
- column = column, func = name)
- except _TooManyArgumentsError as e:
- raise _ColorExpressionDecodingError("extraneous " \
- f"argument (expected {e.count} arguments at most)",
- column = args[e.count].column, func = name)
- except _InvalidArgumentTypeError as e:
- raise _ColorExpressionDecodingError("type mismatch for " \
- f"argument {e.index + 1}: expected {e.expected}, " \
- f"got {e.got}", column = args[e.index].column,
- func = name)
- except _InvalidArgumentValueError as e:
- raise _ColorExpressionDecodingError("erroneous value " \
- f"for argument {e.index + 1}: {e.text}",
- column = args[e.index].column, func = name)
- except NotImplementedError:
- raise _ColorExpressionDecodingError("not implemented",
- column = column, func = name)
- else:
- if match['ncol']:
- # The match is probably a natural color (ncol), we ought
- # to parse it and get the following arguments or, if
- # anything is invalid, to treat it as a color name.
-
- name = match['ncol']
-
- # First, get the letter and proportion.
-
- letter = name[0]
- number = float(name[1:])
-
- if number >= 0 and number < 100:
- # Get the following arguments and check.
-
- args = recurse(column + match.start('nextargs'),
- _get_color_pattern().fullmatch(match['nextargs'] \
- or ""))
-
- try:
- assert len(args) >= 2
- w = args[0].value.to_factor()
- b = args[1].value.to_factor()
- except:
- w = 0
- b = 0
- else:
- args = args[2:]
-
- # Calculate the color and return the args.
-
- color = Color(Color.Type.HWB,
- _Angle(_Angle.Type.DEG, 'RYGCBM'.find(letter) \
- * 60 + number / 100 * 60), w, b)
-
- # And finally, return the args.
-
- return (argument(column, _Reference.color(color)),) \
- + args
-
- # The matched value is a named color.
-
- name = match['name']
-
- try:
- # Get the named color (e.g. 'blue').
-
- value = ref.colors[name]
- assert value != None
- except:
- r, g, b = _netscape_color(name)
- value = Color(Color.Type.RGB, r, g, b, 1.0)
-
- value = _Reference.color(value)
-
- return (argument(column, value),) \
- + recurse(column + match.start('nextargs'),
- _get_color_pattern().fullmatch(match['nextargs'] or ""))
-
- # Strip the expression.
-
- lexpr = expr.strip()
- column = (len(expr) - len(lexpr))
- expr = lexpr
- del lexpr
-
- # Match the expression (and check it as a whole directly).
-
- match = _get_color_pattern().fullmatch(expr)
- if match is None:
- raise _ColorExpressionDecodingError("expression parsing failed")
-
- # Get the result and check its type.
-
- results = recurse(column, match)
- if len(results) > 1:
- raise _ColorExpressionDecodingError("extraneous value",
- column = results[1].column)
-
- result = results[0].value
- try:
- result = ref.colors[result]
- except AttributeError:
- raise _ColorExpressionDecodingError("expected a color",
- column = column)
-
- return result
-
-# End of file.
diff --git a/thcolor/_exc.py b/thcolor/_exc.py
deleted file mode 100755
index f4954d8..0000000
--- a/thcolor/_exc.py
+++ /dev/null
@@ -1,161 +0,0 @@
-#!/usr/bin/env python3
-#******************************************************************************
-# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
-# This file is part of the thcolor project, which is MIT-licensed.
-#******************************************************************************
-""" Exception definitions, with internal and external exceptions defined. """
-
-__all__ = ["ColorExpressionDecodingError",
- "NotEnoughArgumentsError", "TooManyArgumentsError",
- "InvalidArgumentTypeError", "InvalidArgumentValueError"]
-
-# ---
-# External exceptions.
-# ---
-
-class ColorExpressionDecodingError(Exception):
- """ A color decoding error has occurred on the text. """
-
- def __init__(self, text, column = None, func = None):
- try:
- self._column = column
- assert self._column >= 0
- except:
- self._column = None
-
- self._func = str(func)
- self._text = str(text)
-
- def __str__(self):
- msg = ""
-
- if self._column is not None:
- msg += f"at column {self._column}"
- if self._func is not None:
- msg += ", "
- if self._func is not None:
- msg += f"for function {repr(self._func)}"
- if msg:
- msg += ": "
-
- return msg + self._text
-
- @property
- def text(self):
- """ Exception message, usually linked to the context. """
-
- return self._text
-
- @property
- def column(self):
- """ Column of the expression at which the exception has occurred,
- ``None`` if the error has occurred on an unknown column or on the
- whole exception. """
-
- return self._column
-
- @property
- def func(self):
- """ Function name we were calling when the error has occurred,
- either on arguments decoding or erroneous argument type or
- value, ``None`` if the context is unknown or the error hasn't
- occurred while calling a function or decoding its arguments. """
-
- return self._func
-
-# ---
-# Internal exceptions.
-# ---
-
-class NotEnoughArgumentsError(Exception):
- """ Not enough arguments. """
-
- def __init__(self, count, name = None):
- self._name = name
- self._count = count
-
- def __str__(self):
- msg = "not enough arguments"
- if self._name is not None:
- msg += f" for function {repr(self._name)}"
- msg += f", expected {self._count} arguments at least"
-
- return msg
-
- @property
- def count(self):
- return self._count
-
-class TooManyArgumentsError(Exception):
- """ Too many arguments. """
-
- def __init__(self, count, name = None):
- self._name = name
- self._count = count
-
- def __str__(self):
- msg = "too many arguments"
- if self._name is not None:
- msg += f" for function {repr(self._name)}"
- msg += f", expected {self._count} arguments at most"
-
- return msg
-
- @property
- def count(self):
- return self._count
-
-class InvalidArgumentTypeError(Exception):
- """ Invalid argument type. """
-
- def __init__(self, index, expected, got, name = None):
- self._name = name
- self._index = index
- self._expected = expected
- self._got = got
-
- def __str__(self):
- msg = f"wrong type for argument {self._index + 1}"
- if self._name:
- msg += f" of function {repr(self._name)}"
- msg += f": expected {self._expected}, got {self._got}"
-
- return msg
-
- @property
- def index(self):
- return self._index
-
- @property
- def expected(self):
- return self._expected
-
- @property
- def got(self):
- return self._got
-
-class InvalidArgumentValueError(Exception):
- """ Invalid argument value. """
-
- def __init__(self, index, text, name = None):
- self._name = name
- self._index = index
- self._text = text
-
- def __str__(self):
- msg = f"erroneous value for argument {self._index + 1}"
- if self._name:
- msg += f" of function {repr(self._name)}"
- msg += f": {self._text}"
-
- return msg
-
- @property
- def index(self):
- return self._index
-
- @property
- def text(self):
- return self._text
-
-# End of file.
diff --git a/thcolor/_ref.py b/thcolor/_ref.py
deleted file mode 100755
index 8a39cbb..0000000
--- a/thcolor/_ref.py
+++ /dev/null
@@ -1,494 +0,0 @@
-#!/usr/bin/env python3
-#******************************************************************************
-# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
-# This file is part of the thcolor project, which is MIT-licensed.
-#******************************************************************************
-""" Named color reference, parent class. """
-
-from inspect import (getfullargspec as _getfullargspec,
- getmembers as _getmembers, ismethod as _ismethod)
-from itertools import count as _count
-from warnings import warn as _warn
-
-from ._angle import Angle as _Angle
-from ._sys import netscape_color as _netscape_color
-from ._exc import (NotEnoughArgumentsError as _NotEnoughArgumentsError,
- TooManyArgumentsError as _TooManyArgumentsError,
- InvalidArgumentTypeError as _InvalidArgumentTypeError)
-
-__all__ = ["Reference"]
-
-_default_reference = None
-_color_cls = None
-
-def _get_color_class():
- global _color_cls
-
- if _color_cls is not None:
- return _color_cls
-
- from ._color import Color
- _color_cls = Color
- return _color_cls
-
-class _type_or:
- """ A type or another. """
-
- def __init__(self, type1, type2):
- self._type1 = type1
- self._type2 = type2
-
- @property
- def type1(self):
- return self._type1
-
- @property
- def type2(self):
- return self._type2
-
- def __repr__(self):
- return f"{repr(self._type1)} | {repr(self._type2)}"
-
- def __str__(self):
- return f"{self._type1} or {self._type2}"
-
- def __or__(self, other):
- return type_or(self, other)
-
- def __contains__(self, other):
- return other in self._type1 or other in self._type2
-
-# ---
-# Main reference definition.
-# ---
-
-class Reference:
- """ Function reference for color parsing and operations. """
-
- def __init__(self):
- pass
-
- # ---
- # Base type and function definitions for parsing.
- # ---
-
- class base_type(type):
- """ The metaclass for all types used below. """
-
- def __new__(mcls, name, bases, attrs):
- return super().__new__(mcls, name, bases, attrs)
-
- def __init__(self, name, bases, attrs):
- self.__name = name
-
- def __contains__(self, other):
- return self if other == self else None
-
- def __or__(self, other):
- return _type_or(self, other)
-
- def __repr__(self):
- return f"<class {repr(self.__name)}>"
-
- def __str__(self):
- return self.__name
-
- class number(metaclass = base_type):
- """ Syntaxical element for expression decoding, representing
- a number, integer or decimal, positive or negative.
-
- This element is usually used to represent a byte value,
- a factor (usually from 0.0 to 1.0), a color or an angle
- in degrees. """
-
- def __init__(self, value):
- if type(value) == str:
- self._strvalue = value
- self._value = float(value)
- else:
- self._value = float(value)
- if self._value == int(self._value):
- self._strvalue = str(int(self._value))
- else:
- self._strvalue = str(value)
-
- def __str__(self):
- return self._strvalue
-
- @property
- def value(self):
- return self._value
-
- def to_byte(self):
- """ Make a byte (from 0 to 255) out of the number. """
-
- try:
- value = int(self._value)
-
- assert value == self._value
- assert 0 <= value < 256
- except:
- raise ValueError("unsuitable value for byte conversion: " \
- f"{repr(self._value)}")
-
- return value
-
- def to_factor(self, min = 0.0, max = 1.0):
- """ Make a factor (usually from 0.0 to 1.0) out of the number. """
-
- if (min is not None and self._value < min) \
- or (max is not None and self._value > max):
- if max is None:
- msg = f"above {min}"
- elif min is None:
- msg = f"under {max}"
- else:
- msg = f"between {min} and {max}"
-
- raise ValueError(f"expected a value {msg}, got {self._value}")
-
- try:
- assert 0.0 <= self._value <= 1.0
- except:
- raise ValueError("expected a value between 0.0 and 1.0, got " \
- f"{self._value}")
-
- return self._value
-
- def to_hue(self):
- """ Make an angle in degrees out of the number. """
-
- return _Angle(_Angle.Type.DEG, self._value)
-
- class percentage(metaclass = base_type):
- """ Syntaxical element for expression decoding, representing
- a percentage (number followed by a '%' sign).
-
- This element is usually used to represent a factor (usually
- from 0% to 100%) or anything that can be factored such as a
- byte value (where 0% represents 0 and 100% represents 255). """
-
- def __init__(self, value):
- self._value = value
-
- if value < 0:
- value = 0
-
- # value can actually be more than 100.
-
- def __repr__(self):
- return f"{self._value} %"
-
- def to_factor(self, min = 0.0, max = 1.0):
- """ Make a factor (usually from 0.0 to 1.0) out of the number. """
-
- value = self._value / 100
- if (min is not None and value < min) \
- or (max is not None and value > max):
- if max is None:
- msg = f"above {min * 100}%"
- elif min is None:
- msg = f"under {max * 100}%"
- else:
- msg = f"between {min * 100}% and {max * 100}%"
-
- raise ValueError(f"expected a percentage {msg}, " \
- f"got {self._value}%")
-
- return value
-
- def to_byte(self):
- """ Make a byte (from 0 to 255) out of the number. """
-
- if self._value < 0:
- self._value = 0
- if self._value > 100:
- self._value = 100
-
- return int(min(self._value / 100, 1.0) * 255)
-
- class angle(metaclass = base_type):
- """ Syntaxical element for expression decoding, representing
- an angle (a number followed by an angle unit: degrees, gradiants,
- radiants or turns). """
-
- def __init__(self, value):
- if not isinstance(value, _Angle):
- raise TypeError("expected an Angle instance")
-
- self._value = value
-
- def __repr__(self):
- return repr(self._value)
-
- def to_hue(self):
- """ Get the :class:`thcolor.Angle` object. """
-
- return self._value
-
- class color(metaclass = base_type):
- """ Syntaxical element for expression decoding, representing
- a color using several methods:
-
- - a hexadecimal code preceeded by a '#', e.g. "#123ABC".
- - a natural color (see
- `NCol <https://www.w3schools.com/colors/colors_ncol.asp>`_).
- - a color name as defined by a :class:`Reference`.
- - a legacy color name using the legacy color algorithm (or
- 'Netscape' color algorithm). """
-
- def __init__(self, value):
- if not isinstance(value, _get_color_class()):
- raise ValueError("expected a Color instance")
-
- self._value = value
-
- def __repr__(self):
- return repr(self._value)
-
- def to_color(self):
- """ Get the :class:`thcolor.Color` object. """
-
- return self._value
-
- # ---
- # Function helper.
- # ---
-
- def alias(*names):
- """ Decorator for aliasing with another name. Defines a lot, you
- know. """
-
- def _decorator(func):
- if not hasattr(func, '_ref_aliases'):
- func._ref_aliases = ()
-
- func._ref_aliases += names
- return func
-
- return _decorator
-
- # ---
- # Function and named color getters.
- # ---
-
- def _get_functions(self):
- """ Function getter, which can be used as ``.functions[name](args…)``.
-
- Provides a wrapper to the method which checks the argument
- types and perform the necessary conversions before passing
- them to the functions.
-
- For example, with a classical CSS reference named ``ref``:
-
- >>> ref.functions['rgb'](ref.number(18), ref.number(52), """ \
- """ref.number(86))
- ... Color(type = Color.Type.RGB, red = 18, green = 52, """ \
- """blue = 86, alpha = 1.0) """
-
- class _FunctionGetter:
- def __init__(self, ref):
- self._fref = ref
-
- def __getitem__(self, name):
- fref = self._fref
- found = False
-
- # First, check if the function name is valid and if the
- # method exists.
-
- members = dict(_getmembers(fref, predicate = _ismethod))
- validname = lambda n: type(n) == str and n[0:1] != '_' \
- and n not in ('functions', 'named', 'default', 'alias')
-
- if validname(name):
- try:
- method = members[name]
- found = True
- except (KeyError, AssertionError):
- pass
-
- # Then, if we haven't found the method, check the aliases.
-
- if not found:
- for member in members.values():
- try:
- aliases = member._ref_aliases
- except (AttributeError, AssertionError):
- continue
-
- if name not in aliases:
- continue
- method = member
- found = True
- break
-
- # If we still haven't found the method, well… time to raise
- # the not found exception.
-
- if not found:
- raise KeyError(repr(name))
-
- # Make a function separated from the class, copy the
- # annotations and add the type check on each argument.
-
- class _MethodCaller:
- def __init__(self, fref, name, func):
- self._fref = fref
- self._name = name
- self._func = func
-
- self.__annotations__ = func.__annotations__
- try:
- del self.__annotations__['self']
- except:
- pass
-
- spec = _getfullargspec(func)
-
- def annotate(arg_name):
- try:
- return spec.annotations[arg_name]
- except:
- return None
-
- self._args = list(map(annotate, spec.args[1:]))
- self._optargs = []
- if spec.defaults:
- self._optargs = self._args[-len(spec.defaults):]
- self._args = self._args[:-len(self._optargs)]
-
- def __call__(self, *args):
- if len(args) < len(self._args):
- raise _NotEnoughArgumentsError(len(self._args),
- self._name)
- if len(args) > len(self._args) + len(self._optargs):
- raise _TooManyArgumentsError(len(self._args),
- self._name)
-
- arg_types = self._args \
- + self._optargs[:len(args) - len(self._args)]
- arg_source = args
- args = []
-
- lit = zip(_count(), arg_source, arg_types)
- for index, arg, exp in lit:
- args.append(arg)
- if exp is None:
- continue
- if type(arg) in exp:
- continue
-
- # If we haven't got a color but a color is one
- # of the accepted types, try to transform into
- # a color to manage number colors using the
- # Netscape transformation such as '123'.
-
- if Reference.color in exp:
- try:
- args[-1] = self._fref.colors[arg]
- except:
- pass
- else:
- continue
-
- raise _InvalidArgumentTypeError(index,
- exp, type(arg), self._name)
-
- return self._func(*args)
-
- return _MethodCaller(self, name, method)
-
- return _FunctionGetter(self)
-
- def _get_colors(self):
- """ Colors getter, which can be used as ``.colors[value]``.
- For example, with a classical CSS reference named ``ref``:
-
- >>> ref.colors['blue']
- ... Color(type = Color.Type.RGB, red = 0, green = 0, """ \
- """blue = 255, alpha = 1.0)
- >>> ref.colors[thcolor.Reference.color(""" \
- """thcolor.Color.from_text('#123456'))]
- ... Color(type = Color.Type.RGB, red = 18, green = 52, """ \
- """blue = 86, alpha = 1.0) """
-
- class _ColorGetter:
- def __init__(self, ref):
- self._cref = ref
-
- def __getitem__(self, key):
- try:
- return key.to_color()
- except AttributeError:
- pass
-
- try:
- name = str(key)
- except:
- raise KeyError(repr(key))
-
- try:
- value = self._cref._color(name)
- except KeyError:
- pass
- except Exception as e:
- _warn(RuntimeWarning, f"{self.__class__.__name__} " \
- f"returned exception {e.__class__.__name__} instead " \
- f"of KeyError for color name {repr(name)}.")
- pass
- else:
- try:
- assert isinstance(value, _get_color_class())
- except AssertionError:
- _warn(RuntimeWarning, f"{self.__class__.__name__} " \
- f"returned non-Color value {repr(value)} for " \
- f"color name {repr(name)}, ignoring.")
- else:
- return value
-
- try:
- r, g, b = _netscape_color(name)
- except:
- pass
- else:
- Color = _get_color_class()
- return Color(Color.Type.RGB, r, g, b)
-
- raise KeyError(repr(key))
-
- return _ColorGetter(self)
-
- functions = property(_get_functions)
- colors = property(_get_colors)
-
- # ---
- # Default methods.
- # ---
-
- def _color(self, name):
- """ Name color getter used behind the
- :attr:`thcolor.Reference.colors` getter, ought to be overriden
- by superseeding classes.
-
- These classes should return ``super()._color(name)`` in case
- they have no match for the given name. """
-
- raise KeyError(f'{name}: no such color')
-
- def default():
- """ Static method for gathering the default reference, by
- default :class:`thcolor.DefaultReference`. Is only used on
- the base :class:`thcolor.Reference` type and shall not be
- overriden. """
-
- global _default_reference
-
- if _default_reference is not None:
- return _default_reference
-
- from .builtin import DefaultReference
- _default_reference = DefaultReference()
- return _default_reference
-
-# End of file.
diff --git a/thcolor/_sys.py b/thcolor/_sys.py
deleted file mode 100755
index 014270a..0000000
--- a/thcolor/_sys.py
+++ /dev/null
@@ -1,237 +0,0 @@
-#!/usr/bin/env python3
-#******************************************************************************
-# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
-# This file is part of the thcolor project, which is MIT-licensed.
-#******************************************************************************
-""" Conversions between color systems. """
-
-from math import (ceil as _ceil, atan2 as _atan2, sqrt as _sqrt, cos as _cos,
- sin as _sin, pow as _pow)
-
-from ._angle import Angle as _Angle
-
-__all__ = ["hls_to_rgb", "rgb_to_hls", "rgb_to_hwb", "hwb_to_rgb",
- "cmyk_to_rgb", "rgb_to_cmyk", "lab_to_rgb", "rgb_to_lab",
- "lch_to_lab", "lab_to_lch", "netscape_color"]
-
-# ---
-# Color systems conversion utilities.
-# ---
-
-def _rgb(r, g, b):
- return tuple(map(lambda x: int(round(x * 255, 0)), (r, g, b)))
-def _hls(hue, s, l):
- return _Angle(_Angle.Type.DEG, round(hue, 2)), round(l, 2), round(s, 2)
-
-def _rgb_to_lrgb(r, g, b):
- def _linearize(val):
- # Undo gamma encoding.
-
- val /= 255
- if val < 0.04045:
- return val / 12.92
-
- return _pow((val + 0.055) / 1.055, 2.4)
-
- return tuple(map(_linearize, (r, g, b)))
-
-def hls_to_rgb(hue, l, s):
- """ Convert HLS to RGB. """
-
- if s == 0:
- # Achromatic color.
-
- return l, l, l
-
- def _hue_to_rgb(t1, t2, hue):
- hue %= 6
-
- if hue < 1:
- return t1 + (t2 - t1) * hue
- elif hue < 3:
- return t2
- elif hue < 4:
- return t1 + (t2 - t1) * (4 - hue)
- return t1
-
- hue = (hue.degrees % 360) / 60
- if l <= 0.5:
- t2 = l * (s + 1)
- else:
- t2 = l + s - (l * s)
-
- t1 = l * 2 - t2
-
- return _rgb(\
- _hue_to_rgb(t1, t2, hue + 2),
- _hue_to_rgb(t1, t2, hue),
- _hue_to_rgb(t1, t2, hue - 2))
-
-def hwb_to_rgb(hue, w, bl):
- """ Convert HWB to RGB color.
- https://drafts.csswg.org/css-color/#hwb-to-rgb """
-
- r, g, b = map(lambda x: x / 255, hls_to_rgb(hue, 0.5, 1.0))
- if w + bl > 1:
- w, bl = map(lambda x: x / (w + bl), (w, bl))
- return _rgb(*map(lambda x: x * (1 - w - bl) + w, (r, g, b)))
-
-def cmyk_to_rgb(c, m, y, k):
- """ Convert CMYK to RGB. """
-
- r = 1 - min(1, c * (1 - k) + k)
- g = 1 - min(1, m * (1 - k) + k)
- b = 1 - min(1, y * (1 - k) + k)
-
- return _rgb(r, g, b)
-
-def lab_to_rgb(l, a, b):
- """ Convert LAB to RGB. """
-
- # TODO
- raise NotImplementedError
-
-def rgb_to_lab(r, g, b):
- """ Convert RGB to LAB. """
-
- # TODO
- raise NotImplementedError
-
-def lab_to_lch(l, a, b):
- """ Convert RGB to LAB. """
-
- h = _Angle(_Angle.Type.RAD, _atan2(b, a))
- c = _sqrt(a * a + b * b)
-
- return (l, c, h)
-
-def lch_to_lab(l, c, h):
- """ Convert LCH to LAB. """
-
- a = c * _cos(h.radians)
- b = c * _sin(h.radians)
-
- return (l, a, b)
-
-def rgb_to_hls(r, g, b):
- """ Convert RGB to HLS. """
-
- r, g, b = map(lambda x: x / 255, (r, g, b))
-
- min_value = min((r, g, b))
- max_value = max((r, g, b))
- chroma = max_value - min_value
-
- if chroma == 0:
- hue = 0
- elif r == max_value:
- hue = (g - b) / chroma
- elif g == max_value:
- hue = (b - r) / chroma + 2
- else:
- hue = (r - g) / chroma + 4
-
- hue = hue * 60 + (hue < 0) * 360
- l = (min_value + max_value) / 2
- if min_value == max_value:
- s = 0
- else:
- s = max_value - min_value
- if l < 0.5:
- s /= max_value + min_value
- else:
- s /= 2 - max_value - min_value
-
- return _hls(hue, l, s)
-
-def rgb_to_hwb(r, g, b):
- """ Convert RGB to HWB. """
-
- r, g, b = map(lambda x: x / 255, (r, g, b))
-
- max_value = max((r, g, b))
- min_value = min((r, g, b))
- chroma = max_value - min_value
-
- if chroma == 0:
- hue = 0
- elif r == max_value:
- hue = (g - b) / chroma
- elif g == max_value:
- hue = (b - r) / chroma + 2
- elif g == max_value:
- hue = (r - g) / chroma + 4
-
- hue = (hue % 6) * 360
- w = min_value
- b = max_value
-
- return _Angle(_Angle.Type.DEG, hue), w, b
-
-def rgb_to_cmyk(r, g, b):
- """ Convert RGB to CMYK. """
-
- r, g, b = map(lambda x: x / 255, (r, g, b))
-
- k = 1 - max((r, g, b))
- if k == 1:
- c, m, y = 0, 0, 0
- else:
- c = (1 - r - k) / (1 - k)
- m = (1 - g - k) / (1 - k)
- y = (1 - b - k) / (1 - k)
-
- return (c, m, y, k)
-
-# ---
-# Other utilities.
-# ---
-
-def netscape_color(name):
- """ Produce a color from a name (which can be all-text, all-digits or
- both), using the Netscape behaviour. """
-
- # Find more about this here: https://stackoverflow.com/a/8333464
- #
- # First of all:
- # - we sanitize our input by replacing invalid characters
- # by '0' characters (the 0xFFFF limit is due to how
- # UTF-16 was managed at the time).
- # - we truncate our input to 128 characters.
-
- name = name.lower()
- name = ''.join(c if c in '0123456789abcdef' \
- else ('0', '00')[ord(c) > 0xFFFF] \
- for c in name[:128])[:128]
-
- # Then we calculate some values we're going to need.
- # `iv` is the size of the zone for a member.
- # `sz` is the size of the digits slice to take in that zone
- # (max. 8).
- # `of` is the offset in the zone of the slice to take.
-
- iv = _ceil(len(name) / 3)
- of = iv - 8 if iv > 8 else 0
- sz = iv - of
-
- # Then we isolate the slices using the values calculated
- # above. `gr` will be an array of 3 or 4 digit strings
- # (depending on the number of members).
-
- gr = list(map(lambda i: name[i * iv + of:i * iv + iv] \
- .ljust(sz, '0'), range(3)))
-
- # Check how many digits we can skip at the beginning of
- # each slice.
-
- pre = min(map(lambda x: len(x) - len(x.lstrip('0')), gr))
- pre = min(pre, sz - 2)
-
- # Then we extract the values.
-
- it = map(lambda x: int('0' + x[pre:pre + 2], 16), gr)
- r, g, b = it
-
- return (r, g, b)
-
-# End of file.
diff --git a/thcolor/angles.py b/thcolor/angles.py
new file mode 100644
index 0000000..23a0c2e
--- /dev/null
+++ b/thcolor/angles.py
@@ -0,0 +1,280 @@
+#!/usr/bin/env python3
+# *****************************************************************************
+# Copyright (C) 2019-2022 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# This file is part of the thcolor project, which is MIT-licensed.
+# *****************************************************************************
+"""Angle representation and conversions."""
+
+from math import pi as _pi
+from typing import Any as _Any, Optional as _Optional
+
+from .utils import round_half_up as _round_half_up
+
+__all__ = [
+ 'Angle', 'DegreesAngle', 'GradiansAngle', 'RadiansAngle', 'TurnsAngle',
+]
+
+
+class Angle:
+ """Abstract class representing an angle within thcolor.
+
+ Used for some color representations (most notably hue).
+ """
+
+ __slots__ = ()
+
+ _value: float
+ _bottom: float = 0
+ _top: float = 1
+
+ def __init__(self):
+ pass
+
+ def __repr__(self):
+ params = (
+ (key, getattr(self, key)) for key in dir(self)
+ if not key.startswith('_') and not callable(getattr(self, key))
+ )
+
+ return (
+ f'{self.__class__.__name__}('
+ f"{', '.join(f'{key}={val!r}' for key, val in params)})"
+ )
+
+ def __eq__(self, other):
+ if not isinstance(other, Angle):
+ return False
+
+ return (
+ round(self.asturns().turns, 6) == round(other.asturns().turns, 6)
+ )
+
+ def asdegrees(self) -> 'DegreesAngle':
+ """Get the current angle as a degrees angle."""
+
+ try:
+ value = self._value
+ ob = self._bottom
+ ot = self._top
+ except AttributeError:
+ raise NotImplementedError from None
+
+ nb = DegreesAngle._bottom
+ nt = DegreesAngle._top
+
+ return DegreesAngle((value - ob) / (ot - ob) * (nt - nb) + nb)
+
+ def asgradians(self) -> 'GradiansAngle':
+ """Get the current angle as a gradians angle."""
+
+ try:
+ value = self._value
+ ob = self._bottom
+ ot = self._top
+ except AttributeError:
+ raise NotImplementedError from None
+
+ nb = GradiansAngle._bottom
+ nt = GradiansAngle._top
+
+ return GradiansAngle((value - ob) / (ot - ob) * (nt - nb) + nb)
+
+ def asradians(self) -> 'RadiansAngle':
+ """Get the current angle as a radians angle."""
+
+ try:
+ value = self._value
+ ob = self._bottom
+ ot = self._top
+ except AttributeError:
+ raise NotImplementedError from None
+
+ nb = RadiansAngle._bottom
+ nt = RadiansAngle._top
+
+ return RadiansAngle((value - ob) / (ot - ob) * (nt - nb) + nb)
+
+ def asturns(self) -> 'TurnsAngle':
+ """Get the current angle as a turns angle."""
+
+ try:
+ value = self._value
+ ob = self._bottom
+ ot = self._top
+ except AttributeError:
+ raise NotImplementedError from None
+
+ nb = TurnsAngle._bottom
+ nt = TurnsAngle._top
+
+ return TurnsAngle((value - ob) / (ot - ob) * (nt - nb) + nb)
+
+ def asprincipal(self):
+ """Get the principal angle."""
+
+ cls = self.__class__
+ value = self._value
+ bottom, top = cls._bottom, cls._top
+
+ return cls((value - bottom) % (top - bottom) + bottom)
+
+ @classmethod
+ def fromtext(
+ cls,
+ expr: str,
+ decoder: _Optional[_Any] = None,
+ ) -> 'Angle':
+ """Create a color from a string.
+
+ :param expr: The expression to decode.
+ """
+
+ if decoder is None:
+ from .builtin import DefaultColorDecoder
+
+ decoder = DefaultColorDecoder()
+
+ results = decoder.decode(expr, prefer_angles=True)
+
+ if len(results) != 1 or not isinstance(results[0], cls):
+ raise ValueError(
+ f'result of expression was not an instance of {cls.__name__}: '
+ f'single color: {results!r}',
+ )
+
+ return results[0]
+
+
+class DegreesAngle(Angle):
+ """An angle expressed in degrees.
+
+ A 270° angle can be created the following way:
+
+ .. code-block:: python
+
+ angle = DegreesAngle(270)
+
+ :param degrees: Degrees, as canonical values are between 0 and 360
+ excluded.
+ """
+
+ __slots__ = ('_value')
+
+ _bottom = 0
+ _top = 360.0
+
+ def __init__(self, degrees: float):
+ self._value = float(degrees) # % 360.0
+
+ def __str__(self):
+ x = self._value
+ return f'{_round_half_up(x, 4)}deg'
+
+ @property
+ def degrees(self) -> float:
+ """Get the degrees."""
+
+ return self._value
+
+
+class GradiansAngle(Angle):
+ """An angle expressed in gradians.
+
+ A 565.5 gradians angle can be created the following way:
+
+ .. code-block:: python
+
+ angle = GradiansAngle(565.5)
+
+ :param gradians: Gradians, as canonical values are between
+ 0 and 400.0 excluded.
+ """
+
+ __slots__ = ('_value')
+
+ _bottom = 0
+ _top = 400.0
+
+ def __init__(self, gradians: float):
+ self._value = float(gradians) # % 400.0
+
+ def __str__(self):
+ x = self._value
+ return f'{_round_half_up(x, 4)}grad'
+
+ @property
+ def gradians(self) -> float:
+ """Get the gradians."""
+
+ return self._value
+
+
+class RadiansAngle(Angle):
+ """An angle expressed in radians.
+
+ A π radians angle can be created the following way:
+
+ .. code-block:: python
+
+ from math import pi
+ angle = RadiansAngle(pi)
+
+ :param radians: Radians, as canonical are between 0 and 2π
+ excluded.
+ """
+
+ __slots__ = ('_value')
+
+ _bottom = 0
+ _top = 2 * _pi
+
+ def __init__(self, radians: float):
+ self._value = float(radians) # % (2 * _pi)
+
+ def __str__(self):
+ x = self._value
+ return f'{int(x) if x == int(x) else x}rad'
+
+ def __repr__(self):
+ r = _round_half_up(self.radians / _pi, 4)
+ return f"{self.__class__.__name__}(radians={f'{r}π' if r else '0'})"
+
+ @property
+ def radians(self) -> float:
+ """Get the radians."""
+
+ return self._value
+
+
+class TurnsAngle(Angle):
+ """An angle expressed in turns.
+
+ A 3.5 turns angle can be created the following way:
+
+ .. code-block:: python
+
+ angle = TurnsAngle(3.5)
+
+ :param turns: Turns, as canonical values are between 0 and 1
+ excluded.
+ """
+
+ __slots__ = ('_value')
+
+ _bottom = 0
+ _top = 1
+
+ def __init__(self, turns: float):
+ self._value = float(turns) # % 1.0
+
+ def __str__(self):
+ x = self._value
+ return f'{_round_half_up(x, 4)}turn'
+
+ @property
+ def turns(self) -> float:
+ """Get the turns."""
+
+ return self._value
+
+# End of file.
diff --git a/thcolor/builtin.py b/thcolor/builtin.py
new file mode 100644
index 0000000..0179768
--- /dev/null
+++ b/thcolor/builtin.py
@@ -0,0 +1,528 @@
+#!/usr/bin/env python3
+# *****************************************************************************
+# Copyright (C) 2019-2022 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# This file is part of the thcolor project, which is MIT-licensed.
+# *****************************************************************************
+"""Builtin decoders using the base elements."""
+
+from typing import Union as _Union
+
+from .angles import Angle as _Angle
+from .colors import (
+ CMYKColor as _CMYKColor, Color as _Color, HSLColor as _HSLColor,
+ HSVColor as _HSVColor, HWBColor as _HWBColor, LABColor as _LABColor,
+ LCHColor as _LCHColor, SRGBColor as _SRGBColor, XYZColor as _XYZColor,
+ YIQColor as _YIQColor, YUVColor as _YUVColor,
+)
+from .decoders import (
+ MetaColorDecoder as _MetaColorDecoder,
+ alias as _alias, fallback as _fallback,
+)
+from .utils import factor as _factor
+
+__all__ = [
+ 'CSS1ColorDecoder', 'CSS2ColorDecoder', 'CSS3ColorDecoder',
+ 'CSS4ColorDecoder', 'DefaultColorDecoder',
+]
+
+_number = _Union[int, float]
+
+
+def _rgb(x):
+ """Return an RGB color out of the given 6-digit hexadecimal code."""
+
+ from thcolor.colors import SRGBColor as _SRGBColor
+
+ return _SRGBColor(
+ int(x[1:3], 16) / 255,
+ int(x[3:5], 16) / 255,
+ int(x[5:7], 16) / 255,
+ )
+
+
+# ---
+# Main colors.
+# ---
+
+
+class CSS1ColorDecoder(_MetaColorDecoder):
+ """Named colors from CSS Level 1.
+
+ See `<https://www.w3.org/TR/CSS1/>`_ for more information.
+ """
+
+ __defaults_to_netscape_color__ = True
+
+ black = _rgb('#000000')
+ silver = _rgb('#c0c0c0')
+ gray = _rgb('#808080')
+ white = _rgb('#ffffff')
+
+ maroon = _rgb('#800000')
+ red = _rgb('#ff0000')
+ purple = _rgb('#800080')
+ fuchsia = _rgb('#ff00ff')
+ green = _rgb('#008000')
+ lime = _rgb('#00ff00')
+ olive = _rgb('#808000')
+ yellow = _rgb('#ffff00')
+ navy = _rgb('#000080')
+ blue = _rgb('#0000ff')
+ teal = _rgb('#008080')
+ aqua = _rgb('#00ffff')
+
+ transparent = _SRGBColor(0, 0, 0, 0)
+
+ @staticmethod
+ def rgb(red: int = 0, green: int = 0, blue: int = 0) -> _Color:
+ """Make an RGB color out of the given components."""
+
+ return _SRGBColor(
+ red=_factor(red, max_=255, clip=True),
+ green=_factor(green, max_=255, clip=True),
+ blue=_factor(blue, max_=255, clip=True),
+ alpha=1.0,
+ )
+
+
+class CSS2ColorDecoder(CSS1ColorDecoder):
+ """Named colors from CSS Level 2 (Revision 1).
+
+ See `<https://www.w3.org/TR/CSS2/>`_ for more information.
+ """
+
+ orange = _rgb('#ffa500')
+
+
+class CSS3ColorDecoder(CSS2ColorDecoder):
+ """Named colors and functions from CSS Color Module Level 3.
+
+ See `<https://drafts.csswg.org/css-color-3/>`_ for more information.
+ """
+
+ darkblue = _rgb('#00008B')
+ mediumblue = _rgb('#0000CD')
+ darkgreen = _rgb('#006400')
+ darkcyan = _rgb('#008B8B')
+ deepskyblue = _rgb('#00BFFF')
+ darkturquoise = _rgb('#00CED1')
+ mediumspringgreen = _rgb('#00FA9A')
+ springgreen = _rgb('#00FF7F')
+ cyan = _rgb('#00FFFF')
+ midnightblue = _rgb('#191970')
+ dodgerblue = _rgb('#1E90FF')
+ lightseagreen = _rgb('#20B2AA')
+ forestgreen = _rgb('#228B22')
+ seagreen = _rgb('#2E8B57')
+ darkslategray = _rgb('#2F4F4F')
+ darkslategrey = _rgb('#2F4F4F')
+ limegreen = _rgb('#32CD32')
+ mediumseagreen = _rgb('#3CB371')
+ turquoise = _rgb('#40E0D0')
+ royalblue = _rgb('#4169E1')
+ steelblue = _rgb('#4682B4')
+ darkslateblue = _rgb('#483D8B')
+ mediumturquoise = _rgb('#48D1CC')
+ indigo = _rgb('#4B0082')
+ darkolivegreen = _rgb('#556B2F')
+ cadetblue = _rgb('#5F9EA0')
+ cornflowerblue = _rgb('#6495ED')
+ mediumaquamarine = _rgb('#66CDAA')
+ dimgray = _rgb('#696969')
+ dimgrey = _rgb('#696969')
+ slateblue = _rgb('#6A5ACD')
+ olivedrab = _rgb('#6B8E23')
+ slategray = _rgb('#708090')
+ slategrey = _rgb('#708090')
+ lightslategray = _rgb('#778899')
+ lightslategrey = _rgb('#778899')
+ mediumslateblue = _rgb('#7B68EE')
+ lawngreen = _rgb('#7CFC00')
+ chartreuse = _rgb('#7FFF00')
+ aquamarine = _rgb('#7FFFD4')
+ grey = _rgb('#808080')
+ skyblue = _rgb('#87CEEB')
+ lightskyblue = _rgb('#87CEFA')
+ blueviolet = _rgb('#8A2BE2')
+ darkred = _rgb('#8B0000')
+ darkmagenta = _rgb('#8B008B')
+ saddlebrown = _rgb('#8B4513')
+ darkseagreen = _rgb('#8FBC8F')
+ lightgreen = _rgb('#90EE90')
+ mediumpurple = _rgb('#9370DB')
+ darkviolet = _rgb('#9400D3')
+ palegreen = _rgb('#98FB98')
+ darkorchid = _rgb('#9932CC')
+ yellowgreen = _rgb('#9ACD32')
+ sienna = _rgb('#A0522D')
+ brown = _rgb('#A52A2A')
+ darkgray = _rgb('#A9A9A9')
+ darkgrey = _rgb('#A9A9A9')
+ lightblue = _rgb('#ADD8E6')
+ greenyellow = _rgb('#ADFF2F')
+ paleturquoise = _rgb('#AFEEEE')
+ lightsteelblue = _rgb('#B0C4DE')
+ powderblue = _rgb('#B0E0E6')
+ firebrick = _rgb('#B22222')
+ darkgoldenrod = _rgb('#B8860B')
+ mediumorchid = _rgb('#BA55D3')
+ rosybrown = _rgb('#BC8F8F')
+ darkkhaki = _rgb('#BDB76B')
+ mediumvioletred = _rgb('#C71585')
+ indianred = _rgb('#CD5C5C')
+ peru = _rgb('#CD853F')
+ chocolate = _rgb('#D2691E')
+ tan = _rgb('#D2B48C')
+ lightgray = _rgb('#D3D3D3')
+ lightgrey = _rgb('#D3D3D3')
+ thistle = _rgb('#D8BFD8')
+ orchid = _rgb('#DA70D6')
+ goldenrod = _rgb('#DAA520')
+ palevioletred = _rgb('#DB7093')
+ crimson = _rgb('#DC143C')
+ gainsboro = _rgb('#DCDCDC')
+ plum = _rgb('#DDA0DD')
+ burlywood = _rgb('#DEB887')
+ lightcyan = _rgb('#E0FFFF')
+ lavender = _rgb('#E6E6FA')
+ darksalmon = _rgb('#E9967A')
+ violet = _rgb('#EE82EE')
+ palegoldenrod = _rgb('#EEE8AA')
+ lightcoral = _rgb('#F08080')
+ khaki = _rgb('#F0E68C')
+ aliceblue = _rgb('#F0F8FF')
+ honeydew = _rgb('#F0FFF0')
+ azure = _rgb('#F0FFFF')
+ sandybrown = _rgb('#F4A460')
+ wheat = _rgb('#F5DEB3')
+ beige = _rgb('#F5F5DC')
+ whitesmoke = _rgb('#F5F5F5')
+ mintcream = _rgb('#F5FFFA')
+ ghostwhite = _rgb('#F8F8FF')
+ salmon = _rgb('#FA8072')
+ antiquewhite = _rgb('#FAEBD7')
+ linen = _rgb('#FAF0E6')
+ lightgoldenrodyellow = _rgb('#FAFAD2')
+ oldlace = _rgb('#FDF5E6')
+ magenta = _rgb('#FF00FF')
+ deeppink = _rgb('#FF1493')
+ orangered = _rgb('#FF4500')
+ tomato = _rgb('#FF6347')
+ hotpink = _rgb('#FF69B4')
+ coral = _rgb('#FF7F50')
+ darkorange = _rgb('#FF8C00')
+ lightsalmon = _rgb('#FFA07A')
+ lightpink = _rgb('#FFB6C1')
+ pink = _rgb('#FFC0CB')
+ gold = _rgb('#FFD700')
+ peachpuff = _rgb('#FFDAB9')
+ navajowhite = _rgb('#FFDEAD')
+ moccasin = _rgb('#FFE4B5')
+ bisque = _rgb('#FFE4C4')
+ mistyrose = _rgb('#FFE4E1')
+ blanchedalmond = _rgb('#FFEBCD')
+ papayawhip = _rgb('#FFEFD5')
+ lavenderblush = _rgb('#FFF0F5')
+ seashell = _rgb('#FFF5EE')
+ cornsilk = _rgb('#FFF8DC')
+ lemonchiffon = _rgb('#FFFACD')
+ floralwhite = _rgb('#FFFAF0')
+ snow = _rgb('#FFFAFA')
+ lightyellow = _rgb('#FFFFE0')
+ ivory = _rgb('#FFFFF0')
+
+ @staticmethod
+ def rgb(
+ red: _number = 0,
+ green: _number = 0,
+ blue: _number = 0,
+ alpha: _number = 1.0,
+ ) -> _Color:
+ """Make an RGB color out of the given components."""
+
+ return _SRGBColor(
+ red=_factor(red, max_=255, clip=True),
+ green=_factor(green, max_=255, clip=True),
+ blue=_factor(blue, max_=255, clip=True),
+ alpha=_factor(alpha, clip=True),
+ )
+
+ @staticmethod
+ def hsl(
+ hue: _Angle,
+ saturation: _number,
+ lightness: _number,
+ alpha: _number = 1.0,
+ ) -> _Color:
+ """Make an HSL color out of the given components."""
+
+ return _HSLColor(
+ hue=hue,
+ saturation=_factor(saturation),
+ lightness=_factor(lightness),
+ alpha=_factor(alpha),
+ )
+
+ rgba = _alias('rgb')
+ hsla = _alias('hsl')
+
+
+class CSS4ColorDecoder(CSS3ColorDecoder):
+ """Named colors and functions from CSS Color Module Level 4.
+
+ See `<https://drafts.csswg.org/css-color/>`_ for more information..
+ """
+
+ __extended_hex_support__ = True
+
+ rebeccapurple = _rgb('#663399')
+
+ @staticmethod
+ def hwb(
+ hue: _Angle,
+ whiteness: _number = 0.0,
+ blackness: _number = 0.0,
+ alpha: _number = 1.0,
+ ) -> _Color:
+ """Make an HWB color out of the given components."""
+
+ return _HWBColor(
+ hue=hue,
+ whiteness=_factor(whiteness),
+ blackness=_factor(blackness),
+ alpha=_factor(alpha),
+ )
+
+ @staticmethod
+ def gray(gray: _number, alpha: _number = 1.0) -> _Color:
+ """Make a gray-scale color out of the given components."""
+
+ gray = _factor(gray, max_=255)
+ return _SRGBColor(gray, gray, gray, _factor(alpha))
+
+ @staticmethod
+ def lab(
+ light: _number,
+ a: _number,
+ b: _number,
+ alpha: _number = 1.0,
+ ) -> _Color:
+ """Make an LAB color out of the given components."""
+
+ return _LABColor(
+ lightness=max(_factor(light), 0.0),
+ a=a, b=b,
+ alpha=_factor(alpha),
+ )
+
+ @staticmethod
+ def lch(
+ light: _number,
+ chroma: _number,
+ hue: _Angle,
+ alpha: _number = 1.0,
+ ) -> _Color:
+ """Make an LCH color out of the given components."""
+
+ return _LCHColor(
+ lightness=max(_factor(light), 0.0),
+ chroma=max(chroma, 0.0),
+ hue=hue,
+ alpha=_factor(alpha),
+ )
+
+ hwba = _alias('hwb')
+
+
+class DefaultColorDecoder(CSS4ColorDecoder):
+ """Functions extending the CSS Color Module Level 4 reference."""
+
+ __ncol_support__ = True
+
+ rbg = _alias('rgb', args=('red', 'blue', 'green', 'alpha'))
+ rbga = _alias('rgb', args=('red', 'blue', 'green', 'alpha'))
+ brg = _alias('rgb', args=('blue', 'red', 'green', 'alpha'))
+ brga = _alias('rgb', args=('blue', 'red', 'green', 'alpha'))
+ bgr = _alias('rgb', args=('blue', 'green', 'red', 'alpha'))
+ bgra = _alias('rgb', args=('blue', 'green', 'red', 'alpha'))
+ gbr = _alias('rgb', args=('green', 'blue', 'red', 'alpha'))
+ gbra = _alias('rgb', args=('green', 'blue', 'red', 'alpha'))
+ grb = _alias('rgb', args=('green', 'red', 'blue', 'alpha'))
+ grba = _alias('rgb', args=('green', 'red', 'blue', 'alpha'))
+ hls = _alias('hsl', args=('hue', 'lightness', 'saturation', 'alpha'))
+ hlsa = _alias('hsl', args=('hue', 'lightness', 'saturation', 'alpha'))
+ hbw = _alias('hwb', args=('hue', 'blackness', 'whiteness', 'alpha'))
+ hbwa = _alias('hwb', args=('hue', 'blackness', 'whiteness', 'alpha'))
+ device_cmyk = _alias('cmyk')
+
+ @staticmethod
+ def cmyk(
+ cyan: _number,
+ magenta: _number = 0.0,
+ yellow: _number = 0.0,
+ black: _number = 0.0,
+ alpha: _number = 1.0,
+ ) -> _Color:
+ """Make a CMYK color out of the given components."""
+
+ return _CMYKColor(
+ cyan=_factor(cyan),
+ magenta=_factor(magenta),
+ yellow=_factor(yellow),
+ black=_factor(black),
+ alpha=_factor(alpha),
+ )
+
+ @staticmethod
+ def hsv(
+ hue: _Angle,
+ saturation: _number,
+ value: _number,
+ alpha: _number = 1.0,
+ ) -> _Color:
+ """Make an HSV color out of the given components."""
+
+ return _HSVColor(
+ hue=hue,
+ saturation=_factor(saturation),
+ value=_factor(value),
+ alpha=_factor(alpha),
+ )
+
+ @staticmethod
+ def xyz(
+ x: _number,
+ y: _number,
+ z: _number,
+ alpha: _number = 1.0,
+ ) -> _Color:
+ """Make an XYZ color out of the given components."""
+
+ return _XYZColor(
+ x=_factor(x),
+ y=_factor(y),
+ z=_factor(z),
+ alpha=_factor(alpha),
+ )
+
+ @staticmethod
+ def yiq(
+ y: _number,
+ i: _number,
+ q: _number,
+ alpha: _number = 1.0,
+ ) -> _Color:
+ """Make a YIQ color out of the given components."""
+
+ return _YIQColor(
+ y=_factor(y),
+ i=_factor(i),
+ q=_factor(q),
+ alpha=_factor(alpha),
+ )
+
+ @staticmethod
+ def yuv(
+ y: _number,
+ u: _number,
+ v: _number,
+ alpha: _number = 1.0,
+ ) -> _Color:
+ """Make a YUV color out of the given components."""
+
+ return _YUVColor(
+ y=_factor(y),
+ u=_factor(u),
+ v=_factor(v),
+ alpha=_factor(alpha),
+ )
+
+ # ---
+ # Get the RGB components of a color.
+ # ---
+
+ @_fallback(_rgb('#ff0000'))
+ @staticmethod
+ def red(color: _Color) -> int:
+ """Get the red channel value from an RGB color."""
+
+ r, g, b, _ = color.assrgb()
+ return r
+
+ @_fallback(_rgb('#00ff00'))
+ @staticmethod
+ def green(color: _Color) -> int:
+ """Get the green channel value from an RGB color."""
+
+ r, g, b, _ = color.assrgb()
+ return g
+
+ @_fallback(_rgb('#0000ff'))
+ @staticmethod
+ def blue(color: _Color) -> int:
+ """Get the blue channel value from an RGB color."""
+
+ r, g, b, _ = color.assrgb()
+ return b
+
+ # ---
+ # Manage the lightness and saturation for HSL colors.
+ # ---
+
+ @staticmethod
+ def darker(by: float, color: _Color) -> _Color:
+ """Make the color darker by a given factor.
+
+ This is accomplished by calling
+ :py:meth:`thcolor.colors.Color.darker`.
+ """
+
+ return color.darker(by)
+
+ @staticmethod
+ def lighter(by: float, color: _Color) -> _Color:
+ """Make the color lighter by a given factor.
+
+ This is accomplished by calling
+ :py:meth:`thcolor.colors.Color.lighter`.
+ """
+
+ return color.lighter(by)
+
+ @staticmethod
+ def desaturate(by: float, color: _Color) -> _Color:
+ """Desaturate the color by a given factor.
+
+ This is accomplished by calling
+ :py:meth:`thcolor.colors.Color.desaturate`.
+ """
+
+ return color.desaturate(by)
+
+ @staticmethod
+ def saturate(by: float, color: _Color) -> _Color:
+ """Saturate the color by a given factor.
+
+ This is accomplished by calling
+ :py:meth:`thcolor.colors.Color.saturate`.
+ """
+
+ return color.saturate(by)
+
+ # ---
+ # Others.
+ # ---
+
+ @staticmethod
+ def ncol(color: _Color) -> _Color:
+ """Return a natural color (NCol).
+
+ This method is actually compatibility with w3color.js.
+ NCols are managed directly without the function, so
+ the function only needs to return the color.
+ """
+
+ return color
+
+
+# End of file.
diff --git a/thcolor/builtin/__init__.py b/thcolor/builtin/__init__.py
deleted file mode 100755
index c9bc0d2..0000000
--- a/thcolor/builtin/__init__.py
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/usr/bin/env python3
-#******************************************************************************
-# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
-# This file is part of the thcolor project, which is MIT-licensed.
-#******************************************************************************
-""" Named colors references, using various sources. """
-
-from ._css import (CSS1Reference, CSS2Reference, CSS3Reference,
- CSS4Reference)
-from ._default import DefaultReference
-
-__all__ = ["CSS1Reference", "CSS2Reference", "CSS3Reference",
- "CSS4Reference", "DefaultReference"]
-
-# End of file.
diff --git a/thcolor/builtin/_css.py b/thcolor/builtin/_css.py
deleted file mode 100755
index ac9e434..0000000
--- a/thcolor/builtin/_css.py
+++ /dev/null
@@ -1,450 +0,0 @@
-#!/usr/bin/env python3
-#******************************************************************************
-# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
-# This file is part of the thcolor project, which is MIT-licensed.
-#******************************************************************************
-""" Named colors and function definitions. Color names are case-insensitive.
- Taken from: https://www.w3schools.com/cssref/css_colors.asp """
-
-from .._color import Color as _Color
-from .._ref import Reference as _Reference
-from .._exc import InvalidArgumentValueError as _InvalidArgumentValueError
-
-__all__ = ["CSS1Reference", "CSS2Reference", "CSS3Reference",
- "CSS4Reference"]
-
-def _rgb(raw):
- r = int(raw[1:3], 16)
- g = int(raw[3:5], 16)
- b = int(raw[5:7], 16)
-
- return _Color(_Color.Type.RGB, r, g, b)
-
-class CSS1Reference(_Reference):
- """ Named colors from `CSS Level 1 <https://www.w3.org/TR/CSS1/>`_. """
-
- number = _Reference.number
- percentage = _Reference.percentage
- color = _Reference.color
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- # ---
- # Named colors.
- # ---
-
- __colors = {
- 'black': _rgb('#000000'),
- 'silver': _rgb('#c0c0c0'),
- 'gray': _rgb('#808080'),
- 'white': _rgb('#ffffff'),
-
- 'maroon': _rgb('#800000'),
- 'red': _rgb('#ff0000'),
- 'purple': _rgb('#800080'),
- 'fuchsia': _rgb('#ff00ff'),
- 'green': _rgb('#008000'),
- 'lime': _rgb('#00ff00'),
- 'olive': _rgb('#808000'),
- 'yellow': _rgb('#ffff00'),
- 'navy': _rgb('#000080'),
- 'blue': _rgb('#0000ff'),
- 'teal': _rgb('#008080'),
- 'aqua': _rgb('#00ffff')}
-
- def _color(self, name):
- if name == 'transparent':
- return _Color(_Color.Type.RGB, 0, 0, 0, 0)
-
- try:
- return self.__colors[name]
- except:
- return super()._color(name)
-
- # ---
- # Utilities.
- # ---
-
- def _rgb(self, rgba, rgb_indexes):
- r, g, b, alpha = rgba
- ri, gi, bi = rgb_indexes
-
- try:
- r = r.to_byte()
- except ValueError as e:
- raise _InvalidArgumentValueError(ri, str(e))
-
- try:
- g = g.to_byte()
- except ValueError as e:
- raise _InvalidArgumentValueError(gi, str(e))
-
- try:
- b = b.to_byte()
- except ValueError as e:
- raise _InvalidArgumentValueError(bi, str(e))
-
- try:
- alpha = alpha.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(3, str(e))
-
- return _Reference.color(_Color(_Color.Type.RGB, r, g, b, alpha))
-
- # ---
- # Functions.
- # ---
-
- def rgb(self, r: number | percentage,
- g: number | percentage = number(0),
- b: number | percentage = number(0)):
- return self._rgb((r, g, b, 1.0), (0, 1, 2))
-
-class CSS2Reference(CSS1Reference):
- """ Named colors from `CSS Level 2 (Revision 1)
- <https://www.w3.org/TR/CSS2/>`_. """
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- # ---
- # Named colors.
- # ---
-
- __colors = {
- 'orange': _rgb('#ffa500')}
-
- def _color(self, name):
- try:
- return self.__colors[name]
- except:
- return super()._color(name)
-
-class CSS3Reference(CSS2Reference):
- """ Named colors and functions from `CSS Color Module Level 3
- <https://drafts.csswg.org/css-color-3/>`_. """
-
- number = _Reference.number
- percentage = _Reference.percentage
- angle = _Reference.angle
- color = _Reference.color
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- # ---
- # Named colors.
- # ---
-
- __colors = {
- 'darkblue': _rgb('#00008B'),
- 'mediumblue': _rgb('#0000CD'),
- 'darkgreen': _rgb('#006400'),
- 'darkcyan': _rgb('#008B8B'),
- 'deepskyblue': _rgb('#00BFFF'),
- 'darkturquoise': _rgb('#00CED1'),
- 'mediumspringgreen': _rgb('#00FA9A'),
- 'springgreen': _rgb('#00FF7F'),
- 'cyan': _rgb('#00FFFF'),
- 'midnightblue': _rgb('#191970'),
- 'dodgerblue': _rgb('#1E90FF'),
- 'lightseagreen': _rgb('#20B2AA'),
- 'forestgreen': _rgb('#228B22'),
- 'seagreen': _rgb('#2E8B57'),
- 'darkslategray': _rgb('#2F4F4F'),
- 'darkslategrey': _rgb('#2F4F4F'),
- 'limegreen': _rgb('#32CD32'),
- 'mediumseagreen': _rgb('#3CB371'),
- 'turquoise': _rgb('#40E0D0'),
- 'royalblue': _rgb('#4169E1'),
- 'steelblue': _rgb('#4682B4'),
- 'darkslateblue': _rgb('#483D8B'),
- 'mediumturquoise': _rgb('#48D1CC'),
- 'indigo': _rgb('#4B0082'),
- 'darkolivegreen': _rgb('#556B2F'),
- 'cadetblue': _rgb('#5F9EA0'),
- 'cornflowerblue': _rgb('#6495ED'),
- 'mediumaquamarine': _rgb('#66CDAA'),
- 'dimgray': _rgb('#696969'),
- 'dimgrey': _rgb('#696969'),
- 'slateblue': _rgb('#6A5ACD'),
- 'olivedrab': _rgb('#6B8E23'),
- 'slategray': _rgb('#708090'),
- 'slategrey': _rgb('#708090'),
- 'lightslategray': _rgb('#778899'),
- 'lightslategrey': _rgb('#778899'),
- 'mediumslateblue': _rgb('#7B68EE'),
- 'lawngreen': _rgb('#7CFC00'),
- 'chartreuse': _rgb('#7FFF00'),
- 'aquamarine': _rgb('#7FFFD4'),
- 'grey': _rgb('#808080'),
- 'skyblue': _rgb('#87CEEB'),
- 'lightskyblue': _rgb('#87CEFA'),
- 'blueviolet': _rgb('#8A2BE2'),
- 'darkred': _rgb('#8B0000'),
- 'darkmagenta': _rgb('#8B008B'),
- 'saddlebrown': _rgb('#8B4513'),
- 'darkseagreen': _rgb('#8FBC8F'),
- 'lightgreen': _rgb('#90EE90'),
- 'mediumpurple': _rgb('#9370DB'),
- 'darkviolet': _rgb('#9400D3'),
- 'palegreen': _rgb('#98FB98'),
- 'darkorchid': _rgb('#9932CC'),
- 'yellowgreen': _rgb('#9ACD32'),
- 'sienna': _rgb('#A0522D'),
- 'brown': _rgb('#A52A2A'),
- 'darkgray': _rgb('#A9A9A9'),
- 'darkgrey': _rgb('#A9A9A9'),
- 'lightblue': _rgb('#ADD8E6'),
- 'greenyellow': _rgb('#ADFF2F'),
- 'paleturquoise': _rgb('#AFEEEE'),
- 'lightsteelblue': _rgb('#B0C4DE'),
- 'powderblue': _rgb('#B0E0E6'),
- 'firebrick': _rgb('#B22222'),
- 'darkgoldenrod': _rgb('#B8860B'),
- 'mediumorchid': _rgb('#BA55D3'),
- 'rosybrown': _rgb('#BC8F8F'),
- 'darkkhaki': _rgb('#BDB76B'),
- 'mediumvioletred': _rgb('#C71585'),
- 'indianred': _rgb('#CD5C5C'),
- 'peru': _rgb('#CD853F'),
- 'chocolate': _rgb('#D2691E'),
- 'tan': _rgb('#D2B48C'),
- 'lightgray': _rgb('#D3D3D3'),
- 'lightgrey': _rgb('#D3D3D3'),
- 'thistle': _rgb('#D8BFD8'),
- 'orchid': _rgb('#DA70D6'),
- 'goldenrod': _rgb('#DAA520'),
- 'palevioletred': _rgb('#DB7093'),
- 'crimson': _rgb('#DC143C'),
- 'gainsboro': _rgb('#DCDCDC'),
- 'plum': _rgb('#DDA0DD'),
- 'burlywood': _rgb('#DEB887'),
- 'lightcyan': _rgb('#E0FFFF'),
- 'lavender': _rgb('#E6E6FA'),
- 'darksalmon': _rgb('#E9967A'),
- 'violet': _rgb('#EE82EE'),
- 'palegoldenrod': _rgb('#EEE8AA'),
- 'lightcoral': _rgb('#F08080'),
- 'khaki': _rgb('#F0E68C'),
- 'aliceblue': _rgb('#F0F8FF'),
- 'honeydew': _rgb('#F0FFF0'),
- 'azure': _rgb('#F0FFFF'),
- 'sandybrown': _rgb('#F4A460'),
- 'wheat': _rgb('#F5DEB3'),
- 'beige': _rgb('#F5F5DC'),
- 'whitesmoke': _rgb('#F5F5F5'),
- 'mintcream': _rgb('#F5FFFA'),
- 'ghostwhite': _rgb('#F8F8FF'),
- 'salmon': _rgb('#FA8072'),
- 'antiquewhite': _rgb('#FAEBD7'),
- 'linen': _rgb('#FAF0E6'),
- 'lightgoldenrodyellow': _rgb('#FAFAD2'),
- 'oldlace': _rgb('#FDF5E6'),
- 'magenta': _rgb('#FF00FF'),
- 'deeppink': _rgb('#FF1493'),
- 'orangered': _rgb('#FF4500'),
- 'tomato': _rgb('#FF6347'),
- 'hotpink': _rgb('#FF69B4'),
- 'coral': _rgb('#FF7F50'),
- 'darkorange': _rgb('#FF8C00'),
- 'lightsalmon': _rgb('#FFA07A'),
- 'lightpink': _rgb('#FFB6C1'),
- 'pink': _rgb('#FFC0CB'),
- 'gold': _rgb('#FFD700'),
- 'peachpuff': _rgb('#FFDAB9'),
- 'navajowhite': _rgb('#FFDEAD'),
- 'moccasin': _rgb('#FFE4B5'),
- 'bisque': _rgb('#FFE4C4'),
- 'mistyrose': _rgb('#FFE4E1'),
- 'blanchedalmond': _rgb('#FFEBCD'),
- 'papayawhip': _rgb('#FFEFD5'),
- 'lavenderblush': _rgb('#FFF0F5'),
- 'seashell': _rgb('#FFF5EE'),
- 'cornsilk': _rgb('#FFF8DC'),
- 'lemonchiffon': _rgb('#FFFACD'),
- 'floralwhite': _rgb('#FFFAF0'),
- 'snow': _rgb('#FFFAFA'),
- 'lightyellow': _rgb('#FFFFE0'),
- 'ivory': _rgb('#FFFFF0')}
-
- def _color(self, name):
- try:
- return self.__colors[name]
- except:
- return super()._color(name)
-
- # ---
- # Utilities.
- # ---
-
- def _hsl(self, hsla, hsl_indexes):
- h, s, l, alpha = hsla
- hi, si, li = hsl_indexes
-
- try:
- h = h.to_hue()
- except ValueError as e:
- raise _InvalidArgumentValueError(hi, str(e))
-
- try:
- s = s.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(si, str(e))
-
- try:
- l = l.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(li, str(e))
-
- try:
- alpha = alpha.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(3, str(e))
-
- return _Reference.color(_Color(_Color.Type.HSL, h, s, l, alpha))
-
- # ---
- # Functions.
- # ---
-
- @_Reference.alias('rgba')
- def rgb(self, r: number | percentage,
- g: number | percentage = number(0), b: number | percentage = number(0),
- alpha: number | percentage = number(1.0)):
- return self._rgb((r, g, b, alpha), (0, 1, 2))
-
- @_Reference.alias('hsla')
- def hsl(self, h: number | angle, s: number | percentage,
- l: number | percentage, alpha: number | percentage = number(1.0)):
- return self._hsl((h, s, l, alpha), (0, 1, 2))
-
-class CSS4Reference(CSS3Reference):
- """ Named colors and functions from `CSS Color Module Level 4
- <https://drafts.csswg.org/css-color/>`_. """
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- number = _Reference.number
- percentage = _Reference.percentage
- angle = _Reference.angle
- color = _Reference.color
-
- # ---
- # Named colors.
- # ---
-
- __colors = {
- 'rebeccapurple': _rgb('#663399')}
-
- def _color(self, name):
- try:
- return self.__colors[name]
- except:
- return super()._color(name)
-
- # ---
- # Utilities.
- # ---
-
- def _hwb(self, hwba, hwb_indexes):
- h, w, b, alpha = hwba
- hi, wi, bi = hwb_indexes
-
- try:
- h = h.to_hue()
- except ValueError as e:
- raise _InvalidArgumentValueError(hi, str(e))
-
- try:
- w = w.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(wi, str(e))
-
- try:
- b = b.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(bi, str(e))
-
- try:
- alpha = alpha.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(3, str(e))
-
- return _Reference.color(_Color(_Color.Type.HWB, h, w, b, alpha))
-
- # ---
- # Functions.
- # ---
-
- @_Reference.alias('hwba')
- def hwb(self, h: number | angle, w: number | percentage = number(0),
- b: number | percentage = number(0),
- alpha: number | percentage = number(1.0)):
- return self._hwb((h, w, b, alpha), (0, 1, 2))
-
- def gray(self, g: number | percentage,
- alpha: number | percentage = number(1.0)):
- try:
- g = g.to_byte()
- except ValueError as e:
- raise _InvalidArgumentValueError(0, str(e))
-
- try:
- alpha = alpha.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(1, str(e))
-
- return _Reference.color(_Color(_Color.Type.RGB, g, g, g, alpha))
-
- def lab(self, l: number, a: number, b: number,
- alpha: number | percentage = number(1.0)):
-
- try:
- l = l.value
- if l < 0:
- l = 0
- l /= 100
- except ValueError as e:
- raise _InvalidArgumentValueError(0, str(e))
-
- a = a.value
- b = b.value
-
- try:
- alpha = alpha.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(3, str(e))
-
- return _Reference.color(_Color(_Color.Type.LAB, l, a, b, alpha))
-
- def lch(self, l: number, c: number, h: number | angle,
- alpha: number | percentage = number(1.0)):
-
- try:
- l = l.value
- if l < 0:
- l = 0
- l /= 100
- except ValueError as e:
- raise _InvalidArgumentValueError(0, str(e))
-
- c = c.value
- if c < 0:
- c = 0
-
- try:
- h = h.to_hue()
- except ValueError as e:
- raise _InvalidArgumentValueError(2, str(e))
-
- try:
- alpha = alpha.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(3, str(e))
-
- return _Reference.color(_Color(_Color.Type.LCH, l, c, h, alpha))
-
-# End of file.
diff --git a/thcolor/builtin/_default.py b/thcolor/builtin/_default.py
deleted file mode 100755
index dd54a2b..0000000
--- a/thcolor/builtin/_default.py
+++ /dev/null
@@ -1,232 +0,0 @@
-#!/usr/bin/env python3
-#******************************************************************************
-# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
-# This file is part of the thcolor project, which is MIT-licensed.
-#******************************************************************************
-""" Named colors and function definitions. Color names are case-insensitive.
- Extends the CSS references. """
-
-from .._color import Color as _Color
-from .._ref import Reference as _Reference
-from ._css import CSS4Reference as _CSS4Reference
-
-__all__ = ["DefaultReference"]
-
-class DefaultReference(_CSS4Reference):
- """ Functions extending the CSS Color Module Level 4 reference. """
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- number = _Reference.number
- percentage = _Reference.percentage
- angle = _Reference.angle
- color = _Reference.color
-
- # ---
- # RGB functions.
- # ---
-
- @_Reference.alias('rbga')
- def rbg(self, r: number | percentage,
- b: number | percentage = number(0), g: number | percentage = number(0),
- alpha: number | percentage = number(1.0)):
- return self._rgb((r, g, b, alpha), (0, 2, 1))
-
- @_Reference.alias('brga')
- def brg(self, b: number | percentage,
- r: number | percentage = number(0), g: number | percentage = number(0),
- alpha: number | percentage = number(1.0)):
- return self._rgb((r, g, b, alpha), (1, 2, 0))
-
- @_Reference.alias('bgra')
- def bgr(self, b: number | percentage,
- g: number | percentage = number(0), r: number | percentage = number(0),
- alpha: number | percentage = number(1.0)):
- return self._rgb((r, g, b, alpha), (2, 1, 0))
-
- @_Reference.alias('gbra')
- def gbr(self, g: number | percentage,
- b: number | percentage = number(0), r: number | percentage = number(0),
- alpha: number | percentage = number(1.0)):
- return self._rgb((r, g, b, alpha), (2, 0, 1))
-
- @_Reference.alias('grba')
- def grb(self, g: number | percentage,
- r: number | percentage = number(0), b: number | percentage = number(0),
- alpha: number | percentage = number(1.0)):
- return self._rgb((r, g, b, alpha), (1, 0, 2))
-
- # ---
- # HLS and HWB aliases.
- # ---
-
- @_Reference.alias('hlsa')
- def hls(self, h: number | angle, l: number | percentage,
- s: number | percentage, alpha: number | percentage = number(1.0)):
- return self._hsl((h, s, l, alpha), (0, 2, 1))
-
- @_Reference.alias('hbwa')
- def hbw(self, h: number | angle, b: number | percentage = number(0),
- w: number | percentage = number(0),
- alpha: number | percentage = number(1.0)):
- return self._hwb((h, w, b, alpha), (0, 2, 1))
-
- # ---
- # CMYK utilities and extensions.
- # ---
-
- def cmyk(self, c: number | percentage,
- m: number | percentage = percentage(0),
- y: number | percentage = percentage(0),
- k = number | percentage(0),
- alpha: number | percentage = number(1.0)):
- ci, mi, yi, ki = 0, 1, 2, 3
-
- try:
- c = c.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(ci, str(e))
-
- try:
- m = m.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(mi, str(e))
-
- try:
- y = y.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(yi, str(e))
-
- try:
- k = k.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(ki, str(e))
-
- try:
- alpha = alpha.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(4, str(e))
-
- return _Reference.color(_Color(_Color.Type.CMYK, c, m, y, k, alpha))
-
- def xyz(self, x: number | percentage, y: number | percentage,
- z: number | percentage, alpha: number | percentage = number(1.0)):
-
- try:
- x = x.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(0, str(e))
-
- try:
- y = y.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(1, str(e))
-
- try:
- z = z.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(2, str(e))
-
- try:
- alpha = alpha.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(3, str(e))
-
- return _Reference.color(_Color(_Color.Type.XYZ, x, y, z, alpha))
-
- # ---
- # Get the RGB components of a color.
- # ---
-
- def red(self, col: color) -> number:
- r, g, b = col.to_color().rgb()
- return _Reference.number(r)
-
- def green(self, col: color) -> number:
- r, g, b = col.to_color().rgb()
- return _Reference.number(g)
-
- def blue(self, col: color) -> number:
- r, g, b = col.to_color().rgb()
- return _Reference.number(b)
-
- # ---
- # Manage the lightness and saturation for HSL colors.
- # ---
-
- def darker(self, by: number | percentage, col: color) -> color:
- try:
- by = by.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(0, str(e))
-
- try:
- col = col.to_color()
- except ValueError as e:
- raise _InvalidArgumentValueError(0, str(e))
-
- h, l, s, a = col.hlsa()
- l = max(l - by, 0.0)
-
- return _Reference.color(_Color(_Color.Type.HSL, h, s, l, a))
-
- def lighter(self, by: number | percentage, col: color) -> color:
- try:
- by = by.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(0, str(e))
-
- try:
- col = col.to_color()
- except ValueError as e:
- raise _InvalidArgumentValueError(0, str(e))
-
- h, l, s, a = col.hlsa()
- l = min(l + by, 1.0)
-
- return _Reference.color(_Color(_Color.Type.HSL, h, s, l, a))
-
- def desaturate(self, by: number | percentage, col: color) -> color:
- try:
- by = by.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(0, str(e))
-
- try:
- col = col.to_color()
- except ValueError as e:
- raise _InvalidArgumentValueError(0, str(e))
-
- h, l, s, a = col.hlsa()
- s = max(s - by, 0.0)
-
- return _Reference.color(_Color(_Color.Type.HSL, h, s, l, a))
-
- def saturate(self, by: number | percentage, col: color) -> color:
- try:
- by = by.to_factor()
- except ValueError as e:
- raise _InvalidArgumentValueError(0, str(e))
-
- try:
- col = col.to_color()
- except ValueError as e:
- raise _InvalidArgumentValueError(0, str(e))
-
- h, l, s, a = col.hlsa()
- s = min(s + by, 1.0)
-
- return _Reference.color(_Color(_Color.Type.HSL, h, s, l, a))
-
- # ---
- # Others.
- # ---
-
- def ncol(self, col: color) -> color:
- # Compatibility with w3color.js! NCols are managed directly without
- # the function, so the function doesn't do anything.
-
- return col
-
-# End of file.
diff --git a/thcolor/colors.py b/thcolor/colors.py
new file mode 100644
index 0000000..cfc9240
--- /dev/null
+++ b/thcolor/colors.py
@@ -0,0 +1,1424 @@
+#!/usr/bin/env python3
+# *****************************************************************************
+# Copyright (C) 2019-2022 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# This file is part of the thcolor project, which is MIT-licensed.
+# *****************************************************************************
+"""Color representations and conversions."""
+
+from math import (
+ atan2 as _atan2, ceil as _ceil, cos as _cos, sin as _sin, sqrt as _sqrt,
+)
+from typing import (
+ Any as _Any, Optional as _Optional, Sequence as _Sequence, Tuple as _Tuple,
+)
+
+from .angles import (
+ Angle as _Angle, DegreesAngle as _DegreesAngle,
+ RadiansAngle as _RadiansAngle, TurnsAngle as _TurnsAngle,
+)
+from .utils import round_half_up as _round_half_up
+
+__all__ = [
+ 'CMYKColor', 'Color', 'HSLColor', 'HSVColor', 'HWBColor',
+ 'LABColor', 'LCHColor', 'SRGBColor', 'XYZColor',
+ 'YIQColor', 'YUVColor',
+]
+
+
+class Color:
+ """Class representing a color within thcolor.
+
+ :param alpha: Value for :py:attr:`alpha`.
+ """
+
+ __slots__ = ('_alpha')
+ _params: _Sequence[str] = ()
+
+ def __init__(self, alpha: float = 1.0):
+ super().__init__()
+
+ try:
+ alpha = float(alpha)
+ except (TypeError, ValueError):
+ raise ValueError(f'alpha should be a float, is {alpha!r}')
+ else:
+ if alpha < 0 or alpha > 1:
+ raise ValueError('alpha should be between 0.0 and 1.0')
+
+ self._alpha = alpha
+
+ def __repr__(self):
+ params = (
+ (key, getattr(self, key))
+ for key in self._params + ('alpha',)
+ )
+ return (
+ f'{self.__class__.__name__}('
+ f'{", ".join(f"{key}={val!r}" for key, val in params)})'
+ )
+
+ def __eq__(self, other: _Any) -> bool:
+ if not isinstance(other, Color):
+ return False
+
+ srgb = tuple(map(lambda x: round(x, 3), self.assrgb()))
+ orgb = tuple(map(lambda x: round(x, 3), other.assrgb()))
+
+ return srgb == orgb
+
+ @property
+ def alpha(self) -> float:
+ """Get the alpha component value.
+
+ Represented as a float varying between 0.0 (invisible)
+ and 1.0 (opaque).
+ """
+
+ return self._alpha
+
+ @classmethod
+ def fromtext(
+ cls,
+ expr: str,
+ decoder: _Optional[_Any] = None,
+ ) -> 'Color':
+ """Create a color from a string.
+
+ :param expr: The expression to decode.
+ """
+
+ if decoder is None:
+ from .builtin import DefaultColorDecoder
+
+ decoder = DefaultColorDecoder()
+
+ results = decoder.decode(expr, prefer_colors=True)
+
+ if len(results) != 1 or not isinstance(results[0], cls):
+ raise ValueError(
+ f'result of expression was not an instance of {cls.__name__}: '
+ f'single color: {results!r}',
+ )
+
+ return results[0]
+
+ # ---
+ # Conversions.
+ # ---
+
+ def assrgb(self) -> 'SRGBColor':
+ """Get an SRGBColor out of the current object."""
+
+ raise NotImplementedError
+
+ def ashsl(self) -> 'HSLColor':
+ """Get an HSLColor out of the current object."""
+
+ return self.assrgb().ashsl()
+
+ def ashsv(self) -> 'HSVColor':
+ """Get an HSVColor out of the current object."""
+
+ return self.assrgb().ashsv()
+
+ def ashwb(self) -> 'HWBColor':
+ """Get an HWBColor out of the current object."""
+
+ return self.assrgb().ashwb()
+
+ def ascmyk(self) -> 'CMYKColor':
+ """Get a CMYKColor out of the current object."""
+
+ return self.assrgb().ascmyk()
+
+ def aslab(self) -> 'LABColor':
+ """Get a LABColor out of the current object."""
+
+ raise NotImplementedError
+
+ def aslch(self) -> 'LCHColor':
+ """Get a LCHColor out of the current object."""
+
+ raise NotImplementedError
+
+ def asxyz(self) -> 'XYZColor':
+ """Get an XYZColor out of the current object."""
+
+ raise NotImplementedError
+
+ def asyiq(self) -> 'YIQColor':
+ """Get an YIQColor out of the current object."""
+
+ return self.assrgb().asyiq()
+
+ def asyuv(self) -> 'YUVColor':
+ """Get an YUVColor out of the current object."""
+
+ return self.assrgb().asyuv()
+
+ # ---
+ # Operations on colors.
+ # ---
+
+ def replace(self, **properties) -> 'Color':
+ """Get the color with the given properties replaced.
+
+ For changing the alpha on an RGB color:
+
+ .. code-block:: python
+
+ >>> SRGBColor(.1, .2, .3).replace(alpha=.5)
+ ... SRGBColor(red=0.1, green=0.2, blue=0.3, alpha=0.5)
+
+ For changing the lightness on an HSL color:
+
+ .. code-block:: pycon
+
+ >>> HSLColor(DegreesAngle(270), .5, 1).replace(lightness=.2)
+ ... HSLColor(hue=DegreesAngle(degrees=270.0), saturation=0.5,
+ ... lightness=0.2, alpha=1.0)
+
+ :param properties: Properties to change from the original color.
+ """
+
+ params = {
+ key: getattr(self, key)
+ for key in (*self._params, 'alpha')
+ }
+
+ for key, value in properties.items():
+ if key not in params:
+ raise KeyError(
+ f'no such argument {key!r} in '
+ f'{self.__class__.__name__} parameters',
+ )
+
+ params[key] = value
+
+ return type(self)(**params)
+
+ def darker(self, by: float = 0.1) -> 'Color':
+ """Get a darker version of the given color.
+
+ :param by: Percentage by which the color should be darker.
+ """
+
+ color = self.ashsl()
+ return color.replace(lightness=max(color.lightness - by, 0.0))
+
+ def lighter(self, by: float = 0.1) -> 'Color':
+ """Get a lighter version of the given color.
+
+ :param by: Percentage by which the color should be lighter.
+ """
+
+ color = self.ashsl()
+ return color.replace(lightness=min(color.lightness + by, 1.0))
+
+ def desaturate(self, by: float = 0.1) -> 'Color':
+ """Get a less saturated version of the given color.
+
+ :param by: Percentage by which the color should be
+ desaturated.
+ """
+
+ color = self.ashsl()
+ return color.replace(saturation=max(color.saturation - by, 0.0))
+
+ def saturate(self, by: float = 0.1) -> 'Color':
+ """Get a more saturated version of the given color.
+
+ :param by: Percentage by which the color should be
+ saturated.
+ """
+
+ color = self.ashsl()
+ return color.replace(saturation=min(color.saturation + by, 1.0))
+
+ def css(self) -> _Sequence[str]:
+ """Get the CSS color descriptions.
+
+ Includes older CSS specifications compatibility,
+ as a sequence of strings.
+
+ For example:
+
+ >>> SRGBColor.frombytes(18, 52, 86, 0.82).css()
+ ... ("#123456", "rgba(18, 52, 86, 82%)")
+ """
+
+ def _percent(prop):
+ per = _round_half_up(prop, 4) * 100
+ if per == int(per):
+ per = int(per)
+ return per
+
+ def _deg(agl):
+ return int(agl.asdegrees().degrees)
+
+ def statements():
+ # Start by yelling a #RRGGBB color, compatible with most
+ # web browsers around the world, followed by the rgba()
+ # notation if the alpha value isn't 1.0.
+ a = _round_half_up(self.alpha, 3)
+
+ try:
+ rgb = self.assrgb()
+ except NotImplementedError:
+ pass
+ else:
+ r, g, b = rgb.asbytes()
+
+ yield f'#{r:02X}{g:02X}{b:02X}'
+
+ if a < 1.0:
+ yield f'rgba({r}, {g}, {b}, {_percent(a)}%)'
+
+ # Then yield more specific CSS declarations in case
+ # they're supported (which would be neat!).
+ if isinstance(self, HSLColor):
+ hue, sat, lgt = (
+ self.hue, self.saturation, self.lightness,
+ )
+ args = (
+ f'{_deg(hue)}deg, {_percent(sat)}%, {_percent(lgt)}%'
+ )
+
+ if a < 1.0:
+ yield f'hsla({args}, {_percent(a)}%)'
+ else:
+ yield f'hsl({args})'
+ elif isinstance(self, HWBColor):
+ hue, wht, blk = (
+ self.hue, self.whiteness, self.blackness)
+
+ args = f'{_deg(hue)}deg, ' \
+ f'{_percent(wht)}%, {_percent(blk)}%'
+
+ if a < 1.0:
+ yield f'hwba({args}, {_percent(a)}%)'
+ else:
+ yield f'hwb({args})'
+
+ return tuple(statements())
+
+# ---
+# Color implementations.
+# ---
+
+
+class SRGBColor(Color):
+ """A color expressed using its channel intensities in the sRGB profile.
+
+ :param red: Value for :py:attr:`red`.
+ :param green: Value for :py:attr:`green`.
+ :param blue: Value for :py:attr:`blue`.
+ :param alpha: Value for :py:attr:`alpha`.
+ """
+
+ __slots__ = ('_red', '_green', '_blue')
+ _params = ('red', 'green', 'blue')
+
+ def __init__(
+ self,
+ red: float,
+ green: float,
+ blue: float,
+ alpha: float = 1.0,
+ ):
+ super().__init__(alpha)
+
+ try:
+ red = float(red)
+ except (TypeError, ValueError):
+ raise ValueError(f'red should be a float, is {red!r}')
+
+ try:
+ green = float(green)
+ except (TypeError, ValueError):
+ raise ValueError(f'green should be a float, is {green!r}')
+
+ try:
+ blue = float(blue)
+ except (TypeError, ValueError):
+ raise ValueError(f'blue should be a float, is {blue!r}')
+
+ self._red = red
+ self._green = green
+ self._blue = blue
+
+ def __iter__(self):
+ return iter((
+ self.red,
+ self.green,
+ self.blue,
+ self.alpha,
+ ))
+
+ @property
+ def red(self) -> float:
+ """Get the intensity of the red channel.
+
+ Represented as a float between 0.0 (dark) and 1.0 (light).
+ """
+
+ return self._red
+
+ @property
+ def green(self) -> float:
+ """Get the intensity of the green channel.
+
+ Represented as a float between 0.0 (dark) and 1.0 (light).
+ """
+
+ return self._green
+
+ @property
+ def blue(self) -> float:
+ """Get the intensity of the blue channel.
+
+ Represented as a float between 0.0 (dark) and 1.0 (light).
+ """
+
+ return self._blue
+
+ @classmethod
+ def frombytes(
+ cls,
+ red: int,
+ green: int,
+ blue: int,
+ alpha: float = 1.0,
+ ) -> 'SRGBColor':
+ """Get an sRGB color from colors using values between 0 and 255."""
+
+ return cls(
+ red=red / 255,
+ green=green / 255,
+ blue=blue / 255,
+ alpha=alpha,
+ )
+
+ @classmethod
+ def fromnetscapecolorname(cls, name: str) -> 'SRGBColor':
+ """Get an sRGB color from a Netscape color name."""
+
+ name = str(name)
+ if name[0] == '#':
+ name = name[1:]
+
+ # Find more about this here: https://stackoverflow.com/a/8333464
+ #
+ # First of all:
+ # - we sanitize our input by replacing invalid characters
+ # by '0' characters (the 0xFFFF limit is due to how
+ # UTF-16 was managed at the time).
+ # - we truncate our input to 128 characters.
+ name = name.lower()
+ name = ''.join(
+ c if c in '0123456789abcdef' else '00'[:1 + (ord(c) > 0xFFFF)]
+ for c in name[:128]
+ )[:128]
+
+ # Then we calculate some values we're going to need.
+ # `iv` is the size of the zone for a member.
+ # `sz` is the size of the digits slice to take in that zone
+ # (max. 8).
+ # `of` is the offset in the zone of the slice to take.
+ iv = _ceil(len(name) / 3)
+ of = iv - 8 if iv > 8 else 0
+ sz = iv - of
+
+ # Then we isolate the slices using the values calculated
+ # above. `gr` will be an array of 3 or 4 digit strings
+ # (depending on the number of members).
+ gr = list(map(
+ lambda i: name[i * iv + of:i * iv + iv].ljust(sz, '0'),
+ range(3),
+ ))
+
+ # Check how many digits we can skip at the beginning of
+ # each slice.
+ pre = min(map(lambda x: len(x) - len(x.lstrip('0')), gr))
+ pre = min(pre, sz - 2)
+
+ # Then we extract the values.
+ r, g, b = map(lambda x: int('0' + x[pre:pre + 2], 16), gr)
+
+ return cls.frombytes(r, g, b)
+
+ def assrgb(self) -> 'SRGBColor':
+ """Get an SRGBColor out of the current object."""
+
+ return SRGBColor(
+ red=self.red,
+ green=self.green,
+ blue=self.blue,
+ alpha=self.alpha,
+ )
+
+ def ashsl(self) -> 'HSLColor':
+ """Get an HSLColor out of the current object."""
+
+ r, g, b = self.red, self.green, self.blue
+
+ min_value = min((r, g, b))
+ max_value = max((r, g, b))
+ chroma = max_value - min_value
+
+ if chroma == 0:
+ hue = 0.
+ elif r == max_value:
+ hue = (g - b) / chroma
+ elif g == max_value:
+ hue = (b - r) / chroma + 2
+ else:
+ hue = (r - g) / chroma + 4
+
+ hue = hue * 60 + (hue < 0) * 360
+ lgt = (min_value + max_value) / 2
+ if min_value == max_value:
+ s = 0.
+ else:
+ s = max_value - min_value
+ if lgt < 0.5:
+ s /= max_value + min_value
+ else:
+ s /= 2 - max_value - min_value
+
+ return HSLColor(
+ hue=_DegreesAngle(_round_half_up(hue, 2)),
+ saturation=_round_half_up(s, 2),
+ lightness=_round_half_up(lgt, 2),
+ alpha=self.alpha,
+ )
+
+ def ashsv(self) -> 'HSVColor':
+ """Get an HSVColor out of the current object."""
+
+ r, g, b = self.red, self.green, self.blue
+ maxc = max(r, g, b)
+ minc = min(r, g, b)
+
+ value = maxc
+ if minc == maxc:
+ turns, saturation = 0., 0.
+ else:
+ saturation = (maxc - minc) / maxc
+ rc, gc, bc = map(lambda x: (maxc - x) / (maxc - minc), (r, g, b))
+
+ if r == maxc:
+ turns = bc - gc
+ elif g == maxc:
+ turns = 2 + rc - bc
+ else:
+ turns = 4 + gc - rc
+
+ turns = (turns / 6) % 1
+
+ return HSVColor(
+ hue=_TurnsAngle(turns),
+ saturation=saturation,
+ value=value,
+ alpha=self.alpha,
+ )
+
+ def ashwb(self) -> 'HWBColor':
+ """Get an HWBColor out of the current object."""
+
+ r, g, b = self.red, self.green, self.blue
+
+ max_ = max((r, g, b))
+ min_ = min((r, g, b))
+ chroma = max_ - min_
+
+ if chroma == 0:
+ hue = 0.
+ elif r == max_:
+ hue = (g - b) / chroma
+ elif g == max_:
+ hue = (b - r) / chroma + 2
+ elif b == max_:
+ hue = (r - g) / chroma + 4
+
+ hue /= 6
+ w = min_
+ b = 1 - max_
+
+ return HWBColor(
+ hue=_TurnsAngle(hue),
+ whiteness=w,
+ blackness=b,
+ alpha=self.alpha,
+ )
+
+ def ascmyk(self) -> 'CMYKColor':
+ """Get a CMYKColor out of the current object."""
+
+ r, g, b, _ = self
+
+ k = 1 - max((r, g, b))
+ if k == 1:
+ c, m, y = 0, 0, 0
+ else:
+ c = (1 - r - k) / (1 - k)
+ m = (1 - g - k) / (1 - k)
+ y = (1 - b - k) / (1 - k)
+
+ return CMYKColor(
+ cyan=c,
+ magenta=m,
+ yellow=y,
+ black=k,
+ alpha=self.alpha,
+ )
+
+ def asyiq(self) -> 'YIQColor':
+ """Get an YIQColor out of the current object."""
+
+ r, g, b = self.red, self.green, self.blue
+
+ return YIQColor(
+ y=.587 * g + .114 * b + .299 * r,
+ i=-.275 * g - .321 * b + .596 * r,
+ q=-.523 * g + .311 * b + .212 * r,
+ alpha=self.alpha,
+ )
+
+ def asbytes(self) -> _Tuple[int, int, int]:
+ """Get the red, blue and green bytes."""
+
+ return (
+ int(_round_half_up(self.red * 255)),
+ int(_round_half_up(self.green * 255)),
+ int(_round_half_up(self.blue * 255)),
+ )
+
+
+class HSLColor(Color):
+ """A color expressed using its hue, saturation and lightness components.
+
+ :param hue: Value for :py:attr:`hue`.
+ :param saturation: Value for :py:attr:`saturation`.
+ :param lightness: Value for :py:attr:`lightness`.
+ :param alpha: Value for :py:attr:`alpha`.
+ """
+
+ __slots__ = ('_hue', '_saturation', '_lightness')
+ _params = ('hue', 'saturation', 'lightness')
+
+ def __init__(
+ self,
+ hue: _Angle,
+ saturation: float,
+ lightness: float,
+ alpha: float = 1.0,
+ ):
+ super().__init__(alpha)
+
+ if not isinstance(hue, _Angle):
+ raise ValueError('hue should be an angle')
+
+ try:
+ saturation = float(saturation)
+ except (TypeError, ValueError):
+ raise ValueError(
+ f'saturation should be a float, is {saturation!r}',
+ )
+ else:
+ if saturation < 0 or saturation > 1:
+ raise ValueError('saturation should be between 0.0 and 1.0')
+
+ try:
+ lightness = float(lightness)
+ except (TypeError, ValueError):
+ raise ValueError(f'lightness should be a float, is {lightness!r}')
+ else:
+ if lightness < 0 or lightness > 1:
+ raise ValueError('lightness should be between 0.0 and 1.0')
+
+ self._hue = hue
+ self._saturation = saturation
+ self._lightness = lightness
+
+ def __iter__(self):
+ return iter((self.hue, self.saturation, self.lightness, self.alpha))
+
+ @property
+ def hue(self) -> _Angle:
+ """Get the hue, as an angle."""
+
+ return self._hue
+
+ @property
+ def saturation(self) -> float:
+ """Get the saturation, between 0.0 and 1.0."""
+
+ return self._saturation
+
+ @property
+ def lightness(self) -> float:
+ """Get the lightness, between 0.0 and 1.0."""
+
+ return self._lightness
+
+ def assrgb(self) -> 'SRGBColor':
+ """Get an SRGBColor out of the current object."""
+
+ hue_obj, s, lgt = self.hue.asdegrees(), self.saturation, self.lightness
+
+ if s == 0:
+ # Achromatic color.
+
+ return SRGBColor(
+ red=lgt,
+ green=lgt,
+ blue=lgt,
+ alpha=self.alpha,
+ )
+
+ def _hue_to_rgb(t1, t2, hue):
+ hue %= 6
+
+ if hue < 1:
+ return t1 + (t2 - t1) * hue
+ elif hue < 3:
+ return t2
+ elif hue < 4:
+ return t1 + (t2 - t1) * (4 - hue)
+ return t1
+
+ hue = (hue_obj.degrees % 360) / 60
+ if lgt <= 0.5:
+ t2 = lgt * (s + 1)
+ else:
+ t2 = lgt + s - (lgt * s)
+
+ t1 = lgt * 2 - t2
+
+ return SRGBColor(
+ red=_hue_to_rgb(t1, t2, hue + 2),
+ green=_hue_to_rgb(t1, t2, hue),
+ blue=_hue_to_rgb(t1, t2, hue - 2),
+ alpha=self.alpha,
+ )
+
+ def ashsl(self) -> 'HSLColor':
+ """Get an HSLColor out of the current object."""
+
+ return HSLColor(
+ hue=self.hue,
+ saturation=self.saturation,
+ lightness=self.lightness,
+ alpha=self.alpha,
+ )
+
+
+class HSVColor(Color):
+ """A color expressed using its hue, saturation and value components.
+
+ :param hue: Value for :py:attr:`hue`.
+ :param saturation: Value for :py:attr:`saturation`.
+ :param value: Value for :py:attr:`value`.
+ :param alpha: Value for :py:attr:`alpha`.
+ """
+
+ __slots__ = ('_hue', '_saturation', '_value')
+ _params = ('hue', 'saturation', 'value')
+
+ def __init__(
+ self,
+ hue: _Angle,
+ saturation: float,
+ value: float,
+ alpha: float = 1.0,
+ ):
+ super().__init__(alpha)
+
+ self._hue = hue
+ self._saturation = saturation
+ self._value = value
+
+ def __iter__(self):
+ return iter((self.hue, self.saturation, self.value, self.alpha))
+
+ @property
+ def hue(self) -> _Angle:
+ """Get the hue, as an angle."""
+
+ return self._hue
+
+ @property
+ def saturation(self) -> float:
+ """Get the saturation, between 0.0 and 1.0."""
+
+ return self._saturation
+
+ @property
+ def value(self) -> float:
+ """Get the value, between 0.0 and 1.0."""
+
+ return self._value
+
+ def assrgb(self) -> 'SRGBColor':
+ """Get an SRGBColor out of the current object."""
+
+ hue, saturation, value = (
+ self.hue.asturns(),
+ self.saturation,
+ self.value,
+ )
+
+ if saturation == 0:
+ r, g, b = value, value, value
+ else:
+ f = hue.turns * 6.0
+ f, i = f - int(f), int(f) % 6
+
+ p = value * (1.0 - saturation)
+ q = value * (1.0 - saturation * f)
+ t = value * (1.0 - saturation * (1.0 - f))
+
+ if i == 0:
+ r, g, b = value, t, p
+ elif i == 1:
+ r, g, b = q, value, p
+ elif i == 2:
+ r, g, b = p, value, t
+ elif i == 3:
+ r, g, b = p, q, value
+ elif i == 4:
+ r, g, b = t, p, value
+ elif i == 5:
+ r, g, b = value, p, q
+
+ return SRGBColor(
+ red=r,
+ green=g,
+ blue=b,
+ alpha=self.alpha,
+ )
+
+ def ashsv(self) -> 'HSVColor':
+ """Get an HSVColor out of the current object."""
+
+ return HSVColor(
+ hue=self.hue,
+ saturation=self.saturation,
+ value=self.value,
+ alpha=self.alpha,
+ )
+
+
+class HWBColor(Color):
+ """A color expressed using its hue, whiteness and blackness components.
+
+ :param hue: Value for :py:attr:`hue`.
+ :param whiteness: Value for :py:attr:`whiteness`.
+ :param blackness: Value for :py:attr:`blackness`.
+ :param alpha: Value for :py:attr:`alpha`.
+ """
+
+ __slots__ = ('_hue', '_whiteness', '_blackness')
+ _params = ('hue', 'whiteness', 'blackness')
+
+ def __init__(
+ self,
+ hue: _Angle,
+ whiteness: float = 0.0,
+ blackness: float = 0.0,
+ alpha: float = 1.0,
+ ):
+ super().__init__(alpha)
+
+ self._hue = hue
+ self._whiteness = whiteness
+ self._blackness = blackness
+
+ def __iter__(self):
+ return iter((self.hue, self.whiteness, self.blackness, self.alpha))
+
+ @property
+ def hue(self) -> _Angle:
+ """Get the hue, as an angle."""
+
+ return self._hue
+
+ @property
+ def whiteness(self) -> float:
+ """Get the whiteness, as a value between 0.0 and 1.0."""
+
+ return self._whiteness
+
+ @property
+ def blackness(self) -> float:
+ """Get the blackness, as a value between 0.0 and 1.0."""
+
+ return self._blackness
+
+ def assrgb(self) -> 'SRGBColor':
+ """Get an SRGBColor out of the current object."""
+
+ hue, w, bl = self.hue, self.whiteness, self.blackness
+
+ color = HSLColor(hue, 1.0, .5).assrgb()
+ r, g, b = color.red, color.green, color.blue
+
+ if w + bl > 1:
+ w, bl = map(lambda x: x / (w + bl), (w, bl))
+
+ r, g, b = map(lambda x: x * (1 - w - bl) + w, (r, g, b))
+
+ return SRGBColor(
+ red=r,
+ green=g,
+ blue=b,
+ alpha=self.alpha,
+ )
+
+ def ashwb(self) -> 'HWBColor':
+ """Get an HWBColor out of the current object."""
+
+ return HWBColor(
+ hue=self.hue,
+ whiteness=self.whiteness,
+ blackness=self.blackness,
+ alpha=self.alpha,
+ )
+
+
+class CMYKColor(Color):
+ """A color expressed using its CMYK channels' intensities.
+
+ :param cyan: Value for :py:attr:`cyan`.
+ :param magenta: Value for :py:attr:`magenta`.
+ :param yellow: Value for :py:attr:`yellow`.
+ :param black: Value for :py:attr:`black`.
+ :param alpha: Value for :py:attr:`alpha`.
+ """
+
+ __slots__ = ('_cyan', '_magenta', '_yellow', '_black')
+ _params = ('cyan', 'magenta', 'yellow', 'black')
+
+ def __init__(
+ self,
+ cyan: float,
+ magenta: float,
+ yellow: float,
+ black: float,
+ alpha: float = 1.0,
+ ):
+ super().__init__(alpha)
+
+ self._cyan = cyan
+ self._magenta = magenta
+ self._yellow = yellow
+ self._black = black
+
+ def __iter__(self):
+ return iter((
+ self.cyan,
+ self.magenta,
+ self.yellow,
+ self.black,
+ self.alpha,
+ ))
+
+ @property
+ def cyan(self):
+ """Get the cyan channel intensity between 0.0 and 1.0."""
+
+ return self._cyan
+
+ @property
+ def magenta(self):
+ """Get the magenta channel intensity between 0.0 and 1.0."""
+
+ return self._magenta
+
+ @property
+ def yellow(self):
+ """Get the yellow channel intensity between 0.0 and 1.0."""
+
+ return self._yellow
+
+ @property
+ def black(self):
+ """Get the black channel intensity between 0.0 and 1.0."""
+
+ return self._black
+
+ def assrgb(self) -> 'SRGBColor':
+ """Get an SRGBColor out of the current object."""
+
+ c, m, y, k = self.cyan, self.magenta, self.yellow, self.black
+
+ r = 1 - min(1, c * (1 - k) + k)
+ g = 1 - min(1, m * (1 - k) + k)
+ b = 1 - min(1, y * (1 - k) + k)
+
+ return SRGBColor(
+ red=r,
+ green=g,
+ blue=b,
+ alpha=self.alpha,
+ )
+
+ def ascmyk(self) -> 'CMYKColor':
+ """Get a CMYKColor out of the current object."""
+
+ return CMYKColor(
+ cyan=self.cyan,
+ magenta=self.magenta,
+ yellow=self.yellow,
+ black=self.black,
+ alpha=self.alpha,
+ )
+
+
+class LABColor(Color):
+ """A color expressed using its CIELAB color space cartesian coordinates.
+
+ :param lightness: Value for :py:attr:`lightness`.
+ :param a: Value for :py:attr:`a`.
+ :param b: Value for :py:attr:`b`.
+ :param alpha: Value for :py:attr:`alpha`.
+ """
+
+ __slots__ = ('_lightness', '_a', '_b')
+ _params = ('lightness', 'a', 'b')
+
+ def __init__(
+ self,
+ lightness: float,
+ a: float,
+ b: float,
+ alpha: float = 1.0,
+ ):
+ super().__init__(alpha)
+
+ self._lightness = lightness
+ self._a = a
+ self._b = b
+
+ def __iter__(self):
+ return iter((self.lightness, self.a, self.b, self.alpha))
+
+ @property
+ def lightness(self) -> float:
+ """Get the CIE lightness.
+
+ Similar to the lightness in the HSL representation.
+ Represented as a float between 0.0 and 1.0.
+ """
+
+ return self._lightness
+
+ @property
+ def a(self) -> float:
+ """Get the A axis value in the Lab colorspace."""
+
+ return self._a
+
+ @property
+ def b(self) -> float:
+ """Get the B axis value in the Lab colorspace."""
+
+ return self._b
+
+ def assrgb(self) -> 'SRGBColor':
+ """Get an SRGBColor out of the current object."""
+
+ return self.asxyz().assrgb()
+
+ def aslab(self) -> 'LABColor':
+ """Get a LABColor out of the current object."""
+
+ return LABColor(
+ lightness=self.lightness,
+ a=self.a,
+ b=self.b,
+ alpha=self.alpha,
+ )
+
+ def aslch(self) -> 'LCHColor':
+ """Get a LCHColor out of the current object."""
+
+ l, a, b = self.lightness, self.a, self.b
+
+ return LCHColor(
+ lightness=l,
+ chroma=_sqrt(a ** 2 + b ** 2),
+ hue=_RadiansAngle(_atan2(b, a)).asprincipal(),
+ alpha=self.alpha,
+ )
+
+
+class LCHColor(Color):
+ """A color expressed using its CIELAB color space polar coordinates.
+
+ :param lightness: Value for :py:attr:`lightness`.
+ :param chroma: Value for :py:attr:`chroma`.
+ :param hue: Value for :py:attr:`hue`.
+ :param alpha: Value for :py:attr:`alpha`.
+ """
+
+ __slots__ = ('_lightness', '_chroma', '_hue', '_alpha')
+ _params = ('lightness', 'chroma', 'hue')
+
+ def __init__(
+ self,
+ lightness: float,
+ chroma: float,
+ hue: _Angle,
+ alpha: float = 1.0,
+ ):
+ super().__init__(alpha)
+
+ try:
+ lightness = float(lightness)
+ except (TypeError, ValueError):
+ raise ValueError(f'lightness should be a float, is {lightness!r}')
+ else:
+ if lightness < 0 or lightness > 1:
+ raise ValueError('lightness should be between 0.0 and 1.0')
+
+ try:
+ chroma = float(chroma)
+ except (TypeError, ValueError):
+ raise ValueError(f'chroma should be a float, is {chroma!r}')
+ else:
+ if chroma < 0:
+ chroma = 0.0
+
+ if not isinstance(hue, _Angle):
+ raise TypeError(f'hue should be an Angle, is {hue!r}')
+
+ self._lightness = lightness
+ self._chroma = chroma
+ self._hue = hue
+
+ def __iter__(self):
+ return iter((self.lightness, self.chroma, self.hue, self.alpha))
+
+ @property
+ def lightness(self) -> float:
+ """Get the CIE lightness.
+
+ Similar to the lightness in the HSL representation.
+ Represented as a float between 0.0 and 1.0.
+ """
+
+ return self._lightness
+
+ @property
+ def chroma(self) -> float:
+ """Get the chroma.
+
+ Represented as a positive number theoretically unbounded.
+ """
+
+ return self._chroma
+
+ @property
+ def hue(self) -> _Angle:
+ """Get the hue, as an angle."""
+
+ return self._hue
+
+ def assrgb(self) -> 'SRGBColor':
+ """Get an SRGBColor out of the current object."""
+
+ return self.aslab().asxyz().assrgb()
+
+ def aslab(self) -> 'LABColor':
+ """Get a LABColor out of the current object."""
+
+ l, c, h = self.lightness, self.chroma, self.hue.asradians()
+
+ return LABColor(
+ lightness=l,
+ a=c * _cos(h.radians),
+ b=c * _sin(h.radians),
+ alpha=self.alpha,
+ )
+
+ def aslch(self) -> 'LCHColor':
+ """Get a LCHColor out of the current object."""
+
+ return LCHColor(
+ lightness=self.lightness,
+ chroma=self.chroma,
+ hue=self.hue,
+ alpha=self.alpha,
+ )
+
+
+class XYZColor(Color):
+ """A color expressed using its CIEXYZ color space coordinates.
+
+ :param x: Value for :py:attr:`x`.
+ :param y: Value for :py:attr:`y`.
+ :param z: Value for :py:attr:`z`.
+ :param alpha: Value for :py:attr:`alpha`.
+ """
+
+ __slots__ = ('_x', '_y', '_z')
+ _params = ('x', 'y', 'z')
+
+ def __init__(self, x: float, y: float, z: float, alpha: float = 1.0):
+ super().__init__(alpha)
+
+ try:
+ x = float(x)
+ except (TypeError, ValueError):
+ raise ValueError(f'x should be a float, is {x!r}')
+ else:
+ if x < 0 or x > 1:
+ raise ValueError('x should be between 0.0 and 1.0')
+
+ try:
+ y = float(y)
+ except (TypeError, ValueError):
+ raise ValueError(f'y should be a float, is {y!r}')
+ else:
+ if y < 0 or y > 1:
+ raise ValueError('y should be between 0.0 and 1.0')
+
+ try:
+ z = float(z)
+ except (TypeError, ValueError):
+ raise ValueError(f'z should be a float, is {z!r}')
+ else:
+ if z < 0 or z > 1:
+ raise ValueError('z should be between 0.0 and 1.0')
+
+ self._x = x
+ self._y = y
+ self._z = z
+
+ def __iter__(self):
+ return iter((self.x, self.y, self.z, self.alpha))
+
+ @property
+ def x(self) -> float:
+ """Get the CIE X component, between 0.0 and 1.0."""
+
+ return self._x
+
+ @property
+ def y(self) -> float:
+ """Get the CIE Y component, between 0.0 and 1.0."""
+
+ return self._y
+
+ @property
+ def z(self) -> float:
+ """Get the CIE Z component, between 0.0 and 1.0."""
+
+ return self._z
+
+ def assrgb(self) -> 'SRGBColor':
+ """Get an SRGBColor out of the current object."""
+
+ # For more information about this algorithm, see these links:
+ #
+ # * http://www.easyrgb.com/en/math.php#text9
+ # (insufficient precision but you get the gist of the algorithm).
+ # * https://stackoverflow.com/a/45238704
+
+ x, y, z = self.x, self.y, self.z
+
+ r = x * 3.2404542 + y * -1.5371385 + z * -.4985314
+ g = x * -.9692660 + y * 1.8760108 + z * .0415560
+ b = x * .0556434 + y * -.2040259 + z * 1.0572252
+
+ r, g, b = map(
+ lambda x: (
+ 1.055 * (x ** (1 / 2.4)) - .055
+ if x > .0031308
+ else 12.92 * x
+ ),
+ (r, g, b),
+ )
+
+ return SRGBColor(
+ red=r,
+ green=g,
+ blue=b,
+ alpha=self.alpha,
+ )
+
+ def aslab(self) -> 'LABColor':
+ """Get a LABColor out of the current object."""
+
+ x, y, z = self.x, self.y, self.z
+
+ # Uses the current industry standard formula, delta E 2000,
+ # on D65/2°.
+ # For more information, see the following links:
+ #
+ # * http://www.easyrgb.com/en/math.php#text9
+ # * https://github.com/cangoektas/xyz-to-lab/blob/master/src/index.js
+
+ D65 = (95.047, 100, 108.883)
+
+ def calculate(data):
+ value, ref = data
+ value = value * 100 / ref
+ if value > .008856:
+ return value ** (1 / 3)
+ return value * 7.878 + 16 / 116
+
+ x, y, z = map(calculate, zip((x, y, z), D65))
+
+ return LABColor(
+ lightness=(116 * y - 16) / 100,
+ a=500 * (x - y),
+ b=200 * (y - z),
+ alpha=self.alpha,
+ )
+
+ def asxyz(self) -> 'XYZColor':
+ """Get an XYZColor out of the current object."""
+
+ return XYZColor(
+ x=self.x,
+ y=self.y,
+ z=self.z,
+ alpha=self.alpha,
+ )
+
+
+class YIQColor(Color):
+ """A color expressed using its YIQ components.
+
+ :param y: Value for :py:attr:`y`.
+ :param i: Value for :py:attr:`i`.
+ :param q: Value for :py:attr:`q`.
+ :param alpha: Value for :py:attr:`alpha`.
+ """
+
+ __slots__ = ('_y', '_i', '_q')
+ _params = ('y', 'i', 'q')
+
+ def __init__(
+ self,
+ y: float,
+ i: float,
+ q: float,
+ alpha: float = 1.0,
+ ):
+ super().__init__(alpha)
+
+ self._y = y
+ self._i = i
+ self._q = q
+
+ def __iter__(self):
+ return iter((self.y, self.i, self.q, self.alpha))
+
+ @property
+ def y(self) -> float:
+ """Get the luma."""
+
+ return self._y
+
+ @property
+ def i(self) -> float:
+ """Get the orange-blue range value."""
+
+ return self._i
+
+ @property
+ def q(self) -> float:
+ """Get the purple-green range value."""
+
+ return self._q
+
+ def assrgb(self) -> 'SRGBColor':
+ """Get an SRGBColor out of the current object."""
+
+ y, i, q = self.y, self.i, self.q
+
+ return SRGBColor(
+ red=max(0.0, min(1.0, (
+ y + .9468822170900693 * i + .6235565819861433 * q
+ ))),
+ green=max(0.0, min(1.0, (
+ y - .27478764629897834 * i - .6356910791873801 * q
+ ))),
+ blue=max(0.0, min(1.0, (
+ y - 1.1085450346420322 * i + 1.7090069284064666 * q
+ ))),
+ alpha=self.alpha,
+ )
+
+ def asyiq(self) -> 'YIQColor':
+ """Get an YIQColor out of the current object."""
+
+ return YIQColor(
+ y=self.y,
+ i=self.i,
+ q=self.q,
+ alpha=self.alpha,
+ )
+
+
+class YUVColor(Color):
+ """A color expressed using its YUV components.
+
+ :param y: Value for :py:attr:`y`.
+ :param u: Value for :py:attr:`u`.
+ :param v: Value for :py:attr:`v`.
+ :param alpha: Value for :py:attr:`alpha`.
+ """
+
+ __slots__ = ('_y', '_u', '_v')
+ _params = ('y', 'u', 'v')
+
+ def __init__(
+ self,
+ y: float,
+ u: float,
+ v: float,
+ alpha: float = 1.0,
+ ):
+ super().__init__(alpha)
+
+ self._y = y
+ self._u = u
+ self._v = v
+
+ def __iter__(self):
+ return iter((self.y, self.u, self.v, self.alpha))
+
+ @property
+ def y(self) -> float:
+ """Get the luma."""
+
+ return self._y
+
+ @property
+ def u(self) -> float:
+ """Get the U chrominance."""
+
+ return self._u
+
+ @property
+ def v(self) -> float:
+ """Get the V chrominance."""
+
+ return self._v
+
+ def asyuv(self) -> 'YUVColor':
+ """Get an YUVColor out of the current object."""
+
+ return YUVColor(
+ y=self.y,
+ u=self.u,
+ v=self.v,
+ alpha=self.alpha,
+ )
+
+# End of file.
diff --git a/thcolor/decoders.py b/thcolor/decoders.py
new file mode 100644
index 0000000..b29f149
--- /dev/null
+++ b/thcolor/decoders.py
@@ -0,0 +1,1240 @@
+#!/usr/bin/env python3
+# *****************************************************************************
+# Copyright (C) 2019-2022 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# This file is part of the thcolor project, which is MIT-licensed.
+# *****************************************************************************
+"""Function and data reference."""
+
+import re as _re
+from abc import ABCMeta as _ABCMeta
+from collections.abc import Mapping as _Mapping
+from enum import Enum as _Enum, auto as _auto
+from inspect import getfullargspec as _getfullargspec
+from itertools import zip_longest as _zip_longest
+from typing import (
+ Any as _Any, List as _List, Optional as _Optional, Sequence as _Sequence,
+ Tuple as _Tuple, Union as _Union,
+)
+
+from .angles import (
+ Angle as _Angle, DegreesAngle as _DegreesAngle,
+ GradiansAngle as _GradiansAngle, RadiansAngle as _RadiansAngle,
+ TurnsAngle as _TurnsAngle,
+)
+from .colors import (
+ Color as _Color, HWBColor as _HWBColor, SRGBColor as _SRGBColor,
+)
+from .errors import ColorExpressionSyntaxError as _ColorExpressionSyntaxError
+from .utils import factor as _factor
+
+__all__ = [
+ 'ColorDecoder', 'MetaColorDecoder',
+ 'alias', 'fallback',
+]
+
+_NO_DEFAULT_VALUE = type('_NO_DEFAULT_VALUE_TYPE', (), {})()
+
+
+def _canonicalkey(x):
+ """Get a canonical key for the decoder mapping."""
+
+ return str(x).replace('-', '_').casefold()
+
+
+def _issubclass(cls, types):
+ """Check if ``cls`` is a subclass of ``types``.
+
+ Until Python 3.10, this function is not aware of the special
+ types available in the standard ``typing`` module; this function
+ aims at fixing this for Python <= 3.9.
+ """
+
+ # Check if is a special case from typing that we know of.
+ try:
+ origin = types.__origin__
+ except AttributeError:
+ if types is _Any:
+ return True
+ else:
+ if origin is _Union or origin is _Optional:
+ return any(_issubclass(cls, type_) for type_ in types.__args__)
+
+ # If ``types`` is any iterable type (tuple, list, you name it),
+ # then we check if the class is a subclass of at least one element
+ # in the iterable.
+ try:
+ types_iter = iter(types)
+ except TypeError:
+ pass
+ else:
+ return any(_issubclass(cls, type_) for type_ in types_iter)
+
+ # We default on the base ``issubclass`` from here.
+ try:
+ return issubclass(cls, types)
+ except TypeError:
+ return False
+
+
+def _isinstance(value, types):
+ """Check if ``cls`` is a subclass of ``types``.
+
+ Until Python 3.10, this function is not aware of the special
+ types available in the standard ``typing`` module; this function
+ aims at fixing this for Python <= 3.9.
+ """
+
+ return _issubclass(type(value), types)
+
+
+def _get_args(func) -> _Sequence[_Tuple[str, _Any, _Any]]:
+ """Get the arguments definition from a function.
+
+ Each argument is returned as a tuple containing:
+
+ * The name of the argument.
+ * The type of the argument.
+ * The default value of the argument;
+ ``_NO_DEFAULT_VALUE`` if the argument has no default value.
+
+ In case an argument is keyword-only with no default
+ value, the function will raise an exception.
+ """
+
+ if isinstance(func, staticmethod):
+ func = func.__func__
+
+ argspec = _getfullargspec(func)
+ try:
+ kwarg = next(
+ arg
+ for arg in argspec.kwonlyargs
+ if arg not in (argspec.kwonlydefaults or ())
+ )
+ except StopIteration:
+ pass
+ else:
+ raise ValueError(
+ f'keyword-only argument {kwarg} has no default value',
+ )
+
+ if argspec.varargs is not None:
+ raise ValueError(
+ 'function has variable arguments, which is unsupported',
+ )
+
+ annotations = getattr(func, '__annotations__', {})
+
+ argnames = argspec.args
+ argtypes = [annotations.get(name) for name in argnames]
+ argdefaultvalues = list(argspec.defaults or ())
+ argdefaultvalues = (
+ [_NO_DEFAULT_VALUE] * (
+ len(argspec.args or ()) - len(argdefaultvalues)
+ )
+ + argdefaultvalues
+ )
+
+ return list(zip(argnames, argtypes, argdefaultvalues))
+
+
+def _make_function(func, name: str, args: _Optional[_Sequence[_Any]]):
+ """Create a function calling another.
+
+ This function will rearrange the given positional
+ arguments with the given argument order ``args``.
+
+ It will also set the default value and annotations of
+ the previous function, rearranged using the given
+ function.
+ """
+
+ # Here, we get the variables for the base function call.
+ # The resulting variables are the following:
+ #
+ # * ``proxy_args``: the proxy arguments as (name, type, defvalue) tuples.
+ # * ``args_index``: a args index -> proxy index correspondance.
+ args_spec = _get_args(func)
+ args_index: _List[_Optional[int]]
+
+ if not args:
+ args_index = list(range(len(args_spec)))
+ proxy_args = args_spec
+ else:
+ args_names = [name for (name, _, _) in args_spec]
+
+ args_index = [None] * len(args_spec)
+ proxy_args = []
+
+ for proxy_index_i, arg in enumerate(args):
+ try:
+ index = args_names.index(arg)
+ except ValueError:
+ raise ValueError(
+ f'{arg!r} is not an argument of the aliased function',
+ )
+
+ args_index[index] = proxy_index_i
+ proxy_args.append(args_spec[index])
+
+ # We want to check that no value without a default value was
+ # left unspecified.
+ for (aname, _, adefvalue), proxy_index in zip(args_spec, args_index):
+ if proxy_index is None and adefvalue is _NO_DEFAULT_VALUE:
+ raise ValueError(
+ f'{aname!r} is left without value in the aliased function',
+ )
+
+ # Produce the function code.
+ locals_ = {'__func': func}
+
+ def _iter_proxy_args(d, args):
+ # First, obtain the index of the last optional argument from the
+ # end, because even arguments with default values before
+ # mandatory arguments cannot have a default value exposed in Python.
+ #
+ # Then, yield the arguments.
+
+ try:
+ mandatory_until = max(
+ index for index, (_, _, adefvalue) in enumerate(args)
+ if adefvalue is _NO_DEFAULT_VALUE
+ )
+ except ValueError:
+ mandatory_until = -1
+
+ for index, (aname, atype, adefvalue) in enumerate(args):
+ atypename = None
+ adefvaluename = None
+
+ if atype is not None:
+ atypename = f'__type{index}'
+ d[atypename] = atype
+
+ if index > mandatory_until and adefvalue is not _NO_DEFAULT_VALUE:
+ adefvaluename = f'__value{index}'
+ d[adefvaluename] = adefvalue
+
+ yield f'{aname}' + (
+ (
+ f': {atypename} = {adefvaluename}',
+ f': {atypename}',
+ )[adefvaluename is None],
+ (f'={adefvaluename}', '')[adefvaluename is None],
+ )[atypename is None]
+
+ def _iter_final_args(d, args_spec, arg_indexes):
+ for index, ((aname, _, adefvalue), p_index) in enumerate(
+ zip(args_spec, arg_indexes),
+ ):
+ if p_index is None:
+ defvaluename = f'__defvalue{index}'
+ d[defvaluename] = adefvalue
+ yield defvaluename
+ else:
+ yield aname
+
+ proxy_args_s = ', '.join(_iter_proxy_args(locals_, proxy_args))
+ final_args_s = ', '.join(_iter_final_args(locals_, args_spec, args_index))
+ keys = ', '.join(locals_.keys())
+
+ code = (
+ f'def __define_alias({keys}):\n'
+ f' def {name}({proxy_args_s}):\n'
+ f' return __func({final_args_s})\n'
+ f' return {name}\n'
+ '\n'
+ f'__func = __define_alias({keys})\n'
+ )
+
+ exec(code, {}, locals_) # noqa: S102
+ func = locals_['__func']
+
+ return func
+
+
+def _ncol_func(
+ angle: _Angle,
+ whiteness: _Union[int, float] = 0,
+ blackness: _Union[int, float] = 0,
+):
+ return _HWBColor(
+ hue=angle,
+ whiteness=_factor(whiteness),
+ blackness=_factor(blackness),
+ )
+
+
+# ---
+# Lexer.
+# ---
+
+
+class _ColorExpressionTokenType(_Enum):
+ """Token type."""
+
+ NAME = _auto()
+ ANGLE = _auto()
+ PERCENTAGE = _auto()
+ INTEGER = _auto()
+ FLOAT = _auto()
+ NCOL = _auto()
+ HEX = _auto()
+ EMPTY = _auto()
+ CALL_START = _auto()
+ CALL_END = _auto()
+
+ # Not generated by the lexer.
+ CALL = _auto()
+
+
+class _ColorExpressionToken:
+ """A token as expressed by the color expression lexer."""
+
+ __slots__ = ('_type', '_name', '_value', '_column', '_rawtext')
+
+ TYPE_NAME = _ColorExpressionTokenType.NAME
+ TYPE_ANGLE = _ColorExpressionTokenType.ANGLE
+ TYPE_PERCENTAGE = _ColorExpressionTokenType.PERCENTAGE
+ TYPE_INTEGER = _ColorExpressionTokenType.INTEGER
+ TYPE_FLOAT = _ColorExpressionTokenType.FLOAT
+ TYPE_NCOL = _ColorExpressionTokenType.NCOL
+ TYPE_HEX = _ColorExpressionTokenType.HEX
+ TYPE_EMPTY = _ColorExpressionTokenType.EMPTY
+ TYPE_CALL_START = _ColorExpressionTokenType.CALL_START
+ TYPE_CALL_END = _ColorExpressionTokenType.CALL_END
+
+ # Not generated by the lexer.
+ TYPE_CALL = _ColorExpressionTokenType.CALL
+
+ def __init__(
+ self,
+ type_: _ColorExpressionTokenType,
+ name=None,
+ value=None,
+ column=None,
+ rawtext=None,
+ ):
+ self._type = type_
+ self._name = name
+ self._value = value
+ self._column = column
+ self._rawtext = rawtext
+
+ def __repr__(self):
+ args = [f'type_=TYPE_{self._type.name}']
+ if self._name is not None:
+ args.append(f'name={self._name!r}')
+ if self._value is not None:
+ args.append(f'value={self._value!r}')
+ if self._column is not None:
+ args.append(f'column={self._column!r}')
+ if self._rawtext is not None:
+ args.append(f'rawtext={self._rawtext!r}')
+
+ return f'{self.__class__.__name__}({", ".join(args)})'
+
+ @property
+ def type_(self):
+ """Token type."""
+
+ return self._type
+
+ @property
+ def name(self):
+ """Token name."""
+
+ return self._name
+
+ @property
+ def value(self):
+ """Token value."""
+
+ return self._value
+
+ @property
+ def column(self):
+ """Column at which the token is located."""
+
+ return self._column
+
+ @property
+ def rawtext(self):
+ """Raw text for decoding."""
+
+ return self._rawtext
+
+
+_colorexpressionpattern = _re.compile(
+ r"""
+ \s*
+ (?P<arg>
+ (
+ (?P<agl>
+ (?P<agl_sign> [+-]?)
+ (?P<agl_val> ([0-9]+(\.[0-9]*)?|[0-9]*\.[0-9]+)) \s*
+ (?P<agl_typ>deg|grad|rad|turns?)
+ )
+ | (?P<per>
+ (?P<per_val>[+-]? [0-9]+(\.[0-9]*)? | [+-]? \.[0-9]+)
+ \s* \%
+ )
+ | (?P<int_val>[+-]? [0-9]+)
+ | (?P<flt_val>[+-]? [0-9]+\.[0-9]* | [+-]? \.[0-9]+)
+ | (?P<ncol>[RYGCBM] [0-9]{0,2} (\.[0-9]*)?)
+ | (
+ \# (?P<hex>
+ [0-9a-f]{3} | [0-9a-f]{4}
+ | [0-9a-f]{6} | [0-9a-f]{8}
+ )
+ )
+ | (?P<name> [a-z0-9_-]+)
+ |
+ ) \s* (?P<sep>,|/|\s|\(|\)|$)
+ )
+ \s*
+ """,
+ _re.VERBOSE | _re.I | _re.M,
+)
+
+
+def _get_color_tokens(string: str, extended_hex: bool = False):
+ """Get color tokens.
+
+ :param string: The string to get the color tokens from.
+ :param extended_hex: Whether 4 or 8-digit hex colors are
+ allowed (``True``) or not (``False``).
+ """
+
+ start: int = 0
+ was_call_end: bool = False
+
+ while string:
+ match = _colorexpressionpattern.match(string)
+ if match is None:
+ s = f'{string[:17]}...' if len(string) > 20 else string
+ raise _ColorExpressionSyntaxError(
+ f'syntax error near {s!r}',
+ column=start,
+ )
+
+ result = match.groupdict()
+ column = start + match.start('arg')
+
+ is_call_end = False
+
+ if result['name']:
+ yield _ColorExpressionToken(
+ _ColorExpressionToken.TYPE_NAME,
+ value=result['name'],
+ column=column,
+ )
+ elif result['agl'] is not None:
+ value = float(result['agl_sign'] + result['agl_val'])
+ typ = result['agl_typ']
+
+ ang: _Angle
+ if typ == 'deg':
+ ang = _DegreesAngle(value)
+ elif typ == 'rad':
+ ang = _RadiansAngle(value)
+ elif typ == 'grad':
+ ang = _GradiansAngle(value)
+ elif typ in ('turn', 'turns'):
+ ang = _TurnsAngle(value)
+ else:
+ raise NotImplementedError
+
+ yield _ColorExpressionToken(
+ _ColorExpressionToken.TYPE_ANGLE,
+ value=ang,
+ column=column,
+ rawtext=result['agl'],
+ )
+ elif result['per'] is not None:
+ yield _ColorExpressionToken(
+ _ColorExpressionToken.TYPE_PERCENTAGE,
+ value=float(result['per_val']) / 100,
+ column=column,
+ rawtext=result['per'],
+ )
+ elif result['int_val'] is not None:
+ yield _ColorExpressionToken(
+ _ColorExpressionToken.TYPE_INTEGER,
+ value=int(result['int_val']),
+ column=column,
+ rawtext=result['int_val'],
+ )
+ elif result['flt_val'] is not None:
+ yield _ColorExpressionToken(
+ _ColorExpressionToken.TYPE_FLOAT,
+ value=float(result['flt_val']),
+ column=column,
+ rawtext=result['flt_val'],
+ )
+ elif result['ncol'] is not None:
+ letter = result['ncol'][0]
+ number = float(result['ncol'][1:])
+
+ if number < 0 or number >= 100:
+ yield _ColorExpressionToken(
+ _ColorExpressionToken.TYPE_NAME,
+ value=result['ncol'],
+ column=column,
+ )
+ else:
+ yield _ColorExpressionToken(
+ _ColorExpressionToken.TYPE_NCOL,
+ value=_DegreesAngle(
+ 'RYGCBM'.find(letter) * 60 + number / 100 * 60,
+ ),
+ column=column,
+ rawtext=result['ncol'],
+ )
+ elif result['hex'] is not None:
+ value_s = result['hex']
+ if len(value_s) in (4, 8) and not extended_hex:
+ raise _ColorExpressionSyntaxError(
+ 'extended hex values are forbidden: '
+ f'{"#" + value_s!r}',
+ column=start,
+ )
+
+ if len(value_s) <= 4:
+ value_s = ''.join(map(lambda x: x + x, value_s))
+
+ r = int(value_s[0:2], 16)
+ g = int(value_s[2:4], 16)
+ b = int(value_s[4:6], 16)
+ a = int(value_s[6:8], 16) / 255.0 if len(value_s) == 8 else 1.0
+
+ yield _ColorExpressionToken(
+ _ColorExpressionToken.TYPE_HEX,
+ value=_SRGBColor.frombytes(r, g, b, a),
+ column=column,
+ rawtext='#' + result['hex'],
+ )
+ else:
+ # ``was_call_end`` hack: take ``func(a, b) / c`` for example.
+ # By default, it is tokenized as the following:
+ #
+ # * name 'func'
+ # * call start
+ # * name 'a'
+ # * name 'b'
+ # * call end
+ # * empty arg [1]
+ # * name 'c'
+ #
+ # The empty arg at [1] is tokenized since an argument could
+ # directly be after the right parenthesis. However, we do not
+ # want this to be considered an empty arg, so we ignore empty
+ # args right after call ends.
+ #
+ # Note that ``func(a, b) / / c`` will still contain an empty
+ # argument before name 'c'.
+ if not was_call_end:
+ yield _ColorExpressionToken(
+ _ColorExpressionToken.TYPE_EMPTY,
+ column=column,
+ rawtext='',
+ )
+
+ sep = result['sep']
+ column = start + match.start('sep')
+
+ if sep == '(':
+ yield _ColorExpressionToken(
+ _ColorExpressionToken.TYPE_CALL_START,
+ column=column,
+ rawtext=sep,
+ )
+ elif sep == ')':
+ is_call_end = True
+ yield _ColorExpressionToken(
+ _ColorExpressionToken.TYPE_CALL_END,
+ column=column,
+ rawtext=sep,
+ )
+
+ start += match.end()
+ string = string[match.end():]
+ was_call_end = is_call_end
+
+
+# ---
+# Base decoder classes.
+# ---
+
+def fallback(value: _Union[_Color, _Angle, int, float]):
+ """Set a fallback value on a function, if used as a variable.
+
+ When a function is used as a symbol instead of a function,
+ by default, it yields that it is callable and should be
+ accompanied with arguments.
+
+ Using this decorator on a function makes it to be evaluated
+ as a color in this case.
+ """
+
+ if not isinstance(value, (_Color, _Angle, int, float)):
+ raise ValueError(
+ 'fallback value should be a color, an angle or a number, '
+ f'is {value!r}',
+ )
+
+ def decorator(func):
+ func.__fallback_value__ = value
+ return func
+
+ return decorator
+
+
+class alias:
+ """Define an alias for a function."""
+
+ __slots__ = ('_name', '_args')
+
+ def __init__(self, name: str, args: _Sequence[str] = ()):
+ self._name = name
+ self._args = tuple(args)
+
+ if len(self._args) != len(set(self._args)):
+ raise ValueError('arguments should be unique')
+ if any(not x or not isinstance(x, str) for x in self._args):
+ raise ValueError('all arguments should be non-empty strings')
+
+ @property
+ def name(self):
+ """Get the name of the function the current alias targets."""
+
+ return self._name
+
+ @property
+ def args(self) -> _Sequence[str]:
+ """Get the arguments' order of the alias."""
+
+ return self._args
+
+
+class ColorDecoder(_Mapping):
+ """Base color decoder.
+
+ This color decoder behaves as a mapping returning syntax elements,
+ with the additional properties controlling its behaviour.
+
+ The properties defined at class definition time are the following:
+
+ ``__mapping__``
+ Defines the base mapping that is copied at the
+ instanciation of each class.
+
+ ``__ncol_support__``
+ Defines whether natural colors (NCol) are
+ supported while decoding or not.
+
+ ``__extended_hex_support__``
+ Defines whether 4 or 8-digit
+ hexadecimal colors (starting with a '#') are allowed or not.
+
+ ``__defaults_to_netscape_color``
+ Defines whether color decoding
+ defaults to Netscape color parsing or not.
+
+ These properties cannot be changed at runtime, although they might
+ be in a future version of thcolor.
+ """
+
+ __slots__ = ('_mapping',)
+ __mapping__: _Mapping = {}
+ __ncol_support__: bool = False
+ __extended_hex_support__: bool = False
+ __defaults_to_netscape_color__: bool = False
+
+ def __init__(self):
+ cls = self.__class__
+
+ mapping = {}
+ for key, value in cls.__mapping__.items():
+ mapping[_canonicalkey(key)] = value
+
+ self._mapping = mapping
+ self._ncol_support = bool(cls.__ncol_support__)
+ self._extended_hex_support = bool(cls.__extended_hex_support__)
+ self._defaults_to_netscape_color = bool(
+ cls.__defaults_to_netscape_color__,
+ )
+
+ def __getattr__(self, key):
+ try:
+ return self[key]
+ except KeyError:
+ raise AttributeError
+
+ def __getitem__(self, key):
+ return self._mapping[_canonicalkey(key)]
+
+ def __iter__(self):
+ return iter(self._mapping)
+
+ def __len__(self):
+ return len(self._mapping)
+
+ def __repr__(self):
+ return f'{self.__class__.__name__}()'
+
+ def decode(
+ self,
+ expr: str,
+ prefer_colors: bool = False,
+ prefer_angles: bool = False,
+ ) -> _Sequence[_Optional[_Union[_Color, _Angle, int, float]]]:
+ """Decode a color expression.
+
+ When top-level result(s) are not colors and colors are
+ actually expected if possible to obtain, the caller should
+ set ``prefer_colors`` to ``True`` in order for top-level
+ conversions to take place.
+
+ Otherwise, when top-level result(s) are not angles and angles
+ are actually expected if possible to obtain, the caller should
+ set ``prefer_angles`` to ``True`` in order for top-level
+ conversions to take place.
+ """
+
+ global _color_pattern
+
+ ncol_support = self._ncol_support
+ extended_hex_support = self._extended_hex_support
+ defaults_to_netscape = self._defaults_to_netscape_color
+
+ # Parsing stage; the results will be in ``current``.
+ #
+ # * ``stack``: the stack of current values.
+ # * ``func_stack``: the function name or special type, as tokens.
+ # Accepted token types here are ``NAME`` and ``NCOL``.
+ # * ``implicit_stack``: when the current function is an implicit
+ # one, this stack contains the arguments left to read in this
+ # implicit function.
+ # * ``current``: the current list of elements, which is put on
+ # top of the stack when a call is started and is popped out of
+ # the top of the stack when a call is ended.
+ stack: _List[_List[_ColorExpressionToken]] = []
+ func_stack: _List[_ColorExpressionToken] = []
+ implicit_stack: _List[int] = []
+ current: _List[_ColorExpressionToken] = []
+
+ def _get_parent_func():
+ """Get the parent function name for exceptions."""
+
+ if not func_stack:
+ return None
+ if func_stack[0].type_ == _ColorExpressionToken.TYPE_NCOL:
+ return '<implicit ncol function>'
+ return func_stack[0].value
+
+ token_iter = _get_color_tokens(
+ expr,
+ extended_hex=extended_hex_support,
+ )
+ for token in token_iter:
+ if (
+ token.type_ == _ColorExpressionToken.TYPE_NCOL
+ and ncol_support
+ ):
+ func_stack.insert(0, token)
+ implicit_stack.insert(0, 3)
+ stack.insert(0, current)
+ current = []
+ elif token.type_ == _ColorExpressionToken.TYPE_NCOL:
+ current.append(_ColorExpressionToken(
+ _ColorExpressionToken.TYPE_NAME,
+ value=token.rawtext,
+ rawtext=token.rawtext,
+ column=token.column,
+ ))
+ elif token.type_ in (
+ _ColorExpressionToken.TYPE_NAME,
+ _ColorExpressionToken.TYPE_ANGLE,
+ _ColorExpressionToken.TYPE_PERCENTAGE,
+ _ColorExpressionToken.TYPE_INTEGER,
+ _ColorExpressionToken.TYPE_FLOAT,
+ _ColorExpressionToken.TYPE_HEX,
+ _ColorExpressionToken.TYPE_EMPTY,
+ ):
+ # Current token is a value, we add it.
+ current.append(token)
+ elif token.type_ == _ColorExpressionToken.TYPE_CALL_START:
+ if func_stack and (
+ func_stack[0].type_ == _ColorExpressionToken.TYPE_NCOL
+ ):
+ implicit_stack[0] += 1
+
+ name_token = current.pop(-1)
+ if name_token.type_ != _ColorExpressionToken.TYPE_NAME:
+ raise _ColorExpressionSyntaxError(
+ 'expected the name of the function to call, '
+ f'got a {name_token.type_.name}',
+ column=name_token.column,
+ func=_get_parent_func(),
+ )
+
+ func_stack.insert(0, name_token)
+ stack.insert(0, current)
+ current = []
+ elif token.type_ == _ColorExpressionToken.TYPE_CALL_END:
+ # First, pop out all of the implicit functions.
+
+ while func_stack:
+ try:
+ name_token = func_stack.pop(0)
+ except IndexError:
+ raise _ColorExpressionSyntaxError(
+ 'extraneous closing parenthesis',
+ column=token.column,
+ func=_get_parent_func(),
+ ) from None
+
+ if name_token.type_ == _ColorExpressionToken.TYPE_NAME:
+ break
+
+ # We pop the function out of the stack with the
+ # arguments we have.
+ implicit_stack.pop(0)
+ old_current = stack.pop(0)
+ old_current.append(_ColorExpressionToken(
+ type_=_ColorExpressionToken.TYPE_CALL,
+ column=name_token.column,
+ name=_ncol_func,
+ value=(name_token, *current),
+ ))
+ current = old_current
+
+ # We have a function name and it is not implicit.
+ old_current = stack.pop(0)
+ old_current.append(_ColorExpressionToken(
+ _ColorExpressionTokenType.CALL,
+ name=name_token.value,
+ value=current,
+ column=name_token.column,
+ ))
+ current = old_current
+ else:
+ raise NotImplementedError(
+ f'unknown token type: {token.type_!r}',
+ )
+
+ while func_stack and (
+ func_stack[0].type_ == _ColorExpressionToken.TYPE_NCOL
+ ):
+ implicit_stack[0] -= 1
+ if implicit_stack[0] > 0:
+ break
+
+ ncol = func_stack.pop(0)
+ implicit_stack.pop(0)
+
+ # We have the required number of tokens for this
+ # to work.
+ old_current = stack.pop(0)
+ old_current.append(_ColorExpressionToken(
+ type_=_ColorExpressionToken.TYPE_CALL,
+ column=ncol.column,
+ name=_ncol_func,
+ value=(ncol, *current),
+ ))
+ current = old_current
+
+ # We want to pop out all implicit functions we have.
+ # If we still have an explicit function in the function stack,
+ # that means a parenthesis was omitted.
+ while func_stack:
+ try:
+ name_token = func_stack.pop(0)
+ except IndexError:
+ break
+
+ if name_token.type_ == _ColorExpressionToken.TYPE_NAME:
+ raise _ColorExpressionSyntaxError(
+ 'missing closing parenthesis',
+ column=len(expr),
+ func=_get_parent_func(),
+ )
+
+ # We pop the function out of the stack with the
+ # arguments we have.
+ implicit_stack.pop(0)
+ old_current = stack.pop(0)
+ old_current.append(_ColorExpressionToken(
+ type_=_ColorExpressionToken.TYPE_CALL,
+ column=name_token.column,
+ name=_ncol_func,
+ value=(name_token, *current),
+ ))
+ current = old_current
+
+ # Evaluating stage.
+ def evaluate(element, parent_func=None):
+ """Evaluate the element in the current context.
+
+ Always returns a tuple where:
+
+ * The first element is the real answer.
+ * The second element is the string which can be used
+ in the case of a color fallback (when Netscape color
+ defaulting is on).
+ """
+
+ if element.type_ == _ColorExpressionTokenType.NAME:
+ try:
+ data = self[element.value]
+ except KeyError:
+ if defaults_to_netscape:
+ return (
+ _SRGBColor.fromnetscapecolorname(element.value),
+ element.value,
+ )
+
+ raise _ColorExpressionSyntaxError(
+ f'unknown value {element.value!r}',
+ column=element.column,
+ func=parent_func,
+ )
+ else:
+ if not isinstance(data, (_Color, _Angle, int, float)):
+ try:
+ fallback_value = data.__fallback_value__
+ except AttributeError:
+ raise _ColorExpressionSyntaxError(
+ f'{element.value!r} is not a value and '
+ f'has no fallback value (is {data!r})',
+ column=element.column,
+ func=parent_func,
+ )
+
+ if not isinstance(fallback_value, (
+ _Color, _Angle, int, float,
+ )):
+ raise _ColorExpressionSyntaxError(
+ f'{element.value!r} fallback value '
+ f'{fallback_value!r} is not a color, '
+ 'an angle or a number.',
+ column=element.column,
+ func=parent_func,
+ )
+
+ data = fallback_value
+
+ return (data, element.value)
+ elif element.type_ != _ColorExpressionTokenType.CALL:
+ return (element.value, element.rawtext)
+
+ func_name = element.name
+ if callable(func_name):
+ func = func_name
+ func_name = '<implicit ncol function>'
+ else:
+ try:
+ func = self[func_name]
+ except KeyError:
+ raise _ColorExpressionSyntaxError(
+ f'function {func_name!r} not found',
+ column=element.column,
+ func=parent_func,
+ )
+
+ # Check the function.
+ try:
+ args_spec = _get_args(func)
+ except ValueError as exc:
+ raise _ColorExpressionSyntaxError(
+ f'function {func_name!r} is unsuitable for '
+ f'calling: {str(exc)}',
+ column=element,
+ func=parent_func,
+ )
+
+ # Check the argument count.
+ #
+ # We remove empty arguments at the end of calls so that
+ # even calls such as ``func(a, b, , , , , ,)`` will only
+ # have arguments 'a' and 'b'.
+ unevaluated_args = element.value
+ while (
+ unevaluated_args and unevaluated_args[-1].type_
+ == _ColorExpressionToken.TYPE_EMPTY
+ ):
+ unevaluated_args.pop(-1)
+
+ if len(unevaluated_args) > len(args_spec):
+ raise _ColorExpressionSyntaxError(
+ f'too many arguments for {func_name!r}: '
+ f'expected {len(args_spec)}, got {len(unevaluated_args)}',
+ column=element.column,
+ func=parent_func,
+ )
+
+ # Now we should evaluate subtokens and check the types of
+ # the function.
+ new_args = []
+
+ for token, (argname, argtype, argdefvalue) in _zip_longest(
+ unevaluated_args, args_spec, fillvalue=None,
+ ):
+ arg, fallback_string, column = None, None, element.column
+ if token is not None:
+ column = token.column
+ arg, *_, fallback_string = evaluate(token)
+
+ arg = arg if arg is not None else argdefvalue
+ if arg is _NO_DEFAULT_VALUE:
+ raise _ColorExpressionSyntaxError(
+ f'expected a value for {argname!r}',
+ column=column,
+ func=func_name,
+ )
+
+ if argtype is None or _isinstance(arg, argtype):
+ pass
+ elif (
+ _issubclass(_Color, argtype) and defaults_to_netscape
+ and fallback_string is not None
+ ):
+ arg = _SRGBColor.fromnetscapecolorname(fallback_string)
+ elif (
+ _issubclass(_Angle, argtype)
+ and isinstance(arg, (int, float))
+ ):
+ arg = _DegreesAngle(arg)
+ elif (
+ _issubclass(float, argtype)
+ and isinstance(arg, int)
+ ):
+ arg = float(arg)
+ elif (
+ _issubclass(int, argtype)
+ and isinstance(arg, float)
+ and arg == int(arg)
+ ):
+ arg = int(arg)
+ else:
+ raise _ColorExpressionSyntaxError(
+ f'{arg!r} did not match expected type {argtype!r}'
+ f' for argument {argname!r}',
+ column=column,
+ func=func_name,
+ )
+
+ new_args.append(arg)
+
+ # Get the result.
+ result = func(*new_args)
+ if result is None:
+ raise _ColorExpressionSyntaxError(
+ f'function {func_name!r} returned an empty '
+ 'result for the following arguments: '
+ f'{", ".join(map(repr, new_args))}.',
+ column=token.column,
+ func=parent_func,
+ )
+
+ return result, None
+
+ return tuple(
+ _SRGBColor.fromnetscapecolorname(fallback_string)
+ if (
+ not isinstance(result, _Color)
+ and prefer_colors
+ and defaults_to_netscape
+ and fallback_string is not None
+ )
+ else _DegreesAngle(result)
+ if (
+ isinstance(result, (int, float))
+ and prefer_angles
+ )
+ else result
+ for result, *_, fallback_string in map(evaluate, current)
+ )
+
+
+# ---
+# Meta base type.
+# ---
+
+
+class _MetaColorDecoderType(_ABCMeta):
+ """The decoder type."""
+
+ def __new__(mcls, clsname, superclasses, attributedict):
+ elements = {}
+ options = {
+ '__ncol_support__': False,
+ '__extended_hex_support__': False,
+ '__defaults_to_netscape_color__': False,
+ }
+
+ # Explore the parents.
+ for supercls in reversed(superclasses):
+ if not issubclass(supercls, ColorDecoder):
+ continue
+
+ for option in options:
+ if getattr(supercls, option, False):
+ options[option] = True
+
+ elements.update(supercls.__mapping__)
+
+ for option, value in options.items():
+ options[option] = attributedict.get(option, value)
+
+ # Instanciate the class.
+ clsattrs = {
+ '__doc__': attributedict.get('__doc__', None),
+ '__mapping__': elements,
+ }
+ clsattrs.update(options)
+
+ cls = super().__new__(mcls, clsname, superclasses, clsattrs)
+ del clsattrs
+
+ # Get the elements and aliases from the current attribute dictionary.
+ aliases = {}
+ childelements = {}
+ for key, value in attributedict.items():
+ if key.startswith('_') or key.endswith('_'):
+ continue
+
+ key = _canonicalkey(key)
+
+ if isinstance(value, alias):
+ aliases[key] = value
+ continue
+ elif callable(value):
+ pass
+ elif isinstance(value, (int, float, _Color, _Angle)):
+ value = value
+ else:
+ raise TypeError(
+ f'{clsname} property {key!r} is neither an alias, '
+ 'a function, a number, a color or an angle',
+ )
+
+ childelements[key] = value
+
+ # Resolve the aliases.
+ #
+ # Because of dependencies, we do the basic solution for that:
+ # We try to resolve aliases as we can, ignoring the ones for which
+ # the aliases are still awaiting. If we could not resolve any
+ # of the aliases in one round and there still are some left,
+ # there must be a cyclic dependency somewhere.
+ #
+ # This is not optimized. However, given the cases we have,
+ # it'll be enough.
+ aliaselements = {}
+ while aliases:
+ resolved = len(aliases)
+ nextaliases = {}
+
+ for key, alias_value in aliases.items():
+ funcname = _canonicalkey(alias_value.name)
+
+ # Check if the current alias still depends on an alias
+ # that hasn't been resolved yet; in that case, ignore
+ # it for now.
+ if funcname in aliases:
+ resolved -= 1
+ nextaliases[key] = alias_value
+ continue
+
+ # Check the function name and arguments.
+ try:
+ func = childelements[funcname]
+ except KeyError:
+ try:
+ func = elements[funcname]
+ except KeyError:
+ raise ValueError(
+ f'{clsname} property {key!r} references '
+ f'undefined function {funcname!r}',
+ ) from None
+
+ if not callable(func):
+ raise ValueError(
+ f'{clsname} property {key!r} '
+ f'references non-function {funcname!r}',
+ )
+
+ aliaselements[_canonicalkey(key)] = _make_function(
+ func, name=key.replace('-', '_'), args=alias_value.args,
+ )
+
+ aliases = nextaliases
+ if aliases and not resolved:
+ raise ValueError(
+ f'could not resolve left aliases in class {clsname}, '
+ 'there might be a cyclic dependency between these '
+ 'aliases: ' + ', '.join(aliases.keys()) + '.',
+ )
+
+ # Add all of the elements in order of importance.
+ elements.update(aliaselements)
+ elements.update(childelements)
+
+ # Check the types of the annotations, just in case.
+ for key, element in elements.items():
+ try:
+ args_spec = _get_args(element)
+ except TypeError:
+ continue
+
+ for argname, argtype, argdefvalue in args_spec:
+ if argtype is None:
+ continue
+
+ if all(
+ not _issubclass(type_, argtype)
+ for type_ in (int, float, _Color, _Angle, type(None))
+ ):
+ raise TypeError(
+ f'function {key!r}, argument {argname!r}: '
+ 'none of the types handled by the decoder are '
+ f'valid subclasses of type hint {argtype}',
+ )
+
+ if argdefvalue is _NO_DEFAULT_VALUE:
+ continue
+
+ if (
+ not _isinstance(argdefvalue, argtype)
+ and not (
+ isinstance(argdefvalue, int)
+ and _issubclass(float, argtype)
+ )
+ and not (
+ isinstance(argdefvalue, float)
+ and int(argdefvalue) == argdefvalue
+ and _issubclass(int, argtype)
+ )
+ and not (
+ isinstance(argdefvalue, (int, float))
+ and _issubclass(_Angle, argtype)
+ )
+ ):
+ raise TypeError(
+ f'function {key!r}, argument {argname!r}: '
+ f'default value {argdefvalue!r} is not a valid '
+ f'instance of type hint {argtype!r}',
+ )
+
+ return cls
+
+
+class MetaColorDecoder(ColorDecoder, metaclass=_MetaColorDecoderType):
+ """Base meta color decoder, which gets the function and things."""
+
+ pass
+
+
+# End of file.
diff --git a/thcolor/errors.py b/thcolor/errors.py
new file mode 100755
index 0000000..5c6e0f4
--- /dev/null
+++ b/thcolor/errors.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+# *****************************************************************************
+# Copyright (C) 2019-2022 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# This file is part of the thcolor project, which is MIT-licensed.
+# *****************************************************************************
+"""Exception definitions."""
+
+__all__ = ['ColorExpressionSyntaxError']
+
+
+class ColorExpressionSyntaxError(Exception):
+ """An error has occurred while decoding a color expression.
+
+ Such an error can happen during parsing or evaluating.
+ """
+
+ def __init__(self, text, column=None, func=None):
+ self._column = column if column is not None and column >= 0 else None
+ self._func = str(func) if func else None
+ self._text = str(text) if text else ''
+
+ def __str__(self):
+ msg = ''
+
+ if self._column is not None:
+ msg += f'at column {self._column}'
+ if self._func is not None:
+ msg += ', '
+ if self._func is not None:
+ msg += f'for function {self._func!r}'
+ if msg:
+ msg += ': '
+
+ return msg + self._text
+
+ @property
+ def text(self):
+ """Exception message, usually linked to the context."""
+
+ return self._text
+
+ @property
+ def column(self):
+ """Column of the expression at which the exception has occurred.
+
+ ``None`` if the error has occurred on an unknown column or on
+ the whole exception.
+ """
+
+ return self._column
+
+ @property
+ def func(self):
+ """Name of the function we were calling when the error occurred.
+
+ Either on arguments decoding or erroneous argument type or value.
+ Is ``None`` if the context is unknown or the error hasn't
+ occurred while calling a function or decoding its
+ arguments.
+ """
+
+ return self._func
+
+
+# End of file.
diff --git a/thcolor/utils.py b/thcolor/utils.py
new file mode 100644
index 0000000..337c794
--- /dev/null
+++ b/thcolor/utils.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+# *****************************************************************************
+# Copyright (C) 2022 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# This file is part of the thcolor project, which is MIT-licensed.
+# *****************************************************************************
+"""Utilities for the thcolor module."""
+
+from typing import Optional as _Optional
+
+__all__ = ['factor', 'round_half_up']
+
+
+def factor(x, max_: int = 100, clip: bool = False):
+ """Return a factor based on if something is a float or an int."""
+
+ if isinstance(x, float):
+ pass
+ elif x in (0, 1) and max_ == 100:
+ x = float(x)
+ else:
+ x /= max_
+
+ if clip:
+ x = max(0, min(1, x))
+
+ return x
+
+
+def round_half_up(number: float, ndigits: _Optional[int] = None) -> float:
+ """Round a number to the nearest integer.
+
+ This function exists because Python's built-in ``round`` function
+ uses half-to-even rounding, also called "Banker's rounding".
+ This means that 1.5 is rounded to 2 and 2.5 is also rounded to 2.
+
+ What we want is a half-to-up rounding, so we have this function.
+ """
+
+ if ndigits is None:
+ ndigits = 0
+
+ base = 10 ** -ndigits
+
+ result = round(
+ (number // base) * base + (
+ base if (number % base) >= (base / 2) else 0
+ ),
+ ndigits=ndigits,
+ )
+
+ if result == int(result):
+ result = int(result)
+ return result
+
+
+# End of file.
diff --git a/thcolor/version.py b/thcolor/version.py
new file mode 100755
index 0000000..886c512
--- /dev/null
+++ b/thcolor/version.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python3
+# *****************************************************************************
+# Copyright (C) 2021-2022 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# This file is part of the thcolor project, which is MIT-licensed.
+# *****************************************************************************
+"""Version definition for the thcolor module."""
+
+__all__ = ['version']
+
+version = '0.4'
+
+# End of file.