Skip to content

Instantly share code, notes, and snippets.

@akien-mga
Created May 30, 2024 20:44
Show Gist options
  • Save akien-mga/0b2832c553e63fded43fade442656650 to your computer and use it in GitHub Desktop.
Save akien-mga/0b2832c553e63fded43fade442656650 to your computer and use it in GitHub Desktop.
PCX Loader class in GDScript
@tool
class_name ImageLoaderPCX
extends ImageFormatLoaderExtension
## Implementation of an ImageFormatLoader for PCX files with the
## [code].pcx[/code] extension.
##
## PCX files are an obsolete image format used in the DOS era, developed in 1985
## by ZSoft Corporation.
##
## @tutorial(ZSoft PCX file format technical reference manual): https://bespin.org/~qz/pc-gpe/pcx.txt
## @tutorial(PCX graphics files explained): http://www.fysnet.net/pcxfile.htm
## @experimental: Only supports a few versions/palette types for now.
const PCX_HEADER_SIZE: int = 128
const PCX_PALETTE256_SIZE: int = 768
class PCXHeader:
var manufacturer: int # u8
var version: int # u8
var encoding: int # u8
var bits_per_pixel: int # u8
var xmin: int # u16
var ymin: int # u16
var xmax: int # u16
var ymax: int # u16
var hdpi: int # u16
var vdpi: int # u16
var palette16: PackedByteArray # 48 bytes
var reserved: int # u8
var num_planes: int # u8
var bytes_per_line: int # u16, for each plane
var palette_type: int # u16
var h_screen_size: int # u16
var v_screen_size: int # u16
var filler: PackedByteArray # 54 bytes
func _get_recognized_extensions() -> PackedStringArray:
return ["pcx"]
func _load_image(image: Image, file: FileAccess, flags: int, scale: float) -> Error:
var len := file.get_length()
var header := PCXHeader.new()
if len < PCX_HEADER_SIZE:
printerr("Invalid PCX file size %d, must be at least %d bytes." \
% [file.get_length(), PCX_HEADER_SIZE])
return ERR_FILE_CORRUPT
header.manufacturer = file.get_8()
header.version = file.get_8()
header.encoding = file.get_8()
header.bits_per_pixel = file.get_8()
header.xmin = file.get_16()
header.ymin = file.get_16()
header.xmax = file.get_16()
header.ymax = file.get_16()
header.hdpi = file.get_16()
header.vdpi = file.get_16()
header.palette16 = file.get_buffer(48)
header.reserved = file.get_8()
header.num_planes = file.get_8()
header.bytes_per_line = file.get_16()
header.palette_type = file.get_16()
header.h_screen_size = file.get_16()
header.v_screen_size = file.get_16()
header.filler = file.get_buffer(54)
if header.version != 5:
printerr("Only PCX files in version 5 with 256 color palette are supported for now.")
return ERR_UNAVAILABLE
# Read and decompress image data.
if header.encoding != 1:
printerr("Unencoded PCX image data isn't supported yet.")
return ERR_UNAVAILABLE
# TODO: See if this can be optimized, takes 19 ms on my laptop on a 400x256 image.
var pre_time := Time.get_ticks_msec()
var size_px := Vector2i(header.xmax - header.xmin + 1, header.ymax - header.ymin + 1)
var pcx_data_len := header.num_planes * header.bytes_per_line * size_px.y
var pcx_data: PackedByteArray
var l := 0
while l < pcx_data_len:
var byte := file.get_8()
if (byte & 0b1100_0000) == 0b1100_0000: # RLE pair.
var run_len := (byte & 0b0011_1111)
var arr: PackedByteArray
arr.resize(run_len)
arr.fill(file.get_8())
pcx_data.append_array(arr)
l += run_len
else: # Single byte.
pcx_data.append(byte)
l += 1
print('Ticks elapsed decoding RLE: %d ms' % (Time.get_ticks_msec() - pre_time))
# Decode and apply color palette.
if header.version == 5 and len <= PCX_HEADER_SIZE + PCX_PALETTE256_SIZE + 1:
printerr("Missing 256 color palette in PCX file, other types not supported.")
return ERR_UNAVAILABLE
pre_time = Time.get_ticks_msec()
var palette256: PackedByteArray
file.seek_end(-PCX_PALETTE256_SIZE - 1)
if file.get_8() == 12:
palette256 = file.get_buffer(PCX_PALETTE256_SIZE)
var pal_file := FileAccess.open(file.get_path().get_basename() + ".palette", FileAccess.WRITE)
pal_file.store_buffer(palette256)
print('Ticks elapsed decoding palette: %d ms' % (Time.get_ticks_msec() - pre_time))
pre_time = Time.get_ticks_msec()
var img_data: PackedByteArray
img_data.resize(pcx_data_len * 3)
for i: int in pcx_data_len:
var j := i * 3
var idx := pcx_data[i] * 3
img_data[j] = palette256[idx]
img_data[j + 1] = palette256[idx + 1]
img_data[j + 2] = palette256[idx + 2]
print('Ticks elapsed applying palette: %d ms' % (Time.get_ticks_msec() - pre_time))
image.set_data(size_px.x, size_px.y, false, Image.FORMAT_RGB8, img_data)
return OK
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment