diff --git a/cc-pic.py b/cc-pic.py new file mode 100644 index 0000000..f5eba3a --- /dev/null +++ b/cc-pic.py @@ -0,0 +1,124 @@ +#!/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 + 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)) + 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 & 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]) + diff --git a/ccpi.lua b/ccpi.lua new file mode 100644 index 0000000..701b1f3 --- /dev/null +++ b/ccpi.lua @@ -0,0 +1,73 @@ + +local colors_list = { + colors.white, + colors.orange, + colors.magenta, + colors.lightBlue, + colors.yellow, + colors.lime, + colors.pink, + colors.gray, + colors.lightGray, + colors.cyan, + colors.purple, + colors.blue, + colors.brown, + colors.green, + colors.red, + colors.black +} + +local function load(path) + local image = { w = 0, h = 0, scale = 1.0, palette = {}, lines = {} } + + local fp, err = io.open(path, "rb") + if not fp then return nil, err end + + local magic = fp:read(4) + if magic ~= "CCPI" then + return nil, "Invalid header: expected CCPI got " .. magic + end + + image.w, image.h = string.byte(fp:read(1)), string.byte(fp:read(1)) + for i = 1, 16 do + image.palette[i] = bit32.lshift(string.byte(fp:read(1)), 16) + image.palette[i] = bit32.bor(image.palette[i], bit32.lshift(string.byte(fp:read(1)), 8)) + image.palette[i] = bit32.bor(image.palette[i], string.byte(fp:read(1))) + end + + for y = 1, image.h do + local line = { s = "", bg = "", fg = "" } + for x = 1, image.w do + line.s = line.s .. fp:read(1) + local color = string.byte(fp:read(1)) + line.bg = line.bg .. string.format("%x", bit32.band(0xF, color)) + line.bg = line.bg .. string.format("%x", bit32.band(0xF, bit32.rshift(color, 4))) + end + table.insert(image.lines, line) + end + + fp:close() + + return image +end + +local function draw(img, ox, oy, monitor) + local t = monitor or term.current() + ox = ox or 1 + oy = oy or 1 + + for i = 1, 16 do + t.setPaletteColor(colors_list[i], img.palette[i]) + end + + for y = 1, img.h do + t.setCursorPos(ox, oy + y - 1) + t.blit(img.lines[y].s, img.lines[y].fg, img.lines[y].bg) + end +end + +return { + load = load, + draw = draw +} diff --git a/rat.cpi b/rat.cpi new file mode 100644 index 0000000..8ffb53b Binary files /dev/null and b/rat.cpi differ