From 14ab33026e08bda49e9586b15221b155672c97d3 Mon Sep 17 00:00:00 2001 From: zefie Date: Sun, 26 Apr 2026 15:23:34 -0400 Subject: [PATCH] update logos, and image decoder settings in config --- zefie_wtvp_minisrv/app.js | 39 +++- .../ServiceVault/SharedROMCache/minisrv.gif | Bin 4378 -> 5740 bytes .../SharedROMCache/splash_minisrv.gif | Bin 8153 -> 10881 bytes .../includes/classes/WTVImage.js | 23 +- zefie_wtvp_minisrv/includes/config.json | 22 +- zefie_wtvp_minisrv/wtv_img_converter.js | 207 ++++++++++++++++++ zefie_wtvp_minisrv/wtv_png_converter.js | 14 +- 7 files changed, 286 insertions(+), 19 deletions(-) create mode 100644 zefie_wtvp_minisrv/wtv_img_converter.js diff --git a/zefie_wtvp_minisrv/app.js b/zefie_wtvp_minisrv/app.js index 0a9be0e0..5d2008fa 100644 --- a/zefie_wtvp_minisrv/app.js +++ b/zefie_wtvp_minisrv/app.js @@ -1338,28 +1338,49 @@ async function sendToClient(socket, headers_obj, data = null) { delete headers_obj['minisrv-no-last-modified']; } - if (minisrv_config.config.decode_unsupported_images) { + if (minisrv_config.config.image_decoder && minisrv_config.config.image_decoder.enabled) { 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") { + if (minisrv_config.config.image_decoder.image_formats && minisrv_config.config.image_decoder.image_formats.includes(headers_obj[contype_key].toLowerCase())) { const convertOpts = { - jpegQuality: minisrv_config.config.decode_unsupported_images_quality, + jpegQuality: minisrv_config.config.image_decoder.jpg_quality, type: 'ALF' }; + + 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; + const sourceData = Buffer.isBuffer(data) ? data : Buffer.from(data); 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'; + var i=0; + while (content_length > minisrv_config.config.image_decoder.max_size && converted.mime === 'image/jpeg') { + // Image is too big, try to reduce quality + if (i < minisrv_config.config.image_decoder.max_quality_tries) { + convertOpts.jpegQuality -= minisrv_config.config.image_decoder.jpeg_interval; + var converted2 = await WTVImage.ImageToWebTV(sourceData, convertOpts); + data = converted2.data; + content_length = data.length; + i++; + } else { + break; + } + } + if (content_length > minisrv_config.config.image_decoder.max_size) { + headers_obj = { + "Status": `400 ${minisrv_config.config.service_name} ran into a technical problem. (Image too large)`, + "Content-type": "text/html" + } + data = ""; + } else { + 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)`, + "Status": `400 ${minisrv_config.config.service_name} ran into a technical problem. (Image not supported by backend, it may be corrupt)`, "Content-type": "text/html" } data = ""; diff --git a/zefie_wtvp_minisrv/includes/ServiceVault/SharedROMCache/minisrv.gif b/zefie_wtvp_minisrv/includes/ServiceVault/SharedROMCache/minisrv.gif index 275e5c843642c364ba5fbf200b73ff08c7ce08c2..11aecf4ec7f8f0727a0ed9f9e95f2be13d0f1d5a 100644 GIT binary patch literal 5740 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 literal 4378 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 Uuot_r54*7*`>`O~6afJMJ9ON$LI3~& diff --git a/zefie_wtvp_minisrv/includes/ServiceVault/SharedROMCache/splash_minisrv.gif b/zefie_wtvp_minisrv/includes/ServiceVault/SharedROMCache/splash_minisrv.gif index a1e58b2de7a2495ddee36c48bbb8f83b15823b2d..748b41f2b5122b3945b226ca7b47369df0e1bcd4 100644 GIT binary patch literal 10881 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 literal 8153 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 */ async ImageToWebTV(input, opts = {}) { - const 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 maxHeight = Number(opts.maxHeight) > 0 ? Number(opts.maxHeight) : null; + if (maxWidth || maxHeight) { + const resizeOpts = { fit: 'inside', withoutEnlargement: true }; + if (maxWidth) resizeOpts.width = maxWidth; + if (maxHeight) resizeOpts.height = maxHeight; + pngBuf = await sharp(pngBuf) + .resize(resizeOpts) + .png() + .toBuffer(); + } const meta = await sharp(pngBuf).metadata(); let usesAlpha = false; @@ -989,8 +1004,10 @@ class WTVImage { if (this.isPalettePNG(pngBuf)) { // Palette/indexed PNGs should preserve palette + tRNS alpha exactly by default. - // Allow forcing re-quantization only when explicitly requested. - const data = opts.forceRequantizePalette + // If resizing was applied, the palette is no longer preserved and we must + // re-quantize the image before producing an Artemis GIF. + const forceRequantize = opts.forceRequantizePalette || maxWidth || maxHeight; + const data = forceRequantize ? await this.encodeArtemisGIF(pngBuf, opts) : await this.paletteImageToArtemisGIF(pngBuf, opts); return { data, mime: 'image/gif' }; diff --git a/zefie_wtvp_minisrv/includes/config.json b/zefie_wtvp_minisrv/includes/config.json index 9520c311..f33603b2 100644 --- a/zefie_wtvp_minisrv/includes/config.json +++ b/zefie_wtvp_minisrv/includes/config.json @@ -22,8 +22,6 @@ "cgi_enabled": false, // Disable CGI by default "php_enabled": false, // Disable PHP by default "php_binpath": "php-cgi", - "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. @@ -112,7 +110,23 @@ Each level of shenanigans includes the previous level (eg 5 will also disable filters like 4) See WTVShenanigans.js for more info. */ - "shenanigans": false + "shenanigans": false, + "image_decoder": { + "enabled": true, + "jpg_quality": 75, + "image_formats": [ + "image/png", + "image/svg+xml", + "image/avif", + "image/tiff", + "image/webp" + ], + "max_height": 0, + "max_width": 640, + "max_file_size": 524288, + "jpeg_interval": 5, // lower quality by this amount to try to lower filesize + "max_quality_tries": 5 // try to decode up to this many times, reducing quality each time, until the file is under the max_file_size. After this many tries, it will error out rather than sending an oversized file to the client. + } }, "services": { // service definitions @@ -218,7 +232,7 @@ "flags": "0x00000001", "privileged": true, "send_tellyscripts": true, // Best left untouched - "send_tellyscript_to_mame": false, + "send_tellyscript_to_mame": true, "dialin_number": 18006138199, "dns1ip": "10.0.0.50", "dns2ip": "8.8.8.8", diff --git a/zefie_wtvp_minisrv/wtv_img_converter.js b/zefie_wtvp_minisrv/wtv_img_converter.js new file mode 100644 index 00000000..e2e2336b --- /dev/null +++ b/zefie_wtvp_minisrv/wtv_img_converter.js @@ -0,0 +1,207 @@ +#!/usr/bin/env node +/** + * wtv_png_converter.js - WebTV PNG/GIF conversion CLI + * + * Usage: + * node wtv_png_converter.js [options] [output] + * + * Commands: + * convert Convert a PNG to the best WebTV format (auto: JPEG or Artemis GIF) + * encode Convert a PNG with alpha to an Artemis ALP/ALF GIF + * decode Convert a WebTV Artemis ALP/ALF GIF back to a PNG + * detect Report whether a GIF contains an Artemis ALP/ALF block + * + * Options: + * --type Artemis variant to use for encoding (default: ALP) + * --colors Palette size for full-color quantization (default: 256) + * --quality JPEG quality when output is JPEG (default: 85) + * --output, -o Output file path (alternative to positional argument) + * --help, -h Show this help + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const WTVImage = require('./includes/classes/WTVImage'); + +// --------------------------------------------------------------------------- +// Argument parser +// --------------------------------------------------------------------------- +function parseArgs(argv) { + const args = { options: {}, positional: [] }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--help' || a === '-h') { + args.options.help = true; + } else if (a === '--type') { + args.options.type = argv[++i]; + } else if (a === '--colors') { + args.options.colors = parseInt(argv[++i], 10); + } else if (a === '--quality') { + args.options.quality = parseInt(argv[++i], 10); + } else if (a === '--max-width') { + args.options.maxWidth = parseInt(argv[++i], 10); + } else if (a === '--max-height') { + args.options.maxHeight = parseInt(argv[++i], 10); + } else if (a === '--output' || a === '-o') { + args.options.output = argv[++i]; + } else if (a.startsWith('--')) { + console.error(`Unknown option: ${a}`); + process.exit(1); + } else { + args.positional.push(a); + } + } + return args; +} + +function printHelp() { + console.log(` +WebTV Image Converter +======================= +Usage: node wtv_img_converter.js [options] [output] + +Commands: + 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) + + encode Convert a PNG with alpha to an Artemis ALP or ALF GIF + (throws if the PNG has no alpha channel) + + decode Convert a WebTV Artemis ALP/ALF GIF back to a standard RGBA PNG + + detect Report whether a file is an Artemis ALP/ALF GIF (no output file needed) + +Options: + --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) + --max-width Maximum width to scale input before encoding + --max-height Maximum height to scale input before encoding + --output, -o Output file path + --help, -h Show this help + +Examples: + node wtv_png_converter.js convert logo.png + node wtv_png_converter.js convert logo.png logo_wtv.gif --type ALF --colors 128 + node wtv_png_converter.js encode icon.png icon.gif --type ALP + node wtv_png_converter.js decode artemis.gif result.png + node wtv_png_converter.js detect artemis.gif +`.trim()); +} + +// --------------------------------------------------------------------------- +// Output path helpers +// --------------------------------------------------------------------------- +function resolveOutput(inputFile, suggestedExt, override) { + if (override) return override; + const base = path.join( + path.dirname(inputFile), + path.basename(inputFile, path.extname(inputFile)) + ); + return base + suggestedExt; +} + +// --------------------------------------------------------------------------- +// Commands +// --------------------------------------------------------------------------- +async function cmdConvert(inputFile, outputFile, opts) { + const ImageBuf = fs.readFileSync(inputFile); + const { data, mime } = await WTVImage.ImageToWebTV(ImageBuf, { + type: opts.type || 'ALF', + colors: opts.colors || 256, + jpegQuality: opts.quality || 85, + maxWidth: opts.maxWidth, + maxHeight: opts.maxHeight + }); + + const ext = mime === 'image/gif' ? '.gif' : '.jpg'; + const dest = resolveOutput(inputFile, ext, outputFile); + fs.writeFileSync(dest, data); + console.log(`[convert] ${inputFile} → ${dest} (${mime}, ${data.length} bytes)`); +} + +async function cmdEncode(inputFile, outputFile, opts) { + 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 = 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 = WTVImage.detect(gifBuf); + if (!type) { + console.error(`[decode] ${inputFile} does not contain an Artemis ALP/ALF block.`); + process.exit(1); + } + + const ImageBuf = await WTVImage.gifToPNG(gifBuf); + const dest = resolveOutput(inputFile, '.png', outputFile); + fs.writeFileSync(dest, ImageBuf); + console.log(`[decode] ${inputFile} (Artemis ${type}) → ${dest} (${ImageBuf.length} bytes)`); +} + +function cmdDetect(inputFile) { + const buf = fs.readFileSync(inputFile); + const type = WTVImage.detect(buf); + if (type) { + console.log(`[detect] ${inputFile} → Artemis ${type}`); + } else { + console.log(`[detect] ${inputFile} → Not an Artemis ALP/ALF GIF`); + } +} + +// --------------------------------------------------------------------------- +// Entry point +// --------------------------------------------------------------------------- +(async () => { + const raw = process.argv.slice(2); + const args = parseArgs(raw); + + if (args.options.help || args.positional.length === 0) { + printHelp(); + process.exit(0); + } + + const command = args.positional[0]; + const inputFile = args.positional[1]; + const outputFile = args.options.output || args.positional[2] || null; + + if (!inputFile) { + console.error('Error: no input file specified.'); + printHelp(); + process.exit(1); + } + + if (!fs.existsSync(inputFile)) { + console.error(`Error: input file not found: ${inputFile}`); + process.exit(1); + } + + try { + switch (command) { + case 'convert': await cmdConvert(inputFile, outputFile, args.options); break; + case 'encode': await cmdEncode(inputFile, outputFile, args.options); break; + case 'decode': await cmdDecode(inputFile, outputFile, args.options); break; + case 'detect': cmdDetect(inputFile); break; + default: + console.error(`Unknown command: ${command}`); + printHelp(); + process.exit(1); + } + } catch (err) { + console.error(`Error: ${err.message}`); + process.exit(1); + } +})(); diff --git a/zefie_wtvp_minisrv/wtv_png_converter.js b/zefie_wtvp_minisrv/wtv_png_converter.js index 989aad76..e2e2336b 100644 --- a/zefie_wtvp_minisrv/wtv_png_converter.js +++ b/zefie_wtvp_minisrv/wtv_png_converter.js @@ -41,6 +41,10 @@ function parseArgs(argv) { args.options.colors = parseInt(argv[++i], 10); } else if (a === '--quality') { args.options.quality = parseInt(argv[++i], 10); + } else if (a === '--max-width') { + args.options.maxWidth = parseInt(argv[++i], 10); + } else if (a === '--max-height') { + args.options.maxHeight = parseInt(argv[++i], 10); } else if (a === '--output' || a === '-o') { args.options.output = argv[++i]; } else if (a.startsWith('--')) { @@ -76,6 +80,8 @@ Options: --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) + --max-width Maximum width to scale input before encoding + --max-height Maximum height to scale input before encoding --output, -o Output file path --help, -h Show this help @@ -106,9 +112,11 @@ function resolveOutput(inputFile, suggestedExt, override) { async function cmdConvert(inputFile, outputFile, opts) { const ImageBuf = fs.readFileSync(inputFile); const { data, mime } = await WTVImage.ImageToWebTV(ImageBuf, { - type: opts.type || 'ALF', - colors: opts.colors || 256, - jpegQuality: opts.quality || 85 + type: opts.type || 'ALF', + colors: opts.colors || 256, + jpegQuality: opts.quality || 85, + maxWidth: opts.maxWidth, + maxHeight: opts.maxHeight }); const ext = mime === 'image/gif' ? '.gif' : '.jpg';