feat(project): optimize image imports and converter
- Optimize imported scene preview images (smart WebP/JPEG/PNG, preserve alpha, keep pixel size) - Update converter to re-encode existing image assets with same algorithm - Improve import/export progress overlay and reduce presentation slide stutter Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { execFile } from 'node:child_process';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
import ffmpegStatic from 'ffmpeg-static';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/** Longest edge; presentation uses the original asset. */
|
||||
export const SCENE_PREVIEW_THUMB_MAX_PX = 320;
|
||||
|
||||
/**
|
||||
* Builds a small WebP still for graph/list previews. Returns null if generation fails (import still succeeds).
|
||||
*/
|
||||
export async function generateScenePreviewThumbnailBytes(
|
||||
sourceAbsPath: string,
|
||||
kind: 'image' | 'video',
|
||||
): Promise<Buffer | null> {
|
||||
try {
|
||||
if (kind === 'image') {
|
||||
return await sharp(sourceAbsPath)
|
||||
.rotate()
|
||||
.resize(SCENE_PREVIEW_THUMB_MAX_PX, SCENE_PREVIEW_THUMB_MAX_PX, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.webp({ quality: 82 })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
const ffmpegPath = ffmpegStatic;
|
||||
if (!ffmpegPath) return null;
|
||||
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dnd-thumb-'));
|
||||
const tmpPng = path.join(tmpDir, 'frame.png');
|
||||
try {
|
||||
const seekSeconds = ['0.5', '0.25', '0'];
|
||||
let extracted = false;
|
||||
for (const ss of seekSeconds) {
|
||||
await fs.rm(tmpPng, { force: true }).catch(() => undefined);
|
||||
try {
|
||||
await execFileAsync(
|
||||
ffmpegPath,
|
||||
[
|
||||
'-hide_banner',
|
||||
'-loglevel',
|
||||
'error',
|
||||
'-y',
|
||||
'-ss',
|
||||
ss,
|
||||
'-i',
|
||||
sourceAbsPath,
|
||||
'-frames:v',
|
||||
'1',
|
||||
tmpPng,
|
||||
],
|
||||
{ maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const st = await fs.stat(tmpPng).catch(() => null);
|
||||
if (st !== null && st.isFile() && st.size > 0) {
|
||||
extracted = true;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
/* try next seek */
|
||||
}
|
||||
}
|
||||
if (!extracted) return null;
|
||||
|
||||
return await sharp(tmpPng)
|
||||
.resize(SCENE_PREVIEW_THUMB_MAX_PX, SCENE_PREVIEW_THUMB_MAX_PX, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.webp({ quality: 82 })
|
||||
.toBuffer();
|
||||
} finally {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user