1036 lines
32 KiB
C
1036 lines
32 KiB
C
/*
|
|
* Iris Image I/O Implementation
|
|
*
|
|
* Pure C implementation for reading and writing images.
|
|
* Supports: PNG (read/write), PPM (read/write), JPEG (read only)
|
|
*
|
|
* PNG implementation uses zlib-style deflate compression.
|
|
* JPEG implementation supports baseline and progressive DCT with various subsampling.
|
|
*/
|
|
|
|
#include "iris.h"
|
|
#include "jpeg.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
|
|
/* ========================================================================
|
|
* Image Creation and Management
|
|
* ======================================================================== */
|
|
|
|
iris_image *iris_image_create(int width, int height, int channels) {
|
|
iris_image *img = (iris_image *)malloc(sizeof(iris_image));
|
|
if (!img) return NULL;
|
|
|
|
img->width = width;
|
|
img->height = height;
|
|
img->channels = channels;
|
|
img->data = (uint8_t *)calloc(width * height * channels, sizeof(uint8_t));
|
|
|
|
if (!img->data) {
|
|
free(img);
|
|
return NULL;
|
|
}
|
|
|
|
return img;
|
|
}
|
|
|
|
void iris_image_free(iris_image *img) {
|
|
if (img) {
|
|
free(img->data);
|
|
free(img);
|
|
}
|
|
}
|
|
|
|
iris_image *iris_image_clone(const iris_image *img) {
|
|
if (!img) return NULL;
|
|
|
|
iris_image *clone = iris_image_create(img->width, img->height, img->channels);
|
|
if (!clone) return NULL;
|
|
|
|
memcpy(clone->data, img->data, img->width * img->height * img->channels);
|
|
return clone;
|
|
}
|
|
|
|
/* ========================================================================
|
|
* PPM/PGM Format (Simple, uncompressed)
|
|
* ======================================================================== */
|
|
|
|
static iris_image *load_ppm(FILE *f) {
|
|
char magic[3];
|
|
int width, height, maxval;
|
|
|
|
if (fscanf(f, "%2s", magic) != 1) return NULL;
|
|
|
|
/* Skip comments */
|
|
int c;
|
|
while ((c = fgetc(f)) == '#') {
|
|
while ((c = fgetc(f)) != '\n' && c != EOF);
|
|
}
|
|
ungetc(c, f);
|
|
|
|
if (fscanf(f, "%d %d %d", &width, &height, &maxval) != 3) return NULL;
|
|
fgetc(f); /* Skip single whitespace after maxval */
|
|
|
|
int channels;
|
|
if (strcmp(magic, "P6") == 0) {
|
|
channels = 3; /* RGB */
|
|
} else if (strcmp(magic, "P5") == 0) {
|
|
channels = 1; /* Grayscale */
|
|
} else {
|
|
return NULL; /* Unsupported format */
|
|
}
|
|
|
|
iris_image *img = iris_image_create(width, height, channels);
|
|
if (!img) return NULL;
|
|
|
|
size_t size = width * height * channels;
|
|
if (fread(img->data, 1, size, f) != size) {
|
|
iris_image_free(img);
|
|
return NULL;
|
|
}
|
|
|
|
return img;
|
|
}
|
|
|
|
static int save_ppm(const iris_image *img, FILE *f) {
|
|
if (img->channels == 1) {
|
|
fprintf(f, "P5\n");
|
|
} else {
|
|
fprintf(f, "P6\n");
|
|
}
|
|
fprintf(f, "%d %d\n255\n", img->width, img->height);
|
|
|
|
/* If 4 channels (RGBA), write only RGB */
|
|
if (img->channels == 4) {
|
|
for (int y = 0; y < img->height; y++) {
|
|
for (int x = 0; x < img->width; x++) {
|
|
uint8_t *p = img->data + (y * img->width + x) * 4;
|
|
fwrite(p, 1, 3, f);
|
|
}
|
|
}
|
|
} else {
|
|
size_t size = img->width * img->height * img->channels;
|
|
if (fwrite(img->data, 1, size, f) != size) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* ========================================================================
|
|
* PNG Format
|
|
* ======================================================================== */
|
|
|
|
/* CRC32 table for PNG */
|
|
static uint32_t crc_table[256];
|
|
static int crc_table_computed = 0;
|
|
|
|
static void make_crc_table(void) {
|
|
for (int n = 0; n < 256; n++) {
|
|
uint32_t c = (uint32_t)n;
|
|
for (int k = 0; k < 8; k++) {
|
|
if (c & 1)
|
|
c = 0xedb88320u ^ (c >> 1);
|
|
else
|
|
c = c >> 1;
|
|
}
|
|
crc_table[n] = c;
|
|
}
|
|
crc_table_computed = 1;
|
|
}
|
|
|
|
static uint32_t update_crc(uint32_t crc, const uint8_t *buf, size_t len) {
|
|
if (!crc_table_computed) make_crc_table();
|
|
|
|
uint32_t c = crc;
|
|
for (size_t n = 0; n < len; n++) {
|
|
c = crc_table[(c ^ buf[n]) & 0xff] ^ (c >> 8);
|
|
}
|
|
return c;
|
|
}
|
|
|
|
static uint32_t png_crc(const uint8_t *buf, size_t len) {
|
|
return update_crc(0xffffffffu, buf, len) ^ 0xffffffffu;
|
|
}
|
|
|
|
/* Adler-32 checksum for zlib */
|
|
static uint32_t adler32(const uint8_t *data, size_t len) {
|
|
uint32_t a = 1, b = 0;
|
|
for (size_t i = 0; i < len; i++) {
|
|
a = (a + data[i]) % 65521;
|
|
b = (b + a) % 65521;
|
|
}
|
|
return (b << 16) | a;
|
|
}
|
|
|
|
/* Simple uncompressed zlib/deflate (store mode) */
|
|
static uint8_t *deflate_store(const uint8_t *data, size_t len, size_t *out_len) {
|
|
/* Zlib header (2 bytes) + deflate blocks + adler32 (4 bytes) */
|
|
size_t max_block = 65535;
|
|
size_t num_blocks = (len + max_block - 1) / max_block;
|
|
size_t total = 2 + num_blocks * 5 + len + 4;
|
|
|
|
uint8_t *out = (uint8_t *)malloc(total);
|
|
if (!out) return NULL;
|
|
|
|
size_t pos = 0;
|
|
|
|
/* Zlib header: CMF=0x78 (deflate, 32K window), FLG=0x01 (no dict, level 0) */
|
|
out[pos++] = 0x78;
|
|
out[pos++] = 0x01;
|
|
|
|
/* Deflate stored blocks */
|
|
size_t remaining = len;
|
|
const uint8_t *src = data;
|
|
while (remaining > 0) {
|
|
size_t block_len = (remaining > max_block) ? max_block : remaining;
|
|
int is_final = (remaining <= max_block) ? 1 : 0;
|
|
|
|
/* Block header: BFINAL (1 bit) + BTYPE=00 (2 bits) = stored */
|
|
out[pos++] = is_final;
|
|
|
|
/* LEN and NLEN (little-endian) */
|
|
out[pos++] = block_len & 0xff;
|
|
out[pos++] = (block_len >> 8) & 0xff;
|
|
out[pos++] = (~block_len) & 0xff;
|
|
out[pos++] = ((~block_len) >> 8) & 0xff;
|
|
|
|
memcpy(out + pos, src, block_len);
|
|
pos += block_len;
|
|
|
|
src += block_len;
|
|
remaining -= block_len;
|
|
}
|
|
|
|
/* Adler-32 checksum (big-endian) */
|
|
uint32_t checksum = adler32(data, len);
|
|
out[pos++] = (checksum >> 24) & 0xff;
|
|
out[pos++] = (checksum >> 16) & 0xff;
|
|
out[pos++] = (checksum >> 8) & 0xff;
|
|
out[pos++] = checksum & 0xff;
|
|
|
|
*out_len = pos;
|
|
return out;
|
|
}
|
|
|
|
/* Write PNG chunk */
|
|
static void write_png_chunk(FILE *f, const char *type, const uint8_t *data, size_t len) {
|
|
/* Length (big-endian) */
|
|
uint8_t len_bytes[4] = {
|
|
(len >> 24) & 0xff,
|
|
(len >> 16) & 0xff,
|
|
(len >> 8) & 0xff,
|
|
len & 0xff
|
|
};
|
|
fwrite(len_bytes, 1, 4, f);
|
|
|
|
/* Type */
|
|
fwrite(type, 1, 4, f);
|
|
|
|
/* Data */
|
|
if (len > 0 && data) {
|
|
fwrite(data, 1, len, f);
|
|
}
|
|
|
|
/* CRC (over type + data) */
|
|
uint8_t *crc_data = (uint8_t *)malloc(4 + len);
|
|
memcpy(crc_data, type, 4);
|
|
if (len > 0 && data) {
|
|
memcpy(crc_data + 4, data, len);
|
|
}
|
|
uint32_t crc = png_crc(crc_data, 4 + len);
|
|
free(crc_data);
|
|
|
|
uint8_t crc_bytes[4] = {
|
|
(crc >> 24) & 0xff,
|
|
(crc >> 16) & 0xff,
|
|
(crc >> 8) & 0xff,
|
|
crc & 0xff
|
|
};
|
|
fwrite(crc_bytes, 1, 4, f);
|
|
}
|
|
|
|
/* Write PNG tEXt chunk (uncompressed text metadata) */
|
|
static void write_png_text_chunk(FILE *f, const char *keyword, const char *text) {
|
|
size_t key_len = strlen(keyword);
|
|
size_t text_len = strlen(text);
|
|
size_t data_len = key_len + 1 + text_len; /* keyword + null + text */
|
|
|
|
uint8_t *data = (uint8_t *)malloc(data_len);
|
|
if (!data) return;
|
|
|
|
memcpy(data, keyword, key_len);
|
|
data[key_len] = 0; /* Null separator */
|
|
memcpy(data + key_len + 1, text, text_len);
|
|
|
|
write_png_chunk(f, "tEXt", data, data_len);
|
|
free(data);
|
|
}
|
|
|
|
static int save_png_with_metadata(const iris_image *img, FILE *f, int64_t seed, int has_seed) {
|
|
/* PNG signature */
|
|
const uint8_t signature[8] = {0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a};
|
|
fwrite(signature, 1, 8, f);
|
|
|
|
/* IHDR chunk */
|
|
uint8_t ihdr[13];
|
|
ihdr[0] = (img->width >> 24) & 0xff;
|
|
ihdr[1] = (img->width >> 16) & 0xff;
|
|
ihdr[2] = (img->width >> 8) & 0xff;
|
|
ihdr[3] = img->width & 0xff;
|
|
ihdr[4] = (img->height >> 24) & 0xff;
|
|
ihdr[5] = (img->height >> 16) & 0xff;
|
|
ihdr[6] = (img->height >> 8) & 0xff;
|
|
ihdr[7] = img->height & 0xff;
|
|
ihdr[8] = 8; /* Bit depth */
|
|
ihdr[9] = (img->channels == 4) ? 6 : (img->channels == 3) ? 2 :
|
|
(img->channels == 2) ? 4 : 0; /* Color type */
|
|
ihdr[10] = 0; /* Compression */
|
|
ihdr[11] = 0; /* Filter */
|
|
ihdr[12] = 0; /* Interlace */
|
|
|
|
write_png_chunk(f, "IHDR", ihdr, 13);
|
|
|
|
/* Write metadata as tEXt chunks */
|
|
if (has_seed) {
|
|
char seed_str[32];
|
|
snprintf(seed_str, sizeof(seed_str), "%lld", (long long)seed);
|
|
write_png_text_chunk(f, "iris:seed", seed_str);
|
|
write_png_text_chunk(f, "Software", "iris (https://github.com/antirez/iris)");
|
|
write_png_text_chunk(f, "iris:model", "iris");
|
|
}
|
|
|
|
/* Prepare raw image data with filter bytes */
|
|
int channels = img->channels;
|
|
if (channels == 4) channels = 4; /* RGBA */
|
|
else if (channels >= 3) channels = 3; /* RGB */
|
|
else channels = 1; /* Grayscale */
|
|
|
|
size_t row_bytes = 1 + img->width * channels; /* +1 for filter byte */
|
|
size_t raw_len = img->height * row_bytes;
|
|
uint8_t *raw = (uint8_t *)malloc(raw_len);
|
|
|
|
for (int y = 0; y < img->height; y++) {
|
|
raw[y * row_bytes] = 0; /* Filter: None */
|
|
for (int x = 0; x < img->width; x++) {
|
|
const uint8_t *src = img->data + (y * img->width + x) * img->channels;
|
|
uint8_t *dst = raw + y * row_bytes + 1 + x * channels;
|
|
|
|
for (int c = 0; c < channels; c++) {
|
|
if (c < img->channels) {
|
|
dst[c] = src[c];
|
|
} else {
|
|
dst[c] = 255; /* Alpha = 255 */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Compress with zlib (store mode) */
|
|
size_t compressed_len;
|
|
uint8_t *compressed = deflate_store(raw, raw_len, &compressed_len);
|
|
free(raw);
|
|
|
|
if (!compressed) return -1;
|
|
|
|
/* IDAT chunk */
|
|
write_png_chunk(f, "IDAT", compressed, compressed_len);
|
|
free(compressed);
|
|
|
|
/* IEND chunk */
|
|
write_png_chunk(f, "IEND", NULL, 0);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int save_png(const iris_image *img, FILE *f) {
|
|
return save_png_with_metadata(img, f, 0, 0);
|
|
}
|
|
|
|
/* Read 4-byte big-endian integer */
|
|
static uint32_t read_be32(FILE *f) {
|
|
uint8_t buf[4];
|
|
if (fread(buf, 1, 4, f) != 4) return 0;
|
|
return ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) |
|
|
((uint32_t)buf[2] << 8) | buf[3];
|
|
}
|
|
|
|
typedef struct {
|
|
const uint8_t *data;
|
|
size_t len;
|
|
size_t bytepos;
|
|
uint32_t bitbuf;
|
|
int bitcount;
|
|
} png_bitstream;
|
|
|
|
static int png_bitstream_fill(png_bitstream *bs, int n) {
|
|
while (bs->bitcount < n && bs->bytepos < bs->len) {
|
|
bs->bitbuf |= (uint32_t)bs->data[bs->bytepos++] << bs->bitcount;
|
|
bs->bitcount += 8;
|
|
}
|
|
return bs->bitcount >= n;
|
|
}
|
|
|
|
static int png_bitstream_get(png_bitstream *bs, int n, uint32_t *out) {
|
|
if (n == 0) {
|
|
*out = 0;
|
|
return 1;
|
|
}
|
|
if (!png_bitstream_fill(bs, n)) return 0;
|
|
*out = bs->bitbuf & ((1u << n) - 1u);
|
|
bs->bitbuf >>= n;
|
|
bs->bitcount -= n;
|
|
return 1;
|
|
}
|
|
|
|
static int png_bitstream_align(png_bitstream *bs) {
|
|
uint32_t discard;
|
|
int skip = bs->bitcount & 7;
|
|
if (skip == 0) return 1;
|
|
return png_bitstream_get(bs, skip, &discard);
|
|
}
|
|
|
|
static int png_bitstream_read_bytes(png_bitstream *bs, uint8_t *out, size_t len) {
|
|
if (bs->bitcount == 0) {
|
|
if (bs->bytepos + len > bs->len) return 0;
|
|
memcpy(out, bs->data + bs->bytepos, len);
|
|
bs->bytepos += len;
|
|
return 1;
|
|
}
|
|
for (size_t i = 0; i < len; i++) {
|
|
uint32_t v;
|
|
if (!png_bitstream_get(bs, 8, &v)) return 0;
|
|
out[i] = (uint8_t)v;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
#define PNG_MAXBITS 15
|
|
|
|
typedef struct {
|
|
uint16_t count[PNG_MAXBITS + 1];
|
|
uint16_t symbol[288];
|
|
} png_huffman;
|
|
|
|
static int png_huffman_build(png_huffman *h, const uint8_t *lengths, int n) {
|
|
uint16_t offs[PNG_MAXBITS + 1];
|
|
int left = 1;
|
|
|
|
memset(h->count, 0, sizeof(h->count));
|
|
for (int i = 0; i < n; i++) {
|
|
if (lengths[i] > PNG_MAXBITS) return 0;
|
|
h->count[lengths[i]]++;
|
|
}
|
|
|
|
for (int len = 1; len <= PNG_MAXBITS; len++) {
|
|
left <<= 1;
|
|
left -= h->count[len];
|
|
if (left < 0) return 0;
|
|
}
|
|
|
|
offs[1] = 0;
|
|
for (int len = 1; len < PNG_MAXBITS; len++) {
|
|
offs[len + 1] = offs[len] + h->count[len];
|
|
}
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
int len = lengths[i];
|
|
if (len) {
|
|
h->symbol[offs[len]++] = (uint16_t)i;
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int png_huffman_decode(png_bitstream *bs, const png_huffman *h, int *symbol) {
|
|
uint32_t code = 0;
|
|
uint32_t first = 0;
|
|
uint32_t index = 0;
|
|
|
|
for (int len = 1; len <= PNG_MAXBITS; len++) {
|
|
uint32_t bit;
|
|
if (!png_bitstream_get(bs, 1, &bit)) return 0;
|
|
code |= bit;
|
|
uint32_t count = h->count[len];
|
|
if (code < first + count) {
|
|
*symbol = h->symbol[index + (code - first)];
|
|
return 1;
|
|
}
|
|
index += count;
|
|
first += count;
|
|
first <<= 1;
|
|
code <<= 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int png_build_fixed_huffman(png_huffman *litlen, png_huffman *dist) {
|
|
uint8_t litlen_lengths[288];
|
|
uint8_t dist_lengths[32];
|
|
|
|
for (int i = 0; i <= 143; i++) litlen_lengths[i] = 8;
|
|
for (int i = 144; i <= 255; i++) litlen_lengths[i] = 9;
|
|
for (int i = 256; i <= 279; i++) litlen_lengths[i] = 7;
|
|
for (int i = 280; i <= 287; i++) litlen_lengths[i] = 8;
|
|
for (int i = 0; i < 32; i++) dist_lengths[i] = 5;
|
|
|
|
if (!png_huffman_build(litlen, litlen_lengths, 288)) return 0;
|
|
if (!png_huffman_build(dist, dist_lengths, 32)) return 0;
|
|
return 1;
|
|
}
|
|
|
|
static int png_build_dynamic_huffman(png_bitstream *bs, png_huffman *litlen, png_huffman *dist) {
|
|
static const uint8_t order[19] = {
|
|
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15
|
|
};
|
|
uint32_t hlit, hdist, hclen;
|
|
uint8_t code_lengths[19] = {0};
|
|
png_huffman code_huff;
|
|
|
|
if (!png_bitstream_get(bs, 5, &hlit)) return 0;
|
|
if (!png_bitstream_get(bs, 5, &hdist)) return 0;
|
|
if (!png_bitstream_get(bs, 4, &hclen)) return 0;
|
|
int nlen = (int)hlit + 257;
|
|
int ndist = (int)hdist + 1;
|
|
int ncode = (int)hclen + 4;
|
|
|
|
if (nlen > 288 || ndist > 32) return 0;
|
|
|
|
for (int i = 0; i < ncode; i++) {
|
|
uint32_t v;
|
|
if (!png_bitstream_get(bs, 3, &v)) return 0;
|
|
code_lengths[order[i]] = (uint8_t)v;
|
|
}
|
|
|
|
if (!png_huffman_build(&code_huff, code_lengths, 19)) return 0;
|
|
|
|
uint8_t lengths[320];
|
|
int total = nlen + ndist;
|
|
int i = 0;
|
|
int prev = 0;
|
|
|
|
while (i < total) {
|
|
int sym;
|
|
if (!png_huffman_decode(bs, &code_huff, &sym)) return 0;
|
|
if (sym <= 15) {
|
|
lengths[i++] = (uint8_t)sym;
|
|
prev = sym;
|
|
} else if (sym == 16) {
|
|
uint32_t repeat;
|
|
if (i == 0) return 0;
|
|
if (!png_bitstream_get(bs, 2, &repeat)) return 0;
|
|
repeat += 3;
|
|
if (i + (int)repeat > total) return 0;
|
|
for (uint32_t r = 0; r < repeat; r++) lengths[i++] = (uint8_t)prev;
|
|
} else if (sym == 17) {
|
|
uint32_t repeat;
|
|
if (!png_bitstream_get(bs, 3, &repeat)) return 0;
|
|
repeat += 3;
|
|
if (i + (int)repeat > total) return 0;
|
|
for (uint32_t r = 0; r < repeat; r++) lengths[i++] = 0;
|
|
prev = 0;
|
|
} else if (sym == 18) {
|
|
uint32_t repeat;
|
|
if (!png_bitstream_get(bs, 7, &repeat)) return 0;
|
|
repeat += 11;
|
|
if (i + (int)repeat > total) return 0;
|
|
for (uint32_t r = 0; r < repeat; r++) lengths[i++] = 0;
|
|
prev = 0;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if (!png_huffman_build(litlen, lengths, nlen)) return 0;
|
|
if (!png_huffman_build(dist, lengths + nlen, ndist)) return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* Zlib inflate (stored, fixed, and dynamic blocks) */
|
|
static uint8_t *inflate_zlib(const uint8_t *data, size_t len, size_t expected_len) {
|
|
if (len < 6) return NULL;
|
|
|
|
uint8_t cmf = data[0];
|
|
uint8_t flg = data[1];
|
|
if ((cmf & 0x0f) != 8) return NULL;
|
|
if (((cmf << 8) + flg) % 31 != 0) return NULL;
|
|
|
|
size_t pos = 2;
|
|
if (flg & 0x20) {
|
|
if (len < 10) return NULL;
|
|
pos += 4;
|
|
}
|
|
if (len < pos + 4) return NULL;
|
|
|
|
size_t deflate_len = len - pos - 4;
|
|
png_bitstream bs = {data + pos, deflate_len, 0, 0, 0};
|
|
|
|
uint8_t *out = (uint8_t *)malloc(expected_len);
|
|
if (!out) return NULL;
|
|
size_t out_pos = 0;
|
|
|
|
static const int len_base[29] = {
|
|
3, 4, 5, 6, 7, 8, 9, 10, 11, 13,
|
|
15, 17, 19, 23, 27, 31, 35, 43, 51, 59,
|
|
67, 83, 99, 115, 131, 163, 195, 227, 258
|
|
};
|
|
static const int len_extra[29] = {
|
|
0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
|
|
1, 1, 2, 2, 2, 2, 3, 3, 3, 3,
|
|
4, 4, 4, 4, 5, 5, 5, 5, 0
|
|
};
|
|
static const int dist_base[30] = {
|
|
1, 2, 3, 4, 5, 7, 9, 13, 17, 25,
|
|
33, 49, 65, 97, 129, 193, 257, 385, 513, 769,
|
|
1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577
|
|
};
|
|
static const int dist_extra[30] = {
|
|
0, 0, 0, 0, 1, 1, 2, 2, 3, 3,
|
|
4, 4, 5, 5, 6, 6, 7, 7, 8, 8,
|
|
9, 9, 10, 10, 11, 11, 12, 12, 13, 13
|
|
};
|
|
|
|
int final = 0;
|
|
while (!final) {
|
|
uint32_t bfinal, btype;
|
|
if (!png_bitstream_get(&bs, 1, &bfinal)) goto fail;
|
|
if (!png_bitstream_get(&bs, 2, &btype)) goto fail;
|
|
final = (int)bfinal;
|
|
|
|
if (btype == 0) {
|
|
if (!png_bitstream_align(&bs)) goto fail;
|
|
uint32_t stored_len, stored_nlen;
|
|
if (!png_bitstream_get(&bs, 16, &stored_len)) goto fail;
|
|
if (!png_bitstream_get(&bs, 16, &stored_nlen)) goto fail;
|
|
if ((stored_len ^ 0xffffu) != stored_nlen) goto fail;
|
|
if (out_pos + stored_len > expected_len) goto fail;
|
|
if (!png_bitstream_read_bytes(&bs, out + out_pos, stored_len)) goto fail;
|
|
out_pos += stored_len;
|
|
} else if (btype == 1 || btype == 2) {
|
|
png_huffman litlen, dist;
|
|
if (btype == 1) {
|
|
if (!png_build_fixed_huffman(&litlen, &dist)) goto fail;
|
|
} else {
|
|
if (!png_build_dynamic_huffman(&bs, &litlen, &dist)) goto fail;
|
|
}
|
|
|
|
for (;;) {
|
|
int sym;
|
|
if (!png_huffman_decode(&bs, &litlen, &sym)) goto fail;
|
|
if (sym < 256) {
|
|
if (out_pos >= expected_len) goto fail;
|
|
out[out_pos++] = (uint8_t)sym;
|
|
} else if (sym == 256) {
|
|
break;
|
|
} else if (sym <= 285) {
|
|
int len_sym = sym - 257;
|
|
uint32_t extra, dist_extra_bits;
|
|
int dist_sym;
|
|
int length = len_base[len_sym];
|
|
if (len_extra[len_sym]) {
|
|
if (!png_bitstream_get(&bs, len_extra[len_sym], &extra)) goto fail;
|
|
length += (int)extra;
|
|
}
|
|
if (!png_huffman_decode(&bs, &dist, &dist_sym)) goto fail;
|
|
if (dist_sym >= 30) goto fail;
|
|
int distance = dist_base[dist_sym];
|
|
if (dist_extra[dist_sym]) {
|
|
if (!png_bitstream_get(&bs, dist_extra[dist_sym], &dist_extra_bits)) goto fail;
|
|
distance += (int)dist_extra_bits;
|
|
}
|
|
if (distance <= 0 || (size_t)distance > out_pos) goto fail;
|
|
if (out_pos + length > expected_len) goto fail;
|
|
for (int i = 0; i < length; i++) {
|
|
out[out_pos] = out[out_pos - distance];
|
|
out_pos++;
|
|
}
|
|
} else {
|
|
goto fail;
|
|
}
|
|
}
|
|
} else {
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
if (out_pos != expected_len) goto fail;
|
|
|
|
uint32_t expected_adler = ((uint32_t)data[len - 4] << 24) |
|
|
((uint32_t)data[len - 3] << 16) |
|
|
((uint32_t)data[len - 2] << 8) |
|
|
(uint32_t)data[len - 1];
|
|
if (adler32(out, expected_len) != expected_adler) goto fail;
|
|
|
|
return out;
|
|
|
|
fail:
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
|
|
/* Apply PNG filter to reconstructed row */
|
|
static void png_unfilter_row(uint8_t *row, const uint8_t *prev_row,
|
|
int filter, int width, int channels) {
|
|
int bpp = channels;
|
|
|
|
switch (filter) {
|
|
case 0: /* None */
|
|
break;
|
|
case 1: /* Sub */
|
|
for (int i = bpp; i < width * channels; i++) {
|
|
row[i] = row[i] + row[i - bpp];
|
|
}
|
|
break;
|
|
case 2: /* Up */
|
|
if (prev_row) {
|
|
for (int i = 0; i < width * channels; i++) {
|
|
row[i] = row[i] + prev_row[i];
|
|
}
|
|
}
|
|
break;
|
|
case 3: /* Average */
|
|
for (int i = 0; i < width * channels; i++) {
|
|
int a = (i >= bpp) ? row[i - bpp] : 0;
|
|
int b = prev_row ? prev_row[i] : 0;
|
|
row[i] = row[i] + (a + b) / 2;
|
|
}
|
|
break;
|
|
case 4: /* Paeth */
|
|
for (int i = 0; i < width * channels; i++) {
|
|
int a = (i >= bpp) ? row[i - bpp] : 0;
|
|
int b = prev_row ? prev_row[i] : 0;
|
|
int c = (prev_row && i >= bpp) ? prev_row[i - bpp] : 0;
|
|
int p = a + b - c;
|
|
int pa = abs(p - a);
|
|
int pb = abs(p - b);
|
|
int pc = abs(p - c);
|
|
int pr = (pa <= pb && pa <= pc) ? a : (pb <= pc) ? b : c;
|
|
row[i] = row[i] + pr;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
static iris_image *load_png(FILE *f) {
|
|
/* Verify signature */
|
|
uint8_t sig[8];
|
|
if (fread(sig, 1, 8, f) != 8) return NULL;
|
|
|
|
const uint8_t expected[8] = {0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a};
|
|
if (memcmp(sig, expected, 8) != 0) return NULL;
|
|
|
|
int width = 0, height = 0, bit_depth = 0, color_type = 0, interlace = 0;
|
|
uint8_t *idat_data = NULL;
|
|
size_t idat_len = 0;
|
|
|
|
/* Read chunks */
|
|
while (1) {
|
|
uint32_t chunk_len = read_be32(f);
|
|
char chunk_type[5] = {0};
|
|
if (fread(chunk_type, 1, 4, f) != 4) break;
|
|
|
|
if (strcmp(chunk_type, "IHDR") == 0) {
|
|
width = read_be32(f);
|
|
height = read_be32(f);
|
|
bit_depth = fgetc(f);
|
|
color_type = fgetc(f);
|
|
(void)fgetc(f); /* compression */
|
|
(void)fgetc(f); /* filter */
|
|
interlace = fgetc(f);
|
|
fseek(f, 4, SEEK_CUR); /* Skip CRC */
|
|
} else if (strcmp(chunk_type, "IDAT") == 0) {
|
|
/* Accumulate IDAT chunks */
|
|
idat_data = (uint8_t *)realloc(idat_data, idat_len + chunk_len);
|
|
if (fread(idat_data + idat_len, 1, chunk_len, f) != chunk_len) {
|
|
free(idat_data);
|
|
return NULL;
|
|
}
|
|
idat_len += chunk_len;
|
|
fseek(f, 4, SEEK_CUR); /* Skip CRC */
|
|
} else if (strcmp(chunk_type, "IEND") == 0) {
|
|
break;
|
|
} else {
|
|
/* Skip unknown chunk */
|
|
fseek(f, chunk_len + 4, SEEK_CUR);
|
|
}
|
|
}
|
|
|
|
if (width == 0 || height == 0 || !idat_data) {
|
|
free(idat_data);
|
|
return NULL;
|
|
}
|
|
|
|
/* Only 8-bit non-interlaced PNGs are supported */
|
|
if (bit_depth != 8) {
|
|
fprintf(stderr, "PNG error: unsupported bit depth %d (only 8-bit supported)\n", bit_depth);
|
|
free(idat_data);
|
|
return NULL;
|
|
}
|
|
if (interlace != 0) {
|
|
fprintf(stderr, "PNG error: interlaced PNGs not supported\n");
|
|
free(idat_data);
|
|
return NULL;
|
|
}
|
|
|
|
/* Determine channels from color type */
|
|
int channels;
|
|
switch (color_type) {
|
|
case 0: channels = 1; break; /* Grayscale */
|
|
case 2: channels = 3; break; /* RGB */
|
|
case 4: channels = 2; break; /* Grayscale + Alpha */
|
|
case 6: channels = 4; break; /* RGBA */
|
|
default:
|
|
free(idat_data);
|
|
return NULL;
|
|
}
|
|
|
|
/* Decompress */
|
|
size_t raw_len = height * (1 + width * channels);
|
|
uint8_t *raw = inflate_zlib(idat_data, idat_len, raw_len);
|
|
free(idat_data);
|
|
|
|
if (!raw) return NULL;
|
|
|
|
/* Create image and apply filters */
|
|
iris_image *img = iris_image_create(width, height, channels);
|
|
if (!img) {
|
|
free(raw);
|
|
return NULL;
|
|
}
|
|
|
|
int row_bytes = 1 + width * channels;
|
|
uint8_t *prev_row = NULL;
|
|
|
|
for (int y = 0; y < height; y++) {
|
|
uint8_t *row_data = raw + y * row_bytes;
|
|
int filter = row_data[0];
|
|
uint8_t *row = row_data + 1;
|
|
|
|
png_unfilter_row(row, prev_row, filter, width, channels);
|
|
|
|
memcpy(img->data + y * width * channels, row, width * channels);
|
|
prev_row = row;
|
|
}
|
|
|
|
free(raw);
|
|
return img;
|
|
}
|
|
|
|
/* ========================================================================
|
|
* JPEG Format (read-only)
|
|
* ======================================================================== */
|
|
|
|
static iris_image *load_jpeg(FILE *f) {
|
|
/* Read entire file into memory and decode using the standalone jpeg.h implementation.
|
|
* This keeps JPEG parsing in one place and avoids duplicated decoder code. */
|
|
fseek(f, 0, SEEK_END);
|
|
long file_size_long = ftell(f);
|
|
if (file_size_long <= 0) return NULL;
|
|
size_t file_size = (size_t)file_size_long;
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
uint8_t *file_data = (uint8_t *)malloc(file_size);
|
|
if (!file_data) return NULL;
|
|
if (fread(file_data, 1, file_size, f) != file_size) {
|
|
free(file_data);
|
|
return NULL;
|
|
}
|
|
|
|
jpeg_image *jpg = jpeg_load_mem(file_data, file_size);
|
|
free(file_data);
|
|
if (!jpg) return NULL;
|
|
|
|
iris_image *img = iris_image_create(jpg->width, jpg->height, jpg->channels);
|
|
if (!img) {
|
|
jpeg_free(jpg);
|
|
return NULL;
|
|
}
|
|
|
|
size_t bytes = (size_t)jpg->width * (size_t)jpg->height * (size_t)jpg->channels;
|
|
memcpy(img->data, jpg->data, bytes);
|
|
jpeg_free(jpg);
|
|
return img;
|
|
}
|
|
|
|
/* ========================================================================
|
|
* Public API
|
|
* ======================================================================== */
|
|
|
|
static const char *get_extension(const char *path) {
|
|
const char *dot = strrchr(path, '.');
|
|
if (!dot || dot == path) return "";
|
|
return dot + 1;
|
|
}
|
|
|
|
iris_image *iris_image_load(const char *path) {
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f) return NULL;
|
|
|
|
iris_image *img = NULL;
|
|
const char *ext = get_extension(path);
|
|
|
|
if (strcasecmp(ext, "png") == 0) {
|
|
img = load_png(f);
|
|
} else if (strcasecmp(ext, "ppm") == 0 || strcasecmp(ext, "pgm") == 0) {
|
|
img = load_ppm(f);
|
|
} else if (strcasecmp(ext, "jpg") == 0 || strcasecmp(ext, "jpeg") == 0) {
|
|
img = load_jpeg(f);
|
|
} else {
|
|
/* Try to detect by magic bytes */
|
|
uint8_t magic[8];
|
|
if (fread(magic, 1, 8, f) == 8) {
|
|
fseek(f, 0, SEEK_SET);
|
|
if (magic[0] == 0x89 && magic[1] == 'P' && magic[2] == 'N' && magic[3] == 'G') {
|
|
img = load_png(f);
|
|
} else if (magic[0] == 'P' && (magic[1] == '5' || magic[1] == '6')) {
|
|
img = load_ppm(f);
|
|
} else if (magic[0] == 0xFF && magic[1] == 0xD8) {
|
|
img = load_jpeg(f);
|
|
}
|
|
}
|
|
}
|
|
|
|
fclose(f);
|
|
return img;
|
|
}
|
|
|
|
int iris_image_save(const iris_image *img, const char *path) {
|
|
if (!img || !path) return -1;
|
|
|
|
FILE *f = fopen(path, "wb");
|
|
if (!f) return -1;
|
|
|
|
int result;
|
|
const char *ext = get_extension(path);
|
|
|
|
if (strcasecmp(ext, "png") == 0) {
|
|
result = save_png(img, f);
|
|
} else if (strcasecmp(ext, "ppm") == 0 || strcasecmp(ext, "pgm") == 0) {
|
|
result = save_ppm(img, f);
|
|
} else {
|
|
/* Default to PNG */
|
|
result = save_png(img, f);
|
|
}
|
|
|
|
fclose(f);
|
|
return result;
|
|
}
|
|
|
|
int iris_image_save_with_seed(const iris_image *img, const char *path, int64_t seed) {
|
|
if (!img || !path) return -1;
|
|
|
|
FILE *f = fopen(path, "wb");
|
|
if (!f) return -1;
|
|
|
|
int result;
|
|
const char *ext = get_extension(path);
|
|
|
|
if (strcasecmp(ext, "png") == 0) {
|
|
result = save_png_with_metadata(img, f, seed, 1);
|
|
} else if (strcasecmp(ext, "ppm") == 0 || strcasecmp(ext, "pgm") == 0) {
|
|
/* PPM doesn't support metadata, just save normally */
|
|
result = save_ppm(img, f);
|
|
} else {
|
|
/* Default to PNG with seed */
|
|
result = save_png_with_metadata(img, f, seed, 1);
|
|
}
|
|
|
|
fclose(f);
|
|
return result;
|
|
}
|
|
|
|
/* ========================================================================
|
|
* Image Manipulation
|
|
* ======================================================================== */
|
|
|
|
iris_image *iris_image_resize(const iris_image *img, int new_width, int new_height) {
|
|
if (!img || new_width <= 0 || new_height <= 0) return NULL;
|
|
|
|
iris_image *resized = iris_image_create(new_width, new_height, img->channels);
|
|
if (!resized) return NULL;
|
|
|
|
float scale_x = (float)img->width / new_width;
|
|
float scale_y = (float)img->height / new_height;
|
|
|
|
for (int y = 0; y < new_height; y++) {
|
|
for (int x = 0; x < new_width; x++) {
|
|
float src_x = (x + 0.5f) * scale_x - 0.5f;
|
|
float src_y = (y + 0.5f) * scale_y - 0.5f;
|
|
|
|
int x0 = (int)floorf(src_x);
|
|
int y0 = (int)floorf(src_y);
|
|
int x1 = x0 + 1;
|
|
int y1 = y0 + 1;
|
|
|
|
float wx = src_x - x0;
|
|
float wy = src_y - y0;
|
|
|
|
x0 = (x0 < 0) ? 0 : (x0 >= img->width) ? img->width - 1 : x0;
|
|
x1 = (x1 < 0) ? 0 : (x1 >= img->width) ? img->width - 1 : x1;
|
|
y0 = (y0 < 0) ? 0 : (y0 >= img->height) ? img->height - 1 : y0;
|
|
y1 = (y1 < 0) ? 0 : (y1 >= img->height) ? img->height - 1 : y1;
|
|
|
|
for (int c = 0; c < img->channels; c++) {
|
|
float v00 = img->data[(y0 * img->width + x0) * img->channels + c];
|
|
float v01 = img->data[(y0 * img->width + x1) * img->channels + c];
|
|
float v10 = img->data[(y1 * img->width + x0) * img->channels + c];
|
|
float v11 = img->data[(y1 * img->width + x1) * img->channels + c];
|
|
|
|
float v = v00 * (1 - wx) * (1 - wy) +
|
|
v01 * wx * (1 - wy) +
|
|
v10 * (1 - wx) * wy +
|
|
v11 * wx * wy;
|
|
|
|
resized->data[(y * new_width + x) * img->channels + c] = (uint8_t)(v + 0.5f);
|
|
}
|
|
}
|
|
}
|
|
|
|
return resized;
|
|
}
|
|
|
|
/* Convert image to specific number of channels */
|
|
iris_image *iris_image_convert(const iris_image *img, int new_channels) {
|
|
if (!img || new_channels < 1 || new_channels > 4) return NULL;
|
|
|
|
iris_image *converted = iris_image_create(img->width, img->height, new_channels);
|
|
if (!converted) return NULL;
|
|
|
|
for (int y = 0; y < img->height; y++) {
|
|
for (int x = 0; x < img->width; x++) {
|
|
const uint8_t *src = img->data + (y * img->width + x) * img->channels;
|
|
uint8_t *dst = converted->data + (y * img->width + x) * new_channels;
|
|
|
|
if (img->channels == 1 && new_channels >= 3) {
|
|
/* Grayscale to RGB(A) */
|
|
dst[0] = dst[1] = dst[2] = src[0];
|
|
if (new_channels == 4) dst[3] = 255;
|
|
} else if (img->channels >= 3 && new_channels == 1) {
|
|
/* RGB to grayscale */
|
|
dst[0] = (uint8_t)(0.299f * src[0] + 0.587f * src[1] + 0.114f * src[2]);
|
|
} else {
|
|
/* Copy available channels */
|
|
for (int c = 0; c < new_channels; c++) {
|
|
if (c < img->channels) {
|
|
dst[c] = src[c];
|
|
} else {
|
|
dst[c] = 255; /* Alpha or missing = 255 */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return converted;
|
|
}
|
|
|
|
/* strcasecmp for portability */
|
|
#ifdef _WIN32
|
|
#define strcasecmp _stricmp
|
|
#endif
|