DEV Community

monkeymore studio
monkeymore studio

Posted on

Building a Browser-Based Image Filter Tool with LUT Color Mapping

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 }>;
}
Enter fullscreen mode Exit fullscreen mode

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
];
Enter fullscreen mode Exit fullscreen mode

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
...
Enter fullscreen mode Exit fullscreen mode

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 };
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

How Color Mapping Works

The color mapping process:

  1. Input: Each pixel's RGB values are used as coordinates
  2. Lookup: The pixel color indexes into the LUT table
  3. 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]);
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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");
};
Enter fullscreen mode Exit fullscreen mode

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

  1. LUT Loading: ~100-500ms for CUBE file parsing
  2. Filter Processing: 50-500ms depending on filter complexity
  3. WebGL Acceleration: Uses GPU for fast rendering
  4. 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)