import sharp from 'sharp'; import { env } from '../env'; import { HttpError } from './http'; export const AVATAR_SIZE = 512; /** Full-size photos from 50-200 MP phone cameras can be well over 10 MB (the app shrinks them first since 1.3.0). */ export const AVATAR_MAX_UPLOAD_BYTES = 25 * 1024 * 1024; /** * Uploaded avatars are stored as a path (`/avatars/?v=`) so they keep * working if the API moves; older rows may hold a full external URL. */ export const publicAvatarUrl = (stored: string | null): string | null => stored?.startsWith('/') ? env.API_PUBLIC_URL.replace(/\/$/, '') + stored : stored; /** * Square-crops, resizes and re-encodes to WebP. Re-encoding also drops EXIF * (phone photos carry GPS location) and anything that isn't image data. */ export async function processAvatar(input: Buffer, log?: (err: unknown) => void): Promise { try { // sharp's default pixel limit (~268 MP) fits a 200 MP camera; JPEGs are shrunk while decoding, so it stays cheap. return await sharp(input, { animated: false }) .rotate() // apply the EXIF orientation before it's stripped .resize(AVATAR_SIZE, AVATAR_SIZE, { fit: 'cover', position: 'attention' }) .webp({ quality: 82 }) .toBuffer(); } catch (err) { log?.(err); throw new HttpError(400, "That file isn't an image we can read. Try a JPEG, PNG, WebP or HEIC."); } }