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 +8 -2
- chromatic/_typing.py +37 -14
- chromatic/_version.py +2 -2
- chromatic/ascii/_array.py +123 -55
- chromatic/ascii/_curses.py +8 -4
- chromatic/ascii/_glyph_proc.py +9 -3
- chromatic/color/colorconv.py +28 -8
- chromatic/color/core.py +209 -143
- chromatic/color/core.pyi +55 -25
- chromatic/color/iterators.py +11 -5
- chromatic/color/palette.py +33 -10
- chromatic/color/palette.pyi +4 -1
- chromatic/data/__init__.py +15 -5
- chromatic/demo.py +42 -11
- {chromatic_python-0.2.2.dist-info → chromatic_python-0.2.3.dist-info}/METADATA +1 -2
- chromatic_python-0.2.3.dist-info/RECORD +28 -0
- {chromatic_python-0.2.2.dist-info → chromatic_python-0.2.3.dist-info}/WHEEL +1 -1
- chromatic_python-0.2.2.dist-info/RECORD +0 -28
- {chromatic_python-0.2.2.dist-info → chromatic_python-0.2.3.dist-info}/licenses/LICENSE +0 -0
- {chromatic_python-0.2.2.dist-info → chromatic_python-0.2.3.dist-info}/top_level.txt +0 -0
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] = {
|
|
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(
|
|
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[
|
|
52
|
-
|
|
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 = (
|
|
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(
|
|
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(
|
|
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[
|
|
217
|
-
|
|
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)
|
|
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, _, _ = (
|
|
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,
|
|
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 = {
|
|
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 = [
|
|
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
|
|
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
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 = (
|
|
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}")
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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'] =
|
|
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)
|
|
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),
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
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(
|
|
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,
|
|
945
|
-
|
|
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
|
-
|
|
987
|
+
ansi_ctx = {}
|
|
954
988
|
prev: ColorStr | None = None
|
|
955
|
-
for
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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(
|
|
964
|
-
|
|
965
|
-
sgr: SgrSequence = getattr(cs, '_sgr_')
|
|
1000
|
+
cs = new_cs(m["text"])
|
|
1001
|
+
sgr = getattr(cs, '_sgr_')
|
|
966
1002
|
if sgr.is_reset():
|
|
967
|
-
|
|
968
|
-
for k in
|
|
969
|
-
del
|
|
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
|
-
|
|
1008
|
+
ansi_ctx[k] = sgr.get_color(k)
|
|
973
1009
|
if sgr.has_bright_colors:
|
|
974
|
-
|
|
975
|
-
elif
|
|
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(
|
|
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
|
-
|
|
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
|
|
983
|
-
|
|
1021
|
+
if not ansi_ctx.get('bright'):
|
|
1022
|
+
ansi_ctx['bright'] = True
|
|
984
1023
|
new_sgr = SgrSequence(
|
|
985
|
-
|
|
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)),
|
|
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: (
|
|
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(
|
|
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:
|
|
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__(
|
|
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(
|
|
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,
|
|
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
|
-
[
|
|
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[
|
|
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,
|
chromatic/ascii/_curses.py
CHANGED
|
@@ -69,13 +69,17 @@ CP437_TRANS_TABLE = MappingProxyType(
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
@overload
|
|
72
|
-
def translate_cp437[_T: (
|
|
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
|
-
|
|
78
|
-
|
|
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(
|
chromatic/ascii/_glyph_proc.py
CHANGED
|
@@ -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,
|
|
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,
|
|
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(
|
|
96
|
+
return np.sort(
|
|
97
|
+
np.array([i for i in codepoints if chr(i).isprintable()], dtype='<u2')
|
|
98
|
+
)
|
chromatic/color/colorconv.py
CHANGED
|
@@ -24,15 +24,28 @@ __all__ = [
|
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
from operator import mul, truediv
|
|
27
|
-
from
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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 = (
|
|
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'),
|
|
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]))
|
|
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
|
|