Files
koring-launcher/electron/core/background-image.ts
T
dream_pep cd4578de5a feat: 全新 Korom 资源管理服务组件,优化内存占用
新增完整的程序本体资源管理与内存优化功能:
1. 新增背景图预处理服务,主进程将大图降采样至屏幕适配尺寸,避免全分辨率大图占用渲染进程大量内存
2. 实现资源注册表系统,支持引用计数、LRU自动逐出与释放回调
3. 新增资源与内存调试面板,可查看渲染进程JS堆、Electron进程内存与资源缓存状态
4. 修复进程内存信息API的单位与字段映射错误
5. 优化背景层缓存键避免过长dataURL,为Silk动画添加窗口隐藏时停帧逻辑减少GPU占用
6. 修复资源注册表类型适配问题并更新项目文档
2026-09-04 18:56:43 +08:00

110 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 背景图处理服务(主进程,程序本体资源管理)。
*
* 目标:把用户自选的大图背景在进入渲染进程前,降采样到
* 「窗口实际需要」的尺寸(长边按 maxEdge 限制),从而避免
* 数 MB~数十 MB 的原图以 base64 + 全分辨率解码的形式常驻内存,
* 且不改变可见显示效果(超出屏幕物理像素的部分在视觉上不可见)。
*
* 规则:
* - 长边 ≤ maxEdge → 原样返回(零损耗,效果 100% 一致);
* - 长边 > maxEdge → 等比 resize 后重编码(JPEG 有损 q0.9 / 带透明通道用 PNG 无损);
* - 动画 GIF / 解析失败 / 无法解码 → 返回 null 或原样,由调用方回退到原始文件(不改变现有行为)。
*/
import electron from 'electron';
import * as fs from 'fs';
import * as path from 'path';
const { nativeImage } = electron;
export interface PreparedBackground {
dataUrl: string | null;
bytes: number;
width: number;
height: number;
optimized: boolean;
}
const MIME_MAP: Record<string, string> = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.webp': 'image/webp',
'.gif': 'image/gif',
'.bmp': 'image/bmp',
};
const TRANSPARENT_MIMES = new Set(['image/png', 'image/webp', 'image/gif']);
function bufferToDataUrl(buffer: Buffer, mime: string): string {
return `data:${mime};base64,${buffer.toString('base64')}`;
}
/**
* 解析并(按需)优化背景图片,返回可直接给 CSS 使用的 data URL。
* @param srcPath 原图文件路径
* @param maxEdge 目标长边上限(像素),默认 4096
*/
export function prepareBackgroundImage(srcPath: string, maxEdge = 4096): PreparedBackground | null {
const raw: PreparedBackground = { dataUrl: null, bytes: 0, width: 0, height: 0, optimized: false };
try {
const buffer = fs.readFileSync(srcPath);
const ext = path.extname(srcPath).toLowerCase();
const mime = MIME_MAP[ext] || 'image/png';
// 动画 GIFnativeImage 只能解码首帧,直接原样返回,避免破坏动画
if (mime === 'image/gif') {
raw.dataUrl = bufferToDataUrl(buffer, mime);
raw.bytes = buffer.length;
return raw;
}
const image = nativeImage.createFromBuffer(buffer);
if (image.isEmpty()) return null;
const size = image.getSize();
const longEdge = Math.max(size.width, size.height);
const needResize = longEdge > maxEdge && size.width > 0 && size.height > 0;
// 仅当确实需要降尺寸时才做重编码(视觉零影响的边界:超出屏幕物理像素的部分不可见);
// 未超限但体积大的图原样返回,避免任何有损重编码改变显示效果。
if (!needResize) {
raw.dataUrl = bufferToDataUrl(buffer, mime);
raw.bytes = buffer.length;
raw.width = size.width;
raw.height = size.height;
return raw;
}
let output = image;
const scale = maxEdge / longEdge;
const w = Math.max(1, Math.round(size.width * scale));
const h = Math.max(1, Math.round(size.height * scale));
output = image.resize({ width: w, height: h, quality: 'best' });
if (output.isEmpty()) return null;
const outSize = output.getSize();
const hasTransparency = TRANSPARENT_MIMES.has(mime);
let outBuffer: Buffer;
let outMime: string;
if (hasTransparency) {
outBuffer = output.toPNG();
outMime = 'image/png';
} else {
outBuffer = output.toJPEG(90);
outMime = 'image/jpeg';
}
if (outBuffer.length === 0) return null;
raw.dataUrl = bufferToDataUrl(outBuffer, outMime);
raw.bytes = outBuffer.length;
raw.width = outSize.width;
raw.height = outSize.height;
raw.optimized = true;
return raw;
} catch {
return null;
}
}