From e2234112ff8dc320c702819f916335961fdeb1e9 Mon Sep 17 00:00:00 2001 From: Daniel Thompson Date: Mon, 6 Apr 2020 21:37:37 +0100 Subject: [PATCH] tools: rle_encode: Add a new "2-bit" encoding mode. --- tools/rle_encode.py | 99 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 94 insertions(+), 5 deletions(-) diff --git a/tools/rle_encode.py b/tools/rle_encode.py index 60b2bdc..64b920e 100755 --- a/tools/rle_encode.py +++ b/tools/rle_encode.py @@ -45,7 +45,76 @@ def encode(im): return (im.width, im.height, bytes(rle)) +def encode_2bit(im): + """2-bit palette based RLE encoder. + + This encoder has a reprogrammable 2-bit palette. This allows it to encode + arbitrary images with a full 8-bit depth but the 2-byte overhead each time + a new colour is introduced means it is not efficient unless the image is + carefully constructed to keep a good locality of reference for the three + non-background colours. + + The encoding competes well with the 1-bit encoder for small monochrome + images but once run-lengths longer than 62 start to become frequent then + this encoding is about 30% larger than a 1-bit encoding. + """ + pixels = im.load() + + rle = [] + rl = 0 + px = pixels[0, 0] + palette = [0, 0xfc, 0x2d, 0xff] + next_color = 1 + + def encode_pixel(px, rl): + nonlocal next_color + px = (px[0] & 0xe0) | ((px[1] & 0xe0) >> 3) | ((px[2] & 0xc0) >> 6) + if px not in palette: + rle.append(next_color << 6) + rle.append(px) + palette[next_color] = px + next_color += 1 + if next_color >= len(palette): + next_color = 1 + px = palette.index(px) + if rl >= 63: + rle.append((px << 6) + 63) + rl -= 63 + while rl >= 255: + rle.append(255) + rl -= 255 + rle.append(rl) + else: + rle.append((px << 6) + rl) + + for y in range(im.height): + for x in range(im.width): + newpx = pixels[x, y] + if newpx == px: + rl += 1 + assert(rl < (1 << 21)) + continue + + # Code the previous run + encode_pixel(px, rl) + + # Start a new run + rl = 1 + px = newpx + + # Handle the final run + encode_pixel(px, rl) + + return (im.width, im.height, bytes(rle)) + def encode_8bit(im): + """Experimental 8-bit RLE encoder. + + For monochrome images this is about 3x less efficient than the 1-bit + encoder. This encoder is not currently used anywhere in wasp-os and + currently there is no decoder either (so don't assume this code + actually works). + """ pixels = im.load() rle = [] @@ -53,7 +122,6 @@ def encode_8bit(im): px = pixels[0, 0] def encode_pixel(px, rl): - print(rl) px = (px[0] & 0xe0) | ((px[1] & 0xe0) >> 3) | ((px[2] & 0xc0) >> 6) rle.append(px) @@ -138,17 +206,38 @@ parser.add_argument('--ascii', action='store_true', help='Run the resulting image(s) through an ascii art decoder') parser.add_argument('--c', action='store_true', help='Render the output as C instead of python') +parser.add_argument('--indent', default=0, type=int, + help='Add extra indentation in the generated code') +parser.add_argument('--2bit', action='store_true', dest='twobit', + help='Generate 2-bit image') +parser.add_argument('--8bit', action='store_true', dest='eightbit', + help='Generate 8-bit image') args = parser.parse_args() for fname in args.files: - image = encode(Image.open(fname)) + if args.eightbit: + image = encode_8bit(Image.open(fname)) + depth = 8 + elif args.twobit: + image = encode_2bit(Image.open(fname)) + depth = 2 + else: + image = encode(Image.open(fname)) + depth = 1 if args.c: render_c(image, fname) else: - print(f'# 1-bit RLE, generated from {fname}, {len(image[2])} bytes') - print(f'{varname(fname)} = {image}') - print() + print(f'# {depth}-bit RLE, generated from {fname}, {len(image[2])} bytes') + # Split the bytestring to ensure each line is short enough to be absorbed + # on the target if needed. + #print(f'{varname(fname)} = {image}') + (x, y, pixels) = image + extra_indent = ' ' * args.indent + print(f'{extra_indent}{varname(fname)} = (\n{extra_indent} {x}, {y},') + for i in range(0, len(pixels), 16): + print(f'{extra_indent} {pixels[i:i+16]}') + print(f'{extra_indent})') if args.ascii: print()