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.
@@ -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,6 @@
1
+ from . import core, palette
2
+ from .colorconv import *
3
+ from .core import *
4
+ from .palette import *
5
+
6
+ __all__ = list(set(core.__all__) | set(colorconv.__all__) | set(palette.__all__))
@@ -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)])