WTVPNG
This commit is contained in:
BIN
zefie_wtvp_minisrv/WebTVLogoJewel.png
Normal file
BIN
zefie_wtvp_minisrv/WebTVLogoJewel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
@@ -26,6 +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 vm = require('vm');
|
||||
const debug = require('debug')('app');
|
||||
const express = require('express');
|
||||
@@ -1337,6 +1338,22 @@ async function sendToClient(socket, headers_obj, data = null) {
|
||||
delete headers_obj['minisrv-no-last-modified'];
|
||||
}
|
||||
|
||||
if (minisrv_config.config.decode_png) {
|
||||
const contype_key = wtvshared.getCaseInsensitiveKey('content-type', headers_obj);
|
||||
if (contype_key) {
|
||||
if (headers_obj[contype_key].toLowerCase() === "image/png") {
|
||||
const convertOpts = {
|
||||
jpegQuality: minisrv_config.config.decode_png_jpeg_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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// if client can do compression, see if its worth enabling
|
||||
@@ -2315,6 +2332,12 @@ if (minisrv_config.config.user_accounts.max_users_per_account > 99) {
|
||||
if (minisrv_config.config.shenanigans) console.log(" * WARNING: Shenanigans level", minisrv_config.config.shenanigans, "enabled");
|
||||
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");
|
||||
|
||||
|
||||
|
||||
process.on('uncaughtException', function (err) {
|
||||
console.error((err && err.stack) ? err.stack : err);
|
||||
});
|
||||
|
||||
1094
zefie_wtvp_minisrv/includes/classes/WTVPNG.js
Normal file
1094
zefie_wtvp_minisrv/includes/classes/WTVPNG.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -22,6 +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.
|
||||
"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.
|
||||
|
||||
199
zefie_wtvp_minisrv/wtv_png_converter.js
Normal file
199
zefie_wtvp_minisrv/wtv_png_converter.js
Normal file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* wtv_png_converter.js - WebTV PNG/GIF conversion CLI
|
||||
*
|
||||
* Usage:
|
||||
* node wtv_png_converter.js <command> [options] <input> [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 <ALP|ALF> Artemis variant to use for encoding (default: ALP)
|
||||
* --colors <n> Palette size for full-color quantization (default: 256)
|
||||
* --quality <n> JPEG quality when output is JPEG (default: 85)
|
||||
* --output, -o <file> Output file path (alternative to positional argument)
|
||||
* --help, -h Show this help
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const WTVPNG = require('./includes/classes/WTVPNG');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 === '--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 PNG/GIF Converter
|
||||
=======================
|
||||
Usage: node wtv_png_converter.js <command> [options] <input> [output]
|
||||
|
||||
Commands:
|
||||
convert Convert a PNG 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 <ALP|ALF> Artemis variant for encoding/convert (default: ALP)
|
||||
--colors <n> Palette size for full-color quantization (default: 256)
|
||||
--quality <n> JPEG quality when output is JPEG (default: 85)
|
||||
--output, -o <file> 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 pngBuf = fs.readFileSync(inputFile);
|
||||
const { data, mime } = await WTVPNG.pngToWebTV(pngBuf, {
|
||||
type: opts.type || 'ALP',
|
||||
colors: opts.colors || 256,
|
||||
jpegQuality: opts.quality || 85
|
||||
});
|
||||
|
||||
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 pngBuf = fs.readFileSync(inputFile);
|
||||
const gifBuf = await WTVPNG.pngToGIF(pngBuf, {
|
||||
type: opts.type || 'ALP',
|
||||
colors: opts.colors || 256
|
||||
});
|
||||
|
||||
const dest = resolveOutput(inputFile, '.gif', outputFile);
|
||||
fs.writeFileSync(dest, gifBuf);
|
||||
const type = WTVPNG.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);
|
||||
if (!type) {
|
||||
console.error(`[decode] ${inputFile} does not contain an Artemis ALP/ALF block.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pngBuf = await WTVPNG.gifToPNG(gifBuf);
|
||||
const dest = resolveOutput(inputFile, '.png', outputFile);
|
||||
fs.writeFileSync(dest, pngBuf);
|
||||
console.log(`[decode] ${inputFile} (Artemis ${type}) → ${dest} (${pngBuf.length} bytes)`);
|
||||
}
|
||||
|
||||
function cmdDetect(inputFile) {
|
||||
const buf = fs.readFileSync(inputFile);
|
||||
const type = WTVPNG.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);
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user