Files
memer/backend/src/services/image.ts

120 lines
3.1 KiB
TypeScript
Raw Normal View History

2026-03-28 01:06:30 -05:00
import sharp from 'sharp';
import fs from 'fs';
2026-03-28 09:30:26 -05:00
import { execFile } from 'child_process';
import { promisify } from 'util';
2026-03-28 01:06:30 -05:00
import { absolutePath, ensureDir } from './storage.js';
2026-03-28 09:30:26 -05:00
const execFileAsync = promisify(execFile);
2026-03-28 01:06:30 -05:00
export interface ImageMeta {
width: number;
height: number;
mimeType: string;
size: number;
}
export async function extractMeta(filePath: string): Promise<ImageMeta> {
const abs = absolutePath(filePath);
const meta = await sharp(abs).metadata();
const stat = fs.statSync(abs);
const mimeMap: Record<string, string> = {
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
webp: 'image/webp',
};
return {
width: meta.width ?? 0,
height: meta.height ?? 0,
mimeType: mimeMap[meta.format ?? ''] ?? 'image/jpeg',
size: stat.size,
};
}
2026-03-28 09:30:26 -05:00
export async function extractVideoMeta(filePath: string, mimeType: string): Promise<ImageMeta> {
const abs = absolutePath(filePath);
const stat = fs.statSync(abs);
try {
const { stdout } = await execFileAsync('ffprobe', [
'-v', 'quiet',
'-print_format', 'json',
'-show_streams',
abs,
]);
const data = JSON.parse(stdout);
const video = (data.streams as any[])?.find((s) => s.codec_type === 'video');
return {
width: video?.width ?? 1280,
height: video?.height ?? 720,
mimeType,
size: stat.size,
};
} catch {
// ffprobe unavailable or failed — use safe defaults
return { width: 1280, height: 720, mimeType, size: stat.size };
}
}
2026-03-28 01:06:30 -05:00
export async function saveBuffer(buffer: Buffer, destRelPath: string): Promise<void> {
ensureDir(destRelPath);
const abs = absolutePath(destRelPath);
fs.writeFileSync(abs, buffer);
}
export interface ResizeOptions {
width?: number;
height?: number;
quality?: number;
}
export async function resizeImage(
srcRelPath: string,
destRelPath: string,
options: ResizeOptions
): Promise<ImageMeta> {
const srcAbs = absolutePath(srcRelPath);
ensureDir(destRelPath);
const destAbs = absolutePath(destRelPath);
const src = await sharp(srcAbs).metadata();
const isGif = src.format === 'gif';
let pipeline = sharp(srcAbs, { animated: isGif });
if (options.width || options.height) {
pipeline = pipeline.resize({
width: options.width,
height: options.height,
fit: 'inside',
withoutEnlargement: true,
});
}
if (!isGif && options.quality) {
if (src.format === 'jpeg') pipeline = pipeline.jpeg({ quality: options.quality });
else if (src.format === 'png') pipeline = pipeline.png({ quality: options.quality });
else if (src.format === 'webp') pipeline = pipeline.webp({ quality: options.quality });
}
await pipeline.toFile(destAbs);
const resultMeta = await sharp(destAbs).metadata();
const stat = fs.statSync(destAbs);
const mimeMap: Record<string, string> = {
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
webp: 'image/webp',
};
return {
width: resultMeta.width ?? 0,
height: resultMeta.height ?? 0,
mimeType: mimeMap[resultMeta.format ?? ''] ?? 'image/jpeg',
size: stat.size,
};
}