feat: ZIP import, rotation, feather slider, lightbox + config déploiement

- Import ZIP produits (HEIC/JPG/PNG, structure dossiers conservée)
- Rotation des images ±90° avec aperçu CSS et export canvas
- Slider récupération des bords (feather 0–5 px)
- Lightbox plein écran (clic miniature, Échap, fond noir)
- Fix timeout HEIC 30s → 120s
- Fix MIME type blobs JSZip pour heic2any
- Dockerfile nginx:alpine + docker-compose.yml pour déploiement

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 11:18:18 +02:00
parent 7f6ca4c3de
commit 86d9b351b1
9 changed files with 368 additions and 81 deletions
+15 -12
View File
@@ -1,28 +1,31 @@
/**
* Convertit un fichier HEIC/HEIF en Blob JPEG.
* @param {File} file
* Convertit un fichier/blob HEIC/HEIF en Blob JPEG.
* @param {File|Blob} fileOrBlob — fichier HEIC/HEIF (File depuis l'input, ou Blob extrait d'un ZIP)
* @returns {Promise<Blob>}
* @throws {TypeError} si l'entrée n'est pas un fichier HEIC/HEIF
* @throws {TypeError} si l'entrée n'est pas un Blob HEIC/HEIF
* @throws {Error} si la conversion échoue ou dépasse 30s
*/
export async function convertHeicToJpeg(file) {
if (!(file instanceof File)) {
throw new TypeError("L'entrée doit être un objet File");
export async function convertHeicToJpeg(fileOrBlob) {
if (!(fileOrBlob instanceof Blob)) {
throw new TypeError("L'entrée doit être un Blob ou un File");
}
// Certains iPhone envoient un type MIME vide pour les HEIC — on l'autorise
const type = file.type.toLowerCase();
const type = fileOrBlob.type.toLowerCase();
if (type !== '' && !['image/heic', 'image/heif'].includes(type)) {
throw new TypeError(`Format attendu : HEIC/HEIF. Reçu : ${file.type || '(vide)'}`);
throw new TypeError(`Format attendu : HEIC/HEIF. Reçu : ${fileOrBlob.type || '(vide)'}`);
}
console.log(`[heicConverter] Début conversion : ${file.name} (${file.type || 'type vide'}, ${(file.size/1024).toFixed(0)} Ko)`);
const label = fileOrBlob instanceof File
? `${fileOrBlob.name} (${fileOrBlob.type || 'type vide'}, ${(fileOrBlob.size / 1024).toFixed(0)} Ko)`
: `blob HEIC (${(fileOrBlob.size / 1024).toFixed(0)} Ko)`;
console.log(`[heicConverter] Début conversion : ${label}`);
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout : conversion > 30s. Vérifiez que le fichier est bien un HEIC valide.')), 30_000)
setTimeout(() => reject(new Error('Timeout : conversion > 120s. Vérifiez que le fichier est bien un HEIC valide.')), 120_000)
);
const conversion = heic2any({
blob: file,
blob: fileOrBlob,
toType: 'image/jpeg',
quality: 0.92,
});
@@ -34,6 +37,6 @@ export async function convertHeicToJpeg(file) {
throw new Error("heic2any n'a pas retourné un Blob");
}
console.log(`[heicConverter] ✅ Conversion réussie : ${file.name}${(blob.size/1024).toFixed(0)} Ko`);
console.log(`[heicConverter] ✅ Conversion réussie → ${(blob.size / 1024).toFixed(0)} Ko`);
return blob;
}