Sanad_Package_3/vendor/mask/image2mask.py

112 lines
4.2 KiB
Python

#!/usr/bin/env python3
"""Convert ANY image (or animated GIF/WEBP) and display it on the mask.
Still image -> fit to the 46x58 oval display and show it.
Animated GIF -> upload its frames as DIY images and PLAY-loop them.
Usage:
python image2mask.py photo.jpg
python image2mask.py logo.png --fit cover --oval
python image2mask.py dance.gif --max-frames 12 --fps 8 --loops 5
python image2mask.py photo.jpg --save out.bin # just save raw bytes, no mask
python image2mask.py photo.jpg --preview # print ASCII preview, no mask
Options:
--fit contain|cover|stretch how to fit the image (default contain)
--bg RRGGBB letterbox/oval background color (default 000000)
--oval black out corners to match the oval panel
--brightness N 0..255 (default 95)
"""
import argparse
import asyncio
import sys
import colorface
from mask import ShiningMask
from exceptions import MaskNotFound
def _hex(s):
s = s.lstrip("#")
return (int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16))
def _ascii(image):
img = image.convert("L")
ramp = " .:-=+*#%@"
out = []
for y in range(0, colorface.DISPLAY_H, 2):
row = "".join(ramp[min(len(ramp) - 1, img.getpixel((x, y)) * len(ramp) // 256)]
for x in range(colorface.DISPLAY_W))
out.append(row)
return "\n".join(out)
async def run(args):
frames = colorface.load_frames(
args.image, max_frames=args.max_frames, fit=args.fit,
bg=_hex(args.bg), oval=args.oval,
)
print(f"loaded {len(frames)} frame(s) from {args.image} (fit={args.fit}, oval={args.oval})")
if args.preview:
print(_ascii(frames[0]))
return
if args.save:
with open(args.save, "wb") as fh:
fh.write(colorface.encode(frames[0]))
print(f"saved raw frame ({colorface.DISPLAY_W}x{colorface.DISPLAY_H} -> 8004 B) to {args.save}")
return
mask = ShiningMask(address=args.address, name_prefix=args.name_prefix)
print("connecting ...")
try:
await mask.connect(timeout=20.0, attempts=8)
except MaskNotFound as exc:
print(f"could not find the mask: {exc}")
return
try:
await mask.set_brightness(args.brightness)
if len(frames) == 1:
await mask.upload_raw_image(colorface.encode(frames[0]), index=1)
print("image shown on the mask.")
else:
print(f"uploading {len(frames)} frames (one-time, ~{len(frames) * 4}s) ...")
await mask.clear_diy()
for i, fr in enumerate(frames, start=1):
await mask.upload_frame(colorface.encode(fr), i)
print(f" frame {i}/{len(frames)}", flush=True)
print(f"animating {args.loops} loop(s) at {args.fps} fps (Ctrl+C to stop) ...")
try:
for _ in range(args.loops):
for i in range(1, len(frames) + 1):
await mask.play_frame(i)
await asyncio.sleep(1.0 / args.fps)
except KeyboardInterrupt:
pass
finally:
await mask.disconnect()
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("image", help="path to any image or animated GIF/WEBP")
ap.add_argument("--fit", choices=["contain", "cover", "stretch"], default="contain")
ap.add_argument("--bg", default="000000", help="background RRGGBB")
ap.add_argument("--oval", action="store_true", help="black out corners (oval shape)")
ap.add_argument("--max-frames", type=int, default=16)
ap.add_argument("--fps", type=float, default=8.0)
ap.add_argument("--loops", type=int, default=10)
ap.add_argument("--brightness", type=int, default=95)
ap.add_argument("--save", help="save the raw 8004-byte frame to a file instead of uploading")
ap.add_argument("--preview", action="store_true", help="print an ASCII preview instead of uploading")
ap.add_argument("--address", help="connect to a specific BLE MAC")
ap.add_argument("--name-prefix", default="MASK")
asyncio.run(run(ap.parse_args()))
if __name__ == "__main__":
main()