function clamp(min, int, max) {
const t = int < min ? min : int; return t > max ? max : t;
}
let bins = new Array(2 ** 16);
class PnnQuant {
constructor(opts) {
this.opts = opts;
this.palette = [];
this.qPixels = [];
this.hasSemiTransparency = false;
this.m_transparentPixelIndex = -1;
this.m_transparentColor = 0xffffff;
}
pnnquan(pixels, nMaxColors) {
let e = 1;
bins.fill(undefined);
for (var i = 0; i < pixels.length; ++i) {
const c = pixels[i];
const r = c & 0xff;
const g = (c >> 8) & 0xff;
const b = (c >> 16) & 0xff;
const a = (c >> 24) & 0xff;
const index = getARGBIndex(a, r, g, b, this.hasSemiTransparency, nMaxColors < 64 || this.m_transparentPixelIndex >= 0);
const bin = undefined !== bins[index] ? bins[index] : bins[index] = nbin();
bin.rc += r;
bin.gc += g;
bin.bc += b;
bin.ac += a;
bin.cnt += 1.0;
}
let maxbins = 0;
for (var i = 0; i < 65536; ++i) {
if (bins[i] === undefined)
continue;
var d = 1.0 / bins[i].cnt;
bins[i].ac *= d;
bins[i].rc *= d;
bins[i].gc *= d;
bins[i].bc *= d;
bins[maxbins++] = bins[i];
}
if (nMaxColors < 16)
e = -1;
const weight = nMaxColors * 1.0 / maxbins;
if (weight > .003 && weight < .005)
e = 0;
if (weight < .025 && PG < 1) {
const delta = 3 * (.025 + weight);
PG -= delta;
PB += delta;
}
const quanFn = getQuanFn(nMaxColors, e);
let j = 0;
for (; j < maxbins - 1; ++j) {
bins[j].fw = j + 1;
bins[j + 1].bk = j;
bins[j].cnt = quanFn(bins[j].cnt);
}
bins[j].cnt = quanFn(bins[j].cnt);
let h;
let l;
let l2;
const heap = new Uint32Array(65537);
for (var i = 0; i < maxbins; ++i) {
find_nn(bins, i, PR, PG, PB);
var err = bins[i].err;
for (l = ++heap[0]; l > 1; l = l2) {
l2 = l >> 1;
if (bins[h = heap[l2]].err <= err)
break;
heap[l] = h;
}
heap[l] = i;
}
const extbins = maxbins - nMaxColors;
for (var i = 0; i < extbins;) {
let tb;
for (; ;) {
let b1 = heap[1];
tb = bins[b1];
if ((tb.tm >= tb.mtm) && (bins[tb.nn].mtm <= tb.tm))
break;
if (tb.mtm == 0xFFFF)
b1 = heap[1] = heap[heap[0]--];
else {
find_nn(bins, b1, PR, PG, PB);
tb.tm = i;
}
var err = bins[b1].err;
for (l = 1; (l2 = l + l) <= heap[0]; l = l2) {
if ((l2 < heap[0]) && (bins[heap[l2]].err > bins[heap[l2 + 1]].err))
++l2;
if (err <= bins[h = heap[l2]].err)
break;
heap[l] = h;
}
heap[l] = b1;
}
const nb = bins[tb.nn];
const n1 = tb.cnt;
const n2 = nb.cnt;
var d = Math.fround(1.0 / (n1 + n2));
tb.ac = d * Math.round(n1 * tb.ac + n2 * nb.ac);
tb.rc = d * Math.round(n1 * tb.rc + n2 * nb.rc);
tb.gc = d * Math.round(n1 * tb.gc + n2 * nb.gc);
tb.bc = d * Math.round(n1 * tb.bc + n2 * nb.bc);
tb.cnt += nb.cnt;
tb.mtm = ++i;
bins[nb.bk].fw = nb.fw;
bins[nb.fw].bk = nb.bk;
nb.mtm = 0xFFFF;
}
if (extbins < 0)
this.palette = new Uint32Array(maxbins);
let k = 0;
for (var i = 0; ; ++k) {
var a = Math.clamp(bins[i].ac, 0, 0xff) | 0;
var r = Math.clamp(bins[i].rc, 0, 0xff) | 0;
var g = Math.clamp(bins[i].gc, 0, 0xff) | 0;
var b = Math.clamp(bins[i].bc, 0, 0xff) | 0;
this.palette[k] = (a << 24) | (b << 16) | (g << 8) | r;
if (this.m_transparentPixelIndex >= 0 && a == 0) {
const temp = this.palette[0];
this.palette[0] = this.m_transparentColor;
this.palette[k] = temp;
}
if ((i = bins[i].fw) == 0)
break;
}
}
quantize_image(pixels, nMaxColors, width, height, dither) {
const qPixels = nMaxColors > 256 ? new Uint16Array(pixels.length) : new Uint8Array(pixels.length);
let pixelIndex = 0;
if (dither) {
const DJ = 4;
const BLOCK_SIZE = 256;
const DITHER_MAX = 20;
const err_len = (width + 2) * DJ;
const clamp = new Int32Array(DJ * BLOCK_SIZE);
const limtb = new Int32Array(2 * BLOCK_SIZE);
for (var i = 0; i < BLOCK_SIZE; ++i) {
clamp[i] = 0;
clamp[i + BLOCK_SIZE] = i;
clamp[i + BLOCK_SIZE * 2] = 0xff;
clamp[i + BLOCK_SIZE * 3] = 0xff;
limtb[i] = -DITHER_MAX;
limtb[i + BLOCK_SIZE] = DITHER_MAX;
}
for (var i = -DITHER_MAX; i <= DITHER_MAX; ++i)
limtb[i + BLOCK_SIZE] = i;
const noBias = this.hasSemiTransparency || nMaxColors < 64;
let dir = 1;
let row0 = new Int32Array(err_len);
let row1 = new Int32Array(err_len);
const lookup = new Int32Array(65536);
for (var i = 0; i < height; ++i) {
if (dir < 0)
pixelIndex += width - 1;
let cursor0 = DJ;
let cursor1 = width * DJ;
row1[cursor1] = row1[cursor1 + 1] = row1[cursor1 + 2] = row1[cursor1 + 3] = 0;
for (let j = 0; j < width; ++j) {
const r = (pixels[pixelIndex] & 0xff);
const g = (pixels[pixelIndex] >>> 8) & 0xff;
const b = (pixels[pixelIndex] >>> 16) & 0xff;
const a = (pixels[pixelIndex] >>> 24) & 0xff;
const ditherPixel = CalcDitherPixel(a, r, g, b, clamp, row0, cursor0, noBias);
let r_pix = ditherPixel[0];
let g_pix = ditherPixel[1];
let b_pix = ditherPixel[2];
let a_pix = ditherPixel[3];
const c1 = (a_pix << 24) | (b_pix << 16) | (g_pix << 8) | r_pix;
if (noBias) {
const offset = getARGBIndex(a_pix, r_pix, g_pix, b_pix, this.hasSemiTransparency, this.m_transparentPixelIndex >= 0);
if (lookup[offset] == 0)
lookup[offset] = (a == 0) ? 1 : nearestColorIndex(this.palette, c1, i + j) + 1;
qPixels[pixelIndex] = lookup[offset] - 1;
}
else
qPixels[pixelIndex] = (a == 0) ? 0 : nearestColorIndex(this.palette, c1, i + j);
const c2 = this.palette[qPixels[pixelIndex]];
const r2 = (c2 & 0xff);
const g2 = (c2 >>> 8) & 0xff;
const b2 = (c2 >>> 16) & 0xff;
const a2 = (c2 >>> 24) & 0xff;
r_pix = limtb[r_pix - r2 + BLOCK_SIZE];
g_pix = limtb[g_pix - g2 + BLOCK_SIZE];
b_pix = limtb[b_pix - b2 + BLOCK_SIZE];
a_pix = limtb[a_pix - a2 + BLOCK_SIZE];
let k = r_pix * 2;
row1[cursor1 - DJ] = r_pix;
row1[cursor1 + DJ] += (r_pix += k);
row1[cursor1] += (r_pix += k);
row0[cursor0 + DJ] += (r_pix + k);
k = g_pix * 2;
row1[cursor1 + 1 - DJ] = g_pix;
row1[cursor1 + 1 + DJ] += (g_pix += k);
row1[cursor1 + 1] += (g_pix += k);
row0[cursor0 + 1 + DJ] += (g_pix + k);
k = b_pix * 2;
row1[cursor1 + 2 - DJ] = b_pix;
row1[cursor1 + 2 + DJ] += (b_pix += k);
row1[cursor1 + 2] += (b_pix += k);
row0[cursor0 + 2 + DJ] += (b_pix + k);
k = a_pix * 2;
row1[cursor1 + 3 - DJ] = a_pix;
row1[cursor1 + 3 + DJ] += (a_pix += k);
row1[cursor1 + 3] += (a_pix += k);
row0[cursor0 + 3 + DJ] += (a_pix + k);
cursor0 += DJ;
cursor1 -= DJ;
pixelIndex += dir;
}
if ((i % 2) == 1)
pixelIndex += width + 1;
dir *= -1;
const temp = row0; row0 = row1; row1 = temp;
}
this.qPixels = qPixels;
return this.qPixels;
}
for (var i = 0; i < qPixels.length; ++i)
qPixels[i] = this.getDitherFn()(this.palette, pixels[i], i);
this.qPixels = qPixels;
return this.qPixels;
}
build_palette(buffer, { PR, PG, PB, fast, colors, transparency }) {
let rt = 1;
let len = buffer.length | 0;
const bins = new Array(2 ** 16);
for (let o = 0; o < bins.length; o++) bins[o] = nbin();
const index = fast
? (r, g, b, a) => (a & 0xF0) << 8 | (r & 0xF0) << 4 | (g & 0xF0) | (b >> 4)
: ((!transparency && 64 <= colors)
? (r, g, b, _) => (r & 0xf8) << 8 | (g & 0xfc) << 3 | (b >> 3)
: (r, g, b, a) => (a & 0x80) << 8 | (r & 0xf8) << 7 | (g & 0xf8) << 2 | (b >> 3));
for (let o = 0; o < len; o++) {
const c = buffer[o];
const r = c & 0xff;
const g = (c >> 8) & 0xff;
const b = (c >> 16) & 0xff;
const a = (c >> 24) & 0xff;
const bin = bins[index(r, g, b, a)];
bin.rc += r;
bin.gc += g;
bin.bc += b;
bin.ac += a;
bin.cnt += 1.0;
}
let size = 0;
let blen = bins.length | 0;
for (let o = 0; o < blen; o++) {
const bin = bins[o];
if (0.0 === bin.cnt) continue;
const d = 1.0 / bin.cnt;
bin.rc *= d;
bin.gc *= d;
bin.bc *= d;
bin.ac *= d;
bins[size++] = bin;
}
if (16 > colors) rt = -1;
const weight = colors / size;
if (0.003 < weight && 0.005 > weight) rt = 0;
if (1 > PG && 0.025 > weight) {
PG -= 3 * (0.025 + weight);
PB += 3 * (0.025 + weight);
}
const q = 0 === rt
? _ => _
: (0 > rt
? (_ => Math.cbrt(_) | 0)
: (64 <= colors
? (_ => Math.sqrt(_) | 0)
: (_ => Math.fround(Math.sqrt(_)))));
len = size - 1;
let offset = 0;
while (len > offset) {
const bin = bins[offset];
bin.fw = 1 + offset;
bin.cnt = q(bin.cnt);
bins[1 + offset].bk = offset++;
}
bins[offset].cnt = q(bins[offset].cnt);
const heap = new Uint32Array(1 + 2 ** 16);
for (let o = 0; o < size; o++) {
find_nn(bins, o, PR, PG, PB);
let l = 0;
const err = bins[o].err;
for (l = ++heap[0]; 1 < l; l >>= 1) {
const h = heap[l >> 1];
if (err >= bins[h].err) break;
heap[l] = h;
}
heap[l] = o;
}
offset = 0;
const ext = (size - colors) | 0;
while (ext > offset) {
let tb;
while (true) {
let b = heap[1]; tb = bins[b];
if ((tb.tm >= tb.mtm) && (tb.tm >= bins[tb.nn].mtm)) break;
if (0xffff === tb.mtm) b = heap[1] = heap[heap[0]--]; else (find_nn(bins, b, PR, PG, PB), tb.tm = offset);
let l = 0;
let l2 = 0;
const err = bins[b].err;
for (l = 1; heap[0] >= (l2 = l + l); l = l2) {
if ((l2 < heap[0]) && (bins[heap[l2]].err > bins[heap[1 + l2]].err)) l2++;
const h = heap[l2];
if (err <= bins[h].err) break;
heap[l] = h;
}
heap[l] = b;
}
const n1 = tb.cnt;
const nb = bins[tb.nn];
tb.cnt += nb.cnt;
tb.mtm = ++offset;
const n2 = nb.cnt;
const d = Math.fround(1.0 / (n1 + n2));
tb.rc = Math.round(n1 * tb.rc + n2 * nb.rc);
tb.gc = Math.round(n1 * tb.gc + n2 * nb.gc);
tb.bc = Math.round(n1 * tb.bc + n2 * nb.bc);
tb.ac = Math.round(n1 * tb.ac + n2 * nb.ac);
nb.mtm = 0xffff;
bins[nb.bk].fw = nb.fw;
bins[nb.fw].bk = nb.bk;
}
const palette = new Uint32Array(0 > ext ? size : colors);
offset = 0;
let woffset = 0;
while (true) {
const bin = bins[offset];
const r = clamp(0, bin.rc, 255) | 0;
const g = clamp(0, bin.gc, 255) | 0;
const b = clamp(0, bin.bc, 255) | 0;
const a = clamp(0, bin.ac, 255) | 0;
palette[woffset] = r | (g << 8) | (b << 16) | (a << 24);
if (a === 0 && transparency) (palette[woffset] = palette[0], palette[0] = 0);
woffset++;
if (0 === (offset = bin.fw)) break;
}
return palette;
}
quantize(width, height, buffer, {
fast = false,
colors = 256,
dither = true,
threshold = 0x42,
transparency = true,
force_transparency = false,
} = {}) {
let t = -1;
let PR = .2126; let PG = .7152; let PB = .0722; let PA = .3333; buffer = buffer.slice();
const len = buffer.length | 0;
colors = clamp(2, colors, 256);
const closest = Object.create(null);
const nearest = Object.create(null);
let palette;
transparency: {
if (!transparency) {
for (let o = 0; o < len; o++) {
const a = (buffer[o] >> 24) & 0xff;
buffer[o] = (a < threshold ? 0 : (buffer[o] & 0xffffff)) | (255 << 24);
}
} else {
for (let o = 0; o < len; o++) {
const a = (buffer[o] >> 24) & 0xff;
buffer[o] = a < threshold ? 0 : ((buffer[o] & 0xffffff) | (255 << 24));
}
t = buffer.indexOf(0);
}
}
if (32 >= colors) PR = PG = PB = PA = 1; else if (512 > width || 512 > height) (PR = .299, PG = .587, PB = .114);
if (2 < colors) {
palette = this.build_palette(buffer, {
PR,
PG,
PB,
fast,
colors,
transparency: transparency && (t !== 1 || force_transparency),
});
console.log(new Uint8Array(palette.buffer));
} else {
palette = new Uint32Array(2);
palette[0] = 0 | (255 << 24);
if (0 === (palette[1] = (t !== -1 || force_transparency) ? 0xffffff : 0xffffffff)) t = 1;
}
return {
palette,
index: [],
transparency: t,
};
}
quantizeImage() {
const pixels = this.opts.pixels.slice();
const width = this.opts.width;
const height = this.opts.height;
const nMaxColors = this.opts.colors;
const dither = this.opts.dithering;
const buffer = pixels;
if (this.opts.alphaThreshold)
alphaThreshold = this.opts.alphaThreshold;
closestMap = Object.create(null);
nearestMap = Object.create(null);
for (let i = 0; i < pixels.length; ++i) {
const a = (pixels[i] >> 24) & 0xff;
if (a < 0xE0) {
if (a == 0) {
this.m_transparentPixelIndex = i;
if (nMaxColors > 2)
this.m_transparentColor = pixels[i];
else
pixels[i] = this.m_transparentColor;
}
else if (a > alphaThreshold) {
this.hasSemiTransparency = true;
}
}
}
if (nMaxColors <= 32)
PR = PG = PB = PA = 1;
else if (width < 512 || height < 512) {
PR = 0.299; PG = 0.587; PB = 0.114;
}
hasSemiTransparency = this.hasSemiTransparency;
this.palette = new Uint32Array(nMaxColors);
if (nMaxColors > 2)
this.pnnquan(pixels, nMaxColors);
else {
if (this.m_transparentPixelIndex >= 0) {
this.palette[0] = this.m_transparentColor;
this.palette[1] = (0xff << 24);
}
else {
this.palette[0] = (0xff << 24);
this.palette[1] = 0xffffffff;
}
}
if (this.m_transparentPixelIndex >= 0) {
if (this.hasSemiTransparency)
this.opts.divisor = 1.25;
const k = this.getDitherFn()(this.palette, pixels[this.m_transparentPixelIndex], 0);
if (nMaxColors > 2)
this.palette[k] = this.m_transparentColor;
else if (this.palette[k] != this.m_transparentColor) {
const temp = this.palette[0];
this.palette[0] = this.palette[1];
this.palette[1] = temp;
}
}
this.qPixels = this.quantize_image(pixels, nMaxColors, width, height, dither);
}
getIndexedPixels() {
return this.qPixels;
}
getPalette() {
return this.palette.buffer;
}
getTransparentIndex() {
return this.m_transparentPixelIndex > -1 ? 0 : -1;
}
getDitherFn() {
return this.opts.dithering ? nearestColorIndex : closestColorIndex;
}
}
if (!Math.clamp) {
Math.clamp = function (a, b, c) {
return this.max(b, this.min(c, a));
};
}
let alphaThreshold = 0xF;
let hasSemiTransparency = false;
let PR = .2126;
let PG = .7152;
let PB = .0722;
let PA = .3333;
let closestMap = {};
let nearestMap = {};
function PnnBin() {
this.ac = this.rc = this.gc = this.bc = 0;
this.cnt = this.err = 0.0;
this.nn = this.fw = this.bk = this.tm = this.mtm = 0;
}
function nbin() {
return {
ac: 0.0, rc: 0.0,
fw: 0, bk: 0, tm: 0,
nn: 0, gc: 0.0, bc: 0.0,
mtm: 0, err: 0.0, cnt: 0.0,
};
}
function getARGBIndex(a, r, g, b, hasSemiTransparency, hasTransparency) {
if (hasSemiTransparency)
return (a & 0xF0) << 8 | (r & 0xF0) << 4 | (g & 0xF0) | (b >> 4);
if (hasTransparency)
return (a & 0x80) << 8 | (r & 0xF8) << 7 | (g & 0xF8) << 2 | (b >> 3);
return (r & 0xF8) << 8 | (g & 0xFC) << 3 | (b >> 3);
}
function sqr(value) {
return value * value;
}
function find_nn(bins, idx, PR, PG, PB) {
let nn = 0;
let err = 1e100;
const bin1 = bins[idx];
const n1 = bin1.cnt;
const wa = bin1.ac;
const wr = bin1.rc;
const wg = bin1.gc;
const wb = bin1.bc;
for (let i = bin1.fw; i != 0; i = bins[i].fw) {
const n2 = bins[i].cnt;
const nerr2 = (n1 * n2) / (n1 + n2);
if (nerr2 >= err)
continue;
let nerr = nerr2 * sqr(bins[i].ac - wa);
if (nerr >= err)
continue;
nerr += nerr2 * PR * sqr(bins[i].rc - wr);
if (nerr >= err)
continue;
nerr += nerr2 * PG * sqr(bins[i].gc - wg);
if (nerr >= err)
continue;
nerr += nerr2 * PB * sqr(bins[i].bc - wb);
if (nerr >= err)
continue;
err = nerr;
nn = i;
}
bin1.err = Math.fround(err);
bin1.nn = nn;
}
function getQuanFn(nMaxColors, quan_rt) {
if (quan_rt > 0) {
if (nMaxColors < 64)
return cnt => Math.fround(Math.sqrt(cnt));
return cnt => Math.sqrt(cnt) | 0;
}
if (quan_rt < 0)
return cnt => Math.cbrt(cnt) | 0;
return cnt => cnt;
}
function nearestColorIndex(palette, pixel, pos) {
const nearest = nearestMap[pixel];
if (nearest != null)
return nearest;
let k = 0;
const r = (pixel & 0xff);
const g = (pixel >>> 8) & 0xff;
const b = (pixel >>> 16) & 0xff;
const a = (pixel >>> 24) & 0xff;
if (a <= alphaThreshold)
return k;
let mindist = 1e100;
for (let i = 0; i < palette.length; i++) {
const r2 = (palette[i] & 0xff);
const g2 = (palette[i] >>> 8) & 0xff;
const b2 = (palette[i] >>> 16) & 0xff;
const a2 = (palette[i] >>> 24) & 0xff;
let curdist = PA * sqr(a2 - a);
if (curdist > mindist)
continue;
curdist += PR * sqr(r2 - r);
if (curdist > mindist)
continue;
curdist += PG * sqr(g2 - g);
if (curdist > mindist)
continue;
curdist += PB * sqr(b2 - b);
if (curdist > mindist)
continue;
mindist = curdist;
k = i;
}
nearestMap[pixel] = k;
return k;
}
function closestColorIndex(palette, pixel, pos) {
const r = (pixel & 0xff);
const g = (pixel >>> 8) & 0xff;
const b = (pixel >>> 16) & 0xff;
const a = (pixel >>> 24) & 0xff;
if (a <= alphaThreshold)
return 0;
let closest = closestMap[pixel];
if (!closest) {
closest = new Array(4);
closest[2] = closest[3] = 0xFFFF;
for (let k = 0; k < palette.length; ++k) {
const r2 = (palette[k] & 0xff);
const g2 = (palette[k] >>> 8) & 0xff;
const b2 = (palette[k] >>> 16) & 0xff;
const a2 = (palette[k] >>> 24) & 0xff;
let err = PR * sqr(r2 - r) + PG * sqr(g2 - g) + PB * sqr(b2 - b);
if (hasSemiTransparency)
err += PA * sqr(a2 - a);
if (err < closest[2]) {
closest[1] = closest[0];
closest[3] = closest[2];
closest[0] = k;
closest[2] = err;
}
else if (err < closest[3]) {
closest[1] = k;
closest[3] = err;
}
}
if (closest[3] == 0xFFFF)
closest[1] = closest[0];
closestMap[pixel] = closest;
}
const MAX_ERR = palette.length;
let idx = (pos + 1) % 2;
if (closest[3] * .67 < (closest[3] - closest[2]))
idx = 0;
else if (closest[0] > closest[1])
idx = pos % 2;
if (closest[idx + 2] >= MAX_ERR)
return nearestColorIndex(palette, pixel, pos);
return closest[idx];
}
function CalcDitherPixel(a, r, g, b, clamp, rowerr, cursor, noBias) {
const ditherPixel = new Int32Array(4);
if (noBias) {
ditherPixel[0] = clamp[((rowerr[cursor] + 0x1008) >> 4) + r];
ditherPixel[1] = clamp[((rowerr[cursor + 1] + 0x1008) >> 4) + g];
ditherPixel[2] = clamp[((rowerr[cursor + 2] + 0x1008) >> 4) + b];
ditherPixel[3] = clamp[((rowerr[cursor + 3] + 0x1008) >> 4) + a];
return ditherPixel;
}
ditherPixel[0] = clamp[((rowerr[cursor] + 0x2010) >> 5) + r];
ditherPixel[1] = clamp[((rowerr[cursor + 1] + 0x1008) >> 4) + g];
ditherPixel[2] = clamp[((rowerr[cursor + 2] + 0x2010) >> 5) + b];
ditherPixel[3] = a;
return ditherPixel;
}
import { decode, encode } from "https://deno.land/x/pngs/mod.ts";
const wheel = await Deno.readFile('./tests/images/milk.png');
const framebuffer = decode(wheel);
const quant = new PnnQuant({
colors: 256,
dithering: true,
width: framebuffer.width,
height: framebuffer.height,
pixels: new Uint32Array(framebuffer.image.buffer),
});
const width = framebuffer.width;
const height = framebuffer.height;
const b = new Uint32Array(framebuffer.image.buffer);
while (true) {
const a = performance.now();
quant.quantizeImage();
console.log(performance.now() - a);
}
const palette = new Uint32Array(quant.getPalette());
const pixels = new Uint32Array(width * height);
quant.getIndexedPixels().forEach((x, i) => pixels[i] = palette[x]);
Deno.writeFile('./tests/images/wheel_quant.png', encode(new Uint8Array(pixels.buffer), width, height));