Skip to content

Image Optimization Plugin

Automatic image optimization for EreoJS with responsive srcsets, blur placeholders, modern formats, and lazy loading.

Installation

bash
bun add @ereo/plugin-images sharp

Setup

1. Add Plugin

ts
// ereo.config.ts
import { defineConfig } from '@ereo/core'
import images from '@ereo/plugin-images'

export default defineConfig({
  plugins: [
    images({
      formats: { webp: true, avif: true },
      quality: 80,
    })
  ]
})

2. Use Components

tsx
import { Image, Picture } from '@ereo/plugin-images'
import heroImg from './hero.jpg'

function Hero() {
  return (
    <Image
      src={heroImg}
      alt="Hero image"
      placeholder="blur"
      priority
    />
  )
}

Image Component

The Image component is a drop-in replacement for the HTML <img> element with automatic optimization.

Import

You can import Image and Picture from either the main package or the /components sub-path — both are equivalent:

ts
// Recommended — shorter
import { Image, Picture } from '@ereo/plugin-images'

// Alternative — explicit sub-path (useful if you only need components)
import { Image, Picture } from '@ereo/plugin-images/components'

Basic Usage

tsx
// With static import (recommended)
import heroImg from './hero.jpg'

<Image src={heroImg} alt="Hero image" />

// With URL string
<Image src="/images/hero.jpg" alt="Hero image" width={800} height={600} />

Props

ts
interface ImageProps {
  // Image source - URL string or imported StaticImageData
  src: string | StaticImageData

  // Alt text for accessibility (required)
  alt: string

  // Dimensions (auto-detected from static imports)
  width?: number
  height?: number

  // Fill parent container
  fill?: boolean

  // Object-fit when using fill mode
  objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'

  // Object-position when using fill mode
  objectPosition?: string

  // Aspect ratio (e.g., '16/9', '4/3')
  aspectRatio?: string

  // Sizes attribute for responsive images
  sizes?: string

  // Placeholder while loading
  placeholder?: 'blur' | 'color' | 'shimmer' | 'empty'

  // Custom blur data URL (for placeholder='blur')
  blurDataURL?: string

  // Image quality (1-100)
  quality?: number

  // Preload for above-the-fold images
  priority?: boolean

  // Loading strategy
  loading?: 'lazy' | 'eager'

  // Decoding strategy
  decoding?: 'async' | 'sync' | 'auto'

  // Custom image loader
  loader?: (params: ImageLoaderParams) => string

  // Disable optimization
  unoptimized?: boolean

  // Event handlers
  onLoad?: (event: SyntheticEvent) => void
  onError?: (event: SyntheticEvent) => void
}

Examples

Static Import with Blur Placeholder

tsx
import productImg from './product.jpg'

<Image
  src={productImg}
  alt="Product photo"
  placeholder="blur"
  quality={85}
/>

Fill Container

tsx
<div style={{ position: 'relative', width: '100%', height: 400 }}>
  <Image
    src="/images/background.jpg"
    alt="Background"
    fill
    objectFit="cover"
    objectPosition="center top"
  />
</div>

Responsive with Sizes

tsx
<Image
  src={heroImg}
  alt="Hero"
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  priority
/>

Aspect Ratio

tsx
<Image
  src={thumbnailImg}
  alt="Thumbnail"
  aspectRatio="16/9"
  width={400}
/>

Picture Component

The Picture component provides art direction for responsive images, allowing different images at different breakpoints.

Import

ts
import { Picture } from '@ereo/plugin-images'

Basic Usage

tsx
import heroMobile from './hero-mobile.jpg'
import heroDesktop from './hero-desktop.jpg'

<Picture
  alt="Hero image"
  sources={[
    { src: heroMobile, media: '(max-width: 640px)' },
    { src: heroDesktop, media: '(min-width: 641px)' },
  ]}
/>

Props

ts
interface PictureProps {
  // Array of sources for different breakpoints/formats
  sources: PictureSource[]

  // Fallback source
  fallback?: string | StaticImageData

  // Alt text (required)
  alt: string

  // Same props as Image component
  width?: number
  height?: number
  fill?: boolean
  objectFit?: ObjectFit
  placeholder?: PlaceholderType
  quality?: number
  priority?: boolean
  // ...
}

interface PictureSource {
  // Image source for this breakpoint
  src: string | StaticImageData

  // Media query for when to use this source
  media?: string

  // MIME type hint
  type?: string

  // Dimensions for this variant
  width?: number
  height?: number

  // Sizes attribute for this source
  sizes?: string
}

Examples

Art Direction

tsx
<Picture
  alt="Product showcase"
  sources={[
    { src: productSquare, media: '(max-width: 480px)' },
    { src: productPortrait, media: '(max-width: 768px)' },
    { src: productLandscape, media: '(min-width: 769px)' },
  ]}
  placeholder="blur"
/>

Format-Based Sources

tsx
<Picture
  alt="Product photo"
  sources={[
    { src: '/product.avif', type: 'image/avif' },
    { src: '/product.webp', type: 'image/webp' },
    { src: '/product.jpg' },
  ]}
/>

Lazy Loading

Images are lazy loaded by default unless priority is set.

Default Behavior

tsx
// Lazy loaded (default)
<Image src={img} alt="Below fold" />

// Eager loaded (for above-the-fold)
<Image src={img} alt="Hero" priority />

Custom Loading Strategy

tsx
<Image
  src={img}
  alt="Custom loading"
  loading="eager"  // Override lazy loading
/>

Placeholders

Blur Placeholder

tsx
import heroImg from './hero.jpg'  // Blur data auto-generated

<Image src={heroImg} alt="Hero" placeholder="blur" />

Dominant Color

tsx
<Image src={heroImg} alt="Hero" placeholder="color" />

Shimmer Effect

tsx
<Image src="/api/photo.jpg" alt="Photo" placeholder="shimmer" />

Custom Blur Data URL

tsx
<Image
  src="/images/hero.jpg"
  alt="Hero"
  placeholder="blur"
  blurDataURL="data:image/webp;base64,..."
  width={1200}
  height={600}
/>

Format Optimization

The plugin automatically converts images to modern formats.

Configuration

ts
images({
  formats: {
    webp: true,   // WebP (recommended, good balance)
    avif: true,   // AVIF (best compression, slower encode)
    jpeg: true,   // JPEG fallback
    png: true,    // PNG for transparency
  }
})

Browser Negotiation

The middleware automatically selects the best format based on the browser's Accept header:

  1. AVIF (if enabled and supported)
  2. WebP (if enabled and supported)
  3. Original format (JPEG/PNG)

Responsive Images

srcset Generation

The plugin automatically generates srcsets for responsive images:

ts
// Default device sizes
const deviceSizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840]

// Default image sizes (for smaller images)
const imageSizes = [16, 32, 48, 64, 96, 128, 256, 384]

Custom Sizes

ts
images({
  sizes: {
    deviceSizes: [640, 1080, 1920],
    imageSizes: [32, 64, 128],
  }
})

Using the sizes Prop

tsx
<Image
  src={heroImg}
  alt="Hero"
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px"
/>

Remote Images

Allow Remote Patterns

ts
images({
  remotePatterns: [
    {
      protocol: 'https',
      hostname: 'cdn.example.com',
      pathname: '/images/*',
    },
    {
      hostname: '*.cloudinary.com',
    },
  ]
})

Legacy Domains

ts
// Deprecated - use remotePatterns instead
images({
  domains: ['cdn.example.com', 'images.unsplash.com']
})

Allow All Remote (Unsafe)

ts
images({
  dangerouslyAllowAllRemote: true  // Not recommended for production
})

Custom Loader

Use a custom loader for external image services:

Cloudinary

tsx
const cloudinaryLoader = ({ src, width, quality }) => {
  return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 80}/${src}`
}

<Image
  src="sample.jpg"
  alt="Cloudinary image"
  loader={cloudinaryLoader}
  width={800}
  height={600}
/>

Imgix

tsx
const imgixLoader = ({ src, width, quality }) => {
  const params = new URLSearchParams({
    w: width.toString(),
    q: (quality || 80).toString(),
    auto: 'format',
  })
  return `https://example.imgix.net/${src}?${params}`
}

<Image src="hero.jpg" alt="Hero" loader={imgixLoader} width={1200} height={600} />

Performance Tuning

Quality Settings

ts
images({
  quality: 80,  // Default quality (1-100)
})
tsx
// Override per image
<Image src={img} alt="High quality" quality={95} />
<Image src={img} alt="Optimized" quality={60} />

Caching

ts
images({
  cacheDir: '.ereo/images',      // Cache directory
  minimumCacheTTL: 31536000,     // 1 year in seconds
})

Max Dimensions

ts
images({
  maxDimension: 3840,  // Maximum width or height
})

Build-time Optimization

ts
images({
  generateBlurPlaceholder: true,   // Generate blur at build
  extractDominantColor: true,      // Extract colors at build
})

Plugin Options

ts
interface ImagePluginConfig {
  // Remote image patterns
  remotePatterns?: RemotePattern[]

  // Output formats to generate
  formats?: {
    webp?: boolean    // Default: true
    avif?: boolean    // Default: false
    jpeg?: boolean    // Default: true
    png?: boolean     // Default: true
  }

  // Default quality (1-100)
  quality?: number    // Default: 80

  // Responsive sizes
  sizes?: {
    deviceSizes?: number[]  // Default: [640, 750, 828, 1080, 1200, 1920, 2048, 3840]
    imageSizes?: number[]   // Default: [16, 32, 48, 64, 96, 128, 256, 384]
  }

  // Cache TTL in seconds
  minimumCacheTTL?: number  // Default: 31536000 (1 year)

  // Cache directory
  cacheDir?: string         // Default: '.ereo/images'

  // Build-time features
  generateBlurPlaceholder?: boolean  // Default: true
  extractDominantColor?: boolean     // Default: true

  // Maximum dimension
  maxDimension?: number     // Default: 3840

  // API endpoint path
  path?: string             // Default: '/_ereo/image'

  // Allow any remote image (unsafe)
  dangerouslyAllowAllRemote?: boolean  // Default: false
}

TypeScript API Reference

This section documents all exported TypeScript interfaces and types.

StaticImageData

Represents metadata for a statically imported image.

ts
interface StaticImageData {
  /** Source URL of the image */
  src: string
  /** Original width in pixels */
  width: number
  /** Original height in pixels */
  height: number
  /** Base64 encoded blur placeholder (if generated) */
  blurDataURL?: string
  /** Dominant color (if extracted) */
  dominantColor?: string
  /** MIME type of the original image */
  type?: string
}

Example:

ts
import heroImg from './hero.jpg'

console.log(heroImg)
// {
//   src: '/images/hero.jpg',
//   width: 1920,
//   height: 1080,
//   blurDataURL: 'data:image/webp;base64,...',
//   dominantColor: 'rgb(26, 43, 60)',
//   type: 'image/jpeg'
// }

ImageVariant

Represents a generated image variant at a specific size and format.

ts
interface ImageVariant {
  /** Output path */
  path: string
  /** Width in pixels */
  width: number
  /** Height in pixels */
  height: number
  /** Output format */
  format: 'webp' | 'avif' | 'jpeg' | 'png'
  /** File size in bytes */
  size: number
}

ImageManifestEntry

Represents an image entry in the build manifest.

ts
interface ImageManifestEntry {
  /** Original source path */
  src: string
  /** Original width */
  width: number
  /** Original height */
  height: number
  /** Generated variants */
  variants: ImageVariant[]
  /** Blur placeholder data URL */
  blurDataURL?: string
  /** Dominant color */
  dominantColor?: string
  /** File hash for cache busting */
  hash: string
}

ProcessedImage

Result of image processing operations.

ts
interface ProcessedImage {
  /** Processed image buffer */
  buffer: Buffer
  /** MIME type */
  contentType: string
  /** Width in pixels */
  width: number
  /** Height in pixels */
  height: number
  /** Output format */
  format: string
}

ImageOptimizationParams

Parameters for image optimization requests.

ts
interface ImageOptimizationParams {
  /** Source image path or URL */
  src: string
  /** Target width */
  width: number
  /** Target height (optional, maintains aspect ratio if omitted) */
  height?: number
  /** Quality (1-100) */
  quality?: number
  /** Output format */
  format?: 'auto' | 'webp' | 'avif' | 'jpeg' | 'png'
}

RemotePattern

Pattern for allowing remote image sources.

ts
interface RemotePattern {
  /** Protocol (http or https) */
  protocol?: 'http' | 'https'
  /** Hostname pattern (supports wildcards like '*.example.com') */
  hostname: string
  /** Port number */
  port?: string
  /** Path prefix pattern (e.g., '/images/*') */
  pathname?: string
}

BlurPlaceholderOptions

Options for blur placeholder generation.

ts
interface BlurPlaceholderOptions {
  /** Width of the placeholder (default: 8px) */
  width?: number
  /** Quality for encoding (default: 10) */
  quality?: number
  /** Blur sigma (default: 1) */
  sigma?: number
}

BlurPlaceholderResult

Result of blur placeholder generation.

ts
interface BlurPlaceholderResult {
  /** Base64-encoded data URL */
  dataURL: string
  /** Width of the placeholder */
  width: number
  /** Height of the placeholder */
  height: number
}

ColorExtractionResult

Result of dominant color extraction.

ts
interface ColorExtractionResult {
  /** Primary dominant color as CSS rgb() string */
  dominant: string
  /** Palette of dominant colors */
  palette: string[]
  /** Primary color as RGB object */
  dominantRGB: RGBColor
  /** Whether the image has significant transparency */
  hasTransparency: boolean
}

RGBColor

RGB color representation.

ts
interface RGBColor {
  r: number
  g: number
  b: number
}

ColorExtractionOptions

Options for color extraction.

ts
interface ColorExtractionOptions {
  /** Number of colors to extract (default: 5) */
  colorCount?: number
  /** Sample size for analysis (default: 64) */
  sampleSize?: number
  /** Minimum saturation for "colorful" detection (default: 0.1) */
  minSaturation?: number
  /** Transparency threshold (default: 0.5) */
  transparencyThreshold?: number
}

ImageMetadata

Image metadata from processing.

ts
interface ImageMetadata {
  width: number
  height: number
  format: string
  space?: string
  channels?: number
  depth?: string
  density?: number
  hasAlpha?: boolean
  orientation?: number
}

ImageLoaderParams

Parameters passed to custom image loaders.

ts
interface ImageLoaderParams {
  src: string
  width: number
  quality?: number
}

type ImageLoader = (params: ImageLoaderParams) => string

ImageProcessor Direct Usage

The ImageProcessor class provides programmatic access to image processing capabilities.

Creating an Instance

ts
import { createImageProcessor, ImageProcessor } from '@ereo/plugin-images'

const processor = createImageProcessor({
  quality: 80,
  formats: { webp: true, avif: true },
  generateBlurPlaceholder: true,
  extractDominantColor: true,
})

// Or instantiate directly
const processor = new ImageProcessor({
  quality: 85,
  maxDimension: 2048,
})

process()

Process a single image with given parameters.

ts
const result = await processor.process(imageBuffer, {
  src: '/images/hero.jpg',
  width: 800,
  quality: 85,
  format: 'webp',
})

console.log(result.buffer)      // Buffer
console.log(result.contentType) // 'image/webp'
console.log(result.width)       // 800
console.log(result.height)      // calculated from aspect ratio
console.log(result.format)      // 'webp'

processWithMetadata()

Process an image and generate all metadata including blur placeholder and dominant color.

ts
const result = await processor.processWithMetadata(imageBuffer, {
  src: '/images/product.jpg',
  width: 600,
  format: 'webp',
})

console.log(result.processed)  // ProcessedImage
console.log(result.metadata)   // ImageMetadata (original dimensions, format, etc.)
console.log(result.blur)       // BlurPlaceholderResult (if enabled)
console.log(result.colors)     // ColorExtractionResult (if enabled)

processFile()

Process a local image file and generate all variants.

ts
const result = await processor.processFile('/path/to/image.jpg')

console.log(result.staticData)  // StaticImageData for component use
console.log(result.path)        // Original file path
console.log(result.variants)    // Array of all generated variants

// Each variant includes:
result.variants.forEach(variant => {
  console.log(variant.width)   // e.g., 640
  console.log(variant.height)  // calculated
  console.log(variant.format)  // 'webp', 'avif', etc.
  console.log(variant.path)    // output path
  console.log(variant.buffer)  // Buffer to write to disk
})

getMetadata()

Get image metadata without processing.

ts
const metadata = await processor.getMetadata(imageBuffer)

console.log(metadata.width)       // 1920
console.log(metadata.height)      // 1080
console.log(metadata.format)      // 'jpeg'
console.log(metadata.hasAlpha)    // false
console.log(metadata.orientation) // 1 (EXIF orientation)
console.log(metadata.density)     // DPI if available

generateBlur()

Generate a blur placeholder for an image.

ts
const blur = await processor.generateBlur(imageBuffer)

console.log(blur.dataURL)  // 'data:image/webp;base64,...'
console.log(blur.width)    // 8 (default)
console.log(blur.height)   // calculated from aspect ratio

extractColor()

Extract dominant color from an image.

ts
const colors = await processor.extractColor(imageBuffer)

console.log(colors.dominant)        // 'rgb(26, 43, 60)'
console.log(colors.palette)         // ['rgb(26, 43, 60)', 'rgb(255, 128, 64)', ...]
console.log(colors.dominantRGB)     // { r: 26, g: 43, b: 60 }
console.log(colors.hasTransparency) // false

isSupported()

Check if a file is a supported image format.

ts
processor.isSupported('image.jpg')   // true
processor.isSupported('image.webp')  // true
processor.isSupported('image.svg')   // true
processor.isSupported('document.pdf') // false

clearCache()

Clear the internal processing cache.

ts
processor.clearCache()

BuildOptimizer Integration

The BuildOptimizer handles batch image processing during production builds.

Creating an Instance

ts
import { createBuildOptimizer, optimizeImages, BuildOptimizer } from '@ereo/plugin-images'

const optimizer = createBuildOptimizer({
  root: process.cwd(),
  outDir: '.ereo/public',
  config: {
    formats: { webp: true, avif: true },
    quality: 80,
  },
  scanDirs: ['public', 'app/assets', 'assets'],
  force: false,  // Set to true to reprocess all images
  onProgress: (current, total, file) => {
    console.log(`Processing ${current}/${total}: ${file}`)
  },
})

BuildOptimizerOptions

ts
interface BuildOptimizerOptions {
  /** Project root directory */
  root: string
  /** Output directory for optimized images */
  outDir: string
  /** Plugin configuration */
  config?: ImagePluginConfig
  /** Directories to scan for images */
  scanDirs?: string[]
  /** Whether to force reprocessing all images */
  force?: boolean
  /** Progress callback */
  onProgress?: (current: number, total: number, file: string) => void
}

run()

Run the build optimization process.

ts
const result = await optimizer.run()

console.log(result.processed)  // Number of images processed
console.log(result.skipped)    // Number unchanged (cached)
console.log(result.variants)   // Total variants generated
console.log(result.totalSize)  // Total output size in bytes
console.log(result.duration)   // Processing time in ms
console.log(result.errors)     // Array of { file, error }

BuildResult

ts
interface BuildResult {
  /** Number of images processed */
  processed: number
  /** Number of images skipped (unchanged) */
  skipped: number
  /** Total variants generated */
  variants: number
  /** Total output size in bytes */
  totalSize: number
  /** Processing time in milliseconds */
  duration: number
  /** Any errors encountered */
  errors: Array<{ file: string; error: string }>
}

getManifest()

Access the manifest manager.

ts
const manifest = optimizer.getManifest()
const allImages = manifest.getAllImages()

One-liner with optimizeImages()

ts
import { optimizeImages } from '@ereo/plugin-images'

const result = await optimizeImages({
  root: process.cwd(),
  outDir: 'dist/images',
  config: { formats: { webp: true } },
})

ManifestManager

The ImageManifestManager tracks processed images and their variants.

Creating an Instance

ts
import { createManifestManager, ImageManifestManager } from '@ereo/plugin-images'

const manifest = createManifestManager('./dist/images')

load()

Load the manifest from disk.

ts
await manifest.load()

save()

Save the manifest to disk.

ts
await manifest.save()

addImage()

Add or update an image entry.

ts
manifest.addImage('/images/hero.jpg', {
  src: '/images/hero.jpg',
  width: 1920,
  height: 1080,
  variants: [
    { path: 'hero-640w.webp', width: 640, height: 360, format: 'webp', size: 12500 },
    { path: 'hero-1080w.webp', width: 1080, height: 608, format: 'webp', size: 35000 },
  ],
  blurDataURL: 'data:image/webp;base64,...',
  dominantColor: 'rgb(26, 43, 60)',
})

getImage()

Get an image entry by source path.

ts
const entry = manifest.getImage('/images/hero.jpg')
if (entry) {
  console.log(entry.width, entry.height)
  console.log(entry.variants.length)
}

needsReprocessing()

Check if an image needs to be reprocessed based on file hash.

ts
const fileHash = 'abc12345'  // MD5 hash of file content
if (manifest.needsReprocessing('/images/hero.jpg', fileHash)) {
  // Process the image
}

removeImage()

Remove an image entry.

ts
manifest.removeImage('/images/old.jpg')

getAllImages()

Get all image entries.

ts
const images = manifest.getAllImages()
// Returns: Record<string, ImageManifestEntry>

for (const [path, entry] of Object.entries(images)) {
  console.log(path, entry.variants.length)
}

Statistics Methods

ts
manifest.getImageCount()   // Number of images
manifest.getVariantCount() // Total variants across all images
manifest.getTotalSize()    // Total size of all variants in bytes

clear()

Clear all entries.

ts
manifest.clear()

Helper Functions

ts
import { generateImageModule, generateSrcset, getBestVariant } from '@ereo/plugin-images'

// Generate a virtual module for image metadata
const moduleCode = generateImageModule(entry, '/assets')
// Returns: 'export default { src: "/assets/hero.jpg", ... };'

// Generate srcset string from variants
const srcset = generateSrcset(entry.variants, '/assets', 'webp')
// Returns: '/assets/hero-640w.webp 640w, /assets/hero-1080w.webp 1080w'

// Get best variant for a given width
const variant = getBestVariant(entry.variants, 800, 'webp')
// Returns the smallest variant >= 800px wide in webp format

Blur Generation Functions

generateBlurPlaceholder()

Generate a tiny blurred version of an image for use as a loading placeholder.

ts
import { generateBlurPlaceholder } from '@ereo/plugin-images'

const blur = await generateBlurPlaceholder(imageBuffer, {
  width: 8,      // Placeholder width (default: 8)
  quality: 10,   // Encoding quality (default: 10)
  sigma: 1,      // Blur amount (default: 1)
})

console.log(blur.dataURL)  // 'data:image/webp;base64,UklGRlYAAABXRUJQ...'
console.log(blur.width)    // 8
console.log(blur.height)   // Calculated from aspect ratio

generateBlurHash()

Generate a blur hash using an SVG gradient representation.

ts
import { generateBlurHash } from '@ereo/plugin-images'

const hash = await generateBlurHash(imageBuffer)

console.log(hash.dataURL)  // 'data:image/svg+xml;base64,...'
console.log(hash.width)    // 4 (landscape) or 3 (portrait)
console.log(hash.height)   // 3 (landscape) or 4 (portrait)

generateCSSBlurPlaceholder()

Generate an ultra-compact blur placeholder optimized for CSS backgrounds.

ts
import { generateCSSBlurPlaceholder } from '@ereo/plugin-images'

const dataURL = await generateCSSBlurPlaceholder(imageBuffer)
// Uses width: 4, quality: 5, sigma: 2 for minimal size

generateShimmerSVG()

Generate an animated shimmer placeholder SVG.

ts
import { generateShimmerSVG, generateShimmerDataURL } from '@ereo/plugin-images'

// Get raw SVG string
const svg = generateShimmerSVG(400, 300, '#f3f4f6')

// Get as data URL
const dataURL = generateShimmerDataURL(400, 300, '#f3f4f6')

Color Utilities

extractDominantColor()

Extract dominant colors from an image using k-means clustering.

ts
import { extractDominantColor } from '@ereo/plugin-images'

const result = await extractDominantColor(imageBuffer, {
  colorCount: 5,              // Number of colors to extract
  sampleSize: 64,             // Analysis sample size
  minSaturation: 0.1,         // Minimum saturation for vibrant colors
  transparencyThreshold: 0.5, // Threshold for transparency detection
})

console.log(result.dominant)        // 'rgb(26, 43, 60)'
console.log(result.palette)         // ['rgb(26, 43, 60)', 'rgb(255, 128, 64)', ...]
console.log(result.dominantRGB)     // { r: 26, g: 43, b: 60 }
console.log(result.hasTransparency) // false

rgbToHex()

Convert RGB color to hex string.

ts
import { rgbToHex } from '@ereo/plugin-images'

const hex = rgbToHex({ r: 255, g: 128, b: 64 })
console.log(hex)  // '#ff8040'

hexToRgb()

Convert hex string to RGB color.

ts
import { hexToRgb } from '@ereo/plugin-images'

const rgb = hexToRgb('#ff8040')
console.log(rgb)  // { r: 255, g: 128, b: 64 }

// Throws Error for invalid hex
hexToRgb('invalid')  // Error: Invalid hex color: invalid

getContrastColor()

Get a contrasting text color (black or white) for a background.

ts
import { getContrastColor } from '@ereo/plugin-images'

const textColor = getContrastColor({ r: 26, g: 43, b: 60 })
console.log(textColor)  // '#ffffff' (white text on dark background)

const textColor2 = getContrastColor({ r: 255, g: 255, b: 200 })
console.log(textColor2)  // '#000000' (black text on light background)

Error Handling

ConfigValidationError

Thrown when plugin configuration is invalid.

ts
import { validateConfig, ConfigValidationError } from '@ereo/plugin-images'

try {
  const config = validateConfig({
    quality: 150,  // Invalid: must be 1-100
  })
} catch (error) {
  if (error instanceof ConfigValidationError) {
    console.log(error.message)  // "Invalid configuration for 'quality': quality must be between 1 and 100"
    console.log(error.field)    // 'quality'
    console.log(error.value)    // 150
  }
}

Common Validation Errors

ts
// Quality out of range
validateConfig({ quality: 0 })    // Error: quality must be between 1 and 100
validateConfig({ quality: 101 })  // Error: quality must be between 1 and 100

// Invalid remote pattern
validateConfig({
  remotePatterns: [{ hostname: '' }]  // Error: hostname is required
})

// Invalid path
validateConfig({ path: 'no-slash' })  // Error: path must be a string starting with "/"

// Invalid sizes
validateConfig({
  sizes: { deviceSizes: [5000] }  // Error: deviceSizes[0] must be a positive number <= 3840
})

Image Processing Error Handling

ts
import { createImageProcessor } from '@ereo/plugin-images'

const processor = createImageProcessor()

try {
  const result = await processor.process(buffer, {
    src: '/image.jpg',
    width: 5000,  // Exceeds maxDimension
  })
} catch (error) {
  console.error('Processing failed:', error.message)
  // "Width 5000 exceeds maximum dimension 3840"
}

// Metadata errors
try {
  const metadata = await processor.getMetadata(corruptBuffer)
} catch (error) {
  console.error('Metadata failed:', error.message)
  // "Unable to read image metadata"
}

Graceful Degradation Pattern

ts
const processor = createImageProcessor({
  generateBlurPlaceholder: true,
  extractDominantColor: true,
})

// processWithMetadata handles errors gracefully
const result = await processor.processWithMetadata(buffer, params)

// blur and colors may be undefined if generation failed
if (result.blur) {
  console.log('Blur generated:', result.blur.dataURL)
} else {
  console.log('Blur generation failed, using fallback')
}

if (result.colors) {
  console.log('Dominant color:', result.colors.dominant)
} else {
  console.log('Color extraction failed, using default')
}

Build Optimizer Error Collection

ts
import { optimizeImages } from '@ereo/plugin-images'

const result = await optimizeImages({
  root: process.cwd(),
  outDir: 'dist/images',
})

// Errors are collected, not thrown
if (result.errors.length > 0) {
  console.log(`${result.errors.length} images failed to process:`)
  result.errors.forEach(({ file, error }) => {
    console.log(`  - ${file}: ${error}`)
  })
}

Caching

The plugin includes a two-tier caching system for optimized images.

Memory Cache

ts
import { MemoryCache } from '@ereo/plugin-images'

const cache = new MemoryCache({
  maxItems: 100,                   // Maximum cached items
  maxSize: 100 * 1024 * 1024,      // 100MB max size
  ttl: 3600000,                    // 1 hour TTL
})

cache.set('key', imageBuffer)
const result = cache.get('key')

// Check and manage cache
cache.has('key')     // boolean
cache.delete('key')  // boolean
cache.clear()

// Statistics
const stats = cache.stats()
console.log(stats.items)    // Current item count
console.log(stats.size)     // Current size in bytes
console.log(stats.maxItems) // Max items allowed
console.log(stats.maxSize)  // Max size allowed

Disk Cache

ts
import { DiskCache } from '@ereo/plugin-images'

const cache = new DiskCache({
  dir: '.ereo/images/cache',
  maxSize: 500 * 1024 * 1024,        // 500MB
  ttl: 7 * 24 * 60 * 60 * 1000,      // 7 days
})

await cache.set('key', imageBuffer)
const result = await cache.get('key')

// Async operations
await cache.has('key')
await cache.delete('key')

// Statistics
const stats = await cache.stats()
console.log(stats.files)  // File count
console.log(stats.size)   // Total size

// Cleanup expired entries
const cleanup = await cache.cleanup()
console.log(cleanup.deleted)  // Files deleted
console.log(cleanup.freed)    // Bytes freed

Two-Tier Cache

ts
import { TwoTierCache } from '@ereo/plugin-images'

const cache = new TwoTierCache({
  memory: { maxItems: 100, maxSize: 50 * 1024 * 1024 },
  disk: { dir: '.ereo/images/cache', maxSize: 500 * 1024 * 1024 },
})

// Checks memory first, then disk (promotes to memory on hit)
const result = await cache.get('key')

// Writes to both tiers
await cache.set('key', imageBuffer)

// Combined statistics
const stats = await cache.stats()
console.log(stats.memory.items)
console.log(stats.disk.files)

Cache Key Generation

ts
import { generateCacheKey } from '@ereo/plugin-images'

const key = generateCacheKey({
  src: '/images/hero.jpg',
  width: 800,
  height: 600,
  quality: 80,
  format: 'webp',
})
// Returns: '/images/hero.jpg:w800:h600:q80:fwebp'

Middleware

The plugin provides runtime image optimization middleware.

createImageMiddleware()

ts
import { createImageMiddleware, imageMiddleware } from '@ereo/plugin-images'

const middleware = createImageMiddleware({
  root: process.cwd(),
  config: {
    formats: { webp: true, avif: true },
    quality: 80,
    remotePatterns: [
      { hostname: 'cdn.example.com' }
    ],
  },
  cache: true,
  cacheDir: '.ereo/images',
})

// Or use the wrapper
const handler = imageMiddleware({
  root: process.cwd(),
  config: { /* ... */ },
})

Endpoint

The middleware handles requests to /_ereo/image with query parameters:

/_ereo/image?src=/images/hero.jpg&w=800&q=80&f=webp
ParameterRequiredDescription
srcYesSource image path or URL
wYesTarget width (1 to maxDimension)
hNoTarget height
qNoQuality 1-100 (default: 80)
fNoFormat: auto, webp, avif, jpeg, png

Response Headers

Content-Type: image/webp
Content-Length: 12345
Cache-Control: public, max-age=31536000, immutable
Vary: Accept
X-Cache: HIT (or MISS)

Format Negotiation

The middleware automatically selects the best format based on the Accept header:

  1. If f parameter is specified (not auto), uses that format
  2. Checks Accept header for image/avif support (if AVIF enabled)
  3. Checks Accept header for image/webp support
  4. Falls back to JPEG

Supported Formats

Input Formats

The plugin accepts the following input formats:

FormatExtensionNotes
JPEG.jpg, .jpegMost common photo format
PNG.pngSupports transparency
WebP.webpModern format with good compression
AVIF.avifBest compression, newer format
GIF.gifAnimated images (first frame only)
SVG.svgVector graphics (passed through)

Output Formats

FormatExtensionMIME TypeNotes
WebP.webpimage/webpDefault, good balance
AVIF.avifimage/avifBest compression, slower encode
JPEG.jpgimage/jpegFallback for older browsers
PNG.pngimage/pngFor images with transparency

Troubleshooting Guide

Common Issues

"Unable to read image dimensions"

Cause: The image file is corrupted or in an unsupported format.

Solution:

ts
// Verify the file is a valid image
const metadata = await processor.getMetadata(buffer)
if (!metadata.width || !metadata.height) {
  console.error('Invalid image file')
}

// Check if format is supported
if (!processor.isSupported(filePath)) {
  console.error('Unsupported format:', extname(filePath))
}

"Width exceeds maximum dimension"

Cause: Requested width is larger than maxDimension (default: 3840).

Solution:

ts
// Either reduce the requested width
const result = await processor.process(buffer, {
  width: Math.min(requestedWidth, 3840),
})

// Or increase maxDimension in config
const processor = createImageProcessor({
  maxDimension: 4096,
})

"Source not allowed"

Cause: Remote image URL doesn't match any configured pattern.

Solution:

ts
images({
  remotePatterns: [
    { hostname: 'cdn.example.com' },
    { hostname: '*.cloudinary.com' },
  ],
  // Or for development only:
  // dangerouslyAllowAllRemote: true,
})

Blur Placeholder Not Generated

Cause: Image is too small or has issues.

Solution:

ts
// Check if blur was generated
const result = await processor.processWithMetadata(buffer, params)
if (!result.blur) {
  console.log('Blur generation failed, using fallback')
  // Use a solid color or shimmer instead
}

Color Extraction Returns Gray

Cause: Image is mostly transparent or has low color variance.

Solution:

ts
const result = await extractDominantColor(buffer)
if (result.hasTransparency) {
  // Use a default color for transparent images
  const color = '#f3f4f6'
}

Build Process Slow

Cause: Processing many large images or generating AVIF format.

Solutions:

ts
// 1. Disable AVIF (slower to encode)
images({
  formats: { webp: true, avif: false },
})

// 2. Reduce device sizes
images({
  sizes: {
    deviceSizes: [640, 1080, 1920],  // Fewer sizes
  }
})

// 3. Use caching (reprocesses only changed files)
// Caching is automatic with the manifest system

Memory Issues During Build

Cause: Processing very large images or too many concurrent operations.

Solution:

ts
// The BuildOptimizer processes images sequentially
// Reduce memory cache size if needed
const cache = new MemoryCache({
  maxItems: 50,
  maxSize: 25 * 1024 * 1024,  // 25MB
})

Debug Logging

ts
// Enable verbose logging
const result = await processor.processWithMetadata(buffer, params)

console.log('Original:', result.metadata.width, 'x', result.metadata.height)
console.log('Processed:', result.processed.width, 'x', result.processed.height)
console.log('Format:', result.processed.format)
console.log('Size:', result.processed.buffer.length, 'bytes')
console.log('Blur:', result.blur ? 'generated' : 'failed')
console.log('Color:', result.colors?.dominant || 'failed')

Best Practices

  1. Use static imports - Get automatic blur placeholders and dimensions
  2. Set priority on LCP images - Mark above-the-fold images as priority
  3. Use appropriate sizes - Help the browser choose the right image
  4. Enable AVIF for new projects - Best compression but slower encode
  5. Cache aggressively - Images are immutable, cache for a year
  6. Use Picture for art direction - Different crops for different screens
  7. Handle errors gracefully - Always check for undefined blur/color results
  8. Validate configuration early - Use validateConfig() to catch issues

Released under the MIT License.