/* * upscaler.cjs — make last-minute, low-resolution pictures look right on the stage TV. * * The problem: the pastor texts pictures minutes before a service. Texted and downloaded images are * typically ~1000px wide, but the portrait TV is 2160x3840 REAL pixels, so every picture gets * stretched about 2x and looks soft. There is no time to fix that by hand, and no file format or CSS * change can invent detail that was never in the file. * * So on upload we run Real-ESRGAN (ncnn/Vulkan) once, on the GPU, fully offline. Measured on the * stage machine itself (Intel UHD, 12th-gen i7, Vulkan 1.4), upscaling a 1000x595 picture: * * realesr-animevideov3 x4 -> 4000px 2.1 s <- chosen * realesrgan-x4plus-anime x4 -> 4000px 13 s * realesrgan-x4plus x4 -> 4000px 41 s * * animevideov3 is a compact model, but on both line art and photographs it sharpened edges as well * as the heavy models AND preserved film grain that the bigger ones smoothed into plastic — while * being the only one fast enough to live in an upload path. Scale is chosen to land near the panel * width instead of blindly 4x, so a 1080px image becomes exactly 2160px rather than a wasteful 4320. * * Safety: the untouched original is copied to an "_originals" folder, and the upscale only replaces * the visible file after it is written and verified bigger. Any failure — missing binary, no Vulkan, * timeout, odd format — leaves the uploaded file exactly as it was. This must never block a service. */ const fs = require('fs'); const path = require('path'); const { spawn } = require('child_process'); const TARGET_WIDTH = 2160; // the stage TV's real horizontal pixels const SKIP_ABOVE_WIDTH = 1800; // already close enough: do not burn time or risk artifacts const MODEL = 'realesr-animevideov3'; const TIMEOUT_MS = 120000; // one huge picture must not hang an upload forever function vendorDir() { const candidates = [ path.join(process.resourcesPath || '', 'app.asar.unpacked', 'vendor', 'upscale'), path.join(path.dirname(process.execPath || ''), 'resources', 'app.asar.unpacked', 'vendor', 'upscale'), path.join(__dirname, 'vendor', 'upscale') ]; for (const d of candidates) { try { if (d && fs.existsSync(path.join(d, 'realesrgan-ncnn-vulkan.exe'))) return d; } catch (_) {} } return null; } function isAvailable() { return !!vendorDir(); } // Dimensions read straight from the file header, so no image library is needed. function imageSize(file) { let b; try { b = fs.readFileSync(file); } catch (_) { return null; } try { if (b.length > 24 && b[0] === 0x89 && b[1] === 0x50) return { w: b.readUInt32BE(16), h: b.readUInt32BE(20) }; if (b[0] === 0x47 && b[1] === 0x49) return { w: b.readUInt16LE(6), h: b.readUInt16LE(8) }; if (b.slice(0, 4).toString() === 'RIFF' && b.slice(8, 12).toString() === 'WEBP') { const t = b.slice(12, 16).toString(); if (t === 'VP8X') return { w: (b.readUIntLE(24, 3) & 0xFFFFFF) + 1, h: (b.readUIntLE(27, 3) & 0xFFFFFF) + 1 }; if (t === 'VP8 ') return { w: b.readUInt16LE(26) & 0x3FFF, h: b.readUInt16LE(28) & 0x3FFF }; if (t === 'VP8L') { const n = b.readUInt32LE(21); return { w: (n & 0x3FFF) + 1, h: ((n >> 14) & 0x3FFF) + 1 }; } } if (b[0] === 0xFF && b[1] === 0xD8) { let i = 2; while (i < b.length - 9) { if (b[i] !== 0xFF) { i++; continue; } const m = b[i + 1]; if (m >= 0xC0 && m <= 0xCF && m !== 0xC4 && m !== 0xC8 && m !== 0xCC) { return { w: b.readUInt16BE(i + 7), h: b.readUInt16BE(i + 5) }; } i += 2 + b.readUInt16BE(i + 2); } } } catch (_) {} return null; } function pickScale(width) { const need = Math.ceil(TARGET_WIDTH / width); return Math.max(2, Math.min(4, need)); // animevideov3 ships x2 / x3 / x4 } /* * Upscale IN PLACE when the picture is too small for the TV. Always resolves with a short report and * never rejects: a failed enhancement must still leave a perfectly usable uploaded file. */ function enhanceIfSmall(filePath) { return new Promise((resolve) => { const done = (status, extra) => resolve(Object.assign({ status: status }, extra || {})); try { const ext = path.extname(filePath).toLowerCase(); // Vector art is already resolution-independent, and video is not ours to touch. if (!/^\.(png|jpe?g|jfif|webp|bmp)$/.test(ext)) return done('skipped-format'); const dir = vendorDir(); if (!dir) return done('unavailable'); const size = imageSize(filePath); if (!size || !size.w) return done('unreadable'); if (size.w >= SKIP_ABOVE_WIDTH) return done('already-sharp', { width: size.w }); const scale = pickScale(size.w); // Write to a temp file and swap it in only on success, so a crash mid-run can never leave a // half-written image where the operator expects their picture. const outExt = (ext === '.jpeg' || ext === '.jfif') ? '.jpg' : ext; const tmp = filePath + '.upscaling' + outExt; const exe = path.join(dir, 'realesrgan-ncnn-vulkan.exe'); const args = ['-i', filePath, '-o', tmp, '-n', MODEL, '-s', String(scale), '-f', outExt.replace('.', '')]; const t0 = Date.now(); let finished = false; const child = spawn(exe, args, { cwd: dir, windowsHide: true }); const killTimer = setTimeout(() => { try { child.kill(); } catch (_) {} }, TIMEOUT_MS); child.on('error', () => { if (finished) return; finished = true; clearTimeout(killTimer); try { fs.unlinkSync(tmp); } catch (_) {} done('spawn-failed'); }); child.on('close', () => { if (finished) return; finished = true; clearTimeout(killTimer); const ms = Date.now() - t0; let out = null; try { out = fs.existsSync(tmp) ? imageSize(tmp) : null; } catch (_) {} if (!out || !out.w || out.w <= size.w) { try { fs.unlinkSync(tmp); } catch (_) {} return done('failed', { ms: ms }); } try { // Keep the untouched original beside the content, in case it is ever wanted back or the // enhancement made an odd call on some picture. const backupDir = path.join(path.dirname(filePath), '_originals'); fs.mkdirSync(backupDir, { recursive: true }); const backup = path.join(backupDir, path.basename(filePath)); if (!fs.existsSync(backup)) fs.copyFileSync(filePath, backup); fs.renameSync(tmp, filePath); } catch (e) { try { fs.unlinkSync(tmp); } catch (_) {} return done('replace-failed', { ms: ms, error: e.message }); } done('enhanced', { from: size.w + 'x' + size.h, to: out.w + 'x' + out.h, scale: scale, ms: ms }); }); } catch (e) { done('error', { error: e.message }); } }); } module.exports = { isAvailable, imageSize, enhanceIfSmall, TARGET_WIDTH, SKIP_ABOVE_WIDTH, MODEL };