Last active
April 4, 2016 01:28
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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