Commit ab56b8ad authored by Robey Pointer's avatar Robey Pointer
Browse files

rename folder to assets

parent 1c0491d5
import argparse
import os.path
import re
import struct
import sys
from typing import List, Optional, Tuple, Union
__version__ = "1.0-20210817"
BLACK = 0
CUTOFF = 63 # (of 255) where we decide a pixel is "on" instead of "off"
BMP_HEADER = bytes([ 0x42, 0x4d ])
HEADER_SIZE = 14
class Framebuffer:
width: int
height: int
color_depth: int
pixels: List[int]
def __init__(self, width: int, height: int, color_depth: int):
self.width = width
self.height = height
self.color_depth = color_depth
self.pixels = [0 for i in range(width * height)]
def __repr__(self):
return f"Framebuffer(width={self.width}, height={self.height}, depth={self.color_depth})"
def set_pixel(self, x: int, y: int, color: int):
self.pixels[y * self.width + x] = color
def get_pixel(self, x: int, y: int) -> int:
return self.pixels[y * self.width + x]
def get_pixel_as_gray(self, x: int, y: int) -> int:
# 0.21 R + 0.72 G + 0.07 B
pixel = self.get_pixel(x, y)
return round(0.21 * ((pixel >> 16) & 0xff) + 0.72 * ((pixel >> 8) & 0xff) + 0.07 * (pixel & 0xff))
# remove the alpha channel by rendering each pixel against a given
# background color.
def render_alpha(self, background_color: int):
for y in range(self.height):
for x in range(self.width):
pixel = self.get_pixel(x, y)
blend = ((pixel >> 24) & 0xff) / 255
color = pixel & 0xffffff
def mix(shift: int) -> int:
pc = (color >> shift) & 0xff
bc = (background_color >> shift) & 0xff
return (int(pc * blend + bc * (1.0 - blend)) & 0xff) << shift
self.set_pixel(x, y, 0xff000000 | mix(16) | mix(8) | mix(0))
# janky bmp decoder
def read_bmp(data: bytes) -> Framebuffer:
if data[:2] != BMP_HEADER:
raise Exception("Not a BMP file")
data_offset = struct.unpack("<I", data[10:14])[0]
pixel_width, pixel_height, _ignore, color_depth = (
struct.unpack("<iiHH", data[HEADER_SIZE + 4 : HEADER_SIZE + 16])
)
if color_depth != 32 and color_depth != 24:
raise Exception("I'm out of my depth")
top_to_bottom = False
if pixel_height < 0:
top_to_bottom = True
pixel_height = -pixel_height
fb = Framebuffer(pixel_width, pixel_height, color_depth)
offset = data_offset
for y in range(0, pixel_height):
row_offset = offset
py = y if top_to_bottom else pixel_height - y - 1
for x in range(0, pixel_width):
if color_depth == 32:
fb.set_pixel(x, py, struct.unpack("<I", data[offset : offset + 4])[0])
offset += 4
elif color_depth == 24:
# assume alpha = 255
lo, hi = struct.unpack("<HB", data[offset : offset + 3])
fb.set_pixel(x, py, hi | (lo << 16) | 0xff000000)
offset += 3
# align
row_len = ((offset - row_offset + 3) >> 2) << 2
offset = row_offset + row_len
return fb
def to_ascii(fb: Framebuffer) -> str:
rv = ""
for y in range(32):
for x in range(128):
rv += ("#" if fb.get_pixel_as_gray(x, y) > 63 else " ")
rv += "\n"
return rv
# vertical stacks of 8 pixels, LSB on top
def to_oled_bytes(fb: Framebuffer, invert: bool = False) -> bytes:
rv: List[int] = []
for row in range(0, 4):
for x in range(0, 128):
byte = 0
for y in range(0, 8):
pixel = 1 if fb.get_pixel_as_gray(x, row * 8 + y) > CUTOFF else 0
byte |= (pixel << y)
rv.append(255 - byte if invert else byte)
return bytes(rv)
# vertical stacks to framebuffer
def to_framebuffer(data: bytes) -> Framebuffer:
fb = Framebuffer(128, 32, 32)
for row in range(0, 4):
for x in range(0, 128):
for y in range(0, 8):
pixel = 0xffffffff if data[row * 128 + x] & (1 << y) else 0
fb.set_pixel(x, row * 8 + y, pixel)
return fb
def compress(data: bytes) -> bytes:
# list of (byte, count):
runs: List[Tuple[int, int]] = []
run = (data[0], 1)
for b in data[1:]:
if run[0] == b:
run = (run[0], run[1] + 1)
else:
runs.append(run)
run = (b, 1)
runs.append(run)
# turn into a stream of either "repeat" (byte, count) or bytes (uncompressed)
stream: List[Union[bytes, Tuple[int, int]]] = []
current: bytearray = bytearray()
for (byte, count) in runs:
if count > 3:
if len(current) > 0:
stream.append(current)
current = bytearray()
stream.append((byte, count))
else:
for i in range(count):
current.append(byte)
if len(current) > 0:
stream.append(current)
# simple encoding: tnnnnnnn
# t = 1: repeat next byte N times
# t = 0: next N bytes are uncompressed
rv = bytearray()
for item in stream:
if type(item) is bytearray:
while len(item) > 127:
rv += bytes([ 127 ]) + item[:127]
item = item[127:]
rv += bytes([ len(item) ]) + item
else:
byte, count = item
while count > 127:
rv += bytes([ 255, byte ])
count -= 127
rv += bytes([ 0x80 | count, byte ])
rv += bytes([ 0 ])
return rv
def uncompress(data: bytes) -> bytes:
rv = bytearray()
i = 0
while i < len(data):
n = data[i] & 0x7f
if data[i] & 0x80:
rv += bytes([ data[i + 1] ] * n)
i += 2
else:
rv += data[i + 1 : i + n + 1]
i += n + 1
return rv
def to_c_code(name: str, data: bytes) -> str:
lines: List[str] = [
"// generated by packbmp.py: RLE format bitmap",
f"const uint8_t {name}[] PROGMEM = " + "{",
]
for i in range(0, len(data), 12):
row = data[i : i + 12]
lines.append(" " + "".join(f"0x{n:02x}, " for n in row))
lines.append("};")
return "\n".join(lines) + "\n"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description = f"packbmp ({__version__}): generates packed binary data from a BMP image"
)
parser.add_argument("-v", "--version", action = "version", version = __version__)
parser.add_argument("--invert", action = "store_true", help = "invert the pixels before converting")
parser.add_argument("--check", action = "store_true", help = "decode the packed image and display as ASCII, as a double-check")
parser.add_argument("filename", nargs = "+", help = "BMP file(s) to convert")
return parser.parse_intermixed_args()
def main():
args = parse_args()
for filename in args.filename:
with open(filename, "rb") as fp:
data = fp.read()
fb = read_bmp(data)
fb.render_alpha(BLACK)
if fb.width != 128 or fb.height != 32:
print(f"The image must be exactly 128x32. This one is {fb.width}x{fb.height}: {filename}")
sys.exit(1)
packed = compress(to_oled_bytes(fb, args.invert))
name = re.compile("[^A-Za-z0-9]").sub("_", os.path.basename(os.path.splitext(filename)[0]))
print(f"Compressed {filename} to {len(packed)} bytes:")
print("")
print(to_c_code(name + "_bitmap", packed))
if args.check:
print("")
print(to_ascii(to_framebuffer(uncompress(packed))))
if __name__ == "__main__":
main()
#!/bin/sh
poetry run python ./packbmp.py "$@"
......@@ -10,8 +10,8 @@
#include "ssd1306.h"
#include "display.h"
#include "fonts/oranj_reform.h"
#include "fonts/bizcat_reform.h"
#include "assets/oranj_reform.h"
#include "assets/bizcat_reform.h"
const monospace_font_t font_oranj = { oranj_reform_font_data, 8, 6 };
const monospace_font_t font_bizcat = { bizcat_reform_font_data, 16, 8 };
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment