import colorsys import logging import math import os from rich.console import Console from PIL import Image, ImageFilter import pygame as pg import filters logging.getLogger(__name__) FONT_PATH = 'fonts/scientifica.ttf' TEXT_WIDTH = 6 # on font size 12 monocraft medium TEXT_HEIGHT = 11 SCALE_FACTOR = 1 RESIZE_FACTOR = 0.75 SCALED_WIDTH = TEXT_WIDTH // SCALE_FACTOR SCALED_HEIGHT = TEXT_HEIGHT // SCALE_FACTOR DEBUG = True ANTI_ALIASING = False if not os.path.exists('./debug/') and DEBUG: os.makedirs('./debug') # '█@?OPoci. ' CHARS = list(reversed([ u'█', '@', '?', 'O', 'P', 'o', 'c', 'i', '.', ' ' ])) NEW_CHARS = ' .:\',`;_<^~%>"?!ir=clsxzL}ovCS(+eknuFJfj{amwPZtIT)*hbdpqyEGKOVXYgU&ABDHQR[]MNW@$#' EDGE_CHARS = '-\\|/' ALL_CHARS = [ u'█', '@', '?', 'O', 'P', 'o', 'c', 'i', '.', ' ', '|', '/', '-', '\\' ] # Character Text Image Format class CTIF: colors: list text: str width: int height: int def __init__(self, image: Image.Image | None = None): self.colors = [] self.text = "" self.width = 0 self.height = 0 if image: self.convert(image) def convert(self, image: Image.Image) -> None: if image.mode != 'RGB': image = image.convert('RGB') image = image.resize((int(image.width * RESIZE_FACTOR), int(image.height * RESIZE_FACTOR))) logging.debug(f'Image Size: {image.size}') L_image = image.convert('L') # logging.debug(f'L_image Size: {L_image.size}') dog = filters.difference_of_gaussians(image, blur_strength_2=1) # logging.debug(f'Dog Size: {dog.size}') dog.save('debug/dog.png') sb, gradient = filters.sobel(dog) # image = image.convert('HSV') w, h = sb.size sb.save('debug/sobel.png') text_grid_width = w // SCALED_WIDTH self.width = text_grid_width text_grind_height = h // SCALED_HEIGHT self.height = text_grind_height y_offset = 0 for y in range(math.floor(h / SCALED_HEIGHT)): x_offset = 0 for x in range(math.floor(w / SCALED_WIDTH)): histogram = {} # Collect the most common char for a group of pixels color = [] for y2 in range(SCALED_HEIGHT): for x2 in range(SCALED_WIDTH): real_x = x2 + x_offset real_y = y2 + y_offset r, g, b = image.getpixel((real_x, real_y)) color.append((r, g, b)) h, s, v = colorsys.rgb_to_hsv(r, g, b) # print(h, s, v) gradient_v = gradient[real_y * w + real_x] char = ' ' if gradient_v: char = self._match_gradient(gradient_v) else: char = NEW_CHARS[round(v/(255/len(NEW_CHARS))) - 1] if char in histogram: histogram[char] += 1 else: histogram[char] = 1 r, g, b = filters.average_colors(color) color_avg_hsv = colorsys.rgb_to_hsv(r/255, g/255, b/255) self.colors.append(color_avg_hsv) # get most common most_common = None score = float('-inf') for char in histogram: if histogram[char] > score: score = histogram[char] most_common = char self.text += str(most_common) x_offset += SCALED_WIDTH y_offset += SCALED_HEIGHT # TODO @0x01FE : refactor plz increment by 30 & 60 def _match_gradient(self, n: float) -> str: n = abs(n) if n < math.radians(30): return '|' elif n < math.radians(60): return '/' elif n < math.radians(120): return '-' elif n < math.radians(150): return '\\' elif n < math.radians(210): return '|' elif n < math.radians(240): return '/' elif n < math.radians(300): return '-' elif n < math.radians(330): return '\\' else: return '|' def render(self) -> None: console = Console() for i, (color, char) in enumerate(zip(self.colors, self.text)): hex_color = '#%x%x%x' % color console.print(f'[{hex_color}]{char}', end='') if i + 1 % self.width == 0: print() def save_image(self, path: str) -> None: logging.debug('Rendering and Saving image...') pg.init() font = pg.font.Font(FONT_PATH, 12) window = pg.display.set_mode((self.width * TEXT_WIDTH, self.height * TEXT_HEIGHT)) x, y = 0, 0 logging.debug(f'Rendering CITF Image with Dim: ({self.width}, {self.height})') for (color, char) in zip(self.colors, self.text): h, s, v = color if char not in EDGE_CHARS: v = (NEW_CHARS.find(char) + 1) * (255/len(NEW_CHARS))/255 # logging.debug(f'HSV Color: {h, s, v}') # Make the bright colors brighter and the darks darker # if v <= 0.4: # v *= 1.5 r, g, b = colorsys.hsv_to_rgb(h, s, v) # logging.debug(f'RGB Color Before: {r, g, b}') r *= 255 g *= 255 b *= 255 r = min(r, 255) g = min(g, 255) b = min(b, 255) # logging.debug(f'RGB Color: {r, g, b}') rendered_char = font.render(char, ANTI_ALIASING, (int(r), int(g), int(b))) window.blit(rendered_char, (x * TEXT_WIDTH, y * TEXT_HEIGHT)) x += 1 if x >= self.width: x = 0 y += 1 pg.display.update() pg.image.save(window, path) pg.quit() def save_edges(self, path: str) -> None: pg.init() font = pg.font.Font(FONT_PATH, 12) window = pg.display.set_mode((self.width * TEXT_WIDTH, self.height * TEXT_HEIGHT)) x, y = 0, 0 logging.debug(f'Rendering CITF Image with Dim: ({self.width}, {self.height})') for (color, char) in zip(self.colors, self.text): if char in EDGE_CHARS: h, s, v = color new_color = colorsys.hsv_to_rgb(h, s, 255) # print(color) rendered_char = font.render(char, ANTI_ALIASING, new_color) window.blit(rendered_char, (x * TEXT_WIDTH, y * TEXT_HEIGHT)) x += 1 if x >= self.width: x = 0 y += 1 pg.display.update() pg.image.save(window, path) pg.quit() def save_citf(self, filename: str) -> None: # 1 byte for h, 1 byte for s, 1 byte for character which will be interperated as v when rendering bin = bytes() # Save Width and Height bin += self.width.to_bytes(2, 'big') bin += self.height.to_bytes(2, 'big') for (color, char) in zip(self.colors, self.text): h, s, v = color h = min(int(h * 255), 255) bin += h.to_bytes(1, 'big') s = min(int(s * 255), 255) bin += s.to_bytes(1, 'big') # for n in (h, s): # i = int(n * 255) # bin += i.to_bytes(1, 'big') char_index = (NEW_CHARS + EDGE_CHARS).index(char) bin += char_index.to_bytes(1, 'big') if char in EDGE_CHARS: v = min(int(v * 255), 255) bin += v.to_bytes(1, 'big') with open(f'{filename}.citf', 'wb+') as file: file.write(bin) def read_citf(self, filepath: str) -> None: self.text = '' self.colors = [] with open(filepath, 'rb') as file: data = file.read() # Get Width and Height self.width = int.from_bytes(data[:2], 'big') self.height = int.from_bytes(data[2:4], 'big') data = data[4:] if len(data) % 3 != 0: logging.error(f"File is bad size. {len(data)}") # Read colors and characters i = 0 for n in range(1, (int(len(data)/3)) + 1): d = data[i:i + 3] # print(f'index: {i}') # print(f'index end: {i + 3}') # print(f'data: {d}') h = d[0:1] h = int.from_bytes(h, 'big') s = d[1:2] s = int.from_bytes(s, 'big') char_n = d[2:3] char_n = int.from_bytes(char_n, 'big') # print(char_n) char = (NEW_CHARS + EDGE_CHARS)[char_n] logging.debug(f'H: {h}, S: {s}, Char: {char}') self.text += char if char in EDGE_CHARS: v = d[i+3:i+4] v = int.from_bytes(v, 'big') self.colors.append((h/255, s/255, v/255)) i += 4 else: self.colors.append((h/255, s/255, 255)) i += 3