Last active
December 17, 2023 06:06
-
-
Save DanielGibson/3b030d69baddd6c40529bd80f4c0ff8a to your computer and use it in GitHub Desktop.
Tool that converts Quake2 .wal to .png (needs stb_image_write.h), most probably won't work on Windows without some changes
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
// the Quake2 standard colormap/palette | |
static unsigned char colormap[256][3] = { | |
{0, 0, 0}, {15, 15, 15}, {31, 31, 31}, {47, 47, 47}, {63, 63, 63}, {75, 75, 75}, | |
{91, 91, 91}, {107, 107, 107}, {123, 123, 123}, {139, 139, 139}, {155, 155, 155}, {171, 171, 171}, | |
{187, 187, 187}, {203, 203, 203}, {219, 219, 219}, {235, 235, 235}, {99, 75, 35}, {91, 67, 31}, | |
{83, 63, 31}, {79, 59, 27}, {71, 55, 27}, {63, 47, 23}, {59, 43, 23}, {51, 39, 19}, | |
{47, 35, 19}, {43, 31, 19}, {39, 27, 15}, {35, 23, 15}, {27, 19, 11}, {23, 15, 11}, | |
{19, 15, 7}, {15, 11, 7}, {95, 95, 111}, {91, 91, 103}, {91, 83, 95}, {87, 79, 91}, | |
{83, 75, 83}, {79, 71, 75}, {71, 63, 67}, {63, 59, 59}, {59, 55, 55}, {51, 47, 47}, | |
{47, 43, 43}, {39, 39, 39}, {35, 35, 35}, {27, 27, 27}, {23, 23, 23}, {19, 19, 19}, | |
{143, 119, 83}, {123, 99, 67}, {115, 91, 59}, {103, 79, 47}, {207, 151, 75}, {167, 123, 59}, | |
{139, 103, 47}, {111, 83, 39}, {235, 159, 39}, {203, 139, 35}, {175, 119, 31}, {147, 99, 27}, | |
{119, 79, 23}, {91, 59, 15}, {63, 39, 11}, {35, 23, 7}, {167, 59, 43}, {159, 47, 35}, | |
{151, 43, 27}, {139, 39, 19}, {127, 31, 15}, {115, 23, 11}, {103, 23, 7}, {87, 19, 0}, | |
{75, 15, 0}, {67, 15, 0}, {59, 15, 0}, {51, 11, 0}, {43, 11, 0}, {35, 11, 0}, | |
{27, 7, 0}, {19, 7, 0}, {123, 95, 75}, {115, 87, 67}, {107, 83, 63}, {103, 79, 59}, | |
{95, 71, 55}, {87, 67, 51}, {83, 63, 47}, {75, 55, 43}, {67, 51, 39}, {63, 47, 35}, | |
{55, 39, 27}, {47, 35, 23}, {39, 27, 19}, {31, 23, 15}, {23, 15, 11}, {15, 11, 7}, | |
{111, 59, 23}, {95, 55, 23}, {83, 47, 23}, {67, 43, 23}, {55, 35, 19}, {39, 27, 15}, | |
{27, 19, 11}, {15, 11, 7}, {179, 91, 79}, {191, 123, 111}, {203, 155, 147}, {215, 187, 183}, | |
{203, 215, 223}, {179, 199, 211}, {159, 183, 195}, {135, 167, 183}, {115, 151, 167}, {91, 135, 155}, | |
{71, 119, 139}, {47, 103, 127}, {23, 83, 111}, {19, 75, 103}, {15, 67, 91}, {11, 63, 83}, | |
{7, 55, 75}, {7, 47, 63}, {7, 39, 51}, {0, 31, 43}, {0, 23, 31}, {0, 15, 19}, | |
{0, 7, 11}, {0, 0, 0}, {139, 87, 87}, {131, 79, 79}, {123, 71, 71}, {115, 67, 67}, | |
{107, 59, 59}, {99, 51, 51}, {91, 47, 47}, {87, 43, 43}, {75, 35, 35}, {63, 31, 31}, | |
{51, 27, 27}, {43, 19, 19}, {31, 15, 15}, {19, 11, 11}, {11, 7, 7}, {0, 0, 0}, | |
{151, 159, 123}, {143, 151, 115}, {135, 139, 107}, {127, 131, 99}, {119, 123, 95}, {115, 115, 87}, | |
{107, 107, 79}, {99, 99, 71}, {91, 91, 67}, {79, 79, 59}, {67, 67, 51}, {55, 55, 43}, | |
{47, 47, 35}, {35, 35, 27}, {23, 23, 19}, {15, 15, 11}, {159, 75, 63}, {147, 67, 55}, | |
{139, 59, 47}, {127, 55, 39}, {119, 47, 35}, {107, 43, 27}, {99, 35, 23}, {87, 31, 19}, | |
{79, 27, 15}, {67, 23, 11}, {55, 19, 11}, {43, 15, 7}, {31, 11, 7}, {23, 7, 0}, | |
{11, 0, 0}, {0, 0, 0}, {119, 123, 207}, {111, 115, 195}, {103, 107, 183}, {99, 99, 167}, | |
{91, 91, 155}, {83, 87, 143}, {75, 79, 127}, {71, 71, 115}, {63, 63, 103}, {55, 55, 87}, | |
{47, 47, 75}, {39, 39, 63}, {35, 31, 47}, {27, 23, 35}, {19, 15, 23}, {11, 7, 7}, | |
{155, 171, 123}, {143, 159, 111}, {135, 151, 99}, {123, 139, 87}, {115, 131, 75}, {103, 119, 67}, | |
{95, 111, 59}, {87, 103, 51}, {75, 91, 39}, {63, 79, 27}, {55, 67, 19}, {47, 59, 11}, | |
{35, 47, 7}, {27, 35, 0}, {19, 23, 0}, {11, 15, 0}, {0, 255, 0}, {35, 231, 15}, | |
{63, 211, 27}, {83, 187, 39}, {95, 167, 47}, {95, 143, 51}, {95, 123, 51}, {255, 255, 255}, | |
{255, 255, 211}, {255, 255, 167}, {255, 255, 127}, {255, 255, 83}, {255, 255, 39}, {255, 235, 31}, | |
{255, 215, 23}, {255, 191, 15}, {255, 171, 7}, {255, 147, 0}, {239, 127, 0}, {227, 107, 0}, | |
{211, 87, 0}, {199, 71, 0}, {183, 59, 0}, {171, 43, 0}, {155, 31, 0}, {143, 23, 0}, | |
{127, 15, 0}, {115, 7, 0}, {95, 0, 0}, {71, 0, 0}, {47, 0, 0}, {27, 0, 0}, | |
{239, 0, 0}, {55, 55, 255}, {255, 0, 0}, {0, 0, 255}, {43, 43, 35}, {27, 27, 23}, | |
{19, 19, 15}, {235, 151, 127}, {195, 115, 83}, {159, 87, 51}, {123, 63, 27}, {235, 211, 199}, | |
{199, 171, 155}, {167, 139, 119}, {135, 107, 87}, {159, 91, 83} | |
}; |
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
// (C) 2023 Daniel Gibson | |
#include <errno.h> | |
#include <limits.h> | |
#include <stdbool.h> | |
#include <stdint.h> | |
#include <stdio.h> | |
#include <sys/types.h> | |
#include <sys/stat.h> | |
#include <unistd.h> | |
#include "colormap.h" | |
#define STB_IMAGE_WRITE_IMPLEMENTATION | |
#include "stb_image_write.h" | |
#define eprintf(...) fprintf(stderr, __VA_ARGS__) | |
typedef struct Image { | |
int width, height; | |
int stride, numColorComponents; | |
unsigned char* data; | |
} Image; | |
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); | |
uint32_t y = getInt16(data+2); | |
return x | (y << 16); | |
} | |
/* 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 | |
Quake2 .wal header: | |
0: char name[32]; | |
32: unsigned width, height; | |
40: unsigned offsets[MIPLEVELS = 4]; | |
56: char animname[32]; // next frame in animation chain | |
88: int flags; | |
92: int contents; | |
96: int value; | |
=> size 100 | |
*/ | |
static Image parseWal(const unsigned char* data, const size_t len) | |
{ | |
Image ret = {0}; | |
ret.width = ret.height = ret.stride = -1; | |
if(len < 100) // that's the size of the Q2 WAL header | |
{ | |
eprintf("Too small for a valid Q2 .wal!\n"); | |
return ret; | |
} | |
const unsigned char* d = data; | |
size_t w = getInt32(d+32); | |
size_t h = getInt32(d+36); | |
uint32_t offset = getInt32(d+40); // only use miplevel 0 | |
size_t dataSize = w*h; | |
if(len < offset+dataSize) | |
{ | |
eprintf(".wal malformed, not long enough to actually contain image data!\n"); | |
return ret; | |
} | |
unsigned char* buf = malloc(dataSize*3); | |
if(buf == NULL) | |
{ | |
eprintf("Couldn't allocate %zd bytes of data - OOM?!\n", dataSize); | |
return ret; | |
} | |
const unsigned char* pix = data+offset; | |
for(size_t i=0; i<dataSize; ++i) | |
{ | |
unsigned char* color = colormap[pix[i]]; | |
unsigned char* outpix = buf + i*3; | |
outpix[0] = color[0]; | |
outpix[1] = color[1]; | |
outpix[2] = color[2]; | |
} | |
ret.width = w; | |
ret.height = h; | |
ret.stride = w*3; | |
ret.numColorComponents = 3; | |
ret.data = buf; | |
return ret; | |
} | |
static unsigned char* loadFile(const char* filename, int* len) | |
{ | |
*len = -1; | |
FILE* f = fopen(filename, "rb"); | |
if(f == NULL) | |
{ | |
eprintf("Failed to open '%s' !\n", filename); | |
return NULL; | |
} | |
struct stat statbuf = {0}; | |
if(fstat(fileno(f), &statbuf) != 0) | |
{ | |
eprintf("Can't get filesize of '%s'\n", filename); | |
fclose(f); | |
return NULL; | |
} | |
if(statbuf.st_size > INT_MAX) | |
{ | |
eprintf("'%s' is too big!\n", filename); | |
fclose(f); | |
return NULL; | |
} | |
unsigned char* ret = malloc(statbuf.st_size); | |
if(ret == NULL) | |
{ | |
eprintf("Couldn't allocate %lld bytes, OOM?!\n", (long long)statbuf.st_size); | |
fclose(f); | |
return NULL; | |
} | |
if(fread(ret, statbuf.st_size, 1, f) != 1) | |
{ | |
eprintf("Couldn't read %s: %d (%s)\n", filename, errno, strerror(errno)); | |
free(ret); | |
fclose(f); | |
return NULL; | |
} | |
*len = statbuf.st_size; | |
fclose(f); | |
return ret; | |
} | |
static bool checkAndCreateDir(const char* outdir) | |
{ | |
struct stat statbuf = {0}; | |
int staterr = 0; | |
if(stat(outdir, &statbuf) != 0) | |
{ | |
staterr = errno; | |
if(staterr != ENOENT) | |
{ | |
eprintf("Error while stat-ing output directory '%s': %d (%s)\n", outdir, errno, strerror(errno)); | |
return false; | |
} | |
} | |
if(staterr) | |
{ | |
// directory doesn't exist yet, create it, recursively | |
char* path = strdup(outdir); | |
char* dirnamestart = path; | |
if(dirnamestart[0] == '/') | |
++dirnamestart; | |
else if(dirnamestart[0] == '.' && dirnamestart[1] == '/') | |
dirnamestart += 2; | |
// if path is /path/to/foo/bar, start with /path, then /path/to, then /path/to/foo | |
// and eventually, after the loop, /path/to/foo/bar | |
// in each iteration make sure that the current path part exists before checking/creating the next part | |
for( char* next = strchr(dirnamestart, '/'); next != NULL; next = strchr(next+1, '/') ) | |
{ | |
// cut off after current directory name | |
*next = '\0'; | |
staterr = 0; | |
if(stat(path, &statbuf) != 0) | |
{ | |
staterr = errno; | |
if(staterr != ENOENT) | |
{ | |
eprintf("Error while stat-ing part of output directory '%s': %d (%s)\n", path, errno, strerror(errno)); | |
return false; | |
} | |
} | |
if(staterr == ENOENT && mkdir(path, 0755) != 0) | |
{ | |
eprintf("Couldn't create (part of) outputdirectory '%s': %d (%s)\n", path, errno, strerror(errno)); | |
free(path); | |
return false; | |
} // else do nothing, this directory already exists | |
*next = '/'; // restore / | |
next = strchr(next+1, '/'); | |
} | |
// create the last path component | |
if(mkdir(outdir, 0755) != 0) | |
{ | |
eprintf("Couldn't create (part of) outputdirectory '%s': %d (%s)\n", path, errno, strerror(errno)); | |
free(path); | |
return false; | |
} | |
free(path); | |
} | |
else if((statbuf.st_mode & S_IFMT) != S_IFDIR) | |
{ | |
eprintf("Given output directory '%s' exists, but is no directory!\n", outdir); | |
return false; | |
} | |
return true; | |
} | |
int main(int argc, char** argv) | |
{ | |
if(argc < 3) | |
{ | |
eprintf("Missing arguments!\n"); | |
eprintf("Usage: %s <output_dir> <input_wal>\n", argv[0]); | |
eprintf(" /path/to/foo.wal will be converted to <output_dir>/foo.png\n"); | |
eprintf(" <output_dir> will be created recursively, if it doesn't exist\n"); | |
return 1; | |
} | |
const char* outdir = argv[1]; | |
const char* walpath = argv[2]; | |
if(!checkAndCreateDir(outdir)) | |
return 1; // we already printed an error | |
int wallen; | |
unsigned char* walbuf = loadFile(walpath, &wallen); | |
if(walbuf == NULL) | |
return 1; | |
Image img = parseWal(walbuf, wallen); | |
free(walbuf); | |
walbuf = NULL; | |
if(img.data == NULL) | |
return 1; | |
// get only the filename from the path to the .wal | |
const char* walname = strrchr(walpath, '/'); | |
if(walname == NULL) | |
walname = walpath; | |
else | |
++walname; // skip '/' | |
char pngpath[4096]; | |
snprintf(pngpath, sizeof(pngpath), "%s/%s", outdir, walname); | |
char* fileending = strrchr(pngpath, '.'); | |
if(fileending == NULL) | |
{ | |
eprintf("Why did the input .wal file path not contain a '.' ?!\n"); | |
free(img.data); | |
return 1; | |
} | |
memcpy(fileending+1, "png", 4); // replace "wal" (or "WAL" or whatever) with "png", incl. terminating \0 | |
if(!stbi_write_png(pngpath, img.width, img.height, img.numColorComponents, img.data, img.stride)) | |
{ | |
eprintf("Writing PNG failed!\n"); | |
free(img.data); | |
return 1; | |
} | |
free(img.data); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment