From 0c79a8017cab626106d7e7448cf523781ae1ee1f Mon Sep 17 00:00:00 2001 From: hkc Date: Sun, 15 Oct 2023 03:13:42 +0300 Subject: [PATCH] ComputerCraft Paletted Image thingie --- cc-pic.py | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ ccpi.lua | 73 ++++++++++++++++++++++++++++++++ rat.cpi | Bin 0 -> 20783 bytes 3 files changed, 197 insertions(+) create mode 100644 cc-pic.py create mode 100644 ccpi.lua create mode 100644 rat.cpi 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 0000000000000000000000000000000000000000..8ffb53bd09b9877d98fb189978d6e200cfe87d78 GIT binary patch literal 20783 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