| import type { createLoggerWithContext } from "@midday/logger"; |
| import convert from "heic-convert"; |
| import sharp from "sharp"; |
| import { IMAGE_SIZES } from "./timeout"; |
|
|
| |
| |
| sharp.cache({ memory: 256, files: 20, items: 100 }); |
| sharp.concurrency(2); |
|
|
| |
| |
| |
| |
| |
| export const MAX_HEIC_FILE_SIZE = 15 * 1024 * 1024; |
|
|
| export interface HeicConversionResult { |
| buffer: Buffer; |
| mimetype: "image/jpeg"; |
| } |
|
|
| export interface ImageProcessingOptions { |
| maxSize?: number; |
| } |
|
|
| export interface ResizeResult { |
| buffer: Buffer; |
| mimetype: string; |
| } |
|
|
| |
| |
| |
| const RESIZABLE_MIMETYPES = new Set([ |
| "image/jpeg", |
| "image/jpg", |
| "image/png", |
| "image/webp", |
| "image/gif", |
| "image/tiff", |
| ]); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function resizeImage( |
| inputBuffer: ArrayBuffer, |
| mimetype: string, |
| logger: ReturnType<typeof createLoggerWithContext>, |
| options?: ImageProcessingOptions, |
| ): Promise<ResizeResult> { |
| const maxSize = options?.maxSize ?? IMAGE_SIZES.MAX_DIMENSION; |
|
|
| |
| if (!inputBuffer || inputBuffer.byteLength === 0) { |
| throw new Error("Input buffer is empty"); |
| } |
|
|
| |
| if (!RESIZABLE_MIMETYPES.has(mimetype.toLowerCase())) { |
| logger.info("Skipping resize for unsupported mimetype", { mimetype }); |
| return { buffer: Buffer.from(inputBuffer), mimetype }; |
| } |
|
|
| try { |
| const image = sharp(Buffer.from(inputBuffer)); |
| const metadata = await image.metadata(); |
|
|
| |
| const width = metadata.width ?? 0; |
| const height = metadata.height ?? 0; |
| if (width <= maxSize && height <= maxSize) { |
| logger.info("Image already within size limits, skipping resize", { |
| width, |
| height, |
| maxSize, |
| }); |
| return { buffer: Buffer.from(inputBuffer), mimetype }; |
| } |
|
|
| |
| const buffer = await image |
| .rotate() |
| .resize({ |
| width: maxSize, |
| height: maxSize, |
| fit: "inside", |
| withoutEnlargement: true, |
| }) |
| .toBuffer(); |
|
|
| logger.info("Image resized successfully", { |
| originalWidth: width, |
| originalHeight: height, |
| maxSize, |
| }); |
|
|
| return { buffer, mimetype }; |
| } catch (error) { |
| logger.warn("Failed to resize image, returning original", { |
| error: error instanceof Error ? error.message : "Unknown error", |
| mimetype, |
| }); |
| |
| return { buffer: Buffer.from(inputBuffer), mimetype }; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function convertHeicToJpeg( |
| inputBuffer: ArrayBuffer, |
| logger: ReturnType<typeof createLoggerWithContext>, |
| options?: ImageProcessingOptions, |
| ): Promise<HeicConversionResult> { |
| const maxSize = options?.maxSize ?? IMAGE_SIZES.MAX_DIMENSION; |
|
|
| |
| if (!inputBuffer || inputBuffer.byteLength === 0) { |
| throw new Error("Input buffer is empty"); |
| } |
|
|
| |
| try { |
| const buffer = await sharp(Buffer.from(inputBuffer)) |
| .rotate() |
| .resize({ |
| width: maxSize, |
| height: maxSize, |
| fit: "inside", |
| withoutEnlargement: true, |
| }) |
| .toFormat("jpeg") |
| .toBuffer(); |
|
|
| logger.info("HEIC conversion successful with sharp"); |
| return { buffer, mimetype: "image/jpeg" }; |
| } catch (sharpError) { |
| logger.warn("Sharp failed to process HEIC, falling back to heic-convert", { |
| error: sharpError instanceof Error ? sharpError.message : "Unknown error", |
| }); |
|
|
| |
| |
| |
| let decodedImage: ArrayBuffer; |
| try { |
| decodedImage = await convert({ |
| |
| buffer: new Uint8Array(inputBuffer), |
| format: "JPEG", |
| quality: 0.8, |
| }); |
| } catch (heicError) { |
| |
| throw new Error( |
| `Failed to convert HEIC image: sharp error: ${sharpError instanceof Error ? sharpError.message : "Unknown"}, heic-convert error: ${heicError instanceof Error ? heicError.message : "Unknown"}`, |
| ); |
| } |
|
|
| |
| if (!decodedImage || decodedImage.byteLength === 0) { |
| throw new Error("Decoded image is empty after heic-convert"); |
| } |
|
|
| |
| try { |
| const buffer = await sharp(Buffer.from(decodedImage)) |
| .rotate() |
| .resize({ |
| width: maxSize, |
| height: maxSize, |
| fit: "inside", |
| withoutEnlargement: true, |
| }) |
| .toFormat("jpeg") |
| .toBuffer(); |
|
|
| logger.info("HEIC conversion successful with heic-convert fallback"); |
| return { buffer, mimetype: "image/jpeg" }; |
| } catch (finalSharpError) { |
| throw new Error( |
| `Failed to process heic-convert output: ${finalSharpError instanceof Error ? finalSharpError.message : "Unknown error"}`, |
| ); |
| } |
| } |
| } |
|
|