Skip to content

Instantly share code, notes, and snippets.

@DanielGibson
Last active April 4, 2016 01:28
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 DanielGibson/2ceeba3fb6141e839c4aaf141b649762 to your computer and use it in GitHub Desktop.
Save DanielGibson/2ceeba3fb6141e839c4aaf141b649762 to your computer and use it in GitHub Desktop.
Hacky code that parses and displays Daikatana .wal files and 8bit BMP files
/*
* Kinda messy code to parse and display 8bit BMPs and Daikatana wals
* Can also be compiled to check whether the colormaps of textures in a
* directory match that of the corresponding colormap.bmp
*
* Note that it currently only displays the first mipmap level of a .wal texture
* and ignores the animname, flags, contents and value fields of the header.
*
* (C) 2016 Daniel Gibson
*
* LICENSE
* This software is dual-licensed to the public domain and under the following
* license: you are granted a perpetual, irrevocable license to copy, modify,
* publish, and distribute this file as you see fit.
*/
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <assert.h>
#include <SDL2/SDL.h>
#define eprintf(...) fprintf(stderr, __VA_ARGS__)
typedef struct
{
unsigned char* data;
int32_t len; // length in bytes
} Buffer;
typedef struct
{
unsigned char r;
unsigned char g;
unsigned char b;
} RGB;
typedef struct
{
int32_t width;
int32_t height;
int32_t paletteSize;
RGB palette[256];
Buffer imgData;
} IndexedBitmap;
// returns -1 on error
static int getFileLength(int fd)
{
struct stat stat;
if(fstat(fd, &stat) != 0) return -1;
return stat.st_size;
}
static int getInt16(const unsigned char* data)
{
return data[0] + ((int)data[1] << 8);
}
static int32_t getInt32(const unsigned char* data)
{
uint32_t x = getInt16(data);
return x + (getInt16(data+2) << 16);
}
// Buffer.len is < 0 on error
static Buffer loadFile(const char* path)
{
Buffer ret = {NULL, -1};
FILE* f = fopen(path, "rb");
if(f == NULL)
{
eprintf("Couldn't open '%s'!\n", path);
return ret;
}
int size = getFileLength(fileno(f));
if(size == -1)
{
eprintf("Couldn't get filesize of '%s'!\n", path);
return ret;
}
if(size == 0)
{
ret.len = 0;
return ret;
}
unsigned char* buf = malloc(size);
if(buf == NULL)
{
eprintf("Couldn't allocate %d bytes of data - Out of Memory?!\n", size);
return ret;
}
if(fread(buf, size, 1, f) != 1)
{
eprintf("Error reading from '%s'!\n", path);
return ret;
}
ret.data = buf;
ret.len = size;
return ret;
}
static void freeBuffer(Buffer* b)
{
free(b->data);
b->data = NULL;
b->len = 0;
}
static IndexedBitmap parseIndexedBMP(Buffer bmp, int headerOnly)
{
IndexedBitmap ret = {0};
ret.width = ret.height = ret.paletteSize = ret.imgData.len = -1;
if(bmp.len < 54) // that's the size of the two BMP headers always present
{
eprintf("Too small for a valid bitmap!\n");
return ret;
}
const unsigned char* d = bmp.data;
if(d[0] != 'B' || d[1] != 'M')
{
eprintf("invalid BMP file!\n");
return ret;
}
int bmpInfoHdrSize = getInt32(d+14);
// there are several bmp versions with different header sizes, but all with
// size>40 should be compatible for our purposes
// TODO: bmpInfoHdrSize == 12 for BMP v1 headers, which need different treatment
if(bmpInfoHdrSize != 40 && bmpInfoHdrSize != 64 && bmpInfoHdrSize != 52 && bmpInfoHdrSize != 56 && bmpInfoHdrSize != 108 && bmpInfoHdrSize != 124)
{
eprintf("BMP header malformed, size %d!\n", bmpInfoHdrSize);
return ret;
}
int compression = getInt32(d+30);
if(getInt16(d+28) != 8 // bpp must be 8 (indexed with 8bit indices)
|| (compression != 0 && compression != 1)) // must be uncompressed or 8bit RLE compressed
{
eprintf("Invalid Bitmap - Only uncompressed, paletted (indexed) 8bit BMPs are supported!\n");
return ret;
}
ret.paletteSize = getInt32(d+46);
if(ret.paletteSize == 0) ret.paletteSize = 256;
if(bmp.len < 54+ret.paletteSize*4)
{
eprintf("Too small for a valid bitmap with a palette with %d entries!\n", ret.paletteSize);
ret.paletteSize = -1;
return ret;
}
const unsigned char* paletteCur = d+14+bmpInfoHdrSize;
for(int i=0; i<ret.paletteSize; ++i)
{
RGB rgb;
rgb.r = paletteCur[2];
rgb.g = paletteCur[1];
rgb.b = paletteCur[0];
ret.palette[i] = rgb;
paletteCur += 4; // each entry has 4 bytes, the last one is ignored
}
// zero out unused palette entries (if any)
for(int i=ret.paletteSize; i<256; ++i)
{
RGB z = {0,0,0};
ret.palette[i] = z;
}
int w = getInt32(d+18);
int h = getInt32(d+22);
int isTopDown = 0;
if(h<0)
{
isTopDown = 1;
h = -h;
}
if(headerOnly)
{
ret.width = w;
ret.height = h;
ret.imgData.len = 0;
ret.imgData.data = NULL;
return ret;
}
int imgDataOffset = getInt32(d+10);
if(!compression && imgDataOffset + w*h > bmp.len)
{
eprintf("Bitmap malformed, not long enough to actually contain image data!\n");
return ret;
}
int dataSize = w*h;
unsigned char* out = malloc(dataSize);
ret.imgData.data = out;
ret.imgData.len = dataSize;
ret.width = w;
ret.height = h;
if(out == NULL)
{
eprintf("Couldn't allocate %d bytes of data - OOM?!\n", dataSize);
return ret;
}
int bmpRowSize = (w+3) & ~3; // rounded up to multiple of 4
if(isTopDown)
{
assert(!compression && "Wikipedia claims RLE compression is only used with bottom up BMPs");
// this is the order we want => iterate through bmp rows and copy them to output
const unsigned char* imgDataCur = d + imgDataOffset;
for(int row=0; row<h; ++row)
{
memcpy(out, imgDataCur, w);
out += w;
imgDataCur += bmpRowSize;
}
}
else
{
// start at last row, go backwards
if(compression == 0)
{
const unsigned char* imgDataCurRow = d + imgDataOffset + bmpRowSize*h - bmpRowSize;
for(int row=0; row<h; ++row)
{
memcpy(out, imgDataCurRow, w);
out += w;
imgDataCurRow -= bmpRowSize;
}
}
else // 8bit RLE compression
{
// some more bytes because bmp row size is multiples of 4
unsigned char* rowBuf = malloc(w+3);
const unsigned char* curIn = d + imgDataOffset;
unsigned char* curOutRow = out + dataSize - w;
for(int row=0; row<h; ++row)
{
unsigned char* curOut = rowBuf;
for(;;)
{
int numRepeats = *curIn;
++curIn;
if(numRepeats > 0)
{
unsigned char b = *curIn;
++curIn;
memset(curOut, b, numRepeats);
curOut += numRepeats;
}
else
{
int numBytes = *curIn;
++curIn;
if(numBytes < 2)
{
// bitmap done/next row => should have written w bytes
// (maybe more for 4byte-padding)
ptrdiff_t writtenBytes = curOut - rowBuf;
assert(writtenBytes >= w && writtenBytes <= w+3);
break;
}
else if(numBytes > 2)
{
// numBytes bytes of uncompressed data following
memcpy(curOut, curIn, numBytes);
curIn += numBytes;
curOut += numBytes;
}
else if(numBytes == 2)
{
eprintf("Unsupported: change pixel position by %d %d ('delta')\n", (int)curIn[1], (int)curIn[2]);
assert(0 && "Unhandled: delta in BMP RLE!");
// TODO - is this relevant? what happens to the skipped pixels?
}
}
}
memcpy(curOutRow, rowBuf, w);
curOutRow -= w;
}
free(rowBuf);
}
}
return ret;
}
/* Daikatana .wal header:
Offset: field
0: char version; // NEW: should be 3
1: char padding[3]; // NEW this is just garbage, skip it
4: char name[32];
36: unsigned width, height; // size of mipmap level 0, found at offset offsets[0]
44: unsigned offsets[9]; // NEW 9 instead of 4 mip maps stored
80: char animname[32];
112: int flags;
116: int contents;
120: unsigned char palette[256*3]; // NEW: 256 RGB color values
888: int value;
=> size 892
*/
static IndexedBitmap parseWal(Buffer wal, int headerOnly)
{
IndexedBitmap ret = {0};
ret.width = ret.height = ret.paletteSize = ret.imgData.len = -1;
if(wal.len < 892) // that's the size of the Daikatana WAL header
{
eprintf("Too small for a valid Daikatana .wal!\n");
return ret;
}
const unsigned char* d = wal.data;
if(d[0] != 3)
{
eprintf("Wrong .wal version (%.32s), must be 3!\n", d);
if(d[0] > 32) eprintf("This could be a Quake2 .wal which is incompatible!\n");
return ret;
}
int w = getInt32(d+36);
int h = getInt32(d+40);
uint32_t offsets[9];
for(int i=0; i<9; ++i)
{
offsets[i] = getInt32(d+44+i*4);
}
const unsigned char* pltCur = d+120;
for(int i=0; i<256; ++i)
{
RGB rgb = {pltCur[0], pltCur[1], pltCur[2]};
ret.palette[i] = rgb;
pltCur += 3;
}
ret.paletteSize = 256;
if(headerOnly)
{
ret.width = w;
ret.height = h;
ret.imgData.data = NULL;
ret.imgData.len = 0;
return ret;
}
int dataSize = w*h;
if(wal.len < 892+dataSize)
{
eprintf(".wal malformed, not long enough to actually contain image data!\n");
return ret;
}
unsigned char* buf = malloc(dataSize);
if(buf == NULL)
{
eprintf("Couldn't allocate %d bytes of data - OOM?!\n", dataSize);
return ret;
}
memcpy(buf, d+offsets[0], dataSize);
ret.width = w;
ret.height = h;
ret.imgData.data = buf;
ret.imgData.len = dataSize;
return ret;
}
static void displayImage(SDL_Surface* surf)
{
SDL_Init(SDL_INIT_VIDEO);
SDL_Window* win = SDL_CreateWindow("test SDL_stbimage.h", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
640, 480, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
SDL_Surface* winSurf = SDL_GetWindowSurface(win);
Uint32 grey = SDL_MapRGB(winSurf->format, 127, 127, 127);
SDL_FillRect(winSurf, NULL, grey);
SDL_BlitSurface(surf, NULL, winSurf, NULL);
SDL_UpdateWindowSurface(win);
while(1)
{
SDL_Delay(200);
SDL_Event ev;
while(SDL_PollEvent(&ev))
{
switch(ev.type)
{
case SDL_QUIT: goto end;
break;
case SDL_KEYDOWN:
if(ev.key.keysym.sym == SDLK_q) goto end;
break;
default:
break;
}
}
winSurf = SDL_GetWindowSurface(win);
SDL_BlitSurface(surf, NULL, winSurf, NULL);
SDL_UpdateWindowSurface(win);
}
end:
SDL_DestroyWindow(win);
SDL_Quit();
}
static SDL_Surface* bmpToSurf(const IndexedBitmap bmp)
{
SDL_Surface* surf = NULL;
Uint32 rmask, gmask, bmask, amask;
// ok, the following is pretty stupid.. SDL_CreateRGBSurfaceFrom() pretends to use
// a void* for the data, but it's really treated as endian-specific Uint32*
// and there isn't even an SDL_PIXELFORMAT_* for 32bit byte-wise RGBA
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
rmask = 0x00ff0000;
gmask = 0x0000ff00;
bmask = 0x000000ff;
amask = 0;
#else // little endian, like x86
rmask = 0x000000ff;
gmask = 0x0000ff00;
bmask = 0x00ff0000;
amask = 0;
#endif
surf = SDL_CreateRGBSurface(0, bmp.width, bmp.height, 24, rmask, gmask, bmask, amask);
if(surf == NULL)
{
eprintf("Error: Failed to create SDL_Surface: %s\n", SDL_GetError());
return NULL;
}
int numPixels = bmp.imgData.len;
unsigned char* px = malloc(numPixels*3);
surf->pixels = px;
for(int i=0; i<numPixels; ++i)
{
RGB p = bmp.palette[bmp.imgData.data[i]];
px[0] = p.r;
px[1] = p.g;
px[2] = p.b;
px += 3;
}
return surf;
}
static int endsWithCaseless(const char* str, const char* end)
{
int strLen = strlen(str);
int endLen = strlen(end);
return (strcasecmp(str+strLen-endLen, end) == 0);
}
static void printPalette(IndexedBitmap* bmp)
{
for(int i=0; i<256; ++i)
{
RGB rgb = bmp->palette[i];
printf("(%.3d %.3d %.3d) ", (int)rgb.r, (int)rgb.g, (int)rgb.b);
}
printf("\n");
}
#if 0
int main(int argc, char** argv)
{
const char* path = NULL;
if(argc > 1) path = argv[1];
else
{
eprintf("Usage: %s /path/to/unp_paks/pak1/textures/e1m1\n", argv[0]);
return 1;
}
DIR* dir = opendir(path);
if(dir == NULL)
{
eprintf("Opening given dir '%s' failed!\n", path);
return 1;
}
if(chdir(path) != 0)
{
eprintf("Switching to dir '%s' failed!\n", path);
return 1;
}
Buffer buf = loadFile("colormap.bmp");
if(buf.len < 54)
{
eprintf("Couldn't open%s colormap.bmp at %s - are you sure it's a texture dir?\n", (buf.len>=0) ? " valid" : "", path);
return 1;
}
IndexedBitmap colormapBmp = parseIndexedBMP(buf, 1);
if(colormapBmp.width < 0)
{
eprintf("Parsing colormap.bmp failed!\n");
return 1;
}
freeBuffer(&buf);
struct dirent* e = readdir(dir);
while(e != NULL)
{
if(endsWithCaseless(e->d_name, ".wal"))
{
buf = loadFile(e->d_name);
if(buf.len < 0)
{
eprintf("Warning: Failed to open '%s'!\n", e->d_name);
}
else
{
IndexedBitmap wal = parseWal(buf, 1);
if(wal.width < 0)
{
printf("## Error: Failed to parse '%s/%s'!\n", path, e->d_name);
}
else
{
if(memcmp(colormapBmp.palette, wal.palette, colormapBmp.paletteSize*sizeof(RGB)) != 0)
{
printf("## Warning: %s/%s has colormap different to colormap.bmp!\n", path, e->d_name);
/*printPalette(&colormapBmp);
printPalette(&wal);
printf("\n");*/
}
else
{
//printf("'%s' is cool\n", e->d_name);
}
}
}
freeBuffer(&buf);
}
e = readdir(dir);
}
closedir(dir);
return 0;
}
#else
int main(int argc, char** argv)
{
const char* filename = NULL;
if(argc > 1) filename = argv[1];
else
{
eprintf("Usage: %s bla.bmp\n", argv[0]);
return 1;
}
int filenameLen = strlen(filename);
Buffer bmpBuf = loadFile(filename);
IndexedBitmap bmp;
if(strcasecmp(filename+filenameLen-4, ".bmp") == 0)
{
bmp = parseIndexedBMP(bmpBuf, 0);
}
else if(strcasecmp(filename+filenameLen-4, ".wal") == 0)
{
bmp = parseWal(bmpBuf, 0);
}
else
{
eprintf("Unsupported filetype %s, only .wal and .bmp supported!\n", filename);
freeBuffer(&bmpBuf);
return 1;
}
freeBuffer(&bmpBuf);
if(bmp.width > 0)
{
SDL_Surface* surf = bmpToSurf(bmp);
if(surf != NULL)
{
displayImage(surf);
}
}
freeBuffer(&bmp.imgData);
return 0;
}
#endif // 1/0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment