Add files via upload

This commit is contained in:
Nexoniarz 2026-06-20 15:03:21 +02:00 • committed by GitHub
commit b9b8c93a22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 1060 additions and 0 deletions

262
README.md Normal file
View file

@ -0,0 +1,262 @@
# YAPF — Yet Another Picture Format
**Fast. Small. Easy.**
YAPF is a lossless image codec built around a single principle: decoding should
be as close to a memory copy as possible. Instead of running a general-purpose
compressor like DEFLATE over pixel data, YAPF encodes images with five cheap
operations — a pixel cache lookup, two flavours of delta encoding, run-length
encoding, and verbatim pixel storage — producing files that are smaller than
uncompressed formats while decoding nearly as fast as `memcpy`.
---
## Why YAPF?
Standard lossless formats like PNG compress well, but decompressing DEFLATE
streams is CPU-intensive. On large images (4K and above) or in
latency-sensitive applications (game engines, real-time pipelines, embedded
systems) this is a meaningful bottleneck.
YAPF trades a modest compression ratio advantage for dramatically lower decode
complexity. There are no Huffman tables to build, no sliding windows to
maintain, and no bit-level unpacking. The decoder is a tight byte-dispatch
loop that any modern CPU handles at memory bandwidth.
### Performance
Benchmark: loading a 5K professional photograph (5120 × 2880 px).
| Format | Load time |
| :----- | ----------: |
| **YAPF** | **0.111 s** |
| PNG | 0.194 s |
YAPF decodes approximately **1.7× faster** than PNG on the same hardware
without SIMD or threading.
### When YAPF excels
- **Pixel art, sprites, UI textures** — large flat regions of identical colour
compress to single RUN bytes; palette-like images benefit heavily from the
cache.
- **Screenshots and diagrams** — solid backgrounds and repeated gradients hit
DIFF/LUMA and the cache constantly.
- **Game asset pipelines** — load textures at startup without paying a
decompression tax every frame.
- **Embedded / resource-constrained targets** — the entire codec fits in ~200
lines of portable C with zero external dependencies.
---
## Integration
The core library is two files. Drop them into your project and compile:
```
yapf.h — public API and format documentation
yapf.c — encoder / decoder implementation
```
```c
#include "yapf.h"
int main(void) {
/* Load */
yapf_image_t *img = yapf_load("photo.yapf");
if (!img) { /* file not found or not a valid YAPF stream */ }
printf("%u x %u, %u channel(s)\n",
img->width, img->height, img->channels);
/* Save */
int rc = yapf_save("output.yapf", img);
if (rc != YAPF_OK) { /* write error */ }
yapf_free(img);
return 0;
}
```
Compile with any C99-capable compiler — no flags required for the baseline:
```sh
gcc -O2 -std=c99 main.c yapf.c -o myapp
```
---
## API Reference
### Types
```c
typedef struct {
uint32_t width; /* pixels per row */
uint32_t height; /* rows */
uint8_t channels; /* 1 gray / 2 gray+alpha / 3 RGB / 4 RGBA */
uint8_t *pixels; /* raw pixel buffer, malloc-owned */
} yapf_image_t;
```
`pixels` is a tightly packed, row-major byte array of `width × height × channels`
bytes. The layout per pixel matches the `channels` value:
| `channels` | Layout per pixel |
| :--------- | :--------------- |
| 1 | `Y` |
| 2 | `Y A` |
| 3 | `R G B` |
| 4 | `R G B A` |
### Functions
```c
yapf_image_t *yapf_load(const char *filename);
```
Decodes a YAPF file from disk. Returns a heap-allocated `yapf_image_t` on
success, `NULL` on any failure (file not found, invalid magic, unsupported
version, out of memory). Free with `yapf_free()`.
```c
int yapf_save(const char *filename, const yapf_image_t *img);
```
Encodes an image to disk. Returns `YAPF_OK` (0) on success or a negative
`YAPF_ERR_*` code on failure.
```c
void yapf_free(yapf_image_t *img);
```
Releases memory returned by `yapf_load()`. Passing `NULL` is safe.
### Return codes
| Constant | Value | Meaning |
| :---------------- | ----: | :----------------------------------- |
| `YAPF_OK` | 0 | Success |
| `YAPF_ERR_INVALID`| –1 | NULL or logically invalid argument |
| `YAPF_ERR_IO` | –2 | File could not be opened or written |
### Channel constants
```c
YAPF_CHANNELS_GRAY 1
YAPF_CHANNELS_GRAY_ALPHA 2
YAPF_CHANNELS_RGB 3
YAPF_CHANNELS_RGBA 4
```
---
## Building the libraries
The build scripts uses the [LLVM](https://releases.llvm.org/) compiler
```sh
# Unix (Linux / macOS)
chmod +x build.sh
./build.sh
# Windows
build.bat
```
Output goes to `dist/` with one subdirectory per platform:
```
dist/
├── windows-x64/ yapf.dll yapf.lib
├── windows-arm64/ yapf.dll yapf.lib
├── macos-x64/ libyapf.dylib
├── macos-arm64/ libyapf.dylib
├── linux-x64/ libyapf.so libyapf.a
└── linux-arm64/ libyapf.so libyapf.a
```
### Optional build flags
| Flag | Effect |
| :---------------- | :-------------------------------------------------- |
| `-DYAPF_USE_SIMD` | Reserved for future SIMD acceleration paths |
The interactive build script will prompt for these options.
---
## Technical Specification
### File header — 14 bytes
| Offset | Size | Type | Description |
| :----- | ---: | :--------- | :-------------------------------------- |
| 0x00 | 4 | `char[4]` | Magic: `"YAPF"` |
| 0x04 | 1 | `uint8_t` | Format version (currently `1`) |
| 0x05 | 4 | `uint32_t` | Width in pixels, little-endian |
| 0x09 | 4 | `uint32_t` | Height in pixels, little-endian |
| 0x0D | 1 | `uint8_t` | Channel count: 1 / 2 / 3 / 4 |
All multi-byte integers are always stored little-endian, regardless of host
architecture.
### Byte-stream opcodes
The payload begins immediately after the header and is decoded left-to-right.
A 64-slot pixel cache (zero-initialised) and a running "previous pixel" state
(`r=0, g=0, b=0, a=255`) are maintained throughout.
| Byte range | Opcode | Size | Description |
| :---------- | :----- | ------: | :-------------------------------------------- |
| 0x00–0x3F | INDEX | 1 byte | Restore pixel from cache slot `byte & 0x3F` |
| 0x40–0x7F | DIFF | 1 byte | Per-channel deltas: dr = bits[5:4]–2, dg = bits[3:2]–2, db = bits[1:0]–2. Range –2..+1. Alpha unchanged. |
| 0x80–0xBF | LUMA | 2 bytes | Green delta dg = (byte & 0x3F)–32. Second byte: dr = ((b1>>4)&0xF)–8+dg, db = (b1&0xF)–8+dg. Alpha unchanged. |
| 0xC0–0xFD | RUN | 1 byte | Repeat previous pixel (byte&0x3F)+1 times (1–62). Cache not updated. |
| 0xFE | RGB | 4 bytes | Next 3 bytes = R, G, B. Alpha unchanged. |
| 0xFF | RGBA | 5 bytes | Next 4 bytes = R, G, B, A. |
After every non-RUN opcode, the cache slot `HASH(r,g,b,a)` is updated and the
pixel becomes the new previous pixel.
```
HASH(r,g,b,a) = (r×3 + g×5 + b×7 + a×11) mod 64
```
The stream ends when exactly `width × height` pixels have been decoded; no
end-of-stream marker is required.
### Pixel buffer layout
Within the decoded buffer, each pixel occupies `channels` consecutive bytes in
the order shown in the API table above. Grayscale images store a single
luminance byte per pixel; the encoder internally promotes gray values to
`r = g = b = Y` for uniform opcode selection.
---
## GIMP Plugin
A plugin for GIMP is located in `extensions/gimp/`. See the
[build instructions](extensions/gimp/howtocompile.txt) for details.
Supported colour modes: Gray, Gray+Alpha, RGB, RGBA.
File extension: `.yapf`. MIME type: `image/x-yapf`.
---
## Platforms
| Platform | Architectures | Library format |
| :------------- | :------------ | :------------------ |
| Windows | x64, ARM64 | `.dll` + `.lib` |
| macOS | ARM64 | `.dylib` |
| Linux (glibc) | x64, ARM64 | `.so` |
| Linux (musl) | x64, ARM64 | `.a` (static) |
The core library (`yapf.c` / `yapf.h`) uses only the C99 standard library
(`<stdio.h>`, `<stdlib.h>`, `<string.h>`, `<stdint.h>`) and compiles cleanly
on any conforming C99 or C++ toolchain with no configuration required.
---
## License
Apache License 2.0 — see [LICENSE](LICENSE).

588
yapf.c Normal file
View file

@ -0,0 +1,588 @@
/*
* yapf.c — Yet Another Picture Format
* Encoder and decoder implementation.
*
* Copyright 2026 Nexoniarz
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "yapf.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ── Constants ───────────────────────────────────────────────────────── */
#define YAPF__MAGIC "YAPF"
#define YAPF__VERSION 2
#define YAPF__HDR_LEN 20
#define YAPF__MIP_ENTRY 8
#define YAPF__MAX_TILE_PX (YAPF_TILE_SIZE * YAPF_TILE_SIZE) /* 1024 */
/* ── Tile pixel type ─────────────────────────────────────────────────── */
/*
* After YCoCg-R, channels 1 (Co) and 2 (Cg) span [-255, 255] and need
* 9 bits, so we use int16_t for all channels internally within a tile.
* Grayscale and alpha channels stay in [0, 255].
*/
typedef struct { int16_t v[4]; } yapf__tpx;
/* ── I/O helpers ─────────────────────────────────────────────────────── */
static uint32_t yapf__r32le(const uint8_t *p) {
return (uint32_t)p[0]
| ((uint32_t)p[1] << 8)
| ((uint32_t)p[2] << 16)
| ((uint32_t)p[3] << 24);
}
static void yapf__w32le(uint8_t *p, uint32_t v) {
p[0] = (uint8_t)(v);
p[1] = (uint8_t)(v >> 8);
p[2] = (uint8_t)(v >> 16);
p[3] = (uint8_t)(v >> 24);
}
/* ── Bit-writer ──────────────────────────────────────────────────────── */
typedef struct {
uint8_t *buf;
size_t cap;
size_t len;
uint8_t acc; /* accumulator */
int bits; /* bits filled in acc (0-7) */
} yapf__bw;
static int yapf__bw_init(yapf__bw *w, size_t cap) {
w->buf = (uint8_t *)malloc(cap ? cap : 64);
if (!w->buf) return -1;
w->cap = cap ? cap : 64;
w->len = 0;
w->acc = 0;
w->bits = 0;
return 0;
}
static int yapf__bw_grow(yapf__bw *w, size_t need) {
if (w->len + need <= w->cap) return 0;
size_t nc = w->cap * 2 + need;
uint8_t *nb = (uint8_t *)realloc(w->buf, nc);
if (!nb) return -1;
w->buf = nb;
w->cap = nc;
return 0;
}
/* Write n bits from v, MSB first; n in [1, 32]. */
static int yapf__bw_put(yapf__bw *w, uint32_t v, int n) {
if (yapf__bw_grow(w, (size_t)(n + 7) / 8 + 2) != 0) return -1;
for (int i = n - 1; i >= 0; i--) {
w->acc = (uint8_t)((w->acc << 1) | ((v >> i) & 1u));
if (++w->bits == 8) {
w->buf[w->len++] = w->acc;
w->acc = 0;
w->bits = 0;
}
}
return 0;
}
/* Flush partial byte (zero-padded) and reset to byte boundary. */
static void yapf__bw_flush(yapf__bw *w) {
if (w->bits > 0) {
w->buf[w->len++] = (uint8_t)(w->acc << (8 - w->bits));
w->acc = 0;
w->bits = 0;
}
}
/* ── Bit-reader ──────────────────────────────────────────────────────── */
typedef struct {
const uint8_t *buf;
size_t len;
size_t pos;
uint8_t acc;
int bits;
} yapf__br;
static void yapf__br_init(yapf__br *r, const uint8_t *buf, size_t len) {
r->buf = buf;
r->len = len;
r->pos = 0;
r->acc = 0;
r->bits = 0;
}
/* Read n bits MSB-first into *out. Returns 0 on success, -1 on underflow. */
static int yapf__br_get(yapf__br *r, int n, uint32_t *out) {
uint32_t v = 0;
for (int i = 0; i < n; i++) {
if (r->bits == 0) {
if (r->pos >= r->len) return -1;
r->acc = r->buf[r->pos++];
r->bits = 8;
}
v = (v << 1) | ((r->acc >> 7) & 1u);
r->acc = (uint8_t)(r->acc << 1);
r->bits--;
}
*out = v;
return 0;
}
/* Discard remaining bits in current byte (tile boundary alignment). */
static void yapf__br_align(yapf__br *r) { r->acc = 0; r->bits = 0; }
/* ── YCoCg-R transform ───────────────────────────────────────────────── */
/*
* Lossless, integer, fully reversible.
* Y ∈ [0,255], Co ∈ [-255,255], Cg ∈ [-255,255].
* Alpha passes through unchanged.
*/
static void yapf__forward(yapf__tpx *dst, const uint8_t *src, int ch) {
if (ch >= 3) {
int r = src[0], g = src[1], b = src[2];
int co = r - b;
int t = b + (co >> 1);
int cg = g - t;
int y = t + (cg >> 1);
dst->v[0] = (int16_t)y;
dst->v[1] = (int16_t)co;
dst->v[2] = (int16_t)cg;
if (ch == 4) dst->v[3] = (int16_t)src[3];
} else {
for (int c = 0; c < ch; c++) dst->v[c] = (int16_t)src[c];
}
}
static void yapf__inverse(uint8_t *dst, const yapf__tpx *src, int ch) {
if (ch >= 3) {
int y = src->v[0];
int co = src->v[1];
int cg = src->v[2];
int t = y - (cg >> 1);
int g = cg + t;
int b = t - (co >> 1);
int r = b + co;
dst[0] = (uint8_t)(r < 0 ? 0 : r > 255 ? 255 : r);
dst[1] = (uint8_t)(g < 0 ? 0 : g > 255 ? 255 : g);
dst[2] = (uint8_t)(b < 0 ? 0 : b > 255 ? 255 : b);
if (ch == 4) dst[3] = (uint8_t)src->v[3];
} else {
for (int c = 0; c < ch; c++) dst[c] = (uint8_t)src->v[c];
}
}
/* ── Predictor ───────────────────────────────────────────────────────── */
/* left + above - diag, unclipped (works for signed int16 channels). */
static int32_t yapf__predict(int16_t L, int16_t A, int16_t D) {
return (int32_t)L + (int32_t)A - (int32_t)D;
}
/* ── Signed bit width ────────────────────────────────────────────────── */
/* Minimum two's-complement bits to represent v. Returns 0 if v == 0. */
static int yapf__sbits(int v) {
if (v == 0) return 0;
if (v < 0) v = ~v; /* largest positive needing same width */
int b = 1; /* sign bit */
while (v > 0) { v >>= 1; b++; }
return b;
}
/* First-pixel bit widths: Co (ch1) and Cg (ch2) need 9 bits each because
* YCoCg-R maps them to [-255, 255]; all other channels stay at 8 bits. */
static int yapf__first_bits(int c, int ch) {
return (ch >= 3 && (c == 1 || c == 2)) ? 9 : 8;
}
/* ── Tile encode ─────────────────────────────────────────────────────── */
static int yapf__encode_tile(
yapf__bw *w,
const uint8_t *pixels, uint32_t img_w,
uint32_t tx, uint32_t ty, uint32_t tw, uint32_t th,
int ch)
{
yapf__tpx raw[YAPF__MAX_TILE_PX];
int max_abs[4] = {0, 0, 0, 0};
int bd[4];
size_t total = (size_t)tw * th;
/* Gather + transform */
for (uint32_t y = 0; y < th; y++) {
for (uint32_t x = 0; x < tw; x++) {
const uint8_t *src =
pixels + ((size_t)(ty + y) * img_w + (tx + x)) * (size_t)ch;
yapf__forward(&raw[y * tw + x], src, ch);
}
}
/* Compute residuals and track max |residual| per channel.
* Pixel (0,0) is stored raw; all others use 2-D prediction. */
for (size_t i = 1; i < total; i++) {
uint32_t x = (uint32_t)(i % tw);
uint32_t y = (uint32_t)(i / tw);
for (int c = 0; c < ch; c++) {
int16_t L = (x > 0) ? raw[y * tw + (x-1)].v[c] : 0;
int16_t A = (y > 0) ? raw[(y-1)*tw + x].v[c] : 0;
int16_t D = (x > 0 && y > 0) ? raw[(y-1)*tw + (x-1)].v[c] : 0;
int32_t pred = yapf__predict(L, A, D);
int res = (int)raw[i].v[c] - (int)pred;
int ab = res < 0 ? -res : res;
if (ab > max_abs[c]) max_abs[c] = ab;
}
}
/* Write per-channel residual bit depths */
for (int c = 0; c < ch; c++) {
bd[c] = yapf__sbits(max_abs[c]);
if (yapf__bw_put(w, (uint32_t)bd[c], 8) != 0) return -1;
}
/* Write first pixel with channel-appropriate width */
for (int c = 0; c < ch; c++) {
int nbits = yapf__first_bits(c, ch);
uint32_t bits = (uint32_t)((int32_t)raw[0].v[c] & ((1 << nbits) - 1));
if (yapf__bw_put(w, bits, nbits) != 0) return -1;
}
/* Write packed residuals for pixels 1 .. total-1 */
for (size_t i = 1; i < total; i++) {
uint32_t x = (uint32_t)(i % tw);
uint32_t y = (uint32_t)(i / tw);
for (int c = 0; c < ch; c++) {
if (bd[c] == 0) continue;
int16_t L = (x > 0) ? raw[y * tw + (x-1)].v[c] : 0;
int16_t A = (y > 0) ? raw[(y-1)*tw + x].v[c] : 0;
int16_t D = (x > 0 && y > 0) ? raw[(y-1)*tw + (x-1)].v[c] : 0;
int32_t pred = yapf__predict(L, A, D);
int res = (int)raw[i].v[c] - (int)pred;
uint32_t bits = (uint32_t)(res & ((1 << bd[c]) - 1));
if (yapf__bw_put(w, bits, bd[c]) != 0) return -1;
}
}
yapf__bw_flush(w);
return 0;
}
/* ── Tile decode ─────────────────────────────────────────────────────── */
static int yapf__decode_tile(
yapf__br *r,
uint8_t *pixels, uint32_t img_w,
uint32_t tx, uint32_t ty, uint32_t tw, uint32_t th,
int ch)
{
yapf__tpx raw[YAPF__MAX_TILE_PX];
int bd[4];
uint32_t bits;
size_t total = (size_t)tw * th;
/* Read residual bit depths */
for (int c = 0; c < ch; c++) {
if (yapf__br_get(r, 8, &bits) != 0) return -1;
bd[c] = (int)bits;
}
/* Read first pixel.
* 8-bit channels (Y, A, grayscale) are unsigned [0,255] — no sign-extend.
* 9-bit channels (Co, Cg) are signed [-255,255] — two's complement. */
for (int c = 0; c < ch; c++) {
int nbits = yapf__first_bits(c, ch);
if (yapf__br_get(r, nbits, &bits) != 0) return -1;
int v;
if (nbits == 9) {
v = (int)(bits & 0xFFu);
if (bits & 0x100u) v -= 256;
} else {
v = (int)bits; /* unsigned: 0–255 */
}
raw[0].v[c] = (int16_t)v;
}
/* Decode pixels 1 .. total-1 */
for (size_t i = 1; i < total; i++) {
uint32_t x = (uint32_t)(i % tw);
uint32_t y = (uint32_t)(i / tw);
for (int c = 0; c < ch; c++) {
int16_t L = (x > 0) ? raw[y * tw + (x-1)].v[c] : 0;
int16_t A = (y > 0) ? raw[(y-1)*tw + x].v[c] : 0;
int16_t D = (x > 0 && y > 0) ? raw[(y-1)*tw + (x-1)].v[c] : 0;
int32_t pred = yapf__predict(L, A, D);
int res = 0;
if (bd[c] > 0) {
if (yapf__br_get(r, bd[c], &bits) != 0) return -1;
int sb = 1 << (bd[c] - 1);
res = (int)(bits & (uint32_t)(sb - 1));
if (bits & (uint32_t)sb) res -= sb;
}
raw[i].v[c] = (int16_t)((int)pred + res);
}
}
yapf__br_align(r);
/* Inverse transform and write to output buffer */
for (uint32_t y = 0; y < th; y++) {
for (uint32_t x = 0; x < tw; x++) {
uint8_t *dst =
pixels + ((size_t)(ty + y) * img_w + (tx + x)) * (size_t)ch;
yapf__inverse(dst, &raw[y * tw + x], ch);
}
}
return 0;
}
/* ── Level encode / decode ───────────────────────────────────────────── */
static int yapf__encode_level(
yapf__bw *w, const uint8_t *pixels, uint32_t mw, uint32_t mh, int ch)
{
uint32_t tx_count = (mw + YAPF_TILE_SIZE - 1) / YAPF_TILE_SIZE;
uint32_t ty_count = (mh + YAPF_TILE_SIZE - 1) / YAPF_TILE_SIZE;
for (uint32_t ty = 0; ty < ty_count; ty++) {
for (uint32_t tx = 0; tx < tx_count; tx++) {
uint32_t tw = (tx + 1) * YAPF_TILE_SIZE <= mw
? YAPF_TILE_SIZE : mw - tx * YAPF_TILE_SIZE;
uint32_t th = (ty + 1) * YAPF_TILE_SIZE <= mh
? YAPF_TILE_SIZE : mh - ty * YAPF_TILE_SIZE;
if (yapf__encode_tile(w, pixels, mw,
tx * YAPF_TILE_SIZE,
ty * YAPF_TILE_SIZE,
tw, th, ch) != 0)
return -1;
}
}
return 0;
}
static int yapf__decode_level(
const uint8_t *data, size_t data_len,
uint8_t *pixels, uint32_t mw, uint32_t mh, int ch)
{
yapf__br r;
yapf__br_init(&r, data, data_len);
uint32_t tx_count = (mw + YAPF_TILE_SIZE - 1) / YAPF_TILE_SIZE;
uint32_t ty_count = (mh + YAPF_TILE_SIZE - 1) / YAPF_TILE_SIZE;
for (uint32_t ty = 0; ty < ty_count; ty++) {
for (uint32_t tx = 0; tx < tx_count; tx++) {
uint32_t tw = (tx + 1) * YAPF_TILE_SIZE <= mw
? YAPF_TILE_SIZE : mw - tx * YAPF_TILE_SIZE;
uint32_t th = (ty + 1) * YAPF_TILE_SIZE <= mh
? YAPF_TILE_SIZE : mh - ty * YAPF_TILE_SIZE;
if (yapf__decode_tile(&r, pixels, mw,
tx * YAPF_TILE_SIZE,
ty * YAPF_TILE_SIZE,
tw, th, ch) != 0)
return -1;
}
}
return 0;
}
/* ========================================================================
* yapf_load
* ======================================================================== */
yapf_image_t *yapf_load(const char *filename) {
if (!filename) return NULL;
FILE *f = fopen(filename, "rb");
if (!f) return NULL;
uint8_t hdr[YAPF__HDR_LEN];
if (fread(hdr, 1, YAPF__HDR_LEN, f) != YAPF__HDR_LEN) goto fail_f;
if (memcmp(hdr, YAPF__MAGIC, 4) != 0 || hdr[4] != YAPF__VERSION) goto fail_f;
uint8_t channels = hdr[5];
uint8_t gpu_fmt = hdr[6];
uint8_t mip_levels = hdr[7];
uint8_t flags = hdr[8];
uint32_t base_w = yapf__r32le(hdr + 12);
uint32_t base_h = yapf__r32le(hdr + 16);
if (channels < 1 || channels > 4) goto fail_f;
if (mip_levels < 1 || mip_levels > YAPF_MAX_MIPS) goto fail_f;
if (base_w == 0 || base_w > YAPF_MAX_DIM) goto fail_f;
if (base_h == 0 || base_h > YAPF_MAX_DIM) goto fail_f;
/* Mip index */
size_t idx_bytes = (size_t)mip_levels * YAPF__MIP_ENTRY;
uint8_t *mip_idx = (uint8_t *)malloc(idx_bytes);
if (!mip_idx) goto fail_f;
if (fread(mip_idx, 1, idx_bytes, f) != idx_bytes) { free(mip_idx); goto fail_f; }
yapf_image_t *img = (yapf_image_t *)calloc(1, sizeof(yapf_image_t));
if (!img) { free(mip_idx); goto fail_f; }
img->width = base_w;
img->height = base_h;
img->channels = channels;
img->gpu_format = gpu_fmt;
img->flags = flags;
img->mip_levels = mip_levels;
img->mips = (uint8_t **)calloc(mip_levels, sizeof(uint8_t *));
if (!img->mips) { free(mip_idx); goto fail_img; }
for (uint8_t m = 0; m < mip_levels; m++) {
uint32_t offset = yapf__r32le(mip_idx + m * YAPF__MIP_ENTRY);
uint32_t size = yapf__r32le(mip_idx + m * YAPF__MIP_ENTRY + 4);
uint32_t mw = base_w >> m; if (mw < 1) mw = 1;
uint32_t mh = base_h >> m; if (mh < 1) mh = 1;
uint8_t *data = (uint8_t *)malloc(size);
if (!data) { free(mip_idx); goto fail_mips; }
if (fseek(f, (long)offset, SEEK_SET) != 0 ||
fread(data, 1, size, f) != size) {
free(data); free(mip_idx); goto fail_mips;
}
size_t px_bytes = (size_t)mw * mh * channels;
img->mips[m] = (uint8_t *)malloc(px_bytes);
if (!img->mips[m]) { free(data); free(mip_idx); goto fail_mips; }
if (yapf__decode_level(data, size, img->mips[m], mw, mh, channels) != 0) {
free(data); free(mip_idx); goto fail_mips;
}
free(data);
}
free(mip_idx);
img->pixels = img->mips[0];
fclose(f);
return img;
fail_mips:
for (uint8_t m = 0; m < mip_levels; m++) free(img->mips[m]);
free(img->mips);
fail_img:
free(img);
fail_f:
fclose(f);
return NULL;
}
/* ========================================================================
* yapf_save
* ======================================================================== */
int yapf_save(const char *filename, const yapf_image_t *img) {
if (!filename || !img || !img->pixels) return YAPF_ERR_INVALID;
if (img->width == 0 || img->width > YAPF_MAX_DIM) return YAPF_ERR_INVALID;
if (img->height == 0 || img->height > YAPF_MAX_DIM) return YAPF_ERR_INVALID;
if (img->channels < 1 || img->channels > 4) return YAPF_ERR_INVALID;
if (img->mip_levels < 1 || img->mip_levels > YAPF_MAX_MIPS) return YAPF_ERR_INVALID;
int ch = (int)img->channels;
uint8_t mip_levels = img->mip_levels;
/* Encode each mip level */
yapf__bw bws[YAPF_MAX_MIPS];
memset(bws, 0, sizeof(bws));
for (uint8_t m = 0; m < mip_levels; m++) {
uint32_t mw = img->width >> m; if (mw < 1) mw = 1;
uint32_t mh = img->height >> m; if (mh < 1) mh = 1;
const uint8_t *src = (m == 0)
? img->pixels
: (img->mips ? img->mips[m] : NULL);
if (!src) {
for (uint8_t j = 0; j < m; j++) free(bws[j].buf);
return YAPF_ERR_INVALID;
}
size_t est = (size_t)mw * mh * (size_t)ch + 256;
if (yapf__bw_init(&bws[m], est) != 0) {
for (uint8_t j = 0; j < m; j++) free(bws[j].buf);
return YAPF_ERR_OOM;
}
if (yapf__encode_level(&bws[m], src, mw, mh, ch) != 0) {
for (uint8_t j = 0; j <= m; j++) free(bws[j].buf);
return YAPF_ERR_OOM;
}
}
/* Compute mip index offsets */
uint32_t offsets[YAPF_MAX_MIPS];
uint32_t cur = YAPF__HDR_LEN + (uint32_t)mip_levels * YAPF__MIP_ENTRY;
for (uint8_t m = 0; m < mip_levels; m++) {
offsets[m] = cur;
cur += (uint32_t)bws[m].len;
}
FILE *f = fopen(filename, "wb");
if (!f) {
for (uint8_t m = 0; m < mip_levels; m++) free(bws[m].buf);
return YAPF_ERR_IO;
}
/* Header */
uint8_t hdr[YAPF__HDR_LEN];
memset(hdr, 0, sizeof(hdr));
memcpy(hdr, YAPF__MAGIC, 4);
hdr[4] = YAPF__VERSION;
hdr[5] = img->channels;
hdr[6] = img->gpu_format;
hdr[7] = mip_levels;
hdr[8] = img->flags;
yapf__w32le(hdr + 12, img->width);
yapf__w32le(hdr + 16, img->height);
fwrite(hdr, 1, YAPF__HDR_LEN, f);
/* Mip index */
for (uint8_t m = 0; m < mip_levels; m++) {
uint8_t entry[YAPF__MIP_ENTRY];
yapf__w32le(entry + 0, offsets[m]);
yapf__w32le(entry + 4, (uint32_t)bws[m].len);
fwrite(entry, 1, YAPF__MIP_ENTRY, f);
}
/* Payload */
int err = YAPF_OK;
for (uint8_t m = 0; m < mip_levels; m++) {
if (fwrite(bws[m].buf, 1, bws[m].len, f) != bws[m].len)
err = YAPF_ERR_IO;
free(bws[m].buf);
}
fclose(f);
return err;
}
/* ========================================================================
* yapf_free
* ======================================================================== */
void yapf_free(yapf_image_t *img) {
if (!img) return;
if (img->mips) {
for (uint8_t m = 0; m < img->mip_levels; m++)
free(img->mips[m]);
free(img->mips);
}
free(img);
}

210
yapf.h Normal file
View file

@ -0,0 +1,210 @@
/*
* yapf.h — Yet Another Picture Format
* Public API and format specification.
*
* Copyright 2026 Nexoniarz
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* -------------------------------------------------------------------------
*
* YAPF — lossless, GPU-ready image format.
*
* Space savings come from two cheap mathematical steps — no entropy coder,
* no lookup tables, no opcode dispatch. Decode is straight arithmetic:
*
* 1. YCoCg-R color transform (RGB↔Y/Co/Cg, fully reversible integers)
* 2. 2-D neighbor prediction + per-tile adaptive bit-packing
*
* The file carries GPU metadata (format hint, mip levels, flags) so a
* loader can upload the decoded buffer straight to a texture without any
* additional transcoding step.
*
* =========================================================================
* FILE FORMAT (version 2)
* =========================================================================
*
* All multi-byte integers are little-endian.
*
* ┌─────────────────────────────────────────────────────────────────────┐
* │ HEADER — 20 bytes │
* ├────────┬──────┬────────────────────────────────────────────────────┤
* │ Offset │ Size │ Field │
* ├────────┼──────┼────────────────────────────────────────────────────┤
* │ 0x00 │ 4 │ Magic: "YAPF" │
* │ 0x04 │ 1 │ Version: 2 │
* │ 0x05 │ 1 │ Channels: 1 gray / 2 gray+A / 3 RGB / 4 RGBA │
* │ 0x06 │ 1 │ GPU format hint (YAPF_GPU_* constant) │
* │ 0x07 │ 1 │ Mip levels (1 = base only, max 16) │
* │ 0x08 │ 1 │ Flags (see YAPF_FLAG_* bitmask) │
* │ 0x09 │ 3 │ Reserved (write 0, ignore on read) │
* │ 0x0C │ 4 │ Base width (uint32, pixels) │
* │ 0x10 │ 4 │ Base height (uint32, pixels) │
* └────────┴──────┴────────────────────────────────────────────────────┘
*
* ┌─────────────────────────────────────────────────────────────────────┐
* │ MIP INDEX — 8 bytes × mip_levels, immediately after header │
* ├────────┬──────┬────────────────────────────────────────────────────┤
* │ +0 │ 4 │ Byte offset of mip data from start of file (uint32) │
* │ +4 │ 4 │ Byte size of mip data (uint32) │
* └────────┴──────┴────────────────────────────────────────────────────┘
*
* ┌─────────────────────────────────────────────────────────────────────┐
* │ PIXEL DATA — per mip level, referenced by the mip index │
* ├─────────────────────────────────────────────────────────────────────┤
* │ Pixels are divided into 32×32 tiles, stored in row-major order. │
* │ The last column / row of tiles may be smaller (no padding). │
* │ │
* │ Encoding pipeline (encoder): │
* │ a) RGB→YCoCg-R (skipped for gray/gray-alpha channels) │
* │ b) 2-D prediction residuals (see below) │
* │ c) Determine per-channel bit depth from max |residual| in tile │
* │ d) Write tile header + packed residuals │
* │ │
* │ Tile layout: │
* │ [bit_depth per channel] 1 byte each, channels bytes total │
* │ [first pixel, raw] 1 byte per channel │
* │ [packed residuals] bit_depth[c] bits per sample, │
* │ stored MSB-first, zero-padded to the │
* │ next byte boundary at the end of tile │
* │ │
* │ Prediction (lossless 2-D): │
* │ pixel (0,0) in tile → stored raw, no residual │
* │ pixel (x,0) → predict = left │
* │ pixel (0,y) → predict = above │
* │ pixel (x,y) → predict = left + above − diag │
* │ residual = (actual − predict) wrapped mod 256, stored signed │
* │ │
* │ Bit packing: │
* │ bits_needed = bit_depth[c] (0 means all residuals are 0) │
* │ Each residual is stored as a (bits_needed)-bit two's-complement │
* │ signed integer, MSB first, packed across byte boundaries. │
* │ bit_depth 1 → only the sign bit (value is −1 or 0) │
* │ bit_depth 0 → no bits written; all samples in channel = 0 │
* │ │
* │ YCoCg-R forward transform (applied before prediction): │
* │ Co = R − B │
* │ t = B + (Co >> 1) │
* │ Cg = G − t │
* │ Y = t + (Cg >> 1) │
* │ Stored channel order: Y, Co, Cg (replaces R, G, B) │
* │ Alpha is left unchanged. │
* │ │
* │ YCoCg-R inverse transform (applied after prediction): │
* │ t = Y − (Cg >> 1) │
* │ G = Cg + t │
* │ B = t − (Co >> 1) │
* │ R = B + Co │
* └─────────────────────────────────────────────────────────────────────┘
*
* GPU FORMAT HINTS (YAPF_GPU_* values for the gpu_format field)
* ──────────────────────────────────────────────────────────────
* 0x00 RGBA8 GL_RGBA8 / VK_FORMAT_R8G8B8A8_UNORM
* 0x01 RGB8 GL_RGB8 / VK_FORMAT_R8G8B8_UNORM
* 0x02 RG8 GL_RG8 / VK_FORMAT_R8G8_UNORM
* 0x03 R8 GL_R8 / VK_FORMAT_R8_UNORM
* 0x04 SRGB8_A8 GL_SRGB8_ALPHA8
* 0x05 SRGB8 GL_SRGB8
*
* FLAGS (YAPF_FLAG_* bitmask for the flags field)
* ─────────────────────────────────────────────────
* bit 0 PREMULT_ALPHA alpha is premultiplied into RGB
* bit 1 SRGB pixels are in sRGB gamma (informational only)
*/
#ifndef YAPF_H
#define YAPF_H
#include <stdint.h>
#include <stddef.h>
/* ── GPU format hints ────────────────────────────────────────────────── */
#define YAPF_GPU_RGBA8 0x00
#define YAPF_GPU_RGB8 0x01
#define YAPF_GPU_RG8 0x02
#define YAPF_GPU_R8 0x03
#define YAPF_GPU_SRGB8_A8 0x04
#define YAPF_GPU_SRGB8 0x05
/* ── Flags ───────────────────────────────────────────────────────────── */
#define YAPF_FLAG_PREMULT_ALPHA 0x01
#define YAPF_FLAG_SRGB 0x02
/* ── Channel constants ───────────────────────────────────────────────── */
#define YAPF_CHANNELS_GRAY 1
#define YAPF_CHANNELS_GRAY_ALPHA 2
#define YAPF_CHANNELS_RGB 3
#define YAPF_CHANNELS_RGBA 4
/* ── Return codes ────────────────────────────────────────────────────── */
#define YAPF_OK 0
#define YAPF_ERR_INVALID -1
#define YAPF_ERR_IO -2
#define YAPF_ERR_OOM -3
#define YAPF_ERR_CORRUPT -4
/* ── Limits ──────────────────────────────────────────────────────────── */
#define YAPF_MAX_DIM 65535u
#define YAPF_MAX_MIPS 16u
#define YAPF_TILE_SIZE 32u
/* ── Image descriptor ────────────────────────────────────────────────── */
typedef struct {
uint32_t width;
uint32_t height;
uint8_t channels; /* 1–4 */
uint8_t gpu_format; /* YAPF_GPU_* */
uint8_t flags; /* YAPF_FLAG_* bitmask */
uint8_t mip_levels; /* number of mip levels stored (1 = base only) */
uint8_t *pixels; /* base-level pixels, malloc-owned */
uint8_t **mips; /* mips[0]=base alias, mips[1..mip_levels-1] owned */
} yapf_image_t;
/* ── Public API ──────────────────────────────────────────────────────── */
#ifdef __cplusplus
extern "C" {
#endif
/*
* yapf_load — decode a .yapf file.
* Returns NULL on any failure (bad magic, truncated data, OOM, I/O error).
* The returned image must be released with yapf_free().
*/
yapf_image_t *yapf_load(const char *filename);
/*
* yapf_save — encode an image to disk.
* img->mip_levels controls how many levels are written; set to 1 to write
* only the base level. img->mips may be NULL when mip_levels == 1.
* Returns YAPF_OK or a negative YAPF_ERR_* code.
*/
int yapf_save(const char *filename, const yapf_image_t *img);
/*
* yapf_free — release all memory owned by an image returned by yapf_load().
* NULL-safe.
*/
void yapf_free(yapf_image_t *img);
#ifdef __cplusplus
}
#endif
#endif /* YAPF_H */