fteqw/imgtool.c
Spoike 9033f7b237 fixed eztv md4 incompatibility.
reimplemented qtvreverse command.
fixed some stuffcmds being handled by the wrong splitscreen seats (was noticable in TF).
rework smartjump to try to be more predictable...
rework relighting to try to be more robust (and more self-contained).
allow the csqc to actually use VF_PROJECTIONOFFSET.
jump now moves upwards instead of trying to lock on to a nearby player when spectating.
assume 32 fullbright pixels when running with a palette.lmp yet no colormap.lmp (happens with some total conversions).
tweaked scoreboard for fainter backgrounds.
rearranged autoid, to be smaller etc.
hacked around dodgy conchars.lmp - don't treat 128*128 qpics as qpics to work around workarounds for buggy wad tools (with a warning).
fixed missing fullbrights on h2holey models.
avoided warning about mod_h2holey_bugged on dedicated servers.
added net_ice_exchangeprivateips, for people worried about exposing lan IPs when using ICE.
sv_public 2: implemented client support for our webrtc broker in order to use our own ICE implementation without needing to faff around with irc accounts or plugins etc.
TODO: ensure at least one ephemerial udp port when using ice or come up with some better sv_port handling
fixed multiple tls bugs (one could cause server problems). change net_enable_tls to disabled by default anyway (reenable for the server to be able to respond to https/wss/tls schemes again).
don't colourmap when there appears to be a highres diffusemap on q1 models.
imgtool now understands exporting from qpics in wads, as well as just mips.
implemented speed-o-meter in ezhud.
added removeinstant builtin to avoid the half-second rule.


git-svn-id: https://svn.code.sf.net/p/fteqw/code/trunk@5614 fc73d0e0-1445-4013-8a0c-d673dee63da5
2020-02-11 18:06:10 +00:00

1835 lines
56 KiB
C

#include "quakedef.h"
#include "shader.h"
#undef stderr
#define stderr stdout
#define LittleLong(s) s
#include <limits.h>
#include <ctype.h>
#ifdef _WIN32
#include <windows.h>
#endif
void VARGS Sys_Error (const char *fmt, ...)
{
va_list argptr;
va_start (argptr,fmt);
vfprintf (stderr,fmt,argptr);
va_end (argptr);
fflush(stderr);
exit(1);
}
void VARGS Con_Printf (const char *fmt, ...)
{
va_list argptr;
va_start (argptr,fmt);
vfprintf (stderr,fmt,argptr);
va_end (argptr);
fflush(stderr);
}
void VARGS Con_DPrintf (const char *fmt, ...)
{
va_list argptr;
va_start (argptr,fmt);
vfprintf (stderr,fmt,argptr);
va_end (argptr);
fflush(stderr);
}
void VARGS Con_ThrottlePrintf (float *timer, int developerlevel, const char *fmt, ...)
{
va_list argptr;
va_start (argptr,fmt);
vfprintf (stderr,fmt,argptr);
va_end (argptr);
fflush(stderr);
}
void *ZF_Malloc(size_t size)
{
#if defined(__linux__)
void *ret = NULL;
if (!posix_memalign(&ret, max(sizeof(float)*4, sizeof(void*)), size))
memset(ret, 0, size);
return ret;
#else
return calloc(size, 1);
#endif
}
void *Z_Malloc(size_t size)
{
void *r = ZF_Malloc(size);
if (!r)
exit(1);
return r;
}
void *BZ_Malloc(size_t size)
{
return Z_Malloc(size);
}
void *BZF_Malloc(size_t size)
{
return Z_Malloc(size);
}
void BZ_Free(void *p)
{
free(p);
}
void Z_Free(void *p)
{
free(p);
}
#include <sys/stat.h>
void FS_CreatePath(const char *pname, enum fs_relative relativeto)
{
char *t = strdup(pname), *sl = t;
while ((sl=strchr(sl, '/')))
{
*sl=0;
#ifdef _WIN32
CreateDirectoryA(t, NULL);
#else
mkdir(t, 0777);
#endif
*sl++='/';
}
free(t);
}
qboolean FS_Remove (const char *path, enum fs_relative relativeto)
{
//remove is part of c89.
if (remove(path) == -1)
return false;
return true;
}
qboolean FS_NativePath(const char *fname, enum fs_relative relativeto, char *out, int outlen)
{
Q_strncpyz(out, fname, outlen);
return true;
}
#ifdef __unix__
#include <sys/stat.h>
#endif
qbyte *FS_LoadMallocFile (const char *path, size_t *fsize)
{
qbyte *data = NULL;
FILE *f;
#ifdef __unix__
struct stat sb;
if (stat(path, &sb) < 0)
return NULL;
if ((sb.st_mode&S_IFMT) != S_IFREG)
return NULL;
#endif
f = fopen(path, "rb");
if (f)
{
long int sz;
if (fseek(f, 0, SEEK_END) >= 0)
{
sz = ftell(f);
if (sz >= 0)
{
*fsize = sz;
fseek(f, 0, SEEK_SET);
data = ZF_Malloc(*fsize+1);
if (data)
{
data[*fsize] = 0;
fread(data, 1, *fsize, f);
}
else
Con_Printf("Unable to allocate memory for %s\n", path);
}
}
fclose(f);
}
return data;
}
#ifdef _WIN32
dllhandle_t *Sys_LoadLibrary(const char *name, dllfunction_t *funcs)
{
return NULL;
}
#else
#include <dlfcn.h>
void Sys_CloseLibrary(dllhandle_t *lib)
{
dlclose((void*)lib);
}
dllhandle_t *Sys_LoadLibrary(const char *name, dllfunction_t *funcs)
{
int i;
dllhandle_t *lib;
lib = NULL;
if (!lib)
lib = dlopen (name, RTLD_LOCAL|RTLD_LAZY);
// if (!lib && !strstr(name, ".so"))
// lib = dlopen (va("%s.so", name), RTLD_LOCAL|RTLD_LAZY);
if (!lib)
{
// Con_DPrintf("%s\n", dlerror());
return NULL;
}
if (funcs)
{
for (i = 0; funcs[i].name; i++)
{
*funcs[i].funcptr = dlsym(lib, funcs[i].name);
if (!*funcs[i].funcptr)
break;
}
if (funcs[i].name)
{
Con_DPrintf("Unable to find symbol \"%s\" in \"%s\"\n", funcs[i].name, name);
Sys_CloseLibrary((dllhandle_t*)lib);
lib = NULL;
}
}
return (dllhandle_t*)lib;
}
#endif
struct imgfile_s
{
vfsfile_t pub;
FILE *f;
};
static qboolean QDECL ImgFile_Close(struct vfsfile_s *file)
{
struct imgfile_s *f = (struct imgfile_s*)file;
fclose(f->f);
free(f);
return true;
}
static int QDECL ImgFile_WriteBytes(struct vfsfile_s *file, const void *buffer, int bytestowrite)
{
struct imgfile_s *f = (struct imgfile_s*)file;
return fwrite(buffer, 1, bytestowrite, f->f);
}
static qboolean QDECL ImgFile_Seek(struct vfsfile_s *file, qofs_t newofs)
{
struct imgfile_s *f = (struct imgfile_s*)file;
if (fseek(f->f, newofs, SEEK_SET)==0)
return true; //success
return false;
}
static qofs_t QDECL ImgFile_Tell(struct vfsfile_s *file)
{
struct imgfile_s *f = (struct imgfile_s*)file;
return ftell(f->f);
}
vfsfile_t *QDECL FS_OpenVFS(const char *filename, const char *mode, enum fs_relative relativeto)
{
if (!strcmp(mode, "wb"))
{
struct imgfile_s *r = malloc(sizeof(*r));
r->f = fopen(filename, mode);
r->pub.seekstyle = SS_UNSEEKABLE;
r->pub.Close = ImgFile_Close;
r->pub.WriteBytes = ImgFile_WriteBytes;
r->pub.Seek = ImgFile_Seek;
r->pub.Tell = ImgFile_Tell;
if (r->f)
return &r->pub;
free(r);
}
return NULL;
}
qboolean COM_WriteFile (const char *filename, enum fs_relative fsroot, const void *data, int len)
{
return false;
}
void QDECL Q_strncpyz(char *d, const char *s, int n)
{
int i;
n--;
if (n < 0)
return; //this could be an error
for (i=0; *s; i++)
{
if (i == n)
break;
*d++ = *s++;
}
*d='\0';
}
void VARGS Q_vsnprintfz (char *dest, size_t size, const char *fmt, va_list argptr)
{
#ifdef _WIN32
#undef _vsnprintf
_vsnprintf (dest, size, fmt, argptr);
#define _vsnprintf unsafe_vsnprintf
#else
#ifdef _DEBUG
if ((size_t)vsnprintf (dest, size, fmt, argptr) > size-1)
Sys_Error("Q_vsnprintfz: truncation");
#else
vsnprintf (dest, size, fmt, argptr);
#endif
#endif
dest[size-1] = 0;
}
void VARGS Q_snprintfz (char *dest, size_t size, const char *fmt, ...)
{
va_list argptr;
va_start (argptr, fmt);
Q_vsnprintfz(dest, size, fmt, argptr);
va_end (argptr);
}
//palette data is used in lmps, as well as written into pcxes or wads, probably some other things.
qbyte *host_basepal;
unsigned int d_8to24rgbtable[256];
unsigned int d_8to24bgrtable[256];
static qbyte default_quakepal[768] =
{ //the quake palette was released into the public domain (or at least gpl) to ease development of tools writing quake-format data.
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,15,11,7,23,15,11,31,23,11,39,27,15,47,35,19,55,43,23,63,47,23,75,55,27,83,59,27,91,67,31,99,75,31,107,83,31,115,87,31,123,95,35,131,103,35,143,111,35,11,11,15,19,19,27,27,27,39,39,39,51,47,47,63,55,55,75,63,63,87,71,71,103,79,79,115,91,91,127,99,99,
139,107,107,151,115,115,163,123,123,175,131,131,187,139,139,203,0,0,0,7,7,0,11,11,0,19,19,0,27,27,0,35,35,0,43,43,7,47,47,7,55,55,7,63,63,7,71,71,7,75,75,11,83,83,11,91,91,11,99,99,11,107,107,15,7,0,0,15,0,0,23,0,0,31,0,0,39,0,0,47,0,0,55,0,0,63,0,0,71,0,0,79,0,0,87,0,0,95,0,0,103,0,0,111,0,0,119,0,0,127,0,0,19,19,0,27,27,0,35,35,0,47,43,0,55,47,0,67,
55,0,75,59,7,87,67,7,95,71,7,107,75,11,119,83,15,131,87,19,139,91,19,151,95,27,163,99,31,175,103,35,35,19,7,47,23,11,59,31,15,75,35,19,87,43,23,99,47,31,115,55,35,127,59,43,143,67,51,159,79,51,175,99,47,191,119,47,207,143,43,223,171,39,239,203,31,255,243,27,11,7,0,27,19,0,43,35,15,55,43,19,71,51,27,83,55,35,99,63,43,111,71,51,127,83,63,139,95,71,155,107,83,167,123,95,183,135,107,195,147,123,211,163,139,227,179,151,
171,139,163,159,127,151,147,115,135,139,103,123,127,91,111,119,83,99,107,75,87,95,63,75,87,55,67,75,47,55,67,39,47,55,31,35,43,23,27,35,19,19,23,11,11,15,7,7,187,115,159,175,107,143,163,95,131,151,87,119,139,79,107,127,75,95,115,67,83,107,59,75,95,51,63,83,43,55,71,35,43,59,31,35,47,23,27,35,19,19,23,11,11,15,7,7,219,195,187,203,179,167,191,163,155,175,151,139,163,135,123,151,123,111,135,111,95,123,99,83,107,87,71,95,75,59,83,63,
51,67,51,39,55,43,31,39,31,23,27,19,15,15,11,7,111,131,123,103,123,111,95,115,103,87,107,95,79,99,87,71,91,79,63,83,71,55,75,63,47,67,55,43,59,47,35,51,39,31,43,31,23,35,23,15,27,19,11,19,11,7,11,7,255,243,27,239,223,23,219,203,19,203,183,15,187,167,15,171,151,11,155,131,7,139,115,7,123,99,7,107,83,0,91,71,0,75,55,0,59,43,0,43,31,0,27,15,0,11,7,0,0,0,255,11,11,239,19,19,223,27,27,207,35,35,191,43,
43,175,47,47,159,47,47,143,47,47,127,47,47,111,47,47,95,43,43,79,35,35,63,27,27,47,19,19,31,11,11,15,43,0,0,59,0,0,75,7,0,95,7,0,111,15,0,127,23,7,147,31,7,163,39,11,183,51,15,195,75,27,207,99,43,219,127,59,227,151,79,231,171,95,239,191,119,247,211,139,167,123,59,183,155,55,199,195,55,231,227,87,127,191,255,171,231,255,215,255,255,103,0,0,139,0,0,179,0,0,215,0,0,255,0,0,255,243,147,255,247,199,255,255,255,159,91,83
};
qbyte GetPaletteIndexNoFB(int red, int green, int blue)
{
int i;
int best=0;
int bestdist=INT_MAX;
int dist;
for (i = 0; i < 256-32; i++)
{
dist =
abs(host_basepal[i*3+0]-red)+
abs(host_basepal[i*3+1]-green)+
abs(host_basepal[i*3+2]-blue);
if (dist < bestdist)
{
bestdist = dist;
best = i;
}
}
return best;
}
static void ImgTool_SetupPalette(void)
{
int i;
//we ought to try to read gfx/palette.lmp, but its probably in a pak
host_basepal = default_quakepal;
for (i = 0; i < 256; i++)
{
d_8to24rgbtable[i] = (host_basepal[i*3+0]<<0)|(host_basepal[i*3+1]<<8)|(host_basepal[i*3+2]<<16);
d_8to24bgrtable[i] = (host_basepal[i*3+0]<<16)|(host_basepal[i*3+1]<<8)|(host_basepal[i*3+2]<<0);
}
}
static void ImgTool_FreeMips(struct pendingtextureinfo *mips)
{
size_t i;
if (mips)
{
for (i = 0; i < mips->mipcount; i++)
if (mips->mip[i].needfree)
BZ_Free(mips->mip[i].data);
if (mips->extrafree)
BZ_Free(mips->extrafree);
BZ_Free(mips);
}
}
sh_config_t sh_config;
viddef_t vid;
typedef struct
{
unsigned int offset; // Position of the entry in WAD
unsigned int dsize; // Size of the entry in WAD file
unsigned int size; // Size of the entry in memory
char type; // type of entry
char cmprs; // Compression. 0 if none.
short dummy; // Not used
char name[16]; // we use only first 8
} wad2entry_t;
typedef struct
{
char magic[4]; //should be WAD2
unsigned int num; //number of entries
unsigned int offset; //location of directory
} wad2_t;
static const char *imagetypename[] = {"2D", "3D", "Cube", "2DArray", "CubemapArray", "INVALID", "INVALID", "INVALID", "INVALID", "INVALID"};
struct opts_s
{
int textype;
const char *defaultext; //.dds or whatever when the output's extension is not explicitly given.
unsigned int flags; //image flags to use (affects how textures get interpreted a little)
unsigned int mipnum; //when exporting to a mipless format, this is the mip level that is actually written. default 0.
uploadfmt_t newpixelformat; //try to convert to this pixel format on export.
};
void Image_GenerateMips(struct pendingtextureinfo *mips, unsigned int flags);
int Image_WritePNG (const char *filename, enum fs_relative fsroot, int compression, void **buffers, int numbuffers, qintptr_t bufferstride, int width, int height, enum uploadfmt fmt, qboolean writemetadata);
qboolean WriteTGA(const char *filename, enum fs_relative fsroot, const qbyte *fte_restrict rgb_buffer, qintptr_t bytestride, int width, int height, enum uploadfmt fmt);
static enum uploadfmt ImgTool_ASTCToLDR(uploadfmt_t fmt)
{
if (fmt >= PTI_ASTC_FIRST && fmt <= PTI_ASTC_LAST)
{
if (fmt >= PTI_ASTC_4X4_HDR)
return (fmt-PTI_ASTC_4X4_HDR)+PTI_ASTC_4X4_LDR;
if (fmt >= PTI_ASTC_4X4_SRGB)
return (fmt-PTI_ASTC_4X4_SRGB)+PTI_ASTC_4X4_LDR;
}
if (fmt == PTI_BC1_RGB)
return PTI_BC1_RGBA;
return fmt;
}
#ifdef _WIN32
static void FS_MakeTempName(char *out, size_t outsize, char *prefix, char *suffix)
{
static char temp_path[MAX_PATH];
char temp_file_name[MAX_PATH];
if (!*temp_path && !GetTempPathA(sizeof(temp_path), temp_path))
Sys_Error("FS_MakeTempName failed to get temp path\n");
if (!GetTempFileNameA(temp_path, prefix, 0, temp_file_name))
Sys_Error("FS_MakeTempName failed\n");
Q_snprintfz(out, outsize, "%s%s", temp_file_name, suffix);
}
#else
#include <unistd.h>
static void FS_MakeTempName(char *out, size_t outsize, char *prefix, char *suffix)
{
snprintf(out, outsize, "/tmp/%sXXXXXX%s", prefix, suffix);
close(mkstemps(out, strlen(suffix))); //bsd4.3/posix1-2001
}
#endif
static qboolean ImgTool_HasAlpha(struct pendingtextureinfo *mips)
{
if (mips->encoding == PTI_RGBA8 || mips->encoding == PTI_BGRA8 || mips->encoding == PTI_LLLA8 || mips->encoding == PTI_RGBA8_SRGB || mips->encoding == PTI_BGRA8_SRGB)
{
size_t l = 0, pixels, p;
qbyte *d;
for (l = 0; l < mips->mipcount; l++)
{
pixels = mips->mip[l].width * mips->mip[l].height * mips->mip[l].depth * 4;
d = mips->mip[l].data;
d+=3;
for (p = 0; p < pixels; p+=4)
if (d[p] != 255)
return true; //a transparent pixel!
}
return false;
}
else if (mips->encoding == PTI_L8A8 || mips->encoding == PTI_L8A8_SRGB)
{
size_t l = 0, pixels, p;
qbyte *d;
for (l = 0; l < mips->mipcount; l++)
{
pixels = mips->mip[l].width * mips->mip[l].height * mips->mip[l].depth * 2;
d = mips->mip[l].data;
d+=1;
for (p = 0; p < pixels; p+=2)
if (d[p] != 255)
return true; //a transparent pixel!
}
return false;
}
else if (mips->encoding == PTI_RGBA16)
{
size_t l = 0, pixels, p;
unsigned short *d;
for (l = 0; l < mips->mipcount; l++)
{
pixels = mips->mip[l].width * mips->mip[l].height * mips->mip[l].depth * 4;
d = mips->mip[l].data;
d+=3;
for (p = 0; p < pixels; p+=4)
if (d[p] != 0xffff)
return true; //a transparent pixel!
}
return false;
}
else
return Image_FormatHasAlpha(mips->encoding);
}
static qboolean ImgTool_ConvertPixelFormat(struct opts_s *args, const char *inname, struct pendingtextureinfo *mips)
{
struct pendingtextureinfo tmp, *ret;
size_t m;
char raw[MAX_OSPATH];
char comp[MAX_OSPATH];
char command[MAX_OSPATH*3];
qbyte *fdata;
size_t fsize;
int bb,bw,bh;
qboolean canktx = false;
uploadfmt_t targfmt = args->newpixelformat;
int d,l, layers, r;
//force it to bc1 if bc2 or bc3 with no alpha channel.
if ((targfmt == PTI_BC2_RGBA || targfmt == PTI_BC3_RGBA) && !ImgTool_HasAlpha(mips))
targfmt = PTI_BC1_RGB;
if (targfmt >= PTI_ASTC_FIRST && targfmt <= PTI_ASTC_LAST)
{
Q_snprintfz(command, sizeof(command), "astcenc -c");
canktx = true;
}
else if (targfmt == PTI_BC1_RGB)
Q_snprintfz(command, sizeof(command), "nvcompress -bc1%s", (args->flags&IF_TRYBUMP)?"n":"");
else if (targfmt == PTI_BC1_RGB_SRGB)
Q_snprintfz(command, sizeof(command), "nvcompress -bc1%s -srgb -dds10", (args->flags&IF_TRYBUMP)?"n":"");
else if (targfmt == PTI_BC1_RGBA)
Q_snprintfz(command, sizeof(command), "nvcompress -bc1a");
else if (targfmt == PTI_BC1_RGBA_SRGB)
Q_snprintfz(command, sizeof(command), "nvcompress -bc1a -srgb -dds10");
else if (targfmt == PTI_BC2_RGBA)
Q_snprintfz(command, sizeof(command), "nvcompress -bc2");
else if (targfmt == PTI_BC2_RGBA_SRGB)
Q_snprintfz(command, sizeof(command), "nvcompress -bc2 -srgb -dds10");
else if (targfmt == PTI_BC3_RGBA)
Q_snprintfz(command, sizeof(command), "nvcompress -bc3%s", (args->flags&IF_TRYBUMP)?"n":"");
else if (targfmt == PTI_BC3_RGBA_SRGB)
Q_snprintfz(command, sizeof(command), "nvcompress -bc3%s -srgb -dds10", (args->flags&IF_TRYBUMP)?"n":"");
else if (targfmt == PTI_BC4_R8)
Q_snprintfz(command, sizeof(command), "nvcompress -bc4");
else if (targfmt == PTI_BC5_RG8)
Q_snprintfz(command, sizeof(command), "nvcompress -bc5");
else if (targfmt == PTI_BC6_RGB_SFLOAT || targfmt == PTI_BC6_RGB_UFLOAT)
Q_snprintfz(command, sizeof(command), "nvcompress -bc6");
else if (targfmt == PTI_BC7_RGBA)
Q_snprintfz(command, sizeof(command), "nvcompress -bc7");
else if (targfmt == PTI_BC7_RGBA_SRGB)
Q_snprintfz(command, sizeof(command), "nvcompress -bc7 -srgb");
else
{
if (mips->encoding != targfmt)
{
qboolean forceformats[PTI_MAX];
for (m = 0; m < PTI_MAX; m++)
forceformats[m] = (m == targfmt);
Image_ChangeFormat(mips, forceformats, PTI_INVALID, inname);
if (mips->encoding == targfmt)
return true;
//switch to common formats...
for (m = 0; m < PTI_MAX; m++)
forceformats[m] = (m == targfmt) || (m==PTI_RGBA8);
Image_ChangeFormat(mips, forceformats, PTI_INVALID, inname);
//and try again...
for (m = 0; m < PTI_MAX; m++)
forceformats[m] = (m == targfmt);
Image_ChangeFormat(mips, forceformats, PTI_INVALID, inname);
return (mips->encoding == targfmt);
}
return false;
}
if (canktx)
FS_MakeTempName(raw, sizeof(raw), "itr", ".ktx");
else
FS_MakeTempName(raw, sizeof(raw), "itr", ".png");
FS_MakeTempName(comp, sizeof(comp), "itc", ".ktx");
tmp.type = mips->type;
tmp.encoding = mips->encoding;
tmp.extrafree = NULL;
tmp.mipcount = 1;
Image_BlockSizeForEncoding(targfmt, &bb, &bw, &bh);
Q_snprintfz(command+strlen(command), sizeof(command)-strlen(command), " \"%s\" \"%s\"", raw, comp);
if (targfmt >= PTI_ASTC_FIRST && targfmt <= PTI_ASTC_LAST)
Q_snprintfz(command+strlen(command), sizeof(command)-strlen(command), " %ix%i -exhaustive", bw, bh);
if (targfmt >= PTI_ASTC_4X4_SRGB && targfmt <= PTI_ASTC_12X12_SRGB)
Q_strncatz(command, " -srgb", sizeof(command));
if (targfmt >= PTI_ASTC_4X4_HDR && targfmt <= PTI_ASTC_12X12_HDR)
Q_strncatz(command, " -hdr", sizeof(command));
if (targfmt >= PTI_BC1_RGB && targfmt <= PTI_BC7_RGBA_SRGB && (strstr(inname, "_n.")||strstr(inname, "_norm.")))
Q_strncatz(command, " -normal", sizeof(command)); //looks like a normalmap... tweak metrics to favour normalised results.
#ifdef _WIN32
Q_strncatz(command, "> NUL 2>&1", sizeof(command));
#else
Q_strncatz(command, ">> /dev/null", sizeof(command));
#endif
if (!canktx)
{
qboolean allowformats[PTI_MAX];
//make sure the source pixel format is acceptable if we're forced to write a png
for (m = 0; m < PTI_MAX; m++)
allowformats[m] =
(m == PTI_RGBA8) || (m == PTI_RGBX8) ||
(m == PTI_BGRA8) || (m == PTI_BGRX8) ||
(m == PTI_LLLA8) || (m == PTI_LLLX8) ||
(m == PTI_RGBA16) ||
(m == PTI_L8) || (m == PTI_L8A8) ||
/*(m == PTI_L16) ||*/
(m == PTI_BGR8) || (m == PTI_BGR8) ||
0;
Image_ChangeFormat(mips, allowformats, PTI_INVALID, inname);
}
Image_BlockSizeForEncoding(mips->encoding, &bb, &bw, &bh);
for (m = 0; m < mips->mipcount; m++)
{
qbyte *srcdata = mips->mip[m].data;
size_t srcsize = mips->mip[m].datasize;
if (mips->type == PTI_3D)
{
layers = 1;
d = mips->mip[m].depth;
tmp.type = PTI_2D;
}
else
{
layers = mips->mip[m].depth;
d = 1;
tmp.type = PTI_2D;
}
for (l = 0; l < layers; l++)
{
Con_DPrintf("Compressing %s mip %u, layer %u\n", inname, (unsigned)m, l);
tmp.mip[0] = mips->mip[m];
tmp.mip[0].needfree = false;
tmp.mip[0].depth = d;
tmp.mip[0].datasize = srcsize/layers;
tmp.mip[0].data = srcdata + l * tmp.mip[0].datasize;
(void)tmp;
if (canktx)
{
#ifdef IMAGEFMT_KTX
if (!Image_WriteKTXFile(raw, FS_SYSTEM, &tmp))
#endif
break;
}
else
{
#ifdef AVAIL_PNGLIB
if (!Image_WritePNG(raw, FS_SYSTEM, 0, &tmp.mip[0].data, 1, tmp.mip[0].width*bb, tmp.mip[0].width, tmp.mip[0].height, tmp.encoding, false))
#endif
break;
}
r = system(command);
if (r != EXIT_SUCCESS)
{
Con_Printf("The following system command failed with code %i: %s\n", r, command);
break;
}
fdata = FS_LoadMallocFile(comp, &fsize);
ret = Image_LoadMipsFromMemory(IF_NOMIPMAP, comp, comp, fdata, fsize);
if (ret && ret->mip[0].width == mips->mip[m].width &&
ret->mip[0].height == mips->mip[m].height &&
ret->mip[0].depth == d &&
ImgTool_ASTCToLDR(ret->encoding) == ImgTool_ASTCToLDR(targfmt))
{
if (layers == 1) //just copy it over. FIXME: memory leak
mips->mip[m] = ret->mip[0];
else
{
if (!l)
{
mips->mip[m].datasize = ret->mip[0].datasize * layers;
mips->mip[m].data = BZ_Malloc(mips->mip[m].datasize);
mips->mip[m].needfree = true;
}
else if (ret->mip[0].datasize != mips->mip[m].datasize/layers)
break; //erk..?
memcpy((qbyte*)mips->mip[m].data + l * ret->mip[0].datasize, ret->mip[0].data, ret->mip[0].datasize);
}
continue;
}
break;
}
if (l != layers)
break;
}
mips->encoding = targfmt;
mips->mipcount = m;
if (mips->mipcount && targfmt >= PTI_BC1_RGB && targfmt <= PTI_BC7_RGBA_SRGB)
{ //d3d has some annoying limitations.
//do not warn for astc files, their block sizes are too weird.
Image_BlockSizeForEncoding(targfmt, &bb, &bw, &bh);
if (mips->mip[0].width%bw || mips->mip[0].height%bh)
Con_Printf("%s: mip0 of %i*%i is not a multiple of %i*%i (d3d warning)\n", inname, mips->mip[0].width, mips->mip[0].height, bw, bh);
}
FS_Remove(raw, FS_SYSTEM);
FS_Remove(comp, FS_SYSTEM);
return true;
}
const char *COM_GetFileExtension (const char *in, const char *term)
{
const char *dot;
if (!term)
term = in + strlen(in);
for (dot = term-1; dot >= in && *dot != '/' && *dot != '\\'; dot--)
{
if (*dot == '.')
return dot;
}
return term+strlen(term);
}
static struct pendingtextureinfo *ImgTool_Read(struct opts_s *args, const char *inname)
{
qbyte *indata;
size_t fsize;
struct pendingtextureinfo *in;
indata = FS_LoadMallocFile(inname, &fsize);
if (!indata)
printf("%s: unable to read\n", inname);
else
{
in = Image_LoadMipsFromMemory(args->flags|IF_NOMIPMAP, inname, inname, indata, fsize);
if (!in)
{
printf("%s: unsupported format\n", inname);
BZ_Free(indata);
}
else
{
printf("%s: %s %s, %i*%i, %i mips\n", inname, imagetypename[in->type], Image_FormatName(in->encoding), in->mip[0].width, in->mip[0].height, in->mipcount);
return in;
}
}
return NULL;
}
static struct pendingtextureinfo *ImgTool_Combine(struct opts_s *args, const char **namelist, unsigned int filecount)
{
struct pendingtextureinfo *r, *t;
unsigned int i;
unsigned int layers = 0;
unsigned int j = 0;
struct
{
const char *fname;
struct pendingtextureinfo *in;
} *srcs, *tmpsrcs;
if (args->textype == PTI_3D)
args->flags |= IF_NOMIPMAP; //generate any mipmaps after...
srcs = alloca(sizeof(*srcs)*filecount);
for (i = 0, j = 0; i < filecount; i++)
{
srcs[j].in = t = ImgTool_Read(args, namelist[i]);
if (srcs[j].in)
{
if (!j)
{
//get the image loader to massage pixel formats...
memset(sh_config.texfmt, 0, sizeof(sh_config.texfmt));
sh_config.texfmt[srcs[j].in->encoding] = true;
}
else if (!t->mipcount || !t->mip[0].data)
{
Con_Printf("%s: no valid image data\n", namelist[i]);
ImgTool_FreeMips(srcs[j].in);
continue;
}
else if (t->encoding != srcs[0].in->encoding)
{
Con_Printf("%s: mismatched pixel format, (%s not %s) cannot combine\n", namelist[i], Image_FormatName(t->encoding), Image_FormatName(srcs[0].in->encoding));
ImgTool_FreeMips(srcs[j].in);
continue;
}
else if (t->type == PTI_CUBE && t->mip[0].depth != 6)
{
Con_Printf("%s: incorrect cubemap data\n", namelist[i]);
ImgTool_FreeMips(srcs[j].in);
continue;
}
else if (srcs[0].in->mip[0].width != t->mip[0].width || srcs[0].in->mip[0].height != t->mip[0].height)
{
Con_Printf("%s: incorrect image size\n", namelist[i]);
ImgTool_FreeMips(srcs[j].in);
continue;
}
else if (t->mip[0].depth == 0)
{
Con_Printf("%s: no layers\n", namelist[i]);
ImgTool_FreeMips(srcs[j].in);
continue;
}
layers += t->mip[0].depth;
srcs[j++].fname = namelist[i];
}
}
filecount = j;
//FIXME: reorder input images to handle ft/bk/lt/rt/up/dn, and flip+rotate+etc to match quake
if (args->textype == PTI_CUBE)
{
int facetype[6];
static const struct {qboolean flipx, flipy, flipd;} skyboxflips[] ={
{true, false, true},
{false, true, true},
{true, true, false},
{false, false, false},
{true, false, true},
{true, false, true}
};
for (i = 0; i < 6; i++)
facetype[i] = 0;
for (i = 0; i < filecount; i++)
{
const char *ex = COM_GetFileExtension(srcs[i].fname, NULL);
if (ex && ex-srcs[i].fname > 2 && srcs[i].in->mip[0].depth == 1)
{
if (!strncasecmp(ex-2, "rt", 2))
facetype[0] = -i-1;
else if (!strncasecmp(ex-2, "lf", 2))
facetype[1] = -i-1;
else if (!strncasecmp(ex-2, "ft", 2))
facetype[2] = -i-1;
else if (!strncasecmp(ex-2, "bk", 2))
facetype[3] = -i-1;
else if (!strncasecmp(ex-2, "up", 2))
facetype[4] = -i-1;
else if (!strncasecmp(ex-2, "dn", 2))
facetype[5] = -i-1;
else if (!strncasecmp(ex-2, "px", 2))
facetype[0] = i+1;
else if (!strncasecmp(ex-2, "nx", 2))
facetype[1] = i+1;
else if (!strncasecmp(ex-2, "py", 2))
facetype[2] = i+1;
else if (!strncasecmp(ex-2, "ny", 2))
facetype[3] = i+1;
else if (!strncasecmp(ex-2, "pz", 2))
facetype[4] = i+1;
else if (!strncasecmp(ex-2, "nz", 2))
facetype[5] = i+1;
}
}
if (facetype[0] && facetype[1] && facetype[2] && facetype[3] && facetype[4] && facetype[5])
{
Con_Printf("Reordering images to match cubemap\n");
tmpsrcs = alloca(sizeof(*tmpsrcs)*filecount);
memcpy(tmpsrcs, srcs, sizeof(*tmpsrcs)*filecount);
for (i = 0; i < 6; i++)
{
if (facetype[i] < 0)
{ //flip to match legacy skyboxes
unsigned bb,bw,bh;
srcs[i] = tmpsrcs[-facetype[i]-1];
t = srcs[i].in;
Image_BlockSizeForEncoding(t->encoding, &bb,&bw,&bh);
if (bw == 1 && bh == 1)
{
for (j = 0; j < t->mipcount; j++)
{
void *data = Image_FlipImage(t->mip[j].data, BZ_Malloc(t->mip[j].datasize), &t->mip[j].width, &t->mip[j].height, bb, skyboxflips[i].flipx, skyboxflips[i].flipy, skyboxflips[i].flipd);
if (t->mip[j].needfree)
Z_Free(t->mip[j].data);
t->mip[j].data = data;
t->mip[j].needfree = true;
}
}
}
else
srcs[i] = tmpsrcs[facetype[i]-1];
}
}
else
Con_Printf("WARNING: Cubemap ordering unknown!\n");
}
if (!filecount)
Con_Printf("no valid input files\n");
else if (!layers)
Con_Printf("Images must have at least one layer\n");
else if (args->textype == PTI_2D && layers != 1)
Con_Printf("2D images must have one layer exactly, sorry\n");
else if (args->textype == PTI_CUBE && layers != 6)
Con_Printf("Cubemaps must have 6 layers exactly\n");
else if (args->textype == PTI_CUBE_ARRAY && layers % 6)
Con_Printf("Cubemap arrays must have a multiple of 6 layers exactly\n");
else
{
t = srcs[0].in;
r = Z_Malloc(sizeof(*t));
r->type = args->textype;
r->extrafree = NULL;
r->encoding = t->encoding;
if (args->textype == PTI_3D)
r->mipcount = 1;
else
r->mipcount = t->mipcount;
for (j = 0; j < t->mipcount; j++)
{
r->mip[j].datasize = t->mip[j].datasize*layers;
r->mip[j].width = t->mip[j].width;
r->mip[j].height = t->mip[j].height;
r->mip[j].depth = 0;
r->mip[j].needfree = true;
r->mip[j].data = BZ_Malloc(r->mip[j].datasize);
}
for (i = 0, j = 0; i < filecount; i++)
{
t = srcs[i].in;
if (!t)
{
ImgTool_FreeMips(r);
return NULL;
}
for (j = 0; j < r->mipcount; j++)
{
if (r->mip[j].width != t->mip[j].width || r->mip[j].height != t->mip[j].height || t->mip[j].depth != 1)
{
Con_Printf("%s: mismatched mipmap sizes\n", namelist[i]);
continue;
}
memcpy((qbyte*)r->mip[j].data + t->mip[j].datasize*r->mip[j].depth, t->mip[j].data, t->mip[j].datasize);
r->mip[j].depth++;
}
}
for (i = 0; i < filecount; i++)
ImgTool_FreeMips(srcs[i].in);
printf("%s: %s %s, %i*%i, %i mips\n", "combined", imagetypename[r->type], Image_FormatName(r->encoding), r->mip[0].width, r->mip[0].height, r->mipcount);
return r;
}
for (i = 0; i < filecount; i++)
ImgTool_FreeMips(srcs[i].in);
return NULL;
}
static void ImgTool_Convert(struct opts_s *args, struct pendingtextureinfo *in, const char *inname, const char *outname)
{
size_t k;
const char *outext;
qboolean allowcompressed = false;
char newout[MAX_OSPATH];
if (!outname)
{
outext = COM_GetFileExtension(inname, NULL);
k = min(MAX_OSPATH-2-strlen(args->defaultext), outext-inname);
memcpy(newout, inname, k);
newout[k++] = '.';
strcpy(newout+k, args->defaultext);
outname = newout;
}
outext = COM_GetFileExtension(outname, NULL);
if (!strcmp(outext, ".dds") || !strcmp(outext, ".ktx"))
allowcompressed = true;
if (in)
{
if (!strcmp(outext, ".ktx") || !strcmp(outext, ".dds") || args->mipnum >= in->mipcount)
{
if (!(args->flags & IF_NOMIPMAP) && in->mipcount == 1)
Image_GenerateMips(in, args->flags);
}
if (args->mipnum >= in->mipcount)
{
ImgTool_FreeMips(in);
Con_Printf("%s: Requested output mip number was out of bounds %i >= %i\n", outname, args->mipnum, in->mipcount);
return;
}
for (k = 0; k < args->mipnum; k++)
{
if (in->mip[k].needfree)
BZ_Free(in->mip[k].data);
}
in->mipcount -= k;
memmove(in->mip, &in->mip[k], sizeof(in->mip[0])*in->mipcount);
printf("%s(%s)->", inname, Image_FormatName(in->encoding));
if (args->newpixelformat != PTI_INVALID && (args->newpixelformat < PTI_BC1_RGB || allowcompressed) && ImgTool_ConvertPixelFormat(args, inname, in))
printf("(%s)->\n", Image_FormatName(in->encoding));
if (!in->mipcount)
printf("%s: unable to convert any mips\n", inname);
#ifdef IMAGEFMT_KTX
else if (!strcmp(outext, ".ktx"))
{
if (!Image_WriteKTXFile(outname, FS_SYSTEM, in))
Con_Printf("%s(%s): Write failed\n", outname, Image_FormatName(in->encoding));
}
#endif
#ifdef IMAGEFMT_DDS
else if (!strcmp(outext, ".dds"))
{
if (!Image_WriteDDSFile(outname, FS_SYSTEM, in))
Con_Printf("%s(%s): Write failed\n", outname, Image_FormatName(in->encoding));
}
#endif
else
{
int bb,bw,bh;
if (in->type != PTI_2D)
Con_Printf("%s: Unable to write %s file to 2d image format\n", outname, imagetypename[in->type]);
#ifdef IMAGEFMT_PNG
else if (!strcmp(outext, ".png"))
{
#ifdef AVAIL_PNGLIB
qboolean outformats[PTI_MAX];
//force the format, because we can.
for (k = 0; k < PTI_MAX; k++)
outformats[k] =
(k == PTI_RGBA8) || (k == PTI_RGBX8) ||
(k == PTI_BGRA8) || (k == PTI_BGRX8) ||
(k == PTI_LLLA8) || (k == PTI_LLLX8) ||
(k == PTI_RGBA16) ||
(k == PTI_L8) || (k == PTI_L8A8) ||
/*(k == PTI_L16) ||*/
(k == PTI_BGR8) || (k == PTI_BGR8) ||
0;
if (!outformats[in->encoding])
Image_ChangeFormat(in, outformats, PTI_INVALID, outname);
Image_BlockSizeForEncoding(in->encoding, &bb, &bw,&bh);
if (!Image_WritePNG(outname, FS_SYSTEM, 0, &in->mip[0].data, 1, in->mip[0].width*bb, in->mip[0].width, in->mip[0].height, in->encoding, false))
#endif
Con_Printf("%s(%s): Write failed\n", outname, Image_FormatName(in->encoding));
}
#endif
#ifdef IMAGEFMT_TGA
else if (!strcmp(outext, ".tga"))
{
qboolean outformats[PTI_MAX];
for (k = 0; k < PTI_MAX; k++)
outformats[k] =
(k == PTI_RGBA8) || (k == PTI_RGBX8) ||
(k == PTI_BGRA8) || (k == PTI_BGRX8) ||
(k == PTI_LLLA8) || (k == PTI_LLLX8) ||
(k == PTI_RGBA16F) || (k == PTI_R16F) || //half-float tgas is a format extension, but allow it.
(k == PTI_L8) || (k == PTI_L8A8) ||
/*(k == PTI_L16) ||*/
(k == PTI_BGR8) || (k == PTI_BGR8) ||
0;
if (!outformats[in->encoding])
Image_ChangeFormat(in, outformats, PTI_INVALID, outname);
Image_BlockSizeForEncoding(in->encoding, &bb, &bw,&bh);
if (!WriteTGA(outname, FS_SYSTEM, in->mip[0].data, in->mip[0].width*bb, in->mip[0].width, in->mip[0].height, in->encoding))
Con_Printf("%s(%s): Write failed\n", outname, Image_FormatName(in->encoding));
}
#endif
else
Con_Printf("%s: Unknown output file format\n", outname);
}
if (in->mipcount > 1)
{
printf("%s(%s): %s %i*%i*%i, %i mips\n", outname, Image_FormatName(in->encoding), imagetypename[in->type], in->mip[0].width, in->mip[0].height, in->mip[0].depth, in->mipcount);
for (k = 0; k < in->mipcount; k++)
printf("\t%u: %i*%i*%i, %u\n", (unsigned)k, in->mip[k].width, in->mip[k].height, in->mip[k].depth, (unsigned)in->mip[k].datasize);
}
else
printf("%s(%s): %s %i*%i*%i, %u bytes\n", outname, Image_FormatName(in->encoding), imagetypename[in->type], in->mip[0].width, in->mip[0].height, in->mip[0].depth, (unsigned)in->mip[0].datasize);
ImgTool_FreeMips(in);
}
fflush(stdout);
}
static void ImgTool_Info(struct opts_s *args, const char *inname)
{
qbyte *indata;
size_t fsize;
size_t m;
struct pendingtextureinfo *in;
indata = FS_LoadMallocFile(inname, &fsize);
if (!indata)
printf("%s: unable to read\n", inname);
else if (fsize >= sizeof(wad2_t) && indata[0] == 'W' && indata[1] == 'A' && indata[2] == 'D')
{
const wad2_t *w = (const wad2_t *)indata;
const wad2entry_t *e = (const wad2entry_t *)(indata+w->offset);
printf("%s: wad%c file with %i entries\n", inname, w->magic[3], w->num);
for (m = 0; m < w->num; m++, e++)
{
switch(e->type)
{
case 67: //hl...
case TYP_MIPTEX:
{
const miptex_t *mip = (const miptex_t *)(indata+e->offset);
/*mip name SHOULD match entry name... but gah!*/
if (strcasecmp(e->name, mip->name))
printf("\t%16.16s (%s): %u*%u%s\n", e->name, mip->name, mip->width, mip->height, mip->offsets[0]?"":" (external data)");
else
printf("\t%16.16s: %u*%u%s\n", mip->name, mip->width, mip->height, mip->offsets[0]?"":" (external data)");
}
break;
case TYP_PALETTE:
printf("\t%16.16s: palette - %u bytes\n", e->name, e->size);
break;
default:
printf("\t%16.16s: ENTRY TYPE %u (%u bytes)\n", e->name, e->type, e->size);
break;
}
}
}
else
{
in = Image_LoadMipsFromMemory(args->flags|IF_NOMIPMAP, inname, inname, indata, fsize);
if (!in)
printf("%-20s: unsupported format\n", inname);
else if (in->mipcount == 1 && in->type == PTI_2D && in->mip[0].depth == 1)
printf("%-20s(%s): %4i*%-4i\n", inname, Image_FormatName(in->encoding), in->mip[0].width, in->mip[0].height);
else if (in->mipcount == 1)
printf("%-20s(%s): %s, %i*%i*%i, %u bytes\n", inname, Image_FormatName(in->encoding), imagetypename[in->type], in->mip[0].width, in->mip[0].height, in->mip[0].depth, (unsigned)in->mip[0].datasize);
else
{
printf("%-20s(%s): %s, %i*%i*%i, %i mips\n", inname, Image_FormatName(in->encoding), imagetypename[in->type], in->mip[0].width, in->mip[0].height, in->mip[0].depth, in->mipcount);
for (m = 0; m < in->mipcount; m++)
printf("\t%u: %i*%i*%i, %u\n", (unsigned)m, in->mip[m].width, in->mip[m].height, in->mip[m].depth, (unsigned)in->mip[m].datasize);
ImgTool_FreeMips(in);
}
}
fflush(stdout);
}
struct filelist_s
{
const char **exts;
size_t numfiles;
struct {
const char *rootpath; //the basepath that was passed to the filelist scan.
char *name;
size_t baselen; //length up to but not including the filename extension.
} *file;
size_t maxfiles; //to avoid reallocs
};
static void FileList_Release(struct filelist_s *list)
{
size_t i;
for (i = 0; i < list->numfiles; i++)
free(list->file[i].name);
free(list->file);
list->numfiles = 0;
list->maxfiles = 0;
}
static void FileList_Add(struct filelist_s *list, const char *rootpath, char *fname)
{
size_t i;
size_t baselen;
const char *ext = COM_GetFileExtension(fname, NULL);
for (i = 0; ; i++)
{
if (!list->exts[i])
return; //extension wasn't in the list.
if (!strcmp(list->exts[i], ext))
break; //one of the accepted extensions
}
baselen = ext?ext-fname:strlen(fname);
for (i = 0; i < list->numfiles; i++)
{
if (list->file[i].baselen == baselen && !strncasecmp(list->file[i].name, fname, baselen))
{
Con_Printf("Ignoring dupe file %s (using %s)\n", fname, list->file[i].name);
return; //file already listed, but maybe with a different extension
}
}
if (i == list->maxfiles)
{
list->maxfiles += 64;
list->file = realloc(list->file, sizeof(*list->file)*list->maxfiles);
}
list->file[i].rootpath = rootpath;
list->file[i].name = strdup(fname);
list->file[i].baselen = baselen;
list->numfiles++;
}
#ifdef _WIN32
static void ImgTool_TreeScan(struct filelist_s *list, const char *rootpath, const char *subpath)
{ //FIXME: convert to utf-8.
HANDLE h;
WIN32_FIND_DATAA fd;
char file[MAX_OSPATH];
if (subpath && *subpath)
Q_snprintfz(file, sizeof(file), "%s/%s", rootpath, subpath);
else
Q_snprintfz(file, sizeof(file), "%s", rootpath);
if (GetFileAttributesA(file) & FILE_ATTRIBUTE_DIRECTORY) //if its a directory then scan it.
Q_snprintfz(file+strlen(file), sizeof(file)-strlen(file), "/*");
h = FindFirstFileA(file, &fd);
if (h != INVALID_HANDLE_VALUE)
{
do
{
if (*fd.cFileName == '.')
continue; //skip .. (and unix hidden files, because urgh)
if (subpath && *subpath)
Q_snprintfz(file, sizeof(file), "%s/%s", subpath, fd.cFileName);
else
Q_snprintfz(file, sizeof(file), "%s", fd.cFileName);
if (fd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
; //don't report hidden entries.
else if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
ImgTool_TreeScan(list, rootpath, file);
else
FileList_Add(list, rootpath, file);
} while(FindNextFileA(h, &fd));
FindClose(h);
}
}
#else
#include <dirent.h>
#include <fnmatch.h>
static void ImgTool_TreeScan(struct filelist_s *list, const char *basepath, const char *subpath)
{
DIR *dir;
char file[MAX_OSPATH];
struct dirent *ent;
if (subpath && *subpath)
Q_snprintfz(file, sizeof(file), "%s/%s", basepath, subpath);
else
Q_snprintfz(file, sizeof(file), "%s", basepath);
dir = opendir(file);
if (!dir)
{
Con_Printf("Failed to open dir %s\n", file);
return;
}
for (;;)
{
ent = readdir(dir);
if (!ent)
break;
if (*ent->d_name == '.')
continue;
else if (ent->d_type == DT_DIR)
{
if (!subpath)
continue;
if (*subpath)
Q_snprintfz(file, sizeof(file), "%s/%s", subpath, ent->d_name);
else
Q_snprintfz(file, sizeof(file), "%s", ent->d_name);
ImgTool_TreeScan(list, basepath, file);
}
else if (ent->d_type == DT_REG)
{
if (subpath && *subpath)
Q_snprintfz(file, sizeof(file), "%s/%s", subpath, ent->d_name);
else
Q_snprintfz(file, sizeof(file), "%s", ent->d_name);
FileList_Add(list, basepath, file);
}
}
closedir(dir);
}
#endif
static void ImgTool_TreeConvert(struct opts_s *args, const char *destpath, const char *srcpath)
{
size_t newfiles=0, skippedfiles=0, processedfiles=0;
char file[MAX_OSPATH];
char dest[MAX_OSPATH];
const char *exts[] = {".png", ".bmp", ".tga", ".jpg", ".exr", ".hdr", NULL};
struct filelist_s list = {exts};
size_t i, destlen = strlen(destpath)+1;
ImgTool_TreeScan(&list, srcpath, "");
if (!list.numfiles)
Con_Printf("No suitable files found in directory: %s\n", srcpath);
for (i = 0; i < list.numfiles; i++)
{
struct stat statsrc, statdst;
Q_snprintfz(file, sizeof(file), "%s/%s", srcpath, list.file[i].name);
Q_snprintfz(dest, sizeof(dest), "%s/%s", destpath, list.file[i].name);
Q_snprintfz(dest+destlen+list.file[i].baselen, sizeof(dest)-destlen-list.file[i].baselen, ".dds");
if (stat(file, &statsrc) < 0)
{
Con_Printf("stat(\"%s\") failed...\n", file);
continue;
}
if (stat(dest, &statdst) < 0)
{
statdst.st_mtime = INT_MIN; //make it look old
newfiles++;
}
if (statdst.st_mtime <= statsrc.st_mtime)
{
processedfiles++;
// Con_Printf("Image file %s -> %s\n", file, dest);
FS_CreatePath(dest, FS_SYSTEM);
ImgTool_Convert(args, ImgTool_Read(args, file), file, dest);
}
else
{
skippedfiles++;
// Con_Printf("Unmodified image file %s -> %s\n", file, dest);
}
}
Con_Printf("found: %u, processed: %u, skipped: %u, new: %u\n", (unsigned int)list.numfiles, (unsigned int)processedfiles, (unsigned int)skippedfiles, (unsigned int)newfiles);
FileList_Release(&list);
return;
}
static void ImgTool_WadExtract(struct opts_s *args, const char *wadname)
{
qbyte *indata;
size_t fsize;
size_t m;
indata = FS_LoadMallocFile(wadname, &fsize);
if (!indata)
printf("%s: unable to read\n", wadname);
else if (fsize >= sizeof(wad2_t) && indata[0] == 'W' && indata[1] == 'A' && indata[2] == 'D')
{
const wad2_t *w = (const wad2_t *)indata;
const wad2entry_t *e = (const wad2entry_t *)(indata+w->offset);
int i;
char clean[sizeof(e->name)+1];
for (m = 0; m < w->num; m++, e++)
{
switch(e->type)
{
case 67: //hl...
case TYP_MIPTEX:
{
miptex_t *mip = (miptex_t *)(indata+e->offset);
struct pendingtextureinfo *out = Z_Malloc(sizeof(*out));
if (!strcmp(e->name, "CONCHARS") && e->size==128*128)
{ //special hack for conchars, which is listed as a miptex for some reason, with no qpic header (it not being a qpic lump)
out->encoding = TF_H2_TRANS8_0;
out->type = PTI_2D;
out->mip[0].width = 128;
out->mip[0].height = 128;
out->mip[0].depth = 1;
out->mip[0].datasize = out->mip[0].width*out->mip[0].height*out->mip[0].depth;
out->mip[0].data = (char*)mip;
out->mipcount = 1;
ImgTool_Convert(args, out, "conchars", NULL);
break;
}
out->encoding = PTI_P8;
out->type = PTI_2D;
for (out->mipcount = 0; out->mipcount < 4 && mip->offsets[out->mipcount]; out->mipcount++)
{
out->mip[out->mipcount].width = mip->width>>out->mipcount;
out->mip[out->mipcount].height = mip->height>>out->mipcount;
out->mip[out->mipcount].depth = 1;
out->mip[out->mipcount].datasize = out->mip[out->mipcount].width*out->mip[out->mipcount].height*out->mip[out->mipcount].depth;
out->mip[out->mipcount].data = (char*)mip + mip->offsets[out->mipcount];
}
if (*mip->name == '*')
*mip->name = '#'; //convert from * to #, so its a valid file name.
ImgTool_Convert(args, out, mip->name, NULL);
}
break;
case TYP_QPIC:
{
int *qpic = (int *)(indata+e->offset);
struct pendingtextureinfo *out = Z_Malloc(sizeof(*out));
size_t sz;
qbyte *p;
if (e->size < 8 || 8+qpic[0]*qpic[1] != e->size)
{
printf("invalid size/header for qpic lump: %s\n", e->name);
break;
}
out->type = PTI_2D;
out->mip[0].width = LittleLong(qpic[0]);
out->mip[0].height = LittleLong(qpic[1]);
out->mip[0].depth = 1;
out->mip[0].datasize = out->mip[0].width*out->mip[0].height*out->mip[0].depth;
out->mip[0].data = (char*)(qpic+2);
out->mipcount = 1;
for (sz = 0, p = out->mip[0].data; sz < out->mip[0].datasize; sz++)
if (p[sz] == 255)
break;
out->encoding = sz<out->mip[0].datasize?TF_TRANS8:PTI_P8;
for (i = 0; i < sizeof(e->name); i++)
{ //lowercase it.
if (e->name[i] >= 'A' && e->name[i] <= 'Z')
clean[i] = (e->name[i]-'A')+'a';
else
clean[i] = e->name[i];
}
clean[sizeof(e->name)] = 0;
ImgTool_Convert(args, out, clean, NULL);
}
break;
case TYP_PALETTE:
default:
printf("skipping %s\n", e->name);
break;
}
}
}
else
printf("%s: does not appear to be a wad file\n", wadname);
}
static void ImgTool_WadConvert(struct opts_s *args, const char *destpath, const char **srcpaths, size_t numpaths, int wadtype/*x,2,3*/)
{
char file[MAX_OSPATH];
const char *exts[] = {".png", ".bmp", ".tga", ".exr", ".hdr", ".dds", ".ktx", ".xcf", ".pcx", ".jpg", NULL};
struct filelist_s list = {exts};
size_t i, u;
vfsfile_t *f;
char *inname;
qbyte *indata;
size_t fsize;
wad2_t wad2;
wad2entry_t *wadentries = NULL, *entry;
size_t maxentries = 0;
miptex_t mip;
qboolean wadpixelformats[PTI_MAX] = {0};
wadpixelformats[PTI_P8] = true;
if (!numpaths)
ImgTool_TreeScan(&list, ".", NULL);
else while(numpaths --> 0)
ImgTool_TreeScan(&list, *srcpaths++, NULL);
if (!list.numfiles)
{
printf("%s: No files specified\n", destpath);
return;
}
f = FS_OpenVFS(destpath, "wb", FS_SYSTEM);
wad2.magic[0] = 'W';
wad2.magic[1] = 'A';
wad2.magic[2] = 'D';
wad2.magic[3] = (wadtype==2)?'3':'2'; //wad3 instead of 2, so we can include a palette for tools to validate against
wad2.num = 0;
wad2.offset = 0;
VFS_WRITE(f, &wad2, 12);
//try to decompress everything to a nice friendly palletizable range.
for (u = 1; u < countof(sh_config.texfmt); u++)
sh_config.texfmt[u] = (u==PTI_RGBA8)||(u==PTI_RGBX8)||(u==PTI_P8);
if (wadtype == 2)
{ //WAD2 texture files generally have a palette lump.
if (wad2.num == maxentries)
{
maxentries += 64;
wadentries = realloc(wadentries, sizeof(*wadentries)*maxentries);
}
entry = &wadentries[wad2.num++];
memset(entry, 0, sizeof(*entry));
Q_strncpyz(entry->name, "PALETTE", 16);
entry->type = TYP_PALETTE;
entry->offset = VFS_TELL(f);
//and the lump data.
VFS_WRITE(f, host_basepal, 256*3);
}
for (i = 0; i < list.numfiles; i++)
{
Q_snprintfz(file, sizeof(file), "%s/%s", list.file[i].rootpath, list.file[i].name);
inname = list.file[i].name;
if (list.file[i].baselen > 15)
{
Con_Printf("Path too long for wad - %s\n", inname);
continue;
}
indata = FS_LoadMallocFile(file, &fsize);
if (!indata)
printf("Unable to open %s\n", inname);
else
{
struct pendingtextureinfo *in = Image_LoadMipsFromMemory(args->flags, inname, file, indata, fsize);
Image_GenerateMips(in, args->flags);
if (in)
{
if (in->mipcount == 1)
Image_GenerateMips(in, args->flags);
if (!in->mipcount)
{
ImgTool_FreeMips(in);
Con_Printf("%s: unable to load any mips\n", inname);
continue;
}
}
if (args->mipnum >= in->mipcount)
{
ImgTool_FreeMips(in);
Con_Printf("%s: not enough mips\n", inname);
continue;
}
//strip out all but the 4 mip levels we care about.
for (u = 0; u < in->mipcount; u++)
{
if (u >= args->mipnum && u < args->mipnum+4)
{
if (!wadtype)
{ //if we're stripping out the wad data (so that the engine ends up requiring external textures) then do it now before palettizing, for efficiency.
if (in->mip[u].needfree)
BZ_Free(in->mip[u].data);
in->mip[u].data = NULL;
in->mip[u].datasize = 0;
}
}
else
{
if (in->mip[u].needfree)
BZ_Free(in->mip[u].data);
memset(&in->mip[u], 0, sizeof(in->mip[u]));
}
}
in->mipcount -= args->mipnum;
if (in->mipcount > 4)
in->mipcount = 4;
memmove(&in->mip[0], &in->mip[args->mipnum], sizeof(in->mip[0])*in->mipcount);
memset(&in->mip[in->mipcount], 0, sizeof(in->mip[0])*((args->mipnum+4)-in->mipcount)); //null it out, just in case.
if (in->encoding != PTI_P8)
Image_ChangeFormat(in, wadpixelformats, (*inname=='{')?TF_TRANS8:PTI_INVALID, inname);
if (in->encoding != PTI_P8)
{ //erk! we failed to palettize...
ImgTool_FreeMips(in);
continue;
}
if (wad2.num == maxentries)
{
maxentries += 64;
wadentries = realloc(wadentries, sizeof(*wadentries)*maxentries);
}
entry = &wadentries[wad2.num++];
Q_strncpyz(entry->name, inname, 16);
entry->name[list.file[i].baselen] = 0; //kill any .tga
if (*entry->name == '#')
*entry->name = '*'; //* is not valid in a filename, yet needed for turbs, so by convention # is used instead. this is only relevant for the first char.
if (wadtype == 3)
{
for (u = 0; u < sizeof(entry->name); u++)
entry->name[u] = toupper(entry->name[u]);
entry->type = 67;
}
else
entry->type = TYP_MIPTEX;
entry->cmprs = 0;
entry->dummy = 0;
entry->offset = VFS_TELL(f);
if (!in->mip[0].width || (in->mip[0].width & 15))
Con_Printf("%s(%i): WARNING: miptex width is not a multiple of 16 - %i*%i\n", inname, args->mipnum, in->mip[0].width, in->mip[0].height);
if (!in->mip[0].height || (in->mip[0].height & 15))
Con_Printf("%s(%i): WARNING: miptex height is not a not multiple of 16 - %i*%i\n", inname, args->mipnum, in->mip[0].width, in->mip[0].height);
if (0)
{
if (!strcasecmp(entry->name, "CONCHARS") && in->mip[0].width==128&&in->mip[0].height==128)
entry->type = TYP_MIPTEX; //yes, weird. match vanilla quake. explicitly avoid qpic to avoid corruption in the first 8 bytes (due to the engine's early endian swapping)
//FIXME: encoding should be pti_trans8_0...
else
{
entry->type = TYP_QPIC;
//qpics need a header
VFS_WRITE(f, &in->mip[0].width, sizeof(int));
VFS_WRITE(f, &in->mip[0].height, sizeof(int));
}
//and now the 8bit pixel data itself
VFS_WRITE(f, in->mip[0].data, in->mip[0].datasize);
}
else
{
memcpy(mip.name, entry->name, sizeof(mip.name));
mip.width = in->mip[0].width;
mip.height = in->mip[0].height;
mip.offsets[0] = in->mip[0].datasize?sizeof(mip):0;
mip.offsets[1] = in->mip[1].datasize?mip.offsets[0]+in->mip[0].datasize:0;
mip.offsets[2] = in->mip[2].datasize?mip.offsets[1]+in->mip[1].datasize:0;
mip.offsets[3] = in->mip[3].datasize?mip.offsets[2]+in->mip[2].datasize:0;
Con_Printf("%s: %ix%i\n", mip.name, mip.width, mip.height);
VFS_WRITE(f, &mip, sizeof(mip));
VFS_WRITE(f, in->mip[0].data, in->mip[0].datasize);
VFS_WRITE(f, in->mip[1].data, in->mip[1].datasize);
VFS_WRITE(f, in->mip[2].data, in->mip[2].datasize);
VFS_WRITE(f, in->mip[3].data, in->mip[3].datasize);
if (wad2.magic[3] == '3')
{
VFS_WRITE(f, "\x00\x01", 2);
VFS_WRITE(f, host_basepal, 256*3);
}
}
entry->size = entry->dsize = VFS_TELL(f)-entry->offset;
ImgTool_FreeMips(in);
}
}
wad2.offset = VFS_TELL(f);
VFS_WRITE(f, wadentries, sizeof(*wadentries)*wad2.num);
VFS_SEEK(f, 0);
VFS_WRITE(f, &wad2, sizeof(wad2));
VFS_CLOSE(f);
FileList_Release(&list);
}
int main(int argc, const char **argv)
{
enum
{
mode_info,
mode_convert,
mode_autotree,
mode_genwadx,
mode_genwad2,
mode_genwad3,
mode_extractwad,
} mode = mode_info;
size_t u, f;
qboolean nomoreopts = false;
struct opts_s args;
size_t files = 0;
for (u = 1; u < countof(sh_config.texfmt); u++)
sh_config.texfmt[u] = true;
args.flags = 0;
args.newpixelformat = PTI_INVALID;
args.mipnum = 0;
args.textype = -1;
args.defaultext = NULL;
sh_config.texture2d_maxsize = 1u<<31;
sh_config.texture3d_maxsize = 1u<<31;
sh_config.texture2darray_maxlayers = 1u<<31;
sh_config.texturecube_maxsize = 8192;
sh_config.texture_non_power_of_two = true;
sh_config.texture_non_power_of_two_pic = true;
sh_config.texture_allow_block_padding = true;
sh_config.npot_rounddown = true; //shouldn't be relevant
sh_config.havecubemaps = true; //I don't think this matters.
ImgTool_SetupPalette();
Image_Init();
if (argc==1)
goto showhelp;
for (u = 1; u < argc; u++)
{
if (*argv[u] == '-' && !nomoreopts)
{
if (!strcmp(argv[u], "--"))
nomoreopts = true;
else if (!strcmp(argv[u], "-?") || !strcmp(argv[u], "--help"))
{
showhelp:
Con_Printf("show info : %s -i *.ktx\n", argv[0]);
Con_Printf("compress : %s --astc_6x6_ldr [--nomips] in.png out.ktx [in2.png out2.ktx]\n", argv[0]);
Con_Printf("compress : %s --bc3_rgba [--premul] [--nomips] in.png out.dds\n\tConvert pixel format (to bc3 aka dxt5) before writing to output file.\n", argv[0]);
Con_Printf("convert : %s --convert in.exr out.dds\n\tConvert to different file format, while trying to preserve pixel formats.\n", argv[0]);
Con_Printf("recursive : %s --auto --astc_6x6_ldr destdir srcdir\n\tCompresses the files to dds (writing to an optionally different directory)", argv[0]);
Con_Printf("decompress : %s --decompress [--exportmip 0] [--nomips] in.ktx out.png\n\tDecompresses any block-compressed pixel data.\n", argv[0]);
Con_Printf("create wad : %s -w [--exportmip 2] out.wad srcdir\n", argv[0]);
Con_Printf("extract wad: %s -x [--ext png] src.wad\n", argv[0]);
Image_PrintInputFormatVersions();
Con_Printf("Supported compressed/interesting pixelformats are:\n");
for (f = 0; f < PTI_MAX; f++)
{
int bb,bw,bh;
Image_BlockSizeForEncoding(f, &bb,&bw,&bh);
if (f >= PTI_ASTC_FIRST && f <= PTI_ASTC_LAST)
Con_Printf(" --%-16s %5.3g-bpp (requires astcenc)\n", Image_FormatName(f), 8*(float)bb/(bw*bh));
else if (f==PTI_BC1_RGB||f==PTI_BC1_RGBA||f==PTI_BC2_RGBA||f==PTI_BC3_RGBA||f==PTI_BC4_R8||f==PTI_BC5_RG8)
Con_Printf(" --%-16s %5.3g-bpp (requires nvcompress)\n", Image_FormatName(f), 8*(float)bb/(bw*bh));
else if (f==PTI_BC6_RGB_UFLOAT || f==PTI_BC6_RGB_SFLOAT || f==PTI_BC7_RGBA)
Con_Printf(" --%-16s %5.3g-bpp (requires nvcompress 2.1+)\n", Image_FormatName(f), 8*(float)bb/(bw*bh));
else if ( f==PTI_RGBA16F ||
f==PTI_RGBA32F ||
f==PTI_E5BGR9 ||
f==PTI_B10G11R11F ||
f==PTI_RGB565 ||
f==PTI_RGBA4444 ||
f==PTI_ARGB4444 ||
f==PTI_RGBA5551 ||
f==PTI_ARGB1555 ||
f==PTI_A2BGR10 ||
// f==PTI_R8 ||
// f==PTI_R16 ||
// f==PTI_R16F ||
// f==PTI_R32F ||
f==PTI_RG8 ||
f==PTI_L8 ||
f==PTI_L8A8 ||
0)
Con_Printf(" --%-16s %5.3g-bpp\n", Image_FormatName(f), 8*(float)bb/(bw*bh));
// else
// Con_DPrintf(" --%-16s %5.3g-bpp (unsupported)\n", Image_FormatName(f), 8*(float)bb/(bw*bh));
}
return EXIT_SUCCESS;
}
else if (!files && (!strcmp(argv[u], "-c") || !strcmp(argv[u], "--convert")))
mode = mode_convert;
else if (!files && (!strcmp(argv[u], "-d") || !strcmp(argv[u], "--decompress")))
{ //remove any (weird) gpu formats
for (f = PTI_BC1_RGB; f < PTI_ASTC_LAST; f++)
sh_config.texfmt[f] = false;
mode = mode_convert;
}
else if (!files && (!strcmp(argv[u], "-r") || !strcmp(argv[u], "--auto")))
mode = mode_autotree;
else if (!files && (!strcmp(argv[u], "-i") || !strcmp(argv[u], "--info")))
mode = mode_info;
else if (!files && (!strcmp(argv[u], "-w") || !strcmp(argv[u], "--genwad3")))
mode = mode_genwad3;
else if (!files && (!strcmp(argv[u], "-w") || !strcmp(argv[u], "--genwad2")))
mode = mode_genwad2;
else if (!files && (!strcmp(argv[u], "-w") || !strcmp(argv[u], "--genwadx")))
mode = mode_genwadx;
else if (!files && (!strcmp(argv[u], "-x") || !strcmp(argv[u], "--extractwad")))
mode = mode_extractwad;
else if (!strcmp(argv[u], "--2d"))
args.textype = PTI_2D;
else if (!strcmp(argv[u], "--3d"))
args.textype = PTI_3D;
else if (!strcmp(argv[u], "--cube"))
args.textype = PTI_CUBE;
else if (!strcmp(argv[u], "--2darray"))
args.textype = PTI_2D_ARRAY;
else if (!strcmp(argv[u], "--cubearray"))
args.textype = PTI_CUBE_ARRAY;
else if (!strcmp(argv[u], "--nomips") )
args.flags |= IF_NOMIPMAP;
else if (!strcmp(argv[u], "--mips"))
args.flags &= ~IF_NOMIPMAP;
else if (!strcmp(argv[u], "--premul") )
args.flags |= IF_PREMULTIPLYALPHA;
else if (!strcmp(argv[u], "--nopremul"))
args.flags &= ~IF_PREMULTIPLYALPHA;
else if (!strcmp(argv[u], "--ext"))
{
if (u+1 < argc)
args.defaultext = argv[++u];
else
{
Con_Printf("--exportmip requires trailing numeric argument\n");
return 1;
}
}
else if (!strcmp(argv[u], "--exportmip"))
{
char *e = "erk";
if (u+1 < argc)
args.mipnum = strtoul(argv[++u], &e, 10);
if (*e)
{
Con_Printf("--exportmip requires trailing numeric argument\n");
return 1;
}
}
else
{
if (argv[u][1] == '-')
{
for (f = 0; f < PTI_MAX; f++)
{
if (!strcasecmp(argv[u]+2, Image_FormatName(f)))
{
args.newpixelformat = f;
mode = mode_convert;
break;
}
}
if (f < PTI_MAX)
continue;
}
Con_Printf("Unknown arg %s\n", argv[u]);
goto showhelp;
}
argv[u] = NULL;
}
else
argv[files++] = argv[u];
}
if (!args.defaultext)
{
if (mode == mode_extractwad)
args.defaultext = "png"; //something the user expects to be able to view easily (and lossless)
else
args.defaultext = "ktx";
}
if (mode == mode_info)
{ //just print info about each listed file.
for (u = 0; u < files; u++)
ImgTool_Info(&args, argv[u]);
}
else if (mode == mode_convert && files > 1 && args.textype>=0) //overwrite input
{
files--;
ImgTool_Convert(&args, ImgTool_Combine(&args, argv, files), "combined", argv[files]);
}
else if (mode == mode_convert && files == 1 && args.textype<0) //overwrite input
ImgTool_Convert(&args, ImgTool_Read(&args, argv[0]), argv[0], NULL);
else if (mode == mode_convert && !(files&1) && args.textype<0) //list of pairs
{
//-c src1 dst1 src2 dst2
for (u = 0; u+1 < files; u+=2)
ImgTool_Convert(&args, ImgTool_Read(&args, argv[u]), argv[u], argv[u+1]);
}
else if (mode == mode_autotree && files == 2)
ImgTool_TreeConvert(&args, argv[0], argv[1]);
else if ((mode == mode_genwad2 || mode == mode_genwad3 || mode == mode_genwadx))
ImgTool_WadConvert(&args, argv[0], argv+1, files-1, mode-mode_genwadx);
else if ((mode == mode_extractwad) && files == 1)
ImgTool_WadExtract(&args, argv[0]);
else
{
printf("%u files\n", (int)files);
printf("unsupported arg count for mode\n");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}