import sharp from "sharp"; import type { CompressOptions, Sharp } from "../types.js"; const FORMAT_MAP: Record = { jpg: "jpeg", png: "png", webp: "webp", avif: "avif", heif: "avif", tiff: "tiff", gif: "gif", }; function formatOpts(format: string, quality: number): Record { const opts: Record = { quality }; if (format === "avif") opts.effort = 4; return opts; } export async function compress(image: Sharp, options: CompressOptions): Promise { const { quality, targetSizeBytes, format } = options; const metadata = await image.metadata(); const detected = metadata.format ?? "jpeg"; const outputFormat = ( FORMAT_MAP[format ?? ""] ?? FORMAT_MAP[detected] ?? detected ) as keyof import("sharp").FormatEnum; if (targetSizeBytes !== undefined) { if (targetSizeBytes <= 0) { throw new Error("Target size must be greater than 0"); } const inputBuffer = await image.toBuffer(); return compressToTargetSize(inputBuffer, outputFormat, targetSizeBytes); } const q = quality ?? 80; if (q < 1 || q > 100) { throw new Error("Quality must be between 1 and 100"); } return image.toFormat(outputFormat, formatOpts(outputFormat, q)); } async function compressToTargetSize( inputBuffer: Buffer, format: keyof import("sharp").FormatEnum, targetBytes: number, ): Promise { let low = 1; let high = 100; let bestQuality = 1; let bestBuffer: Buffer | null = null; const maxIterations = 8; const tolerance = 0.05; // 5% for (let i = 0; i < maxIterations && low <= high; i++) { const mid = Math.min(100, Math.max(1, Math.round((low + high) / 2))); const attempt = sharp(inputBuffer).toFormat(format, formatOpts(format, mid)); const resultBuffer = await attempt.toBuffer(); const resultSize = resultBuffer.length; if (Math.abs(resultSize - targetBytes) / targetBytes <= tolerance) { bestQuality = mid; bestBuffer = resultBuffer; break; } if (resultSize > targetBytes) { high = mid - 1; } else { low = mid + 1; bestQuality = mid; bestBuffer = resultBuffer; } } if (bestBuffer === null) { bestBuffer = await sharp(inputBuffer) .toFormat(format, formatOpts(format, bestQuality)) .toBuffer(); } return sharp(bestBuffer).toFormat(format, formatOpts(format, bestQuality)); }