export const MAX_BLOB_BYTES = 1_000_000 const CLOUDINARY_FIT = 'w_1280,c_limit,q_auto:good,f_jpg' export type PreparedExternalCardThumbnail = { bytes: Uint8Array mimeType: string originalUrl: string sourceUrl: string originalBytes: number uploadBytes: number fitted: boolean } export function withCloudinaryFit(url: string): string | null { let parsed: URL try { parsed = new URL(url) } catch { return null } if (parsed.hostname !== 'res.cloudinary.com') return null const segments = parsed.pathname.split('/').filter(Boolean) if (segments.length < 4) return null const [cloudName, resourceType, deliveryType, ...rest] = segments if (resourceType !== 'image') return null if (deliveryType !== 'upload' && deliveryType !== 'fetch') return null const existingTransform = looksLikeCloudinaryTransform(rest[0]) ? rest.shift() : null const merged = existingTransform ? `${existingTransform},${CLOUDINARY_FIT}` : CLOUDINARY_FIT parsed.pathname = `/${[cloudName, resourceType, deliveryType, merged, ...rest].join('/')}` return parsed.toString() } function looksLikeCloudinaryTransform(segment?: string) { if (!segment) return false if (/^v\d+$/u.test(segment)) return false return /(?:^|,)[a-z]+_/iu.test(segment) } async function fetchImage(url: string) { const imageResponse = await fetch(url) if (!imageResponse.ok) { throw new Error( `Could not fetch external card thumbnail: ${imageResponse.status} ${imageResponse.statusText}`, ) } const mimeType = String(imageResponse.headers.get('content-type') || '') .split(';')[0] .trim() .toLowerCase() if (!mimeType.startsWith('image/')) { throw new Error( `External card thumbnail must be an image, received ${mimeType || 'unknown content type'}`, ) } const bytes = new Uint8Array(await imageResponse.arrayBuffer()) return { bytes, mimeType } } function preparedFromBytes( originalUrl: string, sourceUrl: string, originalBytes: number, image: { bytes: Uint8Array; mimeType: string }, fitted: boolean, ): PreparedExternalCardThumbnail { return { bytes: image.bytes, mimeType: image.mimeType, originalUrl, sourceUrl, originalBytes, uploadBytes: image.bytes.byteLength, fitted, } } export function thumbnailUploadPreview(prepared: PreparedExternalCardThumbnail) { return { type: 'external-card-thumbnail', url: prepared.sourceUrl, originalUrl: prepared.originalUrl, sourceUrl: prepared.sourceUrl, originalBytes: prepared.originalBytes, uploadBytes: prepared.uploadBytes, mimeType: prepared.mimeType, fitted: prepared.fitted, } } export async function prepareExternalCardThumbnail( thumbnailUrl: string, ): Promise { const original = await fetchImage(thumbnailUrl) if (original.bytes.byteLength <= MAX_BLOB_BYTES) { return preparedFromBytes(thumbnailUrl, thumbnailUrl, original.bytes.byteLength, original, false) } const cloudinaryUrl = withCloudinaryFit(thumbnailUrl) if (cloudinaryUrl && cloudinaryUrl !== thumbnailUrl) { const fitted = await fetchImage(cloudinaryUrl) if (fitted.bytes.byteLength <= MAX_BLOB_BYTES) { return preparedFromBytes( thumbnailUrl, cloudinaryUrl, original.bytes.byteLength, fitted, true, ) } } throw new Error( `External card thumbnail is ${original.bytes.byteLength} bytes; Bluesky allows at most ${MAX_BLOB_BYTES}`, ) }