chromatic-python 0.2.2__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/.gitignore +2 -2
  2. {chromatic_python-0.2.2/chromatic_python.egg-info → chromatic_python-0.3.1}/PKG-INFO +2 -2
  3. chromatic_python-0.3.1/chromatic/__init__.py +8 -0
  4. chromatic_python-0.3.1/chromatic/__init__.pyi +74 -0
  5. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/chromatic/_typing.py +73 -32
  6. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/chromatic/_version.py +2 -2
  7. chromatic_python-0.3.1/chromatic/color/__init__.py +3 -0
  8. chromatic_python-0.3.1/chromatic/color/__init__.pyi +86 -0
  9. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/chromatic/color/colorconv.py +85 -48
  10. chromatic_python-0.3.1/chromatic/color/core.py +1714 -0
  11. chromatic_python-0.3.1/chromatic/color/core.pyi +503 -0
  12. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/chromatic/color/iterators.py +13 -7
  13. chromatic_python-0.3.1/chromatic/color/palette.py +585 -0
  14. chromatic_python-0.3.1/chromatic/color/palette.pyi +302 -0
  15. chromatic_python-0.3.1/chromatic/data/__init__.py +33 -0
  16. chromatic_python-0.3.1/chromatic/data/__init__.pyi +16 -0
  17. chromatic_python-0.3.1/chromatic/data/_fetchers.py +55 -0
  18. chromatic_python-0.3.1/chromatic/data/registry.json +5 -0
  19. chromatic_python-0.3.1/chromatic/data/userfont.py +138 -0
  20. chromatic_python-0.3.1/chromatic/data/userfont.pyi +30 -0
  21. chromatic_python-0.3.1/chromatic/data/userfont.schema.json +28 -0
  22. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/chromatic/demo.py +186 -139
  23. chromatic_python-0.3.1/chromatic/image/__init__.pyi +70 -0
  24. {chromatic_python-0.2.2/chromatic/ascii → chromatic_python-0.3.1/chromatic/image}/_array.py +432 -353
  25. {chromatic_python-0.2.2/chromatic/ascii → chromatic_python-0.3.1/chromatic/image}/_curses.py +8 -4
  26. {chromatic_python-0.2.2/chromatic/ascii → chromatic_python-0.3.1/chromatic/image}/_glyph_proc.py +11 -13
  27. {chromatic_python-0.2.2 → chromatic_python-0.3.1/chromatic_python.egg-info}/PKG-INFO +2 -2
  28. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/chromatic_python.egg-info/SOURCES.txt +16 -9
  29. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/chromatic_python.egg-info/requires.txt +1 -0
  30. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/pyproject.toml +0 -2
  31. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/requirements.txt +2 -1
  32. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/tests/test_color_str.py +41 -40
  33. chromatic_python-0.2.2/chromatic/__init__.py +0 -109
  34. chromatic_python-0.2.2/chromatic/color/__init__.py +0 -6
  35. chromatic_python-0.2.2/chromatic/color/core.py +0 -1731
  36. chromatic_python-0.2.2/chromatic/color/core.pyi +0 -359
  37. chromatic_python-0.2.2/chromatic/color/palette.py +0 -554
  38. chromatic_python-0.2.2/chromatic/color/palette.pyi +0 -304
  39. chromatic_python-0.2.2/chromatic/data/__init__.py +0 -188
  40. chromatic_python-0.2.2/chromatic/data/__init__.pyi +0 -15
  41. chromatic_python-0.2.2/chromatic/data/images/hotdog.jpg +0 -0
  42. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/.gitattributes +0 -0
  43. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/LICENSE +0 -0
  44. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/README.md +0 -0
  45. {chromatic_python-0.2.2/chromatic/data/images → chromatic_python-0.3.1/chromatic/data}/butterfly.jpg +0 -0
  46. {chromatic_python-0.2.2/chromatic/data/images → chromatic_python-0.3.1/chromatic/data}/escher.png +0 -0
  47. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/chromatic/data/fonts/consolas.ttf +0 -0
  48. /chromatic_python-0.2.2/chromatic/data/fonts/IBM_VGA_437_8x16.ttf → /chromatic_python-0.3.1/chromatic/data/fonts/vga437.ttf +0 -0
  49. {chromatic_python-0.2.2/chromatic/data/images → chromatic_python-0.3.1/chromatic/data}/goblin_virus.png +0 -0
  50. {chromatic_python-0.2.2/chromatic/ascii → chromatic_python-0.3.1/chromatic/image}/__init__.py +0 -0
  51. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/chromatic_python.egg-info/dependency_links.txt +0 -0
  52. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/chromatic_python.egg-info/top_level.txt +0 -0
  53. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/logo/logo.ANS +0 -0
  54. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/logo/logo.PNG +0 -0
  55. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/setup.cfg +0 -0
  56. {chromatic_python-0.2.2 → chromatic_python-0.3.1}/tests/__init__.py +0 -0
@@ -5,7 +5,7 @@ __pycache__/
5
5
  /dist/
6
6
  /docs/
7
7
  /tests/_*
8
- /chromatic/data/*.json
9
- /chromatic/data/*.pyi
8
+ /chromatic/data/userfont.json
10
9
  /chromatic/_version.py
11
10
  /*.egg-info/
11
+ /build/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chromatic-python
3
- Version: 0.2.2
3
+ Version: 0.3.1
4
4
  Summary: ANSI art image processing and colored terminal text
5
5
  Author: crypt0lith
6
6
  License: MIT License
@@ -27,7 +27,6 @@ License: MIT License
27
27
  Project-URL: Homepage, https://github.com/crypt0lith/chromatic
28
28
  Keywords: ansi,ascii,art,font,image,terminal,parser
29
29
  Classifier: Programming Language :: Python :: 3.12
30
- Classifier: License :: OSI Approved :: MIT License
31
30
  Classifier: Operating System :: OS Independent
32
31
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
33
32
  Classifier: Typing :: Typed
@@ -42,6 +41,7 @@ Requires-Dist: scikit-image~=0.25.2
42
41
  Requires-Dist: scikit-learn~=1.6.1
43
42
  Requires-Dist: fonttools~=4.58.0
44
43
  Requires-Dist: scipy~=1.15.3
44
+ Requires-Dist: lazy_loader~=0.4
45
45
  Dynamic: license-file
46
46
 
47
47
  ![image](/logo/logo.PNG)
@@ -0,0 +1,8 @@
1
+ try:
2
+ from ._version import version as __version__
3
+ except ImportError:
4
+ __version__ = "0.0.0"
5
+
6
+ import lazy_loader as _lazy
7
+
8
+ __getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__)
@@ -0,0 +1,74 @@
1
+ __all__ = [
2
+ 'AnsiImage',
3
+ 'Back',
4
+ 'Color',
5
+ 'ColorNamespace',
6
+ 'ColorStr',
7
+ 'Fore',
8
+ 'SgrParameter',
9
+ 'Style',
10
+ '__version__',
11
+ 'ansi2img',
12
+ 'ansi_quantize',
13
+ 'ansicolor24Bit',
14
+ 'ansicolor4Bit',
15
+ 'ansicolor8Bit',
16
+ 'ansify',
17
+ 'image',
18
+ 'ascii2img',
19
+ 'ascii_printable',
20
+ 'color',
21
+ 'colorbytes',
22
+ 'contrast_stretch',
23
+ 'cp437_printable',
24
+ 'data',
25
+ 'equalize_white_point',
26
+ 'get_font_key',
27
+ 'get_font_object',
28
+ 'get_glyph_masks',
29
+ 'img2ansi',
30
+ 'img2ascii',
31
+ 'named_color',
32
+ 'read_ans',
33
+ 'register_userfont',
34
+ 'render_ans',
35
+ 'reshape_ansi',
36
+ 'to_sgr_array',
37
+ ]
38
+ from . import color, data, image
39
+ from ._version import version as __version__
40
+ from .color import (
41
+ Back,
42
+ Color,
43
+ ColorNamespace,
44
+ ColorStr,
45
+ Fore,
46
+ SgrParameter,
47
+ Style,
48
+ ansicolor24Bit,
49
+ ansicolor4Bit,
50
+ ansicolor8Bit,
51
+ colorbytes,
52
+ named_color,
53
+ )
54
+ from .data import register_userfont
55
+ from .image import (
56
+ AnsiImage,
57
+ ansi2img,
58
+ ansi_quantize,
59
+ ansify,
60
+ ascii2img,
61
+ ascii_printable,
62
+ contrast_stretch,
63
+ cp437_printable,
64
+ equalize_white_point,
65
+ get_font_key,
66
+ get_font_object,
67
+ img2ansi,
68
+ img2ascii,
69
+ read_ans,
70
+ render_ans,
71
+ reshape_ansi,
72
+ to_sgr_array,
73
+ )
74
+ from .image._glyph_proc import get_glyph_masks
@@ -14,12 +14,13 @@ from typing import (
14
14
  Concatenate,
15
15
  Hashable,
16
16
  Iterable,
17
- Literal,
17
+ Literal as L,
18
18
  NamedTuple,
19
19
  Optional,
20
20
  ParamSpec,
21
21
  Protocol,
22
22
  Sequence,
23
+ TYPE_CHECKING,
23
24
  Type,
24
25
  TypeAlias,
25
26
  TypeAliasType,
@@ -40,7 +41,8 @@ from PIL.ImageFont import FreeTypeFont
40
41
  from numpy import dtype, float64, generic, ndarray, number, uint8
41
42
  from numpy._typing import NDArray, _ArrayLike
42
43
 
43
- from chromatic.data import UserFont
44
+ if TYPE_CHECKING:
45
+ from .data import UserFont
44
46
 
45
47
  _P = ParamSpec('_P')
46
48
  _T = TypeVar('_T')
@@ -48,11 +50,15 @@ _T_co = TypeVar('_T_co', covariant=True)
48
50
  _T_contra = TypeVar('_T_contra', contravariant=True)
49
51
  _AnyNumber_co = TypeVar('_AnyNumber_co', number, Number, covariant=True)
50
52
 
51
- type ArrayReducerFunc[_SCT: generic] = Callable[Concatenate[_ArrayLike[_SCT], _P], NDArray[_SCT]]
52
- type ShapedNDArray[_Shape: tuple[int, ...], _SCT: generic] = ndarray[_Shape, dtype[_SCT]]
53
+ type ArrayReducerFunc[_SCT: generic] = Callable[
54
+ Concatenate[_ArrayLike[_SCT], _P], NDArray[_SCT]
55
+ ]
56
+ type ShapedNDArray[_Shape: tuple[int, ...], _SCT = generic] = ndarray[
57
+ _Shape, dtype[_SCT]
58
+ ]
53
59
  type MatrixLike[_SCT: generic] = ShapedNDArray[TupleOf2[int], _SCT]
54
60
  type SquareMatrix[_I: int, _SCT: generic] = ShapedNDArray[TupleOf2[_I], _SCT]
55
- type GlyphArray[_SCT: generic] = SquareMatrix[Literal[24], _SCT]
61
+ type GlyphArray[_SCT: generic] = SquareMatrix[L[24], _SCT]
56
62
  type TupleOf2[_T] = tuple[_T, _T]
57
63
  type TupleOf3[_T] = tuple[_T, _T, _T]
58
64
 
@@ -64,25 +70,45 @@ GlyphBitmask: TypeAlias = GlyphArray[bool]
64
70
  Bitmask: TypeAlias = MatrixLike[bool]
65
71
  GreyscaleGlyphArray: TypeAlias = GlyphArray[float64]
66
72
  GreyscaleArray: TypeAlias = MatrixLike[float64]
67
- RGBArray: TypeAlias = ShapedNDArray[tuple[int, int, Literal[3]], uint8]
68
- RGBPixel: TypeAlias = ShapedNDArray[tuple[Literal[3]], uint8]
73
+ RGBArray: TypeAlias = ShapedNDArray[tuple[int, int, L[3]], uint8]
74
+ RGBPixel: TypeAlias = ShapedNDArray[tuple[L[3]], uint8]
69
75
 
70
76
  RGBImageLike: TypeAlias = Image | RGBArray
71
- RGBVectorLike: TypeAlias = Int3Tuple | IntSequence | RGBPixel
72
- ColorDictKeys = Literal['fg', 'bg']
73
- Ansi4BitAlias = Literal['4b']
74
- Ansi8BitAlias = Literal['8b']
75
- Ansi24BitAlias = Literal['24b']
77
+ RGBVectorLike: TypeAlias = IntSequence | RGBPixel
78
+ ColorDictKeys = L['fg', 'bg']
79
+ Ansi4BitAlias = L['4b']
80
+ Ansi8BitAlias = L['8b']
81
+ Ansi24BitAlias = L['24b']
76
82
  AnsiColorAlias = Ansi4BitAlias | Ansi8BitAlias | Ansi24BitAlias
77
- FontArgType: TypeAlias = FreeTypeFont | UserFont | str | int
83
+ FontArgType: TypeAlias = 'FreeTypeFont | UserFont | str'
84
+
85
+
86
+ def eval_annotation(annotation: str, **kwargs) -> Any:
87
+ globals_ = kwargs.get('globals', {}) | globals()
88
+ locals_ = kwargs.get('locals', {})
89
+ try:
90
+ return subtype(eval(annotation, globals_.copy(), locals_.copy()))
91
+ except NameError as e:
92
+ try:
93
+ import typing
94
+
95
+ globals_[e.name] = getattr(typing, e.name)
96
+ return eval_annotation(annotation, globals=globals_, locals=locals_)
97
+ except AttributeError:
98
+ pass
99
+ raise
78
100
 
79
101
 
80
102
  def type_error_msg(err_obj, *expected, context: str = '', obj_repr=False):
81
103
  n_expected = len(expected)
82
- name_slots = ['{%d.__qualname__!r}' % n for n in range(n_expected)]
83
- if name_slots and n_expected > 1:
104
+ name_slots = ["{%d.__name__!r}" % n for n in range(n_expected)]
105
+ if n_expected > 1:
84
106
  name_slots[-1] = f"or {name_slots[-1]}"
85
- names = (', ' if n_expected > 2 else ' ').join([context.strip()] + name_slots).format(*expected)
107
+ names = (
108
+ (', ' if n_expected > 2 else ' ')
109
+ .join([context.strip(), *name_slots])
110
+ .format(*expected)
111
+ )
86
112
  if not obj_repr:
87
113
  if not isinstance(err_obj, type):
88
114
  err_obj = type(err_obj)
@@ -100,11 +126,14 @@ def is_matching_type(value, typ):
100
126
  origin, args = deconstruct_type(typ)
101
127
  if origin is Union:
102
128
  return any(is_matching_type(value, arg) for arg in args)
103
- elif origin is Literal:
129
+ elif origin is L:
104
130
  return value in args
105
131
  elif isinstance(typ, TypeVar):
106
132
  if typ.__constraints__:
107
- return any(is_matching_type(value, constraint) for constraint in typ.__constraints__)
133
+ return any(
134
+ is_matching_type(value, constraint)
135
+ for constraint in typ.__constraints__
136
+ )
108
137
  else:
109
138
  return True
110
139
  elif origin is type:
@@ -162,7 +191,9 @@ def is_matching_typed_dict(__d: dict, typed_dict: type[dict]) -> tuple[bool, str
162
191
  field = __d.get(name)
163
192
  if field is None or is_matching_type(field, typ):
164
193
  continue
165
- return False, type_error_msg(field, typ, context=f'keyword argument {name!r} of type')
194
+ return False, type_error_msg(
195
+ field, typ, context=f'keyword argument {name!r} of type'
196
+ )
166
197
  return True, ''
167
198
 
168
199
 
@@ -188,20 +219,20 @@ def unionize(__iterable: Iterable[SupportsUnion[_T_contra, _T_co]]) -> _T_co:
188
219
 
189
220
  _GenericAlias = type(Type[...]) | types.GenericAlias
190
221
  _UnionGenericType = type(Union[..., None])
191
- _LiteralGenericType = type(Literal[''])
222
+ _LiteralGenericType = type(L[''])
192
223
  _CallableGenericType = type(Callable[[], ...]) | type(ABC_Callable[[], ...])
193
224
  _CallableType = type(Callable) | ABC_Callable
194
225
 
195
226
 
196
227
  class _BoundedDict[_KT, _VT](OrderedDict[_KT, _VT]):
228
+ """Bounded OrderedDict, mimics FIFO behavior of `functools.lru_cache`"""
197
229
 
198
230
  def __init__(self, *, maxsize: Optional[int] = 128):
199
- """Bounded OrderedDict, mimics FIFO behavior of `functools.lru_cache`"""
200
231
  super().__init__()
201
232
  if maxsize is not None:
202
233
  maxsize = max(maxsize, 0)
203
234
 
204
- @wraps(self.__setitem__)
235
+ @wraps(_BoundedDict.__setitem__)
205
236
  def _fifo(_, key, value):
206
237
  if maxsize <= len(self):
207
238
  self.popitem(last=True)
@@ -213,9 +244,9 @@ class _BoundedDict[_KT, _VT](OrderedDict[_KT, _VT]):
213
244
 
214
245
 
215
246
  _SUBTYPE_CACHE: _BoundedDict[int, ...] = _BoundedDict()
216
- _ATTR_GETTERS: _BoundedDict[..., tuple[Callable[[Iterable], NamedTuple], op.attrgetter]] = (
217
- _BoundedDict()
218
- )
247
+ _ATTR_GETTERS: _BoundedDict[
248
+ ..., tuple[Callable[[Iterable], NamedTuple], op.attrgetter]
249
+ ] = _BoundedDict()
219
250
 
220
251
 
221
252
  def _unique_attrs(obj) -> Optional['NamedTuple']:
@@ -260,7 +291,8 @@ def _sort_attrs(obj, tp_name, attr_names):
260
291
  try:
261
292
  sig = inspect.signature(type(obj))
262
293
  indices = (
263
- dict.fromkeys(field_names, inf) | {p: i for i, p in enumerate(sig.parameters)}
294
+ dict.fromkeys(field_names, inf)
295
+ | {p: i for i, p in enumerate(sig.parameters)}
264
296
  ).values()
265
297
  for names in (attr_names, field_names):
266
298
  names.sort(key=dict(zip(names, indices)).__getitem__)
@@ -282,7 +314,9 @@ def _sort_attrs(obj, tp_name, attr_names):
282
314
  while sig_start not in line:
283
315
  line = next(lines)
284
316
  _, _, params = line.partition(sig_start)
285
- params, _, _ = (s.translate(no_square_parens) for s in params.partition(')'))
317
+ params, _, _ = (
318
+ s.translate(no_square_parens) for s in params.partition(')')
319
+ )
286
320
  maybe_sigs.add(params)
287
321
  except StopIteration:
288
322
  break
@@ -290,15 +324,20 @@ def _sort_attrs(obj, tp_name, attr_names):
290
324
  if maybe_sigs:
291
325
  if len(maybe_sigs) > 1:
292
326
  sig = max(
293
- maybe_sigs, key=lambda s: sum(1 for sub in s.split(', ') if sub in field_names)
327
+ maybe_sigs,
328
+ key=lambda s: sum(1 for sub in s.split(', ') if sub in field_names),
294
329
  )
295
330
  else:
296
331
  sig = maybe_sigs.pop()
297
332
  positions = {x: i for i, x in enumerate(sig.split(', ')) if x}
298
333
  sorted_field_names = sorted(field_names, key=lambda k: positions.get(k, inf))
299
- transitions = {idx: sorted_field_names.index(x) for idx, x in enumerate(field_names)}
334
+ transitions = {
335
+ idx: sorted_field_names.index(x) for idx, x in enumerate(field_names)
336
+ }
300
337
  field_names = sorted_field_names
301
- attr_names = [attr_names[k] for k in map(transitions.__getitem__, range(len(attr_names)))]
338
+ attr_names = [
339
+ attr_names[k] for k in map(transitions.__getitem__, range(len(attr_names)))
340
+ ]
302
341
  for Names in (attr_names, field_names):
303
342
  name_attr = next((s for s in Names if s.strip('_') == 'name'), None)
304
343
  if name_attr is not None:
@@ -335,7 +374,9 @@ def subtype[_T](typ: _T) -> _T:
335
374
  args_list = list(args)
336
375
  if (
337
376
  literals := [
338
- idx for idx, elem in enumerate(args) if isinstance(elem, _LiteralGenericType)
377
+ idx
378
+ for idx, elem in enumerate(args)
379
+ if isinstance(elem, _LiteralGenericType)
339
380
  ]
340
381
  ) and len(literals) > 1:
341
382
 
@@ -345,7 +386,7 @@ def subtype[_T](typ: _T) -> _T:
345
386
  return getattr(value, '__args__', ())
346
387
 
347
388
  start = args[literals.pop(0)]
348
- args_list[args_list.index(start)] = Literal[
389
+ args_list[args_list.index(start)] = L[
349
390
  *start.__args__,
350
391
  *dict.fromkeys(arg for idx in literals for arg in _next_args(idx)),
351
392
  ]
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.2.2'
21
- __version_tuple__ = version_tuple = (0, 2, 2)
20
+ __version__ = version = '0.3.1'
21
+ __version_tuple__ = version_tuple = (0, 3, 1)
@@ -0,0 +1,3 @@
1
+ import lazy_loader as _lazy
2
+
3
+ __getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__)
@@ -0,0 +1,86 @@
1
+ __all__ = [
2
+ 'ANSI_4BIT_RGB',
3
+ 'Back',
4
+ 'CSI',
5
+ 'Color',
6
+ 'ColorNamespace',
7
+ 'ColorStr',
8
+ 'Fore',
9
+ 'SGR_RESET',
10
+ 'SgrParameter',
11
+ 'SgrSequence',
12
+ 'Style',
13
+ 'ansi_4bit_to_rgb',
14
+ 'ansi_8bit_to_rgb',
15
+ 'ansicolor24Bit',
16
+ 'ansicolor4Bit',
17
+ 'ansicolor8Bit',
18
+ 'color_chain',
19
+ 'colorbytes',
20
+ 'get_ansi_type',
21
+ 'int2rgb',
22
+ 'hexstr2rgb',
23
+ 'hsl2rgb',
24
+ 'hsv2rgb',
25
+ 'is_u24',
26
+ 'lab2rgb',
27
+ 'lab2xyz',
28
+ 'named_color',
29
+ 'nearest_ansi_4bit_rgb',
30
+ 'nearest_ansi_8bit_rgb',
31
+ 'randcolor',
32
+ 'rgb2ansi_escape',
33
+ 'rgb2int',
34
+ 'rgb2hexstr',
35
+ 'rgb2hsl',
36
+ 'rgb2hsv',
37
+ 'rgb2lab',
38
+ 'rgb2xyz',
39
+ 'rgb_diff',
40
+ 'rgb_dispatch',
41
+ 'rgb_to_ansi_8bit',
42
+ 'xyz2lab',
43
+ 'xyz2rgb',
44
+ ]
45
+ from . import colorconv, core, iterators, palette
46
+ from .colorconv import (
47
+ ANSI_4BIT_RGB,
48
+ ansi_4bit_to_rgb,
49
+ ansi_8bit_to_rgb,
50
+ int2rgb,
51
+ hexstr2rgb,
52
+ hsl2rgb,
53
+ hsv2rgb,
54
+ is_u24,
55
+ lab2rgb,
56
+ lab2xyz,
57
+ nearest_ansi_4bit_rgb,
58
+ nearest_ansi_8bit_rgb,
59
+ rgb2int,
60
+ rgb2hexstr,
61
+ rgb2hsl,
62
+ rgb2hsv,
63
+ rgb2lab,
64
+ rgb2xyz,
65
+ rgb_diff,
66
+ rgb_to_ansi_8bit,
67
+ xyz2lab,
68
+ xyz2rgb,
69
+ )
70
+ from .core import (
71
+ CSI,
72
+ Color,
73
+ ColorStr,
74
+ SGR_RESET,
75
+ SgrParameter,
76
+ SgrSequence,
77
+ ansicolor24Bit,
78
+ ansicolor4Bit,
79
+ ansicolor8Bit,
80
+ color_chain,
81
+ colorbytes,
82
+ get_ansi_type,
83
+ randcolor,
84
+ rgb2ansi_escape,
85
+ )
86
+ from .palette import Back, ColorNamespace, Fore, Style, named_color, rgb_dispatch
@@ -2,16 +2,16 @@ __all__ = [
2
2
  'ANSI_4BIT_RGB',
3
3
  'ansi_4bit_to_rgb',
4
4
  'ansi_8bit_to_rgb',
5
- 'hex2rgb',
5
+ 'int2rgb',
6
6
  'hexstr2rgb',
7
7
  'hsl2rgb',
8
8
  'hsv2rgb',
9
- 'is_hex_rgb',
9
+ 'is_u24',
10
10
  'lab2rgb',
11
11
  'lab2xyz',
12
12
  'nearest_ansi_4bit_rgb',
13
13
  'nearest_ansi_8bit_rgb',
14
- 'rgb2hex',
14
+ 'rgb2int',
15
15
  'rgb2hexstr',
16
16
  'rgb2hsl',
17
17
  'rgb2hsv',
@@ -23,40 +23,76 @@ __all__ = [
23
23
  'xyz2rgb',
24
24
  ]
25
25
 
26
+ from functools import lru_cache
26
27
  from operator import mul, truediv
27
- from typing import Final, Literal, SupportsInt, cast
28
+ from typing import Final, Literal, SupportsInt, TypeGuard
28
29
 
29
30
  import numpy as np
30
31
 
31
- from .._typing import Float3Tuple, FloatSequence, Int3Tuple, RGBPixel, RGBVectorLike, ShapedNDArray
32
-
33
-
34
- def is_hex_rgb(value, *, strict: bool = False):
35
- if issubclass(type(value), SupportsInt):
36
- if 0x0 <= int(value) <= 0xFFFFFF:
32
+ from .._typing import (
33
+ Float3Tuple,
34
+ FloatSequence,
35
+ Int3Tuple,
36
+ RGBPixel,
37
+ RGBVectorLike,
38
+ ShapedNDArray,
39
+ )
40
+
41
+
42
+ @lru_cache
43
+ def _supports_int(typ: type) -> TypeGuard[type[SupportsInt]]:
44
+ return issubclass(typ, SupportsInt)
45
+
46
+
47
+ def is_u24(value, *, strict: bool = False):
48
+ """Check if value is an unsigned 24-bit integer.
49
+
50
+ Parameters
51
+ ---------
52
+ value
53
+ Input number
54
+ strict : bool
55
+ Whether to return False or raise ValueError on failure
56
+
57
+ Raises
58
+ ------
59
+ ValueError
60
+ Raised when `strict=True` and value is not u24
61
+ """
62
+ if _supports_int(type(value)):
63
+ if 0 <= int(value) <= 0xFFFFFF:
37
64
  return True
38
65
  elif not strict:
39
66
  return False
40
- raise TypeError(f"{value!r} is not a valid RGB color") from None
67
+ raise ValueError(f"{value!r} is not u24")
41
68
 
42
69
 
43
70
  def hexstr2rgb(__str: str) -> Int3Tuple:
44
- if is_hex_rgb(value := int(__str, 16), strict=True):
45
- return hex2rgb(value)
71
+ n = len(__str)
72
+ if n % 4 == 0: # trunc alpha
73
+ n *= 3
74
+ n //= 4
75
+ __str = __str[:n]
76
+ if n == 3: # rgb -> rrggbb
77
+ __str = ''.join(c * 2 for c in __str)
78
+ if is_u24(value := int(__str, 16), strict=True):
79
+ return int2rgb(value)
46
80
 
47
81
 
48
82
  def rgb2hexstr(rgb: RGBVectorLike) -> str:
49
- r, g, b = rgb
50
- return f'{r:02x}{g:02x}{b:02x}'
83
+ return "%02x%02x%02x" % tuple(rgb)
51
84
 
52
85
 
53
- def rgb2hex(rgb: RGBVectorLike) -> int:
86
+ def rgb2int(rgb: RGBVectorLike) -> int:
54
87
  r, g, b = map(int, rgb)
55
88
  return r << 16 | g << 8 | b
56
89
 
57
90
 
58
- def hex2rgb(value: int) -> Int3Tuple:
59
- return (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF
91
+ def int2rgb(__x: int) -> Int3Tuple:
92
+ try:
93
+ return getattr(__x, 'rgb')
94
+ except AttributeError:
95
+ return (__x >> 16) & 0xFF, (__x >> 8) & 0xFF, __x & 0xFF
60
96
 
61
97
 
62
98
  def xyz2lab(xyz: FloatSequence) -> Float3Tuple:
@@ -78,7 +114,10 @@ def lab2xyz(lab: FloatSequence) -> Float3Tuple:
78
114
  x, y, z = map(
79
115
  mul,
80
116
  (95.047, 100.0, 108.883),
81
- map(lambda n: (lambda c: c if c > 0.008856 else (n - 16 / 116) / 7.787)(n**3), (x, y, z)),
117
+ map(
118
+ lambda i: (lambda j: j if j > 0.008856 else (i - 16 / 116) / 7.787)(i**3),
119
+ (x, y, z),
120
+ ),
82
121
  )
83
122
  return x, y, z
84
123
 
@@ -98,47 +137,44 @@ def rgb2xyz(rgb: RGBPixel) -> Float3Tuple:
98
137
 
99
138
 
100
139
  def xyz2rgb(xyz: ShapedNDArray[tuple[Literal[3]], np.float64]) -> Int3Tuple:
101
- r, g, b = (np.clip(M_XYZ2RGB @ np.array(xyz, dtype=np.float64), 0.0, 1.0) * 255.0).astype(int)
140
+ r, g, b = (
141
+ np.clip(M_XYZ2RGB @ np.array(xyz, dtype=np.float64), 0.0, 1.0) * 255.0
142
+ ).astype(int)
102
143
  return r, g, b
103
144
 
104
145
 
105
146
  def hsl2rgb(hsl: FloatSequence) -> Int3Tuple:
106
147
  h, s, L = hsl
107
- h = (h / 360) % 1
148
+ h /= 360
149
+ h %= 1
108
150
  if h < 0:
109
151
  h += 1
110
- r = g = b = L
111
152
  v = (L * (1.0 + s)) if L <= 0.5 else (L + s - L * s)
112
- if v > 0:
153
+ if v > 0 and 0 <= (sextant := int(h)) <= 5:
113
154
  m = L + L - v
114
155
  sv = (v - m) / v
115
156
  h *= 6.0
116
- sextant = int(h)
117
- fract = h - sextant
118
- vsf = v * sv * fract
157
+ vsf = v * sv * (h - sextant)
119
158
  mid1 = m + vsf
120
159
  mid2 = v - vsf
121
- if sextant == 0:
122
- r, g, b = v, mid1, m
123
- elif sextant == 1:
124
- r, g, b = mid2, v, m
125
- elif sextant == 2:
126
- r, g, b = m, v, mid1
127
- elif sextant == 3:
128
- r, g, b = m, mid2, v
129
- elif sextant == 4:
130
- r, g, b = mid1, m, v
131
- elif sextant == 5:
132
- r, g, b = v, m, mid2
133
- r, g, b = (round(x * 255) for x in (r, g, b))
160
+ r, g, b = (
161
+ round(x * 0xFF)
162
+ for x in [
163
+ [v, mid1, m],
164
+ [mid2, v, m],
165
+ [m, v, mid1],
166
+ [m, mid2, v],
167
+ [mid1, m, v],
168
+ ][sextant]
169
+ )
134
170
  else:
135
- r, g, b = (round(L * 255) for _ in range(3))
171
+ r, g, b = [round(L * 0xFF)] * 3
136
172
  return r, g, b
137
173
 
138
174
 
139
175
  def rgb2hsl(rgb: RGBVectorLike) -> Float3Tuple:
140
176
  r, g, b = (x / 255.0 for x in rgb)
141
- m, v = sorted([r, g, b])[::2]
177
+ m, _, v = sorted([r, g, b])
142
178
  L = (m + v) / 2
143
179
  h = s = 0
144
180
  if L > 0:
@@ -245,7 +281,7 @@ def ansi_4bit_to_rgb(value: int):
245
281
  return ANSI_4BIT_RGB[value]
246
282
 
247
283
 
248
- def _4b_lookup():
284
+ def _4b_lookup() -> dict[Int3Tuple, Int3Tuple]:
249
285
  def rgb_dist(rgb, ansi):
250
286
  r_mean = (rgb[:, 0:1] + ansi[:, 0]) / 2
251
287
  r_diff = (rgb[:, 0:1] - ansi[:, 0]) * (2 + r_mean / 256)
@@ -255,14 +291,15 @@ def _4b_lookup():
255
291
 
256
292
  rgb_4b_arr = np.asarray(ANSI_4BIT_RGB)
257
293
  quants = np.stack(
258
- np.meshgrid(*np.repeat(np.arange(32).reshape([1, -1]), 3, 0), indexing='ij'), axis=-1
294
+ np.meshgrid(*np.repeat(np.arange(32).reshape([1, -1]), 3, 0), indexing='ij'),
295
+ axis=-1,
259
296
  ).reshape([-1, 3])
260
- rgb_colors = quants * 8
261
- nearest_colors = rgb_4b_arr[np.argmin(rgb_dist(rgb_colors, rgb_4b_arr), axis=1)]
262
- table = {
263
- tuple(map(int, color)): tuple(map(int, nearest_colors[i])) for i, color in enumerate(quants)
297
+ nearest_colors = rgb_4b_arr[np.argmin(rgb_dist(quants * 8, rgb_4b_arr), axis=1)]
298
+ table: dict = {
299
+ tuple(map(int, color)): tuple(map(int, nearest_colors[i]))
300
+ for i, color in enumerate(quants)
264
301
  }
265
- return cast(dict[Int3Tuple, Int3Tuple], table)
302
+ return table
266
303
 
267
304
 
268
305
  ANSI_4BIT_RGB_MAP = _4b_lookup()