|
| 1 | +import colorama |
| 2 | +import shutil |
| 3 | +import os |
| 4 | +from PIL import Image |
| 5 | +from PIL import ImageDraw |
| 6 | + |
| 7 | +class Ascii(): |
| 8 | + def __init__(self, in_file): |
| 9 | + """ Python ascii image maker """ |
| 10 | + |
| 11 | + self.letter_densities = '" .`-_\':,;^=+/"|)\\<>)iv%xclrs{*}I?!][1taeo7zjLunT#JCwfy325Fp6mqSghVd4EgXPGZbYkOA&8U$@KHDBWNMR0Q' |
| 12 | + |
| 13 | + self.from_pic = Image.open(in_file) |
| 14 | + self.to_pic = Image.new("RGB", self.from_pic.size, "black") |
| 15 | + self.draw = ImageDraw.Draw(self.to_pic) |
| 16 | + |
| 17 | + self.MAX_W, self.MAX_H = self.from_pic.size |
| 18 | + |
| 19 | + def word_artify(self, words="#", step=3): |
| 20 | + """ create ascii image from word string """ |
| 21 | + |
| 22 | + words = words.upper() |
| 23 | + |
| 24 | + h = w = i = 0 |
| 25 | + |
| 26 | + # skip pixels by `step` amount and place characters around image |
| 27 | + while h < self.MAX_H: |
| 28 | + while w < self.MAX_W: |
| 29 | + # use a string as the art characters |
| 30 | + c = words[i] |
| 31 | + |
| 32 | + # loop around |
| 33 | + i = (i+1) % len(words) |
| 34 | + |
| 35 | + # get the color from the pixel |
| 36 | + p = self.from_pic.getpixel((w, h)) |
| 37 | + |
| 38 | + # insert text based on pixel color |
| 39 | + self.draw.text((w, h), c, p) |
| 40 | + |
| 41 | + w += step |
| 42 | + h += step |
| 43 | + w = 0 |
| 44 | + |
| 45 | + self.from_pic.close() |
| 46 | + |
| 47 | + |
| 48 | + def density_artify(self, step=7): |
| 49 | + """ |
| 50 | + Generates the acsii image where the 'words' are selected based on |
| 51 | + the brightness of a pixel. |
| 52 | +
|
| 53 | + A brighter pixel will have a character with lower visual density and |
| 54 | + a less bright pixel will have a character with high visual density. |
| 55 | + """ |
| 56 | + |
| 57 | + if step < 7: |
| 58 | + step = 7 |
| 59 | + |
| 60 | + h = 0 |
| 61 | + w = 0 |
| 62 | + |
| 63 | + # used for brightness (density) levels |
| 64 | + grayscale_img = self.from_pic.convert("L") |
| 65 | + |
| 66 | + while h < self.MAX_H: |
| 67 | + while w < self.MAX_W: |
| 68 | + # get brightness value |
| 69 | + brightness = 255 - grayscale_img.getpixel((w, h)) |
| 70 | + clr = self.from_pic.getpixel((w, h)) |
| 71 | + |
| 72 | + # select required character from the letter_densities list |
| 73 | + char_pos = (brightness/255.0) * (len(self.letter_densities) - 1) |
| 74 | + c = self.letter_densities[int(round(char_pos, 0))] |
| 75 | + self.draw.text((w, h), c, clr) |
| 76 | + |
| 77 | + w += step |
| 78 | + h += step |
| 79 | + w = 0 |
| 80 | + |
| 81 | + self.from_pic.close() |
| 82 | + grayscale_img.close() |
| 83 | + |
| 84 | + |
| 85 | + def terminal_artify(self): |
| 86 | + """ covert image to ascii, display in terminal """ |
| 87 | + |
| 88 | + # all colors are beautiful |
| 89 | + acab = [ |
| 90 | + [( 0, 0, 0), colorama.Fore.LIGHTBLACK_EX], |
| 91 | + [( 0, 0, 255), colorama.Fore.BLUE], |
| 92 | + [( 0, 255, 0), colorama.Fore.GREEN], |
| 93 | + [(255, 0, 0), colorama.Fore.RED], |
| 94 | + [(255, 255, 255), colorama.Fore.WHITE], |
| 95 | + [(255, 0, 255), colorama.Fore.MAGENTA], |
| 96 | + [( 0, 255, 255), colorama.Fore.CYAN], |
| 97 | + [(255, 255, 0), colorama.Fore.YELLOW] |
| 98 | + ] |
| 99 | + |
| 100 | + # convert to floats, linearize |
| 101 | + # in case more colors are added |
| 102 | + acab = [ [[(v/255.0)**2.2 for v in x[0]], x[1]] for x in acab ] |
| 103 | + |
| 104 | + # needed for Windows operating systems |
| 105 | + colorama.init() |
| 106 | + |
| 107 | + canvas = self.from_pic |
| 108 | + current_h, current_w = float(self.MAX_H), float(self.MAX_W) |
| 109 | + |
| 110 | + # resize to fit current dimensions of terminal |
| 111 | + # shutil added get_terminal_size starting in python 3.3 |
| 112 | + t_width, t_height = shutil.get_terminal_size() |
| 113 | + |
| 114 | + if current_h > t_height or current_w > t_width: |
| 115 | + # floating point division |
| 116 | + scalar = max(current_h/t_height, (current_w*2)/t_width) |
| 117 | + current_w = int((current_w*2)/scalar) |
| 118 | + current_h = int((current_h)/scalar) |
| 119 | + dimensions = current_w, current_h |
| 120 | + canvas = self.from_pic.resize(dimensions) |
| 121 | + |
| 122 | + # used for brightness (density) levels |
| 123 | + grayscale_img = canvas.convert("L") |
| 124 | + |
| 125 | + image = "" |
| 126 | + |
| 127 | + for h in range(current_h): |
| 128 | + for w in range(current_w): |
| 129 | + # get brightness value |
| 130 | + brightness = grayscale_img.getpixel((w, h))/255.0 |
| 131 | + pixel = canvas.getpixel((w, h)) |
| 132 | + # getpixel() may return an int, instead of tuple of ints, if the |
| 133 | + # source img is a PNG with a transparency layer. |
| 134 | + if isinstance(pixel, int): |
| 135 | + pixel = (pixel, pixel, 255) |
| 136 | + |
| 137 | + srgb = [ (v/255.0)**2.2 for v in pixel ] |
| 138 | + |
| 139 | + # select required character from the letter_densities list |
| 140 | + char_pos = brightness * (len(self.letter_densities) - 1) |
| 141 | + |
| 142 | + color = self._convert_color(srgb, brightness, acab) |
| 143 | + image += color + self.letter_densities[int(round(char_pos, 0))] |
| 144 | + image += "\n" |
| 145 | + |
| 146 | + # prints the converted image to terminal |
| 147 | + # (remove the last newline) |
| 148 | + print(image[:-1] + colorama.Fore.RESET) |
| 149 | + |
| 150 | + self.from_pic.close() |
| 151 | + grayscale_img.close() |
| 152 | + |
| 153 | + input("Press enter to continue...") |
| 154 | + |
| 155 | + |
| 156 | + ####################################### |
| 157 | + # Helper Methods # |
| 158 | + ####################################### |
| 159 | + |
| 160 | + def _L2_min(self, v1, v2): |
| 161 | + """ |
| 162 | + euclidian norm in a 2 dimensional space |
| 163 | + used for calculating shortest distance |
| 164 | + """ |
| 165 | + |
| 166 | + return (v1[0]-v2[0])**2 + (v1[1]-v2[1])**2 + (v1[2]-v2[2])**2 |
| 167 | + |
| 168 | + def _convert_color(self, rgb, brightness, acab): |
| 169 | + """ convert color using acab data """ |
| 170 | + |
| 171 | + min_distance = 2 |
| 172 | + index = 0 |
| 173 | + |
| 174 | + for i in range(len(acab)): |
| 175 | + tmp = [ v*brightness for v in acab[i][0] ] |
| 176 | + distance = self._L2_min(tmp, rgb) |
| 177 | + |
| 178 | + if distance < min_distance: |
| 179 | + index = i |
| 180 | + min_distance = distance |
| 181 | + |
| 182 | + return acab[index][1] |
| 183 | + |
| 184 | + def save(self, out_file="out.jpg"): |
| 185 | + """ save image to file """ |
| 186 | + |
| 187 | + self.to_pic.save(out_file) |
| 188 | + self.to_pic.close() |
| 189 | + |
0 commit comments