Created
May 3, 2023 00:23
-
-
Save foone/ffc3c0625c12246d1e774989a75f298c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import struct, os, sys,array, glob, itertools, argparse | |
from PIL import Image | |
A8SIGNATURE =0xA0A1A2A3 | |
COMPRESS_NONE = 0 | |
COMPRESS_RLE = 1 | |
COMPRESSION_FORMATS=(COMPRESS_NONE, COMPRESS_RLE) | |
ESCAPE_CODE = 0 | |
ESC_ENDBITMAP = 1 | |
ESC_DELTA = 2 | |
ESC_RANDOM = 3 | |
PALETTE_LENGTH = 256*4 | |
def translate_through_palette(data, pal): | |
out=array.array('B') | |
for c in data: | |
out.extend(pal[ord(c)]) | |
return out | |
def unRLE8(src): | |
out = internalRLE8(src) | |
return ''.join(out) | |
def internalRLE8(src): | |
def readat(o, pattern): | |
size = struct.calcsize(pattern) | |
return struct.unpack('<'+pattern, src[o:o+size]) | |
out=[] | |
offset=0 | |
while True: | |
run,esc = readat(offset,'BB') | |
offset+=2 | |
if run==ESCAPE_CODE: | |
if esc == ESC_ENDBITMAP: | |
return out | |
elif esc == ESC_DELTA: | |
(delta_length,) = readat(offset, 'H') | |
offset+=2 | |
out.append('\0'*delta_length) | |
elif esc >= ESC_RANDOM: | |
direct_copy_length = (esc - (ESC_RANDOM-1)) | |
out.append(src[offset:offset+direct_copy_length]) | |
offset+=direct_copy_length | |
else: | |
out.append(chr(esc)*run) | |
def read_struct(f, pattern): | |
bytes_to_read = struct.calcsize(pattern) | |
loaded_buffer = f.read(bytes_to_read) | |
if len(loaded_buffer) != bytes_to_read: | |
#TODO: provide more context? | |
raise IOError('Hit EOF trying to read {} bytes!'.format(bytes_to_read)) | |
return struct.unpack('<'+pattern, loaded_buffer) | |
# This is not really useful but is nice for symmetry | |
def write_stuct(f, pattern, *args): | |
f.write(struct.pack(pattern, *args)) | |
def load_a8(f): | |
(signature,)=read_struct(f,'L') | |
if signature != A8SIGNATURE: | |
raise IOError('Invalid A8 header. Expected {:04x}, got {:04x}'.format(A8SIGNATURE, signature)) | |
w,h,bpp,compression,compressed_size = read_struct(f,'LLLLL') | |
if bpp != 8: | |
raise IOError('A8 file has {} bits per pixel, only 8 is supported!'.format(bpp)) | |
if compression not in COMPRESSION_FORMATS: | |
raise IOError('A8 file has {} compression, only NONE or RLE is supported!'.format(compression)) | |
# The code ignores the compressed_size, so we will too. | |
# palette is 256 RGBQUADS, so BGRA order. | |
bgra = read_struct(f,'{}B'.format(256*4)) | |
rgba_palette = [(r,g,b,a) for (b,g,r,a) in [bgra[i:i+4] for i in range(0, len(bgra), 4)]] | |
size = w*h | |
src = f.read(size) | |
decompressed_src = unRLE8(src) | |
rawdata = translate_through_palette(decompressed_src, rgba_palette) | |
im=Image.frombytes('RGBA',(w,h), rawdata) | |
return im.transpose(Image.FLIP_TOP_BOTTOM) | |
def convert_to_paletted(im): | |
color_iterator = itertools.count() | |
palettes={} | |
def lookup_color(rgba): | |
r,g,b,a = rgba | |
palkey = (b,g,r,a) | |
try: | |
return palettes[palkey] | |
except KeyError: | |
index = palettes[palkey]=next(color_iterator) | |
return index | |
px = im.load() | |
w,h= im.size | |
outdata=array.array('B') | |
for y in range(h-1, -1, -1): # THe image is encoded bottom to top, like BMP | |
for x in range(w): | |
i=lookup_color(px[x,y]) | |
outdata.append(i) | |
ordered_palette = [(index, key) for (key, index) in palettes.items()] | |
ordered_palette.sort() | |
palette_array = array.array('B') | |
for _,k in ordered_palette: | |
palette_array.extend(k) | |
if len(palette_array) > PALETTE_LENGTH: | |
raise ValueError('Image has too many colors! {} palette entries.'.format(len(palette_array)//4)) | |
elif len(palette_array) < PALETTE_LENGTH: | |
padding = PALETTE_LENGTH - len(palette_array) | |
palette_array.extend([0]*padding) | |
return palette_array, outdata | |
def save_to_a8(im, f): | |
w,h = im.size | |
write_stuct(f, 'LLLLLL', A8SIGNATURE, w, h, 8, 0, 0) | |
palette_array, image_data = convert_to_paletted(im.convert('RGBA')) # to support RGB formats | |
palette_array.tofile(f) | |
image_data.tofile(f) | |
def convert_to_png(a8_filename, png_filename): | |
with open(a8_filename,'rb') as f: | |
im=load_a8(f) | |
im.save(png_filename) | |
def convert_to_a8(png_filename, a8_filename): | |
im = Image.open(png_filename) | |
with open(a8_filename, 'wb') as f: | |
save_to_a8(im, f) | |
def new_ext(filename, ext): | |
base, _ = os.path.splitext(filename) | |
return '{}.{}'.format(base, ext) | |
if __name__=='__main__': | |
parser = argparse.ArgumentParser(description='Convert files to/from the Microsoft A8 format') | |
action_group = parser.add_mutually_exclusive_group(required=True) | |
action_group.add_argument('--to-png', action='store_true', help='Convert files to PNG') | |
action_group.add_argument('--to-a8', action='store_true', help='Convert files to A8') | |
parser.add_argument('files', metavar='FILE', type=str, nargs='*', help='files to convert') | |
parser.add_argument('--all', '-a', action='store_true', help='Convert all applicable files in the local directory') | |
args = parser.parse_args() | |
orig_ext, target_ext = ('a8','png') if args.to_png else ('png','a8') | |
convert_function = convert_to_png if args.to_png else convert_to_a8 | |
if args.all: | |
print 'Converting all .{} files in current directory'.format(orig_ext) | |
args.files = glob.glob('*.{}'.format(orig_ext)) | |
for basefile in args.files: | |
print 'Converting', basefile | |
convert_function(basefile, new_ext(basefile, target_ext)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment