Smoother ALF/ALP
This commit is contained in:
@@ -1369,22 +1369,20 @@ async function sendToClient(socket, headers_obj, data = null, throttle = 0) {
|
|||||||
|
|
||||||
if (minisrv_config.config.image_decoder && minisrv_config.config.image_decoder.enabled) {
|
if (minisrv_config.config.image_decoder && minisrv_config.config.image_decoder.enabled) {
|
||||||
const contype_key = wtvshared.getCaseInsensitiveKey('content-type', headers_obj);
|
const contype_key = wtvshared.getCaseInsensitiveKey('content-type', headers_obj);
|
||||||
let pngOpts = {};
|
|
||||||
if (contype_key) {
|
if (contype_key) {
|
||||||
if (minisrv_config.config.image_decoder.image_formats && minisrv_config.config.image_decoder.image_formats.includes(headers_obj[contype_key].toLowerCase())) {
|
if (minisrv_config.config.image_decoder.image_formats && minisrv_config.config.image_decoder.image_formats.includes(headers_obj[contype_key].toLowerCase())) {
|
||||||
const convertOpts = {
|
const convertOpts = {
|
||||||
jpegQuality: minisrv_config.config.image_decoder.jpg_quality,
|
jpegQuality: minisrv_config.config.image_decoder.jpg_quality,
|
||||||
type: imageArtemisType
|
type: imageArtemisType,
|
||||||
|
imgopts: minisrv_config.config.image_decoder.image_options || null
|
||||||
};
|
};
|
||||||
|
|
||||||
if (minisrv_config.config.image_decoder.max_height > 0) convertOpts.maxHeight = minisrv_config.config.image_decoder.max_height;
|
if (minisrv_config.config.image_decoder.max_height > 0) convertOpts.maxHeight = minisrv_config.config.image_decoder.max_height;
|
||||||
if (minisrv_config.config.image_decoder.max_width > 0) convertOpts.maxWidth = minisrv_config.config.image_decoder.max_width;
|
if (minisrv_config.config.image_decoder.max_width > 0) convertOpts.maxWidth = minisrv_config.config.image_decoder.max_width;
|
||||||
if (minisrv_config.config.image_decoder.png_opts) {
|
|
||||||
pngOpts = minisrv_config.config.image_decoder.png_opts;
|
|
||||||
}
|
|
||||||
const sourceData = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
const sourceData = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
||||||
try {
|
try {
|
||||||
const converted = await WTVImage.ImageToWebTV(sourceData, convertOpts, pngOpts);
|
const converted = await WTVImage.ImageToWebTV(sourceData, convertOpts);
|
||||||
data = converted.data;
|
data = converted.data;
|
||||||
content_length = data.length;
|
content_length = data.length;
|
||||||
var i=0;
|
var i=0;
|
||||||
@@ -1392,7 +1390,7 @@ async function sendToClient(socket, headers_obj, data = null, throttle = 0) {
|
|||||||
// Image is too big, try to reduce quality
|
// Image is too big, try to reduce quality
|
||||||
if (i < minisrv_config.config.image_decoder.max_quality_tries) {
|
if (i < minisrv_config.config.image_decoder.max_quality_tries) {
|
||||||
convertOpts.jpegQuality -= minisrv_config.config.image_decoder.jpeg_interval;
|
convertOpts.jpegQuality -= minisrv_config.config.image_decoder.jpeg_interval;
|
||||||
var converted2 = await WTVImage.ImageToWebTV(sourceData, convertOpts);
|
var converted2 = await WTVImage.ImageToWebTV(sourceData, convertOpts, pngOpts);
|
||||||
data = converted2.data;
|
data = converted2.data;
|
||||||
content_length = data.length;
|
content_length = data.length;
|
||||||
i++;
|
i++;
|
||||||
|
|||||||
@@ -540,72 +540,30 @@ class WTVImage {
|
|||||||
* It is intentionally dependency-light and avoids requiring native imagequant
|
* It is intentionally dependency-light and avoids requiring native imagequant
|
||||||
* bindings or experimental Node flags.
|
* bindings or experimental Node flags.
|
||||||
*/
|
*/
|
||||||
async quantizeArtemisRGBA(rgbaData, width, height, targetColors) {
|
async quantizeArtemisRGBA(rgbaData, width, height, opts) {
|
||||||
const pixelCount = width * height;
|
const pixelCount = width * height;
|
||||||
const quantizeData = Buffer.alloc(pixelCount * 4);
|
|
||||||
|
|
||||||
for (let i = 0; i < pixelCount; i++) {
|
// Use sharp's PNG palette mode (libimagequant) instead of its GIF
|
||||||
const p = i * 4;
|
// encoder. GIF only supports 1-bit alpha so its quantizer collapses
|
||||||
const a = rgbaData[p + 3];
|
// partial-alpha pixels to fully-opaque or fully-transparent before
|
||||||
let tier;
|
// clustering, destroying the per-pixel alpha we need to reconstruct.
|
||||||
if (a === 0) tier = 0;
|
// libimagequant under the PNG path clusters in true 4D RGBA space
|
||||||
else if (a >= 224) tier = 7;
|
// and gives us per-pixel palette indices we can hand to our own
|
||||||
else tier = 1 + ((a - 1) >> 5);
|
// alpha-histogram pass.
|
||||||
quantizeData[p] = ((tier & 0x07) << 5) | (rgbaData[p] >> 3);
|
const pngOpts = {
|
||||||
quantizeData[p + 1] = rgbaData[p + 1];
|
palette: true,
|
||||||
quantizeData[p + 2] = rgbaData[p + 2];
|
colors: Math.max(2, Math.min(256, opts.colors || 256)),
|
||||||
quantizeData[p + 3] = 255; // sharp's GIF encoder needs alpha=255 to keep all pixels distinct
|
// Carry through dither / effort if the caller specified them.
|
||||||
}
|
dither: (opts.imgopts && typeof opts.imgopts.dither === 'number') ? opts.imgopts.dither : 1.0,
|
||||||
|
effort: (opts.imgopts && typeof opts.imgopts.effort === 'number') ? opts.imgopts.effort : 7,
|
||||||
|
compressionLevel: 0,
|
||||||
|
};
|
||||||
|
|
||||||
const quantizedGIFBuf = await sharp(quantizeData, { raw: { width, height, channels: 4 } })
|
const quantizedPNGBuf = await sharp(rgbaData, { raw: { width, height, channels: 4 } })
|
||||||
.gif({ colors: targetColors, effort: 10, dither: 0 })
|
.png(pngOpts)
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
const qHdr = this.parseGIFHeader(quantizedGIFBuf);
|
const { palette: rawPalette, indices, colors } = this.extractPalettePNG(quantizedPNGBuf);
|
||||||
if (!qHdr.hasGCT) throw new Error('Quantized GIF has no global color table');
|
|
||||||
|
|
||||||
const colors = qHdr.gctSize;
|
|
||||||
|
|
||||||
let scanPos = 13 + qHdr.gctBytes;
|
|
||||||
while (scanPos < quantizedGIFBuf.length) {
|
|
||||||
const b = quantizedGIFBuf[scanPos];
|
|
||||||
if (b === 0x2C) break;
|
|
||||||
if (b === 0x3B) throw new Error('No image found in quantized GIF');
|
|
||||||
if (b === 0x21) {
|
|
||||||
scanPos += 2;
|
|
||||||
const label = quantizedGIFBuf[scanPos - 1];
|
|
||||||
if (label === 0xF9) {
|
|
||||||
const gceBlockSize = quantizedGIFBuf[scanPos];
|
|
||||||
scanPos += 1 + gceBlockSize + 1;
|
|
||||||
} else if (label === 0xFF) {
|
|
||||||
const appBlockSize = quantizedGIFBuf[scanPos];
|
|
||||||
scanPos += 1 + appBlockSize;
|
|
||||||
while (scanPos < quantizedGIFBuf.length && quantizedGIFBuf[scanPos] !== 0) {
|
|
||||||
scanPos += quantizedGIFBuf[scanPos] + 1;
|
|
||||||
}
|
|
||||||
scanPos++;
|
|
||||||
} else {
|
|
||||||
while (scanPos < quantizedGIFBuf.length && quantizedGIFBuf[scanPos] !== 0) {
|
|
||||||
scanPos += quantizedGIFBuf[scanPos] + 1;
|
|
||||||
}
|
|
||||||
scanPos++;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
scanPos++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scanPos >= quantizedGIFBuf.length) throw new Error('Could not find image descriptor');
|
|
||||||
|
|
||||||
const imgDescStart = scanPos;
|
|
||||||
const imgDescPacked = quantizedGIFBuf[imgDescStart + 9];
|
|
||||||
const hasLCT = (imgDescPacked & 0x80) !== 0;
|
|
||||||
const lctSize = hasLCT ? (1 << ((imgDescPacked & 0x07) + 1)) : 0;
|
|
||||||
const lzwStart = imgDescStart + 10 + lctSize * 3;
|
|
||||||
const minCodeSize = quantizedGIFBuf[lzwStart];
|
|
||||||
|
|
||||||
const { data: rawLZWData } = this.readSubBlocks(quantizedGIFBuf, lzwStart + 1);
|
|
||||||
const indices = this.lzwDecode(rawLZWData, minCodeSize, pixelCount);
|
|
||||||
|
|
||||||
const rSums = new Float64Array(colors);
|
const rSums = new Float64Array(colors);
|
||||||
const gSums = new Float64Array(colors);
|
const gSums = new Float64Array(colors);
|
||||||
@@ -701,12 +659,6 @@ class WTVImage {
|
|||||||
const paletteSize = opts.colors || 256;
|
const paletteSize = opts.colors || 256;
|
||||||
const type = opts.type || 'ALF';
|
const type = opts.type || 'ALF';
|
||||||
|
|
||||||
// Clamp palette size to valid GIF power-of-two values
|
|
||||||
const validSizes = [2, 4, 8, 16, 32, 64, 128, 256];
|
|
||||||
const targetColors = validSizes.reduce((prev, cur) =>
|
|
||||||
Math.abs(cur - paletteSize) < Math.abs(prev - paletteSize) ? cur : prev
|
|
||||||
);
|
|
||||||
|
|
||||||
const sharpSrc = (typeof input === 'string' || Buffer.isBuffer(input))
|
const sharpSrc = (typeof input === 'string' || Buffer.isBuffer(input))
|
||||||
? sharp(input)
|
? sharp(input)
|
||||||
: input;
|
: input;
|
||||||
@@ -718,7 +670,7 @@ class WTVImage {
|
|||||||
|
|
||||||
const { width, height } = info;
|
const { width, height } = info;
|
||||||
|
|
||||||
const { colors, indices, realPalette, alphaTable, bestZeroIdx } = await this.quantizeArtemisRGBA(rgbaData, width, height, targetColors);
|
const { colors, indices, realPalette, alphaTable, bestZeroIdx } = await this.quantizeArtemisRGBA(rgbaData, width, height, opts);
|
||||||
|
|
||||||
const transparentIdx = (type === 'ALF') ? colors - 1 : 0;
|
const transparentIdx = (type === 'ALF') ? colors - 1 : 0;
|
||||||
const finalIndices = Buffer.from(indices);
|
const finalIndices = Buffer.from(indices);
|
||||||
@@ -1018,7 +970,7 @@ class WTVImage {
|
|||||||
* @param {number} [opts.maxHeight] - maximum height to scale to before encoding
|
* @param {number} [opts.maxHeight] - maximum height to scale to before encoding
|
||||||
* @returns {Promise<{ data: Buffer, mime: string }>}
|
* @returns {Promise<{ data: Buffer, mime: string }>}
|
||||||
*/
|
*/
|
||||||
async ImageToWebTV(input, opts = {}, pngopts = {}) {
|
async ImageToWebTV(input, opts = {}) {
|
||||||
let pngBuf = Buffer.isBuffer(input) ? input : require('fs').readFileSync(input);
|
let pngBuf = Buffer.isBuffer(input) ? input : require('fs').readFileSync(input);
|
||||||
const maxWidth = Number(opts.maxWidth) > 0 ? Number(opts.maxWidth) : null;
|
const maxWidth = Number(opts.maxWidth) > 0 ? Number(opts.maxWidth) : null;
|
||||||
const maxHeight = Number(opts.maxHeight) > 0 ? Number(opts.maxHeight) : null;
|
const maxHeight = Number(opts.maxHeight) > 0 ? Number(opts.maxHeight) : null;
|
||||||
@@ -1029,15 +981,9 @@ class WTVImage {
|
|||||||
const resizeOpts = { fit: 'inside', withoutEnlargement: true };
|
const resizeOpts = { fit: 'inside', withoutEnlargement: true };
|
||||||
if (maxWidth) resizeOpts.width = maxWidth;
|
if (maxWidth) resizeOpts.width = maxWidth;
|
||||||
if (maxHeight) resizeOpts.height = maxHeight;
|
if (maxHeight) resizeOpts.height = maxHeight;
|
||||||
const outputPngOpts = Object.assign({}, pngopts);
|
|
||||||
if (originalIsPalettePNG && outputPngOpts.palette) {
|
|
||||||
// Avoid an extra palette quantization step on an already-indexed PNG.
|
|
||||||
outputPngOpts.palette = false;
|
|
||||||
delete outputPngOpts.colors;
|
|
||||||
}
|
|
||||||
pngBuf = await sharp(pngBuf)
|
pngBuf = await sharp(pngBuf)
|
||||||
.resize(resizeOpts)
|
.resize(resizeOpts)
|
||||||
.png(outputPngOpts)
|
.png()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
}
|
}
|
||||||
const meta = willResize ? await sharp(pngBuf).metadata() : inputMeta;
|
const meta = willResize ? await sharp(pngBuf).metadata() : inputMeta;
|
||||||
|
|||||||
@@ -126,10 +126,11 @@
|
|||||||
"image/tiff",
|
"image/tiff",
|
||||||
"image/webp"
|
"image/webp"
|
||||||
],
|
],
|
||||||
"png_opts": {
|
"image_options": {
|
||||||
"quality": 80,
|
"compressionLevel": 0,
|
||||||
"compressionLevel": 9,
|
"adaptiveFiltering": true,
|
||||||
"palette": true,
|
"dither": 1,
|
||||||
|
"colors": 256,
|
||||||
"effort": 10
|
"effort": 10
|
||||||
},
|
},
|
||||||
"max_height": 2048,
|
"max_height": 2048,
|
||||||
|
|||||||
Reference in New Issue
Block a user