Expo's fetch rejects React Native's { uri, name, type } FormData parts ("Unsupported FormDataPart
implementation"), so every upload failed. Upload an expo-file-system File instead, shrunk to 1024 px
first. The server also takes bigger photos (25 MB, sharp's default pixel limit) and logs why a
picture couldn't be read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Y2iUsKEWDuRGjK5FPmcUP
33 lines
1.4 KiB
TypeScript
33 lines
1.4 KiB
TypeScript
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/<id>?v=<version>`) 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<Buffer> {
|
|
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.");
|
|
}
|
|
}
|