| 1 | /** |
| 2 | * Longest edge, in pixels, a PNG or JPEG attachment keeps. Anthropic rejects an image over |
| 3 | * 2000px on a side once a request holds more than 20 images, and Claude Code forwards images |
| 4 | * sent into a running turn unchanged, so one large paste breaks every later request in that |
| 5 | * thread. Web and mobile scale attachments down to this edge when they are attached. |
| 6 | */ |
| 7 | export const IMAGE_ATTACHMENT_MAX_EDGE = 2000; |
| 8 | |
| 9 | /** Formats a client re-encodes in place. GIF, WebP, and SVG are left as attached. */ |
| 10 | export type DownscalableImageMimeType = "image/png" | "image/jpeg"; |
| 11 | |
| 12 | export function downscalableImageMimeType(mimeType: string): DownscalableImageMimeType | null { |
| 13 | const normalized = mimeType.toLowerCase(); |
| 14 | if (normalized === "image/png") return "image/png"; |
| 15 | if (normalized === "image/jpeg" || normalized === "image/jpg") return "image/jpeg"; |
| 16 | return null; |
| 17 | } |
| 18 | |
| 19 | /** |
| 20 | * The size an image should be scaled to so its long edge fits `IMAGE_ATTACHMENT_MAX_EDGE`, |
| 21 | * keeping its aspect ratio. Returns null when it already fits. |
| 22 | */ |
| 23 | export function fitImageAttachmentSize(size: { |
| 24 | readonly width: number; |
| 25 | readonly height: number; |
| 26 | }): { width: number; height: number } | null { |
| 27 | const longestEdge = Math.max(size.width, size.height); |
| 28 | if (longestEdge <= IMAGE_ATTACHMENT_MAX_EDGE) return null; |
| 29 | const scale = IMAGE_ATTACHMENT_MAX_EDGE / longestEdge; |
| 30 | return { |
| 31 | width: Math.max(1, Math.min(IMAGE_ATTACHMENT_MAX_EDGE, Math.round(size.width * scale))), |
| 32 | height: Math.max(1, Math.min(IMAGE_ATTACHMENT_MAX_EDGE, Math.round(size.height * scale))), |
| 33 | }; |
| 34 | } |