sixel
Version:
Sixel image format for node and browser.
153 lines • 5.77 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.reduce = void 0;
/**
* Copyright (c) 2020 Joerg Breitbart.
* @license MIT
*
* Parts taken from UPNG:
* MIT License, Copyright (c) 2017 Photopea
*/
const Colors_1 = require("./Colors");
const upng_1 = require("./upng");
function clamp8Bit(value) {
return value >= 255 ? 255 : value < 0 ? 0 : value;
}
function applyError(value, r, g, b) {
return ((0xFF00 | clamp8Bit((0, Colors_1.blue)(value) + b))
<< 8 | clamp8Bit((0, Colors_1.green)(value) + g))
<< 8 | clamp8Bit((0, Colors_1.red)(value) + r);
}
/**
* The internal quantizer currently relies on the kd tree quanization from UPNG.
*
* Planned:
* - SIXEL optimized quantizer without alpha channel and reduced RGB (only 1M colors possible)
* - better/customizable dithering algos
* - separate palette creation from image reduction
* - support for predefined palette (needs reconstruction of kd tree)
* - grayscale / monochrome transformations
*
* FIXME: Dithering should respect image dimensions.
*/
function reduce(data, width, colors) {
const data32 = new Uint32Array(data.buffer);
// palette creation with kd tree
const KD = upng_1.quantize.getKDtree(data.slice(), colors);
const leafs = KD[1];
const palette = leafs.map((el) => (0, Colors_1.fromRGBA8888)(el.est.rgba));
const cm = new ColorMatcher(palette);
const indices = new Uint16Array(data32.length);
const len = data32.length;
for (let i = 0; i < len; ++i) {
const v = data32[i];
const r = (0, Colors_1.red)(v);
const g = (0, Colors_1.green)(v);
const b = (0, Colors_1.blue)(v);
// boxed matching - compromise between exact match and performance
const idx = cm.nearest(v);
indices[i] = idx;
// dithering - FIXME: find better algo in terms of output quality and speed
const vp = leafs[idx].est.rgba;
let er = (r - (0, Colors_1.red)(vp)) >> 2;
let eg = (g - (0, Colors_1.green)(vp)) >> 2;
let eb = (b - (0, Colors_1.blue)(vp)) >> 2;
// FIXME: respect idx overflow / left and right border
data32[i + 1] = applyError(data32[i + 1], er, eg, eb);
data32[i + width] = applyError(data32[i + width], er, eg, eb);
er >>= 1;
eg >>= 1;
eb >>= 1;
data32[i + width - 1] = applyError(data32[i + width - 1], er, eg, eb);
data32[i + width + 1] = applyError(data32[i + width + 1], er, eg, eb);
}
return { indices, palette: leafs.map((el) => el.est.rgba) };
}
exports.reduce = reduce;
/**
* Class to do nearest palette color matching with 16x16x16 boxes.
*/
class ColorMatcher {
constructor(palette, radius = 14, radius2 = 42) {
this.palette = palette;
this._boxes = {};
this._boxes2 = {};
// limit: search sphere to add palette points from
// limit2: outer search sphere for uncertain area
// the value is chosen to get an error rate > 5%
// while not penalizing runtime too much (inner sphere is good trade off)
const limit = radius * radius * 3;
const limit2 = radius2 * radius2 * 3;
for (let i = 0; i < 4096; ++i) {
const x = i >> 8;
const y = i >> 4 & 15;
const z = i & 15;
this._nearestPoints(i, (x << 4) + 8, (y << 4) + 8, (z << 4) + 8, limit, limit2);
}
}
_nearestPoints(box, r, g, b, limit, limit2) {
let min = Number.MAX_SAFE_INTEGER;
let idx = -1;
const pointIndices = [];
const pointIndices2 = [];
for (let i = 0; i < this.palette.length; ++i) {
const p_color = this.palette[i];
const d = this._distance(r, g, b, p_color[0], p_color[1], p_color[2]);
if (d < min) {
min = d;
idx = i;
}
if (d < limit) {
pointIndices.push(i);
}
else if (d < limit2) {
pointIndices2.push(i);
}
}
if (pointIndices.length === 0) {
pointIndices.push(idx);
}
this._boxes[box] = pointIndices;
this._boxes2[box] = pointIndices2;
}
_distance(r1, g1, b1, r2, g2, b2) {
const dr = r1 - r2;
const dg = g1 - g2;
const db = b1 - b2;
return dr * dr + dg * dg + db * db;
}
nearest(color) {
const r = (0, Colors_1.red)(color);
const g = (0, Colors_1.green)(color);
const b = (0, Colors_1.blue)(color);
const box = ((r >> 4) << 8) | ((g >> 4) << 4) | (b >> 4);
const indices = this._boxes[box];
let min = Number.MAX_SAFE_INTEGER;
let idx = -1;
// inner sphere handling
for (let i = 0; i < indices.length; ++i) {
const p_color = this.palette[indices[i]];
const d = this._distance(r, g, b, p_color[0], p_color[1], p_color[2]);
if (!d)
return indices[i];
if (d < min) {
min = d;
idx = indices[i];
}
}
// check for outer sphere if point is within uncertain area (d > 8*8 + 8*8 + 8*8)
if (this._distance(r, g, b, (r & 0xF0) + 8, (g & 0xF0) + 8, (b & 0xF0) + 8) > 192) {
const indices = this._boxes2[box];
for (let i = 0; i < indices.length; ++i) {
const p_color = this.palette[indices[i]];
const d = this._distance(r, g, b, p_color[0], p_color[1], p_color[2]);
if (d < min) {
min = d;
idx = indices[i];
}
}
}
return idx;
}
}
//# sourceMappingURL=Quantizer.js.map