Skip to main content

ImageCompressor - Image Optimization Utility

Metadata

  • Path: components/Helper/ImageCompressor.ts
  • Category: Utility
  • Dependencies: None (Browser Canvas API)
  • Reusability Score: ⭐⭐⭐⭐⭐ (100%)
  • Framework: Framework-agnostic (pure JavaScript)

Description

Pure JavaScript utility for compressing images before upload using the browser's Canvas API. Reduces file sizes by 60-80% while maintaining acceptable quality. Essential for any application with image upload functionality.

Key Features:

  • Canvas-based compression
  • Maintains aspect ratio
  • Configurable max width and quality
  • Batch processing support
  • Error handling with fallback to original
  • No external dependencies
  • Works with FileWithPath type (Dropzone compatible)

Use Cases:

  • Profile photo uploads
  • Document scanning
  • Gallery image uploads
  • Any file upload feature

Installation

# No dependencies required - uses browser Canvas API

Complete Code

// components/Helper/ImageCompressor.ts

/**
* File type compatible with Dropzone
*/
export interface FileWithPath extends File {
path?: string;
}

/**
* Compress a single image file
*
* @param file - Image file to compress
* @param maxWidth - Maximum width in pixels (default: 800)
* @param quality - JPEG quality 0-1 (default: 0.7)
* @returns Promise with compressed file
*
* @example
* const compressed = await compressImage(file, 800, 0.7);
* console.log(`Original: ${file.size} bytes`);
* console.log(`Compressed: ${compressed.size} bytes`);
* console.log(`Reduction: ${((1 - compressed.size / file.size) * 100).toFixed(1)}%`);
*/
export const compressImage = async (
file: FileWithPath,
maxWidth: number = 800,
quality: number = 0.7
): Promise<FileWithPath> => {
return new Promise((resolve, reject) => {
// Validate file type
if (!file.type.startsWith('image/')) {
resolve(file); // Return original if not an image
return;
}

const reader = new FileReader();

reader.onload = (e) => {
const img = new Image();

img.onload = () => {
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

if (!ctx) {
resolve(file); // Fallback to original if canvas fails
return;
}

// Calculate dimensions maintaining aspect ratio
let width = img.width;
let height = img.height;

if (width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
}

// Set canvas dimensions
canvas.width = width;
canvas.height = height;

// Draw and compress
ctx.drawImage(img, 0, 0, width, height);

// Convert to blob
canvas.toBlob(
(blob) => {
if (blob) {
// Create new file from blob
const compressedFile = new File([blob], file.name, {
type: 'image/jpeg',
lastModified: Date.now(),
}) as FileWithPath;

// Preserve path if exists (for Dropzone compatibility)
if (file.path) {
compressedFile.path = file.path;
}

resolve(compressedFile);
} else {
// Fallback to original if compression fails
resolve(file);
}
},
'image/jpeg',
quality
);
} catch (error) {
console.error('Image compression error:', error);
resolve(file); // Fallback to original on error
}
};

img.onerror = () => {
console.error('Failed to load image');
reject(new Error('Failed to load image'));
};

img.src = e.target?.result as string;
};

reader.onerror = () => {
console.error('Failed to read file');
reject(new Error('Failed to read file'));
};

reader.readAsDataURL(file);
});
};

/**
* Compress multiple images in parallel
*
* @param files - Array of image files
* @param maxWidth - Maximum width in pixels (default: 800)
* @param quality - JPEG quality 0-1 (default: 0.7)
* @returns Promise with array of compressed files
*
* @example
* const compressed = await compressImages(files, 1024, 0.8);
* console.log(`Compressed ${compressed.length} images`);
*/
export const compressImages = async (
files: FileWithPath[],
maxWidth: number = 800,
quality: number = 0.7
): Promise<FileWithPath[]> => {
return Promise.all(
files.map(file => compressImage(file, maxWidth, quality))
);
};

/**
* Get image dimensions without loading the full image
*
* @param file - Image file
* @returns Promise with dimensions
*/
export const getImageDimensions = (file: File): Promise<{ width: number; height: number }> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onload = (e) => {
const img = new Image();

img.onload = () => {
resolve({
width: img.width,
height: img.height,
});
};

img.onerror = () => reject(new Error('Failed to load image'));
img.src = e.target?.result as string;
};

reader.onerror = () => reject(new Error('Failed to read file'));
reader.readAsDataURL(file);
});
};

/**
* Check if file size exceeds limit
*
* @param file - File to check
* @param maxSizeInMB - Maximum size in MB
* @returns True if file is too large
*/
export const isFileTooLarge = (file: File, maxSizeInMB: number): boolean => {
const maxSizeInBytes = maxSizeInMB * 1024 * 1024;
return file.size > maxSizeInBytes;
};

/**
* Format file size for display
*
* @param bytes - File size in bytes
* @returns Formatted string (e.g., "1.5 MB")
*/
export const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';

const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));

return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
};

Usage Examples

Basic Usage

import { compressImage } from '@/components/Helper/ImageCompressor';

const handleFileUpload = async (file: File) => {
try {
// Compress with default settings (800px, 70% quality)
const compressed = await compressImage(file);

console.log(`Original: ${formatFileSize(file.size)}`);
console.log(`Compressed: ${formatFileSize(compressed.size)}`);
console.log(`Reduction: ${((1 - compressed.size / file.size) * 100).toFixed(1)}%`);

// Upload compressed file
await uploadFile(compressed);
} catch (error) {
console.error('Compression failed:', error);
}
};

With Dropzone Integration

import { Dropzone, IMAGE_MIME_TYPE } from '@mantine/dropzone';
import { compressImages } from '@/components/Helper/ImageCompressor';

const PhotoUpload = () => {
const handleDrop = async (files: File[]) => {
// Compress all images
const compressed = await compressImages(files, 800, 0.7);

// Set to form
formik.setFieldValue('photos', compressed);

// Show success
showNotification({
message: `${compressed.length} photos compressed successfully`,
color: 'green',
});
};

return (
<Dropzone
onDrop={handleDrop}
accept={IMAGE_MIME_TYPE}
maxSize={10 * 1024 * 1024} // 10MB before compression
>
<Text>Drop images here</Text>
</Dropzone>
);
};

Custom Compression Settings

// For profile photos (smaller size, good quality)
const profilePhoto = await compressImage(file, 400, 0.8);

// For gallery images (medium size, balanced)
const galleryImage = await compressImage(file, 1024, 0.75);

// For documents/scans (larger size, high quality)
const documentScan = await compressImage(file, 1920, 0.9);

With Progress Indicator

const CompressWithProgress = () => {
const [progress, setProgress] = useState(0);

const handleCompress = async (files: File[]) => {
setProgress(0);

const compressed: File[] = [];
for (let i = 0; i < files.length; i++) {
const result = await compressImage(files[i]);
compressed.push(result);
setProgress(((i + 1) / files.length) * 100);
}

return compressed;
};

return (
<div>
<Progress value={progress} />
{/* File upload UI */}
</div>
);
};

Conditional Compression

const smartCompress = async (file: File): Promise<File> => {
const maxSize = 2 * 1024 * 1024; // 2MB

// Only compress if file is too large
if (file.size > maxSize) {
console.log('File too large, compressing...');
return await compressImage(file, 800, 0.7);
}

console.log('File size acceptable, skipping compression');
return file;
};

Check Dimensions Before Compression

import { getImageDimensions, compressImage } from '@/components/Helper/ImageCompressor';

const handleUpload = async (file: File) => {
const { width, height } = await getImageDimensions(file);

console.log(`Original dimensions: ${width}x${height}`);

// Adjust compression based on dimensions
const maxWidth = width > 2000 ? 1200 : 800;
const quality = width > 2000 ? 0.8 : 0.7;

const compressed = await compressImage(file, maxWidth, quality);

// New dimensions will be proportional
const newHeight = Math.round((height * maxWidth) / width);
console.log(`New dimensions: ${maxWidth}x${newHeight}`);
};

Configuration Recommendations

Use CaseMax WidthQualityExpected Reduction
Profile photos400px0.870-80%
Thumbnails200px0.785-90%
Gallery images800px0.760-70%
Product photos1024px0.7550-60%
Document scans1920px0.8540-50%
High-quality photos2048px0.930-40%

API Reference

compressImage()

ParameterTypeDefaultDescription
fileFileWithPath-Image file to compress
maxWidthnumber800Maximum width in pixels
qualitynumber0.7JPEG quality (0-1)

Returns: Promise<FileWithPath>

compressImages()

ParameterTypeDefaultDescription
filesFileWithPath[]-Array of image files
maxWidthnumber800Maximum width in pixels
qualitynumber0.7JPEG quality (0-1)

Returns: Promise<FileWithPath[]>

getImageDimensions()

ParameterTypeDescription
fileFileImage file

Returns: Promise<{ width: number; height: number }>

isFileTooLarge()

ParameterTypeDescription
fileFileFile to check
maxSizeInMBnumberMaximum size in MB

Returns: boolean

formatFileSize()

ParameterTypeDescription
bytesnumberFile size in bytes

Returns: string (e.g., "1.5 MB")

Customization Guide

Different Output Format

// Use PNG instead of JPEG (no quality loss, larger file)
canvas.toBlob(
(blob) => {
const file = new File([blob], filename, { type: 'image/png' });
resolve(file);
},
'image/png' // Change format here
);

// Use WebP (better compression, not all browsers support)
canvas.toBlob(
(blob) => {
const file = new File([blob], filename, { type: 'image/webp' });
resolve(file);
},
'image/webp',
0.8 // WebP quality
);

Preserve Original Filename Extension

const getOutputFormat = (originalType: string): string => {
if (originalType === 'image/png') return 'image/png';
if (originalType === 'image/webp') return 'image/webp';
return 'image/jpeg'; // Default
};

// In compression
canvas.toBlob(
(blob) => { /* ... */ },
getOutputFormat(file.type),
quality
);

Add Watermark

const compressWithWatermark = async (
file: File,
watermarkText: string
): Promise<File> => {
// ... compression code ...

// After drawing image, add watermark
ctx.font = '20px Arial';
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.fillText(watermarkText, 10, canvas.height - 10);

// ... rest of code
};

Limit Maximum File Size

const compressToTargetSize = async (
file: File,
targetSizeKB: number,
maxWidth: number = 800
): Promise<File> => {
let quality = 0.9;
let compressed = await compressImage(file, maxWidth, quality);

// Reduce quality until target size reached
while (compressed.size > targetSizeKB * 1024 && quality > 0.3) {
quality -= 0.1;
compressed = await compressImage(file, maxWidth, quality);
}

return compressed;
};

Common Pitfalls

1. Compressing Non-Images

// ❌ Will error on PDF, DOC, etc
const compressed = await compressImage(pdfFile);

// ✅ Check file type first
if (file.type.startsWith('image/')) {
const compressed = await compressImage(file);
} else {
// Handle non-image files differently
}

2. Quality Too Low

// ❌ Too aggressive, visible quality loss
const compressed = await compressImage(file, 800, 0.3);

// ✅ Reasonable quality (0.6-0.8)
const compressed = await compressImage(file, 800, 0.7);

3. Not Handling Errors

// ❌ No error handling
const compressed = await compressImage(file);

// ✅ With error handling and fallback
try {
const compressed = await compressImage(file);
uploadFile(compressed);
} catch (error) {
console.error('Compression failed:', error);
// Upload original file as fallback
uploadFile(file);
}

4. Blocking UI on Large Batches

// ❌ Blocks UI during compression
const compressed = await compressImages(manyFiles);

// ✅ Show progress, process in chunks
const compressInChunks = async (files: File[], chunkSize: number = 5) => {
const results: File[] = [];

for (let i = 0; i < files.length; i += chunkSize) {
const chunk = files.slice(i, i + chunkSize);
const compressed = await compressImages(chunk);
results.push(...compressed);

// Update progress
setProgress(((i + chunkSize) / files.length) * 100);
}

return results;
};

Performance Considerations

Memory Usage

// For very large images, consider reducing maxWidth
const largeImage = file.size > 5 * 1024 * 1024;
const maxWidth = largeImage ? 1024 : 800;

Browser Compatibility

// Check Canvas API support
const isCanvasSupported = (): boolean => {
const canvas = document.createElement('canvas');
return !!(canvas.getContext && canvas.getContext('2d'));
};

if (!isCanvasSupported()) {
console.warn('Canvas API not supported, skipping compression');
return file; // Return original
}

Parallel vs Sequential

// Parallel (faster, more memory)
const compressed = await Promise.all(
files.map(f => compressImage(f))
);

// Sequential (slower, less memory)
const compressed: File[] = [];
for (const file of files) {
compressed.push(await compressImage(file));
}
  • FieldDropZone - Use together for file upload with compression
  • FieldCompressImageDropZone - Pre-integrated component
  • ModalFileViewer - Preview compressed images
  • ExportButtons - Compress images before export

Browser Support

  • ✅ Chrome/Edge 4+
  • ✅ Firefox 3.6+
  • ✅ Safari 3.1+
  • ✅ Opera 9+
  • ✅ IE 9+ (with polyfills)
  • ✅ All modern mobile browsers

Testing

describe('ImageCompressor', () => {
it('compresses image and reduces file size', async () => {
const file = new File(['...'], 'test.jpg', { type: 'image/jpeg' });
const compressed = await compressImage(file, 800, 0.7);

expect(compressed.size).toBeLessThan(file.size);
expect(compressed.type).toBe('image/jpeg');
});

it('handles non-image files gracefully', async () => {
const pdfFile = new File(['...'], 'test.pdf', { type: 'application/pdf' });
const result = await compressImage(pdfFile);

expect(result).toBe(pdfFile); // Returns original
});

it('maintains aspect ratio', async () => {
const file = new File(['...'], 'test.jpg', { type: 'image/jpeg' });
const dimensions = await getImageDimensions(file);

// Test aspect ratio preservation
const aspectRatio = dimensions.width / dimensions.height;
// ... additional tests
});
});

License

MIT - Free to use in any project

Support

For issues or questions, refer to the main documentation in REUSABLE_COMPONENTS_GUIDE.md