wasm-imagequant
v0.1.4
Published
High-performance perceptual color quantization and PNG compressor in WebAssembly (Rust)
Maintainers
Readme
wasm-imagequant
🌟 Overview & Why wasm-imagequant?
For years, libimagequant (the underlying engine of pngquant) has been the gold standard for 8-bit palette color quantization. However, it imposes two severe constraints in modern web and commercial environments:
- GPL v3 Viral Licensing Barrier: Its licensing model forces any proprietary, commercial, or paid software using it to open-source their entire codebase, or purchase expensive commercial exceptions.
- WebAssembly Impedance Mismatch: Its architecture is heavily tied to POSIX threads, Rayon multi-threading, and non-trivial memory layouts, making it bloated, difficult to integrate, and sub-optimal in single-threaded WebAssembly sandboxes.
wasm-imagequant was built from the ground up to solve this. Released under the permissive MIT / Apache-2.0 dual licenses, it is completely commercial-friendly and unencumbered by copyleft restrictions. Powered by modern color science (Oklab Perceptual Color Space), it achieves superior color fidelity, smaller file sizes, predictable memory consumption, and blazing performance.
📊 Comprehensive Benchmarks (vs libimagequant)
1. Synthetic Challenge Benchmark (Gradient + Noise + Translucency Alpha, 800×600)
Run
pnpm run bench(orcargo run --release --example compare) from root to reproduce.
| Metric | Industry Standard libimagequant | Engine wasm-imagequant | Difference & Advantage |
| :--- | :--- | :--- | :--- |
| Perceptual Error (Oklab MSE) | 0.000948 | 0.000563 | ★ 40.63% lower error, no color banding or hue shifts |
| Quantization & Remap Time | 60.67 ms | 46.56 ms | ★ 1.30x faster, VP-Tree metric cache converges rapidly |
| Peak Runtime Memory | Dynamic matrix + Rayon state | $O(\text{Width})$ Rolling Ring Buffer | ★ 99.9% lower memory, zero WASM 32-bit OOM hazard |
| Binary Size & Dependencies | C/FFI or complex multi-thread runtime | Zero external C dependencies, WASM is 117 KB | ★ Ultra-lightweight, instant browser bootstrap |
| PNG Packing Depth | Palette only (requires external encoder) | Built-in Adaptive 1/2/4/8-bit PNG Encoder | ★ 50%~80% extra reduction on simple icons and diagrams |
| Commercial License | GPL v3 (forces open-source derivative) | Permissive (MIT / Apache-2.0) | ★ 100% free for proprietary and commercial software |
2. Real-World Ultra-HD Wallpaper Tests (1080P / 4K UHD / 12 Megapixels)
Run
pnpm run bench:photos(orcargo run --release --example test_user_photos) to test on real-world photos.
| Test Image & Resolution | Raw Bitmap (RGBA) | Quantizer Engine | Pure Remap Time | Output PNG Size | Total Compression | Perceptual Error (Oklab MSE) |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| bingimg_20230504_UHD(1920×1080, 2.07 MP) | 7.91 MB | wasm-imagequantlibimagequant | 346.3 ms343.8 ms | 1,061 KB1,127 KB | 86.90%86.08% | 0.000086 (7.0% better)0.000092 |
| bingimg_20230806_UHD(3840×2160 4K, 8.29 MP) | 31.64 MB | wasm-imagequantlibimagequant | 1,900.4 ms1,337.3 ms | 5,389 KB5,829 KB | 83.37%82.01% | 0.000123 (25.6% better)0.000166 |
| Seaside-Nature-Scenery(3992×2992, 11.94 MP) | 45.56 MB | wasm-imagequantlibimagequant | 1,567.4 ms1,338.5 ms | 4,380 KB5,495 KB | 90.61%88.22% | 0.000044 (Better fidelity)0.000045 |
[!NOTE]
libimagequantautomatically uses all available CPU cores on native Linux via Rayon;wasm-imagequantprocesses 12 Megapixels in ~1.5s on a single CPU core, achieving an algorithmic throughput of over 8 million pixels/sec.- On the 12-Megapixel wallpaper,
wasm-imagequantgenerates a PNG that is 1.11 MB smaller (20.3% smaller file size) thanlibimagequant.
🔬 Architectural Innovations & Pipeline
flowchart TD
classDef step fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#f8fafc;
classDef win fill:#064e3b,stroke:#34d399,stroke-width:2px,color:#f8fafc;
A["Raw RGBA Pixels Buffer (Uncompressed)"]:::step --> B["1. 19-bit Spatial Histogram Downsampling<br/>(HistItem: Aggregates sub-pixel mean values)"]:::step
B --> C["2. Variance-based Perceptual Median Cut<br/>(Oklab Space Median Cut)"]:::step
C --> D["3. Lloyd-Voronoi Centroid Relaxation<br/>(With Dynamic Outlier Recycling)"]:::step
D --> E["4. Vantage Point Tree Metric Acceleration<br/>(Bound Inheritance & Fast Pruning)"]:::step
E --> F["5. Adaptive Serpentine Floyd-Steinberg<br/>(O(Width) Ring Buffer & Noise Suppression)"]:::step
F --> G["6. Adaptive Bit-Depth Indexed-PNG Encoder<br/>(1/2/4/8-bit Packing + tRNS Trimming)"]:::step
G --> H["Ultra-Compact High-Fidelity Indexed PNG"]:::win1. End-to-End Oklab Perceptual Space & Static LUT (src/color.rs)
Replaces traditional non-linear sRGB and flawed CIELAB models with the modern Oklab perceptual space, completely avoiding purple shifts and perceived hue distortion. Features a precomputed static lookup table SRGB_TO_LINEAR_LUT that eliminates expensive powf(2.4) operations, converting coordinates via L1 cache hits.
2. 19-bit Perceptual Weighted Spatial Binning (src/hist.rs)
Applies a 19-bit spatial hash (5-bit R, 5-bit G, 5-bit B, 4-bit A) to compress tens of millions of raw pixels into thousands of feature bins while preserving the precise weighted color averages. This shrinks clustering iterations computation by over 99%.
3. Variance-based Split & Lloyd-Voronoi Centroid Relaxation (src/quant.rs)
The first stage splits boxes along the maximum variance dimension to build an initial palette; the second stage executes Lloyd-Voronoi K-Means relaxation to converge centroids to the optimal Voronoi centers. Includes an outlier recycling mechanism to reactivate unassigned clusters, ensuring all palette entries deliver maximum fidelity.
4. Vantage Point Tree with Distance Bound Inheritance (src/vptree.rs)
Solves the nearest-neighbor search bottleneck in non-Euclidean perceptual space using a Vantage Point Tree. By inheriting the previous pixel's distance as the initial search upper bound and leveraging triangle inequality pruning, spatial queries visit over 70% fewer nodes.
5. Adaptive Serpentine Floyd-Steinberg Dithering (src/dither.rs)
Employs alternating left-to-right and right-to-left serpentine scanning to eliminate directional worm artifacts. An adaptive damping factor automatically suppresses noise on flat color regions and clamps extreme error diffusion on high-contrast edges. The rolling buffer keeps memory consumption at a strict $O(\text{Width})$.
6. Compact Adaptive Bit-Depth PNG Encoder (src/png.rs)
Built-in pure Rust PNG encoder that eliminates external C dependencies. Automatically packs indices into 1-bit (≤2 colors), 2-bit (≤4 colors), 4-bit (≤16 colors), or 8-bit streams, and trims trailing opaque alpha in the tRNS chunk for optimal compression.
🚀 Quick Start
1. Installation
Install via your preferred package manager (pnpm recommended):
# via pnpm
pnpm add wasm-imagequant
# via npm
npm install wasm-imagequant
# via yarn
yarn add wasm-imagequant2. Frontend & Bundler Integration (Vite, Webpack, Next.js, Nuxt, React, Vue)
wasm-imagequant is distributed with native ES Modules and TypeScript definitions.
Standard Browser Usage
import init, { compress_png } from 'wasm-imagequant';
// 1. Initialize WebAssembly runtime (fetches and compiles wasm_imagequant_bg.wasm)
await init();
// 2. Extract RGBA pixel buffer from HTML Canvas
const ctx = canvas.getContext('2d')!;
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// 3. Compress using wasm-imagequant
// Signature: compress_png(rgbaData, width, height, maxColors, ditherStrength)
const result = compress_png(
imgData.data, // Uint8Array: raw RGBA pixels (width * height * 4)
canvas.width, // Image width
canvas.height, // Image height
256, // Max palette colors (2 - 256, default: 256)
0.8 // Dither strength (0.0: off, 1.0: full diffusion, default: 0.8)
);
// 4. Access output PNG bytes and preview
const pngBytes: Uint8Array = result.data; // Uint8Array: valid standard PNG binary stream
const paletteSize: number = result.palette_size; // Actual number of colors assigned
const blob = new Blob([pngBytes], { type: 'image/png' });
const previewUrl = URL.createObjectURL(blob);Vite Explicit Static Asset Import
If your bundler requires explicit URL resolution for static .wasm files:
import init, { compress_png } from 'wasm-imagequant';
import wasmUrl from 'wasm-imagequant/wasm_imagequant_bg.wasm?url';
await init(wasmUrl);Non-blocking Web Worker (Recommended for High Resolutions)
Execute image quantization off the main thread to guarantee 60 FPS UI responsiveness:
// worker.ts
import init, { compress_png } from 'wasm-imagequant';
let isReady = false;
self.onmessage = async (e: MessageEvent<{ rgba: Uint8Array; width: number; height: number; maxColors?: number; dither?: number }>) => {
if (!isReady) {
await init();
isReady = true;
}
const { rgba, width, height, maxColors = 256, dither = 0.8 } = e.data;
const result = compress_png(rgba, width, height, maxColors, dither);
// Zero-copy transfer the result buffer back to the main thread
self.postMessage(result.data, [result.data.buffer]);
};3. Server-side & Node.js Integration (ESM)
In Node.js 18+ or serverless cloud functions:
import fs from 'node:fs/promises';
import init, { compress_png } from 'wasm-imagequant';
// Pass binary buffer directly to init() in Node.js
const wasmBuffer = await fs.readFile(
new URL('node_modules/wasm-imagequant/wasm_imagequant_bg.wasm', import.meta.url)
);
await init(wasmBuffer);
// Compress raw RGBA buffer
const result = compress_png(rgbaBuffer, width, height, 256, 0.8);
await fs.writeFile('output.png', result.data);4. Interactive Web Playground
Launch the local interactive visual playground with one command:
git clone https://github.com/reed-soul/wasm-imagequant.git
cd wasm-imagequant
pnpm install
pnpm devOpen http://localhost:3000/playground/ in your browser to inspect side-by-side comparisons, adjust sliders for color count and dithering, and inspect real-time compression metrics.
4. Native Rust Usage (CLI / Microservice)
Add to your Cargo.toml:
[dependencies]
wasm-imagequant = { path = "../wasm-imagequant" } # Or from crates.iouse wasm_imagequant::quantize_and_compress;
fn main() -> Result<(), String> {
let width = 1920;
let height = 1080;
let raw_rgba = vec![255u8; width * height * 4];
let png_bytes = quantize_and_compress(
&raw_rgba,
width as u32,
height as u32,
256, // max_colors
0.8 // dither
)?;
std::fs::write("output.png", png_bytes).map_err(|e| e.to_string())?;
Ok(())
}🎯 Best Practices & Tuning Guide
| Image Domain | Recommended maxColors | Recommended dither | Rationale & Expected Benefit |
| :--- | :--- | :--- | :--- |
| Natural Photos / Wallpapers / Gradients | 256 | 0.7 ~ 0.85 | Oklab color space eliminates banding; adaptive noise suppression avoids speckled grain on sky and skin. |
| UI Dashboards / Architectural Diagrams / Screenshots | 32 ~ 64 | 0.0 ~ 0.2 | Disabling dithering keeps vector lines and text sharp, improving Deflate compression by 30% ~ 50%. |
| Pre-indexed Graphics (Mode P, ≤200 colors) | 16 ~ 32 | 0.0 | With maxColors=256, existing colors are preserved 100% losslessly. Setting maxColors ≤ 16 activates 4-bit packing, halving the file size. |
📂 Project Structure
wasm-imagequant/
├── Cargo.toml # Root Rust crate configuration (LTO + opt-level 3)
├── package.json # Root pnpm scripts (dev, build, test, bench)
├── README.md # English documentation (default)
├── README.zh-CN.md # Simplified Chinese documentation
├── index.html # Root router redirecting to /playground/
│
├── src/ # Core quantization engine source
│ ├── lib.rs # Public native API & wasm-bindgen exports
│ ├── color.rs # Oklab perceptual conversions & static LUT table
│ ├── hist.rs # 19-bit perceptual weighted histogram downsampling
│ ├── quant.rs # Variance split + Lloyd-Voronoi relaxation + outlier recycling
│ ├── vptree.rs # Vantage Point Tree metric search with bound inheritance
│ ├── dither.rs # Serpentine Floyd-Steinberg with O(W) rolling buffer
│ └── png.rs # Adaptive 1/2/4/8-bit compact Indexed-PNG encoder
│
├── playground/ # Visual Web Testing Platform
│ └── index.html # Dual-pane side-by-side comparison, parameter sliders & stats
│
├── tests/ # Integration & unit test suite
│ └── test_all.rs # Roundtrip color tests, VP-Tree precision, transparency & bit-depth
│
└── examples/ # CLI & benchmarks
├── compare.rs # Head-to-head benchmark against libimagequant
└── test_user_photos.rs # Comprehensive benchmark on real-world 1080P/4K/12MP photos📄 License
This project is licensed under either of:
Commercial Protection: You may freely use, modify, embed, and distribute this library in closed-source proprietary software, commercial applications, and paid cloud services without any GPL viral copyleft obligations.
