Skip to content

Instantly share code, notes, and snippets.

@FelixWolf
Created January 20, 2024 22:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save FelixWolf/d83059a287180fa16a26b1f1f5d68b6d to your computer and use it in GitHub Desktop.
Save FelixWolf/d83059a287180fa16a26b1f1f5d68b6d to your computer and use it in GitHub Desktop.
PCX decoder (Public domain / zlib). Mostly works.
#!/usr/bin/env python3
from PIL import Image
import math
import struct
import io
sPCXHeader = struct.Struct("<B BBB HHHH HH 48s x BHH HH 54x")
class PCX:
VERSION_FIXED_EGA = 0
VERSION_MODIFIABLE_EGA = 2
VERSION_NO_PALETTE = 3
VERSION_WINDOWS = 4
VERSION_24_BIT = 5
def __init__(self, version = 5, compression = 1, bitsPerColor = 8,
resolution = None, bounds = None, DPI = None, EGAPalette = None,\
depth = 3, paletteMode = 1, sourceResolution = None):
self.version = version
self.compression = compression
if resolution and bounds:
raise NameError("Cannot specify both resolution and bounds")
elif resolution:
self.bounds = (0, 0, resolution[0]-1, resolution[1]-1)
elif bounds:
self.bounds = bounds
else:
self.bounds = (0, 0, 639, 479)
self.DPI = DPI or (96, 96)
self.EGAPalette = EGAPalette or [
(0x00, 0x00, 0x00),
(0x00, 0x00, 0xAA),
(0x00, 0xAA, 0x00),
(0x00, 0xAA, 0xAA),
(0xAA, 0x00, 0x00),
(0xAA, 0x00, 0xAA),
(0xAA, 0x55, 0x00),
(0xAA, 0xAA, 0xAA),
(0x55, 0x55, 0x55),
(0x55, 0x55, 0xFF),
(0x55, 0xFF, 0x55),
(0x55, 0xFF, 0xFF),
(0xFF, 0x55, 0x55),
(0xFF, 0x55, 0xFF),
(0xFF, 0xFF, 0x55),
(0xFF, 0xFF, 0xFF),
]
self.data = bytearray()
self.palette = bytearray(256*3)
self.depth = depth
self.paletteMode = paletteMode
self.sourceResolution = sourceResolution or (0, 0)
@property
def width(self):
return (self.bounds[2] - self.bounds[0] + 1)
@property
def height(self):
return (self.bounds[3] - self.bounds[1] + 1)
@classmethod
def fromStream(cls, stream):
magic, version, compression, bpp, minX, minY, maxX, maxY, \
hDPI, vDPI, EGAPalette, depth, bpl, paletteMode, \
hSource, vSource = sPCXHeader.unpack(stream.read(sPCXHeader.size))
if magic != 0x0A:
raise ValueError("Invalid PCX file: Invalid start byte!")
if version not in (0, 2, 3, 4, 5):
raise ValueError("Invalid PCX file: Incorrect version!")
if compression not in (0, 1):
raise ValueError("Invalid PCX file: Invalid compression type!")
self = cls(
version = version,
compression = compression,
bitsPerColor = bpp,
bounds = (minX, minY, maxX, maxY),
DPI = (hDPI, vDPI),
EGAPalette = tuple([
(r, g, b) for r, g, b in zip(EGAPalette[::3], EGAPalette[1::3], EGAPalette[2::3])
]),
depth = depth,
paletteMode = paletteMode,
sourceResolution = (hSource, vSource)
)
dl = (depth * self.width * self.height)
data = bytearray(dl)
if compression:
for h in range(self.height):
i = 0
stop = False
line = bytearray(depth * self.width)
while i < len(line):
length = 1
value = stream.read(1)
if not value:
raise ValueError("Incomplete PCX stream! 1")
value = value[0]
if value & 0xC0 == 0xC0:
length = value & 0x3F
value = stream.read(1)
if not value:
raise ValueError("Incomplete PCX stream! 2")
value = value[0]
for ii in range(length):
for iii in reversed(range(0, 8, bpp)):
if i < len(line):
line[i] = (value >> iii) & ~(0xFF<<bpp)
i += 1
else:
break
if i >= len(line):
break
data[(depth*h*self.width):(depth*h*self.width)+len(line)] = line
else:
data = bytearray(stream.read(len(data)))
self.data = data
self.palette = stream.read(256*3)
return self
@classmethod
def fromBytes(cls, data):
stream = io.BytesIO(data)
stream.seek(0)
return cls.fromStream(stream)
if __name__ == "__main__":
for file in ("font1.pcx", "hose.pcx", "dog.pcx", "cat.pcx", "clown.pcx", "bunny.pcx"):
print(file)
with open(file, "rb") as f:
p = PCX.fromStream(f)
im = None
if p.depth == 1:
im = Image.new("L",(p.width,p.height))
im.putdata(p.data)
if im:
im.save(file+".png")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment