Files
butcem/src/lib/uploads/process.ts
T

75 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import "server-only";
import sharp from "sharp";
import type { DetectedType } from "@/shared/attachments";
/** Ana görsel uzun kenar sınırı (fiş okunabilir kalsın, dosya küçülsün). */
export const MAX_IMAGE_EDGE = 2000;
export const THUMB_EDGE = 320;
/** Dekompresyon bombasına karşı piksel sınırı (~50 MP). */
const MAX_INPUT_PIXELS = 50_000_000;
export type ProcessedImage = {
main: Buffer;
thumb: Buffer;
width: number;
height: number;
};
export class UnreadableImageError extends Error {}
/**
* Görseli normalize eder: EXIF yönüne göre döndürür, en fazla 2000 px'e küçültür ve WebP'ye
* çevirir. sharp çıktıya meta veri yazmaz (withMetadata kullanılmadığı için EXIF/GPS/ICC dahil
* tüm meta veri düşer). HEIC (iPhone, HEVC) sharp'ın hazır derlemesinde çözülemediği için
* libheif (WASM) ile çözülür.
*/
export async function processImage(bytes: Buffer, type: DetectedType): Promise<ProcessedImage> {
try {
const source = type === "heic" ? await decodeHeic(bytes) : sharpFrom(bytes).rotate();
const { data, info } = await source
.resize({
width: MAX_IMAGE_EDGE,
height: MAX_IMAGE_EDGE,
fit: "inside",
withoutEnlargement: true,
})
.webp({ quality: 82 })
.toBuffer({ resolveWithObject: true });
const thumb = await sharp(data)
.resize({ width: THUMB_EDGE, height: THUMB_EDGE, fit: "inside", withoutEnlargement: true })
.webp({ quality: 70 })
.toBuffer();
return { main: data, thumb, width: info.width, height: info.height };
} catch (error) {
throw new UnreadableImageError(error instanceof Error ? error.message : String(error));
}
}
function sharpFrom(bytes: Buffer) {
return sharp(bytes, { failOn: "error", limitInputPixels: MAX_INPUT_PIXELS });
}
async function decodeHeic(bytes: Buffer) {
// Bazı .heic dosyaları aslında AV1 (AVIF) içerir; sharp bunları doğrudan çözebilir.
try {
const meta = await sharpFrom(bytes).metadata();
if (meta.compression === "av1") return sharpFrom(bytes).rotate();
} catch {
// libheif ile denenecek.
}
const { default: decode } = await import("heic-decode");
const image = await decode({ buffer: bytes });
if (image.width * image.height > MAX_INPUT_PIXELS) throw new Error("Görsel çok büyük.");
// libheif yönlendirmeyi (irot/imir) çözerken uygular; ham RGBA piksellerden devam edilir.
return sharp(Buffer.from(image.data.buffer, image.data.byteOffset, image.data.byteLength), {
raw: { width: image.width, height: image.height, channels: 4 },
});
}
/** PDF'ler değiştirilmeden saklanır; yalnızca yapı kontrolü (başlık + dosya sonu işareti). */
export function isPlausiblePdf(bytes: Buffer): boolean {
if (bytes.subarray(0, 5).toString("latin1") !== "%PDF-") return false;
const tail = bytes.subarray(Math.max(0, bytes.length - 2048)).toString("latin1");
return tail.includes("%%EOF");
}