// 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 #include #include #include #include #include #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; }