mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
106 lines
3.6 KiB
TypeScript
106 lines
3.6 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { randomUUID } from "node:crypto";
|
|
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { qoiEncode } from "@snapotter/image-engine";
|
|
import sharp from "sharp";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
let cachedMagickCmd: string | null = null;
|
|
|
|
async function findMagickCmd(): Promise<string> {
|
|
if (cachedMagickCmd) return cachedMagickCmd;
|
|
for (const cmd of ["magick", "convert"]) {
|
|
try {
|
|
await execFileAsync(cmd, ["--version"], { timeout: 5_000 });
|
|
cachedMagickCmd = cmd;
|
|
return cmd;
|
|
} catch {
|
|
/* try next */
|
|
}
|
|
}
|
|
throw new Error("No ImageMagick found.");
|
|
}
|
|
|
|
function magickArgs(cmd: string, args: string[]): string[] {
|
|
return cmd === "magick" ? ["convert", ...args] : args;
|
|
}
|
|
|
|
export async function encodeBmp(inputBuffer: Buffer): Promise<Buffer> {
|
|
const cmd = await findMagickCmd();
|
|
const id = randomUUID();
|
|
const inputPath = join(tmpdir(), `bmp-enc-in-${id}.png`);
|
|
const outputPath = join(tmpdir(), `bmp-enc-out-${id}.bmp`);
|
|
try {
|
|
const pngBuffer = await sharp(inputBuffer).png().toBuffer();
|
|
await writeFile(inputPath, pngBuffer);
|
|
await execFileAsync(cmd, magickArgs(cmd, [inputPath, `bmp3:${outputPath}`]), {
|
|
timeout: 60_000,
|
|
});
|
|
return await readFile(outputPath);
|
|
} finally {
|
|
await rm(inputPath, { force: true }).catch(() => {});
|
|
await rm(outputPath, { force: true }).catch(() => {});
|
|
}
|
|
}
|
|
|
|
export async function encodeIco(inputBuffer: Buffer): Promise<Buffer> {
|
|
const cmd = await findMagickCmd();
|
|
const id = randomUUID();
|
|
const inputPath = join(tmpdir(), `ico-enc-in-${id}.png`);
|
|
const outputPath = join(tmpdir(), `ico-enc-out-${id}.ico`);
|
|
try {
|
|
const pngBuffer = await sharp(inputBuffer)
|
|
.resize(256, 256, { fit: "inside", withoutEnlargement: true })
|
|
.png()
|
|
.toBuffer();
|
|
await writeFile(inputPath, pngBuffer);
|
|
await execFileAsync(cmd, magickArgs(cmd, [inputPath, `ico:${outputPath}`]), {
|
|
timeout: 60_000,
|
|
});
|
|
return await readFile(outputPath);
|
|
} finally {
|
|
await rm(inputPath, { force: true }).catch(() => {});
|
|
await rm(outputPath, { force: true }).catch(() => {});
|
|
}
|
|
}
|
|
|
|
export async function encodeJp2(inputBuffer: Buffer, quality?: number): Promise<Buffer> {
|
|
const id = randomUUID();
|
|
const inputPath = join(tmpdir(), `jp2-enc-in-${id}.png`);
|
|
const outputPath = join(tmpdir(), `jp2-enc-out-${id}.jp2`);
|
|
try {
|
|
const pngBuffer = await sharp(inputBuffer).png().toBuffer();
|
|
await writeFile(inputPath, pngBuffer);
|
|
try {
|
|
const rate = quality ? String(Math.max(1, Math.round(quality / 10))) : "5";
|
|
await execFileAsync("opj_compress", ["-i", inputPath, "-o", outputPath, "-r", rate], {
|
|
timeout: 60_000,
|
|
});
|
|
return await readFile(outputPath);
|
|
} catch {
|
|
/* fall back to ImageMagick */
|
|
}
|
|
const cmd = await findMagickCmd();
|
|
const q = quality ? ["-quality", String(quality)] : [];
|
|
await execFileAsync(cmd, magickArgs(cmd, [inputPath, ...q, `jp2:${outputPath}`]), {
|
|
timeout: 60_000,
|
|
});
|
|
return await readFile(outputPath);
|
|
} finally {
|
|
await rm(inputPath, { force: true }).catch(() => {});
|
|
await rm(outputPath, { force: true }).catch(() => {});
|
|
}
|
|
}
|
|
|
|
export async function encodeQoi(inputBuffer: Buffer): Promise<Buffer> {
|
|
const { data, info } = await sharp(inputBuffer).ensureAlpha().raw().toBuffer({
|
|
resolveWithObject: true,
|
|
});
|
|
const encoded = qoiEncode(new Uint8Array(data), info.width, info.height, 4);
|
|
return Buffer.from(encoded);
|
|
}
|