Files
SnapOtter/packages/image-engine/src/engine.ts
T
SnapOtter b76dc68682 fix: resolve Sharp 0.35.1 and BullMQ type incompatibilities after dep bumps
Sharp 0.35.1 moved FormatEnum to a namespace export and removed "avif"
from FormatEnum (now a separate literal in toFormat). BullMQ 5.78.1
bundles ioredis 5.10.1 while we have 5.11.1, causing structural type
mismatch. Also fixes new Biome 1.9 lint rules.
2026-06-15 15:20:16 +08:00

120 lines
4.1 KiB
TypeScript

import sharp from "sharp";
import { brightness } from "./operations/brightness.js";
import { colorBlindness } from "./operations/color-blindness.js";
import { colorChannels } from "./operations/color-channels.js";
import { compress } from "./operations/compress.js";
import { contrast } from "./operations/contrast.js";
import { convert } from "./operations/convert.js";
import { crop } from "./operations/crop.js";
import { editMetadata } from "./operations/edit-metadata.js";
import { flip } from "./operations/flip.js";
import { grayscale } from "./operations/grayscale.js";
import { invert } from "./operations/invert.js";
import { resize } from "./operations/resize.js";
import { rotate } from "./operations/rotate.js";
import { saturation } from "./operations/saturation.js";
import { sepia } from "./operations/sepia.js";
import { sharpen, sharpenAdvanced } from "./operations/sharpen.js";
import { stripMetadata } from "./operations/strip-metadata.js";
import type {
BrightnessOptions,
ColorBlindnessOptions,
ColorChannelOptions,
CompressOptions,
ContrastOptions,
ConvertOptions,
CropOptions,
EditMetadataOptions,
FlipOptions,
OperationResult,
OutputFormat,
ResizeOptions,
RotateOptions,
SaturationOptions,
Sharp,
SharpenAdvancedOptions,
SharpenOptions,
StripMetadataOptions,
} from "./types.js";
import { getImageInfo } from "./utils/metadata.js";
export interface Operation {
type: string;
options: Record<string, unknown>;
}
const OPERATION_MAP: Record<
string,
(image: Sharp, options: Record<string, unknown>) => Promise<Sharp>
> = {
resize: (img, opts) => resize(img, opts as unknown as ResizeOptions),
crop: (img, opts) => crop(img, opts as unknown as CropOptions),
rotate: (img, opts) => rotate(img, opts as unknown as RotateOptions),
flip: (img, opts) => flip(img, opts as unknown as FlipOptions),
convert: (img, opts) => convert(img, opts as unknown as ConvertOptions),
compress: (img, opts) => compress(img, opts as unknown as CompressOptions),
"strip-metadata": (img, opts) => stripMetadata(img, opts as unknown as StripMetadataOptions),
brightness: (img, opts) => brightness(img, opts as unknown as BrightnessOptions),
contrast: (img, opts) => contrast(img, opts as unknown as ContrastOptions),
saturation: (img, opts) => saturation(img, opts as unknown as SaturationOptions),
"color-blindness": (img, opts) => colorBlindness(img, opts as unknown as ColorBlindnessOptions),
"color-channels": (img, opts) => colorChannels(img, opts as unknown as ColorChannelOptions),
grayscale: (img) => grayscale(img),
sepia: (img) => sepia(img),
sharpen: (img, opts) => sharpen(img, opts as unknown as SharpenOptions),
"sharpen-advanced": (img, opts) =>
sharpenAdvanced(img, opts as unknown as SharpenAdvancedOptions),
invert: (img) => invert(img),
"edit-metadata": (img, opts) => editMetadata(img, opts as unknown as EditMetadataOptions),
};
const FORMAT_MAP: Record<string, string> = {
jpg: "jpeg",
png: "png",
webp: "webp",
avif: "avif",
heif: "avif",
tiff: "tiff",
gif: "gif",
jxl: "jxl",
};
/**
* Process an image through a pipeline of operations.
*
* @param input - The raw image buffer
* @param operations - Array of operations to apply in sequence
* @param outputFormat - Optional output format (defaults to input format)
* @returns The processed image buffer and metadata
*/
export async function processImage(
input: Buffer,
operations: Operation[],
outputFormat?: OutputFormat,
): Promise<OperationResult> {
let image: Sharp = sharp(input);
// Apply each operation in sequence
for (const op of operations) {
const handler = OPERATION_MAP[op.type];
if (!handler) {
throw new Error(`Unknown operation: ${op.type}`);
}
image = await handler(image, op.options);
}
// Convert to output format if specified
if (outputFormat) {
const sharpFormat = FORMAT_MAP[outputFormat];
if (!sharpFormat) {
throw new Error(`Unsupported output format: ${outputFormat}`);
}
image = image.toFormat(sharpFormat as keyof sharp.FormatEnum);
}
const buffer = await image.toBuffer();
const info = await getImageInfo(buffer);
return { buffer, info };
}