2023-10-15 03:13:42 +03:00
|
|
|
#!/usr/bin/env python3
|
2023-10-17 14:40:09 +03:00
|
|
|
# x-run: python3 % ~/downloads/kanade/6bf3cdae12b75326e3c23af73105e5781e42e94e.jpg n25.cpi
|
2023-10-15 03:13:42 +03:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2023-10-17 14:40:09 +03:00
|
|
|
def __init__(self, image: Image.Image):
|
|
|
|
self._img = image.convert("P", palette=Image.ADAPTIVE, colors=16)
|
2023-10-15 03:13:42 +03:00
|
|
|
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
|
2023-10-15 03:32:37 +03:00
|
|
|
# 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
|
2023-10-15 03:13:42 +03:00
|
|
|
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]))
|
2023-10-15 03:29:21 +03:00
|
|
|
io.write(bytes(self._palette[: 16 * 3]))
|
2023-10-15 03:13:42 +03:00
|
|
|
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([
|
2023-10-15 03:29:21 +03:00
|
|
|
(ch + 0x80) & 0xFF,
|
2023-10-15 03:13:42 +03:00
|
|
|
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):
|
2023-10-17 14:40:09 +03:00
|
|
|
size = 143 * 2, 81 * 3 # 286, 243
|
|
|
|
with Image.new("RGB", size) as canv:
|
|
|
|
with Image.open(fp_in) as img:
|
|
|
|
scale = max(img.width / size[0], img.height / size[0])
|
|
|
|
img = img.resize((int(img.width / scale), int(img.height / scale)))
|
|
|
|
canv.paste(img, ((canv.width - img.width) // 2, (canv.height - img.height) // 2))
|
|
|
|
with open(fp_out, "wb") as fp:
|
|
|
|
Converter(canv).export_binary(fp)
|
2023-10-15 03:13:42 +03:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main(argv[1], argv[2])
|
|
|
|
|