| | 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"}`, |
| | ); |
| | } |
| | } |
| | } |
| |
|