Created
February 6, 2018 18:56
-
-
Save camthesaxman/4fc17fc5a42e946aad843081ee9b5616 to your computer and use it in GitHub Desktop.
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
/* | |
* tilesplit | |
* Simple tool to split a PNG into a tileset + tilemap. | |
*/ | |
#include <assert.h> | |
#include <stdbool.h> | |
#include <stdint.h> | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <string.h> | |
#include <png.h> | |
#ifdef _MSC_VER | |
#define FATAL_ERROR(format, ...) \ | |
do { \ | |
fprintf(stderr, format, __VA_ARGS__); \ | |
exit(1); \ | |
} while (0) | |
#else | |
#define FATAL_ERROR(format, ...) \ | |
do { \ | |
fprintf(stderr, format, ##__VA_ARGS__); \ | |
exit(1); \ | |
} while (0) | |
#endif // _MSC_VER | |
struct Color | |
{ | |
unsigned char r; | |
unsigned char g; | |
unsigned char b; | |
}; | |
struct Image | |
{ | |
unsigned int width; | |
unsigned int height; | |
unsigned int colorType; | |
unsigned int bitDepth; | |
unsigned int rowBytes; | |
unsigned char *pixels; | |
unsigned int numColors; | |
struct Color palette[256]; | |
}; | |
struct Tile | |
{ | |
unsigned int row; | |
unsigned int col; | |
unsigned int hash; | |
}; | |
unsigned int tileWidth = 8; | |
unsigned int outputWidth = 8; | |
unsigned int mapBitsPerEntry = 8; | |
struct Image inputImage; | |
void *tileMap; | |
struct Tile *tiles; | |
unsigned int tilesCount; | |
void ReadPngPalette(struct Image *image, png_structp pngReader, png_infop pngInfo) | |
{ | |
png_colorp colors; | |
int numColors; | |
if (png_get_PLTE(pngReader, pngInfo, &colors, &numColors) != PNG_INFO_PLTE) | |
FATAL_ERROR("Failed to retrieve palette\n"); | |
if (numColors > 256) | |
FATAL_ERROR("Images with more than 256 colors are not supported.\n"); | |
image->numColors = numColors; | |
for (int i = 0; i < numColors; i++) | |
{ | |
image->palette[i].r = colors[i].red; | |
image->palette[i].g = colors[i].green; | |
image->palette[i].b = colors[i].blue; | |
} | |
} | |
void WritePngPalette(struct Image *image, png_structp pngReader, png_infop pngInfo) | |
{ | |
png_colorp colors = malloc(image->numColors * sizeof(*colors)); | |
if (colors == NULL) | |
FATAL_ERROR("Failed to allocate PNG palette.\n"); | |
for (int i = 0; i < image->numColors; i++) | |
{ | |
colors[i].red = image->palette[i].r; | |
colors[i].green = image->palette[i].g; | |
colors[i].blue = image->palette[i].b; | |
} | |
png_set_PLTE(pngReader, pngInfo, colors, image->numColors); | |
free(colors); | |
} | |
static void ReadImageFromFile(struct Image *image, const char *path) | |
{ | |
unsigned char sig[8]; | |
png_structp pngReader; | |
png_infop pngInfo; | |
FILE *file; | |
png_byte **rowPointers; | |
unsigned int i; | |
file = fopen(path, "rb"); | |
if (file == NULL) | |
FATAL_ERROR("Failed to open \"%s\" for reading.\n", path); | |
if (fread(sig, 8, 1, file) != 1) | |
FATAL_ERROR("Failed to read PNG signature from \"%s\".\n", path); | |
if (png_sig_cmp(sig, 0, 8)) | |
FATAL_ERROR("\"%s\" does not have a valid PNG signature.\n", path); | |
pngReader = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); | |
if (pngReader == NULL) | |
FATAL_ERROR("Failed to create PNG read struct.\n"); | |
pngInfo = png_create_info_struct(pngReader); | |
if (pngInfo == NULL) | |
FATAL_ERROR("Failed to create PNG info struct.\n"); | |
if (setjmp(png_jmpbuf(pngReader))) | |
FATAL_ERROR("Failed to init I/O for reading \"%s\".\n", path); | |
png_init_io(pngReader, file); | |
png_set_sig_bytes(pngReader, 8); | |
png_read_info(pngReader, pngInfo); | |
// Get info | |
image->width = png_get_image_width(pngReader, pngInfo); | |
image->height = png_get_image_height(pngReader, pngInfo); | |
image->colorType = png_get_color_type(pngReader, pngInfo); | |
image->bitDepth = png_get_bit_depth(pngReader, pngInfo); | |
if (image->colorType != PNG_COLOR_TYPE_PALETTE && image->colorType != PNG_COLOR_TYPE_GRAY) | |
FATAL_ERROR("error: only paletted and grayscale images are supported"); | |
printf("tile width: %u\n", tileWidth); | |
printf("image dimensions: %ux%u\n", image->width, image->height); | |
printf("image bit depth: %u\n", image->bitDepth); | |
// Read palette | |
if (image->colorType == PNG_COLOR_TYPE_PALETTE) | |
ReadPngPalette(image, pngReader, pngInfo); | |
// Read pixels | |
image->rowBytes = png_get_rowbytes(pngReader, pngInfo); | |
image->pixels = malloc(image->height * image->rowBytes); | |
rowPointers = malloc(image->height * sizeof(*rowPointers)); | |
for (i = 0; i < image->height; i++) | |
rowPointers[i] = image->pixels + (i * image->rowBytes); | |
if (setjmp(png_jmpbuf(pngReader))) | |
FATAL_ERROR("Error reading image\n"); | |
png_read_image(pngReader, rowPointers); | |
free(rowPointers); | |
fclose(file); | |
} | |
static void WriteImageToFile(struct Image *image, const char *path) | |
{ | |
FILE *fp = fopen(path, "wb"); | |
if (fp == NULL) | |
FATAL_ERROR("Failed to open \"%s\" for writing.\n", path); | |
png_structp pngReader = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); | |
if (!pngReader) | |
FATAL_ERROR("Failed to create PNG write struct.\n"); | |
png_infop pngInfo = png_create_info_struct(pngReader); | |
if (!pngInfo) | |
FATAL_ERROR("Failed to create PNG info struct.\n"); | |
if (setjmp(png_jmpbuf(pngReader))) | |
FATAL_ERROR("Failed to init I/O for writing \"%s\".\n", path); | |
png_init_io(pngReader, fp); | |
if (setjmp(png_jmpbuf(pngReader))) | |
FATAL_ERROR("Error writing header for \"%s\".\n", path); | |
png_set_IHDR(pngReader, pngInfo, image->width, image->height, | |
image->bitDepth, image->colorType, PNG_INTERLACE_NONE, | |
PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); | |
if (image->colorType == PNG_COLOR_TYPE_PALETTE) | |
WritePngPalette(image, pngReader, pngInfo); | |
png_write_info(pngReader, pngInfo); | |
png_bytepp row_pointers = malloc(image->height * sizeof(png_bytep)); | |
if (row_pointers == NULL) | |
FATAL_ERROR("Failed to allocate row pointers.\n"); | |
int rowbytes = png_get_rowbytes(pngReader, pngInfo); | |
for (int i = 0; i < image->height; i++) | |
row_pointers[i] = (png_bytep)(image->pixels + (i * rowbytes)); | |
if (setjmp(png_jmpbuf(pngReader))) | |
FATAL_ERROR("Error writing \"%s\".\n", path); | |
png_write_image(pngReader, row_pointers); | |
if (setjmp(png_jmpbuf(pngReader))) | |
FATAL_ERROR("Error ending write of \"%s\".\n", path); | |
png_write_end(pngReader, NULL); | |
fclose(fp); | |
png_destroy_write_struct(&pngReader, &pngInfo); | |
free(row_pointers); | |
} | |
static unsigned char *GetTileStartAddr(struct Image *image, unsigned int row, unsigned int col) | |
{ | |
unsigned int x = col * tileWidth; | |
unsigned int y = row * tileWidth; | |
return image->pixels + (x + y * image->width) * image->bitDepth / 8; | |
} | |
static void CreateTile(struct Tile *tile, unsigned int row, unsigned int col) | |
{ | |
const size_t srcRowSize = inputImage.width * inputImage.bitDepth / 8; | |
const size_t tileRowSize = tileWidth * inputImage.bitDepth / 8; | |
unsigned int i, j; | |
unsigned char *src = GetTileStartAddr(&inputImage, row, col); | |
unsigned int hash = 0; | |
for (i = 0; i < tileWidth; i++) | |
{ | |
for (j = 0; j < tileRowSize; j++) | |
hash += src[j]; | |
src += srcRowSize; | |
} | |
tile->row = row; | |
tile->col = col; | |
tile->hash = hash; | |
} | |
static int FindTile(struct Tile *tile) | |
{ | |
unsigned int currTile; | |
for (currTile = 0; currTile < tilesCount; currTile++) | |
{ | |
if (tile->hash == tiles[currTile].hash) | |
{ | |
const size_t imageRowSize = inputImage.width * inputImage.bitDepth / 8; | |
const size_t tileRowSize = tileWidth * inputImage.bitDepth / 8; | |
unsigned int i; | |
unsigned char *src = inputImage.pixels + tile->row * tileWidth * imageRowSize + tile->col * tileRowSize; | |
unsigned char *dest = inputImage.pixels + tiles[currTile].row * tileWidth * imageRowSize + tiles[currTile].col * tileRowSize; | |
for (i = 0; i < tileWidth; i++) | |
{ | |
if (memcmp(src, dest, tileRowSize) != 0) | |
goto check_next; | |
src += imageRowSize; | |
dest += imageRowSize; | |
} | |
return currTile; | |
} | |
check_next: | |
; | |
} | |
return -1; | |
} | |
void ReadImageTiles(const char *fname) | |
{ | |
unsigned int row, col; | |
unsigned int maxEntry = (1 << mapBitsPerEntry) - 1; | |
ReadImageFromFile(&inputImage, fname); | |
unsigned int numTilesX = inputImage.width / tileWidth; | |
unsigned int numTilesY = inputImage.height / tileWidth; | |
tileMap = malloc(numTilesX * numTilesY * mapBitsPerEntry / 8); | |
tiles = malloc(sizeof(*tiles) * numTilesX * numTilesY); | |
tilesCount = 0; | |
for (row = 0; row < numTilesY; row++) | |
{ | |
for (col = 0; col < numTilesX; col++) | |
{ | |
struct Tile tile; | |
CreateTile(&tile, row, col); | |
int tileNum = FindTile(&tile); | |
if (tileNum == -1) // this is a new tile | |
{ | |
tileNum = tilesCount; | |
tiles[tilesCount++] = tile; | |
} | |
if (tileNum > maxEntry) | |
FATAL_ERROR("Image has more than %u tiles\n", maxEntry); | |
if (mapBitsPerEntry == 8) | |
((uint8_t *)tileMap)[row * numTilesX + col] = tileNum; | |
else if (mapBitsPerEntry == 16) | |
((uint16_t *)tileMap)[row * numTilesX + col] = tileNum; | |
else | |
assert(0); | |
} | |
} | |
} | |
void OutputTileSet(char *fname) | |
{ | |
unsigned int i, j; | |
const unsigned int outputTilesX = outputWidth; | |
const unsigned int outputTilesY = (tilesCount + outputTilesX - 1) / outputTilesX; | |
struct Image outputImage = inputImage; | |
outputImage.width = outputTilesX * tileWidth; | |
outputImage.height = outputTilesY * tileWidth; | |
outputImage.rowBytes = outputImage.width * outputImage.bitDepth / 8; | |
outputImage.pixels = malloc(outputImage.rowBytes * outputImage.height); | |
for (i = 0; i < tilesCount; i++) | |
{ | |
unsigned int row = i / outputTilesX; | |
unsigned int col = i % outputTilesX; | |
unsigned char *src = GetTileStartAddr(&inputImage, tiles[i].row, tiles[i].col); | |
unsigned char *dest = GetTileStartAddr(&outputImage, row, col); | |
for (j = 0; j < tileWidth; j++) | |
{ | |
memcpy(dest, src, tileWidth * outputImage.bitDepth / 8); | |
src += inputImage.rowBytes; | |
dest += outputImage.rowBytes; | |
} | |
} | |
WriteImageToFile(&outputImage, fname); | |
} | |
void OutputTileMap(char *fname) | |
{ | |
FILE *file = fopen(fname, "wb"); | |
unsigned int numEntries = (inputImage.width / tileWidth) * (inputImage.height / tileWidth); | |
size_t size = numEntries * mapBitsPerEntry / 8; | |
fwrite(tileMap, size, 1, file); | |
fclose(file); | |
} | |
char *GetFileNameExtension(char *fname) | |
{ | |
char *ext = strrchr(fname, '.'); | |
if (ext != NULL) | |
return ext; | |
else | |
return fname + strlen(fname); | |
} | |
int main(int argc, char **argv) | |
{ | |
int i; | |
char *fname = NULL; | |
char *outfname; | |
char *ext; | |
for (i = 1; i < argc; i++) | |
{ | |
char *arg = argv[i]; | |
if (strcmp("--tile_width", arg) == 0) | |
{ | |
i++; | |
if (i >= argc || sscanf(argv[i], "%u", &tileWidth) != 1) | |
FATAL_ERROR("error: expected number after --tile_width\n"); | |
} | |
else if (strcmp("--map_bits", arg) == 0) | |
{ | |
i++; | |
if (i >= argc || sscanf(argv[i], "%u", &mapBitsPerEntry) != 1) | |
FATAL_ERROR("error: expected number after --map_bits\n"); | |
} | |
else if (strcmp("--output_width", arg) == 0) | |
{ | |
i++; | |
if (i >= argc || sscanf(argv[i], "%u", &outputWidth) != 1) | |
FATAL_ERROR("error: expected number after --output_width\n"); | |
} | |
else | |
{ | |
fname = arg; | |
} | |
} | |
if (fname == NULL) | |
FATAL_ERROR("error: no input file specified\n"); | |
outfname = malloc(strlen(fname) + 11); | |
strcpy(outfname, fname); | |
ext = GetFileNameExtension(outfname); | |
ReadImageTiles(fname); | |
strcpy(ext, "_tiles.png"); | |
OutputTileSet(outfname); | |
strcpy(ext, "_map.bin"); | |
OutputTileMap(outfname); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment