Corrige la compression d'image sur Safari/iPhone (repli PNG au lieu de JPEG)
Build and deploy / deploy (push) Successful in 37s
Build and deploy / deploy (push) Successful in 37s
canvas.toBlob demandait du WebP en dur pour compresser avant l'envoi. Safari ne sait pas l'encoder et retombe silencieusement sur du PNG (imposé par la spec) — un format quasi sans perte, bien plus lourd qu'un JPEG pour une photo, au point de dépasser la limite de taille du bucket malgré une photo source raisonnable (vécu en prod : 4 Mo sur iPhone). Le support WebP est maintenant détecté une fois, avec repli explicite sur JPEG plutôt que de laisser le navigateur choisir. S'applique au Mur (déjà corrigé pour l'extension/Content-Type) et maintenant aussi aux avatars (/profile, /signup). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -64,14 +64,18 @@ export function ProfileForm({
|
||||
let nextAvatarUrl = avatarUrl;
|
||||
|
||||
if (avatarBlob) {
|
||||
const path = `${userId}.webp`;
|
||||
// Extension alignée sur le vrai type du blob (pas ".webp" en dur) :
|
||||
// certains navigateurs (Safari) retombent sur du PNG si le WebP
|
||||
// n'est pas supporté à l'encodage — voir cropImageToBlob.
|
||||
const extension = avatarBlob.type === "image/jpeg" ? "jpg" : avatarBlob.type === "image/png" ? "png" : "webp";
|
||||
const path = `${userId}.${extension}`;
|
||||
const { error: uploadError } = await supabase.storage
|
||||
.from("avatars")
|
||||
.upload(path, avatarBlob, { upsert: true, contentType: "image/webp" });
|
||||
.upload(path, avatarBlob, { upsert: true, contentType: avatarBlob.type || "image/webp" });
|
||||
|
||||
if (uploadError) {
|
||||
setLoading(false);
|
||||
setError("Impossible d'envoyer la photo, réessaie.");
|
||||
setError(`Impossible d'envoyer la photo (${uploadError.message}). Réessaie.`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+11
-2
@@ -120,14 +120,23 @@ export default function SignupPage() {
|
||||
|
||||
let avatarUrl: string | null = null;
|
||||
if (avatarBlob) {
|
||||
const path = `${userId}.webp`;
|
||||
// Extension alignée sur le vrai type du blob (pas ".webp" en dur) :
|
||||
// certains navigateurs (Safari) retombent sur du PNG si le WebP
|
||||
// n'est pas supporté à l'encodage — voir cropImageToBlob.
|
||||
const extension = avatarBlob.type === "image/jpeg" ? "jpg" : avatarBlob.type === "image/png" ? "png" : "webp";
|
||||
const path = `${userId}.${extension}`;
|
||||
const { error: uploadError } = await supabase.storage
|
||||
.from("avatars")
|
||||
.upload(path, avatarBlob, { upsert: true, contentType: "image/webp" });
|
||||
.upload(path, avatarBlob, { upsert: true, contentType: avatarBlob.type || "image/webp" });
|
||||
|
||||
if (!uploadError) {
|
||||
const { data: publicUrlData } = supabase.storage.from("avatars").getPublicUrl(path);
|
||||
avatarUrl = publicUrlData.publicUrl;
|
||||
} else {
|
||||
// Ne bloque pas la finalisation du compte pour une photo optionnelle
|
||||
// — juste de quoi diagnostiquer si quelqu'un signale l'absence
|
||||
// d'avatar après inscription.
|
||||
console.error("Échec de l'envoi de l'avatar à l'inscription :", uploadError);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+27
-7
@@ -9,8 +9,28 @@ function loadImage(src: string): Promise<HTMLImageElement> {
|
||||
});
|
||||
}
|
||||
|
||||
// Certains navigateurs (Safari, historiquement) ne savent pas encoder du
|
||||
// WebP via canvas.toBlob/toDataURL : la spec impose alors un repli
|
||||
// silencieux sur du PNG, un format quasi sans perte — beaucoup plus lourd
|
||||
// qu'un JPEG pour une photo, au point de dépasser la limite de taille du
|
||||
// bucket malgré la "compression" (vécu en prod avec une photo iPhone de
|
||||
// 4 Mo). Détecter le support réel et demander explicitement du JPEG dans
|
||||
// ce cas — plutôt que de laisser le navigateur retomber sur du PNG —
|
||||
// garantit une vraie compression avec perte dans tous les cas.
|
||||
let cachedMimeType: string | null = null;
|
||||
|
||||
function preferredMimeType(): string {
|
||||
if (cachedMimeType) return cachedMimeType;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
cachedMimeType = canvas.toDataURL("image/webp").startsWith("data:image/webp") ? "image/webp" : "image/jpeg";
|
||||
return cachedMimeType;
|
||||
}
|
||||
|
||||
// Découpe la zone sélectionnée, redimensionne (sans jamais agrandir) à
|
||||
// maxSize et compresse en WebP côté client avant l'envoi vers le bucket.
|
||||
// maxSize et compresse (WebP si le navigateur sait l'encoder, sinon JPEG)
|
||||
// côté client avant l'envoi vers le bucket.
|
||||
export async function cropImageToBlob(
|
||||
imageSrc: string,
|
||||
crop: PixelCrop,
|
||||
@@ -33,7 +53,7 @@ export async function cropImageToBlob(
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob(
|
||||
(blob) => (blob ? resolve(blob) : reject(new Error("Échec de la compression de l'image"))),
|
||||
"image/webp",
|
||||
preferredMimeType(),
|
||||
quality,
|
||||
);
|
||||
});
|
||||
@@ -41,10 +61,10 @@ export async function cropImageToBlob(
|
||||
|
||||
// Redimensionne (sans jamais agrandir) au format d'origine — contrairement
|
||||
// à cropImageToBlob qui force un carré pour les avatars — à maxDimension
|
||||
// sur le plus grand côté, et compresse en WebP avant l'envoi vers le
|
||||
// bucket. Le bucket a aussi sa propre limite de taille côté serveur
|
||||
// (défense en profondeur) : cette compression n'est qu'un premier filtre,
|
||||
// pas la seule garantie.
|
||||
// sur le plus grand côté, et compresse (WebP si supporté, sinon JPEG —
|
||||
// voir preferredMimeType) avant l'envoi vers le bucket. Le bucket a aussi
|
||||
// sa propre limite de taille côté serveur (défense en profondeur) : cette
|
||||
// compression n'est qu'un premier filtre, pas la seule garantie.
|
||||
export async function resizeImageToBlob(file: File, maxDimension = 1600, quality = 0.82): Promise<Blob> {
|
||||
const src = URL.createObjectURL(file);
|
||||
try {
|
||||
@@ -65,7 +85,7 @@ export async function resizeImageToBlob(file: File, maxDimension = 1600, quality
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob(
|
||||
(blob) => (blob ? resolve(blob) : reject(new Error("Échec de la compression de l'image"))),
|
||||
"image/webp",
|
||||
preferredMimeType(),
|
||||
quality,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user