Browse Source

Added PCX image loader. Resolves #9.

Brucey 3 years ago
parent
commit
cc0708bed7

+ 27 - 0
pcx.mod/common.bmx

@@ -0,0 +1,27 @@
+' Copyright (c) 2022 Bruce A Henderson
+' 
+' This software is provided 'as-is', without any express or implied
+' warranty. In no event will the authors be held liable for any damages
+' arising from the use of this software.
+' 
+' Permission is granted to anyone to use this software for any purpose,
+' including commercial applications, and to alter it and redistribute it
+' freely, subject to the following restrictions:
+' 
+' 1. The origin of this software must not be misrepresented; you must not
+'    claim that you wrote the original software. If you use this software
+'    in a product, an acknowledgment in the product documentation would be
+'    appreciated but is not required.
+' 2. Altered source versions must be plainly marked as such, and must not be
+'    misrepresented as being the original software.
+' 3. This notice may not be removed or altered from any source distribution.
+' 
+SuperStrict
+
+Import "drpcx/*.h"
+Import "glue.c"
+
+Extern
+	Function drpcx_load:Byte Ptr(callback:Size_T(userData:Object, bufferOut:Byte Ptr, bytesToRead:Size_T), userData:Object, flipped:UInt, w:Int Var, h:Int Var, internalComponents:Int Var, desiredComponents:Int)
+	Function drpcx_free(handle:Byte Ptr)
+End Extern

+ 793 - 0
pcx.mod/drpcx/dr_pcx.h

@@ -0,0 +1,793 @@
+// PCX image loader. Public domain. See "unlicense" statement at the end of this file.
+// dr_pcx - v0.3.1 - 2018-09-11
+//
+// David Reid - [email protected]
+
+// USAGE
+//
+// dr_pcx is a single-file library. To use it, do something like the following in one .c file.
+//     #define DR_PCX_IMPLEMENTATION
+//     #include "dr_pcx.h"
+//
+// You can then #include this file in other parts of the program as you would with any other header file. Do something like
+// the following to load and decode an image:
+//
+//     int width;
+//     int height;
+//     int components
+//     drpcx_uint8* pImageData = drpcx_load_file("my_image.pcx", DRPCX_FALSE, &width, &height, &components, 0);
+//     if (pImageData == NULL) {
+//         // Failed to load image.
+//     }
+//
+//     ...
+//
+//     drpcx_free(pImageData);
+//
+// The boolean parameter (second argument in the above example) is whether or not the image should be flipped upside down.
+// 
+//
+//
+// OPTIONS
+// #define these options before including this file.
+//
+// #define DR_PCX_NO_STDIO
+//   Disable drpcx_load_file().
+//
+//
+//
+// QUICK NOTES
+// - 2-bpp/4-plane and 4-bpp/1-plane formats have not been tested.
+
+#ifndef dr_pcx_h
+#define dr_pcx_h
+
+#include <stddef.h>
+
+#if defined(_MSC_VER) && _MSC_VER < 1600
+typedef   signed char    drpcx_int8;
+typedef unsigned char    drpcx_uint8;
+typedef   signed short   drpcx_int16;
+typedef unsigned short   drpcx_uint16;
+typedef   signed int     drpcx_int32;
+typedef unsigned int     drpcx_uint32;
+typedef   signed __int64 drpcx_int64;
+typedef unsigned __int64 drpcx_uint64;
+#else
+#include <stdint.h>
+typedef int8_t           drpcx_int8;
+typedef uint8_t          drpcx_uint8;
+typedef int16_t          drpcx_int16;
+typedef uint16_t         drpcx_uint16;
+typedef int32_t          drpcx_int32;
+typedef uint32_t         drpcx_uint32;
+typedef int64_t          drpcx_int64;
+typedef uint64_t         drpcx_uint64;
+#endif
+typedef drpcx_uint8      drpcx_bool8;
+typedef drpcx_uint32     drpcx_bool32;
+#define DRPCX_TRUE       1
+#define DRPCX_FALSE      0
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Callback for when data is read. Return value is the number of bytes actually read.
+typedef size_t (* drpcx_read_proc)(void* userData, void* bufferOut, size_t bytesToRead);
+
+
+// Loads a PCX file using the given callbacks.
+drpcx_uint8* drpcx_load(drpcx_read_proc onRead, void* pUserData, drpcx_bool32 flipped, int* x, int* y, int* internalComponents, int desiredComponents);
+
+// Frees memory returned by drpcx_load() and family.
+void drpcx_free(void* pReturnValueFromLoad);
+
+
+#ifndef DR_PCX_NO_STDIO
+// Loads an PCX file from an actual file.
+drpcx_uint8* drpcx_load_file(const char* filename, drpcx_bool32 flipped, int* x, int* y, int* internalComponents, int desiredComponents);
+#endif
+
+// Helper for loading an PCX file from a block of memory.
+drpcx_uint8* drpcx_load_memory(const void* data, size_t dataSize, drpcx_bool32 flipped, int* x, int* y, int* internalComponents, int desiredComponents);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  // dr_pcx_h
+
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// IMPLEMENTATION
+//
+///////////////////////////////////////////////////////////////////////////////
+#ifdef DR_PCX_IMPLEMENTATION
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+
+#ifndef DR_PCX_NO_STDIO
+#include <stdio.h>
+
+static size_t drpcx__on_read_stdio(void* pUserData, void* bufferOut, size_t bytesToRead)
+{
+    return fread(bufferOut, 1, bytesToRead, (FILE*)pUserData);
+}
+
+drpcx_uint8* drpcx_load_file(const char* filename, drpcx_bool32 flipped, int* x, int* y, int* internalComponents, int desiredComponents)
+{
+    FILE* pFile;
+#ifdef _MSC_VER
+    if (fopen_s(&pFile, filename, "rb") != 0) {
+        return NULL;
+    }
+#else
+    pFile = fopen(filename, "rb");
+    if (pFile == NULL) {
+        return NULL;
+    }
+#endif
+
+    drpcx_uint8* pImageData = drpcx_load(drpcx__on_read_stdio, pFile, flipped, x, y, internalComponents, desiredComponents);
+
+    fclose(pFile);
+    return pImageData;
+}
+#endif  // DR_PCX_NO_STDIO
+
+
+typedef struct
+{
+    // A pointer to the beginning of the data. We use a char as the type here for easy offsetting.
+    const unsigned char* data;
+    size_t dataSize;
+    size_t currentReadPos;
+} drpcx_memory;
+
+static size_t drpcx__on_read_memory(void* pUserData, void* bufferOut, size_t bytesToRead)
+{
+    drpcx_memory* memory = (drpcx_memory*)pUserData;
+    assert(memory != NULL);
+    assert(memory->dataSize >= memory->currentReadPos);
+
+    size_t bytesRemaining = memory->dataSize - memory->currentReadPos;
+    if (bytesToRead > bytesRemaining) {
+        bytesToRead = bytesRemaining;
+    }
+
+    if (bytesToRead > 0) {
+        memcpy(bufferOut, memory->data + memory->currentReadPos, bytesToRead);
+        memory->currentReadPos += bytesToRead;
+    }
+
+    return bytesToRead;
+}
+
+drpcx_uint8* drpcx_load_memory(const void* data, size_t dataSize, drpcx_bool32 flipped, int* x, int* y, int* internalComponents, int desiredComponents)
+{
+    drpcx_memory memory;
+    memory.data = (const unsigned char*)data;
+    memory.dataSize = dataSize;
+    memory.currentReadPos = 0;
+    return drpcx_load(drpcx__on_read_memory, &memory, flipped, x, y, internalComponents, desiredComponents);
+}
+
+
+typedef struct
+{
+    drpcx_uint8 header;
+    drpcx_uint8 version;
+    drpcx_uint8 encoding;
+    drpcx_uint8 bpp;
+    drpcx_uint16 left;
+    drpcx_uint16 top;
+    drpcx_uint16 right;
+    drpcx_uint16 bottom;
+    drpcx_uint16 hres;
+    drpcx_uint16 vres;
+    drpcx_uint8 palette16[48];
+    drpcx_uint8 reserved1;
+    drpcx_uint8 bitPlanes;
+    drpcx_uint16 bytesPerLine;
+    drpcx_uint16 paletteType;
+    drpcx_uint16 screenSizeH;
+    drpcx_uint16 screenSizeV;
+    drpcx_uint8 reserved2[54];
+} drpcx_header;
+
+typedef struct
+{
+    drpcx_read_proc onRead;
+    void* pUserData;
+    drpcx_bool32 flipped;
+    drpcx_header header;
+
+    drpcx_uint32 width;
+    drpcx_uint32 height;
+    drpcx_uint32 components;    // 3 = RGB; 4 = RGBA. Only 3 and 4 are supported.
+    drpcx_uint8* pImageData;
+} drpcx;
+
+
+static drpcx_uint8 drpcx__read_byte(drpcx* pPCX)
+{
+    drpcx_uint8 byte = 0;
+    pPCX->onRead(pPCX->pUserData, &byte, 1);
+
+    return byte;
+}
+
+static drpcx_uint8* drpcx__row_ptr(drpcx* pPCX, drpcx_uint32 row)
+{
+    drpcx_uint32 stride = pPCX->width * pPCX->components;
+
+    drpcx_uint8* pRow = pPCX->pImageData;
+    if (pPCX->flipped) {
+        pRow += (pPCX->height - row - 1) * stride;
+    } else {
+        pRow += row * stride;
+    }
+
+    return pRow;
+}
+
+static drpcx_uint8 drpcx__rle(drpcx* pPCX, drpcx_uint8* pRLEValueOut)
+{
+    drpcx_uint8 rleCount;
+    drpcx_uint8 rleValue;
+
+    rleValue = drpcx__read_byte(pPCX);
+    if ((rleValue & 0xC0) == 0xC0) {
+        rleCount = rleValue & 0x3F;
+        rleValue = drpcx__read_byte(pPCX);
+    } else {
+        rleCount = 1;
+    }
+
+
+    *pRLEValueOut = rleValue;
+    return rleCount;
+}
+
+
+drpcx_bool32 drpcx__decode_1bit(drpcx* pPCX)
+{
+    drpcx_uint8 rleCount = 0;
+    drpcx_uint8 rleValue = 0;
+
+    switch (pPCX->header.bitPlanes)
+    {
+        case 1:
+        {
+            for (drpcx_uint32 y = 0; y < pPCX->height; ++y) {
+                drpcx_uint8* pRow = drpcx__row_ptr(pPCX, y);
+                for (drpcx_uint32 x = 0; x < pPCX->header.bytesPerLine; ++x) {
+                    if (rleCount == 0) {
+                        rleCount = drpcx__rle(pPCX, &rleValue);
+                    }
+                    rleCount -= 1;
+
+                    for (int bit = 0; (bit < 8) && ((x*8 + bit) < pPCX->width); ++bit) {
+                        drpcx_uint8 mask = (1 << (7 - bit));
+                        drpcx_uint8 paletteIndex = (rleValue & mask) >> (7 - bit);
+
+                        pRow[0] = paletteIndex * 255;
+                        pRow[1] = paletteIndex * 255;
+                        pRow[2] = paletteIndex * 255;
+                        pRow += 3;
+                    }
+                }
+            }
+
+            return DRPCX_TRUE;
+
+        } break;
+
+        case 2:
+        case 3:
+        case 4:
+        {
+            for (drpcx_uint32 y = 0; y < pPCX->height; ++y) {
+                for (drpcx_uint32 c = 0; c < pPCX->header.bitPlanes; ++c) {
+                    drpcx_uint8* pRow = drpcx__row_ptr(pPCX, y);
+                    for (drpcx_uint32 x = 0; x < pPCX->header.bytesPerLine; ++x) {
+                        if (rleCount == 0) {
+                            rleCount = drpcx__rle(pPCX, &rleValue);
+                        }
+                        rleCount -= 1;
+
+                        for (int bit = 0; (bit < 8) && ((x*8 + bit) < pPCX->width); ++bit) {
+                            drpcx_uint8 mask = (1 << (7 - bit));
+                            drpcx_uint8 paletteIndex = (rleValue & mask) >> (7 - bit);
+
+                            pRow[0] |= ((paletteIndex & 0x01) << c);
+                            pRow += pPCX->components;
+                        }
+                    }
+                }
+
+
+                drpcx_uint8* pRow = drpcx__row_ptr(pPCX, y);
+                for (drpcx_uint32 x = 0; x < pPCX->width; ++x) {
+                    drpcx_uint8 paletteIndex = pRow[0];
+                    for (drpcx_uint32 c = 0; c < pPCX->components; ++c) {
+                        pRow[c] = pPCX->header.palette16[paletteIndex*3 + c];
+                    }
+                    
+                    pRow += pPCX->components;
+                }
+            }
+
+            return DRPCX_TRUE;
+        }
+
+        default: return DRPCX_FALSE;
+    }
+}
+
+drpcx_bool32 drpcx__decode_2bit(drpcx* pPCX)
+{
+    drpcx_uint8 rleCount = 0;
+    drpcx_uint8 rleValue = 0;
+
+    switch (pPCX->header.bitPlanes)
+    {
+        case 1:
+        {
+            drpcx_uint8 paletteCGA[48];
+            paletteCGA[ 0] = 0x00; paletteCGA[ 1] = 0x00; paletteCGA[ 2] = 0x00;    // #000000
+            paletteCGA[ 3] = 0x00; paletteCGA[ 4] = 0x00; paletteCGA[ 5] = 0xAA;    // #0000AA
+            paletteCGA[ 6] = 0x00; paletteCGA[ 7] = 0xAA; paletteCGA[ 8] = 0x00;    // #00AA00
+            paletteCGA[ 9] = 0x00; paletteCGA[10] = 0xAA; paletteCGA[11] = 0xAA;    // #00AAAA
+            paletteCGA[12] = 0xAA; paletteCGA[13] = 0x00; paletteCGA[14] = 0x00;    // #AA0000
+            paletteCGA[15] = 0xAA; paletteCGA[16] = 0x00; paletteCGA[17] = 0xAA;    // #AA00AA
+            paletteCGA[18] = 0xAA; paletteCGA[19] = 0x55; paletteCGA[20] = 0x00;    // #AA5500
+            paletteCGA[21] = 0xAA; paletteCGA[22] = 0xAA; paletteCGA[23] = 0xAA;    // #AAAAAA
+            paletteCGA[24] = 0x55; paletteCGA[25] = 0x55; paletteCGA[26] = 0x55;    // #555555
+            paletteCGA[27] = 0x55; paletteCGA[28] = 0x55; paletteCGA[29] = 0xFF;    // #5555FF
+            paletteCGA[30] = 0x55; paletteCGA[31] = 0xFF; paletteCGA[32] = 0x55;    // #55FF55
+            paletteCGA[33] = 0x55; paletteCGA[34] = 0xFF; paletteCGA[35] = 0xFF;    // #55FFFF
+            paletteCGA[36] = 0xFF; paletteCGA[37] = 0x55; paletteCGA[38] = 0x55;    // #FF5555
+            paletteCGA[39] = 0xFF; paletteCGA[40] = 0x55; paletteCGA[41] = 0xFF;    // #FF55FF
+            paletteCGA[42] = 0xFF; paletteCGA[43] = 0xFF; paletteCGA[44] = 0x55;    // #FFFF55
+            paletteCGA[45] = 0xFF; paletteCGA[46] = 0xFF; paletteCGA[47] = 0xFF;    // #FFFFFF
+
+            drpcx_uint8 cgaBGColor   = pPCX->header.palette16[0] >> 4;
+            drpcx_uint8 i = (pPCX->header.palette16[3] & 0x20) >> 5;
+            drpcx_uint8 p = (pPCX->header.palette16[3] & 0x40) >> 6;
+            //drpcx_uint8 c = (pPCX->header.palette16[3] & 0x80) >> 7;    // Color or monochrome. How is monochrome handled?
+
+            for (drpcx_uint32 y = 0; y < pPCX->height; ++y) {
+                drpcx_uint8* pRow = drpcx__row_ptr(pPCX, y);
+                for (drpcx_uint32 x = 0; x < pPCX->header.bytesPerLine; ++x) {
+                    if (rleCount == 0) {
+                        rleCount = drpcx__rle(pPCX, &rleValue);
+                    }
+                    rleCount -= 1;
+
+                    for (int bit = 0; bit < 4; ++bit) {
+                        if (x*4 + bit < pPCX->width) {
+                            drpcx_uint8 mask = (3 << ((3 - bit) * 2));
+                            drpcx_uint8 paletteIndex = (rleValue & mask) >> ((3 - bit) * 2);
+
+                            drpcx_uint8 cgaIndex;
+                            if (paletteIndex == 0) {    // Background.
+                                cgaIndex = cgaBGColor;
+                            } else {                    // Foreground
+                                cgaIndex = (((paletteIndex << 1) + p) + (i << 3));
+                            }
+
+                            pRow[0] = paletteCGA[cgaIndex*3 + 0];
+                            pRow[1] = paletteCGA[cgaIndex*3 + 1];
+                            pRow[2] = paletteCGA[cgaIndex*3 + 2];
+                            pRow += 3;
+                        }
+                    }
+                }
+            }
+
+            // TODO: According to http://www.fysnet.net/pcxfile.htm, we should use the palette at the end of the file
+            //       instead of the standard CGA palette if the version is equal to 5. With my test files the palette
+            //       at the end of the file does not exist. Research this one.
+            if (pPCX->header.version == 5) {
+                drpcx_uint8 paletteMarker = drpcx__read_byte(pPCX);
+                if (paletteMarker == 0x0C) {
+                    // TODO: Implement Me.
+                }
+            }
+            
+            return DRPCX_TRUE;
+        };
+
+        case 4:
+        {
+            // NOTE: This is completely untested. If anybody knows where I can get a test file please let me know or send it through to me!
+            // TODO: Test Me.
+
+            for (drpcx_uint32 y = 0; y < pPCX->height; ++y) {
+                for (drpcx_uint32 c = 0; c < pPCX->header.bitPlanes; ++c) {
+                    drpcx_uint8* pRow = drpcx__row_ptr(pPCX, y);
+                    for (drpcx_uint32 x = 0; x < pPCX->header.bytesPerLine; ++x) {
+                        if (rleCount == 0) {
+                            rleCount = drpcx__rle(pPCX, &rleValue);
+                        }
+                        rleCount -= 1;
+
+                        for (int bitpair = 0; (bitpair < 4) && ((x*4 + bitpair) < pPCX->width); ++bitpair) {
+                            drpcx_uint8 mask = (4 << (3 - bitpair));
+                            drpcx_uint8 paletteIndex = (rleValue & mask) >> (3 - bitpair);
+
+                            pRow[0] |= ((paletteIndex & 0x03) << (c*2));
+                            pRow += pPCX->components;
+                        }
+                    }
+                }
+
+
+                drpcx_uint8* pRow = drpcx__row_ptr(pPCX, y);
+                for (drpcx_uint32 x = 0; x < pPCX->width; ++x) {
+                    drpcx_uint8 paletteIndex = pRow[0];
+                    for (drpcx_uint32 c = 0; c < pPCX->header.bitPlanes; ++c) {
+                        pRow[c] = pPCX->header.palette16[paletteIndex*3 + c];
+                    }
+                    
+                    pRow += pPCX->components;
+                }
+            }
+
+            return DRPCX_TRUE;
+        };
+
+        default: return DRPCX_FALSE;
+    }
+}
+
+drpcx_bool32 drpcx__decode_4bit(drpcx* pPCX)
+{
+    // NOTE: This is completely untested. If anybody knows where I can get a test file please let me know or send it through to me!
+    // TODO: Test Me.
+
+    if (pPCX->header.bitPlanes > 1) {
+        return DRPCX_FALSE;
+    }
+
+    drpcx_uint8 rleCount = 0;
+    drpcx_uint8 rleValue = 0;
+
+    for (drpcx_uint32 y = 0; y < pPCX->height; ++y) {
+        for (drpcx_uint32 c = 0; c < pPCX->header.bitPlanes; ++c) {
+            drpcx_uint8* pRow = drpcx__row_ptr(pPCX, y);
+            for (drpcx_uint32 x = 0; x < pPCX->header.bytesPerLine; ++x) {
+                if (rleCount == 0) {
+                    rleCount = drpcx__rle(pPCX, &rleValue);
+                }
+                rleCount -= 1;
+
+                for (int nibble = 0; (nibble < 2) && ((x*2 + nibble) < pPCX->width); ++nibble)
+                {
+                    drpcx_uint8 mask = (4 << (1 - nibble));
+                    drpcx_uint8 paletteIndex = (rleValue & mask) >> (1 - nibble);
+
+                    pRow[0] |= ((paletteIndex & 0x0F) << (c*4)); 
+                    pRow += pPCX->components;
+                }
+            }
+        }
+
+
+        drpcx_uint8* pRow = drpcx__row_ptr(pPCX, y);
+        for (drpcx_uint32 x = 0; x < pPCX->width; ++x) {
+            drpcx_uint8 paletteIndex = pRow[0];
+            for (drpcx_uint32 c = 0; c < pPCX->components; ++c) {
+                pRow[c] = pPCX->header.palette16[paletteIndex*3 + c];
+            }
+                    
+            pRow += pPCX->components;
+        }
+    }
+
+    return DRPCX_TRUE;
+}
+
+drpcx_bool32 drpcx__decode_8bit(drpcx* pPCX)
+{
+    drpcx_uint8 rleCount = 0;
+    drpcx_uint8 rleValue = 0;
+    drpcx_uint32 stride = pPCX->width * pPCX->components;
+
+    switch (pPCX->header.bitPlanes)
+    {
+        case 1:
+        {
+            for (drpcx_uint32 y = 0; y < pPCX->height; ++y) {
+                drpcx_uint8* pRow = drpcx__row_ptr(pPCX, y);
+                for (drpcx_uint32 x = 0; x < pPCX->header.bytesPerLine; ++x) {
+                    if (rleCount == 0) {
+                        rleCount = drpcx__rle(pPCX, &rleValue);
+                    }
+                    rleCount -= 1;
+
+                    if (x < pPCX->width) {
+                        pRow[0] = rleValue;
+                        pRow[1] = rleValue;
+                        pRow[2] = rleValue;
+                        pRow += 3;
+                    }
+                }
+            }
+
+            // At this point we can know if we are dealing with a palette or a grayscale image by checking the next byte. If it's equal to 0x0C, we
+            // need to do a simple palette lookup.
+            drpcx_uint8 paletteMarker = drpcx__read_byte(pPCX);
+            if (paletteMarker == 0x0C) {
+                // A palette is present - we need to do a second pass.
+                drpcx_uint8 palette256[768];
+                if (pPCX->onRead(pPCX->pUserData, palette256, sizeof(palette256)) != sizeof(palette256)) {
+                    return DRPCX_FALSE;
+                }
+
+                for (drpcx_uint32 y = 0; y < pPCX->height; ++y) {
+                    drpcx_uint8* pRow = pPCX->pImageData + (y * stride);
+                    for (drpcx_uint32 x = 0; x < pPCX->width; ++x) {
+                        drpcx_uint8 index = pRow[0];
+                        pRow[0] = palette256[index*3 + 0];
+                        pRow[1] = palette256[index*3 + 1];
+                        pRow[2] = palette256[index*3 + 2];
+                        pRow += 3;
+                    }
+                }
+            }
+
+            return DRPCX_TRUE;
+        }
+
+        case 3:
+        case 4:
+        {
+            for (drpcx_uint32 y = 0; y < pPCX->height; ++y) {
+                for (drpcx_uint32 c = 0; c < pPCX->components; ++c) {
+                    drpcx_uint8* pRow = drpcx__row_ptr(pPCX, y);
+                    for (drpcx_uint32 x = 0; x < pPCX->header.bytesPerLine; ++x) {
+                        if (rleCount == 0) {
+                            rleCount = drpcx__rle(pPCX, &rleValue);
+                        }
+                        rleCount -= 1;
+
+                        if (x < pPCX->width) {
+                            pRow[c] = rleValue;
+                            pRow += pPCX->components;
+                        }
+                    }
+                }
+            }
+
+            return DRPCX_TRUE;
+        }
+    }
+
+    return DRPCX_TRUE;
+}
+
+drpcx_uint8* drpcx_load(drpcx_read_proc onRead, void* pUserData, drpcx_bool32 flipped, int* x, int* y, int* internalComponents, int desiredComponents)
+{
+    if (onRead == NULL) return NULL;
+    if (desiredComponents > 4) return NULL;
+
+    drpcx pcx;
+    pcx.onRead    = onRead;
+    pcx.pUserData = pUserData;
+    pcx.flipped   = flipped;
+    if (onRead(pUserData, &pcx.header, sizeof(pcx.header)) != sizeof(pcx.header)) {
+        return NULL;    // Failed to read the header.
+    }
+
+    if (pcx.header.header != 10) {
+        return NULL;    // Not a PCX file.
+    }
+
+    if (pcx.header.encoding != 1) {
+        return NULL;    // Not supporting non-RLE encoding. Would assume a value of 0 indicates raw, unencoded, but that is apparently never used.
+    }
+
+    if (pcx.header.bpp != 1 && pcx.header.bpp != 2 && pcx.header.bpp != 4 && pcx.header.bpp != 8) {
+        return NULL;    // Unsupported pixel format.
+    }
+
+
+    if (pcx.header.left > pcx.header.right) {
+        drpcx_uint16 temp = pcx.header.left;
+        pcx.header.left = pcx.header.right;
+        pcx.header.right = temp;
+    }
+    if (pcx.header.top > pcx.header.bottom) {
+        drpcx_uint16 temp = pcx.header.top;
+        pcx.header.top = pcx.header.bottom;
+        pcx.header.bottom = temp;
+    }
+
+    pcx.width = pcx.header.right - pcx.header.left + 1;
+    pcx.height = pcx.header.bottom - pcx.header.top + 1;
+    pcx.components = (pcx.header.bpp == 8 && pcx.header.bitPlanes == 4) ? 4 : 3;
+
+    size_t dataSize = pcx.width * pcx.height * pcx.components;
+    pcx.pImageData = (drpcx_uint8*)calloc(1, dataSize);   // <-- Clearing to zero is important! Required for proper decoding.
+    if (pcx.pImageData == NULL) {
+        return NULL;    // Failed to allocate memory.
+    }
+
+    drpcx_bool32 result = DRPCX_FALSE;
+    switch (pcx.header.bpp)
+    {
+        case 1:
+        {
+            result = drpcx__decode_1bit(&pcx);
+        } break;
+
+        case 2:
+        {
+            result = drpcx__decode_2bit(&pcx);
+        } break;
+
+        case 4:
+        {
+            result = drpcx__decode_4bit(&pcx);
+        } break;
+
+        case 8:
+        {
+            result = drpcx__decode_8bit(&pcx);
+        } break;
+    }
+
+    if (!result) {
+        free(pcx.pImageData);
+        return NULL;
+    }
+
+    // There's an annoying amount of branching when loading PCX files so for simplicity I'm doing the component conversion as
+    // a second pass.
+    if (desiredComponents == 0) desiredComponents = pcx.components;
+    if (desiredComponents != (int)pcx.components) {
+        drpcx_uint8* pNewImageData = (drpcx_uint8*)malloc(pcx.width * pcx.height * desiredComponents);
+        if (pNewImageData == NULL) {
+            free(pcx.pImageData);
+            return NULL;
+        }
+
+        drpcx_uint8* pSrcData = pcx.pImageData;
+        drpcx_uint8* pDstData = pNewImageData;
+        if (desiredComponents < (int)pcx.components) {
+            // We're reducing the number of components. Just drop the excess.
+            for (drpcx_uint32 i = 0; i < pcx.width*pcx.height; ++i) {
+                for (int c = 0; c < desiredComponents; ++c) {
+                    pDstData[c] = pSrcData[c];
+                } 
+
+                pSrcData += pcx.components;
+                pDstData += desiredComponents;
+            }
+        } else {
+            // We're increasing the number of components. Always ensure the alpha channel is set to 0xFF.
+            if (pcx.components == 1) {
+                for (drpcx_uint32 i = 0; i < pcx.width*pcx.height; ++i) {
+                    for (int c = 0; c < desiredComponents; ++c) {
+                        pDstData[c] = pSrcData[0];
+                    }
+
+                    pSrcData += pcx.components;
+                    pDstData += desiredComponents;
+                }
+            } else if (pcx.components == 2) {
+                for (drpcx_uint32 i = 0; i < pcx.width*pcx.height; ++i) {
+                    pDstData[0] = pSrcData[0];
+                    pDstData[1] = pSrcData[1];
+                    pDstData[2] = 0x00;
+                    if (desiredComponents == 4) pDstData[3] = 0xFF;
+
+                    pSrcData += pcx.components;
+                    pDstData += desiredComponents;
+                }
+            } else {
+                assert(pcx.components == 3);
+                assert(desiredComponents == 4);
+                for (drpcx_uint32 i = 0; i < pcx.width*pcx.height; ++i) {
+                    pDstData[0] = pSrcData[0];
+                    pDstData[1] = pSrcData[1];
+                    pDstData[2] = pSrcData[2];
+                    pDstData[3] = 0xFF;
+
+                    pSrcData += pcx.components;
+                    pDstData += desiredComponents;
+                }
+            }
+        }
+
+        free(pcx.pImageData);
+        pcx.pImageData = pNewImageData;
+    }
+
+    if (x) *x = pcx.width;
+    if (y) *y = pcx.height;
+    if (internalComponents) *internalComponents = pcx.components;
+    return pcx.pImageData;
+}
+
+void drpcx_free(void* pReturnValueFromLoad)
+{
+    free(pReturnValueFromLoad);
+}
+
+#endif // DR_PCX_IMPLEMENTATION
+
+
+// REVISION HISTORY
+//
+// v0.3.1 - 2018-09-11
+//   - Styling fixes.
+//   - Fix a typo.
+//
+// v0.3 - 2018-02-08
+//   - API CHANGE: Rename dr_* types to drpcx_*.
+//
+// v0.2c - 2018-02-07
+//   - Fix a crash.
+//
+// v0.2b - 2018-02-02
+//   - Fix compilation error.
+//
+// v0.2a - 2017-07-16
+//   - Change underlying type for booleans to unsigned.
+//
+// v0.2 - 2016-10-28
+//   - API CHANGE: Add a parameter to drpcx_load() and family to control the number of output components.
+//   - Use custom sized types rather than built-in ones to improve support for older MSVC compilers.
+//
+// v0.1c - 2016-10-23
+//   - A minor change to drpcx_bool8 and drpcx_bool32 types.
+//
+// v0.1b - 2016-10-11
+//   - Use drpcx_bool32 instead of the built-in "bool" type. The reason for this change is that it helps maintain API/ABI consistency
+//     between C and C++ builds.
+//
+// v0.1a - 2016-09-18
+//   - Change date format to ISO 8601 (YYYY-MM-DD)
+//
+// v0.1 - 2016-05-04
+//   - Initial versioned release.
+
+
+// TODO
+// - Test 2-bpp/4-plane and 4-bpp/1-plane formats.
+
+
+/*
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <http://unlicense.org/>
+*/
+

+ 53 - 0
pcx.mod/examples/example_01.bmx

@@ -0,0 +1,53 @@
+SuperStrict
+
+Framework SDL.SDLRenderMax2D
+Import Image.PCX
+
+Local w:Int = DesktopWidth() * .75
+Local h:Int = DeskTopHeight() * .75
+
+Graphics w, h, 0
+
+AutoMidHandle(True)
+
+Local img1:TImage = Loader("marbles.pcx", w, h)
+Local img2:TImage = Loader("gmarbles.pcx", w, h)
+
+If Not img1 Or Not img2 Then
+	Print "Failed to load image"
+	End
+End If
+
+Local image:Int
+Local img:TImage = img1
+
+While Not KeyDown(Key_ESCAPE)
+
+	Cls
+
+	If KeyHit(KEY_SPACE) Then
+		image = Not image
+		If image Then
+			img = img2
+		Else
+			img = img1
+		End If
+	End If
+
+	DrawImage img, w / 2, h / 2
+
+	Flip
+
+Wend
+
+Function Loader:TImage(path:String, maxWidth:Int, maxHeight:Int)
+	Local pix:TPixmap = LoadPixmap( path )
+	If pix Then
+		If pix.width > maxWidth Or pix.height > maxHeight Then
+			Local ratio:Float = Min(maxWidth / Float(pix.width), maxHeight / Float(pix.height))
+			pix = ResizePixmap(pix, Int(pix.width * ratio), Int(pix.height * ratio))
+		End If
+		Return LoadImage(pix)
+	End If
+End Function
+

BIN
pcx.mod/examples/gmarbles.pcx


BIN
pcx.mod/examples/marbles.pcx


+ 22 - 0
pcx.mod/glue.c

@@ -0,0 +1,22 @@
+/*
+  Copyright (c) 2022 Bruce A Henderson
+  
+  This software is provided 'as-is', without any express or implied
+  warranty. In no event will the authors be held liable for any damages
+  arising from the use of this software.
+  
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+  
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#define DR_PCX_IMPLEMENTATION
+#define DR_PCX_NO_STDIO
+#include "dr_pcx.h"

+ 109 - 0
pcx.mod/pcx.bmx

@@ -0,0 +1,109 @@
+' Copyright (c) 2022 Bruce A Henderson
+' 
+' This software is provided 'as-is', without any express or implied
+' warranty. In no event will the authors be held liable for any damages
+' arising from the use of this software.
+' 
+' Permission is granted to anyone to use this software for any purpose,
+' including commercial applications, and to alter it and redistribute it
+' freely, subject to the following restrictions:
+' 
+' 1. The origin of this software must not be misrepresented; you must not
+'    claim that you wrote the original software. If you use this software
+'    in a product, an acknowledgment in the product documentation would be
+'    appreciated but is not required.
+' 2. Altered source versions must be plainly marked as such, and must not be
+'    misrepresented as being the original software.
+' 3. This notice may not be removed or altered from any source distribution.
+' 
+SuperStrict
+
+Rem
+bbdoc: Image/PCX loader
+about:
+The PCX loader module provides the ability to load PCX format #pixmaps.
+End Rem
+Module Image.PCX
+
+ModuleInfo "Version: 1.00"
+ModuleInfo "License: ZLib/PNG License"
+ModuleInfo "Author: Bruce A Henderson"
+
+ModuleInfo "History: 1.00"
+ModuleInfo "History: Initial Release."
+
+Import BRL.pixmap
+
+Import "common.bmx"
+
+Rem
+bbdoc: PCX image format files.
+End Rem
+Type TPcxImage
+
+	Rem
+	bbdoc: Loads PCX image from a file as a #TPixmap.
+	End Rem
+	Function Load:TPixmap(file:String, channels:Int = 0)
+		Local stream:TStream = ReadStream(file)
+		If stream Then
+			Local pix:TPixmap = Load(stream, channels)
+			stream.Close()
+			Return pix
+		End If
+	End Function
+
+	Rem
+	bbdoc: Loads PCX image from stream as a #TPixmap.
+	End Rem
+	Function Load:TPixmap(stream:TStream, channels:Int = 0)
+
+		Local width:Int
+		Local height:Int
+		Local components:Int
+
+		Local pixels:Byte Ptr = drpcx_load(_ReadCallback, stream, False, width, height, components, 0)
+		If pixels Then
+			Local format:Int = PF_RGB888
+			Local align:Int = 3
+			If components = 4 Then
+				format = PF_RGBA8888
+				align = 4
+			End If
+			Local pix:TPixmap = TPixmap.Create(width, height, format, align)
+			
+			MemCopy(pix.pixels, pixels, Size_T(width * height * align))
+
+			drpcx_free(pixels)
+			Return pix
+		End If
+
+	End Function
+
+	Function _ReadCallback:Size_T(streamObj:Object, bufferOut:Byte Ptr, bytesToRead:Size_T)
+		Try
+			Local stream:TStream = TStream(streamObj)
+			If stream Then
+				Local res:Long = stream.ReadBytes(bufferOut, Long(bytesToRead))
+				Return res
+			End If
+		Catch e:Object
+		End Try
+		Return 0
+	End Function
+
+End Type
+
+Private
+
+Type TPixmapLoaderPcx Extends TPixmapLoader
+
+	Method LoadPixmap:TPixmap( stream:TStream ) Override
+
+		Return TPcxImage.Load(stream)
+
+	End Method
+
+End Type
+
+New TPixmapLoaderPcx