// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // toolPng.c - PNG for the JoeyLib host tools (toolPng.h). // // Decode: chunks (CRCs checked), the IDATs inflated as one zlib stream, then // each Adam7 pass (one pass when not interlaced) unfiltered a row at a time // and its pixels unpacked into the image. Encode: one IDAT, each row's // filter the one whose bytes, as signed, sum smallest (the usual // heuristic). #include #include #include #include "toolFile.h" #include "toolPng.h" #include "toolZlib.h" #define PNG_SIGNATURE_BYTES 8u #define CHUNK_OVERHEAD 12u #define IHDR_BYTES 13u #define COLOR_GRAY 0u #define COLOR_RGB 2u #define COLOR_PALETTE 3u #define COLOR_GRAY_ALPHA 4u #define COLOR_RGBA 6u #define FILTER_NONE 0u #define FILTER_SUB 1u #define FILTER_UP 2u #define FILTER_AVERAGE 3u #define FILTER_PAETH 4u #define FILTER_COUNT 5u #define ADAM7_PASSES 7u #define OPAQUE 255u #define MAX_DIMENSION 0x8000u typedef struct DecodeT { uint32_t width; uint32_t height; uint8_t depth; uint8_t color; uint8_t channels; bool hasKey; uint16_t key[3]; ToolImageT *img; } DecodeT; static const uint8_t kSignature[PNG_SIGNATURE_BYTES] = { 0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n' }; static const uint8_t kAdam7X0[ADAM7_PASSES] = { 0, 4, 0, 2, 0, 1, 0 }; static const uint8_t kAdam7Y0[ADAM7_PASSES] = { 0, 0, 4, 0, 2, 0, 1 }; static const uint8_t kAdam7Dx[ADAM7_PASSES] = { 8, 8, 4, 4, 2, 2, 1 }; static const uint8_t kAdam7Dy[ADAM7_PASSES] = { 8, 8, 8, 4, 4, 2, 2 }; static bool appendBytes(uint8_t **buf, size_t *len, size_t *cap, const uint8_t *data, size_t count); static bool appendChunk(uint8_t **buf, size_t *len, size_t *cap, const char *type, const uint8_t *data, size_t count); static void decodePixel(const DecodeT *d, const uint8_t *row, uint32_t i, uint32_t x, uint32_t y); static uint8_t paeth(uint8_t a, uint8_t b, uint8_t c); static uint32_t readBe32(const uint8_t *p); static uint16_t sample(const uint8_t *row, uint32_t index, uint8_t depth); static bool unfilterRow(uint8_t *row, const uint8_t *prior, size_t rowBytes, size_t bpp, uint8_t filter); static void writeBe32(uint8_t *p, uint32_t v); static bool appendBytes(uint8_t **buf, size_t *len, size_t *cap, const uint8_t *data, size_t count) { uint8_t *grown; size_t want; if (*len + count > *cap) { want = *cap == 0u ? 4096u : *cap; while (want < *len + count) { want *= 2u; } grown = (uint8_t *)realloc(*buf, want); if (grown == NULL) { return false; } *buf = grown; *cap = want; } memcpy(*buf + *len, data, count); *len += count; return true; } static bool appendChunk(uint8_t **buf, size_t *len, size_t *cap, const char *type, const uint8_t *data, size_t count) { uint8_t head[8]; uint8_t tail[4]; uint32_t crc; writeBe32(head, (uint32_t)count); memcpy(head + 4, type, 4); crc = toolCrc32(0u, head + 4, 4u); crc = toolCrc32(crc, data, count); writeBe32(tail, crc); return appendBytes(buf, len, cap, head, sizeof(head)) && appendBytes(buf, len, cap, data, count) && appendBytes(buf, len, cap, tail, sizeof(tail)); } // Pixel i of an unfiltered row, stored at (x, y) of the image. static void decodePixel(const DecodeT *d, const uint8_t *row, uint32_t i, uint32_t x, uint32_t y) { uint8_t *out; uint16_t v[4]; uint16_t c; uint16_t maxValue; if (d->color == COLOR_PALETTE) { d->img->pixels[(size_t)y * d->width + x] = (uint8_t)sample(row, i, d->depth); return; } for (c = 0; c < d->channels; c++) { v[c] = sample(row, i * d->channels + c, d->depth); } out = d->img->pixels + ((size_t)y * d->width + x) * 4u; maxValue = (uint16_t)((1u << d->depth) - 1u); if (d->color == COLOR_GRAY || d->color == COLOR_GRAY_ALPHA) { out[0] = (uint8_t)(d->depth == 16u ? v[0] >> 8 : (uint32_t)v[0] * 255u / maxValue); out[1] = out[0]; out[2] = out[0]; if (d->color == COLOR_GRAY_ALPHA) { out[3] = (uint8_t)(d->depth == 16u ? v[1] >> 8 : v[1]); } else { out[3] = (d->hasKey && v[0] == d->key[0]) ? 0u : OPAQUE; } return; } for (c = 0; c < 3u; c++) { out[c] = (uint8_t)(d->depth == 16u ? v[c] >> 8 : v[c]); } if (d->color == COLOR_RGBA) { out[3] = (uint8_t)(d->depth == 16u ? v[3] >> 8 : v[3]); } else { out[3] = (d->hasKey && v[0] == d->key[0] && v[1] == d->key[1] && v[2] == d->key[2]) ? 0u : OPAQUE; } } static uint8_t paeth(uint8_t a, uint8_t b, uint8_t c) { int32_t p; int32_t pa; int32_t pb; int32_t pc; p = (int32_t)a + (int32_t)b - (int32_t)c; pa = abs(p - (int32_t)a); pb = abs(p - (int32_t)b); pc = abs(p - (int32_t)c); if (pa <= pb && pa <= pc) { return a; } return pb <= pc ? b : c; } static uint32_t readBe32(const uint8_t *p) { return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | (uint32_t)p[3]; } // Sample `index` of a row of `depth`-bit samples, packed high bits first. static uint16_t sample(const uint8_t *row, uint32_t index, uint8_t depth) { uint32_t bit; if (depth == 16u) { return (uint16_t)((uint16_t)row[index * 2u] << 8 | row[index * 2u + 1u]); } if (depth == 8u) { return row[index]; } bit = index * depth; return (uint16_t)((row[bit >> 3] >> (8u - depth - (bit & 7u))) & ((1u << depth) - 1u)); } bool toolImageCreate(ToolImageT *img, uint32_t width, uint32_t height, bool indexed) { memset(img, 0, sizeof(*img)); img->width = width; img->height = height; img->indexed = indexed; img->pixels = (uint8_t *)calloc((size_t)width * height, indexed ? 1u : 4u); return img->pixels != NULL || (size_t)width * height == 0u; } void toolImageFree(ToolImageT *img) { free(img->pixels); img->pixels = NULL; } bool toolImageToRgba(const ToolImageT *src, ToolImageT *dst) { size_t n; size_t i; if (!toolImageCreate(dst, src->width, src->height, false)) { return false; } n = (size_t)src->width * src->height; if (!src->indexed) { memcpy(dst->pixels, src->pixels, n * 4u); return true; } for (i = 0; i < n; i++) { memcpy(dst->pixels + i * 4u, src->palette[src->pixels[i]], 4u); } return true; } bool toolPngDecode(const uint8_t *data, size_t len, ToolImageT *img, char err[TOOL_PNG_ERROR_BYTES]) { DecodeT d; uint8_t *idat; uint8_t *raw; uint8_t *prior; uint8_t *row; size_t idatLen; size_t idatCap; size_t rawLen; size_t pos; size_t bpp; size_t rowBytes; size_t at; uint32_t chunkLen; uint32_t passW; uint32_t passH; uint32_t pass; uint32_t passes; uint32_t px; uint32_t py; uint32_t i; uint8_t palette[TOOL_PNG_PALETTE_MAX][4]; uint16_t paletteCount; uint8_t interlace; bool seenEnd; memset(&d, 0, sizeof(d)); memset(palette, OPAQUE, sizeof(palette)); paletteCount = 0u; memset(img, 0, sizeof(*img)); idat = NULL; idatLen = 0; idatCap = 0; seenEnd = false; interlace = 0u; if (len < PNG_SIGNATURE_BYTES || memcmp(data, kSignature, PNG_SIGNATURE_BYTES) != 0) { snprintf(err, TOOL_PNG_ERROR_BYTES, "not a PNG"); return false; } pos = PNG_SIGNATURE_BYTES; while (!seenEnd) { if (pos + CHUNK_OVERHEAD > len) { snprintf(err, TOOL_PNG_ERROR_BYTES, "truncated PNG"); free(idat); return false; } chunkLen = readBe32(data + pos); if (chunkLen > len - pos - CHUNK_OVERHEAD) { snprintf(err, TOOL_PNG_ERROR_BYTES, "truncated PNG chunk"); free(idat); return false; } if (toolCrc32(0u, data + pos + 4u, chunkLen + 4u) != readBe32(data + pos + 8u + chunkLen)) { snprintf(err, TOOL_PNG_ERROR_BYTES, "bad CRC in a %.4s chunk", (const char *)(data + pos + 4u)); free(idat); return false; } if (memcmp(data + pos + 4u, "IHDR", 4) == 0 && chunkLen == IHDR_BYTES) { at = pos + 8u; d.width = readBe32(data + at); d.height = readBe32(data + at + 4u); d.depth = data[at + 8u]; d.color = data[at + 9u]; interlace = data[at + 12u]; } else if (memcmp(data + pos + 4u, "PLTE", 4) == 0) { paletteCount = (uint16_t)(chunkLen / 3u > TOOL_PNG_PALETTE_MAX ? TOOL_PNG_PALETTE_MAX : chunkLen / 3u); for (i = 0; i < paletteCount; i++) { palette[i][0] = data[pos + 8u + i * 3u]; palette[i][1] = data[pos + 9u + i * 3u]; palette[i][2] = data[pos + 10u + i * 3u]; } } else if (memcmp(data + pos + 4u, "tRNS", 4) == 0) { if (d.color == COLOR_PALETTE) { for (i = 0; i < chunkLen && i < TOOL_PNG_PALETTE_MAX; i++) { palette[i][3] = data[pos + 8u + i]; } } else if (d.color == COLOR_GRAY && chunkLen >= 2u) { d.hasKey = true; d.key[0] = (uint16_t)((uint16_t)data[pos + 8u] << 8 | data[pos + 9u]); } else if (d.color == COLOR_RGB && chunkLen >= 6u) { d.hasKey = true; for (i = 0; i < 3u; i++) { d.key[i] = (uint16_t)((uint16_t)data[pos + 8u + i * 2u] << 8 | data[pos + 9u + i * 2u]); } } } else if (memcmp(data + pos + 4u, "IDAT", 4) == 0) { if (!appendBytes(&idat, &idatLen, &idatCap, data + pos + 8u, chunkLen)) { snprintf(err, TOOL_PNG_ERROR_BYTES, "out of memory"); free(idat); return false; } } else if (memcmp(data + pos + 4u, "IEND", 4) == 0) { seenEnd = true; } pos += CHUNK_OVERHEAD + chunkLen; } switch (d.color) { case COLOR_GRAY: d.channels = 1u; break; case COLOR_RGB: d.channels = 3u; break; case COLOR_PALETTE: d.channels = 1u; break; case COLOR_GRAY_ALPHA: d.channels = 2u; break; case COLOR_RGBA: d.channels = 4u; break; default: d.channels = 0u; break; } if (d.channels == 0u || d.width == 0u || d.height == 0u || d.width > MAX_DIMENSION || d.height > MAX_DIMENSION || (d.depth != 1u && d.depth != 2u && d.depth != 4u && d.depth != 8u && d.depth != 16u) || interlace > 1u) { snprintf(err, TOOL_PNG_ERROR_BYTES, "unsupported PNG header (%ux%u, depth %u, color type %u)", d.width, d.height, d.depth, d.color); free(idat); return false; } if (!toolInflate(idat, idatLen, TOOL_ZLIB_ZLIB, &raw, &rawLen)) { snprintf(err, TOOL_PNG_ERROR_BYTES, "bad image data"); free(idat); return false; } free(idat); if (!toolImageCreate(img, d.width, d.height, d.color == COLOR_PALETTE)) { snprintf(err, TOOL_PNG_ERROR_BYTES, "out of memory"); free(raw); return false; } img->paletteCount = paletteCount; memcpy(img->palette, palette, sizeof(palette)); if (img->indexed && img->paletteCount == 0u) { snprintf(err, TOOL_PNG_ERROR_BYTES, "palette PNG without a PLTE"); free(raw); toolImageFree(img); return false; } d.img = img; bpp = (size_t)d.channels * d.depth / 8u; if (bpp == 0u) { bpp = 1u; } passes = interlace != 0u ? ADAM7_PASSES : 1u; pos = 0; for (pass = 0; pass < passes; pass++) { if (interlace != 0u) { passW = d.width > kAdam7X0[pass] ? (d.width - kAdam7X0[pass] + kAdam7Dx[pass] - 1u) / kAdam7Dx[pass] : 0u; passH = d.height > kAdam7Y0[pass] ? (d.height - kAdam7Y0[pass] + kAdam7Dy[pass] - 1u) / kAdam7Dy[pass] : 0u; } else { passW = d.width; passH = d.height; } if (passW == 0u || passH == 0u) { continue; } rowBytes = ((size_t)passW * d.channels * d.depth + 7u) / 8u; prior = NULL; for (py = 0; py < passH; py++) { if (pos + 1u + rowBytes > rawLen) { snprintf(err, TOOL_PNG_ERROR_BYTES, "image data too short"); free(raw); toolImageFree(img); return false; } row = raw + pos + 1u; if (!unfilterRow(row, prior, rowBytes, bpp, raw[pos])) { snprintf(err, TOOL_PNG_ERROR_BYTES, "bad row filter %u", raw[pos]); free(raw); toolImageFree(img); return false; } for (px = 0; px < passW; px++) { if (interlace != 0u) { decodePixel(&d, row, px, kAdam7X0[pass] + px * kAdam7Dx[pass], kAdam7Y0[pass] + py * kAdam7Dy[pass]); } else { decodePixel(&d, row, px, px, py); } } prior = row; pos += 1u + rowBytes; } } free(raw); return true; } bool toolPngEncode(const ToolImageT *img, uint8_t **out, size_t *outLen) { uint8_t *raw; uint8_t *z; uint8_t *buf; uint8_t *trial; uint8_t ihdr[IHDR_BYTES]; uint8_t plte[TOOL_PNG_PALETTE_MAX * 3u]; uint8_t trns[TOOL_PNG_PALETTE_MAX]; size_t len; size_t cap; size_t zLen; size_t bpp; size_t rowBytes; size_t i; uint32_t y; uint32_t cost; uint32_t best; uint16_t trnsCount; uint8_t f; uint8_t bestFilter; uint8_t left; uint8_t up; uint8_t upLeft; uint8_t v; bool ok; const uint8_t *row; const uint8_t *prior; bpp = img->indexed ? 1u : 4u; rowBytes = (size_t)img->width * bpp; raw = (uint8_t *)malloc((rowBytes + 1u) * img->height); trial = (uint8_t *)malloc(rowBytes * FILTER_COUNT); if (raw == NULL || trial == NULL) { free(raw); free(trial); return false; } for (y = 0; y < img->height; y++) { row = img->pixels + (size_t)y * rowBytes; prior = y > 0u ? row - rowBytes : NULL; best = 0xFFFFFFFFu; bestFilter = FILTER_NONE; for (f = 0; f < FILTER_COUNT; f++) { cost = 0u; for (i = 0; i < rowBytes; i++) { left = i >= bpp ? row[i - bpp] : 0u; up = prior != NULL ? prior[i] : 0u; upLeft = (i >= bpp && prior != NULL) ? prior[i - bpp] : 0u; switch (f) { case FILTER_SUB: v = (uint8_t)(row[i] - left); break; case FILTER_UP: v = (uint8_t)(row[i] - up); break; case FILTER_AVERAGE: v = (uint8_t)(row[i] - ((uint32_t)left + up) / 2u); break; case FILTER_PAETH: v = (uint8_t)(row[i] - paeth(left, up, upLeft)); break; default: v = row[i]; break; } trial[f * rowBytes + i] = v; cost += (uint32_t)(v < 128u ? v : 256u - v); } if (cost < best) { best = cost; bestFilter = f; } } raw[(size_t)y * (rowBytes + 1u)] = bestFilter; memcpy(raw + (size_t)y * (rowBytes + 1u) + 1u, trial + bestFilter * rowBytes, rowBytes); } free(trial); ok = toolDeflateZlib(raw, (rowBytes + 1u) * img->height, &z, &zLen); free(raw); if (!ok) { return false; } writeBe32(ihdr, img->width); writeBe32(ihdr + 4, img->height); ihdr[8] = 8u; ihdr[9] = img->indexed ? COLOR_PALETTE : COLOR_RGBA; ihdr[10] = 0u; ihdr[11] = 0u; ihdr[12] = 0u; buf = NULL; len = 0; cap = 0; ok = appendBytes(&buf, &len, &cap, kSignature, PNG_SIGNATURE_BYTES) && appendChunk(&buf, &len, &cap, "IHDR", ihdr, IHDR_BYTES); if (ok && img->indexed) { trnsCount = 0u; for (i = 0; i < img->paletteCount; i++) { plte[i * 3u] = img->palette[i][0]; plte[i * 3u + 1u] = img->palette[i][1]; plte[i * 3u + 2u] = img->palette[i][2]; trns[i] = img->palette[i][3]; if (trns[i] != OPAQUE) { trnsCount = (uint16_t)(i + 1u); } } ok = appendChunk(&buf, &len, &cap, "PLTE", plte, (size_t)img->paletteCount * 3u); if (ok && trnsCount > 0u) { ok = appendChunk(&buf, &len, &cap, "tRNS", trns, trnsCount); } } ok = ok && appendChunk(&buf, &len, &cap, "IDAT", z, zLen) && appendChunk(&buf, &len, &cap, "IEND", NULL, 0u); free(z); if (!ok) { free(buf); return false; } *out = buf; *outLen = len; return true; } bool toolPngLoad(const char *path, ToolImageT *img, char err[TOOL_PNG_ERROR_BYTES]) { uint8_t *data; size_t len; bool ok; if (!toolReadFile(path, &data, &len)) { snprintf(err, TOOL_PNG_ERROR_BYTES, "cannot read it"); return false; } ok = toolPngDecode(data, len, img, err); free(data); return ok; } bool toolPngSave(const char *path, const ToolImageT *img) { uint8_t *data; size_t len; bool ok; if (!toolPngEncode(img, &data, &len)) { return false; } ok = toolWriteFile(path, data, len); free(data); return ok; } static bool unfilterRow(uint8_t *row, const uint8_t *prior, size_t rowBytes, size_t bpp, uint8_t filter) { size_t i; uint8_t left; uint8_t up; uint8_t upLeft; for (i = 0; i < rowBytes; i++) { left = i >= bpp ? row[i - bpp] : 0u; up = prior != NULL ? prior[i] : 0u; upLeft = (i >= bpp && prior != NULL) ? prior[i - bpp] : 0u; switch (filter) { case FILTER_NONE: break; case FILTER_SUB: row[i] = (uint8_t)(row[i] + left); break; case FILTER_UP: row[i] = (uint8_t)(row[i] + up); break; case FILTER_AVERAGE: row[i] = (uint8_t)(row[i] + ((uint32_t)left + up) / 2u); break; case FILTER_PAETH: row[i] = (uint8_t)(row[i] + paeth(left, up, upLeft)); break; default: return false; } } return true; } static void writeBe32(uint8_t *p, uint32_t v) { p[0] = (uint8_t)(v >> 24); p[1] = (uint8_t)(v >> 16); p[2] = (uint8_t)(v >> 8); p[3] = (uint8_t)v; }