New and improved converter

This commit is contained in:
Casey 2024-01-18 15:08:13 +03:00
parent bd9d88e12c
commit c917da78cb
Signed by: hkc
GPG Key ID: F0F6CFE11CDB0960
1 changed files with 147 additions and 32 deletions

179
cc-pic.py
View File

@ -1,9 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# x-run: python3 % ~/downloads/kanade/6bf3cdae12b75326e3c23af73105e5781e42e94e.jpg n25.cpi
from typing import BinaryIO, TextIO from typing import BinaryIO, TextIO
from PIL import Image from PIL import Image
from sys import argv from argparse import ArgumentParser, RawTextHelpFormatter
from textwrap import dedent
class Converter: class Converter:
CC_COLORS = [ CC_COLORS = [
@ -25,17 +26,15 @@ class Converter:
("f", "colors.black"), ("f", "colors.black"),
] ]
PIX_BITS = [ PIX_BITS = [[1, 2], [4, 8], [16, 0]]
[ 1, 2 ],
[ 4, 8 ],
[ 16, 0 ]
]
MAX_DIFF = (3 ** 0.5) * 255 MAX_DIFF = (3**0.5) * 255
def __init__(self, image: Image.Image): def __init__(self, image: Image.Image):
self._img = image.convert("P", palette=Image.ADAPTIVE, colors=16) 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: def _brightness(self, i: int) -> float:
r, g, b = self._palette[i * 3 : (i + 1) * 3] r, g, b = self._palette[i * 3 : (i + 1) * 3]
@ -44,7 +43,9 @@ class Converter:
def _distance(self, a: int, b: int) -> float: def _distance(self, a: int, b: int) -> float:
r1, g1, b1 = self._palette[a * 3 : (a + 1) * 3] r1, g1, b1 = self._palette[a * 3 : (a + 1) * 3]
r2, g2, b2 = self._palette[b * 3 : (b + 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]: def _get_colors(self, x: int, y: int) -> tuple[int, int]:
brightest_i, brightest_l = 0, 0 brightest_i, brightest_l = 0, 0
@ -67,7 +68,9 @@ class Converter:
out: int = 0 out: int = 0
for oy, line in enumerate(self.PIX_BITS): for oy, line in enumerate(self.PIX_BITS):
for ox, bit in enumerate(line): 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 out |= bit
# bottom right pixel fix? # bottom right pixel fix?
if self._is_darker(dark_i, bri_i, self._img.getpixel((x + 1, y + 2))): 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 y in range(0, self._img.height - 2, 3):
for x in range(0, self._img.width - 1, 2): for x in range(0, self._img.width - 1, 2):
ch, bg, fg = self._get_block(x, y) ch, bg, fg = self._get_block(x, y)
io.write(bytes([ io.write(bytes([(ch + 0x80) & 0xFF, fg << 4 | bg]))
(ch + 0x80) & 0xFF,
fg << 4 | bg
]))
def export(self, io: TextIO): def export(self, io: TextIO):
io.write("local m = peripheral.find('monitor')\n") io.write("local m = peripheral.find('monitor')\n")
@ -95,7 +95,9 @@ class Converter:
io.write("-- configuring palette\n") io.write("-- configuring palette\n")
for i in range(16): for i in range(16):
r, g, b = self._palette[i * 3 : (i + 1) * 3] 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("\n")
io.write("-- writing pixels\n") io.write("-- writing pixels\n")
@ -109,23 +111,136 @@ class Converter:
s.append(ch + 0x80) s.append(ch + 0x80)
bgs += self.CC_COLORS[bg][0] bgs += self.CC_COLORS[bg][0]
fgs += self.CC_COLORS[fg][0] fgs += self.CC_COLORS[fg][0]
io.write("m.blit(string.char(%s), '%s', '%s')\n" % ( io.write(
str.join(", ", map(str, s)), "m.blit(string.char(%s), '%s', '%s')\n"
fgs, % (str.join(", ", map(str, s)), fgs, bgs)
bgs )
))
def main(fp_in, fp_out): def main():
size = 143 * 2, 81 * 3 # 286, 243 parser = ArgumentParser(
with Image.new("RGB", size) as canv: description="ComputerCraft Palette Image converter",
with Image.open(fp_in) as img: formatter_class=RawTextHelpFormatter,
scale = max(img.width / size[0], img.height / size[0]) )
img = img.resize((int(img.width / scale), int(img.height / scale))) parser.add_argument(
canv.paste(img, ((canv.width - img.width) // 2, (canv.height - img.height) // 2)) "-t",
with open(fp_out, "wb") as fp: dest="textmode",
Converter(canv).export_binary(fp) 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__": if __name__ == "__main__":
main(argv[1], argv[2]) main()