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<PreparedExternalCardThumbnail> {
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}`,
)
}