Updated Answer
I have made some changes to my original code so that:
- you can pass a filename to read as parameter
- it parses the header and checks the magic string and derives the format (RGBA8888 or RGBA4444) and height and width automatically
- it now handles RGBA8888 like your newly-shared sample image
So, it looks like this:
#!/usr/bin/env python3
import struct
import sys
import numpy as np
from PIL import Image
def loadWaltex(filename):
# Open input file in binary mode
with open(filename, 'rb') as fd:
# Read 16 byte header and extract metadata
# https://zenhax.com/viewtopic.php?t=14164
header = fd.read(16)
magic, vers, fmt, w, h, _ = struct.unpack('4sBBHH6s', header)
if magic != b'WALT':
sys.exit(f'ERROR: {filename} does not start with "WALT" magic string')
# Check if fmt=0 (RGBA8888) or fmt=3 (RGBA4444)
if fmt == 0:
fmtdesc = "RGBA8888"
# Read remainder of file (part following header)
data = np.fromfile(fd, dtype=np.uint8)
R = data[0::4].reshape((h,w))
G = data[1::4].reshape((h,w))
B = data[2::4].reshape((h,w))
A = data[3::4].reshape((h,w))
# Stack the channels to make RGBA image
RGBA = np.dstack((R,G,B,A))
else:
fmtdesc = "RGBA4444"
# Read remainder of file (part following header)
data = np.fromfile(fd, dtype=np.uint16).reshape((h,w))
# Split the RGBA444 out from the uint16
R = (data>>12) & 0xf
G = (data>>8) & 0xf
B = (data>>4) & 0xf
A = data & 0xf
# Stack the channels to make RGBA image
RGBA = np.dstack((R,G,B,A)).astype(np.uint8) << 4
# Debug info for user
print(f'Filename: {filename}, version: {vers}, format: {fmtdesc} ({fmt}), w: {w}, h: {h}')
# Make into PIL Image
im = Image.fromarray(RGBA)
return im
if __name__ == "__main__":
# Load image specified by first parameter
im = loadWaltex(sys.argv[1])
im.save('result.png')
And when you run it with:
./decodeRGBA.py objects.waltex
You get:

The debug output for your two sample images is:
Filename: Carl.waltex, version: 1, format: RGBA4444 (3), w: 1024, h: 1024
Filename: objects.waltex, version: 1, format: RGBA8888 (0), w: 256, h: 1024
Original Answer
I find using Numpy is the easiest approach for this type of thing, and it is also highly performant:
#!/usr/bin/env python3
import numpy as np
from PIL import Image
# Define the known parameters of the image and read into Numpy array
h, w, offset = 1024, 1024, 16
data = np.fromfile('Carl.waltex', dtype=np.uint16, offset=offset).reshape((h,w))
# Split the RGBA4444 out from the uint16
R = (data >> 12) & 0xf
G = (data >> 8) & 0xf
B = (data >> 4) & 0xf
A = data & 0xf
# Stack the 4 individual channels to make an RGBA image
RGBA = np.dstack((R,G,B,A)).astype(np.uint8) << 4
# Make into PIL Image
im = Image.fromarray(RGBA)
im.save('result.png')

Note: Your image has 16 bytes of padding at the start. Sometimes that amount is variable. A useful technique in that case is to read the entire file, work out how many useful samples of pixel data there are (in your case 1024*1024), and then slice the data to take the last N samples - thereby ignoring any variable padding at the start. That would look like this:
# Define the known parameters of the image and read into Numpy array
h, w = 1024, 1024
data = np.fromfile('Carl.waltex', dtype=np.uint16)[-h*w:].reshape((h,w))
If you don't like Numpy and prefer messing about with lists and structs, you can get exactly the same result like this:
#!/usr/bin/env python3
import struct
from PIL import Image
# Define the known parameters of the image
h, w, offset = 1024, 1024, 16
data = open('Carl.waltex', 'rb').read()[offset:]
# Unpack into bunch of h*w unsigned shorts
uint16s = struct.unpack("H" * h *w, data)
# Build a list of RGBA tuples
pixels = []
for RGBA4444 in uint16s:
R = (RGBA4444 >> 8) & 0xf0
G = (RGBA4444 >> 4) & 0xf0
B = RGBA4444 & 0xf0
A = ( RGBA4444 & 0xf) << 4
pixels.append((R,G,B,A))
# Push the list of RGBA tuples into an empty image
RGBA = Image.new('RGBA', (w,h))
RGBA.putdata(pixels)
RGBA.save('result.png')
Note that the Numpy approach is 60x faster than the list-based approach:
Numpy: 3.6 ms ± 73.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
listy: 213 ms ± 712 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
Note: These images and the waltex file format seem to be from the games "Where's My Water?" and "Where's My Perry?". I got some hints as to the header format from ZENHAX.