Files
DndGamePlayer/app/main/project/optimizeImageImport.lib.mjs
T
Ivan Fontosh c9cad4dafd chore: move project-converter out of repo to dnd_project sibling
Remove tools/project-converter; converter now lives next to this repo under
dnd_project/project-converter. Update eslint ignore comment and optimizeImage
lib header. Converter main.js imports dnd_player via ../../dnd_player/...

Made-with: Cursor
2026-04-24 07:10:01 +08:00

219 lines
5.4 KiB
JavaScript

/**
* Visually lossless re-encode for imported raster images (same pixel dimensions).
* Node-only; shared by the main app and ../project-converter (monorepo sibling).
*/
import sharp from 'sharp';
/** @typedef {import('node:buffer').Buffer} Buffer */
/**
* @typedef {Object} OptimizeImageImportResult
* @property {Buffer} buffer
* @property {string} mime
* @property {string} ext filename extension without dot (e.g. webp, jpg, png)
* @property {number} width
* @property {number} height
* @property {boolean} passthrough true if original bytes were kept
*/
const WEBP_EFFORT = 6;
const RASTER_QUALITY = 95;
/**
* @param {Buffer} buf
* @param {import('sharp').Metadata | null} meta
* @returns {OptimizeImageImportResult}
*/
function makePassthrough(buf, meta) {
const fmt = meta?.format;
if (fmt === 'jpeg' || fmt === 'jpg') {
return {
buffer: buf,
mime: 'image/jpeg',
ext: 'jpg',
width: meta?.width ?? 0,
height: meta?.height ?? 0,
passthrough: true,
};
}
if (fmt === 'png') {
return {
buffer: buf,
mime: 'image/png',
ext: 'png',
width: meta?.width ?? 0,
height: meta?.height ?? 0,
passthrough: true,
};
}
if (fmt === 'webp') {
return {
buffer: buf,
mime: 'image/webp',
ext: 'webp',
width: meta?.width ?? 0,
height: meta?.height ?? 0,
passthrough: true,
};
}
if (fmt === 'gif') {
return {
buffer: buf,
mime: 'image/gif',
ext: 'gif',
width: meta?.width ?? 0,
height: meta?.height ?? 0,
passthrough: true,
};
}
if (fmt === 'tiff' || fmt === 'tif') {
return {
buffer: buf,
mime: 'image/tiff',
ext: 'tiff',
width: meta?.width ?? 0,
height: meta?.height ?? 0,
passthrough: true,
};
}
if (fmt === 'bmp') {
return {
buffer: buf,
mime: 'image/bmp',
ext: 'bmp',
width: meta?.width ?? 0,
height: meta?.height ?? 0,
passthrough: true,
};
}
return {
buffer: buf,
mime: 'application/octet-stream',
ext: 'bin',
width: meta?.width ?? 0,
height: meta?.height ?? 0,
passthrough: true,
};
}
/**
* @param {Buffer} outBuf
* @param {number} w0
* @param {number} h0
*/
async function sameDimensionsOrThrow(outBuf, w0, h0) {
const m = await sharp(outBuf).metadata();
if ((m.width ?? 0) !== w0 || (m.height ?? 0) !== h0) {
const err = new Error('encode changed dimensions');
err.code = 'DIM';
throw err;
}
}
/**
* @param {Buffer} src
* @returns {Promise<OptimizeImageImportResult>}
*/
export async function optimizeImageBufferVisuallyLossless(src) {
const input = Buffer.isBuffer(src) ? src : Buffer.from(src);
if (input.length === 0) {
return makePassthrough(input, { width: 0, height: 0, format: 'png' });
}
let meta0;
try {
meta0 = await sharp(input, { failOn: 'error', unlimited: true }).metadata();
} catch {
return makePassthrough(input, null);
}
const width = meta0.width ?? 0;
const height = meta0.height ?? 0;
if (width <= 0 || height <= 0) {
return makePassthrough(input, meta0);
}
const pages = meta0.pages ?? 1;
if (pages > 1) {
return makePassthrough(input, meta0);
}
const rawFmt = meta0.format;
if (rawFmt === 'svg' || rawFmt === 'pdf' || rawFmt === 'heif' || rawFmt === 'jxl' || rawFmt === 'vips') {
return makePassthrough(input, meta0);
}
const hasAlpha = meta0.hasAlpha === true;
try {
if (hasAlpha) {
const webpLo = await sharp(input)
.rotate()
.ensureAlpha()
.webp({ lossless: true, effort: WEBP_EFFORT })
.toBuffer();
const pngOut = await sharp(input)
.rotate()
.ensureAlpha()
.png({ compressionLevel: 9, adaptiveFiltering: true })
.toBuffer();
await sameDimensionsOrThrow(webpLo, width, height);
await sameDimensionsOrThrow(pngOut, width, height);
const pickWebp = webpLo.length <= pngOut.length;
const outBuf = pickWebp ? webpLo : pngOut;
if (outBuf.length >= input.length) {
return makePassthrough(input, meta0);
}
return {
buffer: outBuf,
mime: pickWebp ? 'image/webp' : 'image/png',
ext: pickWebp ? 'webp' : 'png',
width,
height,
passthrough: false,
};
}
const webpColor = await sharp(input)
.rotate()
.webp({
quality: RASTER_QUALITY,
nearLossless: true,
effort: WEBP_EFFORT,
smartSubsample: false,
})
.toBuffer();
const jpegColor = await sharp(input)
.rotate()
.jpeg({
quality: RASTER_QUALITY,
chromaSubsampling: '4:4:4',
mozjpeg: true,
optimizeScans: true,
})
.toBuffer();
await sameDimensionsOrThrow(webpColor, width, height);
await sameDimensionsOrThrow(jpegColor, width, height);
const useWebp = webpColor.length <= jpegColor.length;
const outBuf = useWebp ? webpColor : jpegColor;
if (outBuf.length >= input.length) {
return makePassthrough(input, meta0);
}
return {
buffer: outBuf,
mime: useWebp ? 'image/webp' : 'image/jpeg',
ext: useWebp ? 'webp' : 'jpg',
width,
height,
passthrough: false,
};
} catch (e) {
if (e && typeof e === 'object' && /** @type {{ code?: string }} */ (e).code === 'DIM') {
return makePassthrough(input, meta0);
}
return makePassthrough(input, meta0);
}
}