Skip to content

Instantly share code, notes, and snippets.

@kajott
Last active March 12, 2020 09:46
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 kajott/1e2866f78e9efb3e3323a34bb04f7598 to your computer and use it in GitHub Desktop.
Save kajott/1e2866f78e9efb3e3323a34bb04f7598 to your computer and use it in GitHub Desktop.
simple OpenGL-based KTX texture file viewer
#if 0 // self-compiling code
cc -std=c99 -Wall -Wextra -pedantic -Werror -g -O4 $0 -o ktxview `sdl2-config --cflags --libs` -lGL -lm || exit 1
exec ./ktxview $*
#endif
// simple OpenGL-based KTX texture file viewer
//
// features:
// - simple, based on SDL2 and OpenGL Compatibility Profile
// - self-compiling on GNU/Linux and similar platforms
// - zooming and panning across the texture with the mouse and mouse wheel
// - switchable Y flip ([F] key)
// - initial flip setting detected from KTXorientation tag in the KTX file
// - switchable GL_NEAREST / GL_LINEAR texture filtering ([I] key)
// - switchable alpha compositing (premultiplied/non-premultiplied; [M] key)
//
// caveats:
// - just loads the texture with gl[Compressed]TexImage2D
// - only supports single 2D textures
// - texture arrays and 3D textures show first slice, cube map show +X face
// - ignores any mip-maps (only level 0 is shown)
// - only very basic format checking -> may crash on malformed files
// - no endianness swap for uncompressed texture data ->
// everything but GL_UNSIGNED_BYTES *may* be displayed incorrectly
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
#include <assert.h>
#define SDL_MAIN_HANDLED // don't override main() on Win32
#include <SDL.h>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <GL/gl.h>
#ifndef GL_CLAMP_TO_EDGE
#define GL_CLAMP_TO_EDGE 0x812F
#endif
#else
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>
#endif
#define GRID_SIZE 12
#define ZOOM_STEP pow(2.0, 1.0 / 4)
// global variables
bool g_flip = true;
bool g_premul = false;
bool g_filter = false;
bool g_snap = true;
bool g_autozoom = true;
bool g_redraw = true;
bool g_new_settings = true;
int g_screen_width, g_screen_height; // window size
int g_image_width, g_image_height; // image size
int g_image_x0, g_image_y0; // upper-left corner of image in window coordinates
double g_zoom = 1.0; // image scale
void warn(const char* msg) {
fprintf(stderr, "WARNING: %s\n", msg);
}
int error(const char* msg) {
#ifdef _WIN32
MessageBox(NULL, msg, "KTXView Error", MB_ICONERROR | MB_OK);
#else
fprintf(stderr, "ERROR: %s\n", msg);
#endif
return 1;
}
int sdl_error(const char* msg) {
#ifdef _WIN32
char* str = malloc(strlen(msg) + strlen(SDL_GetError()) + 4);
if (str) {
strcpy(str, msg);
strcat(str, ":\r\n");
strcat(str, SDL_GetError());
}
MessageBox(NULL, str ? str : msg, "KTXView Error", MB_ICONERROR | MB_OK);
#else
fprintf(stderr, "ERROR: %s - %s\n", msg, SDL_GetError());
#endif
return 1;
}
static inline uint32_t swap32(uint32_t x) {
// endianness swap
x = ((x << 8) & 0xFF00FF00) | ((x >> 8) & 0x00FF00FF);
return (x << 16) || (x >> 16);
}
struct ktx_header {
uint8_t identifier[12];
uint32_t endianness;
uint32_t glType;
uint32_t glTypeSize;
uint32_t glFormat;
uint32_t glInternalFormat;
uint32_t glBaseInternalFormat;
uint32_t pixelWidth;
uint32_t pixelHeight;
uint32_t pixelDepth;
uint32_t numberOfArrayElements;
uint32_t numberOfFaces;
uint32_t numberOfMipmapLevels;
uint32_t bytesOfKeyValueData;
uint8_t payload[];
};
struct ktx_image {
uint32_t imageSize;
uint8_t payload[];
};
struct ktx_metadata {
uint32_t keyAndValueByteSize;
char keyAndValue[];
};
static const uint8_t ktx_identifier[12] =
{ 0xAB, 'K', 'T', 'X', ' ', '1', '1', 0xBB, '\r', '\n', 0x1A, '\n' };
static const union {
uint16_t _;
uint8_t is_little_endian;
} endianness = { 1 };
// update image zoom and position around pivot
static void set_zoom(int x, int y, double new_zoom) {
// screen to image of pivot point
double ix = (x - g_image_x0) / g_zoom;
double iy = (y - g_image_y0) / g_zoom;
// apply zoom
g_new_settings = g_autozoom;
g_redraw = true;
g_autozoom = false;
g_zoom = new_zoom;
// compute new image origin
g_image_x0 = (int)(x - ix * g_zoom + 0.5);
g_image_y0 = (int)(y - iy * g_zoom + 0.5);
}
// restrict position to fit into the screen
int restrict_pos(int pos, int image_size, int screen_size) {
image_size = (int)(image_size * g_zoom + 0.5);
int v0 = 0, v1 = screen_size - image_size;
if (v1 < 0) { v0 = v1; v1 = 0; }
return (pos < v0) ? v0 : (pos > v1) ? v1 : pos;
}
int main(int argc, char* argv[]) {
// command-line help
if (argc != 2) {
fprintf(stderr, "Usage: %s <input.ktx>\n", argv[0]);
return 2;
}
// read the file into memory
FILE *f = fopen(argv[1], "rb");
if (!f) {
return error("failed to open the input file");
}
fseek(f, 0, SEEK_END);
size_t file_size = ftell(f);
fseek(f, 0, SEEK_SET);
void *file_data = malloc(file_size);
assert(file_data != NULL);
if (fread(file_data, 1, file_size, f) != file_size) {
return error("I/O error while reading the input file");
}
fclose(f);
// check header
struct ktx_header *header = (struct ktx_header*) file_data;
if (memcmp(header->identifier, ktx_identifier, 12)) {
return error("input file does not have a valid KTX file signature");
}
// endianness check
if (header->endianness == 0x01020304) {
#define swap_field(f) header->f = swap32(header->f)
swap_field(glType);
swap_field(glTypeSize);
swap_field(glFormat);
swap_field(glInternalFormat);
swap_field(glBaseInternalFormat);
swap_field(pixelWidth);
swap_field(pixelHeight);
swap_field(pixelDepth);
swap_field(numberOfArrayElements);
swap_field(numberOfFaces);
swap_field(numberOfMipmapLevels);
swap_field(bytesOfKeyValueData);
#undef swap_field
}
else if (header->endianness != 0x04030201) {
return error("invalid endianness field in KTX file");
}
// dimension check
if (header->pixelWidth < 1) {
return error("invalid image width");
}
if (header->pixelHeight < 1) {
return error("invalid image height (note that only 2D textures are supported)");
}
if (header->pixelDepth != 0) {
warn("3D textures are not supported, only showing first slice");
}
if (header->numberOfArrayElements > 1) {
warn("array textures are not supported, only showing first slice");
}
if (header->numberOfFaces > 1) {
warn("cube map textures are not supported, only showing +X side");
}
if ((sizeof(struct ktx_header) + header->bytesOfKeyValueData) >= file_size) {
return error("invalid KTX header size");
}
const struct ktx_image* image = (const struct ktx_image*) &header->payload[header->bytesOfKeyValueData];
if ((sizeof(struct ktx_header) + header->bytesOfKeyValueData + sizeof(struct ktx_image) + image->imageSize) > file_size) {
return error("invalid KTX image size");
}
// search metadata to get orientation
uint32_t meta_offset = 0;
while ((meta_offset + 4) < header->bytesOfKeyValueData) {
const struct ktx_metadata* meta = (const struct ktx_metadata*) &header->payload[meta_offset];
meta_offset += meta->keyAndValueByteSize + sizeof(struct ktx_metadata);
if (meta_offset > header->bytesOfKeyValueData) {
warn("KTX metadata is corrupt, ignoring");
break;
}
if (!strncmp(meta->keyAndValue, "KTXorientation", meta->keyAndValueByteSize)) {
char comp = 0;
for (uint32_t i = (uint32_t)strlen("KTXorientation"); i < meta->keyAndValueByteSize; ++i) {
char c = meta->keyAndValue[i];
if (isupper(c)) { comp = c; }
if ((comp == 'T') && (c == 'u')) { g_flip = true; }
if ((comp == 'T') && (c == 'd')) { g_flip = false; }
}
}
meta_offset = (meta_offset + 3) & (~3);
}
// dump texture format
printf("%s: %s endian, %dx%d pixels, %s\n", argv[1],
(!(header->endianness == 0x01020304) ^ endianness.is_little_endian) ? "big" : "little",
header->pixelWidth, header->pixelHeight,
g_flip ? "bottom-up" : "top-down");
// create window and OpenGL context
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) {
return sdl_error("failed to initialize SDL");
}
SDL_Window* win = SDL_CreateWindow(argv[1],
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
header->pixelWidth, header->pixelHeight,
SDL_WINDOW_OPENGL | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE);
if (!win) {
return sdl_error("failed to create window");
}
SDL_GLContext ctx = SDL_GL_CreateContext(win);
if (!ctx) {
return sdl_error("failed to create OpenGL context");
}
// create and upload the texture
GLuint tex;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
while (glGetError()); // clear pre-existing errors
if (header->glType) {
glTexImage2D(GL_TEXTURE_2D, 0, header->glInternalFormat,
header->pixelWidth, header->pixelHeight, 0,
header->glFormat, header->glType, image->payload);
}
else {
#if _WIN32 // look up glCompressedTexImage2D (necessary on Win32...)
typedef void (APIENTRY *PFNGLCOMPRESSEDTEXIMAGE2DPROC)
(GLenum, GLint, GLenum, GLsizei, GLsizei, GLint, GLsizei, const void*);
PFNGLCOMPRESSEDTEXIMAGE2DPROC glCompressedTexImage2D =
(PFNGLCOMPRESSEDTEXIMAGE2DPROC) SDL_GL_GetProcAddress("glCompressedTexImage2D");
if (!glCompressedTexImage2D) {
return error("glCompressedTexImage2D function not available");
}
#endif
glCompressedTexImage2D(GL_TEXTURE_2D, 0, header->glInternalFormat,
header->pixelWidth, header->pixelHeight, 0,
image->imageSize, image->payload);
}
GLuint error = glGetError();
if (error) {
fprintf(stderr, "ERROR: invalid texture - OpenGL error 0x%04X\n", error);
}
g_image_width = header->pixelWidth;
g_image_height = header->pixelHeight;
// prepare the transparency grid
GLuint grid;
glGenTextures(1, &grid);
glBindTexture(GL_TEXTURE_2D, grid);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
#define grid_color_1 (0x80 * 0x01010101u)
#define grid_color_2 (0xC0 * 0x01010101u)
static const uint32_t grid_data[] = { grid_color_1, grid_color_2, grid_color_2, grid_color_1 };
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, grid_data);
// other OpenGL preparations
glEnable(GL_TEXTURE_2D);
glClearColor(0.25f, 0.25f, 0.25f, 1.0f);
// event processing loop
bool quit = false;
int mouse_ref_x = 0, mouse_ref_y = 0, mouse_base_x0 = 0, mouse_base_y0 = 0;
bool event_handled = false;
while (!quit && !error) {
// get new event:
// - if we just handled an event, check if there's another one in the queue
// - otherwise, wait for an event
SDL_Event ev;
if (event_handled) {
if (!SDL_PollEvent(&ev)) {
ev.type = SDL_LASTEVENT; // dummy event
}
} else if (!SDL_WaitEvent(&ev)) {
sdl_error("failed to wait for an input event");
break;
}
// handle the event
event_handled = true;
bool update_mouse_ref = false;
if ((ev.type == SDL_WINDOWEVENT) && (ev.window.event == SDL_WINDOWEVENT_EXPOSED)) {
g_redraw = true;
}
else if (ev.type == SDL_KEYDOWN) {
switch (ev.key.keysym.sym) {
case 'f': g_flip = !g_flip; g_new_settings = true; break;
case 'm': g_premul = !g_premul; g_new_settings = true; break;
case 'i': g_filter = !g_filter; g_new_settings = true; break;
case 'a': g_autozoom = !g_autozoom; g_new_settings = true; break;
case 's':
g_snap = !g_snap;
g_new_settings = true;
if (g_snap && !g_autozoom) {
set_zoom(g_screen_width / 2, g_screen_height / 2,
(g_zoom < 0.99) ? 0.5 : floor(g_zoom + 0.5));
}
break;
case '-': case SDLK_KP_MINUS:
set_zoom(g_screen_width / 2, g_screen_height / 2,
(g_zoom <= 1.0) ? 0.5 : floor(g_zoom - 0.1));
break;
case '+': case SDLK_KP_PLUS:
set_zoom(g_screen_width / 2, g_screen_height / 2,
(g_zoom < 1.0) ? 1.0 : ceil(g_zoom + 0.1));
break;
case 'q': case SDLK_ESCAPE:
quit = true;
break;
default:
break;
}
}
else if (ev.type == SDL_MOUSEWHEEL) {
int x, y;
SDL_GetMouseState(&x, &y);
if (ev.wheel.y > 0) {
set_zoom(x, y, g_snap ? ((g_zoom < 1.0) ? 1.0 : ceil(g_zoom + 0.1))
: g_zoom * ZOOM_STEP);
}
else {
set_zoom(x, y, g_snap ? ((g_zoom <= 1.0) ? 0.5 : floor(g_zoom - 0.1))
: g_zoom / ZOOM_STEP);
}
update_mouse_ref = true;
}
else if ((ev.type == SDL_MOUSEBUTTONDOWN) && (ev.button.button == SDL_BUTTON_LEFT)) {
update_mouse_ref = true;
}
else if ((ev.type == SDL_MOUSEMOTION) && (ev.motion.state & SDL_BUTTON_LMASK)) {
g_image_x0 = mouse_base_x0 + ev.motion.x - mouse_ref_x;
g_image_y0 = mouse_base_y0 + ev.motion.y - mouse_ref_y;
g_new_settings = g_autozoom;
g_autozoom = false;
g_redraw = true;
}
else if (ev.type == SDL_QUIT) {
quit = true;
}
else {
event_handled = false;
}
if (update_mouse_ref) {
// latch reference mouse position
mouse_ref_x = ev.button.x;
mouse_ref_y = ev.button.y;
mouse_base_x0 = g_image_x0;
mouse_base_y0 = g_image_y0;
}
if (event_handled) {
continue; // handle potential other events
}
// report settings string
if (g_new_settings) {
printf("\r[F]lip:%s pre[M]ulAlpha:%s f[I]lter:%s [A]utozoom:%s [S]nap:%s ",
g_flip ? "yes" : "no ",
g_premul ? "yes" : "no ",
g_filter ? "yes" : "no ",
g_autozoom ? "yes" : "no ",
g_snap ? "yes" : "no ");
fflush(stdout);
g_new_settings = false;
g_redraw = true;
}
if (g_redraw) {
// query new (possibly changed) window size
int old_width = g_screen_width;
int old_height = g_screen_height;
SDL_GetWindowSize(win, &g_screen_width, &g_screen_height);
glViewport(0, 0, g_screen_width, g_screen_height);
glLoadIdentity();
glOrtho(0,g_screen_width, g_screen_height,0, -1,1);
// update image position and zoom
if (g_autozoom) {
double zoomX = (double)g_screen_width / (double)g_image_width;
double zoomY = (double)g_screen_height / (double)g_image_height;
g_zoom = (zoomX < zoomY) ? zoomX : zoomY;
if (g_snap) {
g_zoom = (g_zoom > 0.999) ? floor(g_zoom) : 0.5;
}
g_image_x0 = (int)((g_screen_width - g_image_width * g_zoom) * 0.5 + 0.5);
g_image_y0 = (int)((g_screen_height - g_image_height * g_zoom) * 0.5 + 0.5);
}
else {
// adjust image position after screen resize;
// prone to rounding errors, but good enough
g_image_x0 += (g_screen_width - old_width) >> 1;
g_image_y0 += (g_screen_height - old_height) >> 1;
// don't push the image outside of the window
g_image_x0 = restrict_pos(g_image_x0, g_image_width, g_screen_width);
g_image_y0 = restrict_pos(g_image_y0, g_image_height, g_screen_height);
}
// compute final image geometry
double dx0 = g_image_x0;
double dx1 = g_image_x0 + g_image_width * g_zoom;
double dy0 = g_image_y0;
double dy1 = g_image_y0 + g_image_height * g_zoom;
double gridX = g_image_width * g_zoom / GRID_SIZE;
double gridY = g_image_height * g_zoom / GRID_SIZE;
// draw checkerboard background
glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_BLEND);
glBindTexture(GL_TEXTURE_2D, grid);
glBegin(GL_QUADS);
glTexCoord2d( 0.0, 0.0); glVertex2d(dx0, dy0);
glTexCoord2d(gridX, 0.0); glVertex2d(dx1, dy0);
glTexCoord2d(gridX, gridY); glVertex2d(dx1, dy1);
glTexCoord2d( 0.0, gridY); glVertex2d(dx0, dy1);
glEnd();
// draw main image
glEnable(GL_BLEND);
glBlendFunc(g_premul ? GL_ONE : GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBindTexture(GL_TEXTURE_2D, tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, g_filter ? GL_LINEAR : GL_NEAREST);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, g_flip ? 1.0f : 0.0f); glVertex2d(dx0, dy0);
glTexCoord2f(1.0f, g_flip ? 1.0f : 0.0f); glVertex2d(dx1, dy0);
glTexCoord2f(1.0f, g_flip ? 0.0f : 1.0f); glVertex2d(dx1, dy1);
glTexCoord2f(0.0f, g_flip ? 0.0f : 1.0f); glVertex2d(dx0, dy1);
glEnd();
// done
SDL_GL_SwapWindow(win);
g_redraw = false;
}
}
// done -- clean up
printf("\n");
SDL_GL_MakeCurrent(NULL, NULL);
SDL_GL_DeleteContext(ctx);
SDL_DestroyWindow(win);
SDL_Quit();
free(file_data);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment