joeylib2/tests/gate/common/gateImage.c

682 lines
23 KiB
C

// 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}