joeylib2/tools/joeyasset/joeyasset.c

306 lines
8.8 KiB
C

// joeyasset: convert a PPM (P6) bitmap into JoeyLib's .jas asset
// format. Designed for hosts with a C99 compiler; no external library
// dependencies.
//
// PPM is a stable, trivially parseable RGB format. ImageMagick / GIMP /
// Photoshop / paint.net all export it directly, so the conversion
// pipeline from a PNG is one extra command:
//
// convert sprite.png sprite.ppm # ImageMagick / GraphicsMagick
// joeyasset sprite.ppm sprite.jas
//
// Quantization rule for v1: every input RGB triple is reduced to the
// IIgs-style 4-bit-per-channel $0RGB form by taking the high nibble of
// each channel. After that reduction the input is required to use no
// more than 16 distinct $0RGB colors. We do NOT dither or do real
// colorspace quantization yet -- inputs are expected to already use a
// 16-color palette. If you need dithering, run the input through a
// quantizer (gimp, pngquant, etc.) first.
//
// Output format is documented in include/joey/asset.h.
#include <ctype.h>
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define JAS_MAGIC0 'J'
#define JAS_MAGIC1 'A'
#define JAS_MAGIC2 'S'
#define JAS_MAGIC3 '1'
#define JAS_PALETTE_ENTRIES 16
#define JAS_HEADER_SIZE 44
#define PPM_TOKEN_MAX 64
static int parsePpmToken(FILE *fp, char *out, int outLen);
static int loadPpm(const char *path, int *outWidth, int *outHeight, uint8_t **outPixels);
static int buildPalette(const uint8_t *rgb, int width, int height, uint16_t *outPalette, uint8_t *outIndices);
static int writeJas(const char *path, int width, int height, const uint16_t *palette, const uint8_t *indices);
static void writeLE16(uint8_t *p, uint16_t v);
static void writeLE16(uint8_t *p, uint16_t v) {
p[0] = (uint8_t)(v & 0xFF);
p[1] = (uint8_t)((v >> 8) & 0xFF);
}
// Reads a single whitespace-separated token from a PPM header,
// skipping comments (# to end-of-line).
static int parsePpmToken(FILE *fp, char *out, int outLen) {
int c;
int pos;
pos = 0;
for (;;) {
c = fgetc(fp);
if (c == EOF) {
return -1;
}
if (isspace(c)) {
continue;
}
if (c == '#') {
while ((c = fgetc(fp)) != EOF && c != '\n') {
/* skip */;
}
continue;
}
break;
}
while (c != EOF && !isspace(c) && c != '#') {
if (pos < outLen - 1) {
out[pos++] = (char)c;
}
c = fgetc(fp);
}
out[pos] = 0;
return 0;
}
static int loadPpm(const char *path, int *outWidth, int *outHeight, uint8_t **outPixels) {
FILE *fp;
char tok[PPM_TOKEN_MAX];
int width;
int height;
int maxval;
size_t pixelBytes;
uint8_t *buf;
size_t read;
fp = fopen(path, "rb");
if (fp == NULL) {
fprintf(stderr, "joeyasset: cannot open %s: %s\n", path, strerror(errno));
return 1;
}
if (parsePpmToken(fp, tok, sizeof(tok)) != 0 || strcmp(tok, "P6") != 0) {
fprintf(stderr, "joeyasset: %s is not a PPM (P6) file\n", path);
fclose(fp);
return 1;
}
if (parsePpmToken(fp, tok, sizeof(tok)) != 0) {
fclose(fp);
return 1;
}
width = atoi(tok);
if (parsePpmToken(fp, tok, sizeof(tok)) != 0) {
fclose(fp);
return 1;
}
height = atoi(tok);
if (parsePpmToken(fp, tok, sizeof(tok)) != 0) {
fclose(fp);
return 1;
}
maxval = atoi(tok);
if (width <= 0 || height <= 0) {
fprintf(stderr, "joeyasset: %s has non-positive dimensions\n", path);
fclose(fp);
return 1;
}
if (maxval != 255) {
fprintf(stderr, "joeyasset: %s maxval %d unsupported (must be 255)\n", path, maxval);
fclose(fp);
return 1;
}
// PPM raster begins after exactly one whitespace byte following the
// maxval token (already consumed by parsePpmToken's trailing read).
// Re-read one byte? No -- parsePpmToken stops after consuming the
// whitespace, so we are positioned at the first raster byte.
pixelBytes = (size_t)width * (size_t)height * 3u;
buf = (uint8_t *)malloc(pixelBytes);
if (buf == NULL) {
fprintf(stderr, "joeyasset: out of memory (%zu bytes)\n", pixelBytes);
fclose(fp);
return 1;
}
read = fread(buf, 1, pixelBytes, fp);
fclose(fp);
if (read != pixelBytes) {
fprintf(stderr, "joeyasset: short raster in %s (got %zu, need %zu)\n",
path, read, pixelBytes);
free(buf);
return 1;
}
*outWidth = width;
*outHeight = height;
*outPixels = buf;
return 0;
}
// Reduce every input RGB triple to a 12-bit $0RGB color. Up to 16
// distinct colors are accepted; the first color encountered is
// assigned palette index 0, the second index 1, etc. Returns the
// number of distinct colors found, or -1 if more than 16.
static int buildPalette(const uint8_t *rgb, int width, int height, uint16_t *outPalette, uint8_t *outIndices) {
int total;
int paletteCount;
int i;
int j;
uint8_t r;
uint8_t g;
uint8_t b;
uint16_t color;
total = width * height;
paletteCount = 0;
for (i = 0; i < JAS_PALETTE_ENTRIES; i++) {
outPalette[i] = 0x0000;
}
for (i = 0; i < total; i++) {
r = rgb[i * 3 + 0] >> 4;
g = rgb[i * 3 + 1] >> 4;
b = rgb[i * 3 + 2] >> 4;
color = (uint16_t)((r << 8) | (g << 4) | b);
for (j = 0; j < paletteCount; j++) {
if (outPalette[j] == color) {
break;
}
}
if (j == paletteCount) {
if (paletteCount >= JAS_PALETTE_ENTRIES) {
return -1;
}
outPalette[paletteCount] = color;
paletteCount++;
}
outIndices[i] = (uint8_t)j;
}
return paletteCount;
}
static int writeJas(const char *path, int width, int height, const uint16_t *palette, const uint8_t *indices) {
FILE *fp;
uint8_t header[JAS_HEADER_SIZE];
int rowBytes;
int x;
int y;
uint8_t byte;
int written;
memset(header, 0, sizeof(header));
header[0] = JAS_MAGIC0;
header[1] = JAS_MAGIC1;
header[2] = JAS_MAGIC2;
header[3] = JAS_MAGIC3;
writeLE16(header + 4, (uint16_t)width);
writeLE16(header + 6, (uint16_t)height);
header[8] = 1;
for (x = 0; x < JAS_PALETTE_ENTRIES; x++) {
writeLE16(header + 12 + (x * 2), palette[x]);
}
fp = fopen(path, "wb");
if (fp == NULL) {
fprintf(stderr, "joeyasset: cannot create %s: %s\n", path, strerror(errno));
return 1;
}
if (fwrite(header, 1, sizeof(header), fp) != sizeof(header)) {
fprintf(stderr, "joeyasset: short write to %s\n", path);
fclose(fp);
return 1;
}
rowBytes = (width + 1) >> 1;
written = 0;
for (y = 0; y < height; y++) {
for (x = 0; x < rowBytes; x++) {
int p0;
int p1;
p0 = x * 2;
p1 = p0 + 1;
byte = (uint8_t)((indices[y * width + p0] & 0x0F) << 4);
if (p1 < width) {
byte = (uint8_t)(byte | (indices[y * width + p1] & 0x0F));
}
if (fputc(byte, fp) == EOF) {
fprintf(stderr, "joeyasset: short write to %s\n", path);
fclose(fp);
return 1;
}
written++;
}
}
fclose(fp);
return 0;
}
int main(int argc, char **argv) {
int width;
int height;
uint8_t *rgb;
uint8_t *indices;
uint16_t palette[JAS_PALETTE_ENTRIES];
int paletteCount;
int rc;
if (argc != 3) {
fprintf(stderr, "usage: joeyasset INPUT.ppm OUTPUT.jas\n");
return 2;
}
if (loadPpm(argv[1], &width, &height, &rgb) != 0) {
return 1;
}
indices = (uint8_t *)malloc((size_t)width * (size_t)height);
if (indices == NULL) {
fprintf(stderr, "joeyasset: out of memory for index buffer\n");
free(rgb);
return 1;
}
paletteCount = buildPalette(rgb, width, height, palette, indices);
if (paletteCount < 0) {
fprintf(stderr,
"joeyasset: input has more than 16 distinct $0RGB colors after\n"
" 4-bit-per-channel quantization. Reduce the input palette and\n"
" retry (e.g. pngquant --nofs 16, or GIMP -> Image -> Mode ->\n"
" Indexed... with 16 colors and no dithering).\n");
free(rgb);
free(indices);
return 1;
}
rc = writeJas(argv[2], width, height, palette, indices);
if (rc == 0) {
printf("joeyasset: wrote %s (%d x %d, %d color%s)\n",
argv[2], width, height, paletteCount,
paletteCount == 1 ? "" : "s");
}
free(rgb);
free(indices);
return rc;
}