cc-stuff/cc-pic.py

129 lines
4.5 KiB
Python

#!/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])