// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // gateImage.c - screenshots and icons for the gate tools (gateImage.h). // // The rules the pictures are read by (checked once against the Python // gates these tools replaced): // - A PNG goes through toolPng to RGBA: grey spread to RGB, 16-bit // samples cut to their high byte, palette entries looked up. // - A BMP is an uncompressed DIB: 1, 4, or 8 bits through its colour // table (an index past the table is black), or 24 or 32 bits of BGR; it // is opaque. // - An ICO's directory is sorted largest picture first and, among equals, // fewest colours first (a stable sort); its default picture is the first. // A picture that is a PNG is decoded as one. A DIB picture is its XOR // bitmap (half the height its header gives), its alpha the 32-bit // picture's fourth bytes or else its AND mask (a set bit transparent), // which is the last bytes of the picture as the directory sizes it. #include #include #include #include "gateArg.h" #include "gateImage.h" #include "toolFile.h" #include "toolPng.h" #define BYTE_BITS 8u #define LE16_BYTES 2u #define SAMPLE_MAX 255u #define RED_SHIFT 16u #define GREEN_SHIFT 8u #define NIBBLE_MASK 0xFu #define NIBBLE_BITS 4u // A 4-bit gun widened to 8 bits: 0xF x 17 is 0xFF. #define NIBBLE_WIDEN 17u #define PIXELS_INITIAL 256u // KEYS's palette 0: a grey ramp (n in each gun), then black, dark grey, // green, and white at 0 to 3 and red at 15 (examples/keys buildPalette). #define KEYS_BLACK 0x000u #define KEYS_UNLIT 0x333u #define KEYS_LIT 0x0F0u #define KEYS_CURSOR 0xFFFu #define KEYS_RED 0xF00u #define KEYS_RED_INDEX 15u #define KEYS_SENTINEL 0xA5u #define KEYS_COUNT_MAX 255u #define ASCII_MAX 0x7Fu // File magic. #define PNG_MAGIC "\x89PNG\r\n\x1a\n" #define PNG_MAGIC_BYTES 8u #define BMP_MAGIC "BM" #define BMP_MAGIC_BYTES 2u #define ICO_MAGIC "\0\0\1\0" #define ICO_MAGIC_BYTES 4u // BMP: the file header, and where its pixels start. #define BMP_FILE_HEADER 14u #define BMP_PIXELS_AT 10u // A DIB's BITMAPINFOHEADER and colour table. #define DIB_HEADER_MIN 40u #define DIB_WIDTH_AT 4u #define DIB_HEIGHT_AT 8u #define DIB_BITS_AT 14u #define DIB_COMPRESSION_AT 16u #define DIB_COLOURS_AT 32u #define DIB_RGB 0u #define DIB_ENTRY_BYTES 4u #define DIB_ROW_BITS 32u #define DIB_BLUE 0u #define DIB_GREEN 1u #define DIB_RED 2u #define DIB_PALETTE_MAX 256u #define BITS_1 1u #define BITS_4 4u #define BITS_8 8u #define BITS_24 24u #define BITS_32 32u #define TOP_BIT 0x80u // ICO: the directory and its entries (a 0 side means 256). #define ICO_COUNT_AT 4u #define ICO_DIR_BYTES 6u #define ICO_ENTRY_BYTES 16u #define ICO_ENTRY_W 0u #define ICO_ENTRY_H 1u #define ICO_ENTRY_COLOURS 2u #define ICO_ENTRY_BPP 6u #define ICO_ENTRY_SIZE 8u #define ICO_ENTRY_OFFSET 12u #define ICO_SIDE_ZERO 256u #define ICO_DEPTH_NONE 256u // gateImageLoad's ICO picture: the default, the sorted directory's first; // and a picture of any depth. #define ICO_DEFAULT_SIDE 0u #define ICO_ANY_BPP 0u // A DIB's layout: the picture's size (the XOR half of an icon's), its bits // a pixel, row order, and where its rows and its colour table are. typedef struct DibT { uint32_t width; uint32_t height; uint32_t bits; bool bottomUp; size_t paletteAt; uint32_t paletteCount; size_t pixelsAt; size_t stride; } DibT; // An ICO directory entry: its picture's size and depth, and where it is. typedef struct IcoEntryT { uint32_t width; uint32_t height; uint32_t colourDepth; uint32_t bpp; uint32_t size; uint32_t offset; } IcoEntryT; static void bmpDecode(const uint8_t *data, size_t len, const char *path, GateImageT *img); static void dibDecode(const uint8_t *data, size_t len, const DibT *dib, const char *path, GateImageT *img); static void dibRead(const uint8_t *data, size_t len, size_t headerAt, size_t pixelsAt, bool icon, const char *path, DibT *dib); static void icoAndMask(const uint8_t *data, size_t len, const IcoEntryT *entry, const char *path, GateImageT *img); static void icoDecode(const uint8_t *data, size_t len, uint32_t side, uint32_t bpp, const char *path, GateImageT *img); static uint32_t icoDepth(uint32_t bpp, uint32_t colours); static bool icoEntryBefore(const IcoEntryT *a, const IcoEntryT *b); static uint32_t iconAnchor(const GateImageT *screen, const GateIconT *icon, uint32_t tolerance); static bool iconAt(const GateImageT *screen, const GateIconT *icon, uint32_t tolerance, uint32_t left, uint32_t top); static void imageAlloc(GateImageT *img, uint32_t width, uint32_t height); static bool magic(const uint8_t *data, size_t len, const char *bytes, size_t count); static int packedCompare(const void *a, const void *b); static void pngDecode(const uint8_t *data, size_t len, const char *path, GateImageT *img); static bool within(const uint8_t *a, const uint8_t *b, uint32_t tolerance); // A BMP file: its DIB, whose rows start where the file header says. static void bmpDecode(const uint8_t *data, size_t len, const char *path, GateImageT *img) { DibT dib; if (len < BMP_FILE_HEADER) { toolFail("%s: truncated BMP", path); } dibRead(data, len, BMP_FILE_HEADER, gateLe32(data + BMP_PIXELS_AT), false, path, &dib); dibDecode(data, len, &dib, path, img); } // A DIB's rows as RGBA, opaque. static void dibDecode(const uint8_t *data, size_t len, const DibT *dib, const char *path, GateImageT *img) { const uint8_t *row; const uint8_t *entry; uint8_t *out; uint32_t x; uint32_t y; uint32_t index; uint32_t shift; if (dib->pixelsAt > len || dib->stride * dib->height > len - dib->pixelsAt) { toolFail("%s: truncated picture", path); } imageAlloc(img, dib->width, dib->height); for (y = 0; y < dib->height; y++) { row = data + dib->pixelsAt + dib->stride * (dib->bottomUp ? dib->height - 1u - y : y); for (x = 0; x < dib->width; x++) { out = img->rgba + ((size_t)y * dib->width + x) * GATE_RGBA_BYTES; if (dib->bits >= BITS_24) { entry = row + (size_t)x * (dib->bits / BYTE_BITS); } else { shift = BYTE_BITS - dib->bits - (x * dib->bits) % BYTE_BITS; index = (row[(x * dib->bits) / BYTE_BITS] >> shift) & ((1u << dib->bits) - 1u); entry = (index < dib->paletteCount) ? data + dib->paletteAt + (size_t)index * DIB_ENTRY_BYTES : NULL; } if (entry != NULL) { out[0] = entry[DIB_RED]; out[1] = entry[DIB_GREEN]; out[2] = entry[DIB_BLUE]; } out[GATE_ALPHA] = SAMPLE_MAX; } } } // A DIB's BITMAPINFOHEADER at headerAt: an icon's picture is the upper // half of the height it gives, and its rows follow its colour table // (pixelsAt 0); a BMP file says where its rows are. static void dibRead(const uint8_t *data, size_t len, size_t headerAt, size_t pixelsAt, bool icon, const char *path, DibT *dib) { const uint8_t *h; uint32_t headerBytes; int32_t height; uint32_t colours; if (headerAt > len || len - headerAt < DIB_HEADER_MIN) { toolFail("%s: truncated bitmap header", path); } h = data + headerAt; headerBytes = gateLe32(h); height = (int32_t)gateLe32(h + DIB_HEIGHT_AT); dib->width = gateLe32(h + DIB_WIDTH_AT); dib->bits = gateLe16(h + DIB_BITS_AT); colours = gateLe32(h + DIB_COLOURS_AT); if (headerBytes < DIB_HEADER_MIN || gateLe32(h + DIB_COMPRESSION_AT) != DIB_RGB || (int32_t)dib->width <= 0 || height == 0) { toolFail("%s: not an uncompressed bitmap", path); } if (dib->bits != BITS_1 && dib->bits != BITS_4 && dib->bits != BITS_8 && dib->bits != BITS_24 && dib->bits != BITS_32) { toolFail("%s: %u bits a pixel is not read", path, (unsigned)dib->bits); } dib->bottomUp = height > 0; dib->height = (uint32_t)(height > 0 ? height : -height); dib->paletteAt = headerAt + headerBytes; dib->paletteCount = 0u; if (dib->bits <= BITS_8) { dib->paletteCount = (colours != 0u) ? colours : (1u << dib->bits); if (dib->paletteCount > DIB_PALETTE_MAX || dib->paletteAt > len || (size_t)dib->paletteCount * DIB_ENTRY_BYTES > len - dib->paletteAt) { toolFail("%s: bad colour table", path); } } if (icon) { dib->height /= 2u; } dib->pixelsAt = (pixelsAt != 0u) ? pixelsAt : dib->paletteAt + (size_t)dib->paletteCount * DIB_ENTRY_BYTES; dib->stride = (((size_t)dib->width * dib->bits + DIB_ROW_BITS - 1u) / DIB_ROW_BITS) * (DIB_ROW_BITS / BYTE_BITS); } uint32_t gateDistance(const uint8_t *a, const uint8_t *b) { uint32_t sum; int32_t d; uint32_t i; sum = 0u; for (i = 0; i < GATE_RGB_BYTES; i++) { d = (int32_t)a[i] - (int32_t)b[i]; sum += (uint32_t)(d * d); } return sum; } void gateHexPrint(const uint8_t *data, size_t len) { size_t i; for (i = 0; i < len; i++) { printf("%02X", (unsigned)data[i]); } } void gateIconAdd(GateIconT *icon, uint32_t x, uint32_t y, const uint8_t *rgb) { GateIconPixelT *grown; if (icon->count == icon->cap) { icon->cap = (icon->cap == 0u) ? PIXELS_INITIAL : icon->cap * 2u; grown = (GateIconPixelT *)realloc(icon->pixels, (size_t)icon->cap * sizeof(GateIconPixelT)); if (grown == NULL) { toolFail("out of memory"); } icon->pixels = grown; } icon->pixels[icon->count].x = x; icon->pixels[icon->count].y = y; memcpy(icon->pixels[icon->count].rgb, rgb, GATE_RGB_BYTES); icon->count++; } bool gateIconFind(const GateImageT *screen, const GateIconT *icon, uint32_t tolerance, uint32_t *left, uint32_t *top) { const GateIconPixelT *a; uint32_t x; uint32_t y; if (icon->count == 0u) { toolFail("the icon has no opaque pixel"); } a = &icon->pixels[iconAnchor(screen, icon, tolerance)]; for (y = a->y; y < screen->height; y++) { for (x = a->x; x < screen->width; x++) { if (within(gateImagePixel(screen, x, y), a->rgb, tolerance) && iconAt(screen, icon, tolerance, x - a->x, y - a->y)) { *left = x - a->x; *top = y - a->y; return true; } } } return false; } void gateIconFree(GateIconT *icon) { free(icon->pixels); icon->pixels = NULL; icon->count = 0u; icon->cap = 0u; } void gateImageFree(GateImageT *img) { free(img->rgba); img->rgba = NULL; } bool gateImageHas(const GateImageT *img, long x, long y) { return x >= 0 && y >= 0 && x < (long)img->width && y < (long)img->height; } void gateImageLoad(const char *path, GateImageT *img) { gateImageLoadIcon(path, ICO_DEFAULT_SIDE, ICO_ANY_BPP, img); } // A picture file by its magic: PNG, BMP, or ICO (side and bpp choose an // ICO's picture). void gateImageLoadIcon(const char *path, uint32_t side, uint32_t bpp, GateImageT *img) { uint8_t *data; size_t len; if (!toolReadFile(path, &data, &len)) { toolFail("cannot read %s", path); } if (magic(data, len, PNG_MAGIC, PNG_MAGIC_BYTES)) { pngDecode(data, len, path, img); } else if (magic(data, len, BMP_MAGIC, BMP_MAGIC_BYTES)) { bmpDecode(data, len, path, img); } else if (magic(data, len, ICO_MAGIC, ICO_MAGIC_BYTES)) { icoDecode(data, len, side, bpp, path, img); } else { toolFail("cannot identify image file %s", path); } free(data); } const uint8_t *gateImagePixel(const GateImageT *img, uint32_t x, uint32_t y) { return img->rgba + ((size_t)y * img->width + x) * GATE_RGBA_BYTES; } void gateKeysExpect(const char *target, ToolBufT *expect) { size_t len; size_t i; bool ascii; len = strlen(target); ascii = true; for (i = 0; i < len; i++) { ascii = ascii && (uint8_t)target[i] <= ASCII_MAX; } if (!ascii || len > KEYS_COUNT_MAX) { gateArgBad("target", target); } toolBufByte(expect, KEYS_SENTINEL); toolBufByte(expect, KEYS_SENTINEL); toolBufByte(expect, (uint32_t)len); toolBufByte(expect, 0u); toolBufAdd(expect, target, len); } void gateKeysPalette(uint16_t words[GATE_KEYS_COLOURS]) { uint32_t i; for (i = 0; i < GATE_KEYS_COLOURS; i++) { words[i] = (uint16_t)((i << (2u * NIBBLE_BITS)) | (i << NIBBLE_BITS) | i); } words[0] = KEYS_BLACK; words[1] = KEYS_UNLIT; words[2] = KEYS_LIT; words[3] = KEYS_CURSOR; words[KEYS_RED_INDEX] = KEYS_RED; } uint32_t gateLe16(const uint8_t *p) { return (uint32_t)p[0] | ((uint32_t)p[1] << BYTE_BITS); } uint32_t gateLe32(const uint8_t *p) { return gateLe16(p) | (gateLe16(p + LE16_BYTES) << (LE16_BYTES * BYTE_BITS)); } uint32_t gateNearest(const uint8_t *rgb, const uint8_t (*palette)[GATE_RGB_BYTES], uint32_t count, uint32_t *distance) { uint32_t best; uint32_t bestDistance; uint32_t d; uint32_t i; best = 0u; bestDistance = 0u; for (i = 0; i < count; i++) { d = gateDistance(rgb, palette[i]); if (i == 0u || d < bestDistance) { best = i; bestDistance = d; } } *distance = bestDistance; return best; } void gateRgb12(uint32_t word, uint8_t *rgb) { rgb[0] = (uint8_t)(((word >> (2u * NIBBLE_BITS)) & NIBBLE_MASK) * NIBBLE_WIDEN); rgb[1] = (uint8_t)(((word >> NIBBLE_BITS) & NIBBLE_MASK) * NIBBLE_WIDEN); rgb[2] = (uint8_t)((word & NIBBLE_MASK) * NIBBLE_WIDEN); } // The alpha of an icon's picture of fewer than 32 bits, from its AND mask: // the last bytes of the picture's, rows padded to 32 bits, bottom row // first; a set bit is transparent. static void icoAndMask(const uint8_t *data, size_t len, const IcoEntryT *entry, const char *path, GateImageT *img) { uint8_t *out; size_t rowBytes; size_t total; size_t maskAt; uint32_t x; uint32_t y; uint8_t bits; rowBytes = (((size_t)img->width + DIB_ROW_BITS - 1u) / DIB_ROW_BITS) * (DIB_ROW_BITS / BYTE_BITS); total = rowBytes * img->height; if ((size_t)entry->offset + entry->size > len || total > (size_t)entry->offset + entry->size) { toolFail("%s: truncated icon mask", path); } maskAt = (size_t)entry->offset + entry->size - total; for (y = 0; y < img->height; y++) { for (x = 0; x < img->width; x++) { bits = data[maskAt + rowBytes * (img->height - 1u - y) + x / BYTE_BITS]; out = img->rgba + ((size_t)y * img->width + x) * GATE_RGBA_BYTES; out[GATE_ALPHA] = (bits & (TOP_BIT >> (x % BYTE_BITS))) ? 0u : SAMPLE_MAX; } } } // An ICO's picture: the first in the sorted directory side x side at bpp // bits (any bits for ICO_ANY_BPP), else (or for side ICO_DEFAULT_SIDE) the // first of all. static void icoDecode(const uint8_t *data, size_t len, uint32_t side, uint32_t bpp, const char *path, GateImageT *img) { IcoEntryT *entries; IcoEntryT held; const uint8_t *e; uint32_t count; uint32_t i; uint32_t j; uint32_t chosen; uint32_t x; uint32_t y; DibT dib; count = gateLe16(data + ICO_COUNT_AT); if (count == 0u || len < ICO_DIR_BYTES + (size_t)count * ICO_ENTRY_BYTES) { toolFail("%s: truncated icon directory", path); } entries = (IcoEntryT *)malloc((size_t)count * sizeof(IcoEntryT)); if (entries == NULL) { toolFail("out of memory"); } for (i = 0; i < count; i++) { e = data + ICO_DIR_BYTES + (size_t)i * ICO_ENTRY_BYTES; entries[i].width = (e[ICO_ENTRY_W] != 0u) ? e[ICO_ENTRY_W] : ICO_SIDE_ZERO; entries[i].height = (e[ICO_ENTRY_H] != 0u) ? e[ICO_ENTRY_H] : ICO_SIDE_ZERO; entries[i].bpp = gateLe16(e + ICO_ENTRY_BPP); entries[i].colourDepth = icoDepth(entries[i].bpp, e[ICO_ENTRY_COLOURS]); entries[i].size = gateLe32(e + ICO_ENTRY_SIZE); entries[i].offset = gateLe32(e + ICO_ENTRY_OFFSET); // An insertion sort, which keeps equals in the directory's order. held = entries[i]; for (j = i; j > 0u && icoEntryBefore(&held, &entries[j - 1u]); j--) { entries[j] = entries[j - 1u]; } entries[j] = held; } chosen = 0u; for (i = 0; i < count && side != ICO_DEFAULT_SIDE; i++) { if (entries[i].width == side && entries[i].height == side && (bpp == ICO_ANY_BPP || bpp == entries[i].colourDepth)) { chosen = i; break; } } held = entries[chosen]; free(entries); if (held.offset >= len) { toolFail("%s: truncated icon", path); } if (magic(data + held.offset, len - held.offset, PNG_MAGIC, PNG_MAGIC_BYTES)) { pngDecode(data + held.offset, len - held.offset, path, img); return; } dibRead(data, len, held.offset, 0u, true, path, &dib); dibDecode(data, len, &dib, path, img); if (held.bpp != BITS_32) { icoAndMask(data, len, &held, path, img); return; } for (y = 0; y < img->height; y++) { for (x = 0; x < img->width; x++) { img->rgba[((size_t)y * img->width + x) * GATE_RGBA_BYTES + GATE_ALPHA] = data[dib.pixelsAt + ((size_t)(img->height - 1u - y) * img->width + x) * GATE_RGBA_BYTES + GATE_ALPHA]; } } } // An ICO entry's colour depth: its bits a pixel, or else the bits its // colour count needs, or else ICO_DEPTH_NONE. static uint32_t icoDepth(uint32_t bpp, uint32_t colours) { uint32_t depth; if (bpp != 0u) { return bpp; } depth = 0u; while (depth < BITS_32 && (1u << depth) < colours) { depth++; } return (depth != 0u) ? depth : ICO_DEPTH_NONE; } // Whether entry a goes before b: bigger first, then fewer colours. static bool icoEntryBefore(const IcoEntryT *a, const IcoEntryT *b) { uint32_t squareA; uint32_t squareB; squareA = a->width * a->height; squareB = b->width * b->height; if (squareA != squareB) { return squareA > squareB; } return a->colourDepth < b->colourDepth; } // The icon's opaque pixel whose colour (within tolerance) the screen holds // least, the first of equals: the screen's colours are counted once, sorted. static uint32_t iconAnchor(const GateImageT *screen, const GateIconT *icon, uint32_t tolerance) { uint32_t *packed; uint32_t *counts; const uint8_t *pixel; uint8_t rgb[GATE_RGB_BYTES]; size_t pixelCount; size_t i; uint32_t unique; uint32_t u; uint32_t p; uint32_t count; uint32_t bestCount; uint32_t anchor; pixelCount = (size_t)screen->width * screen->height; packed = (uint32_t *)malloc((pixelCount + 1u) * sizeof(uint32_t)); counts = (uint32_t *)malloc((pixelCount + 1u) * sizeof(uint32_t)); if (packed == NULL || counts == NULL) { toolFail("out of memory"); } for (i = 0; i < pixelCount; i++) { pixel = screen->rgba + i * GATE_RGBA_BYTES; packed[i] = ((uint32_t)pixel[0] << RED_SHIFT) | ((uint32_t)pixel[1] << GREEN_SHIFT) | pixel[2]; } qsort(packed, pixelCount, sizeof(uint32_t), packedCompare); unique = 0u; for (i = 0; i < pixelCount; i++) { if (unique > 0u && packed[unique - 1u] == packed[i]) { counts[unique - 1u]++; } else { packed[unique] = packed[i]; counts[unique] = 1u; unique++; } } anchor = 0u; bestCount = 0u; for (p = 0; p < icon->count; p++) { count = 0u; for (u = 0; u < unique; u++) { rgb[0] = (uint8_t)(packed[u] >> RED_SHIFT); rgb[1] = (uint8_t)(packed[u] >> GREEN_SHIFT); rgb[2] = (uint8_t)packed[u]; if (within(rgb, icon->pixels[p].rgb, tolerance)) { count += counts[u]; } } if (p == 0u || count < bestCount) { anchor = p; bestCount = count; } } free(packed); free(counts); return anchor; } // Whether the screen shows the icon with its top left at (left, top): it // lies wholly on the screen and every opaque pixel is within tolerance. static bool iconAt(const GateImageT *screen, const GateIconT *icon, uint32_t tolerance, uint32_t left, uint32_t top) { const GateIconPixelT *q; uint32_t p; uint32_t x; uint32_t y; if ((uint64_t)top + icon->height > screen->height || (uint64_t)left + icon->width > screen->width) { return false; } for (p = 0; p < icon->count; p++) { q = &icon->pixels[p]; x = left + q->x; y = top + q->y; if (x >= screen->width || y >= screen->height || !within(gateImagePixel(screen, x, y), q->rgb, tolerance)) { return false; } } return true; } static void imageAlloc(GateImageT *img, uint32_t width, uint32_t height) { img->width = width; img->height = height; img->rgba = (uint8_t *)calloc((size_t)width * height + 1u, GATE_RGBA_BYTES); if (img->rgba == NULL) { toolFail("out of memory"); } } static bool magic(const uint8_t *data, size_t len, const char *bytes, size_t count) { return len >= count && memcmp(data, bytes, count) == 0; } static int packedCompare(const void *a, const void *b) { uint32_t x; uint32_t y; x = *(const uint32_t *)a; y = *(const uint32_t *)b; return (x > y) - (x < y); } // A PNG through toolPng, made RGBA. static void pngDecode(const uint8_t *data, size_t len, const char *path, GateImageT *img) { ToolImageT decoded; ToolImageT rgba; char err[TOOL_PNG_ERROR_BYTES]; if (!toolPngDecode(data, len, &decoded, err)) { toolFail("%s: %s", path, err); } if (!toolImageToRgba(&decoded, &rgba)) { toolFail("out of memory"); } toolImageFree(&decoded); img->width = rgba.width; img->height = rgba.height; img->rgba = rgba.pixels; } // Whether every gun of a is within tolerance of b's. static bool within(const uint8_t *a, const uint8_t *b, uint32_t tolerance) { uint32_t i; for (i = 0; i < GATE_RGB_BYTES; i++) { if ((uint32_t)abs((int32_t)a[i] - (int32_t)b[i]) > tolerance) { return false; } } return true; }