-
Notifications
You must be signed in to change notification settings - Fork 0
/
img.py
executable file
·192 lines (162 loc) · 6.55 KB
/
img.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
#!/usr/bin/env python3
import os
import argparse
from pathlib import Path
from typing import List, Tuple, Dict, Optional, Iterator, Union, BinaryIO
import contextlib
import sys
from PIL import Image # type: ignore
def get_palette() -> List[Tuple[str, str]]:
# Basic colors 0 - 7
# Bright basic colors 8 - 15
palette = [
("00", "000000"),
("01", "800000"),
("02", "008000"),
("03", "808000"),
("04", "000080"),
("05", "800080"),
("06", "008080"),
("07", "c0c0c0"),
("08", "808080"),
("09", "ff0000"),
("10", "00ff00"),
("11", "ffff00"),
("12", "0000ff"),
("13", "ff00ff"),
("14", "00ffff"),
("15", "ffffff"),
]
# Extended colors 16 - 231 (000000, ffffff)
# Almost evenly spread
# values = [0] + [95 + 40 * i for i in range(5)]
# All RGB values with any combination of "values" are valid
color_code = 16
values = ["00", "5f", "87", "af", "d7", "ff"]
for r in values:
for g in values:
for b in values:
palette.append((str(color_code), "".join([r, g, b])))
color_code += 1
# Gray scale 232 - 255 (080808 - eeeeee)
# Almost evenly spread
gray_values = [8 + 10 * i for i in range(24)]
for i in gray_values:
palette.append((str(color_code), "{:02x}{:02x}{:02x}".format(i, i, i)))
color_code += 1
return palette
def create_closest_valid_color_dict() -> Dict[int, str]:
# All RGB values with any combination of `values` are valid
values = ["00", "5f", "87", "af", "d7", "ff"]
closest_hex_lookup = {}
for i in range(256):
distances = [abs(i - int(value, 16)) for value in values]
index_of_minimum = distances.index(min(distances))
closest_hex_lookup[i] = values[index_of_minimum]
return closest_hex_lookup
# Pregenerate lookup table for finding the closest valid color value
CLOSEST_VALID_COLORS = create_closest_valid_color_dict()
# Pregenerate lookup table for converting a valid hexcolor to colorcode
HEX_2_COLORCODE = {hexcolor: colorcode for colorcode, hexcolor in get_palette()}
def get_colorcode_from_rgb(rgb_tuple: Tuple[int, int, int]) -> str:
hexcolor = "".join([CLOSEST_VALID_COLORS[color] for color in rgb_tuple])
return HEX_2_COLORCODE[hexcolor]
def is_valid_image(path: Path) -> bool:
try:
im = Image.open(path)
except:
return False
return True
def flatten_rgba_to_rgb_with_black_background(rgba: Tuple[int, int, int, int]) -> List[int]:
opacity = rgba[3] / 255
return [int(channel * opacity) for channel in rgba[:3]]
def process_image(image: Union[Path, BinaryIO], cols: Optional[int] = None, rows: Optional[int] = None) -> List[str]:
term_cols, term_rows = os.get_terminal_size(1)
if not cols:
cols = term_cols
if not rows:
rows = term_rows
size = (cols, rows*2)
im = Image.open(image)
im.thumbnail(size, Image.Resampling.LANCZOS)
# Convert to known format to avoid issues with transparency and palette based formats
im = im.convert('RGBA')
output = []
for y in range(1, im.size[1], 2): # Use y and y -1 every loop
line = ""
for x in range(0, im.size[0], 1):
# Build image using utf-8 half block symbol
char = "▄"
# Background (top)
rgba = im.getpixel((x, y - 1))
rgb = flatten_rgba_to_rgb_with_black_background(rgba)
colorcode = get_colorcode_from_rgb(rgb)
background_color = "\033[48;5;{}m".format(colorcode)
# Foreground (bottom)
rgba = im.getpixel((x, y))
rgb = flatten_rgba_to_rgb_with_black_background(rgba)
colorcode = get_colorcode_from_rgb(rgb)
foreground_color = "\033[38;5;{}m".format(colorcode)
line += background_color + foreground_color + char
line += "\033[0m" # Clear formatting
output.append(line)
return output
@contextlib.contextmanager
def safe_print() -> Iterator[None]:
try:
yield
finally:
print("\033[0m", end="") # Make sure to not break terminal
def main() -> None:
parser = argparse.ArgumentParser(description="Print image to terminal")
parser.add_argument("image_paths", help="Path to image(s)", nargs="*")
parser.add_argument("-c", "--cols", type=int, help="Columns of image")
parser.add_argument("-r", "--rows", type=int, help="Rows of image")
args = parser.parse_args()
if len(args.image_paths) == 1:
# Fill terminal by default
if args.image_paths == ["-"]:
# Read directly from stdin
image = process_image(sys.stdin.buffer, args.cols, args.rows)
with safe_print():
for text_row in image:
print(text_row)
path = Path(args.image_paths[0])
if is_valid_image(path):
image = process_image(path, args.cols, args.rows)
with safe_print():
for text_row in image:
print(text_row)
else:
# Show thumbs and filenames
if not args.image_paths:
image_paths = Path().iterdir()
else:
image_paths = (Path(path) for path in args.image_paths)
sorted_image_paths = sorted([path for path in image_paths if is_valid_image(path)])
cols = args.cols
if not cols:
cols = 40
blank_line = " " * cols
column_separator = " "
row_separator = "\n"
term_cols, term_rows = os.get_terminal_size(1)
images_per_row = term_cols // (cols + len(column_separator))
current_row_images = []
current_row_paths = []
for path in sorted_image_paths:
current_row_images.append(process_image(path, cols, args.rows))
current_row_paths.append(path)
if len(current_row_images) == images_per_row or path == sorted_image_paths[-1]:
max_rows = max([len(im) for im in current_row_images])
print(column_separator.join(["{:{}}".format(str(path), cols) for path in current_row_paths]))
for line in range(max_rows):
image_lines = [im[line] if len(im) > line else blank_line for im in current_row_images]
output = column_separator.join(image_lines)
with safe_print():
print(output)
print(row_separator, end="")
current_row_images = []
current_row_paths = []
if __name__ == "__main__":
main()