306 lines
8.8 KiB
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;
|
|
}
|