#!/usr/bin/env python3 # x-run: python3 % ~/downloads/kanade/6bf3cdae12b75326e3c23af73105e5781e42e94e.jpg from typing import BinaryIO, TextIO from PIL import Image from sys import argv class Converter: CC_COLORS = [ ("0", "colors.white"), ("1", "colors.orange"), ("2", "colors.magenta"), ("3", "colors.lightBlue"), ("4", "colors.yellow"), ("5", "colors.lime"), ("6", "colors.pink"), ("7", "colors.gray"), ("8", "colors.lightGray"), ("9", "colors.cyan"), ("a", "colors.purple"), ("b", "colors.blue"), ("c", "colors.brown"), ("d", "colors.green"), ("e", "colors.red"), ("f", "colors.black"), ] PIX_BITS = [ [ 1, 2 ], [ 4, 8 ], [ 16, 0 ] ] MAX_DIFF = (3 ** 0.5) * 255 def __init__(self, image: Image.Image, size: tuple[int, int]): self._img = image.copy() self._img.thumbnail(( size[0] * 2, size[1] * 3 )) self._img = self._img.convert("P", palette=Image.ADAPTIVE, colors=16) self._palette: list[int] = self._img.getpalette() # type: ignore def _brightness(self, i: int) -> float: r, g, b = self._palette[i * 3 : (i + 1) * 3] return (r + g + b) / 768 def _distance(self, a: int, b: int) -> float: r1, g1, b1 = self._palette[a * 3 : (a + 1) * 3] r2, g2, b2 = self._palette[b * 3 : (b + 1) * 3] return ((r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2) ** 0.5 / self.MAX_DIFF def _get_colors(self, x: int, y: int) -> tuple[int, int]: brightest_i, brightest_l = 0, 0 darkest_i, darkest_l = 0, 768 for oy, line in enumerate(self.PIX_BITS): for ox, bit in enumerate(line): pix = self._img.getpixel((x + ox, y + oy)) brightness = self._brightness(pix) if brightness > brightest_l: brightest_l, brightest_i = brightness, pix if brightness < darkest_l: darkest_l, darkest_i = brightness, pix return darkest_i, brightest_i def _is_darker(self, bg: int, fg: int, c: int) -> bool: return self._distance(bg, c) < self._distance(fg, c) def _get_block(self, x: int, y: int) -> tuple[int, int, int]: dark_i, bri_i = self._get_colors(x, y) out: int = 0 for oy, line in enumerate(self.PIX_BITS): for ox, bit in enumerate(line): if not self._is_darker(dark_i, bri_i, self._img.getpixel((x + ox, y + oy))): out |= bit # bottom right pixel fix? if self._is_darker(dark_i, bri_i, self._img.getpixel((x + 1, y + 2))): out ^= 31 dark_i, bri_i = bri_i, dark_i return out, dark_i, bri_i def export_binary(self, io: BinaryIO): io.write(b"CCPI") io.write(bytes([self._img.width // 2, self._img.height // 3, 0])) io.write(bytes(self._palette[: 16 * 3])) for y in range(0, self._img.height - 2, 3): for x in range(0, self._img.width - 1, 2): ch, bg, fg = self._get_block(x, y) io.write(bytes([ (ch + 0x80) & 0xFF, fg << 4 | bg ])) def export(self, io: TextIO): io.write("local m = peripheral.find('monitor')\n") io.write("m.setTextScale(0.5)\n") io.write(f"-- image: {self._img.width}x{self._img.height}\n") io.write("\n") io.write("-- configuring palette\n") for i in range(16): r, g, b = self._palette[i * 3 : (i + 1) * 3] io.write(f"m.setPaletteColor({self.CC_COLORS[i][1]}, 0x{r:02x}{g:02x}{b:02x})\n") io.write("\n") io.write("-- writing pixels\n") for i, y in enumerate(range(0, self._img.height - 2, 3), 1): s = [] bgs = "" fgs = "" io.write(f"m.setCursorPos(1, {i}); ") for x in range(0, self._img.width - 1, 2): ch, bg, fg = self._get_block(x, y) s.append(ch + 0x80) bgs += self.CC_COLORS[bg][0] fgs += self.CC_COLORS[fg][0] io.write("m.blit(string.char(%s), '%s', '%s')\n" % ( str.join(", ", map(str, s)), fgs, bgs )) def main(fp_in, fp_out): with Image.open(fp_in) as img: with open(fp_out, "wb") as fp: Converter(img, (164, 81)).export_binary(fp) if __name__ == "__main__": main(argv[1], argv[2])