Aller au contenu principal

Traitement d'images

Traitez les images côté client avant le téléchargement pour réduire les coûts de bande passante et de stockage.

Compression d'image

import { useDropup } from '@samithahansaka/dropup';
import { compressImage } from '@samithahansaka/dropup/image';

function CompressedUploader() {
const { files, actions, getDropProps, getInputProps } = useDropup({
accept: 'image/*',
upload: { url: '/api/upload' },

// Traiter les fichiers avant le téléchargement
onFilesAdded: async (newFiles) => {
for (const file of newFiles) {
// Compresser l'image
const compressed = await compressImage(file.file, {
maxWidth: 1920,
maxHeight: 1080,
quality: 0.8,
});

// Remplacer par la version compressée
actions.updateFileMeta(file.id, {
originalSize: file.size,
compressedFile: compressed,
});
}
},
});

return (
<div {...getDropProps()}>
<input {...getInputProps()} />
<p>Images will be compressed before upload</p>
</div>
);
}

Options de compression

interface CompressOptions {
// Dimensions maximales (conserve le rapport d'aspect)
maxWidth?: number; // par défaut : 1920
maxHeight?: number; // par défaut : 1080

// Qualité (0-1)
quality?: number; // par défaut : 0.8

// Format de sortie
type?: 'image/jpeg' | 'image/png' | 'image/webp'; // par défaut : type original
}

// Exemples
await compressImage(file, { quality: 0.6 }); // Qualité inférieure
await compressImage(file, { maxWidth: 800, maxHeight: 600 }); // Taille plus petite
await compressImage(file, { type: 'image/webp' }); // Convertir en WebP

Aperçu avec compression

import { useDropup } from '@samithahansaka/dropup';
import { compressImage } from '@samithahansaka/dropup/image';
import { useState } from 'react';

function PreviewWithCompression() {
const [compressionStats, setCompressionStats] = useState<Map<string, {
original: number;
compressed: number;
}>>(new Map());

const { files, actions, getDropProps, getInputProps } = useDropup({
accept: 'image/*',

onFilesAdded: async (newFiles) => {
for (const file of newFiles) {
const compressed = await compressImage(file.file, {
maxWidth: 1200,
quality: 0.75,
});

setCompressionStats(prev => new Map(prev).set(file.id, {
original: file.size,
compressed: compressed.size,
}));

// Stocker le fichier compressé pour le téléchargement
actions.updateFileMeta(file.id, { compressedFile: compressed });
}
},
});

const formatSize = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
};

return (
<div>
<div {...getDropProps()} style={styles.dropzone}>
<input {...getInputProps()} />
<p>Drop images to compress</p>
</div>

<div style={styles.grid}>
{files.map(file => {
const stats = compressionStats.get(file.id);
const savings = stats
? ((1 - stats.compressed / stats.original) * 100).toFixed(0)
: 0;

return (
<div key={file.id} style={styles.card}>
{file.preview && (
<img src={file.preview} alt="" style={styles.preview} />
)}
<p>{file.name}</p>
{stats && (
<p style={styles.stats}>
{formatSize(stats.original)}{formatSize(stats.compressed)}
<span style={styles.savings}> (-{savings}%)</span>
</p>
)}
</div>
);
})}
</div>
</div>
);
}

const styles = {
dropzone: {
border: '2px dashed #ccc',
borderRadius: 8,
padding: 40,
textAlign: 'center' as const,
marginBottom: 20,
},
grid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
gap: 16,
},
card: {
border: '1px solid #eee',
borderRadius: 8,
padding: 12,
},
preview: {
width: '100%',
height: 150,
objectFit: 'cover' as const,
borderRadius: 4,
},
stats: {
fontSize: 12,
color: '#666',
},
savings: {
color: '#4caf50',
fontWeight: 'bold',
},
};

Redimensionnement d'image

import { resizeImage } from '@samithahansaka/dropup/image';

// Redimensionner aux dimensions exactes
const resized = await resizeImage(file, {
width: 300,
height: 300,
mode: 'cover', // 'cover' | 'contain' | 'fill'
});

// Créer une miniature
const thumbnail = await resizeImage(file, {
width: 150,
height: 150,
mode: 'cover',
});

Modes de redimensionnement

ModeDescription
coverRemplit toute la zone, peut recadrer
containS'adapte dans la zone, peut avoir un espace vide
fillS'étire pour remplir (peut déformer)

Correction de l'orientation EXIF

Certains appareils photo enregistrent des images avec des données de rotation EXIF. Corrigez l'orientation avant l'affichage :

import { fixOrientation } from '@samithahansaka/dropup/image';

const corrected = await fixOrientation(file);

Recadrer l'image

import { cropImage } from '@samithahansaka/dropup/image';

const cropped = await cropImage(file, {
x: 100, // Début X
y: 50, // Début Y
width: 400, // Largeur de recadrage
height: 400, // Hauteur de recadrage
});

Composant éditeur d'image

import { useDropup } from '@samithahansaka/dropup';
import { compressImage, cropImage } from '@samithahansaka/dropup/image';
import { useState, useRef } from 'react';

function ImageEditor() {
const [selectedFile, setSelectedFile] = useState<DropupFile | null>(null);
const [cropArea, setCropArea] = useState({ x: 0, y: 0, width: 200, height: 200 });

const { files, actions, getDropProps, getInputProps } = useDropup({
accept: 'image/*',
maxFiles: 1,
multiple: false,
});

const handleCrop = async () => {
if (!selectedFile) return;

const cropped = await cropImage(selectedFile.file, cropArea);

// Remplacer l'original par la version recadrée
actions.updateFileMeta(selectedFile.id, {
processedFile: cropped,
});
};

const handleCompress = async () => {
if (!selectedFile) return;

const compressed = await compressImage(selectedFile.file, {
quality: 0.7,
});

actions.updateFileMeta(selectedFile.id, {
processedFile: compressed,
});
};

return (
<div style={styles.container}>
{files.length === 0 ? (
<div {...getDropProps()} style={styles.dropzone}>
<input {...getInputProps()} />
<p>Drop an image to edit</p>
</div>
) : (
<div style={styles.editor}>
<div style={styles.preview}>
<img
src={files[0].preview}
alt=""
style={styles.image}
onClick={() => setSelectedFile(files[0])}
/>
</div>

<div style={styles.tools}>
<h4>Tools</h4>
<button onClick={handleCrop}>Crop</button>
<button onClick={handleCompress}>Compress</button>
<button onClick={() => actions.reset()}>Remove</button>
</div>
</div>
)}
</div>
);
}

const styles = {
container: {
maxWidth: 600,
margin: '0 auto',
},
dropzone: {
border: '2px dashed #ccc',
borderRadius: 8,
padding: 60,
textAlign: 'center' as const,
},
editor: {
display: 'flex',
gap: 20,
},
preview: {
flex: 1,
},
image: {
maxWidth: '100%',
borderRadius: 8,
},
tools: {
width: 150,
display: 'flex',
flexDirection: 'column' as const,
gap: 8,
},
};

Convertir le format d'image

import { convertImage } from '@samithahansaka/dropup/image';

// Convertir en WebP pour une taille de fichier plus petite
const webp = await convertImage(file, 'image/webp');

// Convertir en JPEG
const jpeg = await convertImage(file, 'image/jpeg', { quality: 0.9 });

// Convertir en PNG (sans perte)
const png = await convertImage(file, 'image/png');

Obtenir les métadonnées d'image

import { getImageMetadata } from '@samithahansaka/dropup/image';

const metadata = await getImageMetadata(file);
console.log(metadata);
// {
// width: 1920,
// height: 1080,
// aspectRatio: 1.78,
// orientation: 1, // Orientation EXIF
// hasAlpha: false,
// format: 'image/jpeg',
// }

Traitement en pipeline

Enchaînez plusieurs opérations :

import {
compressImage,
fixOrientation,
resizeImage,
} from '@samithahansaka/dropup/image';

async function processImage(file: File): Promise<File> {
let processed = file;

// Étape 1 : Corriger l'orientation
processed = await fixOrientation(processed);

// Étape 2 : Redimensionner si trop grand
const metadata = await getImageMetadata(processed);
if (metadata.width > 2000 || metadata.height > 2000) {
processed = await resizeImage(processed, {
maxWidth: 2000,
maxHeight: 2000,
});
}

// Étape 3 : Compresser
processed = await compressImage(processed, {
quality: 0.8,
type: 'image/webp',
});

return processed;
}

// Utiliser dans l'uploader
const { files } = useDropup({
accept: 'image/*',
onFilesAdded: async (newFiles) => {
for (const file of newFiles) {
const processed = await processImage(file.file);
actions.updateFileMeta(file.id, { processedFile: processed });
}
},
});

Compatibilité des navigateurs

Le traitement d'image utilise l'API Canvas et est pris en charge dans tous les navigateurs modernes :

FonctionnalitéChromeFirefoxSafariEdge
Redimensionner/Recadrer
JPEG/PNG
WebP14+
Correction EXIF