optimize images
This commit is contained in:
@@ -1001,7 +1001,17 @@ async function processURL(socket, request_headers, pc_services = false) {
|
|||||||
request_headers.query = {};
|
request_headers.query = {};
|
||||||
|
|
||||||
if (request_headers.request_url) {
|
if (request_headers.request_url) {
|
||||||
service_name = socket.service_name || verifyServicePort(decodeURIComponent(request_headers.request_url).split(':')[0], socket);
|
try {
|
||||||
|
// wtv-log handling
|
||||||
|
request_headers.request_url = request_headers.request_url.replaceAll(/\%\+/g, "%20"); // sanitize parentheses for logging
|
||||||
|
service_name = socket.service_name || verifyServicePort(decodeURIComponent(request_headers.request_url).split(':')[0], socket);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(" * Invalid URI: %s", request_headers.request_url);
|
||||||
|
console.error((err && err.stack) ? err.stack : err);
|
||||||
|
const errpage = wtvshared.doErrorPage(400, null, null, pc_services);
|
||||||
|
sendToClient(socket, errpage[0], errpage[1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (minisrv_config.services[service_name]) {
|
if (minisrv_config.services[service_name]) {
|
||||||
allow_double_slash = minisrv_config.services[service_name].allow_double_slash || false;
|
allow_double_slash = minisrv_config.services[service_name].allow_double_slash || false;
|
||||||
enable_multi_query = minisrv_config.services[service_name].enable_multi_query || false;
|
enable_multi_query = minisrv_config.services[service_name].enable_multi_query || false;
|
||||||
@@ -1314,13 +1324,16 @@ async function sendToClient(socket, headers_obj, data = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let imageArtemisType = 'ALP'
|
let imageArtemisType = minisrv_config.config.image_decoder.gif_type || 'ALP';
|
||||||
// Add last modified if not a dynamic script
|
// Add last modified if not a dynamic script
|
||||||
if (socket_sessions[socket.id]) {
|
if (socket_sessions[socket.id]) {
|
||||||
if (socket_sessions[socket.id].request_headers) {
|
if (socket_sessions[socket.id].request_headers) {
|
||||||
if (socket_sessions[socket.id].request_headers.query) {
|
if (socket_sessions[socket.id].request_headers.query) {
|
||||||
if (socket_sessions[socket.id].request_headers.query.forceALF) {
|
if (socket_sessions[socket.id].request_headers.query.type === "ALF" ||
|
||||||
imageArtemisType = 'ALF';
|
socket_sessions[socket.id].request_headers.query.type === "ALP"
|
||||||
|
)
|
||||||
|
{
|
||||||
|
imageArtemisType = socket_sessions[socket.id].request_headers.query.type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (socket_sessions[socket.id].request_headers.service_file_path) {
|
if (socket_sessions[socket.id].request_headers.service_file_path) {
|
||||||
@@ -1365,10 +1378,12 @@ async function sendToClient(socket, headers_obj, data = 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);
|
const converted = await WTVImage.ImageToWebTV(sourceData, convertOpts, pngOpts);
|
||||||
data = converted.data;
|
data = converted.data;
|
||||||
content_length = data.length;
|
content_length = data.length;
|
||||||
var i=0;
|
var i=0;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.3 KiB |
@@ -489,6 +489,156 @@ class WTVImage {
|
|||||||
return { rgba, width, height, type };
|
return { rgba, width, height, type };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quantize an RGBA image into a GIF palette and extract the real palette
|
||||||
|
* and alpha table using gifski-style heuristics.
|
||||||
|
*
|
||||||
|
* This function uses sharp to produce a color-indexed GIF, then rebuilds
|
||||||
|
* the true RGB palette and per-index alpha values from the original pixels.
|
||||||
|
* It is intentionally dependency-light and avoids requiring native imagequant
|
||||||
|
* bindings or experimental Node flags.
|
||||||
|
*/
|
||||||
|
async quantizeArtemisRGBA(rgbaData, width, height, targetColors) {
|
||||||
|
const pixelCount = width * height;
|
||||||
|
const quantizeData = Buffer.alloc(pixelCount * 4);
|
||||||
|
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const p = i * 4;
|
||||||
|
const a = rgbaData[p + 3];
|
||||||
|
let tier;
|
||||||
|
if (a === 0) tier = 0;
|
||||||
|
else if (a >= 224) tier = 7;
|
||||||
|
else tier = 1 + ((a - 1) >> 5);
|
||||||
|
quantizeData[p] = ((tier & 0x07) << 5) | (rgbaData[p] >> 3);
|
||||||
|
quantizeData[p + 1] = rgbaData[p + 1];
|
||||||
|
quantizeData[p + 2] = rgbaData[p + 2];
|
||||||
|
quantizeData[p + 3] = 255; // sharp's GIF encoder needs alpha=255 to keep all pixels distinct
|
||||||
|
}
|
||||||
|
|
||||||
|
const quantizedGIFBuf = await sharp(quantizeData, { raw: { width, height, channels: 4 } })
|
||||||
|
.gif({ colors: targetColors, effort: 10, dither: 0 })
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const qHdr = this.parseGIFHeader(quantizedGIFBuf);
|
||||||
|
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 gSums = new Float64Array(colors);
|
||||||
|
const bSums = new Float64Array(colors);
|
||||||
|
const wSums = new Float64Array(colors);
|
||||||
|
const alphaHists = Array.from({ length: colors }, () => new Uint32Array(256));
|
||||||
|
const counts = new Uint32Array(colors);
|
||||||
|
|
||||||
|
for (let i = 0; i < pixelCount; i++) {
|
||||||
|
const idx = indices[i];
|
||||||
|
if (idx >= colors) continue;
|
||||||
|
const p = i * 4;
|
||||||
|
const a = rgbaData[p + 3];
|
||||||
|
const w = a / 255;
|
||||||
|
rSums[idx] += rgbaData[p] * w;
|
||||||
|
gSums[idx] += rgbaData[p + 1] * w;
|
||||||
|
bSums[idx] += rgbaData[p + 2] * w;
|
||||||
|
wSums[idx] += w;
|
||||||
|
alphaHists[idx][a] += 1;
|
||||||
|
counts[idx]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const realPalette = Buffer.alloc(colors * 3, 0);
|
||||||
|
const alphaTable = Buffer.alloc(colors, 0xFF);
|
||||||
|
for (let i = 0; i < colors; i++) {
|
||||||
|
if (counts[i] === 0) continue;
|
||||||
|
if (wSums[i] > 0) {
|
||||||
|
realPalette[i * 3] = Math.round(rSums[i] / wSums[i]);
|
||||||
|
realPalette[i * 3 + 1] = Math.round(gSums[i] / wSums[i]);
|
||||||
|
realPalette[i * 3 + 2] = Math.round(bSums[i] / wSums[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hist = alphaHists[i];
|
||||||
|
const total = counts[i];
|
||||||
|
|
||||||
|
let modeAlpha = 0;
|
||||||
|
let modeCount = -1;
|
||||||
|
for (let a = 0; a <= 255; a++) {
|
||||||
|
const c = hist[a];
|
||||||
|
if (c > modeCount || (c === modeCount && a === 255)) {
|
||||||
|
modeCount = c;
|
||||||
|
modeAlpha = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let opaqueCount = 0;
|
||||||
|
for (let a = 240; a <= 255; a++) opaqueCount += hist[a];
|
||||||
|
if (total > 0 && (opaqueCount / total) >= 0.50) {
|
||||||
|
alphaTable[i] = 255;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total > 0 && (hist[0] / total) >= 0.50) {
|
||||||
|
alphaTable[i] = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let chosen = modeAlpha;
|
||||||
|
if (chosen >= 252) chosen = 255;
|
||||||
|
else chosen = chosen & 0xF8;
|
||||||
|
alphaTable[i] = chosen;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bestZeroIdx = -1;
|
||||||
|
let bestZeroCount = 0;
|
||||||
|
for (let i = 0; i < colors; i++) {
|
||||||
|
if (alphaTable[i] === 0 && counts[i] > bestZeroCount) {
|
||||||
|
bestZeroIdx = i;
|
||||||
|
bestZeroCount = counts[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { colors, indices, realPalette, alphaTable, bestZeroIdx };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode an RGBA image (raw Buffer or sharp-compatible input) into a WebTV
|
* Encode an RGBA image (raw Buffer or sharp-compatible input) into a WebTV
|
||||||
* Artemis ALF GIF.
|
* Artemis ALF GIF.
|
||||||
@@ -525,173 +675,13 @@ class WTVImage {
|
|||||||
.toBuffer({ resolveWithObject: true });
|
.toBuffer({ resolveWithObject: true });
|
||||||
|
|
||||||
const { width, height } = info;
|
const { width, height } = info;
|
||||||
const pixelCount = width * height;
|
|
||||||
|
|
||||||
|
const { colors, indices, realPalette, alphaTable, bestZeroIdx } = await this.quantizeArtemisRGBA(rgbaData, width, height, targetColors);
|
||||||
|
|
||||||
const quantizeData = Buffer.alloc(pixelCount * 4);
|
|
||||||
for (let i = 0; i < pixelCount; i++) {
|
|
||||||
const p = i * 4;
|
|
||||||
const a = rgbaData[p + 3];
|
|
||||||
let tier;
|
|
||||||
if (a === 0) tier = 0;
|
|
||||||
else if (a >= 224) tier = 7;
|
|
||||||
else tier = 1 + ((a - 1) >> 5);
|
|
||||||
quantizeData[p] = ((tier & 0x07) << 5) | (rgbaData[p] >> 3);
|
|
||||||
quantizeData[p + 1] = rgbaData[p + 1];
|
|
||||||
quantizeData[p + 2] = rgbaData[p + 2];
|
|
||||||
quantizeData[p + 3] = 255; // sharp's GIF encoder needs alpha=255 to keep all pixels distinct
|
|
||||||
}
|
|
||||||
|
|
||||||
// No dithering: dithering would scatter alpha-encoded virtual colors across
|
|
||||||
// entries, corrupting the alpha-per-index mapping.
|
|
||||||
const quantizedGIFBuf = await sharp(quantizeData, { raw: { width, height, channels: 4 } })
|
|
||||||
.gif({ colors: targetColors, effort: 10, dither: 0 })
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
const qHdr = this.parseGIFHeader(quantizedGIFBuf);
|
|
||||||
if (!qHdr.hasGCT) throw new Error('Quantized GIF has no global color table');
|
|
||||||
|
|
||||||
const colors = qHdr.gctSize;
|
|
||||||
|
|
||||||
// Find image descriptor, skipping any extensions
|
|
||||||
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 preImageExt = quantizedGIFBuf.slice(13 + qHdr.gctBytes, scanPos);
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Rebuild the real palette and alpha table from original source pixels.
|
|
||||||
// RGB uses alpha-weighted averaging so transparent pixels (often undefined RGB)
|
|
||||||
// do not skew color. Alpha uses robust histogram quantiles to reduce halos.
|
|
||||||
const rSums = new Float64Array(colors);
|
|
||||||
const gSums = new Float64Array(colors);
|
|
||||||
const bSums = new Float64Array(colors);
|
|
||||||
const wSums = new Float64Array(colors); // alpha-weight sums for RGB
|
|
||||||
const alphaHists = Array.from({ length: colors }, () => new Uint32Array(256));
|
|
||||||
const counts = new Uint32Array(colors);
|
|
||||||
|
|
||||||
for (let i = 0; i < pixelCount; i++) {
|
|
||||||
const idx = indices[i];
|
|
||||||
if (idx >= colors) continue;
|
|
||||||
const p = i * 4;
|
|
||||||
const a = rgbaData[p + 3];
|
|
||||||
const w = a / 255; // weight by alpha
|
|
||||||
rSums[idx] += rgbaData[p] * w;
|
|
||||||
gSums[idx] += rgbaData[p + 1] * w;
|
|
||||||
bSums[idx] += rgbaData[p + 2] * w;
|
|
||||||
wSums[idx] += w;
|
|
||||||
alphaHists[idx][a] += 1;
|
|
||||||
counts[idx]++;
|
|
||||||
}
|
|
||||||
|
|
||||||
const realPalette = Buffer.alloc(colors * 3, 0);
|
|
||||||
const alphaTable = Buffer.alloc(colors, 0xFF);
|
|
||||||
for (let i = 0; i < colors; i++) {
|
|
||||||
if (counts[i] === 0) continue;
|
|
||||||
// For RGB: use alpha-weighted average so transparent pixels don't skew colors.
|
|
||||||
// For fully-transparent entries (wSums[i]≈0) color doesn't matter, leave black.
|
|
||||||
if (wSums[i] > 0) {
|
|
||||||
realPalette[i * 3] = Math.round(rSums[i] / wSums[i]);
|
|
||||||
realPalette[i * 3 + 1] = Math.round(gSums[i] / wSums[i]);
|
|
||||||
realPalette[i * 3 + 2] = Math.round(bSums[i] / wSums[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const hist = alphaHists[i];
|
|
||||||
const total = counts[i];
|
|
||||||
|
|
||||||
|
|
||||||
let modeAlpha = 0;
|
|
||||||
let modeCount = -1;
|
|
||||||
for (let a = 0; a <= 255; a++) {
|
|
||||||
const c = hist[a];
|
|
||||||
if (c > modeCount || (c === modeCount && a === 255)) {
|
|
||||||
modeCount = c;
|
|
||||||
modeAlpha = a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the cluster is mostly opaque (>=50% α≥240), snap to 255.
|
|
||||||
// Protects dialog/text content from anti-aliased edge bleed without
|
|
||||||
// collapsing genuinely-translucent dialog body content.
|
|
||||||
let opaqueCount = 0;
|
|
||||||
for (let a = 240; a <= 255; a++) opaqueCount += hist[a];
|
|
||||||
if (total > 0 && (opaqueCount / total) >= 0.50) {
|
|
||||||
alphaTable[i] = 255;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the cluster is mostly fully-transparent (>=50% α=0), snap to 0.
|
|
||||||
if (total > 0 && (hist[0] / total) >= 0.50) {
|
|
||||||
alphaTable[i] = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let chosen = modeAlpha;
|
|
||||||
if (chosen >= 252) chosen = 255;
|
|
||||||
else chosen = chosen & 0xF8;
|
|
||||||
alphaTable[i] = chosen;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit the full alphaTable (no truncation). While reference WebTV ROM
|
|
||||||
// Artemis GIFs do truncate trailing 0xFF entries, the WebTV renderer
|
|
||||||
// appears to default missing entries to 0x00 (transparent), not 0xFF
|
|
||||||
// (opaque), so any opaque palette entry past the truncation point
|
|
||||||
// would render invisible. Always emit all `colors` entries.
|
|
||||||
let alphaLen = alphaTable.length;
|
|
||||||
const trimmedAlphaTable = alphaTable.slice(0, alphaLen);
|
|
||||||
|
|
||||||
// Find palette index whose alphaTable[idx]===0 with the most pixels.
|
|
||||||
// Use the correct transparent palette slot for ALF vs ALP.
|
|
||||||
const transparentIdx = (type === 'ALF') ? colors - 1 : 0;
|
const transparentIdx = (type === 'ALF') ? colors - 1 : 0;
|
||||||
let bestZeroIdx = -1;
|
|
||||||
let bestZeroCount = 0;
|
|
||||||
for (let i = 0; i < colors; i++) {
|
|
||||||
if (alphaTable[i] === 0 && counts[i] > bestZeroCount) {
|
|
||||||
bestZeroIdx = i;
|
|
||||||
bestZeroCount = counts[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalIndices = Buffer.from(indices);
|
const finalIndices = Buffer.from(indices);
|
||||||
const fullAlpha = Buffer.from(alphaTable);
|
const fullAlpha = Buffer.from(alphaTable);
|
||||||
if (bestZeroIdx >= 0 && bestZeroIdx !== transparentIdx) {
|
if (bestZeroIdx >= 0 && bestZeroIdx !== transparentIdx) {
|
||||||
// Swap the transparent palette entry into the expected slot.
|
|
||||||
const tmpR = realPalette[transparentIdx * 3];
|
const tmpR = realPalette[transparentIdx * 3];
|
||||||
const tmpG = realPalette[transparentIdx * 3 + 1];
|
const tmpG = realPalette[transparentIdx * 3 + 1];
|
||||||
const tmpB = realPalette[transparentIdx * 3 + 2];
|
const tmpB = realPalette[transparentIdx * 3 + 2];
|
||||||
@@ -986,7 +976,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 = {}) {
|
async ImageToWebTV(input, opts = {}, pngopts = {}) {
|
||||||
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;
|
||||||
@@ -996,7 +986,7 @@ class WTVImage {
|
|||||||
if (maxHeight) resizeOpts.height = maxHeight;
|
if (maxHeight) resizeOpts.height = maxHeight;
|
||||||
pngBuf = await sharp(pngBuf)
|
pngBuf = await sharp(pngBuf)
|
||||||
.resize(resizeOpts)
|
.resize(resizeOpts)
|
||||||
.png()
|
.png(pngopts)
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
}
|
}
|
||||||
const meta = await sharp(pngBuf).metadata();
|
const meta = await sharp(pngBuf).metadata();
|
||||||
|
|||||||
@@ -117,6 +117,7 @@
|
|||||||
"shenanigans": false,
|
"shenanigans": false,
|
||||||
"image_decoder": {
|
"image_decoder": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"gif_type": "ALP", // "ALP" or "ALF", see WTVImage.js for details.
|
||||||
"jpg_quality": 75,
|
"jpg_quality": 75,
|
||||||
"image_formats": [
|
"image_formats": [
|
||||||
"image/png",
|
"image/png",
|
||||||
@@ -125,6 +126,12 @@
|
|||||||
"image/tiff",
|
"image/tiff",
|
||||||
"image/webp"
|
"image/webp"
|
||||||
],
|
],
|
||||||
|
"png_opts": {
|
||||||
|
"quality": 80,
|
||||||
|
"compressionLevel": 9,
|
||||||
|
"palette": true,
|
||||||
|
"effort": 10
|
||||||
|
},
|
||||||
"max_height": 2048,
|
"max_height": 2048,
|
||||||
"max_width": 640,
|
"max_width": 640,
|
||||||
"max_file_size": 524288,
|
"max_file_size": 524288,
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ function resolveOutput(inputFile, suggestedExt, override) {
|
|||||||
async function cmdConvert(inputFile, outputFile, opts) {
|
async function cmdConvert(inputFile, outputFile, opts) {
|
||||||
const ImageBuf = fs.readFileSync(inputFile);
|
const ImageBuf = fs.readFileSync(inputFile);
|
||||||
const { data, mime } = await WTVImage.ImageToWebTV(ImageBuf, {
|
const { data, mime } = await WTVImage.ImageToWebTV(ImageBuf, {
|
||||||
type: opts.type || 'ALF',
|
type: opts.type || 'ALP',
|
||||||
colors: opts.colors || 256,
|
colors: opts.colors || 256,
|
||||||
jpegQuality: opts.quality || 85,
|
jpegQuality: opts.quality || 85,
|
||||||
maxWidth: opts.maxWidth,
|
maxWidth: opts.maxWidth,
|
||||||
|
|||||||
Reference in New Issue
Block a user