Skip to main content

Image Processing

Process images client-side before uploading to reduce bandwidth and storage costs.

Image Compression

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' },

// Process files before upload
onFilesAdded: async (newFiles) => {
for (const file of newFiles) {
// Compress the image
const compressed = await compressImage(file.file, {
maxWidth: 1920,
maxHeight: 1080,
quality: 0.8,
});

// Replace with compressed version
actions.updateFileMeta(file.id, {
originalSize: file.size,
compressedFile: compressed,
});
}
},
});

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

Compression Options

interface CompressOptions {
// Maximum dimensions (maintains aspect ratio)
maxWidth?: number; // default: 1920
maxHeight?: number; // default: 1080

// Quality (0-1)
quality?: number; // default: 0.8

// Output format
type?: 'image/jpeg' | 'image/png' | 'image/webp'; // default: original type
}

// Examples
await compressImage(file, { quality: 0.6 }); // Lower quality
await compressImage(file, { maxWidth: 800, maxHeight: 600 }); // Smaller size
await compressImage(file, { type: 'image/webp' }); // Convert to WebP

Preview with 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,
}));

// Store compressed file for upload
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',
},
};

Image Resizing

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

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

// Create thumbnail
const thumbnail = await resizeImage(file, {
width: 150,
height: 150,
mode: 'cover',
});

Resize Modes

ModeDescription
coverFills the entire area, may crop
containFits within area, may have empty space
fillStretches to fill (may distort)

EXIF Orientation Fix

Some cameras save images with EXIF rotation data. Fix orientation before display:

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

const corrected = await fixOrientation(file);

Crop Image

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

const cropped = await cropImage(file, {
x: 100, // Start X
y: 50, // Start Y
width: 400, // Crop width
height: 400, // Crop height
});

Image Editor Component

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);

// Replace original with cropped version
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,
},
};

Convert Image Format

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

// Convert to WebP for smaller file size
const webp = await convertImage(file, 'image/webp');

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

// Convert to PNG (lossless)
const png = await convertImage(file, 'image/png');

Get Image Metadata

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

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

Pipeline Processing

Chain multiple operations:

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

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

// Step 1: Fix orientation
processed = await fixOrientation(processed);

// Step 2: Resize if too large
const metadata = await getImageMetadata(processed);
if (metadata.width > 2000 || metadata.height > 2000) {
processed = await resizeImage(processed, {
maxWidth: 2000,
maxHeight: 2000,
});
}

// Step 3: Compress
processed = await compressImage(processed, {
quality: 0.8,
type: 'image/webp',
});

return processed;
}

// Use in 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 });
}
},
});

Browser Support

Image processing uses the Canvas API and is supported in all modern browsers:

FeatureChromeFirefoxSafariEdge
Resize/Crop
JPEG/PNG
WebP14+
EXIF Fix