├── ascii_magic ├── tests │ ├── __init__.py │ ├── .gitignore │ ├── lion.jpg │ ├── test_palette.py │ ├── test_quick_test.py │ ├── chicken_transparent.png │ ├── test_to_file.py │ ├── test_chars_by_density.py │ ├── test_from_image_object.py │ ├── test_enhance.py │ ├── test_exposed_pil_image.py │ ├── test_from_file.py │ ├── test_from_clipboard.py │ ├── test_from_gemini.py │ ├── test_from_url.py │ ├── test_to_html_file.py │ ├── test_from_swarmui.py │ ├── test_to_terminal.py │ └── test_to_image_file.py ├── fonts │ └── courier_prime.ttf ├── __init__.py ├── functions.py ├── ascii_art_font.py ├── constants.py └── ascii_art.py ├── .flake8 ├── example_moon.png ├── example_gemini.png ├── example_lion_ascii.png ├── example_lion_blue.png ├── example_lion_html.png ├── .gitignore ├── pytest.ini ├── setup.py ├── LICENCE └── README.md /ascii_magic/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 160 3 | ignore = E702 4 | -------------------------------------------------------------------------------- /ascii_magic/tests/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.txt 3 | *.json 4 | *.png 5 | -------------------------------------------------------------------------------- /example_moon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeandroBarone/python-ascii_magic/HEAD/example_moon.png -------------------------------------------------------------------------------- /example_gemini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeandroBarone/python-ascii_magic/HEAD/example_gemini.png -------------------------------------------------------------------------------- /example_lion_ascii.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeandroBarone/python-ascii_magic/HEAD/example_lion_ascii.png -------------------------------------------------------------------------------- /example_lion_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeandroBarone/python-ascii_magic/HEAD/example_lion_blue.png -------------------------------------------------------------------------------- /example_lion_html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeandroBarone/python-ascii_magic/HEAD/example_lion_html.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .hypothesis/ 3 | .pytest_cache/ 4 | 5 | *.egg-info/ 6 | build/ 7 | dist/ 8 | 9 | *.bat -------------------------------------------------------------------------------- /ascii_magic/tests/lion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeandroBarone/python-ascii_magic/HEAD/ascii_magic/tests/lion.jpg -------------------------------------------------------------------------------- /ascii_magic/fonts/courier_prime.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeandroBarone/python-ascii_magic/HEAD/ascii_magic/fonts/courier_prime.ttf -------------------------------------------------------------------------------- /ascii_magic/tests/test_palette.py: -------------------------------------------------------------------------------- 1 | from ascii_magic import AsciiArt 2 | 3 | 4 | def test_palette(): 5 | AsciiArt.print_palette() 6 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = ascii_magic/tests 3 | addopts = -s -v 4 | python_files = test_*.py 5 | python_functions = test_* 6 | -------------------------------------------------------------------------------- /ascii_magic/tests/test_quick_test.py: -------------------------------------------------------------------------------- 1 | from ascii_magic import AsciiArt 2 | 3 | 4 | def test_quick_test(): 5 | AsciiArt.quick_test() 6 | -------------------------------------------------------------------------------- /ascii_magic/tests/chicken_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeandroBarone/python-ascii_magic/HEAD/ascii_magic/tests/chicken_transparent.png -------------------------------------------------------------------------------- /ascii_magic/tests/test_to_file.py: -------------------------------------------------------------------------------- 1 | from ascii_magic import AsciiArt 2 | 3 | 4 | def test_to_file(): 5 | my_art = AsciiArt.from_image('lion.jpg') 6 | my_art.to_file('lion.txt') 7 | -------------------------------------------------------------------------------- /ascii_magic/tests/test_chars_by_density.py: -------------------------------------------------------------------------------- 1 | from ascii_magic import AsciiArt 2 | 3 | 4 | def test_chars_by_density(): 5 | my_art = AsciiArt.from_image('lion.jpg') 6 | my_art.to_terminal(char=' .$@', columns=150) 7 | -------------------------------------------------------------------------------- /ascii_magic/tests/test_from_image_object.py: -------------------------------------------------------------------------------- 1 | from ascii_magic import AsciiArt 2 | 3 | from PIL import Image 4 | 5 | 6 | def test_from_pillow_image(): 7 | with Image.open('lion.jpg') as img: 8 | my_art = AsciiArt.from_pillow_image(img) 9 | my_art.to_terminal() 10 | -------------------------------------------------------------------------------- /ascii_magic/tests/test_enhance.py: -------------------------------------------------------------------------------- 1 | from ascii_magic import AsciiArt 2 | 3 | 4 | def test_enhance(): 5 | my_art = AsciiArt.from_image('lion.jpg') 6 | my_art.to_image_file('output_lion_normal.png', enhance_image=False, full_color=True) 7 | my_art.to_image_file('output_lion_enhance.png', enhance_image=True, full_color=True) 8 | -------------------------------------------------------------------------------- /ascii_magic/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from ascii_magic.ascii_art import AsciiArt 3 | from ascii_magic.constants import Front, Back, CHARS_BY_DENSITY 4 | from ascii_magic.functions import ( 5 | quick_test, 6 | from_image, 7 | from_pillow_image, 8 | from_url, 9 | from_clipboard, 10 | from_gemini, 11 | ) 12 | -------------------------------------------------------------------------------- /ascii_magic/tests/test_exposed_pil_image.py: -------------------------------------------------------------------------------- 1 | from ascii_magic import AsciiArt 2 | 3 | import PIL.ImageEnhance 4 | 5 | 6 | def test_exposed_pil_image(): 7 | my_art = AsciiArt.from_image('chicken_transparent.png') 8 | my_art.image = PIL.ImageEnhance.Brightness(my_art.image).enhance(0.2) 9 | my_art.to_terminal() 10 | -------------------------------------------------------------------------------- /ascii_magic/tests/test_from_file.py: -------------------------------------------------------------------------------- 1 | from ascii_magic import AsciiArt 2 | 3 | 4 | def test_from_file(): 5 | my_art = AsciiArt.from_image('lion.jpg') 6 | my_art.to_terminal() 7 | 8 | 9 | def test_from_file_transparent_bg(): 10 | my_art = AsciiArt.from_image('chicken_transparent.png') 11 | my_art.to_terminal() 12 | -------------------------------------------------------------------------------- /ascii_magic/tests/test_from_clipboard.py: -------------------------------------------------------------------------------- 1 | from ascii_magic import AsciiArt 2 | 3 | import pytest 4 | 5 | 6 | def test_from_clipboard(): 7 | # This test requires an image in the clipboard 8 | try: 9 | my_art = AsciiArt.from_clipboard() 10 | except OSError: 11 | pytest.skip('No image found in the clipboard') 12 | 13 | my_art.to_terminal() 14 | -------------------------------------------------------------------------------- /ascii_magic/tests/test_from_gemini.py: -------------------------------------------------------------------------------- 1 | from ascii_magic import AsciiArt 2 | 3 | import pytest 4 | 5 | import os 6 | 7 | 8 | def test_from_gemini(): 9 | if not os.environ.get('GEMINI_API_KEY'): 10 | pytest.skip('No Gemini API key found on environment (KEY=GEMINI_API_KEY)') 11 | 12 | my_art = AsciiArt.from_gemini('A hyperrealistic portrait of a cow with noble clothes, digital art') 13 | my_art.to_terminal() 14 | -------------------------------------------------------------------------------- /ascii_magic/tests/test_from_url.py: -------------------------------------------------------------------------------- 1 | from ascii_magic import AsciiArt 2 | 3 | 4 | def test_from_url(): 5 | my_art = AsciiArt.from_url('https://cataas.com/cat') 6 | my_art.to_terminal() 7 | 8 | 9 | def test_wrong_url(): 10 | try: 11 | my_art = AsciiArt.from_url('https://images2.alphacoders.com/902/thumb-1920-902946.png') 12 | my_art.to_terminal() 13 | except OSError as e: 14 | print(f'Could not load the image, server said: {e.code} {e.msg}') # type: ignore 15 | -------------------------------------------------------------------------------- /ascii_magic/tests/test_to_html_file.py: -------------------------------------------------------------------------------- 1 | from ascii_magic import AsciiArt 2 | 3 | 4 | def test_to_html_file(): 5 | my_art = AsciiArt.from_image('lion.jpg') 6 | my_art.to_html_file('lion.html') 7 | 8 | 9 | def test_to_html_file_terminal_mode(): 10 | my_art = AsciiArt.from_image('lion.jpg') 11 | my_art.to_html_file('lion_terminal.html', full_color=False) 12 | 13 | 14 | def test_to_html_file_monochrome_mode(): 15 | my_art = AsciiArt.from_image('lion.jpg') 16 | my_art.to_html_file('lion_monochrome.html', monochrome=True) 17 | -------------------------------------------------------------------------------- /ascii_magic/tests/test_from_swarmui.py: -------------------------------------------------------------------------------- 1 | from ascii_magic import AsciiArt 2 | 3 | import pytest 4 | import os 5 | 6 | 7 | def test_from_swamui(): 8 | if not os.environ.get('SWARMUI_SERVER'): 9 | pytest.skip('No SwarmUI server found on environment (KEY=SWARMUI_SERVER)') 10 | 11 | my_art = AsciiArt.from_swamui( 12 | 'A hyperrealistic portrait of a cow with noble clothes, digital art', 13 | raw_input={ 14 | 'width': 1344, 15 | 'height': 768, 16 | } 17 | ) 18 | my_art.to_image_file('test_swarmui.png', full_color=True) 19 | -------------------------------------------------------------------------------- /ascii_magic/tests/test_to_terminal.py: -------------------------------------------------------------------------------- 1 | from ascii_magic import AsciiArt, Back, Front 2 | 3 | 4 | def test_blue_back(): 5 | my_art = AsciiArt.from_image('lion.jpg') 6 | my_art.to_terminal(back=Back.BLUE) 7 | 8 | 9 | def test_red_front(): 10 | my_art = AsciiArt.from_image('lion.jpg') 11 | my_art.to_terminal(front=Front.RED) 12 | 13 | 14 | def test_monochrome(): 15 | my_art = AsciiArt.from_image('lion.jpg') 16 | my_art.to_terminal(monochrome=True) 17 | 18 | 19 | def test_small(): 20 | my_art = AsciiArt.from_image('lion.jpg') 21 | my_art.to_terminal(columns=50) 22 | -------------------------------------------------------------------------------- /ascii_magic/functions.py: -------------------------------------------------------------------------------- 1 | from ascii_magic import AsciiArt 2 | 3 | from PIL import Image 4 | 5 | from typing import Optional 6 | 7 | 8 | def quick_test(): 9 | AsciiArt.quick_test() 10 | 11 | 12 | def from_image(path: str) -> AsciiArt: 13 | return AsciiArt.from_image(path) 14 | 15 | 16 | def from_pillow_image(img: Image.Image) -> AsciiArt: 17 | return AsciiArt.from_pillow_image(img) 18 | 19 | 20 | def from_url(url: str) -> AsciiArt: 21 | return AsciiArt.from_url(url) 22 | 23 | 24 | def from_clipboard() -> AsciiArt: 25 | return AsciiArt.from_clipboard() 26 | 27 | 28 | def from_gemini( 29 | prompt: str, 30 | api_key: Optional[str] = None, 31 | model: Optional[str] = None, 32 | debug: bool = False, 33 | ) -> AsciiArt: 34 | return AsciiArt.from_gemini(prompt, api_key, model, debug) 35 | -------------------------------------------------------------------------------- /ascii_magic/ascii_art_font.py: -------------------------------------------------------------------------------- 1 | from PIL import ImageFont 2 | 3 | import os 4 | 5 | 6 | class AsciiArtFont(): 7 | FONT_SIZE = 18 8 | 9 | def __init__(self, font: str): 10 | font_path = os.path.join(os.path.dirname(__file__), 'fonts', font) 11 | if os.path.exists(font_path): 12 | self._font = ImageFont.truetype(font=font_path, size=self.FONT_SIZE) 13 | else: 14 | self._font = ImageFont.truetype(font=font, size=self.FONT_SIZE) 15 | 16 | def get_font(self) -> ImageFont.ImageFont: 17 | return self._font 18 | 19 | def get_char_size(self) -> tuple[int, int, int]: 20 | bbox = self._font.getbbox('M') 21 | char_width = int(bbox[2] - bbox[0]) 22 | char_height = int(bbox[3] - bbox[1]) 23 | line_height = int(char_height + self.FONT_SIZE / 4) 24 | return char_width, char_height, line_height 25 | 26 | def get_ratio(self) -> float: 27 | char_width, _, line_height = self.get_char_size() 28 | return line_height / char_width 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="ascii_magic", 8 | version="2.7.2", 9 | author="Leandro Barone", 10 | author_email="web@leandrobarone.com.ar", 11 | description="Converts pictures into ASCII art", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/LeandroBarone/python-ascii_magic", 15 | packages=setuptools.find_packages(), 16 | include_package_data=True, 17 | data_files=[ 18 | ("ascii_magic/fonts", ["ascii_magic/fonts/courier_prime.ttf"]), 19 | ("ascii_magic/tests", ["ascii_magic/tests/lion.jpg", "ascii_magic/tests/chicken_transparent.png"]), 20 | ], 21 | classifiers=[ 22 | "Programming Language :: Python :: 3", 23 | "License :: OSI Approved :: MIT License", 24 | "Operating System :: OS Independent", 25 | ], 26 | python_requires='>=3.5', 27 | install_requires=['Pillow'], 28 | ) 29 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Leandro Barone 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /ascii_magic/tests/test_to_image_file.py: -------------------------------------------------------------------------------- 1 | from ascii_magic import AsciiArt 2 | 3 | 4 | def test_to_image_file_monochrome(): 5 | my_art = AsciiArt.from_image('lion.jpg') 6 | my_art.to_image_file( 7 | 'output_lion_monochrome.png', 8 | columns=60, 9 | monochrome=True, 10 | ) 11 | 12 | 13 | def test_to_image_file_green(): 14 | my_art = AsciiArt.from_image('lion.jpg') 15 | my_art.to_image_file( 16 | 'output_lion_green.png', 17 | columns=60, 18 | front='#00FF00', 19 | back='#222', 20 | ) 21 | 22 | 23 | def test_to_image_file_console(): 24 | my_art = AsciiArt.from_image('lion.jpg') 25 | my_art.to_image_file( 26 | 'output_lion_console.png', 27 | columns=60, 28 | ) 29 | 30 | 31 | def test_to_image_file_full_color(): 32 | my_art = AsciiArt.from_image('lion.jpg') 33 | my_art.to_image_file( 34 | 'output_lion_full_color.png', 35 | columns=60, 36 | full_color=True, 37 | height='auto', 38 | ) 39 | 40 | 41 | def test_to_image_file_small_width(): 42 | my_art = AsciiArt.from_image('lion.jpg') 43 | my_art.to_image_file( 44 | 'output_lion_small_width.png', 45 | columns=60, 46 | width=200, 47 | ) 48 | 49 | 50 | def test_to_image_file_small_height(): 51 | my_art = AsciiArt.from_image('lion.jpg') 52 | my_art.to_image_file( 53 | 'output_lion_small_height.png', 54 | columns=60, 55 | height=200, 56 | ) 57 | 58 | 59 | def test_to_image_file_square(): 60 | my_art = AsciiArt.from_image('lion.jpg') 61 | my_art.to_image_file( 62 | 'output_lion_square.png', 63 | columns=60, 64 | width=200, 65 | height=200, 66 | ) 67 | -------------------------------------------------------------------------------- /ascii_magic/constants.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Front(Enum): 5 | BLACK = 30 6 | RED = 31 7 | GREEN = 32 8 | YELLOW = 33 9 | BLUE = 34 10 | MAGENTA = 35 11 | CYAN = 36 12 | WHITE = 37 13 | RESET = 39 14 | LIGHTBLACK = 90 15 | LIGHTRED = 91 16 | LIGHTGREEN = 92 17 | LIGHTYELLOW = 93 18 | LIGHTBLUE = 94 19 | LIGHTMAGENTA = 95 20 | LIGHTCYAN = 96 21 | LIGHTWHITE = 97 22 | 23 | 24 | class Back(Enum): 25 | BLACK = 40 26 | RED = 41 27 | GREEN = 42 28 | YELLOW = 43 29 | BLUE = 44 30 | MAGENTA = 45 31 | CYAN = 46 32 | WHITE = 47 33 | RESET = 49 34 | LIGHTBLACK = 100 35 | LIGHTRED = 101 36 | LIGHTGREEN = 102 37 | LIGHTYELLOW = 103 38 | LIGHTBLUE = 104 39 | LIGHTMAGENTA = 105 40 | LIGHTCYAN = 106 41 | LIGHTWHITE = 107 42 | 43 | 44 | class Modes(Enum): 45 | ASCII = 'ASCII' 46 | TERMINAL = 'TERMINAL' 47 | OBJECT = 'OBJECT' 48 | 49 | HTML = 'HTML' 50 | HTML_MONOCHROME = 'HTML_MONOCHROME' 51 | HTML_TERMINAL = 'HTML_TERMINAL' 52 | HTML_FULL_COLOR = 'HTML_FULL_COLOR' 53 | 54 | 55 | _COLOR_DATA = [ 56 | [(0, 0, 0), Front.LIGHTBLACK, '#222'], 57 | [(0, 0, 255), Front.BLUE, '#00F'], 58 | [(0, 255, 0), Front.GREEN, '#0F0'], 59 | [(255, 0, 0), Front.RED, '#F00'], 60 | [(255, 255, 255), Front.WHITE, '#FFF'], 61 | [(255, 0, 255), Front.MAGENTA, '#F0F'], 62 | [(0, 255, 255), Front.CYAN, '#0FF'], 63 | [(255, 255, 0), Front.YELLOW, '#FF0'] 64 | ] 65 | 66 | PALETTE = [[[(v / 255.0)**2.2 for v in x[0]], x[1], x[2]] for x in _COLOR_DATA] 67 | 68 | CHARS_BY_DENSITY = ' .`-_\':,;^=+/"|)\\<>)iv%xclrs{*}I?!][1taeo7zjLunT#JCwfy325Fp6mqSghVd4EgXPGZbYkOA&8U$@KHDBWNMR0QQ' 69 | 70 | DEFAULT_STYLES = 'display: inline-block; border-width: 4px 6px; border-color: black; color: white; border-style: solid; background-color:black; font-size: 8px;' 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASCII Magic 2 | 3 | Python package that converts images into ASCII art for terminals and HTML. 4 | 5 | Code based on [ProfOak's Ascii Py](https://github.com/ProfOak/Ascii_py/). 6 | 7 | # Changelog 8 | 9 | ### v2.7 - Oct 2025 10 | - SwarmUI support: from_swarmui() 11 | 12 | ### v2.6 - Oct 2025 13 | - Gemini support: from_gemini() 14 | - to_image_file() stroke_width parameter 15 | - Removed Stable Diffusion and DALL-E support (API no longer available) 16 | 17 | ### v2.5 - Oct 2025 18 | - Optional image enhancement 19 | 20 | ### v2.4 - Oct 2025 21 | - Removed Colorama dependency (no longer needed in the latest versions of Windows) 22 | - to_image_file() 23 | - to_character_list() 24 | - print_palette() 25 | - Removed Craiyon support (API no longer available) 26 | 27 | ### v2.3 - Feb 2023 28 | - Craiyon support: from_craiyon() 29 | 30 | ### v2.2 - Feb 2023 31 | - Stable Diffusion support: from_stable_diffusion() 32 | 33 | ### v2.1 - Feb 2023 34 | - DALL-E support: from_dalle() 35 | 36 | ### v2.0 - Feb 2023 37 | - Complete rewrite, full OOP, no longer compatible with 1.x 38 | - Added support for foreground color 39 | - to_html() 40 | 41 | ### v1.6 - Sep 2021 42 | - OOP functionality 43 | - to_file() 44 | 45 | ### v1.5 - Nov 2020 46 | - First public release 47 | 48 | # How to install 49 | 50 | pip install ascii_magic 51 | 52 | # Quickstart 53 | 54 | ```python 55 | from ascii_magic import AsciiArt 56 | 57 | my_art = AsciiArt.from_image('moon.jpg') 58 | my_art.to_terminal() 59 | ``` 60 | 61 | Result: 62 | 63 | ![ASCII Magic example](https://raw.githubusercontent.com/LeandroBarone/python-ascii_magic/master/example_moon.png) 64 | 65 | 66 | # Colors don't work on Windows? Try this 67 | Install [Colorama](https://github.com/tartley/colorama) and run ```colorama.init()``` before printing to the console. 68 | 69 | ``` 70 | pip install colorama 71 | ``` 72 | 73 | ```python 74 | import colorama 75 | from ascii_magic import AsciiArt 76 | 77 | my_art = AsciiArt.from_image('moon.jpg') 78 | colorama.init() 79 | my_art.to_terminal() 80 | ``` 81 | 82 | 83 | # The class AsciiArt 84 | 85 | This module's entire functionality is contained within the class AsciiArt, which has a collection class methods, such as ```AsciiArt.from_image()```, that return ```AsciiArt``` objects with pictures from different sources: files, URLs, the clipboard, etc. 86 | 87 | These objects have multiple methods, such as ```my_art.to_terminal()```, that generate ASCII art pieces from the picture. These methods have parameters such as ```columns``` that allow you to change the appearance of the art piece. 88 | 89 | For convenience, the module ```ascii_magic``` also exposes a collection of functions with the same name as the class methods mentioned above, which do exactly the same. 90 | 91 | Example: 92 | 93 | ```python 94 | from ascii_magic import AsciiArt, from_image 95 | 96 | # This: 97 | my_art = AsciiArt.from_image('lion.jpg') 98 | my_art.to_terminal() 99 | 100 | # Does the same as this: 101 | my_art = from_image('lion.jpg') 102 | my_art.to_terminal() 103 | ``` 104 | 105 | This class is essentially a wrapper for a Pillow image. The property ```AsciiArt.image``` exposes the underlying Pillow object so you can manipulate it directly. 106 | 107 | Example: 108 | 109 | ```python 110 | from ascii_magic import AsciiArt 111 | from PIL import ImageEnhance 112 | 113 | my_art = AsciiArt.from_image('lion.jpg') 114 | my_art.image = ImageEnhance.Brightness(my_art.image).enhance(0.2) 115 | my_art.to_terminal() 116 | ``` 117 | 118 | ## quick_test() 119 | 120 | Loads a cat picture from [Cat as a Service](https://cataas.com/) with the default parameters and prints it to the terminal, allowing you to verify in a single line of code that everything is running O.K. 121 | 122 | ```python 123 | AsciiArt.quick_test() -> None 124 | ``` 125 | 126 | Example: 127 | 128 | ```python 129 | from ascii_magic import AsciiArt 130 | 131 | AsciiArt.quick_test() 132 | ``` 133 | 134 | ## from_image() 135 | 136 | Creates an ```AsciiArt``` object from an image file. 137 | 138 | ```python 139 | from_image(path: str) -> AsciiArt 140 | ``` 141 | 142 | Parameters: 143 | 144 | - ```path (str)```: an image file compatible with Pillow, such as a jpeg or png 145 | 146 | Example: 147 | 148 | ```python 149 | from ascii_magic import AsciiArt, Back 150 | 151 | my_art = AsciiArt.from_image('lion.jpg') 152 | my_art.to_terminal(columns=200, back=Back.BLUE) 153 | ``` 154 | 155 | Result: 156 | 157 | ![ASCII Magic TERMINAL mode example](https://raw.githubusercontent.com/LeandroBarone/python-ascii_magic/master/example_lion_blue.png) 158 | 159 | Example: 160 | 161 | ```python 162 | from ascii_magic import AsciiArt 163 | 164 | my_art = AsciiArt.from_image('lion.jpg') 165 | my_art.to_html_file('ascii_art.html', columns=200, width_ratio=2) 166 | ``` 167 | 168 | Result: 169 | 170 | ![ASCII Magic HTML mode example](https://raw.githubusercontent.com/LeandroBarone/python-ascii_magic/master/example_lion_html.png) 171 | 172 | Example: 173 | 174 | ```python 175 | from ascii_magic import AsciiArt 176 | 177 | my_art = AsciiArt.from_image('lion.jpg') 178 | my_art.to_terminal(columns=200, monochrome=True) 179 | 180 | ``` 181 | 182 | Result: 183 | 184 | ![ASCII Magic ASCII mode example](https://raw.githubusercontent.com/LeandroBarone/python-ascii_magic/master/example_lion_ascii.png) 185 | 186 | 187 | ## from_gemini() 188 | 189 | Creates an ```AsciiArt``` object with [Gemini](https://aistudio.google.com/), a generative platform that can create realistic images from a description in natural language. Requires a [free API key](https://aistudio.google.com/api-keys). The API key can be set in the environment variable ```GEMINI_API_KEY``` or passed as an argument. 190 | 191 | ```python 192 | from_gemini( 193 | prompt: str, 194 | model: str = 'gemini-2.0-flash-preview-image-generation', 195 | api_key: Optional[str] = None, 196 | ) -> AsciiArt 197 | ``` 198 | 199 | Parameters: 200 | 201 | - ```prompt (str)```: a description of an image in natural language 202 | - ```model (str, optional)```: the model to use for generation 203 | - ```api_key (str, optional)```: a Gemini API key 204 | 205 | Example: 206 | 207 | ```python 208 | from ascii_magic import AsciiArt 209 | 210 | api_key = 'aFaKeGeMiNiApIkEy' 211 | my_art = AsciiArt.from_gemini('A portrait of a cow with noble clothes', api_key=api_key) 212 | my_art.to_image_file('example_gemini.png', columns=80, full_color=True) 213 | ``` 214 | 215 | Result: 216 | 217 | ![ASCII Magic Gemini example](https://raw.githubusercontent.com/LeandroBarone/python-ascii_magic/master/example_gemini.png) 218 | 219 | ## from_swarmui() 220 | 221 | Creates an ```AsciiArt``` object from [SwarmUI](https://swarmui.net/), a text-to-image user interface. Requires the URL of a SwarmUI instance, which is usually running on localhost, port 7801. The URL can be set in the environment variable ```SWARMUI_SERVER``` or passed as an argument. 222 | 223 | ```python 224 | from_swarmui( 225 | prompt: str, 226 | width: int = 1280, 227 | height: int = 720, 228 | steps: int = 20, 229 | raw_input: dict = {}, 230 | model: Optional[str] = 'auto', 231 | server: str = 'http://localhost:7801', 232 | ) -> AsciiArt 233 | ``` 234 | 235 | Parameters: 236 | 237 | - ```prompt (str)```: a description of an image in natural language 238 | - ```width (int, optional)```: the width of the image 239 | - ```height (int, optional)```: the height of the image 240 | - ```steps (int, optional)```: the number of steps to generate the image 241 | - ```raw_input (dict, optional)```: additional raw input to pass to the SwarmUI API 242 | - ```model (str | 'auto', optional)```: the model to use for generation, if 'auto', the first model will be used 243 | - ```server (str, optional)```: the URL of a SwarmUI instance 244 | 245 | Example: 246 | 247 | ```python 248 | from ascii_magic import AsciiArt 249 | 250 | my_art = AsciiArt.from_swarmui( 251 | 'A portrait of a cow with noble clothes', 252 | width=640, 253 | height=480, 254 | steps=50, 255 | raw_input={ 256 | 'seed': 42, 257 | 'cfgscale': 8.0, 258 | 'negative_prompt': 'low quality, pixelated, blurry', 259 | }, 260 | server='http://localhost:12345' 261 | ) 262 | my_art.to_image_file('example_swarmui.png', full_color=True) 263 | ``` 264 | 265 | ## from_url() 266 | 267 | Creates an ```AsciiArt``` object from an image URL. Raises an ```urllib.error.URLError``` if something goes wrong while requesting the image, but you can also catch it as an ```OSError``` if you don't want to import ```urllib``` into your project. 268 | 269 | ```python 270 | from_url(url: str) -> AsciiArt 271 | ``` 272 | 273 | Parameters: 274 | 275 | - ```url (str)```: an URL which will be loaded via urllib (supports redirects) 276 | 277 | Example: 278 | 279 | ```python 280 | from ascii_magic import AsciiArt 281 | 282 | try: 283 | my_art = AsciiArt.from_url('https://cataas.com/cat') 284 | except OSError as e: 285 | print(f'Could not load the image, server said: {e.code} {e.msg}') 286 | my_art.to_terminal() 287 | ``` 288 | 289 | ## from_clipboard() 290 | 291 | Creates an ```AsciiArt``` object from the contents of the clipboard. Raises an ```OSError``` if the clipboard doesn't contain an image. Requires [PyGObject](https://pygobject.readthedocs.io/en/latest/getting_started.html) under Linux. 292 | 293 | ```python 294 | from_clipboard() -> AsciiArt 295 | ``` 296 | 297 | Example: 298 | 299 | ```python 300 | from ascii_magic import AsciiArt 301 | 302 | try: 303 | my_art = AsciiArt.from_clipboard() 304 | except OSError: 305 | print('The clipboard does not contain an image') 306 | my_art.to_terminal() 307 | ``` 308 | 309 | ## from_pillow_image() 310 | 311 | Creates an ```AsciiArt``` object from an image object created with Pillow. This allows you to handle the image loading yourself. 312 | 313 | ```python 314 | from_pillow_image(img: PIL.Image) -> AsciiArt 315 | ``` 316 | 317 | Parameters: 318 | 319 | - ```img (obj)```: an image object created with Pillow 320 | 321 | Example: 322 | 323 | ```python 324 | from ascii_magic import AsciiArt 325 | from PIL import Image 326 | 327 | img = Image.open('lion.jpg') 328 | my_art = AsciiArt.from_pillow_image(img) 329 | my_art.to_terminal() 330 | ``` 331 | 332 | ## print_palette() 333 | 334 | Prints the entire 8-color palette to the console. 335 | 336 | Example: 337 | 338 | ```python 339 | from ascii_magic import AsciiArt 340 | 341 | AsciiArt.print_palette() 342 | ``` 343 | 344 | # The AsciiArt object 345 | 346 | An ```AsciiArt``` object created as explained above has a collection of methods, such as ```to_ascii()```, that allows you to create and display ASCII art pieces. All of them return a string, and some have additional functionality, as described below. 347 | 348 | 349 | ## to_ascii() 350 | 351 | Returns a string containing ASCII art and, by default, control characters that allows most terminals (also known as shells) to display color. 352 | 353 | The module ```ascii_magic``` exposes two enums to handle color: ```Front``` and ```Back``` which allow you to select terminal-compatible colors. 354 | 355 | ```python 356 | AsciiArt.to_ascii( 357 | columns: int = 120, 358 | width_ratio: float = 2.2, 359 | char: Optional[str] = None, 360 | enhance_image: bool = False, 361 | monochrome: bool = False, 362 | front: Optional[Front] = None, 363 | back: Optional[Back] = None, 364 | ) -> str 365 | ``` 366 | 367 | Parameters: 368 | 369 | - ```columns (int, optional)```: the number of characters per row, more columns = wider art 370 | - ```width_ratio (float, optional)```: ASCII characters are not squares, so this adjusts the width to height ratio during generation 371 | - ```char (str, optional)```: specifies one or more characters sorted by brightness, such as ' .$@' 372 | - ```enhance_image (bool, optional)```: if set to True, enhances the image before generating ASCII art 373 | - ```monochrome (bool, optional)```: if set to True, completely disables color 374 | - ```front (enum, optional)```: overrides the foreground color with one of: 375 | - ```Front.BLACK``` 376 | - ```Front.RED``` 377 | - ```Front.GREEN``` 378 | - ```Front.YELLOW``` 379 | - ```Front.BLUE``` 380 | - ```Front.MAGENTA``` 381 | - ```Front.CYAN``` 382 | - ```Front.WHITE``` 383 | - ```back (enum, optional)```: sets the background color to one of: 384 | - ```Back.BLACK``` 385 | - ```Back.RED``` 386 | - ```Back.GREEN``` 387 | - ```Back.YELLOW``` 388 | - ```Back.BLUE``` 389 | - ```Back.MAGENTA``` 390 | - ```Back.CYAN``` 391 | - ```Back.WHITE``` 392 | 393 | Example: 394 | 395 | ```python 396 | from ascii_magic import AsciiArt, Back 397 | 398 | my_art = AsciiArt.from_image('lion.jpg') 399 | my_output = my_art.to_ascii(columns=200, back=Back.BLUE) 400 | print(my_output) 401 | ``` 402 | 403 | Result: 404 | 405 | ![ASCII Magic TERMINAL mode example](https://raw.githubusercontent.com/LeandroBarone/python-ascii_magic/master/example_lion_blue.png) 406 | 407 | 408 | ## to_terminal() 409 | 410 | Identical to ```AsciiArt.to_ascii()```, but it also does a ```print()``` of the result, saving you one line of code ;) 411 | 412 | ## to_file() 413 | 414 | Identical to ```AsciiArt.to_ascii()```, but it also saves the result to a text file. 415 | 416 | ```python 417 | AsciiArt.to_file( 418 | path: str, 419 | # ... same parameters as AsciiArt.to_ascii() 420 | ) -> str 421 | ``` 422 | Parameters: 423 | 424 | - ```path (str)```: the output file path 425 | 426 | Example: 427 | 428 | ```python 429 | from ascii_magic import AsciiArt 430 | 431 | my_art = AsciiArt.from_image('lion.jpg') 432 | my_art.to_file('lion.txt', monochrome=True) 433 | ``` 434 | 435 | ## to_html() 436 | 437 | Returns a string with ASCII art created as HTML markup. Accepts the same parameters as ```AsciiArt.to_ascii()```, except for ```back``` and ```front``` colors. By default the HTML ASCII art is generated with a 24-bit palette (16 million colors). 438 | 439 | ```python 440 | AsciiArt.to_html( 441 | full_color: bool = True, 442 | # ... same parameters as AsciiArt.to_ascii(), except back and front colors 443 | ) -> str 444 | ``` 445 | 446 | Parameters: 447 | 448 | - ```full_color (bool, optional)```: if set to False, limits color palette to 8 colors 449 | 450 | Example: 451 | 452 | ```python 453 | from ascii_magic import AsciiArt 454 | 455 | my_art = AsciiArt.from_image('lion.jpg') 456 | my_html_markup = my_art.to_html(columns=200) 457 | ``` 458 | 459 | ## to_html_file() 460 | 461 | Identical to ```AsciiArt.to_html()```, but it also saves the markup to a barebones HTML file inside a ```
``` tag with a bunch of default CSS styles that you can easily open in your browser.
462 | 
463 | ```python
464 | AsciiArt.to_html_file(
465 |     path: str,
466 |     styles: str = '...',  # See description below
467 |     additional_styles: str = '',
468 |     auto_open: bool = False
469 |     # ... same parameters as AsciiArt.to_html()
470 | ) -> str
471 | ```
472 | 
473 | Parameters:
474 | 
475 | - ```path (str)```: the output file path
476 | - ```styles (str, optional)```: a string with a bunch of CSS styles for the ```
``` element, by default:
477 |   - display: inline-block;
478 |   - border-width: 4px 6px;
479 |   - border-color: black;
480 |   - border-style: solid;
481 |   - background-color: black;
482 |   - color: white;
483 |   - font-size: 8px;
484 | - ```additional_styles (str, optional)```: use this to add your own CSS styles without removing the default ones
485 | - ```auto_open (bool, optional)```: if True, the file will be opened with ```webbrowser.open()```
486 | 
487 | 
488 | Example:
489 | 
490 | ```python
491 | from ascii_magic import AsciiArt
492 | 
493 | my_art = AsciiArt.from_image('lion.jpg')
494 | my_art.to_html_file('lion.html', columns=200, additional_styles='font-family: MonoLisa;')
495 | ```
496 | 
497 | Result:
498 | 
499 | ![ASCII Magic HTML mode example](https://raw.githubusercontent.com/LeandroBarone/python-ascii_magic/master/example_lion_html.png)
500 | 
501 | ## to_image_file()
502 | 
503 | Generates a image file with the resulting ASCII art. Accepts the same parameters as ```AsciiArt.to_ascii()```. By default the ASCII art is generated with a 24-bit palette (16 million colors). If both width and height are set to auto, no resizing will be performed. If width or height is set to 'auto', the output will be resized proportionally. If both width and height are specified, the output will be resized to the specified dimensions, ignoring aspect ratio. Returns a 2d character list (see ```to_character_list()``` below).
504 | 
505 | ```python
506 | AsciiArt.to_image_file(
507 |     path: str,
508 |     width: int | 'auto' = 'auto',
509 |     height: int | 'auto' = 'auto',
510 |     border_width: int = 2,
511 |     stroke_width: float = 0.5,
512 |     file_type: 'PNG'|'JPG'|'GIF'|'WEBP' = 'PNG',
513 |     font: str = 'courier_prime.ttf',
514 |     width_ratio: float | 'auto' = 'auto',
515 |     char: Optional[str] = None,
516 |     enhance_image: bool = False,
517 |     monochrome: bool = False,
518 |     full_color: bool = False,
519 |     front: Optional[str] = None,
520 |     back: str = '#000000',
521 | ) -> list[list[dict]]
522 | ```
523 | 
524 | Parameters:
525 | 
526 | - ```path (str)```: the output file path
527 | - ```width (int | 'auto', optional)```: the width of the image
528 | - ```height (int | 'auto', optional)```: the height of the image
529 | - ```border_width (int, optional)```: the width of the border around the entire image
530 | - ```stroke_width (float, optional)```: the width of the stroke around the characters
531 | - ```file_type (str, optional)```: the file type of the image, must be one of 'PNG', 'JPG', 'GIF', 'WEBP'
532 | - ```font (str, optional)```: the font to use for the image
533 | - ```width_ratio (float | 'auto', optional)```: the width ratio of the image, if 'auto', it will be calculated based on the font
534 | - ```char (str, optional)```: specifies one or more characters sorted by brightness, such as ' .$@'
535 | - ```enhance_image (bool, optional)```: if set to True, enhances the image before generating ASCII art
536 | - ```monochrome (bool, optional)```: if set to True, completely disables color
537 | - ```full_color (bool, optional)```: if set to True, uses the full color palette (16 million colors), otherwise uses the terminal color palette (8 colors)
538 | - ```front (str, optional)```: overrides the foreground color with a hex color (e.g. '#00FF00')
539 | - ```back (str, optional)```: background color (default: '#000000')
540 | 
541 | Example:
542 | 
543 | ```python
544 | from ascii_magic import AsciiArt
545 | 
546 | my_art = AsciiArt.from_image('lion.jpg')
547 | my_art.to_image_file('lion_output.png')
548 | ```
549 | 
550 | ## to_character_list()
551 | 
552 | Generates a 2d character list where each character is an object that contains the character, the terminal color, the terminal hex color, and the full hex color.
553 | 
554 | ```python
555 | AsciiArt.to_character_list(
556 |     full_color: bool = False,
557 |     # ... same parameters as AsciiArt.to_ascii()
558 | ) -> list[list[dict]]
559 | ```
560 | 
561 | Parameters:
562 | 
563 | - ```full_color (bool, optional)```: if set to True, uses the full color palette (16 million colors), otherwise uses the terminal color palette (8 colors)
564 | 
565 | Example:
566 | 
567 | ```python
568 | from ascii_magic import AsciiArt
569 | 
570 | my_art = AsciiArt.from_image('lion.jpg')
571 | my_character_list = my_art.to_character_list(columns=60)
572 | print(my_character_list)
573 | ```
574 | 
575 | Output:
576 | 
577 | ```python
578 | [
579 |     [
580 |         { 'character': 'o', 'terminal-color': '\x1b[31m', 'terminal-hex-color': '#FF0000', 'full-hex-color': '#FF3742' },
581 |         { 'character': '%', 'terminal-color': '\x1b[33m', 'terminal-hex-color': '#FF00FF', 'full-hex-color': '#FF43AA' },
582 |         { 'character': '#', 'terminal-color': '\x1b[31m', 'terminal-hex-color': '#FF0000', 'full-hex-color': '#FF3742' },
583 |         # ...
584 |     ],
585 |     [
586 |         { 'character': 'o', 'terminal-color': '\x1b[31m', 'terminal-hex-color': '#FF0000', 'full-hex-color': '#FF3742' },
587 |         { 'character': '%', 'terminal-color': '\x1b[33m', 'terminal-hex-color': '#FF00FF', 'full-hex-color': '#FF43AA' },
588 |         { 'character': '#', 'terminal-color': '\x1b[31m', 'terminal-hex-color': '#FF0000', 'full-hex-color': '#FF3742' },
589 |         # ...
590 |     ],
591 |     # ...
592 | ]
593 | ```
594 | 
595 | # Testing
596 | 
597 | With ```pytest``` installed, run it inside ```ascii_magic/tests/```.
598 | 
599 | # Licence
600 | 
601 | Copyright (c) 2020 Leandro Barone.
602 | 
603 | Usage is provided under the MIT License. See LICENSE for the full details.
604 | 


--------------------------------------------------------------------------------
/ascii_magic/ascii_art.py:
--------------------------------------------------------------------------------
  1 | from ascii_magic.constants import Front, Back, Modes, CHARS_BY_DENSITY, DEFAULT_STYLES, PALETTE
  2 | from ascii_magic.ascii_art_font import AsciiArtFont
  3 | 
  4 | from PIL import Image, ImageDraw, ImageEnhance
  5 | 
  6 | import io
  7 | import os
  8 | import json
  9 | import webbrowser
 10 | import urllib.request
 11 | from typing import Optional, Union, Literal
 12 | from time import time
 13 | 
 14 | 
 15 | class AsciiArt:
 16 |     __VERSION__ = 2.7
 17 | 
 18 |     def __init__(self, image: Image.Image):
 19 |         self._image = image
 20 | 
 21 |     @property
 22 |     def image(self) -> Image.Image:
 23 |         return self._image
 24 | 
 25 |     @image.setter
 26 |     def image(self, value):
 27 |         self._image = value
 28 | 
 29 |     def to_ascii(
 30 |         self,
 31 |         columns: int = 120,
 32 |         width_ratio: float = 2.2,
 33 |         char: Optional[str] = None,
 34 |         monochrome: bool = False,
 35 |         enhance_image: bool = False,
 36 |         back: Optional[Back] = None,
 37 |         front: Optional[Front] = None,
 38 |         debug: bool = False,
 39 |     ):
 40 |         art = self._img_to_art(
 41 |             columns=columns,
 42 |             width_ratio=width_ratio,
 43 |             char=char,
 44 |             mode=Modes.ASCII,
 45 |             monochrome=monochrome,
 46 |             enhance_image=enhance_image,
 47 |             back=back,
 48 |             front=front,
 49 |             debug=debug,
 50 |         )
 51 |         return art
 52 | 
 53 |     def to_terminal(
 54 |         self,
 55 |         columns: int = 120,
 56 |         width_ratio: float = 2.2,
 57 |         char: Optional[str] = None,
 58 |         enhance_image: bool = False,
 59 |         monochrome: bool = False,
 60 |         back: Optional[Back] = None,
 61 |         front: Optional[Front] = None,
 62 |         debug: bool = False,
 63 |     ):
 64 |         art = self._img_to_art(
 65 |             columns=columns,
 66 |             width_ratio=width_ratio,
 67 |             char=char,
 68 |             enhance_image=enhance_image,
 69 |             monochrome=monochrome,
 70 |             back=back,
 71 |             front=front,
 72 |             debug=debug,
 73 |         )
 74 |         print(art)
 75 |         return art
 76 | 
 77 |     def to_file(
 78 |         self,
 79 |         path: str,
 80 |         columns: int = 120,
 81 |         width_ratio: float = 2.2,
 82 |         char: Optional[str] = None,
 83 |         enhance_image: bool = False,
 84 |         monochrome: bool = False,
 85 |         back: Optional[Back] = None,
 86 |         front: Optional[Front] = None,
 87 |         debug: bool = False,
 88 |     ):
 89 |         art = self._img_to_art(
 90 |             columns=columns,
 91 |             width_ratio=width_ratio,
 92 |             char=char,
 93 |             enhance_image=enhance_image,
 94 |             monochrome=monochrome,
 95 |             back=back,
 96 |             front=front,
 97 |             debug=debug,
 98 |         )
 99 |         self._save_to_file(path, art)
100 |         return art
101 | 
102 |     def to_image_file(
103 |         self,
104 |         path: str,
105 |         width: Union[int, Literal['auto']] = 'auto',
106 |         height: Union[int, Literal['auto']] = 'auto',
107 |         border_width: int = 2,
108 |         stroke_width: float = 0.5,
109 |         file_type: Literal['PNG', 'JPG', 'GIF', 'WEBP'] = 'PNG',
110 |         font: str = 'courier_prime.ttf',
111 |         columns: int = 120,
112 |         width_ratio: Union[float, Literal['auto']] = 'auto',
113 |         char: Optional[str] = None,
114 |         enhance_image: bool = False,
115 |         monochrome: bool = False,
116 |         full_color: bool = False,
117 |         front: Optional[Union[Front, str]] = None,
118 |         back: str = '#000000',
119 |         debug: bool = False,
120 |     ):
121 |         try:
122 |             font = AsciiArtFont(font)
123 |         except FileNotFoundError:
124 |             raise FileNotFoundError(f'Font {font} not found')
125 | 
126 |         if width_ratio == 'auto':
127 |             width_ratio = font.get_ratio()
128 | 
129 |         art = self._img_to_art(
130 |             mode=Modes.OBJECT,
131 |             columns=columns,
132 |             width_ratio=width_ratio,
133 |             char=char,
134 |             enhance_image=enhance_image,
135 |             monochrome=monochrome,
136 |             full_color=full_color,
137 |             back=back,
138 |             front=front,
139 |             debug=debug,
140 |         )
141 | 
142 |         self._save_to_image_file(
143 |             path,
144 |             art,
145 |             font=font,
146 |             width=width,
147 |             height=height,
148 |             border_width=border_width,
149 |             stroke_width=stroke_width,
150 |             file_type=file_type,
151 |             monochrome=monochrome,
152 |             full_color=full_color,
153 |             front=front,
154 |             back=back,
155 |         )
156 |         return art
157 | 
158 |     def to_html(
159 |         self,
160 |         columns: int = 120,
161 |         width_ratio: float = 2.2,
162 |         char: Optional[str] = None,
163 |         enhance_image: bool = False,
164 |         monochrome: bool = False,
165 |         full_color: bool = False,
166 |         debug: bool = False,
167 |     ):
168 |         art = self._img_to_art(
169 |             mode=Modes.HTML,
170 |             columns=columns,
171 |             width_ratio=width_ratio,
172 |             char=char,
173 |             enhance_image=enhance_image,
174 |             monochrome=monochrome,
175 |             full_color=full_color,
176 |             debug=debug,
177 |         )
178 |         return art
179 | 
180 |     def to_html_file(
181 |         self,
182 |         path: str,
183 |         columns: int = 120,
184 |         width_ratio: float = 2.2,
185 |         char: Optional[str] = None,
186 |         enhance_image: bool = False,
187 |         monochrome: bool = False,
188 |         full_color: bool = True,
189 |         styles: str = DEFAULT_STYLES,
190 |         additional_styles: str = '',
191 |         auto_open: bool = False,
192 |         debug: bool = False,
193 |     ):
194 |         art = self._img_to_art(
195 |             mode=Modes.HTML,
196 |             columns=columns,
197 |             width_ratio=width_ratio,
198 |             char=char,
199 |             enhance_image=enhance_image,
200 |             monochrome=monochrome,
201 |             full_color=full_color,
202 |             debug=debug,
203 |         )
204 |         self._save_to_html_file(
205 |             path,
206 |             art,
207 |             styles=styles,
208 |             additional_styles=additional_styles,
209 |             auto_open=auto_open
210 |         )
211 |         return art
212 | 
213 |     def to_character_list(
214 |         self,
215 |         columns: int = 120,
216 |         width_ratio: float = 2.2,
217 |         char: Optional[str] = None,
218 |         monochrome: bool = False,
219 |         full_color: bool = False,
220 |         back: Optional[Back] = None,
221 |         front: Optional[Front] = None,
222 |         debug: bool = False,
223 |     ) -> list[list[dict]]:
224 |         return self._img_to_art(
225 |             mode=Modes.OBJECT,
226 |             columns=columns,
227 |             width_ratio=width_ratio,
228 |             char=char,
229 |             monochrome=monochrome,
230 |             full_color=full_color,
231 |             back=back,
232 |             front=front,
233 |             debug=debug,
234 |         )
235 | 
236 |     def _img_to_art(
237 |         self,
238 |         columns: int = 120,
239 |         width_ratio: float = 2.2,
240 |         char: Optional[str] = None,
241 |         mode: Modes = Modes.TERMINAL,
242 |         enhance_image: bool = False,
243 |         monochrome: bool = False,
244 |         full_color: bool = False,
245 |         back: Optional[Back] = None,
246 |         front: Optional[Front] = None,
247 |         debug: bool = False,
248 |     ) -> str:
249 |         if monochrome and full_color:
250 |             full_color = False
251 | 
252 |         if mode == Modes.TERMINAL and monochrome:
253 |             mode = Modes.ASCII
254 | 
255 |         if mode == Modes.HTML:
256 |             if monochrome:
257 |                 mode = Modes.HTML_MONOCHROME
258 |             elif full_color:
259 |                 mode = Modes.HTML_FULL_COLOR
260 |             else:
261 |                 mode = Modes.HTML_TERMINAL
262 | 
263 |         if mode not in Modes:
264 |             raise ValueError('Unknown output mode ' + str(mode))
265 | 
266 |         img_w, img_h = self._image.size
267 |         scalar = img_w * width_ratio / columns
268 |         img_w = int(img_w * width_ratio / scalar)
269 |         img_h = int(img_h / scalar)
270 |         rgb_img = self._image.resize((img_w, img_h))
271 |         if enhance_image:
272 |             rgb_img = ImageEnhance.Brightness(rgb_img).enhance(1.2)
273 |             rgb_img = ImageEnhance.Color(rgb_img).enhance(1.2)
274 |         color_palette = self._image.getpalette()
275 | 
276 |         grayscale_img = rgb_img.convert("L")
277 | 
278 |         chars = char if char else CHARS_BY_DENSITY
279 | 
280 |         if debug:
281 |             rgb_img.save('rgb.jpg')
282 |             grayscale_img.save('grayscale.jpg')
283 | 
284 |         lines = []
285 |         for h in range(img_h):
286 |             line = []
287 |             for w in range(img_w):
288 |                 # get brightness value
289 |                 brightness = grayscale_img.getpixel((w, h)) / 255
290 |                 pixel = rgb_img.getpixel((w, h))
291 | 
292 |                 # getpixel() may return an int, instead of tuple of ints, if the source img is a PNG with a transparency layer
293 |                 if isinstance(pixel, int):
294 |                     pixel = (pixel, pixel, 255) if color_palette is None else tuple(color_palette[pixel * 3:pixel * 3 + 3])
295 | 
296 |                 rgb = [(v / 255.0)**2.2 for v in pixel]
297 |                 char = chars[int(brightness * (len(chars) - 1))]
298 |                 character = self.get_color_data(char, rgb, brightness)
299 | 
300 |                 line.append(character)
301 |             lines.append(line)
302 | 
303 |         if mode == Modes.ASCII:
304 |             art = ''
305 |             for line in lines:
306 |                 for character in line:
307 |                     art += character['character']
308 |                 art += '\n'
309 |             return art
310 | 
311 |         if mode == Modes.TERMINAL:
312 |             art = ''
313 |             for line in lines:
314 |                 if back:
315 |                     art += self.cc(back)
316 | 
317 |                 previous_color = None
318 |                 for character in line:
319 |                     current_color = self.cc(front) if front else character['terminal-color']
320 |                     if current_color == previous_color:
321 |                         art += character['character']
322 |                     else:
323 |                         previous_color = current_color
324 |                         art += current_color + character['character']
325 | 
326 |                 if back:
327 |                     art += self.cc(Back.RESET)
328 | 
329 |                 art += self.cc(Front.RESET)
330 |                 art += '\n'
331 |             return art
332 | 
333 |         if mode == Modes.OBJECT:
334 |             art = []
335 |             for line in lines:
336 |                 art.append([])
337 |                 for character in line:
338 |                     art[-1].append(character)
339 |             return art
340 | 
341 |         if mode == Modes.HTML_MONOCHROME:
342 |             art = ''
343 |             for line in lines:
344 |                 art += ''
345 | 
346 |                 for character in line:
347 |                     art += '' + character['character'] + ''
348 | 
349 |                 art += ''
350 |                 art += '
' 351 | return art 352 | 353 | if mode == Modes.HTML_TERMINAL: 354 | art = '' 355 | for line in lines: 356 | art += '' 357 | 358 | for character in line: 359 | art += f'' + character['character'] + '' 360 | 361 | art += '' 362 | art += '
' 363 | 364 | return art 365 | 366 | if mode == Modes.HTML_FULL_COLOR: 367 | art = '' 368 | for line in lines: 369 | art += '' 370 | 371 | for character in line: 372 | art += f'' + character['character'] + '' 373 | 374 | art += '' 375 | art += '
' 376 | 377 | return art 378 | 379 | @staticmethod 380 | def cc(color: Front | Back) -> str: 381 | return '\033[' + str(color.value) + 'm' 382 | 383 | @staticmethod 384 | def l2_min(v1: list, v2: list) -> float: 385 | return (v1[0] - v2[0])**2 + (v1[1] - v2[1])**2 + (v1[2] - v2[2])**2 386 | 387 | @staticmethod 388 | def get_color_data(char: str, rgb: Union[list, tuple], brightness: float) -> dict: 389 | min_distance = 2 390 | index = 0 391 | 392 | for i in range(len(PALETTE)): 393 | tmp = [v * brightness for v in PALETTE[i][0]] 394 | distance = AsciiArt.l2_min(tmp, rgb) 395 | 396 | if distance < min_distance: 397 | index = i 398 | min_distance = distance 399 | 400 | return { 401 | 'character': char, 402 | 'terminal-color': AsciiArt.cc(PALETTE[index][1]), 403 | 'terminal-hex-color': PALETTE[index][2], 404 | 'full-hex-color': '#{:02x}{:02x}{:02x}'.format(*(int(c * 200 + 55) for c in rgb)), 405 | } 406 | 407 | @staticmethod 408 | def _save_to_file(path: str, art: str) -> None: 409 | with open(path, 'w') as f: 410 | f.write(art) 411 | 412 | @staticmethod 413 | def _save_to_image_file( 414 | path: str, 415 | art: list, 416 | width: Union[int, Literal['auto']] = 'auto', 417 | height: Union[int, Literal['auto']] = 'auto', 418 | border_width: int = 2, 419 | stroke_width: float = 0.5, 420 | file_type: Literal['PNG', 'JPG', 'GIF', 'WEBP'] = 'PNG', 421 | font: Optional[AsciiArtFont] = None, 422 | monochrome: bool = False, 423 | full_color: bool = False, 424 | front: Optional[str] = None, 425 | back: str = '#000000', 426 | ) -> None: 427 | if font is None: 428 | font = AsciiArtFont('courier_prime.ttf') 429 | char_width, _, line_height = font.get_char_size() 430 | 431 | cols = max(len(line) for line in art) 432 | rows = len(art) 433 | 434 | img_width = cols * char_width + border_width * 2 435 | img_height = rows * line_height + border_width * 2 436 | 437 | img = Image.new('RGB', (img_width, img_height), color=back) 438 | draw = ImageDraw.Draw(img) 439 | 440 | y = border_width - 1 441 | for line in art: 442 | x = border_width 443 | for character in line: 444 | fg_color = None 445 | if front: 446 | fg_color = front 447 | elif full_color: 448 | fg_color = character['full-hex-color'] 449 | elif monochrome: 450 | fg_color = '#FFFFFF' 451 | else: 452 | fg_color = character['terminal-hex-color'] 453 | 454 | draw.text((x, y), character['character'], fill=fg_color, font=font.get_font(), stroke_width=stroke_width) 455 | x += char_width 456 | y += line_height 457 | 458 | target_width = width if width != 'auto' else img_width 459 | target_height = height if height != 'auto' else img_height 460 | 461 | if target_width != img_width and height == 'auto': 462 | target_height = int(target_height * target_width / img_width) 463 | if target_height != img_height and width == 'auto': 464 | target_width = int(target_width * target_height / img_height) 465 | 466 | if target_width != img_width or target_height != img_height: 467 | img = img.resize((target_width, target_height), Image.Resampling.LANCZOS) 468 | 469 | img.save(path, file_type) 470 | 471 | @staticmethod 472 | def _save_to_html_file( 473 | path: str, 474 | art: str, 475 | styles: str = DEFAULT_STYLES, 476 | additional_styles: str = '', 477 | auto_open: bool = False, 478 | ) -> None: 479 | html = f""" 480 | 481 | ASCII art 482 | 483 | 484 | 485 |
{art}
486 | 487 | """ 488 | with open(path, 'w') as f: 489 | f.write(html) 490 | if auto_open: 491 | webbrowser.open(path) 492 | 493 | @classmethod 494 | def quick_test(cls): 495 | img = cls.from_url('https://cataas.com/cat') 496 | img.to_terminal() 497 | 498 | @classmethod 499 | def print_palette(cls): 500 | for f in Front: 501 | if f == Front.RESET: 502 | continue 503 | for b in Back: 504 | if b == Back.RESET: 505 | continue 506 | print( 507 | f.name + ' on ' + b.name + ' = ', 508 | cls.cc(f), 509 | cls.cc(b), 510 | 'ASCII_MAGIC', 511 | cls.cc(Front.RESET), 512 | cls.cc(Back.RESET), 513 | ) 514 | 515 | @classmethod 516 | def from_url(cls, url: str) -> 'AsciiArt': 517 | img = cls._load_url(url) 518 | return AsciiArt(img) 519 | 520 | @classmethod 521 | def from_image(cls, path: str) -> 'AsciiArt': 522 | img = cls._load_file(path) 523 | return AsciiArt(img) 524 | 525 | @classmethod 526 | def from_pillow_image(cls, img: Image.Image) -> 'AsciiArt': 527 | return AsciiArt(img) 528 | 529 | @classmethod 530 | def from_clipboard(cls) -> 'AsciiArt': 531 | img = cls._load_clipboard() 532 | return AsciiArt(img) 533 | 534 | @classmethod 535 | def from_gemini( 536 | cls, 537 | prompt: str, 538 | model: str = None, 539 | api_key: Optional[str] = None, 540 | debug: bool = False 541 | ) -> 'AsciiArt': 542 | image = cls._load_gemini(prompt, model=model, api_key=api_key, debug=debug) 543 | return AsciiArt(image) 544 | 545 | @classmethod 546 | def from_swamui( 547 | cls, 548 | prompt: str, 549 | width: int = 1280, 550 | height: int = 720, 551 | steps: int = 20, 552 | raw_input: dict = {}, 553 | server: str = 'http://localhost:7801', 554 | model: str = 'auto', 555 | debug: bool = False 556 | ) -> 'AsciiArt': 557 | image = cls._load_swarmui( 558 | prompt, 559 | width=width, 560 | height=height, 561 | steps=steps, 562 | raw_input=raw_input, 563 | server=server, 564 | model=model, 565 | debug=debug, 566 | ) 567 | return AsciiArt(image) 568 | 569 | @classmethod 570 | def _load_url(cls, url: str) -> Image.Image: 571 | 572 | with urllib.request.urlopen(url) as response: 573 | return Image.open(response) 574 | 575 | @classmethod 576 | def _load_file(cls, path: str) -> Image.Image: 577 | return Image.open(path) 578 | 579 | @classmethod 580 | def _load_clipboard(cls) -> Image.Image: 581 | try: 582 | from PIL import ImageGrab 583 | img = ImageGrab.grabclipboard() 584 | except (NotImplementedError, ImportError): 585 | img = cls._load_clipboard_linux() 586 | 587 | if not img: 588 | raise OSError('The clipboard does not contain an image') 589 | 590 | return img 591 | 592 | @classmethod 593 | def _load_clipboard_linux(cls) -> Image.Image: 594 | try: 595 | import gi # type: ignore 596 | gi.require_version("Gtk", "3.0") # type: ignore 597 | from gi.repository import Gtk, Gdk # type: ignore 598 | except ModuleNotFoundError: 599 | print('Accessing the clipboard under Linux requires the PyGObject module') 600 | print('Ubuntu/Debian: sudo apt install python3-gi python3-gi-cairo gir1.2-gtk-3.0') 601 | print('Fedora: sudo dnf install python3-gobject gtk3') 602 | print('Arch: sudo pacman -S python-gobject gtk3') 603 | print('openSUSE: sudo zypper install python3-gobject python3-gobject-Gdk typelib-1_0-Gtk-3_0 libgtk-3-0') 604 | exit() 605 | 606 | clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) 607 | 608 | try: 609 | buffer = clipboard.wait_for_image() 610 | data = buffer.get_pixels() 611 | w = buffer.props.width 612 | h = buffer.props.height 613 | stride = buffer.props.rowstride 614 | except Exception: 615 | raise OSError('The clipboard does not contain an image') 616 | 617 | mode = 'RGB' 618 | img = Image.frombytes(mode, (w, h), data, 'raw', mode, stride) 619 | return img 620 | 621 | @classmethod 622 | def _load_gemini( 623 | cls, 624 | prompt: str, 625 | model: str = None, 626 | api_key: Optional[str] = None, 627 | debug: bool = False 628 | ) -> Image.Image: 629 | try: 630 | from google import genai 631 | except ModuleNotFoundError: 632 | print('Using Gemini requires the google-genai module') 633 | print('pip install google-genai') 634 | exit() 635 | 636 | environ_api_key = os.environ.get('GEMINI_API_KEY') 637 | if not api_key and environ_api_key: 638 | api_key = environ_api_key 639 | 640 | if not api_key: 641 | raise ValueError('You must set up an API key before accessing Gemini') 642 | 643 | if not model: 644 | model = 'gemini-2.0-flash-preview-image-generation' 645 | 646 | client = genai.Client( 647 | api_key=api_key, 648 | ) 649 | 650 | response = client.models.generate_content( 651 | model=model, 652 | contents=prompt, 653 | config=genai.types.GenerateContentConfig( 654 | response_modalities=["IMAGE", "TEXT"], 655 | ), 656 | ) 657 | 658 | if debug: 659 | with open(str(int(time())) + '_gemini.txt', 'w') as f: 660 | f.write(str(response)) 661 | 662 | for part in response.parts: 663 | if part.inline_data: 664 | generated_image = part.as_image() 665 | if debug: 666 | try: 667 | with open(str(int(time())) + '_gemini.png', 'wb') as f: 668 | f.write(generated_image.image_bytes) 669 | except Exception: 670 | pass 671 | return Image.open(io.BytesIO(generated_image.image_bytes)) 672 | 673 | raise OSError('No images generated') 674 | 675 | @classmethod 676 | def _load_swarmui( 677 | cls, 678 | prompt: str, 679 | width: int = 1280, 680 | height: int = 720, 681 | steps: int = 20, 682 | raw_input: dict = {}, 683 | server: str = 'http://localhost:7801', 684 | model: str = 'auto', 685 | debug: bool = False 686 | ) -> Image.Image: 687 | environ_server = os.environ.get('SWARMUI_SERVER') 688 | if not server and environ_server: 689 | server = environ_server 690 | 691 | if not server: 692 | raise ValueError('You must set up a SwarmUI server before accessing SwarmUI') 693 | 694 | session_response = urllib.request.urlopen( 695 | urllib.request.Request( 696 | f'{server}/API/GetNewSession', 697 | data=json.dumps({}).encode('utf-8'), 698 | headers={'Content-Type': 'application/json'}, 699 | method='POST' 700 | ) 701 | ) 702 | session_data = json.loads(session_response.read().decode('utf-8')) 703 | session_id = session_data.get('session_id') 704 | 705 | if not session_id: 706 | raise OSError('Failed to obtain session_id from SwarmUI server') 707 | 708 | # Pick model from server if none was provided 709 | if model == 'auto': 710 | models_response = urllib.request.urlopen( 711 | urllib.request.Request( 712 | f'{server}/API/ListModels', 713 | data=json.dumps({ 714 | 'session_id': session_id, 715 | 'path': '', 716 | 'depth': 3, 717 | }).encode('utf-8'), 718 | headers={'Content-Type': 'application/json'}, 719 | method='POST', 720 | ) 721 | ) 722 | 723 | models = json.loads(models_response.read().decode('utf-8')) 724 | 725 | if debug: 726 | with open(str(int(time())) + '_swarmui_ListModels_response.txt', 'w') as f: 727 | f.write(str(models)) 728 | 729 | model = models['files'][0]['name'] 730 | 731 | generate_response = urllib.request.urlopen( 732 | urllib.request.Request( 733 | f'{server}/API/GenerateText2Image', 734 | data=json.dumps({ 735 | 'session_id': session_id, 736 | 'images': 1, 737 | 'model': model, 738 | 'prompt': prompt, 739 | 'width': width, 740 | 'height': height, 741 | 'steps': steps, 742 | **raw_input, 743 | }).encode('utf-8'), 744 | headers={'Content-Type': 'application/json'}, 745 | method='POST', 746 | ) 747 | ) 748 | 749 | generate_data = json.loads(generate_response.read().decode('utf-8')) 750 | 751 | if debug: 752 | with open(str(int(time())) + '_swarmui_GenerateText2Image_response.txt', 'w') as f: 753 | f.write(str(generate_data)) 754 | 755 | if 'error' in generate_data: 756 | raise OSError(generate_data['error']) 757 | 758 | if 'images' in generate_data and len(generate_data['images']) > 0: 759 | image_path = generate_data['images'][0] 760 | image_path_parsed = urllib.parse.quote(image_path, safe='/') 761 | image_url = f'{server}/{image_path_parsed}' 762 | return cls._load_url(image_url) 763 | 764 | raise OSError('No images generated by SwarmUI') 765 | --------------------------------------------------------------------------------