Task 589: .QOI File Format
Task 589: .QOI File Format
1. List of all the properties of this file format intrinsic to its file system
Based on the QOI file format specification, the intrinsic properties (header fields defining the file structure) are:
- Magic bytes: A 4-byte string "qoif" identifying the file format.
- Width: Unsigned 32-bit integer (big-endian) representing the image width in pixels.
- Height: Unsigned 32-bit integer (big-endian) representing the image height in pixels.
- Channels: Unsigned 8-bit integer (3 for RGB or 4 for RGBA), serving as a hint for color channels.
- Colorspace: Unsigned 8-bit integer (0 for sRGB with linear alpha, 1 for all channels linear), advisory for interpretation.
These are followed by variable-length data chunks encoding the pixels and an 8-byte end marker (0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01).
2. Two direct download links for files of format .QOI
- https://raw.githubusercontent.com/phoboslab/qoi/master/testimages/dice.qoi
- https://raw.githubusercontent.com/phoboslab/qoi/master/testimages/kodim23.qoi
3. Ghost blog embedded HTML JavaScript for drag and drop .QOI file to dump properties
Here's a self-contained HTML snippet with embedded JavaScript that can be embedded in a Ghost blog post (or any HTML-supporting platform). It creates a drag-and-drop area; when a .QOI file is dropped, it reads the file and displays the properties on screen.
4. Python class for .QOI files
Here's a Python class that can open, decode, read, write .QOI files, and print the properties to console. It includes full decoder (to extract pixels as a list of [r, g, b, a] tuples) and encoder (to create .QOI from pixels).
import struct
import sys
class QOI:
def __init__(self, filename=None):
self.magic = b'qoif'
self.width = 0
self.height = 0
self.channels = 4
self.colorspace = 0
self.pixels = [] # List of [r, g, b, a] for decoded pixels
if filename:
self.load(filename)
def load(self, filename):
with open(filename, 'rb') as f:
data = f.read()
self._decode(data)
def _decode(self, data):
if len(data) < 14:
raise ValueError("Invalid QOI file: too short")
self.magic = data[0:4]
if self.magic != b'qoif':
raise ValueError("Invalid QOI magic bytes")
self.width, = struct.unpack('>I', data[4:8])
self.height, = struct.unpack('>I', data[8:12])
self.channels = data[12]
self.colorspace = data[13]
if self.channels not in (3, 4) or self.colorspace not in (0, 1):
raise ValueError("Invalid channels or colorspace")
p = 14
px_len = self.width * self.height * 4 # Always decode to RGBA
self.pixels = []
run = 0
index = [[0, 0, 0, 0] for _ in range(64)]
px = [0, 0, 0, 255]
while len(self.pixels) * 4 < px_len:
if run > 0:
run -= 1
else:
if p >= len(data) - 8:
raise ValueError("Unexpected end of data")
b1 = data[p]
p += 1
if b1 == 0xfe: # QOI_OP_RGB
px[0] = data[p]
px[1] = data[p+1]
px[2] = data[p+2]
p += 3
elif b1 == 0xff: # QOI_OP_RGBA
px[0] = data[p]
px[1] = data[p+1]
px[2] = data[p+2]
px[3] = data[p+3]
p += 4
elif (b1 & 0xc0) == 0x00: # QOI_OP_INDEX
px = index[b1].copy()
elif (b1 & 0xc0) == 0x40: # QOI_OP_DIFF
px[0] = (px[0] + ((b1 >> 4) & 0x03) - 2) % 256
px[1] = (px[1] + ((b1 >> 2) & 0x03) - 2) % 256
px[2] = (px[2] + (b1 & 0x03) - 2) % 256
elif (b1 & 0xc0) == 0x80: # QOI_OP_LUMA
b2 = data[p]
p += 1
dg = (b1 & 0x3f) - 32
px[0] = (px[0] + dg - 8 + ((b2 >> 4) & 0x0f)) % 256
px[1] = (px[1] + dg) % 256
px[2] = (px[2] + dg - 8 + (b2 & 0x0f)) % 256
elif (b1 & 0xc0) == 0xc0: # QOI_OP_RUN
run = (b1 & 0x3f) + 1 - 1 # bias -1, but +1 for loop
idx = (px[0] * 3 + px[1] * 5 + px[2] * 7 + px[3] * 11) % 64
index[idx] = px.copy()
self.pixels.extend(px)
# Check end marker
if data[p:p+8] != b'\x00\x00\x00\x00\x00\x00\x00\x01':
raise ValueError("Invalid QOI end marker")
def print_properties(self):
print("QOI File Properties:")
print(f"Magic bytes: {self.magic.decode('ascii')}")
print(f"Width: {self.width}")
print(f"Height: {self.height}")
print(f"Channels: {self.channels}")
print(f"Colorspace: {self.colorspace}")
def encode(self):
if not self.pixels:
raise ValueError("No pixels to encode")
data = bytearray()
data.extend(self.magic)
data.extend(struct.pack('>I', self.width))
data.extend(struct.pack('>I', self.height))
data.append(self.channels)
data.append(self.colorspace)
index = [[0, 0, 0, 0] for _ in range(64)]
px = [0, 0, 0, 255]
run = 0
i = 0
while i < len(self.pixels):
px_new = self.pixels[i:i+4]
i += 4
if px_new == px:
run += 1
if run == 62 or i == len(self.pixels):
data.append(0xc0 | (run - 1))
run = 0
continue
if run > 0:
data.append(0xc0 | (run - 1))
run = 0
idx = (px_new[0] * 3 + px_new[1] * 5 + px_new[2] * 7 + px_new[3] * 11) % 64
if index[idx] == px_new:
data.append(0x00 | idx)
else:
index[idx] = px_new.copy()
if px_new[3] == px[3]:
dr = px_new[0] - px[0]
dg = px_new[1] - px[1]
db = px_new[2] - px[2]
if dr in range(-2, 2) and dg in range(-2, 2) and db in range(-2, 2):
data.append(0x40 | ((dr + 2) << 4) | ((dg + 2) << 2) | (db + 2))
elif dg in range(-32, 32) and dr - dg in range(-8, 8) and db - dg in range(-8, 8):
data.append(0x80 | (dg + 32))
data.append(((dr - dg + 8) << 4) | (db - dg + 8))
else:
data.append(0xfe)
data.extend(px_new[0:3])
else:
data.append(0xff)
data.extend(px_new)
px = px_new.copy()
data.extend(b'\x00\x00\x00\x00\x00\x00\x00\x01')
return bytes(data)
def save(self, filename):
with open(filename, 'wb') as f:
f.write(self.encode())
# Example usage:
# q = QOI('example.qoi')
# q.print_properties()
# q.save('output.qoi')
5. Java class for .QOI files
Here's a Java class that can open, decode, read, write .QOI files, and print the properties to console. It uses ByteBuffer for big-endian reading.
import java.io.*;
import java.nio.*;
import java.util.*;
public class QOI {
private String magic = "qoif";
private int width = 0;
private int height = 0;
private byte channels = 4;
private byte colorspace = 0;
private List<int[]> pixels = new ArrayList<>(); // List of int[4] {r, g, b, a}
public QOI(String filename) throws IOException {
load(filename);
}
public QOI() {}
public void load(String filename) throws IOException {
byte[] data;
try (FileInputStream fis = new FileInputStream(filename)) {
data = fis.readAllBytes();
}
decode(data);
}
private void decode(byte[] data) throws IOException {
if (data.length < 14) {
throw new IOException("Invalid QOI file: too short");
}
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
byte[] magicBytes = new byte[4];
bb.get(magicBytes);
magic = new String(magicBytes);
if (!magic.equals("qoif")) {
throw new IOException("Invalid QOI magic bytes");
}
width = bb.getInt();
height = bb.getInt();
channels = bb.get();
colorspace = bb.get();
if (channels != 3 && channels != 4 || (colorspace != 0 && colorspace != 1)) {
throw new IOException("Invalid channels or colorspace");
}
int p = 14;
int pxLen = width * height;
pixels.clear();
int run = 0;
int[][] index = new int[64][4];
int[] px = {0, 0, 0, 255};
while (pixels.size() < pxLen) {
if (run > 0) {
run--;
} else {
if (p >= data.length - 8) {
throw new IOException("Unexpected end of data");
}
int b1 = data[p] & 0xff;
p++;
if (b1 == 0xfe) { // QOI_OP_RGB
px[0] = data[p] & 0xff;
px[1] = data[p+1] & 0xff;
px[2] = data[p+2] & 0xff;
p += 3;
} else if (b1 == 0xff) { // QOI_OP_RGBA
px[0] = data[p] & 0xff;
px[1] = data[p+1] & 0xff;
px[2] = data[p+2] & 0xff;
px[3] = data[p+3] & 0xff;
p += 4;
} else if ((b1 & 0xc0) == 0x00) { // QOI_OP_INDEX
px = index[b1].clone();
} else if ((b1 & 0xc0) == 0x40) { // QOI_OP_DIFF
px[0] = (px[0] + ((b1 >> 4) & 0x03) - 2) & 0xff;
px[1] = (px[1] + ((b1 >> 2) & 0x03) - 2) & 0xff;
px[2] = (px[2] + (b1 & 0x03) - 2) & 0xff;
} else if ((b1 & 0xc0) == 0x80) { // QOI_OP_LUMA
int b2 = data[p] & 0xff;
p++;
int dg = (b1 & 0x3f) - 32;
px[0] = (px[0] + dg - 8 + ((b2 >> 4) & 0x0f)) & 0xff;
px[1] = (px[1] + dg) & 0xff;
px[2] = (px[2] + dg - 8 + (b2 & 0x0f)) & 0xff;
} else if ((b1 & 0xc0) == 0xc0) { // QOI_OP_RUN
run = (b1 & 0x3f) + 1 - 1; // for loop
}
int idx = (px[0] * 3 + px[1] * 5 + px[2] * 7 + px[3] * 11) % 64;
index[idx] = px.clone();
}
pixels.add(px.clone());
}
// Check end marker
boolean endMatch = true;
for (int i = 0; i < 7; i++) {
if (data[p + i] != 0) endMatch = false;
}
if (data[p + 7] != 1 || !endMatch) {
throw new IOException("Invalid QOI end marker");
}
}
public void printProperties() {
System.out.println("QOI File Properties:");
System.out.println("Magic bytes: " + magic);
System.out.println("Width: " + width);
System.out.println("Height: " + height);
System.out.println("Channels: " + channels);
System.out.println("Colorspace: " + colorspace);
}
public byte[] encode() {
if (pixels.isEmpty()) {
throw new IllegalStateException("No pixels to encode");
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
baos.write(magic.getBytes());
ByteBuffer bb = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN);
bb.putInt(width);
bb.putInt(height);
baos.write(bb.array());
baos.write(channels);
baos.write(colorspace);
int[][] index = new int[64][4];
int[] px = {0, 0, 0, 255};
int run = 0;
for (int[] pxNew : pixels) {
if (Arrays.equals(pxNew, px)) {
run++;
if (run == 62 || pixels.indexOf(pxNew) == pixels.size() - 1) {
baos.write(0xc0 | (run - 1));
run = 0;
}
continue;
}
if (run > 0) {
baos.write(0xc0 | (run - 1));
run = 0;
}
int idx = (pxNew[0] * 3 + pxNew[1] * 5 + pxNew[2] * 7 + pxNew[3] * 11) % 64;
if (Arrays.equals(index[idx], pxNew)) {
baos.write(0x00 | idx);
} else {
index[idx] = pxNew.clone();
if (pxNew[3] == px[3]) {
int dr = pxNew[0] - px[0];
int dg = pxNew[1] - px[1];
int db = pxNew[2] - px[2];
if (dr >= -2 && dr <= 1 && dg >= -2 && dg <= 1 && db >= -2 && db <= 1) {
baos.write(0x40 | ((dr + 2) << 4) | ((dg + 2) << 2) | (db + 2));
} else if (dg >= -32 && dg <= 31 && dr - dg >= -8 && dr - dg <= 7 && db - dg >= -8 && db - dg <= 7) {
baos.write(0x80 | (dg + 32));
baos.write(((dr - dg + 8) << 4) | (db - dg + 8));
} else {
baos.write(0xfe);
baos.write(pxNew[0]);
baos.write(pxNew[1]);
baos.write(pxNew[2]);
}
} else {
baos.write(0xff);
baos.write(pxNew[0]);
baos.write(pxNew[1]);
baos.write(pxNew[2]);
baos.write(pxNew[3]);
}
}
px = pxNew.clone();
}
baos.write(new byte[]{0,0,0,0,0,0,0,1});
} catch (IOException e) {
// Won't happen with BAOS
}
return baos.toByteArray();
}
public void save(String filename) throws IOException {
try (FileOutputStream fos = new FileOutputStream(filename)) {
fos.write(encode());
}
}
// Example usage:
// public static void main(String[] args) throws IOException {
// QOI q = new QOI("example.qoi");
// q.printProperties();
// q.save("output.qoi");
// }
}
6. JavaScript class for .QOI files
Here's a JavaScript class (ES6) that can open (via async FileReader or buffer), decode, read, write .QOI files, and print the properties to console. For node.js, use fs; here it's browser-friendly with async.
class QOI {
constructor() {
this.magic = 'qoif';
this.width = 0;
this.height = 0;
this.channels = 4;
this.colorspace = 0;
this.pixels = []; // Array of [r, g, b, a]
}
async loadFromFile(file) {
const buffer = await file.arrayBuffer();
this.decode(buffer);
}
decode(buffer) {
const data = new Uint8Array(buffer);
if (data.length < 14) {
throw new Error('Invalid QOI file: too short');
}
const view = new DataView(buffer);
this.magic = new TextDecoder().decode(data.subarray(0, 4));
if (this.magic !== 'qoif') {
throw new Error('Invalid QOI magic bytes');
}
this.width = view.getUint32(4, false);
this.height = view.getUint32(8, false);
this.channels = data[12];
this.colorspace = data[13];
if (![3, 4].includes(this.channels) || ![0, 1].includes(this.colorspace)) {
throw new Error('Invalid channels or colorspace');
}
let p = 14;
const pxLen = this.width * this.height;
this.pixels = [];
let run = 0;
const index = Array.from({length: 64}, () => [0, 0, 0, 0]);
let px = [0, 0, 0, 255];
while (this.pixels.length < pxLen) {
if (run > 0) {
run--;
} else {
if (p >= data.length - 8) {
throw new Error('Unexpected end of data');
}
const b1 = data[p];
p++;
if (b1 === 0xfe) { // QOI_OP_RGB
px[0] = data[p];
px[1] = data[p+1];
px[2] = data[p+2];
p += 3;
} else if (b1 === 0xff) { // QOI_OP_RGBA
px[0] = data[p];
px[1] = data[p+1];
px[2] = data[p+2];
px[3] = data[p+3];
p += 4;
} else if ((b1 & 0xc0) === 0x00) { // QOI_OP_INDEX
px = [...index[b1]];
} else if ((b1 & 0xc0) === 0x40) { // QOI_OP_DIFF
px[0] = (px[0] + ((b1 >> 4) & 0x03) - 2) & 0xff;
px[1] = (px[1] + ((b1 >> 2) & 0x03) - 2) & 0xff;
px[2] = (px[2] + (b1 & 0x03) - 2) & 0xff;
} else if ((b1 & 0xc0) === 0x80) { // QOI_OP_LUMA
const b2 = data[p];
p++;
const dg = (b1 & 0x3f) - 32;
px[0] = (px[0] + dg - 8 + ((b2 >> 4) & 0x0f)) & 0xff;
px[1] = (px[1] + dg) & 0xff;
px[2] = (px[2] + dg - 8 + (b2 & 0x0f)) & 0xff;
} else if ((b1 & 0xc0) === 0xc0) { // QOI_OP_RUN
run = (b1 & 0x3f) + 1 - 1;
}
const idx = (px[0] * 3 + px[1] * 5 + px[2] * 7 + px[3] * 11) % 64;
index[idx] = [...px];
}
this.pixels.push([...px]);
}
// Check end marker
const end = data.subarray(p, p+8);
if (![...end].every((v, i) => i < 7 ? v === 0 : v === 1)) {
throw new Error('Invalid QOI end marker');
}
}
printProperties() {
console.log('QOI File Properties:');
console.log(`Magic bytes: ${this.magic}`);
console.log(`Width: ${this.width}`);
console.log(`Height: ${this.height}`);
console.log(`Channels: ${this.channels}`);
console.log(`Colorspace: ${this.colorspace}`);
}
encode() {
if (this.pixels.length === 0) {
throw new Error('No pixels to encode');
}
const data = new Uint8Array(14 + this.pixels.length * 5 + 8); // Upper bound
let p = 0;
const encoder = new TextEncoder();
const magicBytes = encoder.encode(this.magic);
data.set(magicBytes, p); p += 4;
new DataView(data.buffer).setUint32(p, this.width, false); p += 4;
new DataView(data.buffer).setUint32(p, this.height, false); p += 4;
data[p++] = this.channels;
data[p++] = this.colorspace;
const index = Array.from({length: 64}, () => [0, 0, 0, 0]);
let px = [0, 0, 0, 255];
let run = 0;
for (let i = 0; i < this.pixels.length; i++) {
const pxNew = this.pixels[i];
if (pxNew.every((v, j) => v === px[j])) {
run++;
if (run === 62 || i === this.pixels.length - 1) {
data[p++] = 0xc0 | (run - 1);
run = 0;
}
continue;
}
if (run > 0) {
data[p++] = 0xc0 | (run - 1);
run = 0;
}
const idx = (pxNew[0] * 3 + pxNew[1] * 5 + pxNew[2] * 7 + pxNew[3] * 11) % 64;
if (index[idx].every((v, j) => v === pxNew[j])) {
data[p++] = 0x00 | idx;
} else {
index[idx] = [...pxNew];
if (pxNew[3] === px[3]) {
const dr = pxNew[0] - px[0];
const dg = pxNew[1] - px[1];
const db = pxNew[2] - px[2];
if (dr >= -2 && dr <= 1 && dg >= -2 && dg <= 1 && db >= -2 && db <= 1) {
data[p++] = 0x40 | ((dr + 2) << 4) | ((dg + 2) << 2) | (db + 2);
} else if (dg >= -32 && dg <= 31 && dr - dg >= -8 && dr - dg <= 7 && db - dg >= -8 && db - dg <= 7) {
data[p++] = 0x80 | (dg + 32);
data[p++] = ((dr - dg + 8) << 4) | (db - dg + 8);
} else {
data[p++] = 0xfe;
data[p++] = pxNew[0];
data[p++] = pxNew[1];
data[p++] = pxNew[2];
}
} else {
data[p++] = 0xff;
data[p++] = pxNew[0];
data[p++] = pxNew[1];
data[p++] = pxNew[2];
data[p++] = pxNew[3];
}
}
px = [...pxNew];
}
data[p++] = 0; data[p++] = 0; data[p++] = 0; data[p++] = 0;
data[p++] = 0; data[p++] = 0; data[p++] = 0; data[p++] = 1;
return data.subarray(0, p).buffer;
}
// Example usage (browser):
// const q = new QOI();
// const input = document.querySelector('input[type="file"]');
// input.addEventListener('change', async (e) => {
// await q.loadFromFile(e.target.files[0]);
// q.printProperties();
// const encoded = q.encode();
// // Use encoded ArrayBuffer, e.g., download it
// });
}
7. C "class" for .QOI files
In C, we use a struct with functions (no classes). Here's a complete C implementation that can open, decode, read, write .QOI files, and print properties to stdout. Compile with gcc -o qoi qoi.c.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#define QOI_MAGIC 'q'<<24 | 'o'<<16 | 'i'<<8 | 'f'
typedef struct {
uint32_t magic;
uint32_t width;
uint32_t height;
uint8_t channels;
uint8_t colorspace;
uint8_t *pixels; // RGBA bytes, width * height * 4
} QOI_t;
QOI_t* qoi_create() {
QOI_t* q = (QOI_t*)malloc(sizeof(QOI_t));
if (!q) return NULL;
memset(q, 0, sizeof(QOI_t));
q->magic = QOI_MAGIC;
q->channels = 4;
q->colorspace = 0;
return q;
}
void qoi_destroy(QOI_t* q) {
if (q->pixels) free(q->pixels);
free(q);
}
int qoi_load(QOI_t* q, const char* filename) {
FILE* f = fopen(filename, "rb");
if (!f) return -1;
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t* data = (uint8_t*)malloc(size);
fread(data, 1, size, f);
fclose(f);
int ret = qoi_decode(q, data, size);
free(data);
return ret;
}
int qoi_decode(QOI_t* q, const uint8_t* data, int size) {
if (size < 14) return -1;
q->magic = data[0]<<24 | data[1]<<16 | data[2]<<8 | data[3];
if (q->magic != QOI_MAGIC) return -1;
q->width = data[4]<<24 | data[5]<<16 | data[6]<<8 | data[7];
q->height = data[8]<<24 | data[9]<<16 | data[10]<<8 | data[11];
q->channels = data[12];
q->colorspace = data[13];
if (q->channels != 3 && q->channels != 4 || (q->colorspace != 0 && q->colorspace != 1)) return -1;
int p = 14;
int px_len = q->width * q->height * 4;
q->pixels = (uint8_t*)malloc(px_len);
if (!q->pixels) return -1;
int run = 0;
uint8_t index[64][4] = {0};
uint8_t px[4] = {0, 0, 0, 255};
int px_pos = 0;
while (px_pos < px_len) {
if (run > 0) {
run--;
} else {
if (p >= size - 8) {
free(q->pixels);
return -1;
}
uint8_t b1 = data[p++];
if (b1 == 0xfe) { // QOI_OP_RGB
px[0] = data[p++];
px[1] = data[p++];
px[2] = data[p++];
} else if (b1 == 0xff) { // QOI_OP_RGBA
px[0] = data[p++];
px[1] = data[p++];
px[2] = data[p++];
px[3] = data[p++];
} else if ((b1 & 0xc0) == 0x00) { // QOI_OP_INDEX
memcpy(px, index[b1], 4);
} else if ((b1 & 0xc0) == 0x40) { // QOI_OP_DIFF
px[0] = (px[0] + ((b1 >> 4) & 0x03) - 2) & 0xff;
px[1] = (px[1] + ((b1 >> 2) & 0x03) - 2) & 0xff;
px[2] = (px[2] + (b1 & 0x03) - 2) & 0xff;
} else if ((b1 & 0xc0) == 0x80) { // QOI_OP_LUMA
uint8_t b2 = data[p++];
int dg = (b1 & 0x3f) - 32;
px[0] = (px[0] + dg - 8 + ((b2 >> 4) & 0x0f)) & 0xff;
px[1] = (px[1] + dg) & 0xff;
px[2] = (px[2] + dg - 8 + (b2 & 0x0f)) & 0xff;
} else if ((b1 & 0xc0) == 0xc0) { // QOI_OP_RUN
run = (b1 & 0x3f) + 1 - 1;
}
int idx = (px[0] * 3 + px[1] * 5 + px[2] * 7 + px[3] * 11) % 64;
memcpy(index[idx], px, 4);
}
memcpy(q->pixels + px_pos, px, 4);
px_pos += 4;
}
// Check end marker
if (memcmp(data + p, "\x00\x00\x00\x00\x00\x00\x00\x01", 8) != 0) {
free(q->pixels);
return -1;
}
return 0;
}
void qoi_print_properties(const QOI_t* q) {
printf("QOI File Properties:\n");
printf("Magic bytes: qoif\n");
printf("Width: %u\n", q->width);
printf("Height: %u\n", q->height);
printf("Channels: %u\n", q->channels);
printf("Colorspace: %u\n", q->colorspace);
}
int qoi_encode(const QOI_t* q, uint8_t** out_data, int* out_size) {
if (!q->pixels) return -1;
int max_size = q->width * q->height * (q->channels + 1) + 14 + 8;
uint8_t* data = (uint8_t*)malloc(max_size);
if (!data) return -1;
int p = 0;
data[p++] = 'q'; data[p++] = 'o'; data[p++] = 'i'; data[p++] = 'f';
data[p++] = (q->width >> 24) & 0xff; data[p++] = (q->width >> 16) & 0xff;
data[p++] = (q->width >> 8) & 0xff; data[p++] = q->width & 0xff;
data[p++] = (q->height >> 24) & 0xff; data[p++] = (q->height >> 16) & 0xff;
data[p++] = (q->height >> 8) & 0xff; data[p++] = q->height & 0xff;
data[p++] = q->channels;
data[p++] = q->colorspace;
uint8_t index[64][4] = {0};
uint8_t px[4] = {0, 0, 0, 255};
int run = 0;
int px_len = q->width * q->height * 4;
for (int px_pos = 0; px_pos < px_len; px_pos += 4) {
uint8_t px_new[4];
memcpy(px_new, q->pixels + px_pos, 4);
if (memcmp(px_new, px, 4) == 0) {
run++;
if (run == 62 || px_pos + 4 == px_len) {
data[p++] = 0xc0 | (run - 1);
run = 0;
}
continue;
}
if (run > 0) {
data[p++] = 0xc0 | (run - 1);
run = 0;
}
int idx = (px_new[0] * 3 + px_new[1] * 5 + px_new[2] * 7 + px_new[3] * 11) % 64;
if (memcmp(index[idx], px_new, 4) == 0) {
data[p++] = 0x00 | idx;
} else {
memcpy(index[idx], px_new, 4);
if (px_new[3] == px[3]) {
int dr = px_new[0] - px[0];
int dg = px_new[1] - px[1];
int db = px_new[2] - px[2];
if (dr >= -2 && dr <= 1 && dg >= -2 && dg <= 1 && db >= -2 && db <= 1) {
data[p++] = 0x40 | ((dr + 2) << 4) | ((dg + 2) << 2) | (db + 2);
} else if (dg >= -32 && dg <= 31 && dr - dg >= -8 && dr - dg <= 7 && db - dg >= -8 && db - dg <= 7) {
data[p++] = 0x80 | (dg + 32);
data[p++] = ((dr - dg + 8) << 4) | (db - dg + 8);
} else {
data[p++] = 0xfe;
data[p++] = px_new[0];
data[p++] = px_new[1];
data[p++] = px_new[2];
}
} else {
data[p++] = 0xff;
data[p++] = px_new[0];
data[p++] = px_new[1];
data[p++] = px_new[2];
data[p++] = px_new[3];
}
}
memcpy(px, px_new, 4);
}
memset(data + p, 0, 7);
data[p + 7] = 1;
p += 8;
*out_data = data;
*out_size = p;
return 0;
}
int qoi_save(const QOI_t* q, const char* filename) {
uint8_t* data;
int size;
if (qoi_encode(q, &data, &size) != 0) return -1;
FILE* f = fopen(filename, "wb");
if (!f) {
free(data);
return -1;
}
fwrite(data, 1, size, f);
fclose(f);
free(data);
return 0;
}
// Example usage:
// int main() {
// QOI_t* q = qoi_create();
// if (qoi_load(q, "example.qoi") == 0) {
// qoi_print_properties(q);
// qoi_save(q, "output.qoi");
// }
// qoi_destroy(q);
// return 0;
// }