chromatic-python 0.6.1__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chromatic_python-0.7.0/.github/workflows/release.yml +18 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/PKG-INFO +5 -2
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/__init__.pyi +0 -5
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/__main__.py +53 -16
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/_typing.py +7 -13
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/_version.py +3 -3
- chromatic_python-0.7.0/chromatic/color/colorconv.py +358 -0
- chromatic_python-0.7.0/chromatic/color/colorconv.pyi +176 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/color/core.py +605 -395
- chromatic_python-0.7.0/chromatic/color/core.pyi +672 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/color/palette.py +162 -30
- chromatic_python-0.7.0/chromatic/color/palette.pyi +280 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/demo.py +62 -48
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/image/__init__.pyi +2 -4
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/image/_array.py +546 -548
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/image/_curses.py +26 -36
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic_python.egg-info/PKG-INFO +5 -2
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic_python.egg-info/SOURCES.txt +2 -1
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic_python.egg-info/scm_file_list.json +23 -22
- chromatic_python-0.7.0/chromatic_python.egg-info/scm_version.json +8 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/pyproject.toml +4 -1
- chromatic_python-0.6.1/.github/workflows/publish.yml +0 -29
- chromatic_python-0.6.1/chromatic/color/colorconv.py +0 -355
- chromatic_python-0.6.1/chromatic/color/core.pyi +0 -550
- chromatic_python-0.6.1/chromatic/color/palette.pyi +0 -258
- chromatic_python-0.6.1/chromatic_python.egg-info/scm_version.json +0 -8
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/.gitattributes +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/.github/dependabot.yml +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/.gitignore +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/README.md +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/banner.png +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/__init__.py +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/color/__init__.py +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/color/__init__.pyi +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/data/__init__.py +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/data/__init__.pyi +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/data/_fetchers.py +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/data/fonts/userfont.schema.json +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/data/fonts/vga437.ttf +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/data/images/butterfly.jpg +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/data/images/escher.png +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/data/images/goblin_virus.png +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/data/images/registry.json +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/data/userfont.py +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/data/userfont.pyi +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/image/__init__.py +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic/image/_glyph.py +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic_python.egg-info/dependency_links.txt +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic_python.egg-info/entry_points.txt +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic_python.egg-info/requires.txt +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/chromatic_python.egg-info/top_level.txt +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/setup.cfg +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/tests/__init__.py +0 -0
- {chromatic_python-0.6.1 → chromatic_python-0.7.0}/tests/test_color_str.py +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [released]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
id-token: write
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
environment: pypi
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v7
|
|
16
|
+
- uses: astral-sh/setup-uv@v7
|
|
17
|
+
- run: uv build
|
|
18
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chromatic-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: ANSI art image processing and colored terminal text
|
|
5
5
|
Author: crypt0lith
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: repository, https://github.com/crypt0lith/chromatic.git
|
|
8
8
|
Keywords: ansi,ascii,art,font,image,terminal,parser
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
9
11
|
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
10
13
|
Classifier: Operating System :: OS Independent
|
|
11
14
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
15
|
Classifier: Typing :: Typed
|
|
13
|
-
Requires-Python: <4.0,>=3.
|
|
16
|
+
Requires-Python: <4.0,>=3.12
|
|
14
17
|
Description-Content-Type: text/markdown
|
|
15
18
|
Requires-Dist: numpy<2.6,>=1.25.2
|
|
16
19
|
Requires-Dist: pillow>=12.2.0
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
__all__ = [
|
|
2
|
-
'AnsiImage',
|
|
3
2
|
'Back',
|
|
4
3
|
'Color',
|
|
5
4
|
'ColorNamespace',
|
|
@@ -8,7 +7,6 @@ __all__ = [
|
|
|
8
7
|
'SgrParameter',
|
|
9
8
|
'Style',
|
|
10
9
|
'UserFont',
|
|
11
|
-
'__version__',
|
|
12
10
|
'ansi2img',
|
|
13
11
|
'ansi_quantize',
|
|
14
12
|
'ansicolor24Bit',
|
|
@@ -40,7 +38,6 @@ __all__ = [
|
|
|
40
38
|
'rgb2hexstr',
|
|
41
39
|
'rgb2int',
|
|
42
40
|
'sort_glyphs',
|
|
43
|
-
'to_sgr_array',
|
|
44
41
|
'ttf_extract_codepoints',
|
|
45
42
|
'userfonts',
|
|
46
43
|
'set_default_ansi',
|
|
@@ -69,7 +66,6 @@ from .color import (
|
|
|
69
66
|
)
|
|
70
67
|
from .data import UserFont, register_userfont, userfonts
|
|
71
68
|
from .image import (
|
|
72
|
-
AnsiImage,
|
|
73
69
|
ansi2img,
|
|
74
70
|
ansi_quantize,
|
|
75
71
|
ansify,
|
|
@@ -85,6 +81,5 @@ from .image import (
|
|
|
85
81
|
read_ans,
|
|
86
82
|
render_ans,
|
|
87
83
|
reshape_ansi,
|
|
88
|
-
to_sgr_array,
|
|
89
84
|
)
|
|
90
85
|
from .image._glyph import get_glyph_masks, sort_glyphs, ttf_extract_codepoints
|
|
@@ -197,19 +197,48 @@ def parse_args():
|
|
|
197
197
|
def color_parser(allow_rgba=False):
|
|
198
198
|
def parsed_color(
|
|
199
199
|
s: str, /
|
|
200
|
-
) -> tuple[int, int, int] | tuple[int, int, int
|
|
200
|
+
) -> tuple[int, int, int, int] | tuple[int, int, int]:
|
|
201
201
|
try:
|
|
202
202
|
return color_ns[s.strip().replace(" ", "_").upper()]
|
|
203
203
|
except KeyError:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
204
|
+
if any(c not in "0123456789abcdef" for c in s.casefold()):
|
|
205
|
+
raise
|
|
206
|
+
n = len(s)
|
|
207
|
+
if allow_rgba and n % 4 == 0:
|
|
208
|
+
if n == 4:
|
|
209
|
+
s = "".join(c * 2 for c in s)
|
|
210
|
+
x = int(s, 16)
|
|
211
|
+
if not 0 <= x < (1 << 32):
|
|
212
|
+
raise ValueError(f"{x:#x} is not u32")
|
|
213
|
+
return (
|
|
214
|
+
(x >> 24) & 0xFF,
|
|
215
|
+
(x >> 16) & 0xFF,
|
|
216
|
+
(x >> 8) & 0xFF,
|
|
217
|
+
x & 0xFF,
|
|
218
|
+
) # fmt: skip
|
|
219
|
+
elif n % 3 == 0:
|
|
220
|
+
if n == 3:
|
|
221
|
+
s = "".join(c * 2 for c in s)
|
|
222
|
+
x = int(s, 16)
|
|
223
|
+
if not 0 <= x < (1 << 24):
|
|
224
|
+
raise ValueError(f"{x:#x} is not u24")
|
|
225
|
+
return (x >> 16) & 0xFF, (x >> 8) & 0xFF, x & 0xFF
|
|
226
|
+
raise ValueError(
|
|
227
|
+
"expected hex digit with nbytes in {}, got {}".format(
|
|
228
|
+
(3, 4, 6, 8), int(s, 16).bit_length() // 8
|
|
229
|
+
)
|
|
230
|
+
)
|
|
209
231
|
|
|
210
232
|
return parsed_color
|
|
211
233
|
|
|
212
234
|
color_opts = ansify_base.add_argument_group("color defaults")
|
|
235
|
+
color_opts.add_argument(
|
|
236
|
+
"--bg",
|
|
237
|
+
dest="bg_default",
|
|
238
|
+
metavar="COLOR",
|
|
239
|
+
type=color_parser(allow_rgba=True),
|
|
240
|
+
help="default background color. can be hex rgb(a) value or color name",
|
|
241
|
+
)
|
|
213
242
|
color_opts.add_argument(
|
|
214
243
|
"--fg",
|
|
215
244
|
dest="fg_default",
|
|
@@ -218,11 +247,10 @@ def parse_args():
|
|
|
218
247
|
help="default foreground color. can be hex rgb(a) value or color name",
|
|
219
248
|
)
|
|
220
249
|
color_opts.add_argument(
|
|
221
|
-
"--
|
|
222
|
-
dest="
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
help="default background color. can be hex rgb(a) value or color name",
|
|
250
|
+
"--alpha",
|
|
251
|
+
dest="alpha",
|
|
252
|
+
action="store_true",
|
|
253
|
+
help="use alpha channel with defaults: bg=0, fg=0xFF",
|
|
226
254
|
)
|
|
227
255
|
|
|
228
256
|
from_img_base = new_base_parser()
|
|
@@ -444,9 +472,7 @@ def parse_args():
|
|
|
444
472
|
formatter_class=EnvHelpFormatter,
|
|
445
473
|
)
|
|
446
474
|
subcmd_p_ansify.add_argument(
|
|
447
|
-
dest="img",
|
|
448
|
-
metavar="IMAGEFILE",
|
|
449
|
-
help="input image",
|
|
475
|
+
dest="img", metavar="IMAGEFILE", help="input image"
|
|
450
476
|
)
|
|
451
477
|
|
|
452
478
|
font_cmd_subparser = cmd_subparsers.add_parser("font")
|
|
@@ -471,15 +497,26 @@ def _call_from_ns[R](f: abc.Callable[..., R], /, ns, **kwargs) -> R:
|
|
|
471
497
|
f_kwargs[k] = kwargs[k]
|
|
472
498
|
return f(*f_args, **f_kwargs)
|
|
473
499
|
|
|
500
|
+
|
|
474
501
|
def handle_image(ns):
|
|
475
502
|
vars(ns).setdefault("outfile_callback", ns._outfile_callback)
|
|
503
|
+
if getattr(ns, "alpha", False):
|
|
504
|
+
for k, x in [("bg_default", 0), ("fg_default", 0xFF)]:
|
|
505
|
+
v = getattr(ns, k, None) or (x,) * 4
|
|
506
|
+
if len(v) == 3:
|
|
507
|
+
v = *v, x
|
|
508
|
+
setattr(ns, k, v)
|
|
509
|
+
delattr(ns, "alpha")
|
|
476
510
|
match ns.subcmd:
|
|
477
511
|
case "ansify":
|
|
478
|
-
from .image import
|
|
512
|
+
from .image import ansi2img, img2ansi
|
|
479
513
|
|
|
480
514
|
ansi_array = _call_from_ns(img2ansi, ns)
|
|
481
515
|
if hasattr(ns, "dumpfile"):
|
|
482
|
-
ns.dumpfile.writelines(
|
|
516
|
+
ns.dumpfile.writelines(
|
|
517
|
+
f"{s}\n".encode() for s in map("".join, ansi_array)
|
|
518
|
+
)
|
|
519
|
+
ns.dumpfile.write(b"\x1b[0m")
|
|
483
520
|
img = _call_from_ns(ansi2img, ns, ansi_array=ansi_array)
|
|
484
521
|
try:
|
|
485
522
|
outpath = ns.outfile_callback(ns, img)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import collections.abc as abc
|
|
2
|
-
import sys
|
|
3
2
|
import types
|
|
4
3
|
import typing as tp
|
|
5
4
|
from functools import reduce
|
|
@@ -16,14 +15,9 @@ if tp.TYPE_CHECKING:
|
|
|
16
15
|
type ArrayReducerFunc[_SCT: np.generic, **_P] = abc.Callable[
|
|
17
16
|
tp.Concatenate[_ArrayLike[_SCT], _P], NDArray[_SCT]
|
|
18
17
|
]
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
]
|
|
23
|
-
else:
|
|
24
|
-
type ShapedNDArray[_Shape: tuple[int, ...], _SCT: np.generic] = np.ndarray[
|
|
25
|
-
_Shape, np.dtype[_SCT]
|
|
26
|
-
]
|
|
18
|
+
type ShapedNDArray[_Shape: tuple[int, ...], _SCT: np.generic] = np.ndarray[
|
|
19
|
+
_Shape, np.dtype[_SCT]
|
|
20
|
+
]
|
|
27
21
|
type MatrixLike[_SCT: np.generic] = ShapedNDArray[TupleOf2[int], _SCT]
|
|
28
22
|
type SquareMatrix[_I: int, _SCT: np.generic] = ShapedNDArray[TupleOf2[_I], _SCT]
|
|
29
23
|
type GlyphArray[_SCT: np.generic] = SquareMatrix[L[24], _SCT]
|
|
@@ -45,11 +39,11 @@ RGBPixel: tp.TypeAlias = ShapedNDArray[tuple[L[3]], np.uint8]
|
|
|
45
39
|
RGBImageLike: tp.TypeAlias = Image | RGBArray
|
|
46
40
|
RGBVectorLike: tp.TypeAlias = IntSequence | RGBPixel
|
|
47
41
|
ColorDictKeys = L['fg', 'bg']
|
|
48
|
-
Ansi4BitAlias = L['4b']
|
|
49
|
-
Ansi8BitAlias = L['8b']
|
|
50
|
-
Ansi24BitAlias = L['24b']
|
|
42
|
+
Ansi4BitAlias = L['4b', 1]
|
|
43
|
+
Ansi8BitAlias = L['8b', 2]
|
|
44
|
+
Ansi24BitAlias = L['24b', 3]
|
|
51
45
|
AnsiColorAlias = Ansi4BitAlias | Ansi8BitAlias | Ansi24BitAlias
|
|
52
|
-
FontArgType: tp.TypeAlias =
|
|
46
|
+
FontArgType: tp.TypeAlias = tp.Union[FreeTypeFont, "UserFont", str]
|
|
53
47
|
|
|
54
48
|
|
|
55
49
|
def type_error_msg(err_obj, *expected, context: str = '', obj_repr=False):
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.7.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 7, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g029be355a'
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
'ANSI_4BIT_RGB',
|
|
3
|
+
'ansi_4bit_to_rgb',
|
|
4
|
+
'ansi_8bit_to_rgb',
|
|
5
|
+
'int2rgb',
|
|
6
|
+
'hexstr2rgb',
|
|
7
|
+
'hsl2rgb',
|
|
8
|
+
'hsv2rgb',
|
|
9
|
+
'is_u24',
|
|
10
|
+
'lab2rgb',
|
|
11
|
+
'lab2xyz',
|
|
12
|
+
'nearest_ansi_4bit_rgb',
|
|
13
|
+
'nearest_ansi_8bit_rgb',
|
|
14
|
+
'rgb2int',
|
|
15
|
+
'rgb2hexstr',
|
|
16
|
+
'rgb2hsl',
|
|
17
|
+
'rgb2hsv',
|
|
18
|
+
'rgb2lab',
|
|
19
|
+
'rgb2xyz',
|
|
20
|
+
'rgb_diff',
|
|
21
|
+
'rgb_to_ansi_8bit',
|
|
22
|
+
'xyz2lab',
|
|
23
|
+
'xyz2rgb',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
import typing as tp
|
|
27
|
+
from functools import lru_cache
|
|
28
|
+
from types import MappingProxyType as mappingproxy
|
|
29
|
+
from typing import Final, Literal as L, SupportsInt, TypeGuard
|
|
30
|
+
|
|
31
|
+
import numpy as np
|
|
32
|
+
|
|
33
|
+
from .._typing import Float3Tuple, Int3Tuple, RGBPixel, RGBVectorLike, ShapedNDArray
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@lru_cache
|
|
37
|
+
def _supports_int(typ: type, /) -> TypeGuard[type[SupportsInt]]:
|
|
38
|
+
return issubclass(typ, SupportsInt)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_u24(value, *, strict: bool = False) -> bool:
|
|
42
|
+
"""Check if value is an unsigned 24-bit integer.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
---------
|
|
46
|
+
value
|
|
47
|
+
Input number
|
|
48
|
+
strict : bool
|
|
49
|
+
Whether to return False or raise ValueError on failure
|
|
50
|
+
|
|
51
|
+
Raises
|
|
52
|
+
------
|
|
53
|
+
ValueError
|
|
54
|
+
Raised when `strict=True` and value is not u24
|
|
55
|
+
"""
|
|
56
|
+
if _supports_int(value.__class__):
|
|
57
|
+
if 0 <= int(value) < (1 << 24):
|
|
58
|
+
return True
|
|
59
|
+
elif not strict:
|
|
60
|
+
return False
|
|
61
|
+
raise ValueError(f"{value!r} is not u24")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def hexstr2rgb(s: str, /) -> Int3Tuple:
|
|
65
|
+
n = len(s)
|
|
66
|
+
if n % 4 == 0: # trunc alpha
|
|
67
|
+
n *= 3
|
|
68
|
+
n //= 4
|
|
69
|
+
s = s[:n]
|
|
70
|
+
if n == 3: # rgb -> rrggbb
|
|
71
|
+
s = ''.join(c * 2 for c in s)
|
|
72
|
+
x = int(s, 16)
|
|
73
|
+
if not 0 <= x < (1 << 24):
|
|
74
|
+
raise ValueError(f"{x:#x} is not u24")
|
|
75
|
+
return int2rgb(x)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def rgb2hexstr(rgb: RGBVectorLike, /) -> str:
|
|
79
|
+
return "%02x%02x%02x" % tuple(rgb)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def rgb2int(rgb: RGBVectorLike, /) -> int:
|
|
83
|
+
r, g, b = map(int, rgb)
|
|
84
|
+
return r << 16 | g << 8 | b
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def int2rgb(x: int, /) -> Int3Tuple:
|
|
88
|
+
x = int(x) & 0xFFFFFF
|
|
89
|
+
return (x >> 16) & 0xFF, (x >> 8) & 0xFF, x & 0xFF
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# fmt: off
|
|
93
|
+
# sRGB RGB to XYZ [M]
|
|
94
|
+
M_RGB2XYZ = np.array(
|
|
95
|
+
[[0.4124564, 0.3575761, 0.1804375],
|
|
96
|
+
[0.2126729, 0.7151522, 0.0721750],
|
|
97
|
+
[0.0193339, 0.1191920, 0.9503041]],
|
|
98
|
+
dtype=np.float64
|
|
99
|
+
)
|
|
100
|
+
M_XYZ2RGB = np.linalg.inv(M_RGB2XYZ)
|
|
101
|
+
|
|
102
|
+
# D65 reference white
|
|
103
|
+
REFWT = M_RGB2XYZ.sum(axis=1)
|
|
104
|
+
|
|
105
|
+
M_RGB2XYZ.flags.writeable = \
|
|
106
|
+
M_XYZ2RGB.flags.writeable = \
|
|
107
|
+
REFWT.flags.writeable = False
|
|
108
|
+
# fmt: on
|
|
109
|
+
|
|
110
|
+
EPS = 216 / 24389
|
|
111
|
+
LIN = (1 / 3) * (6 / 29) ** -2
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def xyz2rgb(xyz, /):
|
|
115
|
+
out = np.clip(np.asarray(xyz, dtype=np.float64) @ M_XYZ2RGB.T, 0.0, 1.0)
|
|
116
|
+
return np.rint(out * 255).astype(np.uint8)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def rgb2xyz(rgb, /):
|
|
120
|
+
return (np.asarray(rgb, dtype=np.float64) / 255.0) @ M_RGB2XYZ.T
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def xyz2lab(xyz, /):
|
|
124
|
+
arr = np.asarray(xyz, dtype=np.float64)
|
|
125
|
+
shape = arr.shape
|
|
126
|
+
arr = np.atleast_2d(arr)
|
|
127
|
+
n = arr / REFWT
|
|
128
|
+
f = np.where(n > EPS, np.cbrt(n), LIN * n + (16 / 116))
|
|
129
|
+
fx, fy, fz = f[..., 0], f[..., 1], f[..., 2]
|
|
130
|
+
L = 116.0 * fy - 16.0
|
|
131
|
+
a = 500.0 * (fx - fy)
|
|
132
|
+
b = 200.0 * (fy - fz)
|
|
133
|
+
return np.stack((L, a, b), axis=-1).reshape(shape)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def lab2xyz(lab, /):
|
|
137
|
+
arr = np.asarray(lab, dtype=np.float64)
|
|
138
|
+
shape = arr.shape
|
|
139
|
+
arr = np.atleast_2d(arr)
|
|
140
|
+
L, a, b = arr[..., 0], arr[..., 1], arr[..., 2]
|
|
141
|
+
fy = (L + 16.0) / 116.0
|
|
142
|
+
fx = a / 500.0 + fy
|
|
143
|
+
fz = fy - b / 200.0
|
|
144
|
+
f = np.stack((fx, fy, fz), axis=-1)
|
|
145
|
+
f3 = f**3
|
|
146
|
+
n = np.where(f3 > EPS, f3, (f - (16 / 116)) / LIN)
|
|
147
|
+
return (n * REFWT).reshape(shape)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def hsl2rgb(hsl, /):
|
|
151
|
+
arr = np.asarray(hsl, dtype=np.float32)
|
|
152
|
+
shape = arr.shape
|
|
153
|
+
arr = np.atleast_2d(arr)
|
|
154
|
+
h, s, L = arr[..., 0], arr[..., 1], arr[..., 2]
|
|
155
|
+
C = (1.0 - np.abs(2.0 * L - 1.0)) * s
|
|
156
|
+
h6 = h * 6.0
|
|
157
|
+
i = np.floor(h6).astype(int)
|
|
158
|
+
X = C * (1.0 - np.abs(h6 % 2.0 - 1.0))
|
|
159
|
+
m = L - C / 2.0
|
|
160
|
+
xs = np.stack([C + m, X + m, m], axis=-1)
|
|
161
|
+
P = np.array(
|
|
162
|
+
[[0, 1, 2],
|
|
163
|
+
[1, 0, 2],
|
|
164
|
+
[2, 0, 1],
|
|
165
|
+
[2, 1, 0],
|
|
166
|
+
[1, 2, 0],
|
|
167
|
+
[0, 2, 1]]
|
|
168
|
+
) # fmt: skip
|
|
169
|
+
out = np.take_along_axis(xs, P[i % 6], axis=-1)
|
|
170
|
+
return np.rint(out * 255).astype(np.uint8).reshape(shape)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def rgb2hsl(rgb, /):
|
|
174
|
+
arr = np.asarray(rgb, dtype=np.float32) / 255.0
|
|
175
|
+
shape = arr.shape
|
|
176
|
+
arr = np.atleast_2d(arr)
|
|
177
|
+
r, g, b = arr[..., 0], arr[..., 1], arr[..., 2]
|
|
178
|
+
m = np.min(arr, axis=-1)
|
|
179
|
+
v = np.max(arr, axis=-1)
|
|
180
|
+
C = v - m
|
|
181
|
+
L = (v + m) / 2.0
|
|
182
|
+
s = np.zeros_like(v)
|
|
183
|
+
denom = 1.0 - np.abs(2.0 * L - 1.0)
|
|
184
|
+
ok = denom != 0
|
|
185
|
+
s[ok] = C[ok] / denom[ok]
|
|
186
|
+
nz = C != 0
|
|
187
|
+
rmax = (v == r) & nz
|
|
188
|
+
gmax = (v == g) & nz
|
|
189
|
+
bmax = (v == b) & nz
|
|
190
|
+
h = np.zeros_like(v)
|
|
191
|
+
h[rmax] = ((g[rmax] - b[rmax]) / C[rmax]) % 6
|
|
192
|
+
h[gmax] = ((b[gmax] - r[gmax]) / C[gmax]) + 2
|
|
193
|
+
h[bmax] = ((r[bmax] - g[bmax]) / C[bmax]) + 4
|
|
194
|
+
h = (h / 6.0) % 1.0
|
|
195
|
+
return np.stack((h, s, L), axis=-1).reshape(shape)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def hsv2rgb(hsv, /):
|
|
199
|
+
arr = np.asarray(hsv, dtype=np.float32)
|
|
200
|
+
shape = arr.shape
|
|
201
|
+
arr = np.atleast_2d(arr)
|
|
202
|
+
h, s, v = arr[..., 0], arr[..., 1], arr[..., 2]
|
|
203
|
+
h6 = h * 6.0
|
|
204
|
+
i = np.floor(h6).astype(int)
|
|
205
|
+
f = h6 - i
|
|
206
|
+
xs = np.stack([v, v * (1 - s), v * (1 - f * s), v * (1 - (1 - f) * s)], axis=-1)
|
|
207
|
+
P = np.array(
|
|
208
|
+
[[0, 3, 1],
|
|
209
|
+
[2, 0, 1],
|
|
210
|
+
[1, 0, 3],
|
|
211
|
+
[1, 2, 0],
|
|
212
|
+
[3, 1, 0],
|
|
213
|
+
[0, 1, 2]]
|
|
214
|
+
) # fmt: skip
|
|
215
|
+
out = np.take_along_axis(xs, P[i % 6], axis=-1)
|
|
216
|
+
return np.rint(out * 255).astype(np.uint8).reshape(shape)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def rgb2hsv(rgb, /):
|
|
220
|
+
arr = np.asarray(rgb, dtype=np.float32) / 255.0
|
|
221
|
+
shape = arr.shape
|
|
222
|
+
arr = np.atleast_2d(arr)
|
|
223
|
+
r, g, b = arr[..., 0], arr[..., 1], arr[..., 2]
|
|
224
|
+
m = np.min(arr, axis=-1)
|
|
225
|
+
v = np.max(arr, axis=-1)
|
|
226
|
+
C = v - m
|
|
227
|
+
ok = v != 0
|
|
228
|
+
s = np.zeros_like(v)
|
|
229
|
+
s[ok] = C[ok] / v[ok]
|
|
230
|
+
nz = C != 0
|
|
231
|
+
rmax = (v == r) & nz
|
|
232
|
+
gmax = (v == g) & nz
|
|
233
|
+
bmax = (v == b) & nz
|
|
234
|
+
h = np.zeros_like(v)
|
|
235
|
+
h[rmax] = ((g[rmax] - b[rmax]) / C[rmax]) % 6
|
|
236
|
+
h[gmax] = ((b[gmax] - r[gmax]) / C[gmax]) + 2
|
|
237
|
+
h[bmax] = ((r[bmax] - g[bmax]) / C[bmax]) + 4
|
|
238
|
+
h = (h / 6.0) % 1.0
|
|
239
|
+
return np.stack((h, s, v), axis=-1).reshape(shape)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def lab2rgb(lab, /):
|
|
243
|
+
return xyz2rgb(lab2xyz(lab))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def rgb2lab(rgb, /):
|
|
247
|
+
return xyz2lab(rgb2xyz(rgb))
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def rgb_diff(rgb1, rgb2, /):
|
|
251
|
+
return lab2rgb((rgb2lab(rgb1) + rgb2lab(rgb2)) / 2)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
ANSI_4BIT_RGB = (
|
|
255
|
+
(0, 0, 0), # black
|
|
256
|
+
(170, 0, 0), # red
|
|
257
|
+
(0, 170, 0), # green
|
|
258
|
+
(170, 85, 0), # yellow
|
|
259
|
+
(0, 0, 170), # blue
|
|
260
|
+
(170, 0, 170), # magenta
|
|
261
|
+
(0, 170, 170), # cyan
|
|
262
|
+
(170, 170, 170), # white
|
|
263
|
+
(85, 85, 85), # bright black (grey)
|
|
264
|
+
(255, 85, 85), # bright red
|
|
265
|
+
(85, 255, 85), # bright green
|
|
266
|
+
(255, 255, 85), # bright yellow
|
|
267
|
+
(85, 85, 255), # bright blue
|
|
268
|
+
(255, 85, 255), # bright magenta
|
|
269
|
+
(85, 255, 255), # bright cyan
|
|
270
|
+
(255, 255, 255), # bright white
|
|
271
|
+
) # fmt: skip
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def ansi_4bit_to_rgb(value: int, /):
|
|
275
|
+
offset = 0
|
|
276
|
+
if value > 37:
|
|
277
|
+
if value <= 47:
|
|
278
|
+
offset -= 10
|
|
279
|
+
elif value <= 97:
|
|
280
|
+
offset += 8
|
|
281
|
+
else:
|
|
282
|
+
offset -= 2
|
|
283
|
+
value %= 30
|
|
284
|
+
value += offset
|
|
285
|
+
return ANSI_4BIT_RGB[value]
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _4b_lookup() -> ShapedNDArray[tuple[L[32], L[32], L[32], L[3]], np.uint8]:
|
|
289
|
+
def rgb_dist(rgb, ansi):
|
|
290
|
+
r_mean = (rgb[:, 0:1] + ansi[:, 0]) / 2
|
|
291
|
+
r_diff = (rgb[:, 0:1] - ansi[:, 0]) * (2 + r_mean / 256)
|
|
292
|
+
g_diff = (rgb[:, 1:2] - ansi[:, 1]) * 4
|
|
293
|
+
b_diff = (rgb[:, 2:3] - ansi[:, 2]) * (2 + (255 - r_mean) / 256)
|
|
294
|
+
return r_diff**2 + g_diff**2 + b_diff**2
|
|
295
|
+
|
|
296
|
+
rgb_4b_arr = np.asarray(ANSI_4BIT_RGB)
|
|
297
|
+
shape = (32,) * 3
|
|
298
|
+
quants = np.indices(shape).reshape(3, -1).T
|
|
299
|
+
nearest_colors = rgb_4b_arr[np.argmin(rgb_dist(quants * 8, rgb_4b_arr), axis=1)]
|
|
300
|
+
lut = nearest_colors.reshape(*shape, 3).astype(np.uint8)
|
|
301
|
+
lut.flags.writeable = False
|
|
302
|
+
return lut # type: ignore
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
ANSI_4BIT_RGB_LUT = _4b_lookup()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def nearest_ansi_4bit_rgb(rgb, /):
|
|
309
|
+
if is_3tuple := (rgb.__class__ is tuple and len(rgb) == 3):
|
|
310
|
+
r, g, b = (min(x >> 3, 32) for x in rgb)
|
|
311
|
+
else:
|
|
312
|
+
arr = np.asarray(rgb, dtype=np.uint8) >> 3
|
|
313
|
+
if not arr.shape or arr.shape[-1] != 3:
|
|
314
|
+
raise ValueError
|
|
315
|
+
r, g, b = arr[..., 0], arr[..., 1], arr[..., 2]
|
|
316
|
+
out = ANSI_4BIT_RGB_LUT[r, g, b]
|
|
317
|
+
return tuple(out.tolist()) if is_3tuple else out
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def nearest_ansi_8bit_rgb(value, /):
|
|
321
|
+
return ansi_8bit_to_rgb(rgb_to_ansi_8bit(value))
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def ansi_8bit_to_rgb(value, /):
|
|
325
|
+
arr = np.atleast_1d(value).astype(np.uint8)
|
|
326
|
+
out = np.empty((*arr.shape, 3), dtype=np.uint8)
|
|
327
|
+
mask = np.ones(arr.shape, dtype=np.bool_)
|
|
328
|
+
if np.any(ansi_16c := arr < 16):
|
|
329
|
+
out[ansi_16c] = np.take(ANSI_4BIT_RGB, arr[ansi_16c], axis=0)
|
|
330
|
+
mask &= ~ansi_16c
|
|
331
|
+
if np.any(colorcube := (arr >= 16) & (arr < 232)):
|
|
332
|
+
c = arr[colorcube] - 16
|
|
333
|
+
out[colorcube, 0] = (c // 36) * 51
|
|
334
|
+
out[colorcube, 1] = (c % 36 // 6) * 51
|
|
335
|
+
out[colorcube, 2] = (c % 6) * 51
|
|
336
|
+
mask &= ~colorcube
|
|
337
|
+
out[mask] = (8 + (arr[mask] - 232) * 10)[:, None]
|
|
338
|
+
return tuple(out[0].tolist()) if np.isscalar(value) else out
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def rgb_to_ansi_8bit(rgb, /) -> int | ShapedNDArray[tuple[int, ...], np.uint8]:
|
|
342
|
+
is_3tuple = rgb.__class__ is tuple and len(rgb) == 3
|
|
343
|
+
arr = np.asarray(rgb, dtype=np.uint8)
|
|
344
|
+
out = np.zeros(arr.shape[:-1], dtype=np.uint8)
|
|
345
|
+
mask = np.ones(arr.shape[:-1], dtype=np.bool_)
|
|
346
|
+
if np.any(grey := (arr[..., 1:] == arr[..., 0:1]).all(axis=-1)):
|
|
347
|
+
c = arr[..., 0]
|
|
348
|
+
r_lo = grey & (c < 8)
|
|
349
|
+
r_hi = grey & (c > 248)
|
|
350
|
+
mid = grey & ~(r_lo | r_hi)
|
|
351
|
+
out[r_lo] = 16
|
|
352
|
+
out[r_hi] = 231
|
|
353
|
+
out[mid] = np.rint((c[mid] - 8) / 247 * 24) + 232
|
|
354
|
+
mask &= ~grey
|
|
355
|
+
rest = np.rint(arr[mask] / 255 * 5)
|
|
356
|
+
r, g, b = rest[..., 0], rest[..., 1], rest[..., 2]
|
|
357
|
+
out[mask] = 16 + (36 * r) + (6 * g) + b
|
|
358
|
+
return out.item() if is_3tuple else out
|