chromatic-python 0.2.2__py3-none-any.whl → 0.2.3__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 CHANGED
@@ -52,7 +52,9 @@ try:
52
52
  from setuptools import find_packages
53
53
  from pkgutil import iter_modules
54
54
 
55
- tree: dict[str, dict | ModuleType] = {__name__: {'__module__': sys.modules[__name__]}}
55
+ tree: dict[str, dict | ModuleType] = {
56
+ __name__: {'__module__': sys.modules[__name__]}
57
+ }
56
58
  children = set()
57
59
  for pkg in find_packages(path):
58
60
  children.add(pkg)
@@ -93,7 +95,11 @@ try:
93
95
 
94
96
  @wraps(submodule.__dir__)
95
97
  def wrapped():
96
- s = set(attr for attr in init_dir if is_local(getattr(submodule, attr, None)))
98
+ s = set(
99
+ attr
100
+ for attr in init_dir
101
+ if is_local(getattr(submodule, attr, None))
102
+ )
97
103
  if hasattr(submodule, '__all__'):
98
104
  s.update(submodule.__all__)
99
105
  return list(s)
chromatic/_typing.py CHANGED
@@ -48,8 +48,12 @@ _T_co = TypeVar('_T_co', covariant=True)
48
48
  _T_contra = TypeVar('_T_contra', contravariant=True)
49
49
  _AnyNumber_co = TypeVar('_AnyNumber_co', number, Number, covariant=True)
50
50
 
51
- type ArrayReducerFunc[_SCT: generic] = Callable[Concatenate[_ArrayLike[_SCT], _P], NDArray[_SCT]]
52
- type ShapedNDArray[_Shape: tuple[int, ...], _SCT: generic] = ndarray[_Shape, dtype[_SCT]]
51
+ type ArrayReducerFunc[_SCT: generic] = Callable[
52
+ Concatenate[_ArrayLike[_SCT], _P], NDArray[_SCT]
53
+ ]
54
+ type ShapedNDArray[_Shape: tuple[int, ...], _SCT: generic] = ndarray[
55
+ _Shape, dtype[_SCT]
56
+ ]
53
57
  type MatrixLike[_SCT: generic] = ShapedNDArray[TupleOf2[int], _SCT]
54
58
  type SquareMatrix[_I: int, _SCT: generic] = ShapedNDArray[TupleOf2[_I], _SCT]
55
59
  type GlyphArray[_SCT: generic] = SquareMatrix[Literal[24], _SCT]
@@ -82,7 +86,11 @@ def type_error_msg(err_obj, *expected, context: str = '', obj_repr=False):
82
86
  name_slots = ['{%d.__qualname__!r}' % n for n in range(n_expected)]
83
87
  if name_slots and n_expected > 1:
84
88
  name_slots[-1] = f"or {name_slots[-1]}"
85
- names = (', ' if n_expected > 2 else ' ').join([context.strip()] + name_slots).format(*expected)
89
+ names = (
90
+ (', ' if n_expected > 2 else ' ')
91
+ .join([context.strip()] + name_slots)
92
+ .format(*expected)
93
+ )
86
94
  if not obj_repr:
87
95
  if not isinstance(err_obj, type):
88
96
  err_obj = type(err_obj)
@@ -104,7 +112,10 @@ def is_matching_type(value, typ):
104
112
  return value in args
105
113
  elif isinstance(typ, TypeVar):
106
114
  if typ.__constraints__:
107
- return any(is_matching_type(value, constraint) for constraint in typ.__constraints__)
115
+ return any(
116
+ is_matching_type(value, constraint)
117
+ for constraint in typ.__constraints__
118
+ )
108
119
  else:
109
120
  return True
110
121
  elif origin is type:
@@ -162,7 +173,9 @@ def is_matching_typed_dict(__d: dict, typed_dict: type[dict]) -> tuple[bool, str
162
173
  field = __d.get(name)
163
174
  if field is None or is_matching_type(field, typ):
164
175
  continue
165
- return False, type_error_msg(field, typ, context=f'keyword argument {name!r} of type')
176
+ return False, type_error_msg(
177
+ field, typ, context=f'keyword argument {name!r} of type'
178
+ )
166
179
  return True, ''
167
180
 
168
181
 
@@ -213,9 +226,9 @@ class _BoundedDict[_KT, _VT](OrderedDict[_KT, _VT]):
213
226
 
214
227
 
215
228
  _SUBTYPE_CACHE: _BoundedDict[int, ...] = _BoundedDict()
216
- _ATTR_GETTERS: _BoundedDict[..., tuple[Callable[[Iterable], NamedTuple], op.attrgetter]] = (
217
- _BoundedDict()
218
- )
229
+ _ATTR_GETTERS: _BoundedDict[
230
+ ..., tuple[Callable[[Iterable], NamedTuple], op.attrgetter]
231
+ ] = _BoundedDict()
219
232
 
220
233
 
221
234
  def _unique_attrs(obj) -> Optional['NamedTuple']:
@@ -260,7 +273,8 @@ def _sort_attrs(obj, tp_name, attr_names):
260
273
  try:
261
274
  sig = inspect.signature(type(obj))
262
275
  indices = (
263
- dict.fromkeys(field_names, inf) | {p: i for i, p in enumerate(sig.parameters)}
276
+ dict.fromkeys(field_names, inf)
277
+ | {p: i for i, p in enumerate(sig.parameters)}
264
278
  ).values()
265
279
  for names in (attr_names, field_names):
266
280
  names.sort(key=dict(zip(names, indices)).__getitem__)
@@ -282,7 +296,9 @@ def _sort_attrs(obj, tp_name, attr_names):
282
296
  while sig_start not in line:
283
297
  line = next(lines)
284
298
  _, _, params = line.partition(sig_start)
285
- params, _, _ = (s.translate(no_square_parens) for s in params.partition(')'))
299
+ params, _, _ = (
300
+ s.translate(no_square_parens) for s in params.partition(')')
301
+ )
286
302
  maybe_sigs.add(params)
287
303
  except StopIteration:
288
304
  break
@@ -290,15 +306,20 @@ def _sort_attrs(obj, tp_name, attr_names):
290
306
  if maybe_sigs:
291
307
  if len(maybe_sigs) > 1:
292
308
  sig = max(
293
- maybe_sigs, key=lambda s: sum(1 for sub in s.split(', ') if sub in field_names)
309
+ maybe_sigs,
310
+ key=lambda s: sum(1 for sub in s.split(', ') if sub in field_names),
294
311
  )
295
312
  else:
296
313
  sig = maybe_sigs.pop()
297
314
  positions = {x: i for i, x in enumerate(sig.split(', ')) if x}
298
315
  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)}
316
+ transitions = {
317
+ idx: sorted_field_names.index(x) for idx, x in enumerate(field_names)
318
+ }
300
319
  field_names = sorted_field_names
301
- attr_names = [attr_names[k] for k in map(transitions.__getitem__, range(len(attr_names)))]
320
+ attr_names = [
321
+ attr_names[k] for k in map(transitions.__getitem__, range(len(attr_names)))
322
+ ]
302
323
  for Names in (attr_names, field_names):
303
324
  name_attr = next((s for s in Names if s.strip('_') == 'name'), None)
304
325
  if name_attr is not None:
@@ -335,7 +356,9 @@ def subtype[_T](typ: _T) -> _T:
335
356
  args_list = list(args)
336
357
  if (
337
358
  literals := [
338
- idx for idx, elem in enumerate(args) if isinstance(elem, _LiteralGenericType)
359
+ idx
360
+ for idx, elem in enumerate(args)
361
+ if isinstance(elem, _LiteralGenericType)
339
362
  ]
340
363
  ) and len(literals) > 1:
341
364
 
chromatic/_version.py CHANGED
@@ -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.2.3'
21
+ __version_tuple__ = version_tuple = (0, 2, 3)
chromatic/ascii/_array.py CHANGED
@@ -122,7 +122,9 @@ def get_font_key(font: FreeTypeFont):
122
122
 
123
123
 
124
124
  @overload
125
- def get_font_object(font: FontArgType, *, retpath: Literal[False] = False) -> FreeTypeFont: ...
125
+ def get_font_object(
126
+ font: FontArgType, *, retpath: Literal[False] = False
127
+ ) -> FreeTypeFont: ...
126
128
 
127
129
 
128
130
  @overload
@@ -130,7 +132,9 @@ def get_font_object(font: FontArgType, *, retpath: Literal[True]) -> str: ...
130
132
 
131
133
 
132
134
  @lru_cache
133
- def get_font_object(font: FontArgType, *, retpath: bool = False) -> Union[FreeTypeFont, str]:
135
+ def get_font_object(
136
+ font: FontArgType, *, retpath: bool = False
137
+ ) -> Union[FreeTypeFont, str]:
134
138
  """Obtain a FreeTypeFont object or its filepath from various input types.
135
139
 
136
140
  Parameters
@@ -271,7 +275,12 @@ def render_font_str(__s: str, font: FontArgType):
271
275
  maxlen = max(map(len, lines))
272
276
  stacked = np.vstack(
273
277
  [
274
- np.hstack([np.array(render_font_char(c, font=font), dtype=np.uint8) for c in line])
278
+ np.hstack(
279
+ [
280
+ np.array(render_font_char(c, font=font), dtype=np.uint8)
281
+ for c in line
282
+ ]
283
+ )
275
284
  for line in map(lambda x: f'{x:<{maxlen}}', lines)
276
285
  ]
277
286
  )
@@ -279,7 +288,9 @@ def render_font_str(__s: str, font: FontArgType):
279
288
  return render_font_char(__s, font)
280
289
 
281
290
 
282
- def render_font_char(__c: str, font: FontArgType, size=(24, 24), fill: Int3Tuple = (255, 255, 255)):
291
+ def render_font_char(
292
+ __c: str, font: FontArgType, size=(24, 24), fill: Int3Tuple = (0xFF, 0xFF, 0xFF)
293
+ ):
283
294
  """Render a one-character string as an image.
284
295
 
285
296
  Parameters
@@ -314,7 +325,9 @@ def render_font_char(__c: str, font: FontArgType, size=(24, 24), fill: Int3Tuple
314
325
  draw = ImageDraw.Draw(img)
315
326
  font_obj = get_font_object(font)
316
327
  bbox = draw.textbbox((0, 0), __c, font=font_obj)
317
- x_offset, y_offset = ((size[i] - (bbox[i + 2] - bbox[i])) // 2 - bbox[i] for i in range(2))
328
+ x_offset, y_offset = (
329
+ (size[i] - (bbox[i + 2] - bbox[i])) // 2 - bbox[i] for i in range(2)
330
+ )
318
331
  draw.text((x_offset, y_offset), __c, font=font_obj, fill=fill)
319
332
  return img
320
333
 
@@ -419,11 +432,14 @@ def ansi_quantize(
419
432
  context = "{}=type[{}]".format(
420
433
  f"{ansi_type=}".partition('=')[0],
421
434
  ' | '.join(
422
- getattr(x, '__name__', f"{x}") for x in unionize(_ANSI_QUANTIZERS.keys()).__args__
435
+ getattr(x, '__name__', f"{x}")
436
+ for x in unionize(_ANSI_QUANTIZERS.keys()).__args__
423
437
  ),
424
438
  )
425
439
  raise TypeError(type_error_msg(ansi_type, context=context))
426
- if eq_f := {True: contrast_stretch, 'white_point': equalize_white_point}.get(equalize):
440
+ if eq_f := {True: contrast_stretch, 'white_point': equalize_white_point}.get(
441
+ equalize
442
+ ):
427
443
  img = eq_f(img)
428
444
  if img.size > 1024**2: # downsize for faster quantization
429
445
  w, h, _ = img.shape
@@ -479,7 +495,9 @@ def contrast_stretch(img: RGBArray) -> RGBArray:
479
495
  equalize_white_point
480
496
  """
481
497
  p2, p98 = np.percentile(img, (2, 98))
482
- return cast(RGBArray, ski.exposure.rescale_intensity(cast(..., img), in_range=(p2, p98)))
498
+ return cast(
499
+ RGBArray, ski.exposure.rescale_intensity(cast(..., img), in_range=(p2, p98))
500
+ )
483
501
 
484
502
 
485
503
  def scale_saturation(img: RGBArray, alpha: float = None) -> RGBArray:
@@ -668,12 +686,16 @@ def img2ansi(
668
686
  if ansi_type is not DEFAULT_ANSI:
669
687
  ansi_type = get_ansi_type(ansi_type)
670
688
  bg_wrapper = ColorStr('{}', color_spec={'bg': bg}, ansi_type=ansi_type, reset=False)
671
- base_ascii, color_arr = img2ascii(__img, __font, factor, char_set, sort_glyphs, ret_img=True)
689
+ base_ascii, color_arr = img2ascii(
690
+ __img, __font, factor, char_set, sort_glyphs, ret_img=True
691
+ )
672
692
  lines = base_ascii.splitlines()
673
693
  h, w = map(len, (lines, lines[0]))
674
694
  if ansi_type is not ansicolor24Bit:
675
695
  color_arr = ansi_quantize(color_arr, ansi_type=ansi_type, equalize=equalize)
676
- elif eq_func := {True: contrast_stretch, 'white_point': equalize_white_point}.get(equalize):
696
+ elif eq_func := {True: contrast_stretch, 'white_point': equalize_white_point}.get(
697
+ equalize
698
+ ):
677
699
  color_arr = eq_func(color_arr)
678
700
  color_arr = Image.fromarray(color_arr, mode='RGB').resize(
679
701
  (w, h), resample=Image.Resampling.LANCZOS
@@ -750,7 +772,7 @@ def ansi2img(
750
772
  font_size=24,
751
773
  *,
752
774
  fg_default: Int3Tuple | str = (170, 170, 170),
753
- bg_default: Int3Tuple | str | Literal['auto'] = (0, 0, 0),
775
+ bg_default: Int3Tuple | str | Literal['auto'] = 'auto',
754
776
  ):
755
777
  """Render an ANSI array as an image.
756
778
 
@@ -790,7 +812,8 @@ def ansi2img(
790
812
  font = truetype(get_font_object(__font, retpath=True), font_size)
791
813
  row_height = _get_bbox_shape(font)[-1]
792
814
  max_row_width = max(
793
- sum(font.getbbox(color_str.base_str)[2] for color_str in row) for row in __ansi_array
815
+ sum(font.getbbox(color_str.base_str)[2] for color_str in row)
816
+ for row in __ansi_array
794
817
  )
795
818
  iw, ih = map(int, (max_row_width, n_rows * row_height))
796
819
  input_fg = fg_default
@@ -818,7 +841,10 @@ def ansi2img(
818
841
  fill=bg_color or (0, 0, 0),
819
842
  )
820
843
  draw.text(
821
- (x_offset, y_offset), color_str.base_str, font=font, fill=fg_color or input_fg
844
+ (x_offset, y_offset),
845
+ color_str.base_str,
846
+ font=font,
847
+ fill=fg_color or input_fg,
822
848
  )
823
849
  x_offset += text_width
824
850
  y_offset += row_height
@@ -827,10 +853,9 @@ def ansi2img(
827
853
 
828
854
  def ansify(
829
855
  __img: RGBImageLike | PathLike[str] | str,
830
- /,
831
- *,
832
- font: FontArgType = UserFont.IBM_VGA_437_8X16,
856
+ __font: FontArgType = UserFont.IBM_VGA_437_8X16,
833
857
  font_size: int = 16,
858
+ *,
834
859
  factor: int = 200,
835
860
  char_set: Iterable[str] = None,
836
861
  sort_glyphs: bool | type[reversed] = True,
@@ -842,7 +867,7 @@ def ansify(
842
867
  return ansi2img(
843
868
  img2ansi(
844
869
  __img,
845
- font,
870
+ __font,
846
871
  factor=factor,
847
872
  char_set=char_set,
848
873
  ansi_type=ansi_type,
@@ -850,7 +875,7 @@ def ansify(
850
875
  equalize=equalize,
851
876
  bg=bg,
852
877
  ),
853
- font,
878
+ __font,
854
879
  font_size=font_size,
855
880
  fg_default=fg,
856
881
  bg_default=bg,
@@ -885,7 +910,9 @@ def _is_rgb_imagelike(__obj: Any) -> TypeGuard[Union[RGBArray, ImageType]]:
885
910
  return _is_rgb_array(__obj) or _is_rgb_image(__obj)
886
911
 
887
912
 
888
- _LiteralDigitStr: TypeAlias = Sequence[Literal['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']]
913
+ _LiteralDigitStr: TypeAlias = Sequence[
914
+ Literal['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
915
+ ]
889
916
 
890
917
 
891
918
  def _is_csi_param(__c: str) -> TypeGuard[Literal[';'] | _LiteralDigitStr]:
@@ -893,7 +920,9 @@ def _is_csi_param(__c: str) -> TypeGuard[Literal[';'] | _LiteralDigitStr]:
893
920
 
894
921
 
895
922
  def reshape_ansi(__str: str, h: int, w: int):
896
- if len(__str.splitlines()) == h and all(sum(map(len, x)) == w for x in to_sgr_array(__str)):
923
+ if len(__str.splitlines()) == h and all(
924
+ sum(map(len, x)) == w for x in to_sgr_array(__str)
925
+ ):
897
926
  return __str
898
927
  arr = [['\x00'] * w for _ in range(h)]
899
928
  offsets = dict.fromkeys(range(h), 0)
@@ -938,51 +967,66 @@ def reshape_ansi(__str: str, h: int, w: int):
938
967
  )
939
968
 
940
969
 
970
+ @lru_cache(maxsize=None)
971
+ def sgr_span_re_pattern():
972
+ import re
973
+ from ..color.core import sgr_re_pattern
974
+
975
+ sgr_re = sgr_re_pattern().pattern
976
+ return re.compile(rf"(?P<params>{sgr_re})?(?P<text>[^\x1b]*)")
977
+
978
+
941
979
  @lru_cache
942
980
  def to_sgr_array(__str: str, ansi_type: AnsiColorParam = DEFAULT_ANSI):
943
981
  ansi_typ = get_ansi_type(ansi_type)
944
- new_cs = partial(ColorStr, ansi_type=ansi_typ, no_reset=True)
945
- fix_bold = lambda v: (b'1;%s' % v) if type(v) is ansicolor4Bit else v
946
- resets_btok = {b'39': 'fg', b'49': 'bg'}
947
- resets = frozenset(resets_btok)
948
- pad_esc = {0x1B: '\x00\x1b'}
982
+ new_cs = partial(ColorStr, ansi_type=ansi_typ, reset=False)
983
+ sgr: SgrSequence
949
984
  x = []
950
- sgr: SgrSequence | None
951
985
  for line in __str.splitlines():
952
986
  xs = []
953
- ansi_meta = {}
987
+ ansi_ctx = {}
954
988
  prev: ColorStr | None = None
955
- for s in filter(None, line.translate(pad_esc).split('\x00')):
956
- sgr = None
957
- if s[: (i := min(2, len(s) - 1))] == '\x1b[':
958
- params, _, text = s[i:].partition('m')
959
- cs = new_cs(text, sgr := SgrSequence([int(b) for b in params.split(';')]))
989
+ for m in sgr_span_re_pattern().finditer(line):
990
+ if m["params"]:
991
+ params, text = (
992
+ m["params"].removeprefix('\x1b[').removesuffix('m'),
993
+ m["text"],
994
+ )
995
+ sgr = SgrSequence(map(int, params.split(';')))
996
+ cs = new_cs(text, sgr)
960
997
  if sgr.is_color():
961
998
  prev = cs
962
999
  else:
963
- cs = new_cs(s)
964
- if sgr is None:
965
- sgr: SgrSequence = getattr(cs, '_sgr_')
1000
+ cs = new_cs(m["text"])
1001
+ sgr = getattr(cs, '_sgr_')
966
1002
  if sgr.is_reset():
967
- ansi_meta.clear()
968
- for k in resets.intersection(sgr.values()):
969
- del ansi_meta[resets_btok[k]]
1003
+ ansi_ctx.clear()
1004
+ for k in set.intersection({b'39', b'49'}, sgr.values()):
1005
+ del ansi_ctx['fg' if k == b'39' else 'bg']
970
1006
  if sgr.is_color():
971
1007
  for k in sgr.rgb_dict.keys():
972
- ansi_meta[k] = sgr.get_color(k)
1008
+ ansi_ctx[k] = sgr.get_color(k)
973
1009
  if sgr.has_bright_colors:
974
- ansi_meta['bright'] = True
975
- elif ansi_meta.get('bright') or (prev is not None and b'1' in sgr.values()):
1010
+ ansi_ctx['bright'] = True
1011
+ elif ansi_ctx.get('bright') or (prev is not None and b'1' in sgr.values()):
976
1012
  if sgr.is_color():
977
- new_sgr = SgrSequence([fix_bold(v) for v in sgr.values()])
1013
+ new_sgr = SgrSequence(
1014
+ b'1;%s' % v if type(v) is ansicolor4Bit else v
1015
+ for v in sgr.values()
1016
+ )
978
1017
  for k in new_sgr.rgb_dict.keys():
979
- ansi_meta[k] = new_sgr.get_color(k)
1018
+ ansi_ctx[k] = new_sgr.get_color(k)
980
1019
  prev = cs = new_cs(cs.base_str, color_spec=new_sgr)
981
1020
  elif prev:
982
- if not ansi_meta.get('bright'):
983
- ansi_meta['bright'] = True
1021
+ if not ansi_ctx.get('bright'):
1022
+ ansi_ctx['bright'] = True
984
1023
  new_sgr = SgrSequence(
985
- sgr.values() + [p._value_ for p in getattr(prev, '_sgr_') if p.is_color()]
1024
+ v
1025
+ for vs in (
1026
+ sgr.values(),
1027
+ (p._value_ for p in getattr(prev, '_sgr_') if p.is_color()),
1028
+ )
1029
+ for v in vs
986
1030
  )
987
1031
  prev = cs = new_cs(cs.base_str, new_sgr)
988
1032
  xs.append(cs.as_ansi_type(ansi_typ))
@@ -1019,11 +1063,17 @@ def render_ans(
1019
1063
  'auto' will determine background color dynamically.
1020
1064
  """
1021
1065
  return ansi2img(
1022
- to_sgr_array(reshape_ansi(__str, *shape)), font, font_size, bg_default=bg_default
1066
+ to_sgr_array(reshape_ansi(__str, *shape)),
1067
+ font,
1068
+ font_size,
1069
+ bg_default=bg_default,
1023
1070
  )
1024
1071
 
1025
1072
 
1026
- def read_ans[AnyStr: (str, bytes)](__path: PathLike[AnyStr] | AnyStr, encoding='cp437') -> str:
1073
+ def read_ans[AnyStr: (
1074
+ str,
1075
+ bytes,
1076
+ )](__path: PathLike[AnyStr] | AnyStr, encoding='cp437') -> str:
1027
1077
  """Read a .ANS file and return the content as a string.
1028
1078
 
1029
1079
  Extends code page 437 translation if `encoding='cp437'`, and truncates any SAUCE metadata.
@@ -1106,13 +1156,19 @@ class AnsiImage:
1106
1156
  bg_default=None,
1107
1157
  **kwargs,
1108
1158
  ) -> ImageType:
1109
- return ansi2img(self.data, font, font_size, bg_default=bg_default or 'auto', **kwargs)
1159
+ return ansi2img(
1160
+ self.data, font, font_size, bg_default=bg_default or 'auto', **kwargs
1161
+ )
1110
1162
 
1111
1163
  def translate(self, __table: Mapping[int, str | int | None]):
1112
1164
  if not __table:
1113
1165
  return self
1114
1166
  table = {
1115
- k: v if v not in frozenset(x for c in ' \t\n\r\v\f' for x in (c, ord(c))) else ' '
1167
+ k: (
1168
+ v
1169
+ if v not in frozenset(x for c in ' \t\n\r\v\f' for x in (c, ord(c)))
1170
+ else ' '
1171
+ )
1116
1172
  for (k, v) in __table.items()
1117
1173
  if k != ord('\n')
1118
1174
  }
@@ -1122,7 +1178,9 @@ class AnsiImage:
1122
1178
  data[row][col] = data[row][col].translate(table)
1123
1179
  return type(self)(data, ansi_type=self.ansi_format)
1124
1180
 
1125
- def __init__(self, arr: list[list[ColorStr]], *, ansi_type: AnsiColorParam = DEFAULT_ANSI):
1181
+ def __init__(
1182
+ self, arr: list[list[ColorStr]], *, ansi_type: AnsiColorParam = DEFAULT_ANSI
1183
+ ):
1126
1184
  self.data = arr
1127
1185
  self._ansi_format_ = get_ansi_type(ansi_type)
1128
1186
  self.fp = None
@@ -1139,7 +1197,9 @@ class AnsiImage:
1139
1197
  initial = line[0]
1140
1198
  for s in line[1:]:
1141
1199
  if s.ansi_partition()[::2] == initial.ansi_partition()[::2]:
1142
- initial = initial.replace(initial.base_str, initial.base_str + s.base_str)
1200
+ initial = initial.replace(
1201
+ initial.base_str, initial.base_str + s.base_str
1202
+ )
1143
1203
  else:
1144
1204
  buffer.append(initial)
1145
1205
  initial = s
@@ -1174,7 +1234,9 @@ def _scaled_hu_moments(arr: MatrixLike):
1174
1234
 
1175
1235
 
1176
1236
  def approx_gridlike(
1177
- __fp: PathLike[str] | str, shape: TupleOf2[int], font: FontArgType = UserFont.IBM_VGA_437_8X16
1237
+ __fp: PathLike[str] | str,
1238
+ shape: TupleOf2[int],
1239
+ font: FontArgType = UserFont.IBM_VGA_437_8X16,
1178
1240
  ):
1179
1241
  def _get_grid_indices(arr: np.ndarray):
1180
1242
  regions = ski.measure.regionprops(ski.measure.label(_canny_edges(arr)))
@@ -1192,7 +1254,11 @@ def approx_gridlike(
1192
1254
  cc = c[0] + np.asarray(cs := range(c[-1])) * w
1193
1255
  return cast(
1194
1256
  list[TupleOf2[slice]],
1195
- [np.index_exp[rr[rx] : (rr + h)[rx], cc[cx] : (cc + w)[cx]] for rx in rs for cx in cs],
1257
+ [
1258
+ np.index_exp[rr[rx] : (rr + h)[rx], cc[cx] : (cc + w)[cx]]
1259
+ for rx in rs
1260
+ for cx in cs
1261
+ ],
1196
1262
  )
1197
1263
 
1198
1264
  with Image.open(__fp).convert('L') as grey:
@@ -1231,7 +1297,9 @@ def approx_gridlike(
1231
1297
 
1232
1298
  for u_indices in map(clustered_grid.__eq__, np.unique_values(clustered_grid)):
1233
1299
  u_slice = thresh[
1234
- grid_indices[next(idx for (idx, v) in enumerate(np.ravel(u_indices)) if v is True)]
1300
+ grid_indices[
1301
+ next(idx for (idx, v) in enumerate(np.ravel(u_indices)) if v is True)
1302
+ ]
1235
1303
  ]
1236
1304
  char_grid[u_indices] = min(
1237
1305
  glyph_map,
@@ -69,13 +69,17 @@ CP437_TRANS_TABLE = MappingProxyType(
69
69
 
70
70
 
71
71
  @overload
72
- def translate_cp437[_T: (int, str)](__x: str, *, ignore: _T | Iterable[_T] = ...) -> str: ...
72
+ def translate_cp437[_T: (
73
+ int,
74
+ str,
75
+ )](__x: str, *, ignore: _T | Iterable[_T] = ...) -> str: ...
73
76
 
74
77
 
75
78
  @overload
76
- def translate_cp437[
77
- _T: (int, str)
78
- ](__iter: Iterable[str], *, ignore: _T | Iterable[_T] = ...) -> Iterator[str]: ...
79
+ def translate_cp437[_T: (
80
+ int,
81
+ str,
82
+ )](__iter: Iterable[str], *, ignore: _T | Iterable[_T] = ...) -> Iterator[str]: ...
79
83
 
80
84
 
81
85
  def translate_cp437(
@@ -17,13 +17,17 @@ from .._typing import FontArgType, GlyphArray, GlyphBitmask, ShapedNDArray
17
17
 
18
18
  @overload
19
19
  def get_glyph_masks(
20
- __font: FontArgType, char_set: Sequence[str] = ..., dist_transform: Literal[False] = False
20
+ __font: FontArgType,
21
+ char_set: Sequence[str] = ...,
22
+ dist_transform: Literal[False] = False,
21
23
  ) -> dict[str, GlyphBitmask]: ...
22
24
 
23
25
 
24
26
  @overload
25
27
  def get_glyph_masks(
26
- __font: FontArgType, char_set: Sequence[str] = ..., dist_transform: Literal[True] = ...
28
+ __font: FontArgType,
29
+ char_set: Sequence[str] = ...,
30
+ dist_transform: Literal[True] = ...,
27
31
  ) -> dict[str, GlyphArray[float64]]: ...
28
32
 
29
33
 
@@ -89,4 +93,6 @@ def ttf_extract_codepoints(
89
93
  for table in font['cmap'].tables:
90
94
  codepoints |= table.cmap.keys()
91
95
 
92
- return np.sort(np.array([i for i in codepoints if chr(i).isprintable()], dtype='<u2'))
96
+ return np.sort(
97
+ np.array([i for i in codepoints if chr(i).isprintable()], dtype='<u2')
98
+ )
@@ -24,15 +24,28 @@ __all__ = [
24
24
  ]
25
25
 
26
26
  from operator import mul, truediv
27
- from typing import Final, Literal, SupportsInt, cast
27
+ from functools import lru_cache
28
+ from typing import Final, Literal, SupportsInt, cast, TypeGuard
28
29
 
29
30
  import numpy as np
30
31
 
31
- from .._typing import Float3Tuple, FloatSequence, Int3Tuple, RGBPixel, RGBVectorLike, ShapedNDArray
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)
32
45
 
33
46
 
34
47
  def is_hex_rgb(value, *, strict: bool = False):
35
- if issubclass(type(value), SupportsInt):
48
+ if _supports_int(type(value)):
36
49
  if 0x0 <= int(value) <= 0xFFFFFF:
37
50
  return True
38
51
  elif not strict:
@@ -47,7 +60,7 @@ def hexstr2rgb(__str: str) -> Int3Tuple:
47
60
 
48
61
  def rgb2hexstr(rgb: RGBVectorLike) -> str:
49
62
  r, g, b = rgb
50
- return f'{r:02x}{g:02x}{b:02x}'
63
+ return f"{r:02x}{g:02x}{b:02x}"
51
64
 
52
65
 
53
66
  def rgb2hex(rgb: RGBVectorLike) -> int:
@@ -78,7 +91,10 @@ def lab2xyz(lab: FloatSequence) -> Float3Tuple:
78
91
  x, y, z = map(
79
92
  mul,
80
93
  (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)),
94
+ map(
95
+ lambda i: (lambda j: j if j > 0.008856 else (i - 16 / 116) / 7.787)(i**3),
96
+ (x, y, z),
97
+ ),
82
98
  )
83
99
  return x, y, z
84
100
 
@@ -98,7 +114,9 @@ def rgb2xyz(rgb: RGBPixel) -> Float3Tuple:
98
114
 
99
115
 
100
116
  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)
117
+ r, g, b = (
118
+ np.clip(M_XYZ2RGB @ np.array(xyz, dtype=np.float64), 0.0, 1.0) * 255.0
119
+ ).astype(int)
102
120
  return r, g, b
103
121
 
104
122
 
@@ -255,12 +273,14 @@ def _4b_lookup():
255
273
 
256
274
  rgb_4b_arr = np.asarray(ANSI_4BIT_RGB)
257
275
  quants = np.stack(
258
- np.meshgrid(*np.repeat(np.arange(32).reshape([1, -1]), 3, 0), indexing='ij'), axis=-1
276
+ np.meshgrid(*np.repeat(np.arange(32).reshape([1, -1]), 3, 0), indexing='ij'),
277
+ axis=-1,
259
278
  ).reshape([-1, 3])
260
279
  rgb_colors = quants * 8
261
280
  nearest_colors = rgb_4b_arr[np.argmin(rgb_dist(rgb_colors, rgb_4b_arr), axis=1)]
262
281
  table = {
263
- tuple(map(int, color)): tuple(map(int, nearest_colors[i])) for i, color in enumerate(quants)
282
+ tuple(map(int, color)): tuple(map(int, nearest_colors[i]))
283
+ for i, color in enumerate(quants)
264
284
  }
265
285
  return cast(dict[Int3Tuple, Int3Tuple], table)
266
286