Fix avatar upload failing on JPEG/WebP/GIF without a profile color.

embedMetadataInImage returned null for non-PNG when there was nothing to embed, which blocked plain avatar saves after format/library churn.
This commit is contained in:
2026-07-25 20:09:22 +10:00
parent 99a294791e
commit 2603ca8e5c
5 changed files with 158 additions and 36 deletions

View File

@@ -258,6 +258,59 @@ export function embedColorInImage(imageData: ArrayBuffer | Uint8Array, color: st
}
}
/**
* True when metadata includes fields that only PNG tEXt chunks can store.
*/
export function needsPngForMetadata(metadata: ImageMetadata): boolean {
return Boolean(metadata.banner || metadata.avatarBorderColor || metadata.gradient);
}
/**
* Convert image data to PNG via canvas (needed for banner/border/gradient metadata).
*/
export async function convertImageDataToPng(
imageData: ArrayBuffer | Uint8Array
): Promise<Uint8Array | null> {
const bytes = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
const format = detectImageFormat(bytes);
if (format === 'png') return bytes;
if (format === 'unknown') return null;
try {
const blob = uint8ArrayToBlob(bytes, getMimeType(format));
const bitmap = await createImageBitmap(blob);
try {
const canvas = document.createElement('canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const ctx = canvas.getContext('2d');
if (!ctx) return null;
ctx.drawImage(bitmap, 0, 0);
const pngBlob = await new Promise<Blob | null>((resolve) => {
canvas.toBlob(resolve, 'image/png');
});
if (!pngBlob) return null;
return new Uint8Array(await pngBlob.arrayBuffer());
} finally {
bitmap.close();
}
} catch (error) {
console.error('[convertImageDataToPng] Conversion failed:', error);
return null;
}
}
/**
* Build a Blob from Uint8Array without relying on BufferSource/BlobPart quirks
* across TS DOM lib / Electron versions (avoids SharedArrayBuffer typing issues).
*/
export function uint8ArrayToBlob(data: Uint8Array, type: string): Blob {
const copy = new ArrayBuffer(data.byteLength);
new Uint8Array(copy).set(data);
return new Blob([copy], { type });
}
/**
* Embed paarrot metadata (color and/or banner) into image data
* Preserves existing metadata that is not being updated
@@ -270,18 +323,27 @@ export function embedMetadataInImage(
metadata: ImageMetadata
): Uint8Array | null {
const format = detectImageFormat(imageData);
const bytes = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
switch (format) {
case 'png':
return embedMetadataInPNG(imageData, metadata);
// For other formats, only embed color for now
// Non-PNG: color via format-specific chunks; banner/border/gradient need PNG (convert first)
case 'webp':
case 'jpeg':
case 'gif':
if (needsPngForMetadata(metadata)) {
console.warn(
'[embedMetadataInImage] Banner/border/gradient require PNG; convert with convertImageDataToPng first. Got:',
format
);
return null;
}
if (metadata.color) {
return embedColorInImage(imageData, metadata.color);
}
return null;
// Supported format with nothing to embed — succeed as a no-op so avatar upload isn't blocked
return bytes;
default:
console.warn('[embedMetadataInImage] Unknown image format, cannot embed metadata');
return null;