PaintByChar 0.7.0__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.
- paintbychar-0.7.0/LICENSE +21 -0
- paintbychar-0.7.0/MANIFEST.in +2 -0
- paintbychar-0.7.0/PKG-INFO +140 -0
- paintbychar-0.7.0/README.md +103 -0
- paintbychar-0.7.0/pyproject.toml +27 -0
- paintbychar-0.7.0/setup.cfg +30 -0
- paintbychar-0.7.0/src/PaintByChar.egg-info/PKG-INFO +140 -0
- paintbychar-0.7.0/src/PaintByChar.egg-info/SOURCES.txt +12 -0
- paintbychar-0.7.0/src/PaintByChar.egg-info/dependency_links.txt +1 -0
- paintbychar-0.7.0/src/PaintByChar.egg-info/requires.txt +2 -0
- paintbychar-0.7.0/src/PaintByChar.egg-info/top_level.txt +2 -0
- paintbychar-0.7.0/src/paintbychar.py +266 -0
- paintbychar-0.7.0/tests/test_main.py +290 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 James Derrick
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PaintByChar
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Convert grid strings to colored images with per-character coloring.
|
|
5
|
+
Home-page: https://github.com/jgd10/PaintByChar
|
|
6
|
+
Author: James Derrick
|
|
7
|
+
Author-email: James Derrick <jgd10.github@gmail.com>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2025 James Derrick
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
|
|
30
|
+
Project-URL: Homepage, https://github.com/jgd10/PaintByChar
|
|
31
|
+
Requires-Python: >=3.12
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
License-File: LICENSE
|
|
34
|
+
Requires-Dist: pillow
|
|
35
|
+
Requires-Dist: matplotlib
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# PaintByChar
|
|
39
|
+
|
|
40
|
+
<img src="assets/logo.png" alt="PaintByChar logo" width="200" />
|
|
41
|
+
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
[](https://python.org)
|
|
44
|
+
[](https://pypi.org/project/PaintByChar/)
|
|
45
|
+
[](https://paintbychar.readthedocs.io/en/latest/)
|
|
46
|
+
[](https://github.com/jgd10/PaintByChar/actions/workflows/python-tests.yml)
|
|
47
|
+
|
|
48
|
+
A tiny Python library for turning ASCII grids into colorful images. It is designed for quick visualizations of mazes, maps, game boards, and Advent of Code-style 2D text puzzles.
|
|
49
|
+
|
|
50
|
+
## Why PaintByChar?
|
|
51
|
+
|
|
52
|
+
- Turn a rectangular text grid into a rendered image in a few lines
|
|
53
|
+
- Map each character to a color automatically or via your own palette
|
|
54
|
+
- Support multiple rendering styles for cells, text, and background-label combinations
|
|
55
|
+
- Works well for generative art, puzzle visualizations, and debugging text-based layouts
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- `string_to_image()` for direct text-to-image conversion
|
|
60
|
+
- `file_to_image()` for processing a text file as a grid
|
|
61
|
+
- Preset colormaps such as `viridis`, `plasma`, and `terrain`
|
|
62
|
+
- Custom `value_colors` dictionaries for full control over mapping
|
|
63
|
+
- Render modes: filled cells, colored text, and colored cells with background text
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install paintbychar
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Quick example
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
import paintbychar as pbc
|
|
75
|
+
|
|
76
|
+
grid = """0123
|
|
77
|
+
4567
|
|
78
|
+
89AB
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
img = pbc.string_to_image(
|
|
82
|
+
grid,
|
|
83
|
+
preset="viridis",
|
|
84
|
+
cell_size=24,
|
|
85
|
+
render_style=pbc.RenderStyle.COLORED_CELLS,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
pbc.save_image(img, "output_basic.png")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Example output
|
|
92
|
+
|
|
93
|
+
<div align="center">
|
|
94
|
+
<img src="README_assets/output_basic.png" alt="Basic example output" width="640" />
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
### More example images
|
|
98
|
+
|
|
99
|
+
<div align="center">
|
|
100
|
+
<img src="README_assets/style_colored_text.png" alt="Styled text output" width="320" />
|
|
101
|
+
<img src="README_assets/output_preset_plasma.png" alt="Preset output" width="320" />
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
## More examples
|
|
105
|
+
|
|
106
|
+
The repository includes several runnable examples in the [`examples/`](examples/) folder, including:
|
|
107
|
+
|
|
108
|
+
- `basic_example.py`
|
|
109
|
+
- `file_example.py`
|
|
110
|
+
- `styles_example.py`
|
|
111
|
+
- `preset_example.py`
|
|
112
|
+
- `advent_of_code_example.py`
|
|
113
|
+
|
|
114
|
+
## API reference
|
|
115
|
+
|
|
116
|
+
See the [documentation](docs/api.md) for a quick overview of the public functions
|
|
117
|
+
and options, or browse the [live docs site](https://paintbychar.readthedocs.io/en/latest/).
|
|
118
|
+
|
|
119
|
+
There is also documentation on Read the Docs with installation instructions,
|
|
120
|
+
usage examples, and API reference. [Read the Docs](https://paintbychar.readthedocs.io/en/latest/index.html)
|
|
121
|
+
|
|
122
|
+
## Contributing
|
|
123
|
+
|
|
124
|
+
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions, testing guidance, and the pull request process.
|
|
125
|
+
|
|
126
|
+
## Code of conduct
|
|
127
|
+
|
|
128
|
+
This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md).
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
133
|
+
|
|
134
|
+
## Project status
|
|
135
|
+
|
|
136
|
+
PaintByChar is currently a small, focused library aimed at grid-based text rendering and visualization. It is suitable for personal, educational, and open-source use, and is prepared for broader community contributions as it matures.
|
|
137
|
+
|
|
138
|
+
## Social preview
|
|
139
|
+
|
|
140
|
+
A repository preview image is included in [`assets/social-preview.png`](assets/social-preview.png) and can be used as a GitHub social preview or banner asset.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# PaintByChar
|
|
2
|
+
|
|
3
|
+
<img src="assets/logo.png" alt="PaintByChar logo" width="200" />
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://python.org)
|
|
7
|
+
[](https://pypi.org/project/PaintByChar/)
|
|
8
|
+
[](https://paintbychar.readthedocs.io/en/latest/)
|
|
9
|
+
[](https://github.com/jgd10/PaintByChar/actions/workflows/python-tests.yml)
|
|
10
|
+
|
|
11
|
+
A tiny Python library for turning ASCII grids into colorful images. It is designed for quick visualizations of mazes, maps, game boards, and Advent of Code-style 2D text puzzles.
|
|
12
|
+
|
|
13
|
+
## Why PaintByChar?
|
|
14
|
+
|
|
15
|
+
- Turn a rectangular text grid into a rendered image in a few lines
|
|
16
|
+
- Map each character to a color automatically or via your own palette
|
|
17
|
+
- Support multiple rendering styles for cells, text, and background-label combinations
|
|
18
|
+
- Works well for generative art, puzzle visualizations, and debugging text-based layouts
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- `string_to_image()` for direct text-to-image conversion
|
|
23
|
+
- `file_to_image()` for processing a text file as a grid
|
|
24
|
+
- Preset colormaps such as `viridis`, `plasma`, and `terrain`
|
|
25
|
+
- Custom `value_colors` dictionaries for full control over mapping
|
|
26
|
+
- Render modes: filled cells, colored text, and colored cells with background text
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install paintbychar
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick example
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import paintbychar as pbc
|
|
38
|
+
|
|
39
|
+
grid = """0123
|
|
40
|
+
4567
|
|
41
|
+
89AB
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
img = pbc.string_to_image(
|
|
45
|
+
grid,
|
|
46
|
+
preset="viridis",
|
|
47
|
+
cell_size=24,
|
|
48
|
+
render_style=pbc.RenderStyle.COLORED_CELLS,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
pbc.save_image(img, "output_basic.png")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Example output
|
|
55
|
+
|
|
56
|
+
<div align="center">
|
|
57
|
+
<img src="README_assets/output_basic.png" alt="Basic example output" width="640" />
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
### More example images
|
|
61
|
+
|
|
62
|
+
<div align="center">
|
|
63
|
+
<img src="README_assets/style_colored_text.png" alt="Styled text output" width="320" />
|
|
64
|
+
<img src="README_assets/output_preset_plasma.png" alt="Preset output" width="320" />
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
## More examples
|
|
68
|
+
|
|
69
|
+
The repository includes several runnable examples in the [`examples/`](examples/) folder, including:
|
|
70
|
+
|
|
71
|
+
- `basic_example.py`
|
|
72
|
+
- `file_example.py`
|
|
73
|
+
- `styles_example.py`
|
|
74
|
+
- `preset_example.py`
|
|
75
|
+
- `advent_of_code_example.py`
|
|
76
|
+
|
|
77
|
+
## API reference
|
|
78
|
+
|
|
79
|
+
See the [documentation](docs/api.md) for a quick overview of the public functions
|
|
80
|
+
and options, or browse the [live docs site](https://paintbychar.readthedocs.io/en/latest/).
|
|
81
|
+
|
|
82
|
+
There is also documentation on Read the Docs with installation instructions,
|
|
83
|
+
usage examples, and API reference. [Read the Docs](https://paintbychar.readthedocs.io/en/latest/index.html)
|
|
84
|
+
|
|
85
|
+
## Contributing
|
|
86
|
+
|
|
87
|
+
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions, testing guidance, and the pull request process.
|
|
88
|
+
|
|
89
|
+
## Code of conduct
|
|
90
|
+
|
|
91
|
+
This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md).
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
96
|
+
|
|
97
|
+
## Project status
|
|
98
|
+
|
|
99
|
+
PaintByChar is currently a small, focused library aimed at grid-based text rendering and visualization. It is suitable for personal, educational, and open-source use, and is prepared for broader community contributions as it matures.
|
|
100
|
+
|
|
101
|
+
## Social preview
|
|
102
|
+
|
|
103
|
+
A repository preview image is included in [`assets/social-preview.png`](assets/social-preview.png) and can be used as a GitHub social preview or banner asset.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "PaintByChar"
|
|
7
|
+
version = "0.7.0"
|
|
8
|
+
description = "Convert grid strings to colored images with per-character coloring."
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "James Derrick", email = "jgd10.github@gmail.com" }
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = { file = "LICENSE" }
|
|
14
|
+
requires-python = ">=3.12"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"pillow",
|
|
17
|
+
"matplotlib"
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/jgd10/PaintByChar"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools]
|
|
24
|
+
package-dir = {"" = "src"}
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["src"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = paintbychar
|
|
3
|
+
version = 0.1.0
|
|
4
|
+
description = Character-block image generator
|
|
5
|
+
long_description = file: README.md
|
|
6
|
+
long_description_content_type = text/markdown
|
|
7
|
+
author = James Derrick
|
|
8
|
+
author_email = jgd10.github@gmail.com
|
|
9
|
+
url = https://github.com/jgd10/PaintByChar
|
|
10
|
+
license = MIT
|
|
11
|
+
classifiers =
|
|
12
|
+
Programming Language :: Python :: 3
|
|
13
|
+
License :: OSI Approved :: MIT License
|
|
14
|
+
Operating System :: OS Independent
|
|
15
|
+
|
|
16
|
+
[options]
|
|
17
|
+
py_modules =
|
|
18
|
+
main
|
|
19
|
+
paintbychar
|
|
20
|
+
package_dir =
|
|
21
|
+
= src
|
|
22
|
+
install_requires =
|
|
23
|
+
pillow
|
|
24
|
+
matplotlib
|
|
25
|
+
python_requires = >=3.12
|
|
26
|
+
|
|
27
|
+
[egg_info]
|
|
28
|
+
tag_build =
|
|
29
|
+
tag_date = 0
|
|
30
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PaintByChar
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Convert grid strings to colored images with per-character coloring.
|
|
5
|
+
Home-page: https://github.com/jgd10/PaintByChar
|
|
6
|
+
Author: James Derrick
|
|
7
|
+
Author-email: James Derrick <jgd10.github@gmail.com>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2025 James Derrick
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
|
|
30
|
+
Project-URL: Homepage, https://github.com/jgd10/PaintByChar
|
|
31
|
+
Requires-Python: >=3.12
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
License-File: LICENSE
|
|
34
|
+
Requires-Dist: pillow
|
|
35
|
+
Requires-Dist: matplotlib
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# PaintByChar
|
|
39
|
+
|
|
40
|
+
<img src="assets/logo.png" alt="PaintByChar logo" width="200" />
|
|
41
|
+
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
[](https://python.org)
|
|
44
|
+
[](https://pypi.org/project/PaintByChar/)
|
|
45
|
+
[](https://paintbychar.readthedocs.io/en/latest/)
|
|
46
|
+
[](https://github.com/jgd10/PaintByChar/actions/workflows/python-tests.yml)
|
|
47
|
+
|
|
48
|
+
A tiny Python library for turning ASCII grids into colorful images. It is designed for quick visualizations of mazes, maps, game boards, and Advent of Code-style 2D text puzzles.
|
|
49
|
+
|
|
50
|
+
## Why PaintByChar?
|
|
51
|
+
|
|
52
|
+
- Turn a rectangular text grid into a rendered image in a few lines
|
|
53
|
+
- Map each character to a color automatically or via your own palette
|
|
54
|
+
- Support multiple rendering styles for cells, text, and background-label combinations
|
|
55
|
+
- Works well for generative art, puzzle visualizations, and debugging text-based layouts
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- `string_to_image()` for direct text-to-image conversion
|
|
60
|
+
- `file_to_image()` for processing a text file as a grid
|
|
61
|
+
- Preset colormaps such as `viridis`, `plasma`, and `terrain`
|
|
62
|
+
- Custom `value_colors` dictionaries for full control over mapping
|
|
63
|
+
- Render modes: filled cells, colored text, and colored cells with background text
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install paintbychar
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Quick example
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
import paintbychar as pbc
|
|
75
|
+
|
|
76
|
+
grid = """0123
|
|
77
|
+
4567
|
|
78
|
+
89AB
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
img = pbc.string_to_image(
|
|
82
|
+
grid,
|
|
83
|
+
preset="viridis",
|
|
84
|
+
cell_size=24,
|
|
85
|
+
render_style=pbc.RenderStyle.COLORED_CELLS,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
pbc.save_image(img, "output_basic.png")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Example output
|
|
92
|
+
|
|
93
|
+
<div align="center">
|
|
94
|
+
<img src="README_assets/output_basic.png" alt="Basic example output" width="640" />
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
### More example images
|
|
98
|
+
|
|
99
|
+
<div align="center">
|
|
100
|
+
<img src="README_assets/style_colored_text.png" alt="Styled text output" width="320" />
|
|
101
|
+
<img src="README_assets/output_preset_plasma.png" alt="Preset output" width="320" />
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
## More examples
|
|
105
|
+
|
|
106
|
+
The repository includes several runnable examples in the [`examples/`](examples/) folder, including:
|
|
107
|
+
|
|
108
|
+
- `basic_example.py`
|
|
109
|
+
- `file_example.py`
|
|
110
|
+
- `styles_example.py`
|
|
111
|
+
- `preset_example.py`
|
|
112
|
+
- `advent_of_code_example.py`
|
|
113
|
+
|
|
114
|
+
## API reference
|
|
115
|
+
|
|
116
|
+
See the [documentation](docs/api.md) for a quick overview of the public functions
|
|
117
|
+
and options, or browse the [live docs site](https://paintbychar.readthedocs.io/en/latest/).
|
|
118
|
+
|
|
119
|
+
There is also documentation on Read the Docs with installation instructions,
|
|
120
|
+
usage examples, and API reference. [Read the Docs](https://paintbychar.readthedocs.io/en/latest/index.html)
|
|
121
|
+
|
|
122
|
+
## Contributing
|
|
123
|
+
|
|
124
|
+
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions, testing guidance, and the pull request process.
|
|
125
|
+
|
|
126
|
+
## Code of conduct
|
|
127
|
+
|
|
128
|
+
This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md).
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
133
|
+
|
|
134
|
+
## Project status
|
|
135
|
+
|
|
136
|
+
PaintByChar is currently a small, focused library aimed at grid-based text rendering and visualization. It is suitable for personal, educational, and open-source use, and is prepared for broader community contributions as it matures.
|
|
137
|
+
|
|
138
|
+
## Social preview
|
|
139
|
+
|
|
140
|
+
A repository preview image is included in [`assets/social-preview.png`](assets/social-preview.png) and can be used as a GitHub social preview or banner asset.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
setup.cfg
|
|
6
|
+
src/paintbychar.py
|
|
7
|
+
src/PaintByChar.egg-info/PKG-INFO
|
|
8
|
+
src/PaintByChar.egg-info/SOURCES.txt
|
|
9
|
+
src/PaintByChar.egg-info/dependency_links.txt
|
|
10
|
+
src/PaintByChar.egg-info/requires.txt
|
|
11
|
+
src/PaintByChar.egg-info/top_level.txt
|
|
12
|
+
tests/test_main.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
6
|
+
from typing import Tuple, Union
|
|
7
|
+
|
|
8
|
+
from PIL.ImageFont import FreeTypeFont
|
|
9
|
+
|
|
10
|
+
__version__ = "0.7.0"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
COLOR_PRESETS: dict[str, tuple[int, int, int]] = {
|
|
14
|
+
"white": (255, 255, 255),
|
|
15
|
+
"black": (0, 0, 0),
|
|
16
|
+
"light_gray": (245, 245, 245),
|
|
17
|
+
"dark_gray": (50, 50, 50),
|
|
18
|
+
"pastel_blue": (174, 198, 207),
|
|
19
|
+
"pastel_green": (152, 251, 152),
|
|
20
|
+
"pastel_pink": (255, 182, 193),
|
|
21
|
+
"cream": (255, 253, 208),
|
|
22
|
+
"beige": (245, 245, 220),
|
|
23
|
+
"mint": (189, 252, 201),
|
|
24
|
+
"navy": (10, 25, 47),
|
|
25
|
+
"charcoal": (34, 40, 49),
|
|
26
|
+
"soft_yellow": (255, 250, 205),
|
|
27
|
+
"gray": (128, 128, 128),
|
|
28
|
+
"blue": (70, 130, 180),
|
|
29
|
+
"green": (60, 179, 113),
|
|
30
|
+
"pink": (255, 182, 193),
|
|
31
|
+
"yellow": (255, 223, 0),
|
|
32
|
+
"teal": (0, 128, 128),
|
|
33
|
+
"brown": (139, 69, 19),
|
|
34
|
+
"red": (220, 20, 60),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def resolve_color(value: Union[str, Tuple[int, int, int]]) -> tuple[int, int, int]:
|
|
39
|
+
"""
|
|
40
|
+
Resolve a color value which can be:
|
|
41
|
+
- a preset name from BG_PRESETS (e.g., 'cream')
|
|
42
|
+
- an RGB tuple already (e.g., (255, 255, 255))
|
|
43
|
+
Returns an (R, G, B) tuple.
|
|
44
|
+
"""
|
|
45
|
+
if isinstance(value, tuple):
|
|
46
|
+
if len(value) != 3:
|
|
47
|
+
raise ValueError(f"RGB tuple must have 3 elements, got {len(value)}")
|
|
48
|
+
for channel in value:
|
|
49
|
+
if not isinstance(channel, int):
|
|
50
|
+
raise ValueError(f"RGB channel value {channel} is not an integer")
|
|
51
|
+
if not (0 <= channel <= 255):
|
|
52
|
+
raise ValueError(f"RGB channel value {channel} out of range 0-255")
|
|
53
|
+
return value
|
|
54
|
+
if isinstance(value, str):
|
|
55
|
+
if value.lower() in COLOR_PRESETS:
|
|
56
|
+
return COLOR_PRESETS[value]
|
|
57
|
+
raise ValueError(f"Unsupported bg color: {value}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RenderStyle(Enum):
|
|
62
|
+
"""Enumeration for fill options in the image generation."""
|
|
63
|
+
COLORED_CELLS = "colored_cells"
|
|
64
|
+
COLORED_TEXT = "colored_text"
|
|
65
|
+
COLORED_CELLS_WITH_BACKGROUND_TEXT = (
|
|
66
|
+
"colored_cells_with_background_text"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_colormap_dict(colormap_name: str) -> dict[str, tuple[int, ...]]:
|
|
71
|
+
"""Generate a color mapping dictionary from a matplotlib colormap.
|
|
72
|
+
|
|
73
|
+
Uses the characters '0'-'9' as keys and maps them to colors sampled from the
|
|
74
|
+
specified colormap. Supported colormaps include 'viridis', 'plasma',
|
|
75
|
+
'inferno', 'magma', 'cividis', 'terrain', and 'coolwarm'.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
colormap_name (str): Name of the matplotlib colormap to use.
|
|
79
|
+
Returns:
|
|
80
|
+
dict[str, tuple[int, ...]]: A dictionary mapping string digits '0'-'9' to RGB color tuples.
|
|
81
|
+
"""
|
|
82
|
+
cmap = plt.get_cmap(colormap_name)
|
|
83
|
+
colors = [tuple(int(255 * c) for c in cmap(i / 9)[:3]) for i in range(10)]
|
|
84
|
+
return {str(i): colors[i] for i in range(10)}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
PRESETS = {'viridis': get_colormap_dict('viridis'),
|
|
88
|
+
'plasma': get_colormap_dict('plasma'),
|
|
89
|
+
'inferno': get_colormap_dict('inferno'),
|
|
90
|
+
'magma': get_colormap_dict('magma'),
|
|
91
|
+
'cividis': get_colormap_dict('cividis'), #
|
|
92
|
+
'terrain': get_colormap_dict('terrain'),
|
|
93
|
+
'coolwarm': get_colormap_dict(
|
|
94
|
+
'coolwarm')} # type: dict[str, dict[str, tuple[int, ...]]]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class InputError(Exception):
|
|
98
|
+
"""Custom exception for invalid input data."""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def check_grid_string(grid_str: str) -> bool:
|
|
103
|
+
"""Check if all lines in the string block have the same length.
|
|
104
|
+
|
|
105
|
+
Input can only be a rectangular grid of single characters
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
grid_str (str): The string block representing the grid.
|
|
109
|
+
Raises:
|
|
110
|
+
InputError: If the lines have inconsistent lengths.
|
|
111
|
+
Returns:
|
|
112
|
+
bool: True if all lines have the same length.
|
|
113
|
+
"""
|
|
114
|
+
lines = grid_str.strip().split('\n')
|
|
115
|
+
max_width = 0
|
|
116
|
+
for line in lines:
|
|
117
|
+
max_width = max(max_width, len(line))
|
|
118
|
+
if max_width == 0:
|
|
119
|
+
raise InputError("The string block must not be empty.")
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def file_to_image(file_path: Path | str,
|
|
124
|
+
value_colors: Optional[dict[str, tuple[int, ...]]] = None,
|
|
125
|
+
preset: Optional[str] = None,
|
|
126
|
+
background_color: tuple[int, int, int] = (255, 255, 255),
|
|
127
|
+
cell_size: int = 32,
|
|
128
|
+
render_style: RenderStyle = RenderStyle.COLORED_CELLS,
|
|
129
|
+
font_path: Path = None,
|
|
130
|
+
font_size: Optional[int] = None) -> Image:
|
|
131
|
+
"""Read a string block from a file and convert it to an image.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
file_path (Path | str): Path to the file containing the string block.
|
|
135
|
+
value_colors (Optional[dict[str, tuple[int, ...]]]): Mapping of values to RGB colors.
|
|
136
|
+
preset (Optional[str]): Name of a preset colormap to use.
|
|
137
|
+
background_color (tuple[int, int, int]): Background color as an RGB tuple.
|
|
138
|
+
cell_size (int): Size of each cell in pixels.
|
|
139
|
+
render_style (RenderStyle): Style for rendering the image.
|
|
140
|
+
font_path (Path): Path to the font file to use for rendering text.
|
|
141
|
+
font_size (Optional[int]): Size of the font to use for rendering text.
|
|
142
|
+
Returns:
|
|
143
|
+
Image: The generated image.
|
|
144
|
+
"""
|
|
145
|
+
grid_str = Path(file_path).read_text()
|
|
146
|
+
img = string_to_image(grid_str, value_colors, preset, background_color, cell_size,
|
|
147
|
+
render_style, font_path, font_size)
|
|
148
|
+
return img
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def string_to_image(grid_str: str,
|
|
152
|
+
value_colors: Optional[dict[str, tuple[int, ...]]] = None,
|
|
153
|
+
preset: Optional[str] = None,
|
|
154
|
+
background_color: tuple[int, int, int] | str = (255, 255, 255),
|
|
155
|
+
cell_size: int = 32,
|
|
156
|
+
render_style: RenderStyle = RenderStyle.COLORED_CELLS,
|
|
157
|
+
font_path: Path = None,
|
|
158
|
+
font_size: Optional[int] = None) -> Image:
|
|
159
|
+
"""Convert a string block to an image.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
grid_str (str): The string block representing the grid.
|
|
163
|
+
value_colors (Optional[dict[str, tuple[int, ...]]]): Mapping of values to RGB colors.
|
|
164
|
+
preset (Optional[str]): Name of a preset colormap to use.
|
|
165
|
+
background_color (tuple[int, int, int] | str): Background color as an RGB tuple
|
|
166
|
+
or one of the preset strings.
|
|
167
|
+
cell_size (int): Size of each cell in pixels.
|
|
168
|
+
render_style (RenderStyle): Style for rendering the image.
|
|
169
|
+
font_path (Path): Path to the font file to use for rendering text.
|
|
170
|
+
font_size (Optional[int]): Size of the font to use for rendering text.
|
|
171
|
+
Returns:
|
|
172
|
+
Image: The generated image.
|
|
173
|
+
"""
|
|
174
|
+
check_grid_string(grid_str)
|
|
175
|
+
lines = grid_str.strip().split('\n')
|
|
176
|
+
height = len(lines)
|
|
177
|
+
width = max(len(line) for line in lines)
|
|
178
|
+
|
|
179
|
+
value_colors, font = get_set_mappings(cell_size, value_colors,
|
|
180
|
+
font_path, font_size, preset)
|
|
181
|
+
background_color = resolve_color(background_color)
|
|
182
|
+
|
|
183
|
+
img = Image.new('RGB', (width * cell_size, height * cell_size), background_color)
|
|
184
|
+
draw = ImageDraw.Draw(img)
|
|
185
|
+
for y, line in enumerate(lines):
|
|
186
|
+
for x, char in enumerate(line):
|
|
187
|
+
xy = [x * cell_size, y * cell_size, (x + 1) * cell_size,
|
|
188
|
+
(y + 1) * cell_size]
|
|
189
|
+
draw.rectangle(xy, fill=background_color)
|
|
190
|
+
match render_style:
|
|
191
|
+
case RenderStyle.COLORED_CELLS_WITH_BACKGROUND_TEXT:
|
|
192
|
+
color = resolve_color(value_colors.get(char, (0, 0, 0)))
|
|
193
|
+
draw.rectangle(xy, fill=color)
|
|
194
|
+
draw_character(background_color, cell_size, char, draw,
|
|
195
|
+
font, x, y)
|
|
196
|
+
case RenderStyle.COLORED_CELLS:
|
|
197
|
+
color = resolve_color(value_colors.get(char, (0, 0, 0)))
|
|
198
|
+
draw.rectangle(xy, fill=color)
|
|
199
|
+
case RenderStyle.COLORED_TEXT:
|
|
200
|
+
color = resolve_color(value_colors.get(char, (0, 0, 0)))
|
|
201
|
+
draw_character(color, cell_size, char, draw,
|
|
202
|
+
font, x, y)
|
|
203
|
+
case _:
|
|
204
|
+
raise ValueError(
|
|
205
|
+
f"Invalid show_chars option: {render_style}")
|
|
206
|
+
return img
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def draw_character(background_color: tuple[int, int, int], cell_size: int,
|
|
210
|
+
char: str, draw: ImageDraw, font: ImageFont.FreeTypeFont | ImageFont.ImageFont,
|
|
211
|
+
x: int, y: int):
|
|
212
|
+
bbox = draw.textbbox((0, 0), char, font=font)
|
|
213
|
+
w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
214
|
+
tx = x * cell_size + (cell_size - w) // 2
|
|
215
|
+
ty = y * cell_size + (cell_size - h) // 2
|
|
216
|
+
draw.text((tx, ty), char, fill=background_color, font=font)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def get_set_mappings(cell_size: int,
|
|
220
|
+
value_colors: Optional[dict[str, tuple[int, ...]]],
|
|
221
|
+
font_path: Optional[Path], font_size: int, preset: str)\
|
|
222
|
+
-> \
|
|
223
|
+
tuple[
|
|
224
|
+
dict[str, tuple[int, ...]], ImageFont.FreeTypeFont | ImageFont.ImageFont]:
|
|
225
|
+
"""Get value color mapping and font.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
cell_size (int): Size of each cell in pixels.
|
|
229
|
+
value_colors (Optional[dict[str, tuple[int, ...]]]): Mapping of values to RGB colors.
|
|
230
|
+
font_path (Optional[Path]): Path to the font file to use for rendering text.
|
|
231
|
+
font_size (int): Size of the font to use for rendering text.
|
|
232
|
+
preset (str): Name of a preset colormap to use.
|
|
233
|
+
Returns:
|
|
234
|
+
tuple[dict[str, tuple[int, ...]], ImageFont.FreeTypeFont | ImageFont.ImageFont]:
|
|
235
|
+
The value color mapping and the font object.
|
|
236
|
+
"""
|
|
237
|
+
if preset:
|
|
238
|
+
value_colors = PRESETS.get(preset, {})
|
|
239
|
+
elif value_colors is None:
|
|
240
|
+
value_colors = {}
|
|
241
|
+
# Use bold Consolas if available, else fallback
|
|
242
|
+
if font_path is None:
|
|
243
|
+
font_path = "consolab.ttf" # Bold Consolas
|
|
244
|
+
if font_size is None:
|
|
245
|
+
font_size = int(cell_size)
|
|
246
|
+
try:
|
|
247
|
+
font = ImageFont.truetype(font_path, font_size)
|
|
248
|
+
except OSError:
|
|
249
|
+
try:
|
|
250
|
+
font = ImageFont.truetype("DejaVuSans.ttf", size=24)
|
|
251
|
+
except OSError:
|
|
252
|
+
font = ImageFont.load_default()
|
|
253
|
+
return value_colors, font
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def save_image(img: Image, out_path: Path | str) -> None:
|
|
257
|
+
"""Save the image to the specified path.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
img (Image): The image to save.
|
|
261
|
+
out_path (Path | str): The path to save the image to.
|
|
262
|
+
Returns:
|
|
263
|
+
None
|
|
264
|
+
"""
|
|
265
|
+
img.save(out_path)
|
|
266
|
+
print(f"Saved image to {out_path}")
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import pytest
|
|
5
|
+
from PIL import ImageFont, ImageDraw
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_main_module():
|
|
9
|
+
# Load src/main.py by path to avoid package import issues.
|
|
10
|
+
root = Path(__file__).resolve().parents[1]
|
|
11
|
+
src_path = root / "src" / "paintbychar.py"
|
|
12
|
+
spec = importlib.util.spec_from_file_location("project_main", src_path)
|
|
13
|
+
module = importlib.util.module_from_spec(spec)
|
|
14
|
+
sys.modules["project_main"] = module
|
|
15
|
+
spec.loader.exec_module(module)
|
|
16
|
+
return module
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestGridString:
|
|
20
|
+
@pytest.mark.parametrize("grid_str", ["abc\ndef\nghi", "abc\ndef\nghi\n",
|
|
21
|
+
"1234\n5678\n9012", "A\nB\nC", "■\n■", "X Y Z\n1 2 3\n! @ #", "L",
|
|
22
|
+
"NOP"])
|
|
23
|
+
def test_grid_string_valid(self, grid_str):
|
|
24
|
+
m = load_main_module()
|
|
25
|
+
assert m.check_grid_string(grid_str) is True
|
|
26
|
+
|
|
27
|
+
@pytest.mark.parametrize("grid_str", ["", "\n", " "])
|
|
28
|
+
def test_grid_string_invalid(self, grid_str):
|
|
29
|
+
m = load_main_module()
|
|
30
|
+
with pytest.raises(m.InputError):
|
|
31
|
+
m.check_grid_string(grid_str)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_get_set_mappings_font_fallback():
|
|
35
|
+
m = load_main_module()
|
|
36
|
+
char_map, font = m.get_set_mappings(12, None, Path("nonexistent-font.ttf"),
|
|
37
|
+
None, None)
|
|
38
|
+
assert isinstance(char_map, dict)
|
|
39
|
+
# default font should be returned when truetype fails
|
|
40
|
+
assert (isinstance(font, ImageFont.FreeTypeFont) or isinstance(font,
|
|
41
|
+
ImageFont.ImageFont))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestRenderStyle:
|
|
45
|
+
@pytest.mark.parametrize("style", ["COLORED_CELLS", "COLORED_TEXT",
|
|
46
|
+
"COLORED_CELLS_WITH_BACKGROUND_TEXT"])
|
|
47
|
+
def test_render_style_valid(self, style):
|
|
48
|
+
m = load_main_module()
|
|
49
|
+
assert m.RenderStyle[style] is not None
|
|
50
|
+
|
|
51
|
+
@pytest.mark.parametrize("invalid_style",
|
|
52
|
+
["INVALID_OPTION", "colored_cells",
|
|
53
|
+
"Colored_Text", "", None])
|
|
54
|
+
def test_render_style_invalid(self, invalid_style):
|
|
55
|
+
m = load_main_module()
|
|
56
|
+
with pytest.raises(KeyError):
|
|
57
|
+
_ = m.RenderStyle[invalid_style]
|
|
58
|
+
|
|
59
|
+
def test_string_to_image_background_color_is_used_for_empty_character_area(
|
|
60
|
+
self):
|
|
61
|
+
m = load_main_module()
|
|
62
|
+
img = m.string_to_image("O", background_color=(11, 22, 33),
|
|
63
|
+
cell_size=100,
|
|
64
|
+
render_style=m.RenderStyle.COLORED_TEXT,
|
|
65
|
+
value_colors={"O": (200, 201, 202)},
|
|
66
|
+
font_size=166)
|
|
67
|
+
pixels = set(img.getdata())
|
|
68
|
+
assert (11, 22, 33) in pixels
|
|
69
|
+
assert (200, 201, 202) in pixels
|
|
70
|
+
|
|
71
|
+
def test_string_to_image_background_mode_draws_character_in_background_color(
|
|
72
|
+
self):
|
|
73
|
+
m = load_main_module()
|
|
74
|
+
img = m.string_to_image("O", background_color=(11, 22, 33),
|
|
75
|
+
cell_size=100,
|
|
76
|
+
render_style=m.RenderStyle.COLORED_CELLS_WITH_BACKGROUND_TEXT,
|
|
77
|
+
value_colors={"O": (200, 201, 202)},
|
|
78
|
+
font_size=166)
|
|
79
|
+
pixels = set(img.getdata())
|
|
80
|
+
assert (200, 201, 202) in pixels
|
|
81
|
+
assert (11, 22, 33) in pixels
|
|
82
|
+
|
|
83
|
+
def test_string_to_image_COLORED_CELLS_mode_fills_every_pixel_in_cell(self):
|
|
84
|
+
m = load_main_module()
|
|
85
|
+
img = m.string_to_image("A", background_color=(11, 22, 33),
|
|
86
|
+
cell_size=10,
|
|
87
|
+
render_style=m.RenderStyle.COLORED_CELLS,
|
|
88
|
+
value_colors={"A": (200, 201, 202)})
|
|
89
|
+
assert set(img.getdata()) == {(200, 201, 202)}
|
|
90
|
+
|
|
91
|
+
def test_string_to_image_invalid_render_style(self):
|
|
92
|
+
m = load_main_module()
|
|
93
|
+
grid = "A"
|
|
94
|
+
with pytest.raises(ValueError):
|
|
95
|
+
m.string_to_image(grid, value_colors={"A": (0, 0, 0)},
|
|
96
|
+
cell_size=10, render_style="INVALID_OPTION")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@pytest.mark.parametrize("grid_str", ["", "\n"])
|
|
100
|
+
def test_string_to_image_rejects_invalid_grid(grid_str):
|
|
101
|
+
m = load_main_module()
|
|
102
|
+
with pytest.raises(m.InputError):
|
|
103
|
+
img = m.string_to_image(grid_str)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_string_to_image_rectangular_dimensions():
|
|
107
|
+
m = load_main_module()
|
|
108
|
+
img = m.string_to_image("AB\nCD", cell_size=7)
|
|
109
|
+
assert img.size == (14, 14)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_string_to_image_irregular_dimensions():
|
|
113
|
+
m = load_main_module()
|
|
114
|
+
img = m.string_to_image("ABCE\nCDE\nAC\n\nDEFGH", cell_size=5)
|
|
115
|
+
assert img.size == (25, 25)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@pytest.mark.parametrize("value",
|
|
119
|
+
[(1, 2), (1, 2, 3, 4), (1.5, 2, 3), ("1", 2, 3)])
|
|
120
|
+
def test_resolve_color_rejects_malformed_rgb_tuples(value):
|
|
121
|
+
m = load_main_module()
|
|
122
|
+
with pytest.raises(ValueError):
|
|
123
|
+
m.resolve_color(value)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@pytest.mark.parametrize("cell_size", [0, -1])
|
|
127
|
+
def test_string_to_image_rejects_non_positive_cell_size(cell_size):
|
|
128
|
+
m = load_main_module()
|
|
129
|
+
with pytest.raises(ValueError):
|
|
130
|
+
m.string_to_image("A", cell_size=cell_size)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_missing_character_mapping_uses_black():
|
|
134
|
+
m = load_main_module()
|
|
135
|
+
img = m.string_to_image("A", cell_size=10,
|
|
136
|
+
render_style=m.RenderStyle.COLORED_CELLS)
|
|
137
|
+
assert set(img.getdata()) == {(0, 0, 0)}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@pytest.mark.parametrize("value", [True, False])
|
|
141
|
+
def test_bool_rgb(value):
|
|
142
|
+
m = load_main_module()
|
|
143
|
+
img = m.string_to_image("A", cell_size=10,
|
|
144
|
+
value_colors={'A': (value, value, value)},
|
|
145
|
+
render_style=m.RenderStyle.COLORED_CELLS,
|
|
146
|
+
background_color='red')
|
|
147
|
+
assert set(img.getdata()) == {(value, value, value)}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_preset_takes_precedence_over_character_mapping():
|
|
151
|
+
m = load_main_module()
|
|
152
|
+
img = m.string_to_image("5", preset="viridis", cell_size=10,
|
|
153
|
+
render_style=m.RenderStyle.COLORED_CELLS,
|
|
154
|
+
value_colors={"5": (1, 2, 3)})
|
|
155
|
+
assert img.getpixel((5, 5)) == m.PRESETS["viridis"]["5"]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_file_to_image_accepts_path_object(tmp_path):
|
|
159
|
+
m = load_main_module()
|
|
160
|
+
path = tmp_path / "grid.txt"
|
|
161
|
+
path.write_text("AB\nCD")
|
|
162
|
+
img = m.file_to_image(path, cell_size=6,
|
|
163
|
+
value_colors={char: (1, 2, 3) for char in "ABCD"},
|
|
164
|
+
render_style=m.RenderStyle.COLORED_CELLS)
|
|
165
|
+
assert img.size == (12, 12)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_file_to_image_accepts_trailing_newline(tmp_path):
|
|
169
|
+
m = load_main_module()
|
|
170
|
+
path = tmp_path / "grid.txt"
|
|
171
|
+
path.write_text("A\n")
|
|
172
|
+
img = m.file_to_image(path, cell_size=6, value_colors={"A": (1, 2, 3)},
|
|
173
|
+
render_style=m.RenderStyle.COLORED_CELLS)
|
|
174
|
+
assert img.size == (6, 6)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@pytest.mark.parametrize("colormap_name",
|
|
178
|
+
["viridis", "plasma", "inferno", "magma", "cividis",
|
|
179
|
+
"terrain", "coolwarm"])
|
|
180
|
+
def test_preset_applied_to_value_colors(colormap_name):
|
|
181
|
+
import matplotlib.pyplot as plt
|
|
182
|
+
m = load_main_module()
|
|
183
|
+
cmap = plt.get_cmap(colormap_name)
|
|
184
|
+
grid = "5"
|
|
185
|
+
img = m.string_to_image(grid, preset=colormap_name, cell_size=90,
|
|
186
|
+
render_style=m.RenderStyle.COLORED_CELLS)
|
|
187
|
+
assert img.size == (90, 90)
|
|
188
|
+
assert img.getpixel((50, 50)) == tuple(
|
|
189
|
+
[int(c * 255) for c in cmap(5 / 9)[:3]])
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_file_to_image_reads_file(tmp_path):
|
|
193
|
+
m = load_main_module()
|
|
194
|
+
p = tmp_path / "grid.txt"
|
|
195
|
+
p.write_text("X")
|
|
196
|
+
# ensure the mapping for X is provided to get deterministic colors
|
|
197
|
+
img = m.file_to_image(str(p), value_colors={"X": (1, 2, 3)},
|
|
198
|
+
background_color=(255, 255, 255), cell_size=8,
|
|
199
|
+
render_style=m.RenderStyle.COLORED_CELLS)
|
|
200
|
+
assert img.size == (8, 8)
|
|
201
|
+
assert img.getpixel((4, 4)) == (1, 2, 3)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_file_to_image_invalid_file(tmp_path):
|
|
205
|
+
m = load_main_module()
|
|
206
|
+
p = tmp_path / "invalid_grid.txt"
|
|
207
|
+
p.write_text("\n") # inconsistent line lengths
|
|
208
|
+
with pytest.raises(m.InputError):
|
|
209
|
+
m.file_to_image(str(p), value_colors={"A": (0, 0, 0), "B": (0, 0, 0),
|
|
210
|
+
"C": (0, 0, 0)}, cell_size=10,
|
|
211
|
+
render_style=m.RenderStyle.COLORED_TEXT)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_file_to_image_nonexistent_file():
|
|
215
|
+
m = load_main_module()
|
|
216
|
+
with pytest.raises(FileNotFoundError):
|
|
217
|
+
m.file_to_image("nonexistent_file.txt", value_colors={"A": (0, 0, 0)},
|
|
218
|
+
cell_size=10, render_style=m.RenderStyle.COLORED_TEXT)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_get_colormap_dict_length_and_values():
|
|
222
|
+
m = load_main_module()
|
|
223
|
+
colormap_name = "viridis"
|
|
224
|
+
colormap_dict = m.get_colormap_dict(colormap_name)
|
|
225
|
+
assert len(colormap_dict) == 10
|
|
226
|
+
for i in range(10):
|
|
227
|
+
color = colormap_dict[str(i)]
|
|
228
|
+
assert isinstance(color, tuple)
|
|
229
|
+
assert len(color) == 3
|
|
230
|
+
for channel in color:
|
|
231
|
+
assert 0 <= channel <= 255
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_get_colormap_dict_invalid_name():
|
|
235
|
+
m = load_main_module()
|
|
236
|
+
with pytest.raises(ValueError):
|
|
237
|
+
m.get_colormap_dict("invalid_colormap_name")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_presets_contain_expected_keys():
|
|
241
|
+
m = load_main_module()
|
|
242
|
+
expected_keys = {'viridis', 'plasma', 'inferno', 'magma', 'cividis',
|
|
243
|
+
'terrain', 'coolwarm'}
|
|
244
|
+
assert set(m.PRESETS.keys()) == expected_keys
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_save_image_to_path(tmp_path):
|
|
248
|
+
from PIL import Image
|
|
249
|
+
m = load_main_module()
|
|
250
|
+
grid = "A"
|
|
251
|
+
img = m.string_to_image(grid, value_colors={"A": (100, 150, 200)},
|
|
252
|
+
cell_size=10, render_style=m.RenderStyle.COLORED_CELLS)
|
|
253
|
+
output_path = tmp_path / "output_image.png"
|
|
254
|
+
m.save_image(img, output_path)
|
|
255
|
+
assert output_path.exists()
|
|
256
|
+
loaded_img = Image.open(output_path)
|
|
257
|
+
assert loaded_img.size == img.size
|
|
258
|
+
assert loaded_img.getpixel((5, 5)) == (100, 150, 200)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class TestColorPresets:
|
|
262
|
+
@pytest.mark.parametrize("preset_name",
|
|
263
|
+
["white", "black", "light_gray", "dark_gray",
|
|
264
|
+
"pastel_blue", "pastel_green", "pastel_pink",
|
|
265
|
+
"cream", "beige", "mint", "navy", "charcoal",
|
|
266
|
+
"soft_yellow", "gray", "blue", "green",
|
|
267
|
+
"pink", "yellow", "teal", "brown", "red"])
|
|
268
|
+
def test_resolve_color_valid(self, preset_name):
|
|
269
|
+
m = load_main_module()
|
|
270
|
+
color = m.resolve_color(preset_name)
|
|
271
|
+
assert isinstance(color, tuple)
|
|
272
|
+
assert len(color) == 3
|
|
273
|
+
for channel in color:
|
|
274
|
+
assert 0 <= channel <= 255
|
|
275
|
+
|
|
276
|
+
@pytest.mark.parametrize("invalid_value",
|
|
277
|
+
["unknown_color", (256, 0, 0), (-1, 0, 0),
|
|
278
|
+
"123,456,789", 12345, None])
|
|
279
|
+
def test_resolve_color_invalid(self, invalid_value):
|
|
280
|
+
m = load_main_module()
|
|
281
|
+
with pytest.raises(ValueError):
|
|
282
|
+
color = m.resolve_color(invalid_value)
|
|
283
|
+
for channel in color:
|
|
284
|
+
assert 0 <= channel <= 255
|
|
285
|
+
|
|
286
|
+
# TODO: Add a test once the expected behavior for an unknown preset is defined.
|
|
287
|
+
# TODO: Add tests for font_size=0, negative font_size, and a valid custom font.
|
|
288
|
+
# TODO: Add tests for empty files, CRLF files, and Unicode file contents.
|
|
289
|
+
# TODO: Add tests for save_image overwriting files and nonexistent parent
|
|
290
|
+
# paths.
|