Files
PythonInUnreal/Content/Python/png_reader.py
T
2026-09-05 13:33:04 +02:00

87 lines
3.2 KiB
Python

"""Minimal pure-Python PNG decoder (stdlib only: zlib + struct).
Supports 8-bit, non-interlaced grayscale / grayscale+alpha / RGB / RGBA PNGs -
which covers ordinary exported heightmap/relief images. Deliberately does not
depend on Pillow or any other package, since Unreal's embedded Python has no
pip packages installed by default.
"""
import struct, zlib
_CHANNELS_BY_COLOR_TYPE = {0: 1, 2: 3, 4: 2, 6: 4} # 3 (palette) is unsupported
def _paeth(a, b, c):
p = a + b - c
pa, pb, pc = abs(p - a), abs(p - b), abs(p - c)
if pa <= pb and pa <= pc:
return a
if pb <= pc:
return b
return c
def decode_png(path):
"""Returns (width, height, channels, pixels) where pixels is a flat
bytearray of width*height*channels bytes, row-major, top row first."""
with open(path, "rb") as f:
data = f.read()
if data[:8] != b"\x89PNG\r\n\x1a\n":
raise ValueError(f"Not a PNG file: {path}")
width = height = bit_depth = color_type = interlace = None
idat = bytearray()
pos = 8
while pos < len(data):
length = struct.unpack(">I", data[pos:pos + 4])[0]
ctype = data[pos + 4:pos + 8]
chunk = data[pos + 8:pos + 8 + length]
if ctype == b"IHDR":
width, height, bit_depth, color_type, _comp, _filt, interlace = struct.unpack(">IIBBBBB", chunk)
elif ctype == b"IDAT":
idat.extend(chunk)
elif ctype == b"IEND":
break
pos += 8 + length + 4 # length + type + data + crc
if bit_depth != 8:
raise ValueError(f"Only 8-bit PNGs are supported (got bit depth {bit_depth}) - re-export as 8-bit: {path}")
if interlace != 0:
raise ValueError(f"Interlaced PNGs are not supported - re-export as non-interlaced: {path}")
if color_type not in _CHANNELS_BY_COLOR_TYPE:
raise ValueError(f"Unsupported PNG color type {color_type} (palette?) - re-export as grayscale/RGB/RGBA: {path}")
channels = _CHANNELS_BY_COLOR_TYPE[color_type]
raw = zlib.decompress(bytes(idat))
stride = width * channels
pixels = bytearray(width * height * channels)
prev_row = bytearray(stride)
row_start = 0
for y in range(height):
filter_type = raw[row_start]
row = bytearray(raw[row_start + 1: row_start + 1 + stride])
row_start += 1 + stride
if filter_type == 1: # Sub
for i in range(channels, stride):
row[i] = (row[i] + row[i - channels]) & 0xFF
elif filter_type == 2: # Up
for i in range(stride):
row[i] = (row[i] + prev_row[i]) & 0xFF
elif filter_type == 3: # Average
for i in range(stride):
a = row[i - channels] if i >= channels else 0
row[i] = (row[i] + ((a + prev_row[i]) >> 1)) & 0xFF
elif filter_type == 4: # Paeth
for i in range(stride):
a = row[i - channels] if i >= channels else 0
b = prev_row[i]
c = prev_row[i - channels] if i >= channels else 0
row[i] = (row[i] + _paeth(a, b, c)) & 0xFF
# filter_type == 0 (None): no-op
pixels[y * stride:(y + 1) * stride] = row
prev_row = row
return width, height, channels, pixels