Created
July 5, 2023 08:24
-
-
Save lifeart/8015568b3ccccbbddfefcd13a671454c to your computer and use it in GitHub Desktop.
QOI - The "Quite OK Image" format for fast, lossless image compression
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://raw.githubusercontent.com/phoboslab/qoi/master/qoi.h | |
class QoiRGBA { | |
constructor(r = 0, g = 0, b = 0, a = 255) { | |
this.r = r; | |
this.g = g; | |
this.b = b; | |
this.a = a; | |
} | |
} | |
const QOI_MAGIC = 0x716f6966; | |
const QOI_HEADER_SIZE = 14; | |
const QOI_PIXELS_MAX = 400000000; | |
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_MASK_2 = 0xc0; | |
function qoiColorHash(color) { | |
return (color.r * 3 + color.g * 5 + color.b * 7 + color.a * 11) % 64; | |
} | |
function qoiWrite32(arr, p, v) { | |
arr[p++] = (v >> 24) & 0xff; | |
arr[p++] = (v >> 16) & 0xff; | |
arr[p++] = (v >> 8) & 0xff; | |
arr[p++] = v & 0xff; | |
return p; | |
} | |
function qoiRead32(arr, p) { | |
const v = | |
(arr[p++] << 24) | (arr[p++] << 16) | (arr[p++] << 8) | arr[p++]; | |
return { value: v, position: p }; | |
} | |
function qoiEncode(data, desc) { | |
if ( | |
data === null || | |
desc === null || | |
desc.width === 0 || | |
desc.height === 0 || | |
desc.channels < 3 || | |
desc.channels > 4 || | |
desc.colorspace > 1 || | |
desc.height >= QOI_PIXELS_MAX / desc.width | |
) { | |
return null; | |
} | |
const max_size = | |
desc.width * desc.height * (desc.channels + 1) + | |
QOI_HEADER_SIZE + | |
8; | |
let p = 0; | |
const bytes = new Uint8Array(max_size); | |
p = qoiWrite32(bytes, p, QOI_MAGIC); | |
p = qoiWrite32(bytes, p, desc.width); | |
p = qoiWrite32(bytes, p, desc.height); | |
bytes[p++] = desc.channels; | |
bytes[p++] = desc.colorspace; | |
const pixels = new Uint8Array(data); | |
const index = Array(64).fill(new QoiRGBA()); | |
let run = 0; | |
let px_prev = new QoiRGBA(); | |
let px = new QoiRGBA(); | |
const px_len = desc.width * desc.height * desc.channels; | |
const px_end = px_len - desc.channels; | |
const channels = desc.channels; | |
for (let px_pos = 0; px_pos < px_len; px_pos += channels) { | |
px.r = pixels[px_pos + 0]; | |
px.g = pixels[px_pos + 1]; | |
px.b = pixels[px_pos + 2]; | |
if (channels === 4) { | |
px.a = pixels[px_pos + 3]; | |
} | |
if (px.r === px_prev.r && px.g === px_prev.g && px.b === px_prev.b && px.a === px_prev.a) { | |
run++; | |
if (run === 62 || px_pos === px_end) { | |
bytes[p++] = QOI_OP_RUN | (run - 1); | |
run = 0; | |
} | |
} else { | |
if (run > 0) { | |
bytes[p++] = QOI_OP_RUN | (run - 1); | |
run = 0; | |
} | |
const index_pos = qoiColorHash(px) % 64; | |
if ( | |
index[index_pos].r === px.r && | |
index[index_pos].g === px.g && | |
index[index_pos].b === px.b && | |
index[index_pos].a === px.a | |
) { | |
bytes[p++] = QOI_OP_INDEX | index_pos; | |
} else { | |
index[index_pos] = new QoiRGBA(px.r, px.g, px.b, px.a); | |
if (px.a === px_prev.a) { | |
const vr = px.r - px_prev.r; | |
const vg = px.g - px_prev.g; | |
const vb = px.b - px_prev.b; | |
const vg_r = vr - vg; | |
const vg_b = vb - vg; | |
if ( | |
vr > -3 && vr < 2 && | |
vg > -3 && vg < 2 && | |
vb > -3 && vb < 2 | |
) { | |
bytes[p++] = QOI_OP_DIFF | (vr + 2) << 4 | (vg + 2) << 2 | (vb + 2); | |
} else if ( | |
vg_r > -9 && vg_r < 8 && | |
vg > -33 && vg < 32 && | |
vg_b > -9 && vg_b < 8 | |
) { | |
bytes[p++] = QOI_OP_LUMA | (vg + 32); | |
bytes[p++] = (vg_r + 8) << 4 | (vg_b + 8); | |
} else { | |
bytes[p++] = QOI_OP_RGB; | |
bytes[p++] = px.r; | |
bytes[p++] = px.g; | |
bytes[p++] = px.b; | |
} | |
} else { | |
bytes[p++] = QOI_OP_RGBA; | |
bytes[p++] = px.r; | |
bytes[p++] = px.g; | |
bytes[p++] = px.b; | |
bytes[p++] = px.a; | |
} | |
} | |
} | |
px_prev = new QoiRGBA(px.r, px.g, px.b, px.a); | |
} | |
for (let i = 0; i < 8; i++) { | |
bytes[p++] = i === 7 ? 1 : 0; | |
} | |
return bytes.slice(0, p); | |
} | |
function qoiDecode(data, desc, channels) { | |
if ( | |
data === null || | |
desc === null || | |
(channels !== 0 && channels !== 3 && channels !== 4) || | |
data.length < QOI_HEADER_SIZE + 8 | |
) { | |
return null; | |
} | |
const bytes = new Uint8Array(data); | |
let p = 0; | |
const header_magic = qoiRead32(bytes, p); | |
p = header_magic.position; | |
const header_width = qoiRead32(bytes, p); | |
p = header_width.position; | |
const header_height = qoiRead32(bytes, p); | |
p = header_height.position; | |
const header_channels = bytes[p++]; | |
const header_colorspace = bytes[p++]; | |
if ( | |
header_width.value === 0 || | |
header_height.value === 0 || | |
header_channels < 3 || | |
header_channels > 4 || | |
header_colorspace > 1 || | |
header_magic.value !== QOI_MAGIC || | |
header_height.value >= QOI_PIXELS_MAX / header_width.value | |
) { | |
return null; | |
} | |
desc.width = header_width.value; | |
desc.height = header_height.value; | |
desc.channels = header_channels; | |
desc.colorspace = header_colorspace; | |
if (channels === 0) { | |
channels = desc.channels; | |
} | |
const px_len = desc.width * desc.height * channels; | |
const pixels = new Uint8Array(px_len); | |
const index = Array(64).fill(new QoiRGBA()); | |
let px = new QoiRGBA(); | |
let run = 0; | |
const chunks_len = data.length - 8; | |
for (let px_pos = 0; px_pos < px_len; px_pos += channels) { | |
if (run > 0) { | |
run--; | |
} else if (p < chunks_len) { | |
const b1 = bytes[p++]; | |
if (b1 === QOI_OP_RGB) { | |
px.r = bytes[p++]; | |
px.g = bytes[p++]; | |
px.b = bytes[p++]; | |
} else if (b1 === QOI_OP_RGBA) { | |
px.r = bytes[p++]; | |
px.g = bytes[p++]; | |
px.b = bytes[p++]; | |
px.a = bytes[p++]; | |
} else if ((b1 & QOI_MASK_2) === QOI_OP_INDEX) { | |
px = index[b1]; | |
} else if ((b1 & QOI_MASK_2) === QOI_OP_DIFF) { | |
px.r += ((b1 >> 4) & 0x03) - 2; | |
px.g += ((b1 >> 2) & 0x03) - 2; | |
px.b += (b1 & 0x03) - 2; | |
} else if ((b1 & QOI_MASK_2) === QOI_OP_LUMA) { | |
const b2 = bytes[p++]; | |
const vg = (b1 & 0x3f) - 32; | |
px.r += vg - 8 + ((b2 >> 4) & 0x0f); | |
px.g += vg; | |
px.b += vg - 8 + (b2 & 0x0f); | |
} else if ((b1 & QOI_MASK_2) === QOI_OP_RUN) { | |
run = b1 & 0x3f; | |
} | |
index[qoiColorHash(px) % 64] = new QoiRGBA(px.r, px.g, px.b, px.a); | |
} | |
pixels[px_pos + 0] = px.r; | |
pixels[px_pos + 1] = px.g; | |
pixels[px_pos + 2] = px.b; | |
if (channels === 4) { | |
pixels[px_pos + 3] = px.a; | |
} | |
} | |
return pixels; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment