Introduction
In this article, we'll explore how to implement a powerful browser-based image filter tool. This implementation supports multiple filter types including basic adjustments, blur effects, vintage looks, and most importantly, LUT (Look-Up Table) based color grading - the same technology used by professional photo editing software.
Why Browser-Based Filtering?
1. Privacy Protection
When users apply filters in the browser, their photos never leave their device. This is essential for:
- Personal photos requiring privacy
- Business documents
- Any content users want to keep local
2. Zero Server Costs
Processing images in the browser eliminates the need for:
- GPU servers for image processing
- Bandwidth for uploading/downloading
- Storage costs
3. Instant Preview
Processing locally means instant feedback. Users can see filter effects immediately.
Technical Architecture
Core Implementation
1. Data Structures
interface ImageFile {
id: string;
file: File;
previewUrl: string;
filteredUrl?: string;
originalWidth: number;
originalHeight: number;
}
interface FilterDefinition {
id: string;
name?: string;
nameKey?: string;
category: "basic" | "blur" | "vintage" | "monochrome" | "portrait" | "film" | "landscape" | "artistic" | "glow";
params?: Record<string, { min: number; max: number; default: number; step?: number }>;
}
2. Filter Categories
The tool supports various filter categories:
const ALL_FILTERS: FilterDefinition[] = [
// Basic adjustments
{ id: "adjustment", category: "basic", params: { brightness, contrast, saturation, gamma } },
{ id: "brightness", category: "basic" },
{ id: "contrast", category: "basic" },
{ id: "saturate", category: "basic" },
{ id: "hue", category: "basic" },
// Blur effects
{ id: "blur", category: "blur" },
{ id: "kawase-blur", category: "blur" },
{ id: "zoom-blur", category: "blur" },
// Vintage
{ id: "sepia", category: "vintage" },
{ id: "polaroid", category: "vintage" },
{ id: "kodachrome", category: "vintage" },
// Monochrome
{ id: "grayscale", category: "monochrome" },
{ id: "negative", category: "monochrome" },
// LUT-based (landscape)
{ id: "lut-blue-hour", category: "landscape" },
{ id: "lut-orange-blue", category: "landscape" },
{ id: "lut-cold-chrome", category: "landscape" },
// ... more LUT presets
];
LUT Color Mapping - Deep Dive
What is LUT?
LUT (Look-Up Table) is a color grading technology that maps input colors to output colors. It's widely used in:
- Professional photo editing (Lightroom, Photoshop)
- Video color grading (DaVinci Resolve, Final Cut Pro)
- Cinema and film production
CUBE File Format
The tool uses the .cube format, which is the industry standard for LUT files:
TITLE "Blue Hour"
LUT_3D_SIZE 33
DOMAIN_MIN 0.0 0.0 0.0
DOMAIN_MAX 1.0 1.0 1.0
# R G B values (0-1 range)
0.000000 0.000000 0.000000
0.010000 0.000000 0.000000
...
Parsing CUBE Files
const parseCubeLut = (content: string): { size: number; data: number[] } | null => {
const lines = content.trim().split('\n');
let size = 0;
const data: number[] = [];
for (const line of lines) {
const trimmed = line.trim();
// Parse LUT size: LUT_3D_SIZE 33
if (trimmed.startsWith('LUT_3D_SIZE')) {
size = parseInt(trimmed.split(' ')[1]);
}
// Skip metadata lines
else if (trimmed === '' || trimmed.startsWith('#') ||
trimmed.startsWith('TITLE') || trimmed.startsWith('DOMAIN')) {
continue;
}
// Parse RGB values
else {
const parts = trimmed.split(/\s+/).filter(p => p !== '');
if (parts.length >= 3) {
const r = parseFloat(parts[0]);
const g = parseFloat(parts[1]);
const b = parseFloat(parts[2]);
if (!isNaN(r) && !isNaN(g) && !isNaN(b)) {
data.push(r, g, b);
}
}
}
}
if (size === 0 || data.length === 0) return null;
return { size, data };
};
Generating LUT Image for Color Mapping
The key to applying LUT in the browser is converting the CUBE data into a color lookup image that PixiJS can use with ColorMapFilter:
const generateLutImage = (lutData: { size: number; data: number[] }): HTMLCanvasElement => {
const { size, data } = lutData;
const canvas = document.createElement('canvas');
// Create a horizontally-stretched LUT image
// Width = size * size, Height = size
canvas.width = size * size;
canvas.height = size;
const ctx = canvas.getContext('2d')!;
const imageData = ctx.createImageData(canvas.width, canvas.height);
// Convert LUT data to image pixels
// Each pixel represents a color mapping entry
const pixelCount = data.length / 3;
for (let x = 0; x < pixelCount; x++) {
// Calculate position in the LUT image
// The CUBE data is organized as: R changes fastest, then G, then B
const startY = Math.floor(x / size) % size;
const startX = x % size + size * Math.floor(Math.floor(x / size) / size);
const index = 4 * (startY * size * size + startX);
// Convert from 0-1 range to 0-255
imageData.data[index] = Math.min(255, Math.max(0, data[x * 3] * 255)); // R
imageData.data[index + 1] = Math.min(255, Math.max(0, data[x * 3 + 1] * 255)); // G
imageData.data[index + 2] = Math.min(255, Math.max(0, data[x * 3 + 2] * 255)); // B
imageData.data[index + 3] = 255; // Alpha
}
ctx.putImageData(imageData, 0, 0);
return canvas;
};
How Color Mapping Works
The color mapping process:
- Input: Each pixel's RGB values are used as coordinates
- Lookup: The pixel color indexes into the LUT table
- Output: The new mapped color replaces the original
For a 33x33x33 LUT:
- Total entries: 35,937 color mappings
- Each input color maps to a new output color
- Creates smooth, continuous color transformations
Loading Preset LUTs
const loadPresetLut = useCallback(async (lutFile: string) => {
// Check cache first
if (lutCache[lutFile]) {
setLutImage(lutCache[lutFile]);
return;
}
try {
// Fetch CUBE file from GitHub
const response = await fetch(
`https://raw.githubusercontent.com/linmingren/openmodels/main/lut/${lutFile}`
);
const content = await response.text();
const lutData = parseCubeLut(content);
if (!lutData) return;
// Generate LUT image for PixiJS ColorMapFilter
const lutImageUrl = generateLutImage(lutData);
// Cache for reuse
setLutCache(prev => ({ ...prev, [lutFile]: lutImageUrl }));
setLutImage(lutImageUrl);
} catch (err) {
console.error("Error loading CUBE file:", err);
}
}, [lutCache]);
Applying LUT with PixiJS
const applyLutFilter = useCallback(async () => {
if (!currentImage || !selectedFilter || !lutImage) return;
const PIXI = await import("pixi.js");
const { ColorMapFilter } = PIXIFilters;
// Create PixiJS application
const app = new PIXI.Application({
width: currentImage.originalWidth,
height: currentImage.originalHeight,
backgroundAlpha: 0,
});
// Load image as texture
const texture = PIXI.Texture.from(currentImage.previewUrl);
const sprite = new PIXI.Sprite(texture);
sprite.width = currentImage.originalWidth;
sprite.height = currentImage.originalHeight;
app.stage.addChild(sprite);
// Create ColorMapFilter with LUT image
const lutElement = lutImage instanceof HTMLCanvasElement
? lutImage
: await loadImage(lutImage);
const colorMapFilter = new ColorMapFilter(lutElement);
colorMapFilter.nearest = true;
colorMapFilter.updateColorMap();
sprite.filters = [colorMapFilter];
app.render();
// Extract result as base64
const dataUrl = await app.renderer.extract.base64(sprite);
setImages(prev => prev.map((img, i) =>
i === selectedIndex ? { ...img, filteredUrl: dataUrl } : img
));
}, [currentImage, selectedFilter, lutImage]);
Preset LUTs Available
The tool includes 13 professional LUT presets:
| LUT Name | Description |
|---|---|
| Blue Hour | Cool blue tones for twilight photos |
| Orange & Blue | Cinematic orange-blue contrast |
| Cold Chrome | Desaturated cool tones |
| Crisp Autumn | Warm, rich autumn colors |
| Lush Green | Enhanced greens for nature |
| Magic Hour | Golden hour warmth |
| Natural Boost | Subtle natural enhancement |
| Waves | Blue-teal oceanic tones |
| Soft B&W | Soft black and white |
Filter Rendering Engines
1. PixiJS (Basic Filters)
const applyPixiFilter = async () => {
const PIXI = await import("pixi.js");
const app = new PIXI.Application({
width: originalWidth,
height: originalHeight,
});
const sprite = PIXI.Sprite.from(previewUrl);
// Use built-in ColorMatrixFilter
const colorMatrix = new PIXI.ColorMatrixFilter();
switch (filterId) {
case "grayscale":
colorMatrix.grayscale(1);
break;
case "sepia":
colorMatrix.sepia();
break;
case "negative":
colorMatrix.negative();
break;
case "brightness":
colorMatrix.brightness(amount, false);
break;
case "contrast":
colorMatrix.contrast(amount);
break;
}
sprite.filters = [colorMatrix];
app.render();
const dataUrl = await app.renderer.extract.base64(sprite);
};
2. Pixi-Filters (Advanced Effects)
// Bloom filter
filter = new PIXIFilters.BloomFilter({
bloomScale: 1,
brightness: 0.5,
blur: 10,
});
// Glitch effect
filter = new PIXIFilters.GlitchFilter({
slices: 5,
offset: 10,
});
// CRT effect
filter = new PIXIFilters.CRTFilter({
curvature: 10,
lineWidth: 1,
noise: 0.1,
});
3. glfx-es6 (WebGL Effects)
const applyGlfxFilter = async () => {
const glfx = await import("glfx-es6");
const canvas = glfx.canvas(document.createElement('canvas'));
canvas.width = width;
canvas.height = height;
const texture = canvas.texture(imageElement);
canvas.draw(texture).update();
switch (filterId) {
case "glfx-brightness-contrast":
canvas.brightnessContrast(brightness, contrast);
break;
case "glfx-sepia":
canvas.sepia(amount);
break;
case "glfx-vignette":
canvas.vignette(amount, softness);
break;
case "glfx-unsharpen":
canvas.unsharpMask(amount, radius);
break;
case "glfx-swirl":
canvas.swirl(angle, radius);
break;
}
canvas.update();
const dataUrl = canvas.toDataURL("image/png");
};
Processing Flow
Supported Filter Categories
| Category | Filters | Engine |
|---|---|---|
| Basic | brightness, contrast, saturation, hue, adjustment | PixiJS |
| Blur | blur, kawase-blur, zoom-blur, triangle-blur | Pixi-filters / glfx |
| Vintage | sepia, polaroid, kodachrome, lsd | PixiJS |
| Monochrome | grayscale, desaturate, negative, soft-bw | PixiJS / LUT |
| Portrait | bloom, advanced-bloom, sharpen | Pixi-filters |
| Film | noise, vignette, ink | glfx-es6 |
| Landscape | blue-hour, orange-blue, cold-chrome, etc. | LUT ColorMap |
| Artistic | dot, glitch, rgb-split, crt, swirl, warp | Pixi-filters |
| Glow | color-overlay | Pixi-filters |
Performance Characteristics
- LUT Loading: ~100-500ms for CUBE file parsing
- Filter Processing: 50-500ms depending on filter complexity
- WebGL Acceleration: Uses GPU for fast rendering
- Caching: LUT images cached for instant reuse
Conclusion
Browser-based image filtering with LUT color mapping brings professional-grade color grading to the web. The implementation uses:
- PixiJS for basic filters and color matrix operations
- pixi-filters for advanced effects (bloom, glitch, CRT)
- glfx-es6 for WebGL-based image processing
- LUT Color Mapping for professional color grading
The LUT system parses .cube files and generates lookup images for the ColorMapFilter, enabling the same color transformations used in professional software.
Try it yourself at Free Image Tools
Experience the power of browser-based image filters and professional LUT color grading. No upload required - your images stay on your device!



Top comments (0)