Studio: Preserve transparency in uploaded profile avatars (#5200)

* fix: preserve transparency in uploaded profile avatars

* fix: guard unsupported canvas mime fallback
This commit is contained in:
Lee Jackson 2026-04-28 10:39:48 +01:00 committed by GitHub
commit 230d58872d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -3,6 +3,11 @@
const MAX_EDGE = 256;
const MAX_BYTES = 380_000;
const MAX_DATA_URL_LENGTH = Math.floor(MAX_BYTES * 1.35);
const JPEG_QUALITY_START = 0.88;
const WEBP_QUALITY_START = 0.9;
const MIN_QUALITY = 0.45;
const QUALITY_STEP = 0.08;
function loadImage(file: File): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
@ -20,8 +25,33 @@ function loadImage(file: File): Promise<HTMLImageElement> {
});
}
function canvasHasTransparency(ctx: CanvasRenderingContext2D, width: number, height: number): boolean {
const { data } = ctx.getImageData(0, 0, width, height);
for (let i = 3; i < data.length; i += 4) {
if (data[i] < 255) return true;
}
return false;
}
function encodeCanvasWithinLimit(
canvas: HTMLCanvasElement,
mimeType: string,
startQuality: number,
): string | null {
let quality = startQuality;
let dataUrl = canvas.toDataURL(mimeType, quality);
if (!dataUrl.startsWith(`data:${mimeType}`)) return null;
while (dataUrl.length > MAX_DATA_URL_LENGTH && quality > MIN_QUALITY) {
quality -= QUALITY_STEP;
dataUrl = canvas.toDataURL(mimeType, quality);
}
return dataUrl.length <= MAX_DATA_URL_LENGTH ? dataUrl : null;
}
/**
* Downscale and re-encode as JPEG so localStorage stays within reasonable size.
* Downscale the image and keep transparency when present while staying localStorage-friendly.
*/
export async function resizeImageFileToDataUrl(file: File): Promise<string> {
const img = await loadImage(file);
@ -40,14 +70,14 @@ export async function resizeImageFileToDataUrl(file: File): Promise<string> {
if (!ctx) throw new Error("Canvas not available");
ctx.drawImage(img, 0, 0, cw, ch);
let quality = 0.88;
let dataUrl = canvas.toDataURL("image/jpeg", quality);
while (dataUrl.length > MAX_BYTES * 1.35 && quality > 0.45) {
quality -= 0.08;
dataUrl = canvas.toDataURL("image/jpeg", quality);
const hasTransparency = canvasHasTransparency(ctx, cw, ch);
if (hasTransparency) {
const webpDataUrl = encodeCanvasWithinLimit(canvas, "image/webp", WEBP_QUALITY_START);
if (webpDataUrl) return webpDataUrl;
}
if (dataUrl.length > MAX_BYTES * 1.35) {
throw new Error("Image is still too large after compression. Try a smaller file.");
}
return dataUrl;
const jpegDataUrl = encodeCanvasWithinLimit(canvas, "image/jpeg", JPEG_QUALITY_START);
if (jpegDataUrl) return jpegDataUrl;
throw new Error("Image is still too large after compression. Try a smaller file.");
}