diff --git a/zefie_wtvp_minisrv/app.js b/zefie_wtvp_minisrv/app.js index 2c619113..0a9be0e0 100644 --- a/zefie_wtvp_minisrv/app.js +++ b/zefie_wtvp_minisrv/app.js @@ -26,7 +26,7 @@ const WTVClientCapabilities = require(classPath + "/WTVClientCapabilities.js"); const WTVClientSessionData = require(classPath + "/WTVClientSessionData.js"); const WTVMime = require(classPath + "/WTVMime.js"); const WTVFlashrom = require(classPath + "/WTVFlashrom.js"); -const WTVPNG = require(classPath + "/WTVPNG.js"); +const WTVImage = require(classPath + "/WTVImage.js"); const vm = require('vm'); const debug = require('debug')('app'); const express = require('express'); @@ -1338,21 +1338,32 @@ async function sendToClient(socket, headers_obj, data = null) { 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); if (contype_key) { if (headers_obj[contype_key].toLowerCase() === "image/png" || 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") { const convertOpts = { - jpegQuality: minisrv_config.config.decode_png_jpeg_quality, + jpegQuality: minisrv_config.config.decode_unsupported_images_quality, type: 'ALF' }; const sourceData = Buffer.isBuffer(data) ? data : Buffer.from(data); - const converted = await WTVPNG.pngToWebTV(sourceData, convertOpts); - data = converted.data; - content_length = data.length; - headers_obj[contype_key] = (converted.mime === 'image/jpeg') ? 'image/jpeg' : 'image/gif'; + try { + const converted = await WTVImage.ImageToWebTV(sourceData, convertOpts); + data = converted.data; + 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"); // PNG -if (minisrv_config.config.decode_png) console.log(" * PNG will be processed for WebTV clients"); -else console.log(" * PNG will not be processed, and sent to client as-is"); +if (minisrv_config.config.decode_png) console.log(" * WebTV Unsupported images will be processed and converted for WebTV clients"); +else console.log(" * WebTV Unsupported images will not be processed, and sent to client as-is"); diff --git a/zefie_wtvp_minisrv/includes/classes/WTVPNG.js b/zefie_wtvp_minisrv/includes/classes/WTVImage.js similarity index 97% rename from zefie_wtvp_minisrv/includes/classes/WTVPNG.js rename to zefie_wtvp_minisrv/includes/classes/WTVImage.js index effb4656..17e5fb2e 100644 --- a/zefie_wtvp_minisrv/includes/classes/WTVPNG.js +++ b/zefie_wtvp_minisrv/includes/classes/WTVImage.js @@ -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 * sharp-compatible sources) to the appropriate WebTV format: @@ -32,7 +32,7 @@ const zlib = require('zlib'); // Class wrapper // --------------------------------------------------------------------------- -class WTVPNG { +class WTVImage { // --------------------------------------------------------------------------- // Low-level GIF 89a helpers // --------------------------------------------------------------------------- @@ -873,7 +873,7 @@ class WTVPNG { * @param {'ALP'|'ALF'} [opts.type='ALP'] * @returns {Promise} */ - async palettePNGToArtemisGIF(pngBuf, opts = {}) { + async paletteImageToArtemisGIF(pngBuf, opts = {}) { const type = opts.type || 'ALP'; 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 * @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 meta = await sharp(pngBuf).metadata(); let usesAlpha = false; @@ -992,7 +992,7 @@ class WTVPNG { // Allow forcing re-quantization only when explicitly requested. const data = opts.forceRequantizePalette ? await this.encodeArtemisGIF(pngBuf, opts) - : await this.palettePNGToArtemisGIF(pngBuf, opts); + : await this.paletteImageToArtemisGIF(pngBuf, opts); return { data, mime: 'image/gif' }; } @@ -1001,9 +1001,9 @@ class WTVPNG { return { data, mime: 'image/gif' }; } - async pngToArtemisGIF(input, opts = {}) { - const result = await this.pngToWebTV(input, opts); - if (result.mime !== 'image/gif') throw new Error('Input PNG has no alpha; cannot encode as Artemis GIF. Use pngToWebTV() instead.'); + async ImageToArtemisGIF(input, opts = {}) { + const result = await this.ImageToWebTV(input, opts); + if (result.mime !== 'image/gif') throw new Error('Input image has no alpha; cannot encode as Artemis GIF. Use ImageToWebTV() instead.'); return result.data; } @@ -1031,7 +1031,7 @@ class WTVPNG { * @returns {'ALP'|'ALF'|null} */ 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 }>} */ static decode(gifBuf) { - return WTVPNG._impl.decodeArtemisGIF(gifBuf); + return WTVImage._impl.decodeArtemisGIF(gifBuf); } /** @@ -1053,21 +1053,21 @@ class WTVPNG { * @returns {Promise} */ 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 {object} [opts] * @returns {Promise<{ data: Buffer, mime: string }>} */ - static pngToWebTV(input, opts = {}) { - return WTVPNG._impl.pngToWebTV(input, opts); + static ImageToWebTV(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. * @param {string|Buffer} input * @param {object} [opts] @@ -1075,8 +1075,8 @@ class WTVPNG { * @param {'ALP'|'ALF'} [opts.type='ALP'] * @returns {Promise} */ - static pngToGIF(input, opts = {}) { - return WTVPNG._impl.pngToArtemisGIF(input, opts); + static ImageToGIF(input, opts = {}) { + return WTVImage._impl.ImageToArtemisGIF(input, opts); } /** @@ -1085,10 +1085,10 @@ class WTVPNG { * @returns {Promise} */ 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; diff --git a/zefie_wtvp_minisrv/includes/config.json b/zefie_wtvp_minisrv/includes/config.json index 09a3e73b..9520c311 100644 --- a/zefie_wtvp_minisrv/includes/config.json +++ b/zefie_wtvp_minisrv/includes/config.json @@ -22,8 +22,8 @@ "cgi_enabled": false, // Disable CGI by default "php_enabled": false, // Disable PHP by default "php_binpath": "php-cgi", - "decode_png": true, // Attempt to decode PNG into JPG/ALP/ALF/GIF - "decode_png_jpeg_quality": 75, // JPEG quality for decoded PNGs, 0-100 lower is worse quality but smaller files. + "decode_unsupported_images": true, // Attempt to decode images WebTV doesn't support into JPG/ALF/GIF + "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. "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. diff --git a/zefie_wtvp_minisrv/wtv_png_converter.js b/zefie_wtvp_minisrv/wtv_png_converter.js index 49586d8f..989aad76 100644 --- a/zefie_wtvp_minisrv/wtv_png_converter.js +++ b/zefie_wtvp_minisrv/wtv_png_converter.js @@ -24,7 +24,7 @@ const fs = require('fs'); const path = require('path'); -const WTVPNG = require('./includes/classes/WTVPNG'); +const WTVImage = require('./includes/classes/WTVImage'); // --------------------------------------------------------------------------- // Argument parser @@ -55,12 +55,12 @@ function parseArgs(argv) { function printHelp() { console.log(` -WebTV PNG/GIF Converter +WebTV Image Converter ======================= -Usage: node wtv_png_converter.js [options] [output] +Usage: node wtv_img_converter.js [options] [output] Commands: - convert Convert a PNG to the best WebTV format + convert Convert an image to the best WebTV format - No alpha → JPEG - Palette PNG → Artemis GIF (palette 1:1, no requantization) - 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) Options: - --type Artemis variant for encoding/convert (default: ALP) + --type Artemis variant for encoding/convert (default: ALF) --colors Palette size for full-color quantization (default: 256) --quality JPEG quality when output is JPEG (default: 85) --output, -o Output file path @@ -104,9 +104,9 @@ function resolveOutput(inputFile, suggestedExt, override) { // Commands // --------------------------------------------------------------------------- async function cmdConvert(inputFile, outputFile, opts) { - const pngBuf = fs.readFileSync(inputFile); - const { data, mime } = await WTVPNG.pngToWebTV(pngBuf, { - type: opts.type || 'ALP', + const ImageBuf = fs.readFileSync(inputFile); + const { data, mime } = await WTVImage.ImageToWebTV(ImageBuf, { + type: opts.type || 'ALF', colors: opts.colors || 256, jpegQuality: opts.quality || 85 }); @@ -118,35 +118,35 @@ async function cmdConvert(inputFile, outputFile, opts) { } async function cmdEncode(inputFile, outputFile, opts) { - const pngBuf = fs.readFileSync(inputFile); - const gifBuf = await WTVPNG.pngToGIF(pngBuf, { - type: opts.type || 'ALP', + const ImageBuf = fs.readFileSync(inputFile); + const gifBuf = await WTVImage.ImageToGIF(ImageBuf, { + type: opts.type || 'ALF', colors: opts.colors || 256 }); const dest = resolveOutput(inputFile, '.gif', outputFile); fs.writeFileSync(dest, gifBuf); - const type = WTVPNG.detect(gifBuf); + const type = WTVImage.detect(gifBuf); console.log(`[encode] ${inputFile} → ${dest} (Artemis ${type}, ${gifBuf.length} bytes)`); } async function cmdDecode(inputFile, outputFile, opts) { const gifBuf = fs.readFileSync(inputFile); - const type = WTVPNG.detect(gifBuf); + const type = WTVImage.detect(gifBuf); if (!type) { console.error(`[decode] ${inputFile} does not contain an Artemis ALP/ALF block.`); process.exit(1); } - const pngBuf = await WTVPNG.gifToPNG(gifBuf); + const ImageBuf = await WTVImage.gifToPNG(gifBuf); const dest = resolveOutput(inputFile, '.png', outputFile); - fs.writeFileSync(dest, pngBuf); - console.log(`[decode] ${inputFile} (Artemis ${type}) → ${dest} (${pngBuf.length} bytes)`); + fs.writeFileSync(dest, ImageBuf); + console.log(`[decode] ${inputFile} (Artemis ${type}) → ${dest} (${ImageBuf.length} bytes)`); } function cmdDetect(inputFile) { const buf = fs.readFileSync(inputFile); - const type = WTVPNG.detect(buf); + const type = WTVImage.detect(buf); if (type) { console.log(`[detect] ${inputFile} → Artemis ${type}`); } else {