magic-webp
v1.0.11
Published
Convert PNG/JPEG/GIF to WebP and process images (crop, resize) in the browser using WebAssembly. Supports animated GIFs.
Maintainers
Readme
🎨 magic-webp
Convert PNG/JPEG/GIF to WebP and process images in the browser using WebAssembly
Convert images to WebP and process them (crop, resize) directly in the browser with native performance. Built on top of Google's libwebp compiled to WebAssembly.
🎮 Live Demo • Features • Installation • Quick Start • API
✨ Features
- 🔄 Convert to WebP — PNG, JPEG, GIF → WebP (60-80% smaller files)
- 🎬 Animated GIF Support — Converts animated GIFs to animated WebP
- ✂️ Crop — Extract regions (preserves animation frames)
- 📐 Resize — Multiple modes: cover, contain, fill, inside, outside
- 🎚️ Quality Control — Lossy (0-100) or lossless compression
- 🚀 Fast — Native libwebp with SIMD optimizations (5-10x faster)
- 🌐 Browser-first — No server required, runs entirely client-side
- 🔒 Thread-safe — Automatic operation queuing for concurrent calls
- 📦 Zero dependencies — Pure WebAssembly, no external libraries
📦 Installation
npm install magic-webp
# or
pnpm add magic-webp
# or
yarn add magic-webp🚀 Quick Start
Convert Images to WebP
import { MagicWebp } from 'magic-webp';
// Convert PNG/JPEG/GIF to WebP
const file = document.querySelector('input[type="file"]').files[0];
const webp = await MagicWebp.convert(file, 75, false); // quality: 75, lossless: false
// Get the result
const blob = webp.toBlob();
const url = URL.createObjectURL(blob);
// Download or display
document.querySelector('img').src = url;Process WebP Images
⚠️ Important:
fromBlob,fromBytes, andfromUrlaccept WebP files only. To process PNG/JPEG/GIF, useMagicWebp.convert()first.
import { MagicWebp } from 'magic-webp';
// Convert to WebP first (if input is PNG/JPEG/GIF)
const file = document.querySelector('input[type="file"]').files[0];
const webp = await MagicWebp.convert(file, 75, false);
// Then process: resize
const resized = await webp.resize(400, 400, { mode: 'cover', quality: 75 });
// Or crop
const cropped = await webp.crop(0, 0, 200, 200, 75);
// Get result
const blob = resized.toBlob();Using Web Worker (Recommended for Production)
import { MagicWebpWorker } from 'magic-webp';
import WorkerUrl from 'magic-webp/worker?worker&url'; // Vite only
const worker = new MagicWebpWorker(WorkerUrl);
// Convert in background
const blob = await worker.convert(file, 75, false);
// Or load and process
await worker.load(webpFile);
const resized = await worker.resize(400, 400, { mode: 'cover' });
worker.terminate();✨ Benefits: Non-blocking UI, better performance, automatic request queuing
⚠️ Important: Web Worker Requirements
1. Same-Origin Policy
- Worker file must be served from the same domain as your app
- ❌ Won't work:
new MagicWebpWorker('https://cdn.example.com/worker.js') - ✅ Works:
new MagicWebpWorker('/worker.js')(same domain)
2. Module Type
- Worker must be loaded as ES module (
type: 'module') - Already handled by
MagicWebpWorkerconstructor
3. CORS Headers (if serving from different path)
- If worker is on subdomain, ensure proper CORS headers:
Access-Control-Allow-Origin: *
4. File Serving
- Worker file must be accessible via HTTP/HTTPS
- ❌ Won't work with
file://protocol (local files) - ✅ Use local dev server:
npx serveorpython -m http.server
5. Build Tools
- Vite: Use
?worker&urlimport syntax:import WorkerUrl from 'magic-webp/worker?worker&url'; const webp = new MagicWebpWorker(WorkerUrl); - Webpack: Use
worker-loaderor native Worker support - Without bundler: Copy
node_modules/magic-webp/lib/worker.jsto your public folder and usenew MagicWebpWorker('/worker.js')
Common Issues:
// ❌ WRONG: Cross-origin
const webp = new MagicWebpWorker('https://cdn.com/worker.js');
// Error: Failed to construct 'Worker': Script at '...' cannot be accessed from origin '...'
// ✅ CORRECT: Vite
import WorkerUrl from 'magic-webp/worker?worker&url';
const webp = new MagicWebpWorker(WorkerUrl);
// ✅ CORRECT: Without bundler (copy lib/worker.js to public/ first)
const webp = new MagicWebpWorker('/worker.js');Alternative: Main Thread (Simpler, but blocks UI)
⚠️ Important:
fromBlobaccepts WebP files only. UseMagicWebp.convert()first to convert PNG/JPEG/GIF.
import { MagicWebp } from 'magic-webp';
// If input is already WebP:
const img = await MagicWebp.fromBlob(webpFile);
// If input is PNG/JPEG/GIF — convert first:
// const img = await MagicWebp.convert(file, 75, false);
const resized = await img.resize(400, 400, { mode: 'cover', quality: 75 });
const blob = resized.toBlob();⚠️ Note: Main thread usage blocks the UI during processing. Use
MagicWebpWorkerfor production apps.
📖 API
Convert Images
import { MagicWebp } from 'magic-webp';
// Convert PNG/JPEG/GIF to WebP
const webp = await MagicWebp.convert(
blob, // File or Blob
75, // quality: 0-100 (default: 75)
false // lossless: true/false (default: false)
);
// Animated GIF → Animated WebP (preserves all frames!)
const animatedWebp = await MagicWebp.convert(gifBlob, 75, false);Load WebP Images
import { MagicWebp } from 'magic-webp';
// From File/Blob (WebP only)
const webp = await MagicWebp.fromBlob(blob);
// From URL
const webp = await MagicWebp.fromUrl('https://example.com/image.webp');
// From Uint8Array
const webp = await MagicWebp.fromBytes(uint8Array);Process Images
// Crop
const cropped = await webp.crop(x, y, width, height, quality);
// Resize
const resized = await webp.resize(width, height, { mode, position, quality });
// Get result
const blob = webp.toBlob();
const bytes = webp.toBytes();Using Web Worker
import { MagicWebpWorker } from 'magic-webp';
import WorkerUrl from 'magic-webp/worker?worker&url'; // Vite only
const worker = new MagicWebpWorker(WorkerUrl);
// Convert
const blob = await worker.convert(file, 75, false);
// Load and process
await worker.load(webpFile);
const resized = await worker.resize(400, 400, { mode: 'cover' });
worker.terminate();Transformations
All transformation methods are async and return Promise<MagicWebp>.
Crop
// Crop 200x200 region starting at (50, 50)
const cropped = await img.crop(50, 50, 200, 200, quality);Resize
// Cover - fills dimensions, crops excess (default)
const cover = await img.resize(400, 400, { mode: 'cover' });
// Contain - fits inside dimensions, preserves aspect ratio
const contain = await img.resize(400, 400, { mode: 'contain' });
// Fill - stretches to exact dimensions (may distort)
const fill = await img.resize(400, 400, { mode: 'fill' });
// Inside - like contain, but never enlarges
const inside = await img.resize(400, 400, { mode: 'inside' });
// Outside - like cover, but never reduces
const outside = await img.resize(400, 400, { mode: 'outside' });
// With position (for cover/outside modes)
const banner = await img.resize(1200, 400, {
mode: 'cover',
position: 'top', // 'center', 'top', 'bottom', 'left', 'right', etc.
quality: 75 // 0-100, default 75 (balanced - recommended)
});Output
// As Blob
const blob = img.toBlob();
// As Uint8Array
const bytes = img.toBytes();
// As Data URL
const dataUrl = await img.toDataUrl();
// As Object URL
const objectUrl = img.toObjectUrl();Resize Options
interface ResizeOptions {
mode?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside'; // default: 'cover'
position?: 'center' | 'top' | 'bottom' | 'left' | 'right' | // default: 'center'
'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
quality?: number; // 0-100, default: 75 (balanced)
}💡 Quality Recommendations:
| Quality | File Size | Visual Quality | Use Case | Recommended For | |---------|-----------|----------------|----------|-----------------| | 60-70 | Smallest | Visible artifacts | Thumbnails, previews | Low priority images | | 75-85 | Medium | Good balance | Most web images | ✅ Default (75) | | 90-95 | Large | Excellent | Important photos | Hero images, portfolios | | 100 | Largest | Perfect (lossless) | Archival, editing | When quality is critical |
Quality Recommendations
- 60-70: High compression, visible artifacts (good for thumbnails)
- 75-85: Balanced quality/size (recommended for most cases)
- 90-95: High quality, minimal artifacts (for important images)
- 100: Lossless, largest file size (perfect quality preservation)
Supported Formats
Conversion (to WebP):
- ✅ PNG → WebP (static)
- ✅ JPEG → WebP (static)
- ✅ GIF → WebP (animated, preserves all frames!)
- ✅ WebP → WebP (re-encode with different quality)
Processing (WebP only):
- ✅ Static WebP (crop, resize)
- ✅ Animated WebP (crop, resize - all frames processed)
Output:
- ✅ WebP (lossy or lossless)
- ✅ Animated WebP (preserves timing, loop count)
Note: All operations (crop, resize) work on both static and animated WebP. For animated images, each frame is processed individually while preserving animation metadata.
💡 Examples
Convert and Optimize
import { MagicWebp } from 'magic-webp';
// Simple conversion (PNG/JPEG → WebP)
const webp = await MagicWebp.convert(pngFile, 75, false);
const blob = webp.toBlob(); // 60-80% smaller!
// Lossless conversion (perfect quality)
const lossless = await MagicWebp.convert(pngFile, 100, true);
// Animated GIF → Animated WebP
const animatedWebp = await MagicWebp.convert(gifFile, 75, false);
// Preserves all frames and timing!
// Convert and resize in one go
const webp = await MagicWebp.convert(jpegFile, 80, false);
const thumbnail = await webp.resize(200, 200, { mode: 'cover' });Process WebP Images
import { MagicWebpWorker } from 'magic-webp';
import WorkerUrl from 'magic-webp/worker?worker&url'; // Vite only
const worker = new MagicWebpWorker(WorkerUrl);
await worker.load(webpFile);
// Avatar - square 200x200, centered
const avatar = await worker.resize(200, 200, { mode: 'cover', quality: 85 });
// Product preview - fit inside 300x300
const preview = await worker.resize(300, 300, { mode: 'contain', quality: 75 });
// Banner - 1200x400, crop from top
const banner = await worker.resize(1200, 400, {
mode: 'cover',
position: 'top',
quality: 90
});
// Crop specific region
const cropped = await worker.crop(50, 50, 200, 200, 75);
worker.terminate();Batch Processing
import { MagicWebp } from 'magic-webp';
// Convert multiple images
const files = Array.from(fileInput.files);
const converted = await Promise.all(
files.map(file => MagicWebp.convert(file, 75, false))
);
// Get all blobs
const blobs = converted.map(webp => webp.toBlob());🔧 Advanced Usage
Memory Management
import WorkerUrl from 'magic-webp/worker?worker&url'; // Vite only
// Worker automatically manages memory, but you should terminate when done
const webp = new MagicWebpWorker(WorkerUrl);
// ... use worker ...
// Clean up when component unmounts or app closes
webp.terminate();Debug Mode
By default, magic-webp runs silently in production (no console logs). Enable debug mode for development:
import { setDebugMode } from 'magic-webp';
// Enable debug logging (disabled by default)
setDebugMode(true);
// Now you'll see detailed logs:
// [magic-webp] Loading Emscripten WASM module...
// [magic-webp] WASM module ready
// [magic-webp] Processing 45678 bytes
// [magic-webp] Cropping: 0,0 200x200, quality: 75
// etc.
// Disable debug mode
setDebugMode(false);💡 Tip: Enable debug mode only during development. In production, logs are automatically disabled for better performance and cleaner console.
Error Handling
import WorkerUrl from 'magic-webp/worker?worker&url'; // Vite only
const webp = new MagicWebpWorker(WorkerUrl);
try {
await webp.load(file);
const blob = await webp.resize(400, 400, { mode: 'cover' });
} catch (error) {
console.error('Processing failed:', error);
// Handle error (show message to user, retry, etc.)
}Multiple Workers (Parallel Processing)
import WorkerUrl from 'magic-webp/worker?worker&url'; // Vite only
// Create multiple workers for parallel processing
const workers = [
new MagicWebpWorker(WorkerUrl),
new MagicWebpWorker(WorkerUrl),
new MagicWebpWorker(WorkerUrl)
];
// Process multiple images in parallel
const results = await Promise.all(
files.map((file, i) => {
const worker = workers[i % workers.length];
return worker.load(file).then(() =>
worker.resize(400, 400, { mode: 'cover' })
);
})
);
// Clean up
workers.forEach(w => w.terminate());Browser Compatibility
- ✅ Chrome 80+
- ✅ Firefox 79+
- ✅ Safari 14+
- ✅ Edge 80+
- ❌ IE 11 (no WebAssembly support)
Check before using:
if (typeof WebAssembly === 'undefined') {
console.error('WebAssembly not supported');
// Fallback to server-side processing
}
if (typeof Worker === 'undefined') {
console.warn('Web Workers not supported, using main thread');
// Use MagicWebp instead of MagicWebpWorker
}🛠️ Development
Prerequisites
- Node.js 18+
- pnpm (recommended) or npm
- Emscripten SDK (automatically installed)
Setup
# Clone repository
git clone https://github.com/medzhidov/magic-webp.git
cd magic-webp
# Install dependencies
pnpm install
# Build WASM module
pnpm build:wasm
# Run demo
pnpm demo:watchProject Structure
magic-webp/
├── src-c/ # C source code
│ ├── animation.c # WebP animation processing
│ └── magic_webp.c # Core functions
├── src-js/ # TypeScript API
│ ├── index.ts # Main API
│ └── *.test.ts # Tests
├── demo/ # Demo application
│ ├── index.html
│ ├── main.ts
│ └── worker.ts # Web Worker for processing
├── tests/ # Native C tests
├── libwebp/ # Google's libwebp (submodule)
└── lib/ # Build output (TypeScript + WASM, generated)Build Commands
# Build WASM module
pnpm build:wasm
# Run TypeScript tests
pnpm test
# Run native C tests
pnpm test:native
# Run demo (dev server)
pnpm demo:watch
# Build demo for production
pnpm demo:buildHow It Works
- C Code (
src-c/) - Uses libwebp's WebPPicture API for high-quality image processing - Animation Support - Processes each frame individually, preserving timing and metadata
- Emscripten - Compiles C code to WebAssembly with SIMD optimizations
- TypeScript API (
src-js/) - Provides clean, type-safe interface - Operation Queue - Ensures thread-safety by serializing WASM calls
- Web Worker (demo) - Keeps UI responsive during processing
Performance
- 5-10x faster than pure JavaScript implementations
- SIMD optimizations (SSE2, NEON) for resize operations
- Optimized cover mode - single pass resize+crop (2x faster)
- Minimal memory - in-place operations where possible
📋 Quick Reference
Resize Modes
| Mode | Behavior | Use Case |
|------|----------|----------|
| cover | Fills dimensions, crops excess | Avatars, thumbnails |
| contain | Fits inside, preserves aspect | Product images, previews |
| fill | Stretches to exact size | Backgrounds (may distort) |
| inside | Like contain, never enlarges | Thumbnails of small images |
| outside | Like cover, never reduces | Cropping large images |
Position Options (for cover/outside)
| Position | Description |
|----------|-------------|
| center | Center of image (default) |
| top | Top center |
| bottom | Bottom center |
| left | Left center |
| right | Right center |
| top-left | Top left corner |
| top-right | Top right corner |
| bottom-left | Bottom left corner |
| bottom-right | Bottom right corner |
Quality Guidelines
| Quality | File Size | Visual Quality | Use Case | |---------|-----------|----------------|----------| | 60-70 | Smallest | Visible artifacts | Thumbnails, previews | | 75-85 | Medium | Good balance | Most web images | | 90-95 | Large | Excellent | Important images, photos | | 100 | Largest | Perfect (lossless) | Archival, editing |
📄 License
MIT © Ilia Medzhidov
🙏 Acknowledgments
- libwebp - Google's WebP library
- Emscripten - C/C++ to WebAssembly compiler
Made with ❤️ for the web
