chromatic-python 0.3.3__tar.gz → 0.3.4__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 (48) hide show
  1. {chromatic_python-0.3.3/chromatic_python.egg-info → chromatic_python-0.3.4}/PKG-INFO +1 -1
  2. chromatic_python-0.3.4/chromatic/__main__.py +105 -0
  3. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/_version.py +3 -3
  4. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/color/core.py +93 -123
  5. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/color/core.pyi +3 -2
  6. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/color/palette.py +20 -25
  7. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/color/palette.pyi +2 -4
  8. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/data/userfont.py +17 -2
  9. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/demo.py +78 -134
  10. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/image/_array.py +14 -14
  11. {chromatic_python-0.3.3 → chromatic_python-0.3.4/chromatic_python.egg-info}/PKG-INFO +1 -1
  12. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic_python.egg-info/SOURCES.txt +1 -0
  13. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/.gitattributes +0 -0
  14. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/.gitignore +0 -0
  15. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/LICENSE +0 -0
  16. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/README.md +0 -0
  17. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/__init__.py +0 -0
  18. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/__init__.pyi +0 -0
  19. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/_typing.py +0 -0
  20. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/color/__init__.py +0 -0
  21. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/color/__init__.pyi +0 -0
  22. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/color/colorconv.py +0 -0
  23. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/color/iterators.py +0 -0
  24. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/data/__init__.py +0 -0
  25. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/data/__init__.pyi +0 -0
  26. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/data/_fetchers.py +0 -0
  27. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/data/butterfly.jpg +0 -0
  28. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/data/escher.png +0 -0
  29. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/data/fonts/consolas.ttf +0 -0
  30. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/data/fonts/vga437.ttf +0 -0
  31. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/data/goblin_virus.png +0 -0
  32. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/data/registry.json +0 -0
  33. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/data/userfont.pyi +0 -0
  34. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/data/userfont.schema.json +0 -0
  35. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/image/__init__.py +0 -0
  36. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/image/__init__.pyi +0 -0
  37. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/image/_curses.py +0 -0
  38. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic/image/_glyph_proc.py +0 -0
  39. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic_python.egg-info/dependency_links.txt +0 -0
  40. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic_python.egg-info/requires.txt +0 -0
  41. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/chromatic_python.egg-info/top_level.txt +0 -0
  42. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/logo/logo.ANS +0 -0
  43. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/logo/logo.PNG +0 -0
  44. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/pyproject.toml +0 -0
  45. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/requirements.txt +0 -0
  46. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/setup.cfg +0 -0
  47. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/tests/__init__.py +0 -0
  48. {chromatic_python-0.3.3 → chromatic_python-0.3.4}/tests/test_color_str.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chromatic-python
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: ANSI art image processing and colored terminal text
5
5
  Author: crypt0lith
6
6
  Project-URL: Homepage, https://github.com/crypt0lith/chromatic
@@ -0,0 +1,105 @@
1
+ from . import __version__, __name__ as prog
2
+
3
+
4
+ def banner():
5
+ from chromatic import read_ans
6
+ from base64 import b64decode
7
+ import io
8
+
9
+ art = read_ans(
10
+ io.TextIOWrapper(
11
+ io.BytesIO(
12
+ b64decode(
13
+ b"""\
14
+ G1swbQ0KICAgICAgICAgICAgICAgICAgG1s5MG36ICAg+vogICAgICAgICAgG1swOzM0bfobWzMy
15
+ bfobWzkxbfobWzkwbSAbWzA7MzRt+vobWzkzbfobWzkxbfobWzA7MzVt+vobWzkwbSAgICAgICAg
16
+ ICAg+iAgIPr6DQobWzBtICAgICAgICAgG1s5MG36ICAgIPouICD6IC4g+vogG1swOzM0bfobWzMy
17
+ bfobWzk0bfobWzkwbSAbWzA7MzVt+htbMzRt+i4bWzkxbfobWzkybS4bWzA7MzRt+htbMzJt+htb
18
+ MzFt+htbMzJtLhtbOTFtLhtbMDszNG36G1s5Mm36G1swOzMxbfobWzMybS4bWzkzbS4bWzA7MzJt
19
+ +htbOTFt+htbOTBt+htbMDszNW36G1szNG36G1szMm36G1s5NG36G1s5MG36G1swOzM1bfobWzkw
20
+ bSAg+iAuIPr6IC4g+iAgICD6DQobWzBtICAgG1s5MG0gICAuICAgIC4g+jouICAgICAgG1swOzM0
21
+ bS4bWzMybS4bWzk0bS4bWzkwbS4bWzA7MzVtLhtbMzRtLhtbMzJtLhtbOTFtLhtbMDszNG06G1s5
22
+ NG36G1s5Mm06G1swOzMxbS4bWzM1bTouG1s5MG0gICAgIBtbMDszNG0uLhtbOTJtLhtbMDszMW0u
23
+ G1szMm36G1s5NG36G1swOzMybS4bWzkxbS4bWzkwbSAbWzA7MzVtLhtbMzRtLhtbMzJtLhtbOTRt
24
+ LhtbOTBtIBtbMDszNW0uG1s5MG0gICAgICAuLiD6IC4gICAgLg0KG1swbSAbWzkwbSAgICA6ICAg
25
+ IDogIGw6IC4gICAgG1swOzM0bTobWzMybTobWzk0bTobWzkwbSAbWzA7MzVtOhtbMzRtOhtbMzJt
26
+ OhtbOTFtOhtbMDszNG1sOhtbOTJtbBtbMDszMW06G1szMm0uG1s5NG0uG1s5MG0gG1swOzM1bS4b
27
+ WzkwbSAgG1swOzMybSAgG1szNG0uOhtbOTRtLhtbOTJtOhtbMDszMW06G1szNW06G1szNG06G1sz
28
+ Mm06G1s5MW06G1s5MG1sG1swOzM1bTobWzM0bTobWzMybTobWzk0bTobWzkwbSAbWzA7MzVtOhtb
29
+ OTBtICAgICAuOjogICA6ICAgIDobWzA7MzJtG1szN20NChtbOTBtICAgIGwgICAgbCAgJGwgICA6
30
+ ICAbWzA7MzRtbBtbMzJtbBtbOTRtbBtbOTBtIBtbMDszNW1sG1szNG1sG1szMm1sG1s5MW1sG1sw
31
+ OzM0bSRsG1s5Mm0kG1swOzMxbWwbWzM1bSQbWzM0bTobWzMybTobWzk0bTobWzkwbSAbWzA7MzVt
32
+ OhtbOTBtIBtbMDszMm0gG1szNG06G1szMm06G1s5NG06G1swOzM0bWxsG1s5Mm1sG1swOzMxbWwb
33
+ WzM1bWwbWzM0bWwbWzMybWwbWzkxbWwbWzkwbSAbWzA7MzVtbBtbMzRtbBtbMzJtbBtbOTRtbBtb
34
+ OTBtIBtbMDszNW1sG1s5MG0gICA6ICBsbCAgIGwgICAgbBtbMDszMm0bWzM3bQ0KG1s5MG0gICAk
35
+ ICAgICQgICAkICAgICBsG1swOzM0bSQbWzMybSQbWzk0bSQbWzkwbSAbWzA7MzVtJBtbMzRtJBtb
36
+ MzJtJBtbOTFtJBtbOTBtIBtbMDszNG0kG1szMm0kG1szMW0kG1s5MG0gG1swOzM1bSQbWzkwbSQb
37
+ WzA7MzRtbBtbMzJtbBtbOTRtbBtbOTBtIBtbMDszNW1sG1szNG1sG1szMm1sG1s5NG1sG1s5MG0g
38
+ G1swOzM1bWwbWzM0bSQkG1s5Mm0kG1swOzMxbSQbWzM1bSQbWzM0bSQbWzMybSQbWzkxbSQbWzkw
39
+ bSAbWzA7MzVtJBtbMzRtJBtbMzJtJBtbOTRtJBtbOTBtIBtbMDszNW0kG1s5MG0gbCAgICAkJCAg
40
+ ICQgICAgJBtbMDszMm0bWzM3bQ0KG1s5MG0gICDbICAgINsgICDb3CAgICAgG1swOzM0bdsbWzMy
41
+ bdsbWzk0bdsbWzkwbSQbWzA7MzVt2xtbMzRt2xtbMzJt2xtbOTFt2xtbOTBt3BtbMDszNG3b3Btb
42
+ MzFt2xtbOTJt3BtbMDszNW3b3BtbOTBtIBtbMDszNG0kG1szMm0kG1s5NG0kG1swOzM0bSQbWzMy
43
+ bSQbWzk0bSQbWzkwbSAbWzA7MzVtJBtbMzRt3NsbWzkzbdwbWzkybdsbWzA7MzFt2xtbMzVt2xtb
44
+ MzRt2xtbMzJt2xtbOTFt2xtbOTBtIBtbMDszNW3bG1szNG3bG1szMm3bG1s5NG3bG1s5MG0gG1sw
45
+ OzM1bdsbWzkwbSQgICAg3NvbICAg2yAgICDbG1swOzMybRtbMzdtDQobWzkwbSAgICfcICAgJ9wg
46
+ ICfcN9wuICAbWzA7MzRtJ9wbWzk0bSfcG1swOzM1bScbWzM0bSfcG1s5MW0n3BtbMDszNG0n3Btb
47
+ MzFtJ9wbWzkybTfcLhtbMDszNG3b3BtbOTRt2xtbOTFt3C4bWzk0bdsbWzA7MzRt3BtbOTNtLtw3
48
+ G1s5Mm3cG1swOzMxbdwnG1szNG3cJxtbOTFt3CcbWzA7MzVt3BtbMzRt3CcbWzk0bdwnG1swOzM1
49
+ bdwnG1s5MG3bIC7cN9zcJyAg3CcgICDcJxtbMDszMm0bWzM3bQ0KG1s5MG0gICAgJzfcLiAnN9wu
50
+ JzfcLmpfLBtbMDszNG0nNxtbOTRtJzfcLhtbMDszNG03G1s5MW0nN9wuG1swOzMxbSc33C4bWzkx
51
+ bWobWzA7MzFtXyxq3CxfG1s5MW1qG1swOzMxbS7cNycbWzkxbS7cNycbWzA7MzRt3BtbOTRtLtw3
52
+ JxtbMDszNW03JxtbOTBt3Cxfai7cNycu3DcnIC7cNycbWzA7MzJtG1szN20NChtbOTBtICAgICAg
53
+ ICffN2rcLF83LrCyN2os3GobWzA7MzRtJ98bWzk0bSffN2rcLF8bWzkxbTcbWzk0bS4bWzA7MzFt
54
+ sLI3aizcajeysBtbOTRtLhtbOTFtNxtbOTRtXyzcajffJxtbMDszNW3fJxtbOTBtN2os3Go3srAu
55
+ N18s3Go33ycbWzA7MzJtG1szN20NChtbOTBtICAgICAgICAgICzcajey3N8n39wnJ9zfJ98bWzA7
56
+ MzRtLNwbWzk0bSzcajeyG1swOzMxbdzfG1s5MW0n39wbWzA7MzFtJxtbOTNtJxtbOTFt3N8nG1sw
57
+ OzMxbd/cG1s5NG2yN2rcLBtbMDszNW3cLBtbOTBt3N8n39wnJ9zfJ9/csjdq3CwbWzA7MzJtG1sz
58
+ N20NChtbOTBtICAgICAgICDc3yewICDdsNzdIN0g3t3eG1swOzM0bdzfG1s5NG3c3ycbWzkxbbAb
59
+ WzA7MzRt3bAbWzMxbd2wG1s5NG3cG1s5Mm3dG1swOzMybd0bWzkxbd0bWzA7MzRt3RtbOTFt3t0b
60
+ Wzkzbd7dG1s5NG3cG1swOzMxbbDdG1szNG2wJxtbOTFtsBtbOTRtJ9/cG1swOzM1bd/cG1s5MG3d
61
+ IN0g3t3e3dyw3SAgsCff3BtbMDszMm0bWzM3bQ0KG1s5MG0gICAgICAg3t0gIN/cICDf3N4gICDb
62
+ IBtbMDszNG3e3RtbOTRt3t0bWzA7MzRt39wbWzkxbd/cG1swOzM0bd/cG1szMW3f3BtbOTRt3htb
63
+ MDszNW3cG1szNG3bG1szMm3bG1s5MW3bG1swOzM0bd7cG1s5NG3eG1swOzMxbdzfG1szNG3c3xtb
64
+ OTFt3N8bWzA7MzRt3t0bWzk0bd7dG1swOzM1bd7dG1s5MG0gICDbICDe3N8gINzfICDe3RtbMDsz
65
+ Mm0bWzM3bQ0KG1s5MG0gICAgICAg2yAgICAgNy7c3zcu3SDe3RtbMDszNG3bG1szMm3bG1s5NG3b
66
+ G1s5MG3cG1swOzM1bdsbWzkwbTcbWzA7MzRtNy4bWzkxbTcuG1s5NG3c3xtbMDszMW03LhtbOTFt
67
+ 3RtbMDszNG3dG1s5MW3e3RtbMDszMW0uNxtbOTRt39wbWzkxbS43G1swOzM1bS43G1s5MG0gG1sw
68
+ OzM0bdsbWzMybdsbWzk0bdsbWzkwbd8bWzA7MzVt2xtbOTBtLt0g3t0uN9/cLjcgICAgINsbWzA7
69
+ MzJtG1szN20NChtbOTBtICAgICAgIN7dICAgIC7f3z/cJ9/cP98bWzA7MzRt3t0bWzk0bd7dG1sw
70
+ OzM1bd7dG1szNG0u3xtbOTRtLt8bWzkxbd8/3BtbMDszMW0n39w/3ycbWzkxbdw/3xtbOTRt3y4b
71
+ WzA7MzVt3y4bWzM0bd7dG1s5NG3e3RtbMDszNW3e3RtbOTBtJ9/cP98n3D/f3y4gICAg3t0bWzA7
72
+ MzJtG1szN20NChtbOTBtICAgICAgICDf3CAg3F8u3D9fLtzfP9wuG1swOzM0bd/cG1s5NG3f3Btb
73
+ MDszNG3cXxtbOTJt3BtbOTFtXy7cPxtbMDszMW1fLtzfP9wuXxtbOTFtP9wuXxtbMDszNW0uG1sz
74
+ NG3c3xtbOTRt3N8bWzA7MzVt3N8bWzkwbV8u3N8/3C5fP9wuXyAgINzfG1swOzMybRtbMzdtDQob
75
+ WzkwbSAgICAgICAgICLfP9wuXyDf3yIgICAiIt8bWzA7MzRtIt8bWzk0bSLfP9wuXxtbMDszNG3f
76
+ G1szMW3f3yIbWzM1bd8bWzM0bSIiG1s5Mm0iG1swOzMxbSLf3xtbMzRtLhtbOTRtXy7cP98iG1sw
77
+ OzM1bd8iG1s5MG3f3yIgICAiIt/fIF8u3D/fIhtbMDszMm0bWzM3bQ0KG1s5MG0gICAgICAgICAg
78
+ ICAgIt/fICAgICAgICAgIN/fIhtbMDszNG0i3xtbOTRtIt/fG1swOzM1bd/fG1s5MG0gICAbWzA7
79
+ MzJtICAbWzkwbSAbWzA7MzRt398bWzk0bd/fIhtbMDszNW3fIhtbOTBtICLf3yAgICAgICAgICDf
80
+ 3yIbWzA7MzJtG1szN20NChtbMG0NChtbOTBtezogXjgwfRtbMG0="""
81
+ )
82
+ ),
83
+ encoding="cp437",
84
+ )
85
+ )
86
+
87
+ return art.format(f"{prog} {__version__}")
88
+
89
+
90
+ def main():
91
+ import argparse
92
+
93
+ parser = argparse.ArgumentParser(
94
+ prog=prog,
95
+ description=banner(),
96
+ formatter_class=argparse.RawDescriptionHelpFormatter,
97
+ )
98
+ parser.add_argument(
99
+ "--version", action="version", version=f"%(prog)s {__version__}"
100
+ )
101
+ parser.parse_args()
102
+
103
+
104
+ if __name__ == '__main__':
105
+ main()
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.3.3'
32
- __version_tuple__ = version_tuple = (0, 3, 3)
31
+ __version__ = version = '0.3.4'
32
+ __version_tuple__ = version_tuple = (0, 3, 4)
33
33
 
34
- __commit_id__ = commit_id = 'g23243f5d4'
34
+ __commit_id__ = commit_id = 'gb93479602'
@@ -11,6 +11,7 @@ __all__ = [
11
11
  'color_chain',
12
12
  'colorbytes',
13
13
  'get_ansi_type',
14
+ 'is_vt_enabled',
14
15
  'randcolor',
15
16
  'rgb2ansi_escape',
16
17
  ]
@@ -18,6 +19,7 @@ __all__ = [
18
19
  import os
19
20
  import random
20
21
  import re
22
+ import sys
21
23
  from collections import Counter
22
24
  from collections.abc import Buffer
23
25
  from copy import deepcopy
@@ -53,9 +55,9 @@ from .colorconv import (
53
55
  )
54
56
  from .._typing import AnsiColorAlias, ColorDictKeys, Int3Tuple
55
57
 
56
- os.system('')
58
+ # os.system('')
57
59
 
58
- CSI: Final[bytes] = b'['
60
+ CSI: Final[bytes] = b'\x1b['
59
61
  SGR_RESET: Final[bytes] = CSI + b'0m'
60
62
  SGR_RESET_S: Final[str] = SGR_RESET.decode()
61
63
 
@@ -346,42 +348,48 @@ class ansicolor24Bit(colorbytes):
346
348
  alias = '24b'
347
349
 
348
350
 
349
- _SUPPORTS_256 = frozenset(
350
- [
351
- 'ANSICON',
352
- 'COLORTERM',
353
- 'ConEmuANSI',
354
- 'PYCHARM_HOSTED',
355
- 'TERM',
356
- 'TERMINAL_EMULATOR',
357
- 'TERM_PROGRAM',
358
- 'WT_SESSION',
359
- ]
360
- )
361
-
351
+ if os.name == 'nt':
352
+ from ctypes import windll, wintypes
362
353
 
363
- def is_vt_proc_enabled():
364
- if os.name != 'nt' or os.environ.keys() & _SUPPORTS_256:
365
- return True
354
+ def _enable_vt_processing(handle: int):
355
+ ENABLE_VT_PROCESSING = 0x0004
356
+ k32 = windll.kernel32
357
+ k32.GetStdHandle.restype = wintypes.HANDLE
358
+ k32.GetConsoleMode.restype = k32.SetConsoleMode.restype = wintypes.BOOL
359
+ h = k32.GetStdHandle(handle)
360
+ if h == -1:
361
+ return False
362
+ mode = wintypes.DWORD()
363
+ if not k32.GetConsoleMode(h, byref(mode)):
364
+ return False
365
+ mode.value |= ENABLE_VT_PROCESSING
366
+ return bool(k32.SetConsoleMode(h, mode))
367
+
368
+ def is_vt_enabled():
369
+ if os.environ.keys() & {
370
+ 'ANSICON',
371
+ 'COLORTERM',
372
+ 'ConEmuANSI',
373
+ 'PYCHARM_HOSTED',
374
+ 'TERM',
375
+ 'TERMINAL_EMULATOR',
376
+ 'TERM_PROGRAM',
377
+ 'WT_SESSION',
378
+ }:
379
+ return True
380
+ ok = False
381
+ for fd, handle in [(sys.stdout, -11), (sys.stderr, -12)]:
382
+ if getattr(fd, "isatty", lambda: False)():
383
+ ok |= _enable_vt_processing(handle)
384
+ return ok
366
385
 
367
- from ctypes import windll, wintypes
386
+ else:
368
387
 
369
- STD_OUTPUT_HANDLE = -11
370
- ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
371
- kernel32 = windll.kernel32
372
- kernel32.GetStdHandle.restype = wintypes.HANDLE
373
- kernel32.GetConsoleMode.restype = kernel32.SetConsoleMode.restype = wintypes.BOOL
374
- handle = kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
375
- if handle == -1:
376
- return False
377
- mode = wintypes.DWORD()
378
- if not kernel32.GetConsoleMode(handle, byref(mode)):
379
- return False
380
- mode.value |= ENABLE_VIRTUAL_TERMINAL_PROCESSING
381
- return kernel32.SetConsoleMode(handle, mode)
388
+ def is_vt_enabled():
389
+ return True
382
390
 
383
391
 
384
- DEFAULT_ANSI = ansicolor8Bit if is_vt_proc_enabled() else ansicolor4Bit
392
+ DEFAULT_ANSI = ansicolor8Bit if is_vt_enabled() else ansicolor4Bit
385
393
 
386
394
  AnsiColorFormat: TypeAlias = ansicolor4Bit | ansicolor8Bit | ansicolor24Bit
387
395
  AnsiColorType: TypeAlias = type[AnsiColorFormat]
@@ -393,7 +401,7 @@ _ANSI_COLOR_TYPES = cast(
393
401
  _ANSI_FORMAT_MAP = {k: x for x in _ANSI_COLOR_TYPES for k in [x, x.alias]}
394
402
 
395
403
 
396
- @lru_cache
404
+ @lru_cache(maxsize=len(_ANSI_COLOR_TYPES))
397
405
  def _is_ansi_type(typ: type):
398
406
  try:
399
407
  return typ in _ANSI_COLOR_TYPES
@@ -401,7 +409,7 @@ def _is_ansi_type(typ: type):
401
409
  return False
402
410
 
403
411
 
404
- @lru_cache
412
+ @lru_cache(maxsize=len(_ANSI_FORMAT_MAP))
405
413
  def get_ansi_type(typ):
406
414
  try:
407
415
  return _ANSI_FORMAT_MAP[typ]
@@ -432,7 +440,7 @@ def set_default_ansi(typ):
432
440
 
433
441
 
434
442
  @lru_cache(maxsize=1)
435
- def sgr_re_pattern():
443
+ def sgr_pattern():
436
444
  uint8_re = r"(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)"
437
445
  ansicolor_re = f"[3-4]8;(?:2(?:;{uint8_re}){{3}}|5;{uint8_re})"
438
446
  sgr_param_re = (
@@ -445,7 +453,7 @@ def sgr_re_pattern():
445
453
  def _split_ansi_escape(__s: str) -> list[tuple['SgrSequence', str]] | None:
446
454
  out = []
447
455
  i = 0
448
- for m in sgr_re_pattern().finditer(__s):
456
+ for m in sgr_pattern().finditer(__s):
449
457
  text = __s[i : (j := m.start())]
450
458
  if i != j:
451
459
  out.append(text)
@@ -643,8 +651,9 @@ def _iter_normalized_sgr[_T: (
643
651
  Buffer,
644
652
  SupportsInt,
645
653
  )](__iter: bytes | bytearray | Iterable[_T]) -> Iterator[int | AnsiColorFormat]:
646
- __iter: ...
647
- for elt in __iter.split(b';') if isinstance(__iter, (bytes, bytearray)) else __iter:
654
+ if isinstance(__iter, (bytes, bytearray)):
655
+ __iter = __iter.split(b';')
656
+ for elt in __iter:
648
657
  match elt:
649
658
  case (colorbytes() as cb) | SgrParamBuffer(colorbytes() as cb):
650
659
  yield cb
@@ -938,44 +947,43 @@ _END_RESET_PATTERN = re.compile(r"\x1b\[0?m$")
938
947
  _unset: Any = object()
939
948
 
940
949
 
941
- def _make_colorstr[_T: ColorStr](cls: type[_T], obj=_unset, *args, **kwargs) -> _T:
942
- sgr = SgrSequence()
943
- fg = kwargs.pop('fg', _unset)
944
- bg = kwargs.pop('bg', _unset)
945
- ansi_type = kwargs.pop('ansi_type', _unset)
946
- reset = kwargs.pop('reset', _unset)
947
- if kwargs:
948
- if not kwargs.keys() <= {'encoding', 'errors'}:
949
- raise ValueError(
950
- f"unexpected keyword arguments: {(kwargs.keys() - {'encoding', 'errors'})}"
951
- )
950
+ def _colorstr[_T: ColorStr](
951
+ cls: type[_T],
952
+ obj=_unset,
953
+ /,
954
+ fg=None,
955
+ bg=None,
956
+ *,
957
+ encoding=_unset,
958
+ errors=_unset,
959
+ ansi_type=_unset,
960
+ reset=True,
961
+ ) -> _T:
962
+ if buf_kwargs := {
963
+ k: v
964
+ for k, v in locals().items()
965
+ if k in {"encoding", "errors"} and v is not _unset
966
+ }:
952
967
  if not isinstance(obj, Buffer):
953
- raise ValueError(
954
- "expected buffer or bytes-like object, "
955
- f"got {type(obj).__name__!r} object instead"
956
- )
957
- encoding = kwargs.pop('encoding', 'utf-8')
958
- errors = kwargs.pop('errors', 'strict')
959
- if isinstance(obj, (bytes, bytearray)):
960
- obj = obj.decode(encoding, errors)
961
- else:
962
- obj = bytes(obj).decode(encoding, errors)
968
+ raise ValueError(f"unexpected keyword arguments: {set(buf_kwargs)}")
969
+ elif not isinstance(obj, (bytes, bytearray)):
970
+ obj = bytes(obj)
971
+ obj = obj.decode(**buf_kwargs)
972
+ sgr = SgrSequence()
973
+
963
974
  if obj is not _unset:
964
975
  if isinstance(obj, str):
965
976
  base_str = str(obj)
966
- while m := sgr_re_pattern().match(base_str):
967
- sgr.extend(m[0].removeprefix('\x1b[').removesuffix('m').encode())
977
+ sgr_match = sgr_pattern().match
978
+ while m := sgr_match(base_str):
979
+ sgr.extend(m[0].removeprefix("\x1b[").removesuffix('m').encode())
968
980
  base_str = base_str[m.end() :]
969
981
  base_str = _END_RESET_PATTERN.sub('', base_str)
970
982
  else:
971
983
  base_str = str(obj)
972
984
  else:
973
985
  base_str = ''
974
- if '\x1b[' in base_str:
975
- raise ValueError
976
- if reset is _unset:
977
- reset = True
978
- elif type(reset) is not bool:
986
+ if type(reset) is not bool:
979
987
  reset = bool(reset)
980
988
  if ansi_type is not _unset:
981
989
  ansi_type = get_ansi_type(ansi_type)
@@ -987,34 +995,8 @@ def _make_colorstr[_T: ColorStr](cls: type[_T], obj=_unset, *args, **kwargs) ->
987
995
  key=lambda x: x[1],
988
996
  )[0]
989
997
  try:
990
- if fg is bg is _unset:
991
- match args:
992
- case []:
993
- fg = bg = None
994
- case [_ as fg]:
995
- bg = None
996
- case [_ as fg, _ as bg]:
997
- pass
998
- case _:
999
- raise ValueError
1000
- elif fg is _unset:
1001
- match args:
1002
- case []:
1003
- fg = None
1004
- case [_ as fg]:
1005
- pass
1006
- case _:
1007
- raise ValueError
1008
- elif bg is _unset:
1009
- match args:
1010
- case []:
1011
- bg = None
1012
- case [_ as bg]:
1013
- pass
1014
- case _:
1015
- raise ValueError
1016
- for k, v in dict.items({'fg': fg, 'bg': bg}):
1017
- if v is None:
998
+ for k, v in locals().items():
999
+ if k not in {"fg", "bg"} or v is None:
1018
1000
  continue
1019
1001
  match v:
1020
1002
  case Color(rgb=(_ as r, _ as g, _ as b)):
@@ -1028,27 +1010,21 @@ def _make_colorstr[_T: ColorStr](cls: type[_T], obj=_unset, *args, **kwargs) ->
1028
1010
  case _:
1029
1011
  raise TypeError(type(v))
1030
1012
  sgr.append(ansi_type.from_rgb((k, (r, g, b))).to_param_buffer())
1031
- except (ValueError, TypeError) as e:
1032
- if isinstance(e, ValueError):
1033
- err = ValueError(
1034
- "ColorStr expected at most 2 positional arguments, " f"got {len(args)}"
1035
- )
1036
- else:
1037
- [typ] = e.args
1038
- err = TypeError(
1039
- "expected integer or vector of 3 integers, "
1040
- f"got {typ.__name__!r} object instead"
1041
- )
1013
+ except TypeError as e:
1014
+ [typ] = e.args
1015
+ err = TypeError(
1016
+ "expected integer or vector of 3 integers, "
1017
+ f"got {typ.__name__!r} object instead"
1018
+ )
1042
1019
  err.__cause__ = e.__cause__
1043
1020
  raise err
1044
- inst: Any = str.__new__(
1045
- cls, ''.join([str(sgr), base_str, SGR_RESET_S if reset else ''])
1046
- )
1021
+ suffix = SGR_RESET_S if reset else ''
1022
+ inst: Any = str.__new__(cls, f"{sgr}{base_str}{suffix}")
1047
1023
  inst.__dict__ |= {
1048
1024
  '_sgr': sgr,
1049
1025
  '_base_str': base_str,
1050
1026
  '_ansi_type': ansi_type,
1051
- '_reset': reset,
1027
+ '_reset': suffix,
1052
1028
  }
1053
1029
  return inst
1054
1030
 
@@ -1062,7 +1038,7 @@ class ColorStr(str):
1062
1038
  sgr = kwargs.get('sgr', self._sgr)
1063
1039
  base_str = kwargs.get('base_str', self.base_str)
1064
1040
  suffix = SGR_RESET_S if kwargs.get('reset', self.reset) else ''
1065
- inst: Any = str.__new__(type(self), ''.join([str(sgr), base_str, suffix]))
1041
+ inst = super().__new__(type(self), f"{sgr}{base_str}{suffix}")
1066
1042
  inst.__dict__ |= vars(self) | {f'_{k}': v for k, v in kwargs.items()}
1067
1043
  return inst
1068
1044
 
@@ -1070,7 +1046,7 @@ class ColorStr(str):
1070
1046
  r"""Returns a 3-tuple of parts of the string
1071
1047
  (sgr, base string, '\x1B[0m' or '')
1072
1048
  """
1073
- return str(self._sgr), self.base_str, SGR_RESET_S if self.reset else ''
1049
+ return str(self._sgr), self.base_str, self._reset
1074
1050
 
1075
1051
  def as_ansi_type(self, __ansi_type):
1076
1052
  """Convert all ANSI colors in the `ColorStr` to a single ANSI type.
@@ -1091,10 +1067,7 @@ class ColorStr(str):
1091
1067
  if self.rgb_dict and ansi_type is not self.ansi_format:
1092
1068
  sgr = SgrSequence(self._sgr)
1093
1069
  sgr.rgb_dict = sgr._rgb_dict, ansi_type
1094
- inst = str.__new__(
1095
- type(self),
1096
- ''.join([str(sgr), self.base_str, SGR_RESET_S if self.reset else '']),
1097
- )
1070
+ inst = super().__new__(type(self), f"{sgr}{self.base_str}{self._reset}")
1098
1071
  inst.__dict__ |= vars(self) | {'_sgr': sgr, '_ansi_type': ansi_type}
1099
1072
  return inst
1100
1073
  return self
@@ -1288,10 +1261,7 @@ class ColorStr(str):
1288
1261
  sgr.remove(bx)
1289
1262
  else:
1290
1263
  sgr.append(bx)
1291
- inst = super().__new__(
1292
- type(self),
1293
- ''.join([str(sgr), self.base_str, SGR_RESET_S if self.reset else '']),
1294
- )
1264
+ inst = super().__new__(type(self), f"{sgr}{self.base_str}{self._reset}")
1295
1265
  inst.__dict__ |= vars(self) | {
1296
1266
  '_sgr': sgr,
1297
1267
  '_ansi_type': sgr.ansi_type() or self.ansi_format,
@@ -1371,7 +1341,7 @@ class ColorStr(str):
1371
1341
  )
1372
1342
  )
1373
1343
 
1374
- def ljust(self, __width, __fillchar=" "):
1344
+ def ljust(self, __width, __fillchar=' '):
1375
1345
  return self._weak_var_update(base_str=self.base_str.ljust(__width, __fillchar))
1376
1346
 
1377
1347
  def lower(self):
@@ -1519,7 +1489,7 @@ class ColorStr(str):
1519
1489
  return self._weak_var_update(base_str=self.base_str * __value)
1520
1490
 
1521
1491
  def __new__(cls, obj=_unset, *args, **kwargs):
1522
- return _make_colorstr(cls, obj, *args, **kwargs)
1492
+ return _colorstr(cls, obj, *args, **kwargs)
1523
1493
 
1524
1494
  def __repr__(self):
1525
1495
  return f'{type(self).__name__}({str(self)!r})'
@@ -1574,7 +1544,7 @@ class ColorStr(str):
1574
1544
 
1575
1545
  @property
1576
1546
  def reset(self):
1577
- return getattr(self, '_reset')
1547
+ return bool(self._reset)
1578
1548
 
1579
1549
  @property
1580
1550
  def rgb_dict(self):
@@ -54,7 +54,7 @@ from chromatic._typing import (
54
54
  TupleOf3,
55
55
  )
56
56
 
57
-
57
+ def is_vt_enabled() -> bool: ...
58
58
  @overload
59
59
  def get_ansi_type[_T: AnsiColorType](typ: _T) -> _T: ...
60
60
  @overload
@@ -68,7 +68,7 @@ def randcolor() -> Color: ...
68
68
  def rgb2ansi_escape(
69
69
  fmt: AnsiColorAlias | AnsiColorType, mode: ColorDictKeys, rgb: Int3Tuple
70
70
  ) -> bytes: ...
71
- def sgr_re_pattern() -> re.Pattern[str]: ...
71
+ def sgr_pattern() -> re.Pattern[str]: ...
72
72
 
73
73
  class ansicolor4Bit(colorbytes):
74
74
  alias: ClassVar[L['4b']]
@@ -266,6 +266,7 @@ class ColorStr(str):
266
266
  @property
267
267
  def rgb_dict(self) -> MappingProxyType[ColorDictKeys, Int3Tuple]: ...
268
268
  _sgr: SgrSequence
269
+ _reset: L["\x1b[0m", ""]
269
270
 
270
271
  class _ColorChainKwargs(TypedDict, total=False):
271
272
  ansi_type: AnsiColorAlias | type[AnsiColorFormat]
@@ -417,7 +417,7 @@ class _color_ns_getter:
417
417
  raise err
418
418
 
419
419
 
420
- def rgb_dispatch(names=()):
420
+ def rgb_dispatch(*names: str):
421
421
  def decorator(__f):
422
422
  def fix_signature(__f):
423
423
  from .._typing import eval_annotation
@@ -430,9 +430,9 @@ def rgb_dispatch(names=()):
430
430
  if not (isbuiltin(__f) or getattr(__f, '__module__', '') == 'builtins'):
431
431
  raise
432
432
  return signature(lambda *args, **kwargs: ...)
433
- variadic = set(
433
+ variadic = {
434
434
  name for name in [argspec.varargs, argspec.varkw] if name is not None
435
- )
435
+ }
436
436
  all_arg_names = variadic.union(argspec.args + argspec.kwonlyargs)
437
437
  rgb_args = all_arg_names.intersection(
438
438
  dict.get({'*': argspec.varargs, '**': argspec.varkw}, arg, arg)
@@ -491,21 +491,16 @@ def rgb_dispatch(names=()):
491
491
  setattr(__f, '__signature__', wrapper_sig)
492
492
  return update_wrapper(wrapper, __f)
493
493
 
494
- if callable(names):
495
- user_func, names = names, ()
496
- return decorator(user_func)
497
- elif isinstance(names, tuple):
498
- if any(type(x) is not str for x in names):
499
- clsname = next(t for t in map(type, names) if t is not str).__name__
494
+ if names and callable(names[0]):
495
+ func, *names = names
496
+ else:
497
+ func = None
498
+ for x in names:
499
+ if type(x) is not str:
500
500
  raise TypeError(
501
- f"expected tuple of strings, "
502
- f"got tuple containing {clsname!r} object instead"
501
+ f"found {type(x).__name__!r} object in names, expected only str"
503
502
  )
504
- return decorator
505
- raise TypeError(
506
- "expected callable or variable names tuple, "
507
- f"got {type(names).__name__!r} object instead"
508
- )
503
+ return decorator if func is None else decorator(func)
509
504
 
510
505
 
511
506
  def _make_named_color_map() -> ...:
@@ -564,19 +559,19 @@ named_color = _make_named_color_map()
564
559
 
565
560
  def named_color_idents():
566
561
  return [
567
- ColorStr(name.replace('_', ' ').lower(), color, ansi_type='24b')
562
+ ColorStr(name.translate({0x5F: 0x20}).lower(), color, ansi_type='24b')
568
563
  for name, color in ColorNamespace.asdict().items()
569
564
  ]
570
565
 
571
566
 
572
- def __getattr__(name: ...) -> ...:
573
- if name == 'Back':
574
- return AnsiBack()
575
- if name == 'Fore':
576
- return AnsiFore()
577
- if name == 'Style':
578
- return AnsiStyle()
579
- raise AttributeError(f"Module {__name__!r} has no attribute {name!r}")
567
+ def __getattr__(name: str) -> ...:
568
+ try:
569
+ return globals().setdefault(
570
+ name, {'Back': AnsiBack, 'Fore': AnsiFore, 'Style': AnsiStyle}[name]()
571
+ )
572
+ except KeyError:
573
+ pass
574
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
580
575
 
581
576
 
582
577
  if TYPE_CHECKING:
@@ -291,11 +291,9 @@ class ColorNamespace[NamedColor = Color](DynamicNamespace[NamedColor]):
291
291
  named_color: MappingProxyType[tuple[Literal['4b', '24b'], str], Color]
292
292
 
293
293
  @overload
294
- def rgb_dispatch[_F: Callable[..., Any]](__f: _F, /) -> _F: ...
294
+ def rgb_dispatch[_F: Callable[..., Any]](__f: _F, /, *names: str) -> _F: ...
295
295
  @overload
296
- def rgb_dispatch[_F: Callable[..., Any]](
297
- names: tuple[str, ...] = (),
298
- ) -> Callable[[_F], _F]: ...
296
+ def rgb_dispatch[_F: Callable[..., Any]](*names: str) -> Callable[[_F], _F]: ...
299
297
 
300
298
  Back: AnsiBack
301
299
  Fore: AnsiFore
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  import os
3
3
  import os.path as osp
4
- from dataclasses import dataclass, field
4
+ from dataclasses import field, dataclass, MISSING, fields
5
5
  from types import MappingProxyType
6
6
  from typing import AnyStr, TYPE_CHECKING
7
7
 
@@ -23,7 +23,7 @@ if not osp.exists(os.environ["CHROMATIC_FONTDIR"]):
23
23
  _TRUETYPE_EXT = frozenset({'.ttf', '.ttc'})
24
24
 
25
25
 
26
- @dataclass(frozen=True, slots=True)
26
+ @dataclass(frozen=True, slots=True, repr=False)
27
27
  class UserFont:
28
28
  font: str
29
29
  size: int = field(default=24, kw_only=True)
@@ -38,6 +38,21 @@ class UserFont:
38
38
  osp.join(os.environ["CHROMATIC_DATADIR"], self.font), strict=True
39
39
  )
40
40
 
41
+ def __repr__(self):
42
+ cls = type(self)
43
+ inst_fields = {}
44
+ for f in fields(cls): # noqa
45
+ v = getattr(self, f.name)
46
+ if f.default is MISSING or v != f.default:
47
+ inst_fields[f.name] = v
48
+ try:
49
+ inst_fields["font"] = os.fspath(self)
50
+ except (OSError, FileNotFoundError):
51
+ pass
52
+ return f"{cls.__name__}(%s)" % ', '.join(
53
+ f"{k}={v!r}" for k, v in inst_fields.items()
54
+ )
55
+
41
56
  def to_truetype(self):
42
57
  from PIL.ImageFont import truetype
43
58
 
@@ -4,9 +4,15 @@ import sys
4
4
  import time
5
5
  from os import PathLike
6
6
  from pathlib import PurePath
7
+ from random import choices as get_random
7
8
  from types import FunctionType
8
9
  from typing import Callable
9
10
 
11
+ from numpy import ndarray
12
+ from skimage.metrics import mean_squared_error
13
+
14
+ import chromatic as cm
15
+
10
16
 
11
17
  class FunctionNamespace:
12
18
 
@@ -21,35 +27,23 @@ DEMO_FUNCS = FunctionNamespace()
21
27
  @DEMO_FUNCS.register
22
28
  def escher_dragon_ascii():
23
29
  """Displays the image-to-ASCII transform of 'Dragon' by M.C. Escher."""
24
- from chromatic.image import ascii2img, img2ascii
25
- from chromatic.data import userfont, escher
26
-
27
- input_img = escher()
28
- font = userfont['vga437']
30
+ input_img = cm.data.escher()
31
+ font = cm.userfont['vga437']
29
32
  char_set = r" ._-~+<vX♦'^Vx>|πΦ0Ω#$║╫"
30
-
31
- ascii_str = img2ascii(
33
+ ascii_str = cm.img2ascii(
32
34
  input_img, font, factor=240, char_set=char_set, sort_glyphs=True
33
35
  )
34
-
35
- ascii_img = ascii2img(ascii_str, font, font_size=16, fg='white', bg='black')
36
-
36
+ ascii_img = cm.ascii2img(ascii_str, font, font_size=16, fg='white', bg='black')
37
37
  ascii_img.show()
38
38
 
39
39
 
40
40
  @DEMO_FUNCS.register
41
41
  def escher_dragon_256color():
42
42
  """Displays the image-to-ANSI transform of 'Dragon' by M.C. Escher in 8-bit color."""
43
- from chromatic.image import ansi2img, img2ansi
44
- from chromatic.data import userfont, escher
45
-
46
- input_img = escher()
47
- font = userfont['vga437']
48
-
49
- ansi_array = img2ansi(input_img, font, factor=240, ansi_type='8b', equalize=True)
50
-
51
- ansi_img = ansi2img(ansi_array, font, font_size=16)
52
-
43
+ input_img = cm.data.escher()
44
+ font = cm.userfont['vga437']
45
+ ansi_array = cm.img2ansi(input_img, font, factor=240, ansi_type='8b', equalize=True)
46
+ ansi_img = cm.ansi2img(ansi_array, font, font_size=16)
53
47
  ansi_img.show()
54
48
 
55
49
 
@@ -59,101 +53,72 @@ def butterfly_16color():
59
53
 
60
54
  Good ol' C-x M-c M-butterfly...
61
55
  """
62
- from chromatic.color import ansicolor4Bit
63
- from chromatic.image import ansi2img, img2ansi
64
- from chromatic.data import userfont, butterfly
65
-
66
- input_img = butterfly()
67
-
68
- font = userfont['vga437']
69
-
56
+ input_img = cm.data.butterfly()
57
+ font = cm.data.userfont['vga437']
70
58
  char_set = r"'·,•-_→+<>ⁿ*%⌂7√Iï∞πbz£9yîU{}1αHSw♥æ?GX╕╒éà⌡MF╝╩ΘûǃQ½☻Ŷ┤▄╪║▒█"
71
-
72
- ansi_array = img2ansi(
73
- input_img, font, factor=200, char_set=char_set, ansi_type=ansicolor4Bit
59
+ ansi_array = cm.img2ansi(
60
+ input_img,
61
+ font,
62
+ factor=200,
63
+ char_set=char_set,
64
+ equalize=True,
65
+ ansi_type=cm.ansicolor4Bit,
74
66
  )
75
-
76
- ansi_img = ansi2img(ansi_array, font, font_size=16)
77
-
67
+ ansi_img = cm.ansi2img(ansi_array, font, font_size=16)
78
68
  ansi_img.show()
79
69
 
80
70
 
81
71
  @DEMO_FUNCS.register
82
72
  def butterfly_truecolor():
83
73
  """Displays the image-to-ANSI transform of 'Spider Lily & Papilio xuthus' in 24-bit color."""
84
- from chromatic.image import ansi2img, img2ansi
85
- from chromatic.data import userfont, butterfly
86
-
87
- input_img = butterfly()
88
-
89
- font = userfont['vga437']
90
-
91
- ansi_array = img2ansi(
74
+ input_img = cm.data.butterfly()
75
+ font = cm.userfont['vga437']
76
+ ansi_array = cm.img2ansi(
92
77
  input_img, font, factor=200, ansi_type='24b', equalize='white_point'
93
78
  )
94
-
95
- ansi_img = ansi2img(ansi_array, font, font_size=16)
96
-
79
+ ansi_img = cm.ansi2img(ansi_array, font, font_size=16)
97
80
  ansi_img.show()
98
81
 
99
82
 
100
83
  @DEMO_FUNCS.register
101
84
  def butterfly_randcolor():
102
- from chromatic.image import ansi2img, img2ansi
103
- from chromatic.color import randcolor, rgb2hsv, hsv2rgb, Color
104
- from chromatic.data import userfont, butterfly
105
-
106
- input_img = butterfly()
107
-
108
- font = userfont['vga437']
109
-
110
- ansi_array = img2ansi(
85
+ input_img = cm.data.butterfly()
86
+ font = cm.userfont['vga437']
87
+ ansi_array = cm.img2ansi(
111
88
  input_img, font, factor=200, ansi_type='8b', equalize='white_point'
112
89
  )
113
-
114
90
  for row in range(len(ansi_array)):
115
91
  for idx, cs in enumerate(ansi_array[row]):
116
92
  if (fg := cs.fg) is not None:
117
- _, _, v = rgb2hsv(fg.rgb)
118
- h, s, _ = rgb2hsv(randcolor().rgb)
119
- ansi_array[row][idx] = cs.recolor(fg=Color.from_rgb(hsv2rgb((h, s, v))))
120
-
121
- ansi_img = ansi2img(ansi_array, font, font_size=16)
122
-
93
+ _, _, v = cm.color.rgb2hsv(fg.rgb)
94
+ h, s, _ = cm.color.rgb2hsv(cm.color.randcolor().rgb)
95
+ ansi_array[row][idx] = cs.recolor(
96
+ fg=cm.Color.from_rgb(cm.color.hsv2rgb((h, s, v)))
97
+ )
98
+ ansi_img = cm.ansi2img(ansi_array, font, font_size=16)
123
99
  ansi_img.show()
124
100
 
125
101
 
126
102
  @DEMO_FUNCS.register
127
103
  def goblin_virus_truecolor():
128
104
  """`G-O-B-L-I-N VIRUS <https://imgur.com/n0Mng2P>`__"""
129
- from chromatic.image import ansi2img, img2ansi
130
- from chromatic.data import userfont, goblin_virus
131
-
132
- input_img = goblin_virus()
133
-
134
- font = userfont['vga437']
135
-
105
+ input_img = cm.data.goblin_virus()
106
+ font = cm.userfont['vga437']
136
107
  char_set = r' .-|_⌐¬^:()═+<>v≥≤«*»x└┘π╛╘┴┐┌┬╧╚╙X╒╜╨#0╓╝╩╤╥│╔┤├╞╗╦┼╪║╟╠╫╣╬░▒▓█▄▌▐▀'
137
-
138
- ansi_array = img2ansi(
108
+ ansi_array = cm.img2ansi(
139
109
  input_img, font, factor=200, char_set=char_set, ansi_type='24b', equalize=False
140
110
  )
141
-
142
- ansi_img = ansi2img(ansi_array, font, font_size=16)
143
-
111
+ ansi_img = cm.ansi2img(ansi_array, font, font_size=16)
144
112
  ansi_img.show()
145
113
 
146
114
 
147
115
  @DEMO_FUNCS.register
148
116
  def named_colors():
149
- from chromatic.color.palette import named_color_idents, ColorNamespace
150
- from chromatic.color.colorconv import rgb2hsv, rgb2lab
151
-
152
- print(f"{'.'.join([ColorNamespace.__module__, ColorNamespace.__name__])}:")
153
- named = named_color_idents()
117
+ print("{0.__module__}.{0.__name__}:".format(cm.ColorNamespace))
118
+ named = cm.color.palette.named_color_idents()
154
119
  whites = [0]
155
120
  for idx, n in enumerate(named):
156
- hsv = rgb2hsv(n.fg.rgb)
121
+ hsv = cm.color.rgb2hsv(n.fg.rgb)
157
122
  if all(
158
123
  map(lambda i, x: math.isclose(hsv[i], x, abs_tol=0.16), (-1, 1), (1, 0))
159
124
  ):
@@ -161,33 +126,33 @@ def named_colors():
161
126
  whites.pop()
162
127
  whites.append(idx)
163
128
  whites.append(-1)
164
- buffer = []
165
129
  for start, stop in zip(whites, whites[1:]):
166
130
  xs = sorted(
167
131
  named[start + 1 if start else None : stop + 1 if ~stop else None],
168
- key=lambda x: rgb2lab(x.fg.rgb),
132
+ key=lambda x: cm.color.rgb2lab(x.fg.rgb),
169
133
  )
170
- buffer.append(xs)
171
- for line in buffer:
172
- print(' | '.join(line))
134
+ print(' | '.join(xs))
173
135
 
174
136
 
175
137
  @DEMO_FUNCS.register
176
138
  def color_cube():
177
139
  """Print the ANSI256 6x6x6 color cube"""
178
140
  fmt_code = lambda n: f"\x1b[48;5;{n}m{n: >4}"
179
- for i in range(0, 8, 4):
180
- for j in range(i, i + 8):
181
- print(fmt_code(i + j), end='')
182
- else:
141
+ ansi_256_codes = iter(range(0x100))
142
+ for i in range(0x10):
143
+ if i and i % 8 == 0:
183
144
  print("\x1b[m")
184
- for i in range(6):
185
- for j in range(0x10, 0xE8, 6):
186
- print(fmt_code(i + j), end='')
187
- else:
145
+ print(fmt_code(next(ansi_256_codes)), end='')
146
+ else:
147
+ print("\x1b[m")
148
+ for i in range(6**3):
149
+ if i and i % 6**2 == 0:
188
150
  print("\x1b[m")
189
- for i in range(0xE8, 0x100):
190
- print(fmt_code(i), end='')
151
+ print(fmt_code(next(ansi_256_codes)), end='')
152
+ else:
153
+ print("\x1b[m")
154
+ for x in ansi_256_codes:
155
+ print(fmt_code(x), end='')
191
156
  else:
192
157
  print("\x1b[m")
193
158
 
@@ -198,20 +163,11 @@ def color_table():
198
163
 
199
164
  A handful of stylistic SGR parameters are displayed as well.
200
165
  """
201
- from chromatic.color import (
202
- ColorStr,
203
- Color,
204
- SgrParameter,
205
- ansicolor24Bit,
206
- ansicolor4Bit,
207
- ansicolor8Bit,
208
- ColorNamespace,
209
- )
210
166
 
211
- ansi_types = [ansicolor4Bit, ansicolor8Bit, ansicolor24Bit]
167
+ ansi_types = [cm.ansicolor4Bit, cm.ansicolor8Bit, cm.ansicolor24Bit]
212
168
 
213
- colors: dict[str, Color] = {
214
- name.title(): getattr(ColorNamespace, name)
169
+ colors: dict[str, cm.Color] = {
170
+ name.title(): getattr(cm.ColorNamespace, name)
215
171
  for name in [
216
172
  'BLACK',
217
173
  'WHITE',
@@ -226,10 +182,10 @@ def color_table():
226
182
  }
227
183
  spacing = max(map(len, colors)) + 1
228
184
  fg_colors = [
229
- ColorStr(f"{name: ^{spacing}}", fg=color, ansi_type=ansicolor24Bit)
185
+ cm.ColorStr(f"{name: ^{spacing}}", fg=color, ansi_type=cm.ansicolor24Bit)
230
186
  for name, color in colors.items()
231
187
  ]
232
- bg_colors = [ColorStr().recolor(bg=None)] + [
188
+ bg_colors = [cm.ColorStr().recolor(bg=None)] + [
233
189
  c.recolor(fg=None, bg=c.fg) for c in fg_colors
234
190
  ]
235
191
  print(
@@ -244,36 +200,24 @@ def color_table():
244
200
  print()
245
201
  print('\nstyles:', end='\t')
246
202
  style_params = [
247
- SgrParameter.BOLD,
248
- SgrParameter.ITALICS,
249
- SgrParameter.CROSSED_OUT,
250
- SgrParameter.ENCIRCLED,
251
- SgrParameter.SINGLE_UNDERLINE,
252
- SgrParameter.DOUBLE_UNDERLINE,
253
- SgrParameter.NEGATIVE,
203
+ cm.SgrParameter.BOLD,
204
+ cm.SgrParameter.ITALICS,
205
+ cm.SgrParameter.CROSSED_OUT,
206
+ cm.SgrParameter.ENCIRCLED,
207
+ cm.SgrParameter.SINGLE_UNDERLINE,
208
+ cm.SgrParameter.DOUBLE_UNDERLINE,
209
+ cm.SgrParameter.NEGATIVE,
254
210
  ]
255
- for style in style_params[:-1]:
211
+ for style in style_params:
256
212
  print(
257
- ColorStr('.'.join([SgrParameter.__qualname__, style.name])).update_sgr(
213
+ cm.ColorStr(f"{cm.SgrParameter.__qualname__}.{style.name}").update_sgr(
258
214
  style
259
215
  ),
260
- end='\x1b[0m'.ljust(8),
261
- )
262
- else:
263
- print(
264
- ColorStr(f"{SgrParameter.__qualname__}.{style_params[-1].name}").update_sgr(
265
- style_params[-1]
266
- )
216
+ end=('\n' if style is style_params[-1] else "\x1b[0m".ljust(8)),
267
217
  )
268
218
 
269
219
 
270
220
  def glyph_comparisons(__output_dir: str | PathLike[str] = None):
271
- from chromatic import get_glyph_masks
272
- from chromatic.data import userfont
273
- from chromatic.image import cp437_printable
274
- from numpy import ndarray
275
- from random import choices as get_random
276
- from skimage.metrics import mean_squared_error
277
221
 
278
222
  def _find_best_matches(
279
223
  glyph_masks1: dict[str, ndarray], glyph_masks2: dict[str, ndarray]
@@ -292,13 +236,13 @@ def glyph_comparisons(__output_dir: str | PathLike[str] = None):
292
236
 
293
237
  if __output_dir and not os.path.isdir(__output_dir):
294
238
  raise NotADirectoryError(__output_dir)
295
- user_fonts = [pair := (userfont['vga437'], userfont['consolas']), pair[::-1]]
239
+ user_fonts = [pair := (cm.userfont['vga437'], cm.userfont['consolas']), pair[::-1]]
296
240
  trans_table = str.maketrans({']': None, '0': ' ', '[': ' '})
297
- char_set = cp437_printable()
241
+ char_set = cm.cp437_printable()
298
242
  separator = '#' * 100
299
243
  for font1, font2 in user_fonts:
300
- glyph_masks_1 = get_glyph_masks(font1, char_set, dist_transform=True)
301
- glyph_masks_2 = get_glyph_masks(font2, char_set, dist_transform=True)
244
+ glyph_masks_1 = cm.get_glyph_masks(font1, char_set, dist_transform=True)
245
+ glyph_masks_2 = cm.get_glyph_masks(font2, char_set, dist_transform=True)
302
246
  best_matches_ = _find_best_matches(glyph_masks_1, glyph_masks_2)
303
247
  txt = ''.join(
304
248
  '->'.center(32, ' ')
@@ -52,7 +52,7 @@ from ..color.core import (
52
52
  ansicolor4Bit,
53
53
  ansicolor8Bit,
54
54
  get_ansi_type,
55
- sgr_re_pattern,
55
+ sgr_pattern,
56
56
  )
57
57
  from ..color.palette import rgb_dispatch
58
58
  from ..data import UserFont, userfont
@@ -366,7 +366,7 @@ def ansi_quantize(
366
366
  img: RGBArray,
367
367
  ansi_type: type[ansicolor4Bit | ansicolor8Bit],
368
368
  *,
369
- equalize: bool | Literal['white_point'] = True,
369
+ equalize: bool | Literal['white_point'] = False,
370
370
  ):
371
371
  """Color-quantize an RGB array into ANSI 4-bit or 8-bit color space.
372
372
 
@@ -583,7 +583,7 @@ def img2ascii(
583
583
  return ascii_str
584
584
 
585
585
 
586
- @rgb_dispatch(('bg',))
586
+ @rgb_dispatch('bg')
587
587
  def img2ansi(
588
588
  __img: RGBImageLike | PathLike[str] | str,
589
589
  __font: FontArgType = userfont['vga437'],
@@ -591,7 +591,7 @@ def img2ansi(
591
591
  char_set: Iterable[str] = None,
592
592
  ansi_type: AnsiColorParam = DEFAULT_ANSI,
593
593
  sort_glyphs: bool | type[reversed] = True,
594
- equalize: bool | Literal['white_point'] = True,
594
+ equalize: bool | Literal['white_point'] = False,
595
595
  bg: Color | Int3Tuple | str = (0, 0, 0),
596
596
  ):
597
597
  """Convert an image to an ANSI array.
@@ -681,7 +681,7 @@ def img2ansi(
681
681
  return xs
682
682
 
683
683
 
684
- @rgb_dispatch(('fg', 'bg'))
684
+ @rgb_dispatch('fg', 'bg')
685
685
  def ascii2img(
686
686
  __ascii: str,
687
687
  __font: FontArgType = userfont['vga437'],
@@ -733,7 +733,7 @@ def ascii2img(
733
733
  return img
734
734
 
735
735
 
736
- @rgb_dispatch(('fg_default', 'bg_default'))
736
+ @rgb_dispatch('fg_default', 'bg_default')
737
737
  def ansi2img(
738
738
  __ansi_array: list[list[ColorStr]],
739
739
  __font: FontArgType = userfont['vga437'],
@@ -829,7 +829,7 @@ def ansify(
829
829
  char_set: Iterable[str] = None,
830
830
  sort_glyphs: bool | type[reversed] = True,
831
831
  ansi_type: AnsiColorParam = DEFAULT_ANSI,
832
- equalize: bool | Literal['white_point'] = True,
832
+ equalize: bool | Literal['white_point'] = False,
833
833
  fg: Int3Tuple | str = (170, 170, 170),
834
834
  bg: Int3Tuple | str | Literal['auto'] = (0, 0, 0),
835
835
  ):
@@ -886,8 +886,8 @@ def _is_csi_param(__c: str) -> TypeGuard[Literal[';'] | LiteralDigit]:
886
886
 
887
887
 
888
888
  @lru_cache(maxsize=1)
889
- def sgr_span_re_pattern():
890
- sgr_re = sgr_re_pattern().pattern.removeprefix(r'\x1b\[')
889
+ def cursor_or_sgr_pattern():
890
+ sgr_re = sgr_pattern().pattern.removeprefix(r'\x1b\[')
891
891
  return re.compile(
892
892
  rf"(?:\x1b\[(?:(?P<cursor>\d*[A-G]|\d*(?:;\d*)?H)|(?P<sgr>{sgr_re})))?(?P<text>[^\x1b]*)"
893
893
  )
@@ -951,7 +951,7 @@ def _sub_bold_colors(lines: Iterable[str]) -> Iterator[str]:
951
951
  for line in lines:
952
952
  bold_bit = False
953
953
  prev_colors = {}
954
- yield sgr_re_pattern().sub(sub, line)
954
+ yield sgr_pattern().sub(sub, line)
955
955
 
956
956
 
957
957
  def reshape_ansi(__str: str, w: int, h: int) -> str:
@@ -982,7 +982,7 @@ def reshape_ansi(__str: str, w: int, h: int) -> str:
982
982
  cur = cursor()
983
983
  x, y = next(cur)
984
984
  for line in _sub_bold_colors(__str.splitlines()):
985
- for m in sgr_span_re_pattern().finditer(line + '\n'):
985
+ for m in cursor_or_sgr_pattern().finditer(line + '\n'):
986
986
  if cg := m['cursor']:
987
987
  write_cell(' ')
988
988
  param, code = cg[:-1], cg[-1]
@@ -1018,10 +1018,10 @@ def reshape_ansi(__str: str, w: int, h: int) -> str:
1018
1018
  write_cell(ch, incr=True)
1019
1019
 
1020
1020
  out_lines = []
1021
- any_sgr_re = re.compile(r"\x1b\[\d*(?:;\d*)*[A-HJ-KS-Tmf]")
1021
+ any_ansi_seq = re.compile(r"\x1b\[\d*(?:;\d*)*[A-HJ-KS-Tmf]")
1022
1022
  for row in arr:
1023
1023
  out_row = ''.join(cell.translate({0: ' '}) for cell in row)
1024
- lhs, rhs = map(' '.__mul__, divmod(w - len(any_sgr_re.sub('', out_row)), 2))
1024
+ lhs, rhs = map(' '.__mul__, divmod(w - len(any_ansi_seq.sub('', out_row)), 2))
1025
1025
  out_lines.append(f"{lhs}{out_row}{rhs}")
1026
1026
 
1027
1027
  return '\n'.join(out_lines)
@@ -1034,7 +1034,7 @@ def to_sgr_array(__str: str, ansi_type: AnsiColorParam = None):
1034
1034
  xs = []
1035
1035
  for line in _sub_bold_colors(__str.splitlines()):
1036
1036
  x = []
1037
- for m in sgr_span_re_pattern().finditer(line):
1037
+ for m in cursor_or_sgr_pattern().finditer(line):
1038
1038
  text = m["text"]
1039
1039
  if m["sgr"]:
1040
1040
  sgr = SgrSequence(map(int, m["sgr"].removesuffix('m').split(';')))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chromatic-python
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: ANSI art image processing and colored terminal text
5
5
  Author: crypt0lith
6
6
  Project-URL: Homepage, https://github.com/crypt0lith/chromatic
@@ -6,6 +6,7 @@ pyproject.toml
6
6
  requirements.txt
7
7
  chromatic/__init__.py
8
8
  chromatic/__init__.pyi
9
+ chromatic/__main__.py
9
10
  chromatic/_typing.py
10
11
  chromatic/_version.py
11
12
  chromatic/demo.py