mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: add QOI codec, format encoders (BMP/ICO/JP2/QOI), fix JXL output bug
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
const QOI_MAGIC = 0x716f6966; // "qoif"
|
||||
const QOI_OP_INDEX = 0x00;
|
||||
const QOI_OP_DIFF = 0x40;
|
||||
const QOI_OP_LUMA = 0x80;
|
||||
const QOI_OP_RUN = 0xc0;
|
||||
const QOI_OP_RGB = 0xfe;
|
||||
const QOI_OP_RGBA = 0xff;
|
||||
const QOI_END_MARKER = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1]);
|
||||
|
||||
function hash(r: number, g: number, b: number, a: number): number {
|
||||
return (r * 3 + g * 5 + b * 7 + a * 11) % 64;
|
||||
}
|
||||
|
||||
export interface QoiHeader {
|
||||
width: number;
|
||||
height: number;
|
||||
channels: 3 | 4;
|
||||
colorspace: 0 | 1;
|
||||
}
|
||||
|
||||
export function qoiDecode(data: Uint8Array): { header: QoiHeader; pixels: Uint8Array } {
|
||||
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
||||
if (view.getUint32(0) !== QOI_MAGIC) throw new Error("Not a QOI file");
|
||||
|
||||
const width = view.getUint32(4);
|
||||
const height = view.getUint32(8);
|
||||
const channels = data[12] as 3 | 4;
|
||||
const colorspace = data[13] as 0 | 1;
|
||||
|
||||
if (width === 0 || height === 0) throw new Error("Invalid QOI dimensions");
|
||||
if (channels !== 3 && channels !== 4) throw new Error("Invalid QOI channels");
|
||||
|
||||
const totalPixels = width * height;
|
||||
const pixels = new Uint8Array(totalPixels * 4);
|
||||
const index = new Uint8Array(64 * 4);
|
||||
|
||||
let r = 0,
|
||||
g = 0,
|
||||
b = 0,
|
||||
a = 255;
|
||||
let pos = 14;
|
||||
let px = 0;
|
||||
|
||||
while (px < totalPixels) {
|
||||
const byte = data[pos++];
|
||||
|
||||
if (byte === QOI_OP_RGB) {
|
||||
r = data[pos++];
|
||||
g = data[pos++];
|
||||
b = data[pos++];
|
||||
} else if (byte === QOI_OP_RGBA) {
|
||||
r = data[pos++];
|
||||
g = data[pos++];
|
||||
b = data[pos++];
|
||||
a = data[pos++];
|
||||
} else {
|
||||
const op = byte & 0xc0;
|
||||
if (op === QOI_OP_INDEX) {
|
||||
const idx = (byte & 0x3f) * 4;
|
||||
r = index[idx];
|
||||
g = index[idx + 1];
|
||||
b = index[idx + 2];
|
||||
a = index[idx + 3];
|
||||
} else if (op === QOI_OP_DIFF) {
|
||||
r = (r + ((byte >> 4) & 0x03) - 2) & 0xff;
|
||||
g = (g + ((byte >> 2) & 0x03) - 2) & 0xff;
|
||||
b = (b + (byte & 0x03) - 2) & 0xff;
|
||||
} else if (op === QOI_OP_LUMA) {
|
||||
const b2 = data[pos++];
|
||||
const dg = (byte & 0x3f) - 32;
|
||||
r = (r + dg + ((b2 >> 4) & 0x0f) - 8) & 0xff;
|
||||
g = (g + dg) & 0xff;
|
||||
b = (b + dg + (b2 & 0x0f) - 8) & 0xff;
|
||||
} else {
|
||||
// QOI_OP_RUN
|
||||
let run = (byte & 0x3f) + 1;
|
||||
while (run-- > 0 && px < totalPixels) {
|
||||
const off = px * 4;
|
||||
pixels[off] = r;
|
||||
pixels[off + 1] = g;
|
||||
pixels[off + 2] = b;
|
||||
pixels[off + 3] = a;
|
||||
px++;
|
||||
}
|
||||
const h = hash(r, g, b, a) * 4;
|
||||
index[h] = r;
|
||||
index[h + 1] = g;
|
||||
index[h + 2] = b;
|
||||
index[h + 3] = a;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const h = hash(r, g, b, a) * 4;
|
||||
index[h] = r;
|
||||
index[h + 1] = g;
|
||||
index[h + 2] = b;
|
||||
index[h + 3] = a;
|
||||
const off = px * 4;
|
||||
pixels[off] = r;
|
||||
pixels[off + 1] = g;
|
||||
pixels[off + 2] = b;
|
||||
pixels[off + 3] = a;
|
||||
px++;
|
||||
}
|
||||
|
||||
return { header: { width, height, channels, colorspace }, pixels };
|
||||
}
|
||||
|
||||
export function qoiEncode(
|
||||
pixels: Uint8Array,
|
||||
width: number,
|
||||
height: number,
|
||||
channels: 3 | 4 = 4,
|
||||
): Uint8Array {
|
||||
const maxSize = 14 + width * height * (channels + 1) + QOI_END_MARKER.length;
|
||||
const out = new Uint8Array(maxSize);
|
||||
const view = new DataView(out.buffer);
|
||||
|
||||
view.setUint32(0, QOI_MAGIC);
|
||||
view.setUint32(4, width);
|
||||
view.setUint32(8, height);
|
||||
out[12] = channels;
|
||||
out[13] = 0; // sRGB
|
||||
|
||||
const index = new Uint8Array(64 * 4);
|
||||
let pos = 14;
|
||||
let prevR = 0,
|
||||
prevG = 0,
|
||||
prevB = 0,
|
||||
prevA = 255;
|
||||
let run = 0;
|
||||
const totalPixels = width * height;
|
||||
|
||||
for (let px = 0; px < totalPixels; px++) {
|
||||
const off = px * channels;
|
||||
const r = pixels[off];
|
||||
const g = pixels[off + 1];
|
||||
const b = pixels[off + 2];
|
||||
const a = channels === 4 ? pixels[off + 3] : 255;
|
||||
|
||||
if (r === prevR && g === prevG && b === prevB && a === prevA) {
|
||||
run++;
|
||||
if (run === 62 || px === totalPixels - 1) {
|
||||
out[pos++] = QOI_OP_RUN | (run - 1);
|
||||
run = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (run > 0) {
|
||||
out[pos++] = QOI_OP_RUN | (run - 1);
|
||||
run = 0;
|
||||
}
|
||||
|
||||
const h = hash(r, g, b, a);
|
||||
const idx = h * 4;
|
||||
|
||||
if (index[idx] === r && index[idx + 1] === g && index[idx + 2] === b && index[idx + 3] === a) {
|
||||
out[pos++] = QOI_OP_INDEX | h;
|
||||
} else {
|
||||
index[idx] = r;
|
||||
index[idx + 1] = g;
|
||||
index[idx + 2] = b;
|
||||
index[idx + 3] = a;
|
||||
|
||||
if (a !== prevA) {
|
||||
out[pos++] = QOI_OP_RGBA;
|
||||
out[pos++] = r;
|
||||
out[pos++] = g;
|
||||
out[pos++] = b;
|
||||
out[pos++] = a;
|
||||
} else {
|
||||
const dr = r - prevR;
|
||||
const dg = g - prevG;
|
||||
const db = b - prevB;
|
||||
const drDg = dr - dg;
|
||||
const dbDg = db - dg;
|
||||
|
||||
if (dr > -3 && dr < 2 && dg > -3 && dg < 2 && db > -3 && db < 2) {
|
||||
out[pos++] = QOI_OP_DIFF | ((dr + 2) << 4) | ((dg + 2) << 2) | (db + 2);
|
||||
} else if (dg > -33 && dg < 32 && drDg > -9 && drDg < 8 && dbDg > -9 && dbDg < 8) {
|
||||
out[pos++] = QOI_OP_LUMA | (dg + 32);
|
||||
out[pos++] = ((drDg + 8) << 4) | (dbDg + 8);
|
||||
} else {
|
||||
out[pos++] = QOI_OP_RGB;
|
||||
out[pos++] = r;
|
||||
out[pos++] = g;
|
||||
out[pos++] = b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prevR = r;
|
||||
prevG = g;
|
||||
prevB = b;
|
||||
prevA = a;
|
||||
}
|
||||
|
||||
out.set(QOI_END_MARKER, pos);
|
||||
pos += QOI_END_MARKER.length;
|
||||
return out.slice(0, pos);
|
||||
}
|
||||
Reference in New Issue
Block a user