diff --git a/app/(app)/lish/createPub/createPublication.ts b/app/(app)/lish/createPub/createPublication.ts index 5adcc7ee..c553988c 100644 --- a/app/(app)/lish/createPub/createPublication.ts +++ b/app/(app)/lish/createPub/createPublication.ts @@ -13,6 +13,7 @@ import { Vercel } from "@vercel/sdk"; import { isProductionDomain } from "src/utils/isProductionDeployment"; import { string } from "zod"; import { getPublicationType } from "src/utils/collectionHelpers"; +import { resizePublicationIcon } from "src/utils/resizePublicationIcon"; import { PubThemeDefaultsRGB } from "components/ThemeManager/themeDefaults"; const VERCEL_TOKEN = process.env.VERCEL_TOKEN; @@ -81,11 +82,10 @@ export async function createPublication({ // Upload the icon if provided if (iconFile && iconFile.size > 0) { - const buffer = await iconFile.arrayBuffer(); - const uploadResult = await agent.com.atproto.repo.uploadBlob( - new Uint8Array(buffer), - { encoding: iconFile.type }, - ); + const { data, encoding } = await resizePublicationIcon(iconFile); + const uploadResult = await agent.com.atproto.repo.uploadBlob(data, { + encoding, + }); iconBlob = uploadResult.data.blob; } diff --git a/app/(app)/lish/createPub/updatePublication.ts b/app/(app)/lish/createPub/updatePublication.ts index 4eaf4874..7abec1d7 100644 --- a/app/(app)/lish/createPub/updatePublication.ts +++ b/app/(app)/lish/createPub/updatePublication.ts @@ -16,6 +16,7 @@ import { type NormalizedPublication, } from "src/utils/normalizeRecords"; import { getPublicationType } from "src/utils/collectionHelpers"; +import { resizePublicationIcon } from "src/utils/resizePublicationIcon"; type UpdatePublicationResult = | { success: true; publication: any } @@ -263,11 +264,10 @@ export async function updatePublication({ // Upload icon if provided let iconBlob = normalizedPub?.icon; if (iconFile && iconFile.size > 0) { - const buffer = await iconFile.arrayBuffer(); - const uploadResult = await agent.com.atproto.repo.uploadBlob( - new Uint8Array(buffer), - { encoding: iconFile.type }, - ); + const { data, encoding } = await resizePublicationIcon(iconFile); + const uploadResult = await agent.com.atproto.repo.uploadBlob(data, { + encoding, + }); if (uploadResult.data.blob) { iconBlob = uploadResult.data.blob; } diff --git a/src/utils/resizePublicationIcon.ts b/src/utils/resizePublicationIcon.ts new file mode 100644 index 00000000..1fb6d94e --- /dev/null +++ b/src/utils/resizePublicationIcon.ts @@ -0,0 +1,29 @@ +import sharp from "sharp"; + +// Publication icons are rendered at most at 48px (the `w-12 h-12` PubIcon +// variant / 40px header). We keep a 3x buffer for high-DPI ("retina") displays, +// so there's no point uploading anything larger than this to the PDS. +const MAX_ICON_SIZE = 144; + +// Scale an uploaded icon down to the size it's actually displayed at and +// re-encode it as WebP. Uses `fit: "inside"` so aspect ratio is preserved (the +// square crop happens via CSS at display time) and `withoutEnlargement` so +// small icons aren't upscaled. Falls back to the original bytes if sharp can't +// process the input (e.g. an unsupported format). +export async function resizePublicationIcon( + file: File, +): Promise<{ data: Uint8Array; encoding: string }> { + const original = new Uint8Array(await file.arrayBuffer()); + try { + const resized = await sharp(Buffer.from(original)) + .resize(MAX_ICON_SIZE, MAX_ICON_SIZE, { + fit: "inside", + withoutEnlargement: true, + }) + .webp({ quality: 90 }) + .toBuffer(); + return { data: new Uint8Array(resized), encoding: "image/webp" }; + } catch { + return { data: original, encoding: file.type }; + } +}