diff --git a/cc-pic.py b/cc-pic.py index 83dd74f..85d175f 100644 --- a/cc-pic.py +++ b/cc-pic.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -# x-run: python3 % ~/downloads/kanade/6bf3cdae12b75326e3c23af73105e5781e42e94e.jpg n25.cpi from typing import BinaryIO, TextIO from PIL import Image -from sys import argv +from argparse import ArgumentParser, RawTextHelpFormatter +from textwrap import dedent + class Converter: CC_COLORS = [ @@ -25,17 +26,15 @@ class Converter: ("f", "colors.black"), ] - PIX_BITS = [ - [ 1, 2 ], - [ 4, 8 ], - [ 16, 0 ] - ] + PIX_BITS = [[1, 2], [4, 8], [16, 0]] - MAX_DIFF = (3 ** 0.5) * 255 + MAX_DIFF = (3**0.5) * 255 def __init__(self, image: Image.Image): self._img = image.convert("P", palette=Image.ADAPTIVE, colors=16) - self._palette: list[int] = self._img.getpalette() # type: ignore + self._palette: list[int] = self._img.getpalette() or [] + if len(self._palette) < 16 * 3: + self._palette += [0] * ((16 * 3) - len(self._palette)) def _brightness(self, i: int) -> float: r, g, b = self._palette[i * 3 : (i + 1) * 3] @@ -44,7 +43,9 @@ class Converter: 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 + 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 @@ -67,7 +68,9 @@ class Converter: 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))): + 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))): @@ -82,10 +85,7 @@ class Converter: 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 - ])) + io.write(bytes([(ch + 0x80) & 0xFF, fg << 4 | bg])) def export(self, io: TextIO): io.write("local m = peripheral.find('monitor')\n") @@ -95,7 +95,9 @@ class Converter: 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( + f"m.setPaletteColor({self.CC_COLORS[i][1]}, 0x{r:02x}{g:02x}{b:02x})\n" + ) io.write("\n") io.write("-- writing pixels\n") @@ -109,23 +111,136 @@ class Converter: 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 - )) + io.write( + "m.blit(string.char(%s), '%s', '%s')\n" + % (str.join(", ", map(str, s)), fgs, bgs) + ) -def main(fp_in, fp_out): - 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) +def main(): + parser = ArgumentParser( + description="ComputerCraft Palette Image converter", + formatter_class=RawTextHelpFormatter, + ) + parser.add_argument( + "-t", + dest="textmode", + action="store_true", + help="Output a Lua script instead of binary image", + ) + parser.add_argument( + "-W", + dest="width", + default=4 * 8 - 1, + type=int, + help="Width in characters", + ) + parser.add_argument( + "-H", + dest="height", + default=3 * 6 - 2, + type=int, + help="Height in characters", + ) + parser.add_argument( + "-p", + dest="placement", + choices=("center", "cover", "tile", "full", "extend", "fill"), + default="full", + help=dedent( + """\ + Image placement mode (same as in hsetroot) + -p center Render image centered on screen + -p cover Centered on screen, scaled to fill fully + -p tile Render image tiles + -p full Maximum aspect ratio + -p extend Same as "full" but filling borders + -p fill Stretch to fill""" + ), + ) + parser.add_argument("image_path") + parser.add_argument("output_path") + + args = parser.parse_args() + + with Image.new("RGB", (args.width * 2, args.height * 3)) as canv: + with Image.open(args.image_path).convert("RGB") as img: + if args.placement == "fill": + canv.paste(img.resize(canv.size), (0, 0)) + elif args.placement in ("full", "extend", "cover"): + aspect = canv.width / img.width + if (img.height * aspect > canv.height) != ( + args.placement == "cover" + ): + aspect = canv.height / img.height + new_w, new_h = int(img.width * aspect), int( + img.height * aspect + ) + top = int((canv.height - new_h) / 2) + left = int((canv.width - new_w) / 2) + resized_img = img.resize((new_w, new_h)) + canv.paste(resized_img, (left, top)) + if args.placement == "extend": + if left > 0: + right = left - 1 + new_w + w = 1 + while right + w < canv.width: + canv.paste( + canv.crop( + (left + 1 - w, 0, left + 1, canv.height) + ), + (left + 1 - w * 2, 0), + ) + canv.paste( + canv.crop((right, 0, right + w, canv.height)), + (right + w, 0), + ) + w *= 2 + if top > 0: + bottom = top - 1 + new_h + h = 1 + while bottom + h < canv.height: + canv.paste( + canv.crop( + (0, top + 1 - h, canv.width, top + 1) + ), + (top + 1 - h * 2, 0), + ) + canv.paste( + canv.crop((0, bottom, canv.width, bottom + h)), + (0, bottom + h), + ) + h *= 2 + + elif args.placement in ("center", "tile"): + left = int((canv.width - img.width) / 2) + top = int((canv.height - img.height) / 2) + if args.placement == "tile": + while left > 0: + left -= img.width + while top > 0: + top -= img.height + x = left + while x < canv.width: + y = top + while y < canv.height: + canv.paste(img, (x, y)) + y += img.height + x += img.width + else: + canv.paste(img, (left, top)) + else: + pass + + converter = Converter(canv) + converter._img.save("/tmp/_ccpictmp.png") + if args.textmode: + with open(args.output_path, "w") as fp: + converter.export(fp) + else: + with open(args.output_path, "wb") as fp: + converter.export_binary(fp) + if __name__ == "__main__": - main(argv[1], argv[2]) - + main()