chromatic-python 0.1.0__py3-none-any.whl
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/__init__.py +26 -0
- chromatic/_typing.py +168 -0
- chromatic/ascii/__init__.py +5 -0
- chromatic/ascii/_array.py +1059 -0
- chromatic/ascii/_curses.py +153 -0
- chromatic/ascii/_glyph_proc.py +107 -0
- chromatic/color/__init__.py +6 -0
- chromatic/color/colorconv.py +316 -0
- chromatic/color/core.py +1677 -0
- chromatic/color/core.pyi +421 -0
- chromatic/color/palette.py +693 -0
- chromatic/color/palette.pyi +330 -0
- chromatic/data/__init__.py +189 -0
- chromatic/data/__init__.pyi +15 -0
- chromatic/data/fonts/IBM_VGA_437_8x16.ttf +0 -0
- chromatic/data/fonts/consolas.ttf +0 -0
- chromatic/data/images/butterfly.jpg +0 -0
- chromatic/data/images/escher.png +0 -0
- chromatic/data/images/goblin_virus.png +0 -0
- chromatic/data/images/hotdog.jpg +0 -0
- chromatic/demo.py +417 -0
- chromatic_python-0.1.0.dist-info/LICENSE +21 -0
- chromatic_python-0.1.0.dist-info/METADATA +39 -0
- chromatic_python-0.1.0.dist-info/RECORD +26 -0
- chromatic_python-0.1.0.dist-info/WHEEL +5 -0
- chromatic_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
'ctrl',
|
|
3
|
+
'isprint',
|
|
4
|
+
'ControlCharacter',
|
|
5
|
+
'alt',
|
|
6
|
+
'ascii_printable',
|
|
7
|
+
'cp437_printable',
|
|
8
|
+
'isctrl',
|
|
9
|
+
'translate_cp437',
|
|
10
|
+
'unctrl'
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
from enum import IntEnum
|
|
14
|
+
from types import MappingProxyType
|
|
15
|
+
from typing import Iterable, Iterator, overload, Union
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ControlCharacter(IntEnum):
|
|
19
|
+
NUL = 0x00 # ^@
|
|
20
|
+
SOH = 0x01 # ^A
|
|
21
|
+
STX = 0x02 # ^B
|
|
22
|
+
ETX = 0x03 # ^C
|
|
23
|
+
EOT = 0x04 # ^D
|
|
24
|
+
ENQ = 0x05 # ^E
|
|
25
|
+
ACK = 0x06 # ^F
|
|
26
|
+
BEL = 0x07 # ^G
|
|
27
|
+
BS = 0x08 # ^H
|
|
28
|
+
TAB = 0x09 # ^I
|
|
29
|
+
HT = 0x09 # ^I
|
|
30
|
+
LF = 0x0a # ^J
|
|
31
|
+
NL = 0x0a # ^J
|
|
32
|
+
VT = 0x0b # ^K
|
|
33
|
+
FF = 0x0c # ^L
|
|
34
|
+
CR = 0x0d # ^M
|
|
35
|
+
SO = 0x0e # ^N
|
|
36
|
+
SI = 0x0f # ^O
|
|
37
|
+
DLE = 0x10 # ^P
|
|
38
|
+
DC1 = 0x11 # ^Q
|
|
39
|
+
DC2 = 0x12 # ^R
|
|
40
|
+
DC3 = 0x13 # ^S
|
|
41
|
+
DC4 = 0x14 # ^T
|
|
42
|
+
NAK = 0x15 # ^U
|
|
43
|
+
SYN = 0x16 # ^V
|
|
44
|
+
ETB = 0x17 # ^W
|
|
45
|
+
CAN = 0x18 # ^X
|
|
46
|
+
EM = 0x19 # ^Y
|
|
47
|
+
SUB = 0x1a # ^Z
|
|
48
|
+
ESC = 0x1b # ^[
|
|
49
|
+
FS = 0x1c # ^\
|
|
50
|
+
GS = 0x1d # ^]
|
|
51
|
+
RS = 0x1e # ^^
|
|
52
|
+
US = 0x1f # ^_
|
|
53
|
+
DEL = 0x7f # delete
|
|
54
|
+
NBSP = 0xa0 # non-breaking hard space
|
|
55
|
+
SP = 0x20 # space
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
CP437_TRANS_TABLE = MappingProxyType(
|
|
59
|
+
{
|
|
60
|
+
0: None, 1: 0x263a, 2: 0x263b, 3: 0x2665, 4: 0x2666, 5: 0x2663, 6: 0x2660, 7: 0x2022,
|
|
61
|
+
8: 0x25d8, 9: 0x25cb, 10: 0x25d9, 11: 0x2642, 12: 0x2640, 13: 0x266a, 14: 0x266b,
|
|
62
|
+
15: 0x263c, 16: 0x25ba, 17: 0x25c4, 18: 0x2195, 19: 0x203c, 20: 0xb6, 21: 0xa7,
|
|
63
|
+
22: 0x25ac, 23: 0x21a8, 24: 0x2191, 25: 0x2193, 0x1a: 0x2192, 0x1b: 0x2190,
|
|
64
|
+
0x1c: 0x221f, 0x1d: 0x2194, 0x1e: 0x25b2, 0x1f: 0x25bc, 0x7f: 0x2302, 0xa0: None
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@overload
|
|
69
|
+
def translate_cp437[_T: (int, str)](
|
|
70
|
+
__x: str,
|
|
71
|
+
*,
|
|
72
|
+
ignore: Union[_T, Iterable[_T]] = ...
|
|
73
|
+
) -> str:
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@overload
|
|
78
|
+
def translate_cp437[_T: (int, str)](
|
|
79
|
+
__iter: Iterable[str],
|
|
80
|
+
*,
|
|
81
|
+
ignore: Union[_T, Iterable[_T]] = ...
|
|
82
|
+
) -> Iterator[str]:
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def translate_cp437(
|
|
87
|
+
__x: Union[str, Iterable[str]],
|
|
88
|
+
*,
|
|
89
|
+
ignore: Union[int, Iterable[int]] = None
|
|
90
|
+
) -> Union[str, Iterator[str]]:
|
|
91
|
+
keys_view = set(CP437_TRANS_TABLE.keys())
|
|
92
|
+
if ignore is not None:
|
|
93
|
+
if isinstance(ignore, Iterable):
|
|
94
|
+
keys_view.difference_update(ignore)
|
|
95
|
+
else:
|
|
96
|
+
keys_view.discard(ignore)
|
|
97
|
+
trans_table = {k: v for (k, v) in
|
|
98
|
+
CP437_TRANS_TABLE.items()
|
|
99
|
+
if k in keys_view}
|
|
100
|
+
if not isinstance(__x, str):
|
|
101
|
+
return iter(map(lambda s: str.translate(s, trans_table), __x))
|
|
102
|
+
return __x.translate(trans_table)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def cp437_printable():
|
|
106
|
+
"""Return a string containing all graphical characters in code page 437"""
|
|
107
|
+
return translate_cp437(bytes(range(256)).decode(encoding='cp437'))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def ascii_printable():
|
|
111
|
+
return bytes(range(32, 127)).decode(encoding='ascii')
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _ctoi(c: Union[str, int]):
|
|
115
|
+
if isinstance(c, str):
|
|
116
|
+
return ord(c)
|
|
117
|
+
else:
|
|
118
|
+
return c
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def isprint(c: Union[str, int]):
|
|
122
|
+
return 32 <= _ctoi(c) <= 126
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def isctrl(c: Union[str, int]):
|
|
126
|
+
return 0 <= _ctoi(c) < 32
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def ctrl(c: Union[str, int]):
|
|
130
|
+
if isinstance(c, str):
|
|
131
|
+
return chr(_ctoi(c) & 0x1f)
|
|
132
|
+
else:
|
|
133
|
+
return _ctoi(c) & 0x1f
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def alt(c: Union[str, int]):
|
|
137
|
+
if isinstance(c, str):
|
|
138
|
+
return chr(_ctoi(c) | 0x80)
|
|
139
|
+
else:
|
|
140
|
+
return _ctoi(c) | 0x80
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def unctrl(c: Union[str, int]):
|
|
144
|
+
bits = _ctoi(c)
|
|
145
|
+
if bits == 0x7f:
|
|
146
|
+
rep = '^?'
|
|
147
|
+
elif isprint(bits & 0x7f):
|
|
148
|
+
rep = chr(bits & 0x7f)
|
|
149
|
+
else:
|
|
150
|
+
rep = '^' + chr(((bits & 0x7f) | 0x20) + 0x20)
|
|
151
|
+
if bits & 0x80:
|
|
152
|
+
return '!' + rep
|
|
153
|
+
return rep
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
'get_glyph_masks',
|
|
5
|
+
'ttf_extract_codepoints',
|
|
6
|
+
'sort_glyphs'
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
from os import PathLike
|
|
10
|
+
from typing import Literal, overload, Sequence, Union
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
from fontTools.ttLib import TTFont
|
|
14
|
+
from numpy import float64, uint8
|
|
15
|
+
from scipy.ndimage import distance_transform_edt
|
|
16
|
+
|
|
17
|
+
from ._array import otsu_mask
|
|
18
|
+
from ._curses import ascii_printable
|
|
19
|
+
from .._typing import (
|
|
20
|
+
FontArgType, GlyphArray, GlyphBitmask, ShapedNDArray
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@overload
|
|
25
|
+
def get_glyph_masks(
|
|
26
|
+
__font: FontArgType,
|
|
27
|
+
char_set: Sequence[str] = ...,
|
|
28
|
+
dist_transform: Literal[False] = False
|
|
29
|
+
) -> dict[str, GlyphBitmask]:
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@overload
|
|
34
|
+
def get_glyph_masks(
|
|
35
|
+
__font: FontArgType,
|
|
36
|
+
char_set: Sequence[str] = ...,
|
|
37
|
+
dist_transform: Literal[True] = ...
|
|
38
|
+
) -> dict[str, GlyphArray[float64]]:
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@overload
|
|
43
|
+
def get_glyph_masks(
|
|
44
|
+
__font: FontArgType,
|
|
45
|
+
char_set: Sequence[str] = ...,
|
|
46
|
+
dist_transform: bool = ...
|
|
47
|
+
) -> dict[str, GlyphArray[Union[uint8, float64]]]:
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_glyph_masks(
|
|
52
|
+
__font: FontArgType,
|
|
53
|
+
char_set: Sequence[str] = None,
|
|
54
|
+
dist_transform: bool = False
|
|
55
|
+
) -> dict[str, GlyphArray[Union[uint8, float64]]]:
|
|
56
|
+
from ._array import get_font_object, render_font_char
|
|
57
|
+
|
|
58
|
+
char_set = char_set or ascii_printable()
|
|
59
|
+
font = get_font_object(__font)
|
|
60
|
+
|
|
61
|
+
def _get_threshold(__c: str):
|
|
62
|
+
out = otsu_mask(
|
|
63
|
+
render_font_char(__c, font).convert('L'))
|
|
64
|
+
if dist_transform is True:
|
|
65
|
+
return distance_transform_edt(out)
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
space = _get_threshold(' ')
|
|
69
|
+
non_printable = _get_threshold('�')
|
|
70
|
+
glyph_masks = {}
|
|
71
|
+
for char in set(char_set):
|
|
72
|
+
thresh = _get_threshold(char)
|
|
73
|
+
if np.array_equal(thresh, non_printable):
|
|
74
|
+
thresh = space
|
|
75
|
+
glyph_masks[char] = thresh
|
|
76
|
+
return glyph_masks
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def sort_glyphs(__s: str,
|
|
80
|
+
font: FontArgType,
|
|
81
|
+
reverse: bool = False):
|
|
82
|
+
def _sum_mask(item: tuple[str, np.ndarray]):
|
|
83
|
+
return item[0], np.sum(item[1])
|
|
84
|
+
|
|
85
|
+
return ''.join(
|
|
86
|
+
char for (char, value) in sorted(
|
|
87
|
+
map(
|
|
88
|
+
_sum_mask,
|
|
89
|
+
get_glyph_masks(
|
|
90
|
+
font, __s,
|
|
91
|
+
dist_transform=True
|
|
92
|
+
).items()),
|
|
93
|
+
key=lambda x: x[1],
|
|
94
|
+
reverse=reverse)
|
|
95
|
+
if value > 0 or char == ' ')
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def ttf_extract_codepoints(
|
|
99
|
+
__fp: PathLike[str] | str,
|
|
100
|
+
**kwargs
|
|
101
|
+
) -> ShapedNDArray[tuple[int], np.uint16]:
|
|
102
|
+
codepoints = set()
|
|
103
|
+
with TTFont(__fp, **kwargs) as font:
|
|
104
|
+
for table in font['cmap'].tables:
|
|
105
|
+
codepoints |= table.cmap.keys()
|
|
106
|
+
|
|
107
|
+
return np.sort(np.array([i for i in codepoints if chr(i).isprintable()], dtype='<u2'))
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
'ANSI_4BIT_RGB',
|
|
3
|
+
'ansi_4bit_to_rgb',
|
|
4
|
+
'ansi_8bit_to_rgb',
|
|
5
|
+
'hex2rgb',
|
|
6
|
+
'hexstr2rgb',
|
|
7
|
+
'hsl2rgb',
|
|
8
|
+
'hsv2rgb',
|
|
9
|
+
'is_hex_rgb',
|
|
10
|
+
'lab2rgb',
|
|
11
|
+
'lab2xyz',
|
|
12
|
+
'nearest_ansi_4bit_rgb',
|
|
13
|
+
'nearest_ansi_8bit_rgb',
|
|
14
|
+
'rgb2hex',
|
|
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
|
+
from typing import cast, Final, Literal, SupportsInt
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
from .._typing import Float3Tuple, FloatSequence, Int3Tuple, RGBPixel, RGBVectorLike, ShapedNDArray
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_hex_rgb(value, *, strict: bool = False):
|
|
34
|
+
if issubclass(type(value), SupportsInt):
|
|
35
|
+
if 0x0 <= int(value) <= 0xFFFFFF:
|
|
36
|
+
return True
|
|
37
|
+
elif not strict:
|
|
38
|
+
return False
|
|
39
|
+
raise TypeError(
|
|
40
|
+
f"{value!r} is not a valid RGB color") from None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def hexstr2rgb(__str: str) -> Int3Tuple:
|
|
44
|
+
if is_hex_rgb(value := int(__str, 16), strict=True):
|
|
45
|
+
return hex2rgb(value)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def rgb2hexstr(rgb: RGBVectorLike) -> str:
|
|
49
|
+
r, g, b = rgb
|
|
50
|
+
return f'{r:02x}{g:02x}{b:02x}'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def rgb2hex(rgb: RGBVectorLike) -> int:
|
|
54
|
+
r, g, b = map(int, rgb)
|
|
55
|
+
return r << 16 | g << 8 | b
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def hex2rgb(value: int) -> Int3Tuple:
|
|
59
|
+
return (value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def xyz2lab(xyz: Float3Tuple | FloatSequence) -> Float3Tuple:
|
|
63
|
+
x_ref, y_ref, z_ref = 95.047, 100.0, 108.883
|
|
64
|
+
f = lambda n: n ** (1 / 3) if n > 0.008856 else (7.787 * n) + (16 / 116)
|
|
65
|
+
x, y, z = xyz
|
|
66
|
+
x = f(x / x_ref)
|
|
67
|
+
y = f(y / y_ref)
|
|
68
|
+
z = f(z / z_ref)
|
|
69
|
+
L = (116 * y) - 16
|
|
70
|
+
a = 500 * (x - y)
|
|
71
|
+
b = 200 * (y - z)
|
|
72
|
+
return L, a, b
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def lab2xyz(lab: Float3Tuple | FloatSequence) -> Float3Tuple:
|
|
76
|
+
x_ref, y_ref, z_ref = 95.047, 100.0, 108.883
|
|
77
|
+
f_inv = lambda n: cubic if (cubic := n ** 3) > 0.008856 else (n - 16 / 116) / 7.787
|
|
78
|
+
L, a, b = lab
|
|
79
|
+
f_y = (L + 16) / 116
|
|
80
|
+
f_x = a / 500 + f_y
|
|
81
|
+
f_z = f_y - b / 200
|
|
82
|
+
x = x_ref * f_inv(f_x)
|
|
83
|
+
y = y_ref * f_inv(f_y)
|
|
84
|
+
z = z_ref * f_inv(f_z)
|
|
85
|
+
return x, y, z
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
M_RGB2XYZ = np.array(
|
|
89
|
+
[[0.4124564, 0.3575761, 0.1804375],
|
|
90
|
+
[0.2126729, 0.7151522, 0.0721750],
|
|
91
|
+
[0.0193339, 0.1191920, 0.9503041]],
|
|
92
|
+
dtype=np.float64)
|
|
93
|
+
M_XYZ2RGB = np.linalg.inv(M_RGB2XYZ)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def rgb2xyz(rgb: RGBPixel) -> Float3Tuple:
|
|
97
|
+
x, y, z = M_RGB2XYZ @ (np.array(rgb, dtype=np.float64) / 255.0)
|
|
98
|
+
return x, y, z
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def xyz2rgb(xyz: ShapedNDArray[tuple[Literal[3]], np.float64]) -> Int3Tuple:
|
|
102
|
+
r, g, b = (np.clip(M_XYZ2RGB @ np.array(xyz, dtype=np.float64), 0.0, 1.0) * 255.0).astype(int)
|
|
103
|
+
return r, g, b
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def hsl2rgb(hsl: Float3Tuple | FloatSequence) -> Int3Tuple:
|
|
107
|
+
h, s, L = hsl
|
|
108
|
+
h = (h / 360) % 1
|
|
109
|
+
if h < 0:
|
|
110
|
+
h += 1
|
|
111
|
+
r = g = b = L
|
|
112
|
+
v = (L * (1.0 + s)) if L <= 0.5 else (L + s - L * s)
|
|
113
|
+
if v > 0:
|
|
114
|
+
m = L + L - v
|
|
115
|
+
sv = (v - m) / v
|
|
116
|
+
h *= 6.0
|
|
117
|
+
sextant = int(h)
|
|
118
|
+
fract = h - sextant
|
|
119
|
+
vsf = v * sv * fract
|
|
120
|
+
mid1 = m + vsf
|
|
121
|
+
mid2 = v - vsf
|
|
122
|
+
if sextant == 0:
|
|
123
|
+
r, g, b = v, mid1, m
|
|
124
|
+
elif sextant == 1:
|
|
125
|
+
r, g, b = mid2, v, m
|
|
126
|
+
elif sextant == 2:
|
|
127
|
+
r, g, b = m, v, mid1
|
|
128
|
+
elif sextant == 3:
|
|
129
|
+
r, g, b = m, mid2, v
|
|
130
|
+
elif sextant == 4:
|
|
131
|
+
r, g, b = mid1, m, v
|
|
132
|
+
elif sextant == 5:
|
|
133
|
+
r, g, b = v, m, mid2
|
|
134
|
+
r, g, b = (round(x * 255) for x in (r, g, b))
|
|
135
|
+
else:
|
|
136
|
+
r, g, b = (round(L * 255) for _ in range(3))
|
|
137
|
+
return r, g, b
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def rgb2hsl(rgb: RGBVectorLike) -> Float3Tuple:
|
|
141
|
+
r, g, b = (x / 255.0 for x in rgb)
|
|
142
|
+
m, v = sorted([r, g, b])[::2]
|
|
143
|
+
L = (m + v) / 2
|
|
144
|
+
h = s = 0
|
|
145
|
+
if L > 0:
|
|
146
|
+
vm = v - m
|
|
147
|
+
s = vm / (v + m) if L <= 0.5 else vm / (2 - v - m)
|
|
148
|
+
if vm > 0:
|
|
149
|
+
r2 = (v - r) / vm
|
|
150
|
+
g2 = (v - g) / vm
|
|
151
|
+
b2 = (v - b) / vm
|
|
152
|
+
if v == r:
|
|
153
|
+
h = b2 - g2
|
|
154
|
+
elif v == g:
|
|
155
|
+
h = 2 + r2 - b2
|
|
156
|
+
else:
|
|
157
|
+
h = 4 + g2 - r2
|
|
158
|
+
h = (h / 6) % 1
|
|
159
|
+
return (360 * h, s, L)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def hsv2rgb(hsv: Float3Tuple | FloatSequence) -> Int3Tuple:
|
|
163
|
+
h, s, v = hsv
|
|
164
|
+
c = v * s
|
|
165
|
+
x = c * (1 - abs((h / 60) % 2 - 1))
|
|
166
|
+
m = v - c
|
|
167
|
+
if h < 0:
|
|
168
|
+
h += 360
|
|
169
|
+
h %= 360
|
|
170
|
+
if h < 60:
|
|
171
|
+
r, g, b = c, x, 0
|
|
172
|
+
elif h < 120:
|
|
173
|
+
r, g, b = x, c, 0
|
|
174
|
+
elif h < 180:
|
|
175
|
+
r, g, b = 0, c, x
|
|
176
|
+
elif h < 240:
|
|
177
|
+
r, g, b = 0, x, c
|
|
178
|
+
elif h < 300:
|
|
179
|
+
r, g, b = x, 0, c
|
|
180
|
+
else:
|
|
181
|
+
r, g, b = c, 0, x
|
|
182
|
+
r, g, b = (int(round((i + m) * 255)) for i in (r, g, b))
|
|
183
|
+
return r, g, b
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def rgb2hsv(rgb: RGBVectorLike) -> Float3Tuple:
|
|
187
|
+
r, g, b = (x / 255.0 for x in rgb)
|
|
188
|
+
m, v = sorted([r, g, b])[::2]
|
|
189
|
+
delta = v - m
|
|
190
|
+
if delta == 0:
|
|
191
|
+
h = 0
|
|
192
|
+
elif v == r:
|
|
193
|
+
h = (g - b) / delta % 6
|
|
194
|
+
elif v == g:
|
|
195
|
+
h = (b - r) / delta + 2
|
|
196
|
+
else:
|
|
197
|
+
h = (r - g) / delta + 4
|
|
198
|
+
h *= 60
|
|
199
|
+
if h < 0:
|
|
200
|
+
h += 360
|
|
201
|
+
s = 0 if v == 0 else delta / v
|
|
202
|
+
return h, s, v
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def lab2rgb(lab: Float3Tuple | FloatSequence) -> Int3Tuple:
|
|
206
|
+
xyz = lab2xyz(lab)
|
|
207
|
+
return xyz2rgb(np.array(xyz, dtype=np.float64))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def rgb2lab(rgb: RGBVectorLike) -> Float3Tuple:
|
|
211
|
+
xyz = rgb2xyz(rgb)
|
|
212
|
+
return xyz2lab(xyz)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
ANSI_4BIT_RGB: Final[list[Int3Tuple]] = [
|
|
216
|
+
(0, 0, 0), # black
|
|
217
|
+
(170, 0, 0), # red
|
|
218
|
+
(0, 170, 0), # green
|
|
219
|
+
(170, 85, 0), # yellow
|
|
220
|
+
(0, 0, 170), # blue
|
|
221
|
+
(170, 0, 170), # magenta
|
|
222
|
+
(0, 170, 170), # cyan
|
|
223
|
+
(170, 170, 170), # white
|
|
224
|
+
(85, 85, 85), # bright black (grey)
|
|
225
|
+
(255, 85, 85), # bright red
|
|
226
|
+
(85, 255, 85), # bright green
|
|
227
|
+
(255, 255, 85), # bright yellow
|
|
228
|
+
(85, 85, 255), # bright blue
|
|
229
|
+
(255, 85, 255), # bright magenta
|
|
230
|
+
(85, 255, 255), # bright cyan
|
|
231
|
+
(255, 255, 255) # bright white
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def ansi_4bit_to_rgb(value: int):
|
|
236
|
+
offset = 0
|
|
237
|
+
if value > 37:
|
|
238
|
+
if value <= 47:
|
|
239
|
+
offset -= 10
|
|
240
|
+
elif value <= 97:
|
|
241
|
+
offset += 8
|
|
242
|
+
else:
|
|
243
|
+
offset -= 2
|
|
244
|
+
value %= 30
|
|
245
|
+
value += offset
|
|
246
|
+
return ANSI_4BIT_RGB[value]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _4b_lookup():
|
|
250
|
+
def rgb_dist(rgb, ansi):
|
|
251
|
+
r_mean = (rgb[:, 0:1] + ansi[:, 0]) / 2
|
|
252
|
+
r_diff = (rgb[:, 0:1] - ansi[:, 0]) * (2 + r_mean / 256)
|
|
253
|
+
g_diff = (rgb[:, 1:2] - ansi[:, 1]) * 4
|
|
254
|
+
b_diff = (rgb[:, 2:3] - ansi[:, 2]) * (2 + (255 - r_mean) / 256)
|
|
255
|
+
return r_diff ** 2 + g_diff ** 2 + b_diff ** 2
|
|
256
|
+
|
|
257
|
+
rgb_4b_arr = np.asarray(ANSI_4BIT_RGB)
|
|
258
|
+
quants = np.stack(
|
|
259
|
+
np.meshgrid(*np.repeat(np.arange(32).reshape([1, -1]), 3, 0), indexing='ij'),
|
|
260
|
+
axis=-1).reshape([-1, 3])
|
|
261
|
+
rgb_colors = quants * 8
|
|
262
|
+
nearest_colors = rgb_4b_arr[np.argmin(rgb_dist(rgb_colors, rgb_4b_arr), axis=1)]
|
|
263
|
+
table = {
|
|
264
|
+
tuple(map(int, color)): tuple(map(int, nearest_colors[i]))
|
|
265
|
+
for i, color in enumerate(quants)}
|
|
266
|
+
return cast(dict[Int3Tuple, Int3Tuple], table)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
ANSI_4BIT_RGB_MAP = _4b_lookup()
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _quantize_rgb(rgb: RGBVectorLike):
|
|
273
|
+
r, g, b = rgb
|
|
274
|
+
return min(r >> 3, 0x1f), min(g >> 3, 0x1f), min(b >> 3, 0x1f)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def nearest_ansi_4bit_rgb(value: RGBVectorLike) -> Int3Tuple:
|
|
278
|
+
return ANSI_4BIT_RGB_MAP[_quantize_rgb(value)]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def nearest_ansi_8bit_rgb(value: RGBVectorLike) -> Int3Tuple:
|
|
282
|
+
try:
|
|
283
|
+
return ansi_8bit_to_rgb(rgb_to_ansi_8bit(value))
|
|
284
|
+
except ValueError:
|
|
285
|
+
raise ValueError(
|
|
286
|
+
f"invalid RGB value: {value!r}") from None
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def ansi_8bit_to_rgb(value: int):
|
|
290
|
+
if 0 <= value < 16:
|
|
291
|
+
return ANSI_4BIT_RGB[value]
|
|
292
|
+
elif value < 232:
|
|
293
|
+
value -= 16
|
|
294
|
+
return value // 36 * 51, (value % 36 // 6) * 51, (value % 6) * 51
|
|
295
|
+
elif value <= 255:
|
|
296
|
+
grey = 8 + (value - 232) * 10
|
|
297
|
+
return grey, grey, grey
|
|
298
|
+
raise ValueError(
|
|
299
|
+
f"expected an unsigned 8-bit integer, got {value}")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def rgb_to_ansi_8bit(rgb: RGBVectorLike) -> int:
|
|
303
|
+
if len(set(rgb)) == 1:
|
|
304
|
+
c = rgb[0]
|
|
305
|
+
if c < 8:
|
|
306
|
+
return 16
|
|
307
|
+
if c > 248:
|
|
308
|
+
return 231
|
|
309
|
+
return round((c - 8) / 247 * 24) + 232
|
|
310
|
+
r, g, b = (round((x / 255) * 5) for x in rgb)
|
|
311
|
+
return 16 + (36 * r) + (6 * g) + b
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def rgb_diff(rgb1: Int3Tuple, rgb2: Int3Tuple) -> Int3Tuple:
|
|
315
|
+
lab1, lab2 = map(rgb2lab, (rgb1, rgb2))
|
|
316
|
+
return lab2rgb([(i + j) / 2 for i, j in zip(lab1, lab2)])
|