Rename WTVPNG to WTVImage since it handles more than just PNG
This commit is contained in:
@@ -26,7 +26,7 @@ const WTVClientCapabilities = require(classPath + "/WTVClientCapabilities.js");
|
|||||||
const WTVClientSessionData = require(classPath + "/WTVClientSessionData.js");
|
const WTVClientSessionData = require(classPath + "/WTVClientSessionData.js");
|
||||||
const WTVMime = require(classPath + "/WTVMime.js");
|
const WTVMime = require(classPath + "/WTVMime.js");
|
||||||
const WTVFlashrom = require(classPath + "/WTVFlashrom.js");
|
const WTVFlashrom = require(classPath + "/WTVFlashrom.js");
|
||||||
const WTVPNG = require(classPath + "/WTVPNG.js");
|
const WTVImage = require(classPath + "/WTVImage.js");
|
||||||
const vm = require('vm');
|
const vm = require('vm');
|
||||||
const debug = require('debug')('app');
|
const debug = require('debug')('app');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
@@ -1338,21 +1338,32 @@ async function sendToClient(socket, headers_obj, data = null) {
|
|||||||
delete headers_obj['minisrv-no-last-modified'];
|
delete headers_obj['minisrv-no-last-modified'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (minisrv_config.config.decode_png) {
|
if (minisrv_config.config.decode_unsupported_images) {
|
||||||
const contype_key = wtvshared.getCaseInsensitiveKey('content-type', headers_obj);
|
const contype_key = wtvshared.getCaseInsensitiveKey('content-type', headers_obj);
|
||||||
if (contype_key) {
|
if (contype_key) {
|
||||||
if (headers_obj[contype_key].toLowerCase() === "image/png" ||
|
if (headers_obj[contype_key].toLowerCase() === "image/png" ||
|
||||||
headers_obj[contype_key].toLowerCase() === "image/svg+xml" ||
|
headers_obj[contype_key].toLowerCase() === "image/svg+xml" ||
|
||||||
|
headers_obj[contype_key].toLowerCase() === "image/avif" ||
|
||||||
|
headers_obj[contype_key].toLowerCase() === "image/tiff" ||
|
||||||
headers_obj[contype_key].toLowerCase() === "image/webp") {
|
headers_obj[contype_key].toLowerCase() === "image/webp") {
|
||||||
const convertOpts = {
|
const convertOpts = {
|
||||||
jpegQuality: minisrv_config.config.decode_png_jpeg_quality,
|
jpegQuality: minisrv_config.config.decode_unsupported_images_quality,
|
||||||
type: 'ALF'
|
type: 'ALF'
|
||||||
};
|
};
|
||||||
const sourceData = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
const sourceData = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
||||||
const converted = await WTVPNG.pngToWebTV(sourceData, convertOpts);
|
try {
|
||||||
data = converted.data;
|
const converted = await WTVImage.ImageToWebTV(sourceData, convertOpts);
|
||||||
content_length = data.length;
|
data = converted.data;
|
||||||
headers_obj[contype_key] = (converted.mime === 'image/jpeg') ? 'image/jpeg' : 'image/gif';
|
content_length = data.length;
|
||||||
|
headers_obj[contype_key] = (converted.mime === 'image/jpeg') ? 'image/jpeg' : 'image/gif';
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error converting image for client:", e);
|
||||||
|
headers_obj = {
|
||||||
|
"Status": `400 ${minisrv_config.config.service_name} ran into a technical problem. (Image not supported)`,
|
||||||
|
"Content-type": "text/html"
|
||||||
|
}
|
||||||
|
data = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2336,8 +2347,8 @@ if (minisrv_config.config.shenanigans) console.log(" * WARNING: Shenanigans leve
|
|||||||
else console.log(" * Shenanigans disabled");
|
else console.log(" * Shenanigans disabled");
|
||||||
|
|
||||||
// PNG
|
// PNG
|
||||||
if (minisrv_config.config.decode_png) console.log(" * PNG will be processed for WebTV clients");
|
if (minisrv_config.config.decode_png) console.log(" * WebTV Unsupported images will be processed and converted for WebTV clients");
|
||||||
else console.log(" * PNG will not be processed, and sent to client as-is");
|
else console.log(" * WebTV Unsupported images will not be processed, and sent to client as-is");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* WTVPNG - WebTV PNG/Image Conversion Utility
|
* WTVImage - WebTV PNG/Image Conversion Utility
|
||||||
*
|
*
|
||||||
* WebTV cannot display PNG natively. This class converts PNGs (and other
|
* WebTV cannot display PNG natively. This class converts PNGs (and other
|
||||||
* sharp-compatible sources) to the appropriate WebTV format:
|
* sharp-compatible sources) to the appropriate WebTV format:
|
||||||
@@ -32,7 +32,7 @@ const zlib = require('zlib');
|
|||||||
// Class wrapper
|
// Class wrapper
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
class WTVPNG {
|
class WTVImage {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Low-level GIF 89a helpers
|
// Low-level GIF 89a helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -873,7 +873,7 @@ class WTVPNG {
|
|||||||
* @param {'ALP'|'ALF'} [opts.type='ALP']
|
* @param {'ALP'|'ALF'} [opts.type='ALP']
|
||||||
* @returns {Promise<Buffer>}
|
* @returns {Promise<Buffer>}
|
||||||
*/
|
*/
|
||||||
async palettePNGToArtemisGIF(pngBuf, opts = {}) {
|
async paletteImageToArtemisGIF(pngBuf, opts = {}) {
|
||||||
const type = opts.type || 'ALP';
|
const type = opts.type || 'ALP';
|
||||||
const { palette, alphaTable, indices, width, height, colors } = this.extractPalettePNG(pngBuf);
|
const { palette, alphaTable, indices, width, height, colors } = this.extractPalettePNG(pngBuf);
|
||||||
|
|
||||||
@@ -960,7 +960,7 @@ class WTVPNG {
|
|||||||
* @param {number} [opts.jpegQuality=85] - JPEG quality (0-100) when no alpha
|
* @param {number} [opts.jpegQuality=85] - JPEG quality (0-100) when no alpha
|
||||||
* @returns {Promise<{ data: Buffer, mime: string }>}
|
* @returns {Promise<{ data: Buffer, mime: string }>}
|
||||||
*/
|
*/
|
||||||
async pngToWebTV(input, opts = {}) {
|
async ImageToWebTV(input, opts = {}) {
|
||||||
const pngBuf = Buffer.isBuffer(input) ? input : require('fs').readFileSync(input);
|
const pngBuf = Buffer.isBuffer(input) ? input : require('fs').readFileSync(input);
|
||||||
const meta = await sharp(pngBuf).metadata();
|
const meta = await sharp(pngBuf).metadata();
|
||||||
let usesAlpha = false;
|
let usesAlpha = false;
|
||||||
@@ -992,7 +992,7 @@ class WTVPNG {
|
|||||||
// Allow forcing re-quantization only when explicitly requested.
|
// Allow forcing re-quantization only when explicitly requested.
|
||||||
const data = opts.forceRequantizePalette
|
const data = opts.forceRequantizePalette
|
||||||
? await this.encodeArtemisGIF(pngBuf, opts)
|
? await this.encodeArtemisGIF(pngBuf, opts)
|
||||||
: await this.palettePNGToArtemisGIF(pngBuf, opts);
|
: await this.paletteImageToArtemisGIF(pngBuf, opts);
|
||||||
return { data, mime: 'image/gif' };
|
return { data, mime: 'image/gif' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1001,9 +1001,9 @@ class WTVPNG {
|
|||||||
return { data, mime: 'image/gif' };
|
return { data, mime: 'image/gif' };
|
||||||
}
|
}
|
||||||
|
|
||||||
async pngToArtemisGIF(input, opts = {}) {
|
async ImageToArtemisGIF(input, opts = {}) {
|
||||||
const result = await this.pngToWebTV(input, opts);
|
const result = await this.ImageToWebTV(input, opts);
|
||||||
if (result.mime !== 'image/gif') throw new Error('Input PNG has no alpha; cannot encode as Artemis GIF. Use pngToWebTV() instead.');
|
if (result.mime !== 'image/gif') throw new Error('Input image has no alpha; cannot encode as Artemis GIF. Use ImageToWebTV() instead.');
|
||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1031,7 +1031,7 @@ class WTVPNG {
|
|||||||
* @returns {'ALP'|'ALF'|null}
|
* @returns {'ALP'|'ALF'|null}
|
||||||
*/
|
*/
|
||||||
static detect(gifBuf) {
|
static detect(gifBuf) {
|
||||||
return WTVPNG._impl.detectArtemisType(gifBuf);
|
return WTVImage._impl.detectArtemisType(gifBuf);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1040,7 +1040,7 @@ class WTVPNG {
|
|||||||
* @returns {Promise<{ rgba: Buffer, width: number, height: number, type: string }>}
|
* @returns {Promise<{ rgba: Buffer, width: number, height: number, type: string }>}
|
||||||
*/
|
*/
|
||||||
static decode(gifBuf) {
|
static decode(gifBuf) {
|
||||||
return WTVPNG._impl.decodeArtemisGIF(gifBuf);
|
return WTVImage._impl.decodeArtemisGIF(gifBuf);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1053,21 +1053,21 @@ class WTVPNG {
|
|||||||
* @returns {Promise<Buffer>}
|
* @returns {Promise<Buffer>}
|
||||||
*/
|
*/
|
||||||
static encode(input, opts = {}) {
|
static encode(input, opts = {}) {
|
||||||
return WTVPNG._impl.encodeArtemisGIF(input, opts);
|
return WTVImage._impl.encodeArtemisGIF(input, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a PNG to the appropriate WebTV format.
|
* Convert an unsupported image to the appropriate WebTV format.
|
||||||
* @param {string|Buffer} input
|
* @param {string|Buffer} input
|
||||||
* @param {object} [opts]
|
* @param {object} [opts]
|
||||||
* @returns {Promise<{ data: Buffer, mime: string }>}
|
* @returns {Promise<{ data: Buffer, mime: string }>}
|
||||||
*/
|
*/
|
||||||
static pngToWebTV(input, opts = {}) {
|
static ImageToWebTV(input, opts = {}) {
|
||||||
return WTVPNG._impl.pngToWebTV(input, opts);
|
return WTVImage._impl.ImageToWebTV(input, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a PNG with alpha to a WebTV Artemis GIF.
|
* Convert a image with alpha to a WebTV Artemis GIF.
|
||||||
* Throws if the input has no alpha channel.
|
* Throws if the input has no alpha channel.
|
||||||
* @param {string|Buffer} input
|
* @param {string|Buffer} input
|
||||||
* @param {object} [opts]
|
* @param {object} [opts]
|
||||||
@@ -1075,8 +1075,8 @@ class WTVPNG {
|
|||||||
* @param {'ALP'|'ALF'} [opts.type='ALP']
|
* @param {'ALP'|'ALF'} [opts.type='ALP']
|
||||||
* @returns {Promise<Buffer>}
|
* @returns {Promise<Buffer>}
|
||||||
*/
|
*/
|
||||||
static pngToGIF(input, opts = {}) {
|
static ImageToGIF(input, opts = {}) {
|
||||||
return WTVPNG._impl.pngToArtemisGIF(input, opts);
|
return WTVImage._impl.ImageToArtemisGIF(input, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1085,10 +1085,10 @@ class WTVPNG {
|
|||||||
* @returns {Promise<Buffer>}
|
* @returns {Promise<Buffer>}
|
||||||
*/
|
*/
|
||||||
static gifToPNG(input) {
|
static gifToPNG(input) {
|
||||||
return WTVPNG._impl.artemisGIFtoPNG(input);
|
return WTVImage._impl.artemisGIFtoPNG(input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WTVPNG._impl = new WTVPNG();
|
WTVImage._impl = new WTVImage();
|
||||||
|
|
||||||
module.exports = WTVPNG;
|
module.exports = WTVImage;
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
"cgi_enabled": false, // Disable CGI by default
|
"cgi_enabled": false, // Disable CGI by default
|
||||||
"php_enabled": false, // Disable PHP by default
|
"php_enabled": false, // Disable PHP by default
|
||||||
"php_binpath": "php-cgi",
|
"php_binpath": "php-cgi",
|
||||||
"decode_png": true, // Attempt to decode PNG into JPG/ALP/ALF/GIF
|
"decode_unsupported_images": true, // Attempt to decode images WebTV doesn't support into JPG/ALF/GIF
|
||||||
"decode_png_jpeg_quality": 75, // JPEG quality for decoded PNGs, 0-100 lower is worse quality but smaller files.
|
"decode_unsupported_images_quality": 75, // JPEG quality for decoded PNGs, 0-100 lower is worse quality but smaller files.
|
||||||
"SessionStore": "SessionStore", // Where we store account (session) data. Best left unchanged.
|
"SessionStore": "SessionStore", // Where we store account (session) data. Best left unchanged.
|
||||||
"SharedROMCache": "SharedROMCache", // Shared ROMCache (wtv-service:/ROMCache/, where wtv-service is any configured service). Found under service vault. Best left unchanged.
|
"SharedROMCache": "SharedROMCache", // Shared ROMCache (wtv-service:/ROMCache/, where wtv-service is any configured service). Found under service vault. Best left unchanged.
|
||||||
"enable_shared_romcache": true, // Disabling this will cause a lot of problems without manual intervention. Best left unchanged.
|
"enable_shared_romcache": true, // Disabling this will cause a lot of problems without manual intervention. Best left unchanged.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const WTVPNG = require('./includes/classes/WTVPNG');
|
const WTVImage = require('./includes/classes/WTVImage');
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Argument parser
|
// Argument parser
|
||||||
@@ -55,12 +55,12 @@ function parseArgs(argv) {
|
|||||||
|
|
||||||
function printHelp() {
|
function printHelp() {
|
||||||
console.log(`
|
console.log(`
|
||||||
WebTV PNG/GIF Converter
|
WebTV Image Converter
|
||||||
=======================
|
=======================
|
||||||
Usage: node wtv_png_converter.js <command> [options] <input> [output]
|
Usage: node wtv_img_converter.js <command> [options] <input> [output]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
convert Convert a PNG to the best WebTV format
|
convert Convert an image to the best WebTV format
|
||||||
- No alpha → JPEG
|
- No alpha → JPEG
|
||||||
- Palette PNG → Artemis GIF (palette 1:1, no requantization)
|
- Palette PNG → Artemis GIF (palette 1:1, no requantization)
|
||||||
- Full-color RGBA → Artemis GIF (quantized)
|
- Full-color RGBA → Artemis GIF (quantized)
|
||||||
@@ -73,7 +73,7 @@ Commands:
|
|||||||
detect Report whether a file is an Artemis ALP/ALF GIF (no output file needed)
|
detect Report whether a file is an Artemis ALP/ALF GIF (no output file needed)
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--type <ALP|ALF> Artemis variant for encoding/convert (default: ALP)
|
--type <ALP|ALF> Artemis variant for encoding/convert (default: ALF)
|
||||||
--colors <n> Palette size for full-color quantization (default: 256)
|
--colors <n> Palette size for full-color quantization (default: 256)
|
||||||
--quality <n> JPEG quality when output is JPEG (default: 85)
|
--quality <n> JPEG quality when output is JPEG (default: 85)
|
||||||
--output, -o <file> Output file path
|
--output, -o <file> Output file path
|
||||||
@@ -104,9 +104,9 @@ function resolveOutput(inputFile, suggestedExt, override) {
|
|||||||
// Commands
|
// Commands
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
async function cmdConvert(inputFile, outputFile, opts) {
|
async function cmdConvert(inputFile, outputFile, opts) {
|
||||||
const pngBuf = fs.readFileSync(inputFile);
|
const ImageBuf = fs.readFileSync(inputFile);
|
||||||
const { data, mime } = await WTVPNG.pngToWebTV(pngBuf, {
|
const { data, mime } = await WTVImage.ImageToWebTV(ImageBuf, {
|
||||||
type: opts.type || 'ALP',
|
type: opts.type || 'ALF',
|
||||||
colors: opts.colors || 256,
|
colors: opts.colors || 256,
|
||||||
jpegQuality: opts.quality || 85
|
jpegQuality: opts.quality || 85
|
||||||
});
|
});
|
||||||
@@ -118,35 +118,35 @@ async function cmdConvert(inputFile, outputFile, opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function cmdEncode(inputFile, outputFile, opts) {
|
async function cmdEncode(inputFile, outputFile, opts) {
|
||||||
const pngBuf = fs.readFileSync(inputFile);
|
const ImageBuf = fs.readFileSync(inputFile);
|
||||||
const gifBuf = await WTVPNG.pngToGIF(pngBuf, {
|
const gifBuf = await WTVImage.ImageToGIF(ImageBuf, {
|
||||||
type: opts.type || 'ALP',
|
type: opts.type || 'ALF',
|
||||||
colors: opts.colors || 256
|
colors: opts.colors || 256
|
||||||
});
|
});
|
||||||
|
|
||||||
const dest = resolveOutput(inputFile, '.gif', outputFile);
|
const dest = resolveOutput(inputFile, '.gif', outputFile);
|
||||||
fs.writeFileSync(dest, gifBuf);
|
fs.writeFileSync(dest, gifBuf);
|
||||||
const type = WTVPNG.detect(gifBuf);
|
const type = WTVImage.detect(gifBuf);
|
||||||
console.log(`[encode] ${inputFile} → ${dest} (Artemis ${type}, ${gifBuf.length} bytes)`);
|
console.log(`[encode] ${inputFile} → ${dest} (Artemis ${type}, ${gifBuf.length} bytes)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdDecode(inputFile, outputFile, opts) {
|
async function cmdDecode(inputFile, outputFile, opts) {
|
||||||
const gifBuf = fs.readFileSync(inputFile);
|
const gifBuf = fs.readFileSync(inputFile);
|
||||||
const type = WTVPNG.detect(gifBuf);
|
const type = WTVImage.detect(gifBuf);
|
||||||
if (!type) {
|
if (!type) {
|
||||||
console.error(`[decode] ${inputFile} does not contain an Artemis ALP/ALF block.`);
|
console.error(`[decode] ${inputFile} does not contain an Artemis ALP/ALF block.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pngBuf = await WTVPNG.gifToPNG(gifBuf);
|
const ImageBuf = await WTVImage.gifToPNG(gifBuf);
|
||||||
const dest = resolveOutput(inputFile, '.png', outputFile);
|
const dest = resolveOutput(inputFile, '.png', outputFile);
|
||||||
fs.writeFileSync(dest, pngBuf);
|
fs.writeFileSync(dest, ImageBuf);
|
||||||
console.log(`[decode] ${inputFile} (Artemis ${type}) → ${dest} (${pngBuf.length} bytes)`);
|
console.log(`[decode] ${inputFile} (Artemis ${type}) → ${dest} (${ImageBuf.length} bytes)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmdDetect(inputFile) {
|
function cmdDetect(inputFile) {
|
||||||
const buf = fs.readFileSync(inputFile);
|
const buf = fs.readFileSync(inputFile);
|
||||||
const type = WTVPNG.detect(buf);
|
const type = WTVImage.detect(buf);
|
||||||
if (type) {
|
if (type) {
|
||||||
console.log(`[detect] ${inputFile} → Artemis ${type}`);
|
console.log(`[detect] ${inputFile} → Artemis ${type}`);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user