AgentSkillsCN

Image Optimization

图像优化

SKILL.md

Image Optimization

Overview

Image optimization reduces file size while maintaining visual quality, improving page load times and reducing bandwidth costs. This skill covers image formats, compression techniques, and optimization strategies.

Table of Contents

  1. Image Formats
  2. Compression
  3. Resizing and Scaling
  4. Responsive Images
  5. Lazy Loading
  6. Image CDN
  7. Next.js Image Component
  8. Sharp Library (Node.js)
  9. Pillow (Python)
  10. Automated Optimization
  11. Performance Metrics
  12. Best Practices

Image Formats

JPEG (Joint Photographic Experts Group)

  • Use Case: Photographs, complex images with gradients
  • Compression: Lossy
  • Transparency: No
  • Browser Support: Universal
  • Best Quality: 80-90%
typescript
// jpeg.ts
import sharp from 'sharp';

async function optimizeJPEG(inputPath: string, outputPath: string, quality: number = 85): Promise<void> {
  await sharp(inputPath)
    .jpeg({
      quality,
      progressive: true, // Progressive JPEG for faster loading
      mozjpeg: true,    // Better compression
    })
    .toFile(outputPath);
}
python
# jpeg.py
from PIL import Image

def optimize_jpeg(input_path: str, output_path: str, quality: int = 85) -> None:
    """Optimize JPEG image."""
    img = Image.open(input_path)
    
    # Convert to RGB if necessary
    if img.mode != 'RGB':
        img = img.convert('RGB')
    
    img.save(
        output_path,
        'JPEG',
        quality=quality,
        optimize=True,
        progressive=True
    )

PNG (Portable Network Graphics)

  • Use Case: Graphics, logos, images with transparency
  • Compression: Lossless
  • Transparency: Yes (alpha channel)
  • Browser Support: Universal
  • Best For: Images with few colors or requiring transparency
typescript
// png.ts
import sharp from 'sharp';

async function optimizePNG(inputPath: string, outputPath: string): Promise<void> {
  await sharp(inputPath)
    .png({
      compressionLevel: 9,  // Maximum compression
      adaptiveFiltering: true,
      palette: true,  // Use palette for images with few colors
    })
    .toFile(outputPath);
}
python
# png.py
from PIL import Image

def optimize_png(input_path: str, output_path: str) -> None:
    """Optimize PNG image."""
    img = Image.open(input_path)
    
    # Convert to RGBA if necessary
    if img.mode != 'RGBA':
        img = img.convert('RGBA')
    
    img.save(
        output_path,
        'PNG',
        optimize=True,
        compress_level=9
    )

WebP

  • Use Case: Modern web applications
  • Compression: Lossy and lossless
  • Transparency: Yes
  • Browser Support: Modern browsers (95%+)
  • Best For: General web images
typescript
// webp.ts
import sharp from 'sharp';

async function convertToWebP(inputPath: string, outputPath: string, quality: number = 80): Promise<void> {
  await sharp(inputPath)
    .webp({
      quality,
      nearLossless: true,
      smartSubsample: true,
    })
    .toFile(outputPath);
}
python
# webp.py
from PIL import Image

def convert_to_webp(input_path: str, output_path: str, quality: int = 80) -> None:
    """Convert image to WebP format."""
    img = Image.open(input_path)
    
    # Convert to RGBA if necessary
    if img.mode != 'RGBA':
        img = img.convert('RGBA')
    
    img.save(
        output_path,
        'WEBP',
        quality=quality,
        method=6,  # Compression method (0-6, higher = slower but better)
        lossless=False
    )

AVIF

  • Use Case: Next-generation web images
  • Compression: Superior to WebP
  • Transparency: Yes
  • Browser Support: Modern browsers (70%+)
  • Best For: Maximum compression
typescript
// avif.ts
import sharp from 'sharp';

async function convertToAVIF(inputPath: string, outputPath: string, quality: number = 75): Promise<void> {
  await sharp(inputPath)
    .avif({
      quality,
      effort: 6,  // Compression effort (0-9, higher = slower but better)
    })
    .toFile(outputPath);
}
python
# avif.py
from PIL import Image

def convert_to_avif(input_path: str, output_path: str, quality: int = 75) -> None:
    """Convert image to AVIF format."""
    img = Image.open(input_path)
    
    # AVIF requires pillow-heif
    try:
        from pillow_heif import register_heif_opener
        register_heif_opener()
    except ImportError:
        raise ImportError("pillow-heif is required for AVIF support")
    
    img.save(
        output_path,
        'AVIF',
        quality=quality,
        speed=4  # Encoding speed (0-10, lower = slower but better)
    )

Format Comparison

FormatCompressionTransparencySizeBrowser SupportBest Use Case
JPEGLossyNoMedium100%Photos
PNGLosslessYesLarge100%Graphics, logos
WebPBothYesSmall95%Web images
AVIFBothYesSmallest70%Modern web

Compression

Lossy vs Lossless

typescript
// compression-types.ts
import sharp from 'sharp';

// Lossy compression (JPEG, WebP)
async function lossyCompression(input: Buffer, quality: number = 80): Promise<Buffer> {
  return sharp(input)
    .jpeg({ quality })
    .toBuffer();
}

// Lossless compression (PNG)
async function losslessCompression(input: Buffer): Promise<Buffer> {
  return sharp(input)
    .png({ compressionLevel: 9 })
    .toBuffer();
}

Quality Settings

typescript
// quality-settings.ts
import sharp from 'sharp';

interface QualityLevels {
  low: number;    // 50-60 - Smallest file, visible artifacts
  medium: number; // 70-80 - Good balance
  high: number;   // 85-95 - Best quality, larger file
}

const qualityPresets: Record<string, QualityLevels> = {
  thumbnail: { low: 50, medium: 60, high: 70 },
  web: { low: 70, medium: 80, high: 85 },
  print: { low: 85, medium: 90, high: 95 },
};

async function optimizeWithQuality(
  input: Buffer,
  format: 'jpeg' | 'webp' | 'avif',
  preset: keyof typeof qualityPresets,
  level: keyof QualityLevels
): Promise<Buffer> {
  const quality = qualityPresets[preset][level];

  switch (format) {
    case 'jpeg':
      return sharp(input).jpeg({ quality }).toBuffer();
    case 'webp':
      return sharp(input).webp({ quality }).toBuffer();
    case 'avif':
      return sharp(input).avif({ quality }).toBuffer();
  }
}
python
# quality_settings.py
from PIL import Image
from typing import Literal

def optimize_with_quality(
    input_path: str,
    output_path: str,
    format: Literal['JPEG', 'WEBP', 'AVIF'],
    quality: int = 80
) -> None:
    """Optimize image with specific quality."""
    img = Image.open(input_path)
    
    if format == 'JPEG':
        if img.mode != 'RGB':
            img = img.convert('RGB')
        img.save(output_path, 'JPEG', quality=quality, optimize=True)
    
    elif format == 'WEBP':
        if img.mode != 'RGBA':
            img = img.convert('RGBA')
        img.save(output_path, 'WEBP', quality=quality, method=6)
    
    elif format == 'AVIF':
        if img.mode != 'RGBA':
            img = img.convert('RGBA')
        img.save(output_path, 'AVIF', quality=quality, speed=4)

Resizing and Scaling

Basic Resizing

typescript
// resizing.ts
import sharp from 'sharp';

async function resizeImage(
  input: Buffer,
  width: number,
  height?: number,
  options?: {
    fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside';
    position?: 'top' | 'bottom' | 'left' | 'right' | 'center';
  }
): Promise<Buffer> {
  return sharp(input)
    .resize(width, height, {
      fit: options?.fit || 'cover',
      position: options?.position || 'center',
      withoutEnlargement: true,  // Don't upscale
    })
    .toBuffer();
}
python
# resizing.py
from PIL import Image
from typing import Optional, Literal

def resize_image(
    input_path: str,
    output_path: str,
    width: int,
    height: Optional[int] = None,
    fit: Literal['cover', 'contain', 'fill'] = 'cover'
) -> None:
    """Resize image with specified dimensions."""
    img = Image.open(input_path)
    
    if fit == 'cover':
        # Crop to fill the exact dimensions
        img = ImageOps.fit(
            img,
            (width, height or width),
            method=Image.Resampling.LANCZOS,
            centering=(0.5, 0.5)
        )
    elif fit == 'contain':
        # Resize to fit within dimensions
        img.thumbnail((width, height or width), Image.Resampling.LANCZOS)
    elif fit == 'fill':
        # Resize and stretch
        img = img.resize((width, height or width), Image.Resampling.LANCZOS)
    
    img.save(output_path)

Generate Multiple Sizes

typescript
// multiple-sizes.ts
import sharp from 'sharp';

interface ImageSize {
  name: string;
  width: number;
  height?: number;
  quality?: number;
}

async function generateImageSizes(
  input: Buffer,
  sizes: ImageSize[],
  format: 'jpeg' | 'webp' | 'png' = 'webp'
): Promise<Record<string, Buffer>> {
  const results: Record<string, Buffer> = {};

  for (const size of sizes) {
    let pipeline = sharp(input);

    if (size.width || size.height) {
      pipeline = pipeline.resize(size.width, size.height, {
        fit: 'inside',
        withoutEnlargement: true,
      });
    }

    switch (format) {
      case 'jpeg':
        pipeline = pipeline.jpeg({ quality: size.quality || 80 });
        break;
      case 'webp':
        pipeline = pipeline.webp({ quality: size.quality || 80 });
        break;
      case 'png':
        pipeline = pipeline.png({ compressionLevel: 9 });
        break;
    }

    results[size.name] = await pipeline.toBuffer();
  }

  return results;
}

// Usage
const sizes = [
  { name: 'thumbnail', width: 150, height: 150, quality: 70 },
  { name: 'small', width: 300, quality: 75 },
  { name: 'medium', width: 600, quality: 80 },
  { name: 'large', width: 1200, quality: 85 },
];

const variants = await generateImageSizes(inputBuffer, sizes, 'webp');
python
# multiple_sizes.py
from PIL import Image
from typing import List, Dict
from dataclasses import dataclass

@dataclass
class ImageSize:
    name: str
    width: int
    height: int = None
    quality: int = 80

def generate_image_sizes(
    input_path: str,
    sizes: List[ImageSize],
    format: str = 'WEBP'
) -> Dict[str, bytes]:
    """Generate multiple image sizes."""
    img = Image.open(input_path)
    results = {}
    
    for size in sizes:
        resized = img.copy()
        
        if size.width or size.height:
            resized.thumbnail((size.width, size.height or size.width), Image.Resampling.LANCZOS)
        
        output = io.BytesIO()
        
        if format == 'JPEG':
            if resized.mode != 'RGB':
                resized = resized.convert('RGB')
            resized.save(output, 'JPEG', quality=size.quality, optimize=True)
        elif format == 'WEBP':
            if resized.mode != 'RGBA':
                resized = resized.convert('RGBA')
            resized.save(output, 'WEBP', quality=size.quality, method=6)
        elif format == 'PNG':
            if resized.mode != 'RGBA':
                resized = resized.convert('RGBA')
            resized.save(output, 'PNG', optimize=True, compress_level=9)
        
        results[size.name] = output.getvalue()
    
    return results

Responsive Images

srcset and sizes

html
<!-- responsive-images.html -->
<img
  src="image-800.webp"
  srcset="
    image-400.webp 400w,
    image-800.webp 800w,
    image-1200.webp 1200w,
    image-1600.webp 1600w
  "
  sizes="
    (max-width: 600px) 400px,
    (max-width: 1200px) 800px,
    (max-width: 1600px) 1200px,
    1600px
  "
  alt="Responsive image"
  loading="lazy"
/>

Picture Element

html
<!-- picture-element.html -->
<picture>
  <source
    srcset="image.avif"
    type="image/avif"
  />
  <source
    srcset="image.webp"
    type="image/webp"
  />
  <img
    src="image.jpg"
    alt="Fallback image"
    loading="lazy"
  />
</picture>

Generate Responsive Images

typescript
// responsive-generator.ts
import sharp from 'sharp';

interface ResponsiveConfig {
  widths: number[];
  formats: Array<{ name: string; format: 'jpeg' | 'webp' | 'avif'; quality?: number }>;
}

async function generateResponsiveImages(
  input: Buffer,
  config: ResponsiveConfig
): Promise<Record<string, Buffer>> {
  const results: Record<string, Buffer> = {};

  for (const format of config.formats) {
    for (const width of config.widths) {
      const filename = `${format.name}-${width}.${format.format}`;

      results[filename] = await sharp(input)
        .resize(width, null, {
          fit: 'inside',
          withoutEnlargement: true,
        })
        .toFormat(format.format, { quality: format.quality || 80 })
        .toBuffer();
    }
  }

  return results;
}

// Generate HTML
function generateResponsiveHTML(
  baseName: string,
  config: ResponsiveConfig
): string {
  const webpFormat = config.formats.find(f => f.format === 'webp');
  const avifFormat = config.formats.find(f => f.format === 'avif');
  const jpegFormat = config.formats.find(f => f.format === 'jpeg');

  const webpSrcset = webpFormat
    ? config.widths.map(w => `${baseName}-${w}.webp ${w}w`).join(', ')
    : '';

  const avifSrcset = avifFormat
    ? config.widths.map(w => `${baseName}-${w}.avif ${w}w`).join(', ')
    : '';

  const jpegSrcset = jpegFormat
    ? config.widths.map(w => `${baseName}-${w}.jpg ${w}w`).join(', ')
    : '';

  const sizes = config.widths.map(w => `(max-width: ${w}px) ${w}px`).join(', ');

  return `
<picture>
  ${avifFormat ? `<source srcset="${avifSrcset}" type="image/avif" />` : ''}
  ${webpFormat ? `<source srcset="${webpSrcset}" type="image/webp" />` : ''}
  <img
    src="${baseName}-${config.widths[0]}.jpg"
    srcset="${jpegSrcset}"
    sizes="${sizes}"
    alt="Responsive image"
    loading="lazy"
  />
</picture>
  `.trim();
}

// Usage
const config: ResponsiveConfig = {
  widths: [400, 800, 1200, 1600],
  formats: [
    { name: 'image', format: 'avif', quality: 75 },
    { name: 'image', format: 'webp', quality: 80 },
    { name: 'image', format: 'jpeg', quality: 85 },
  ],
};

const images = await generateResponsiveImages(inputBuffer, config);
const html = generateResponsiveHTML('image', config);

Lazy Loading

Native Lazy Loading

html
<!-- native-lazy-loading.html -->
<img
  src="image.jpg"
  alt="Lazy loaded image"
  loading="lazy"
  width="800"
  height="600"
/>

JavaScript Lazy Loading (Intersection Observer)

typescript
// lazy-loading.ts
class LazyLoader {
  private observer: IntersectionObserver;

  constructor() {
    this.observer = new IntersectionObserver(
      this.handleIntersect.bind(this),
      {
        rootMargin: '50px 0px',
        threshold: 0.01,
      }
    );
  }

  observe(element: HTMLImageElement): void {
    this.observer.observe(element);
  }

  private handleIntersect(entries: IntersectionObserverEntry[]): void {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target as HTMLImageElement;
        const src = img.dataset.src;
        const srcset = img.dataset.srcset;

        if (src) {
          img.src = src;
        }

        if (srcset) {
          img.srcset = srcset;
        }

        img.onload = () => {
          img.classList.add('loaded');
        };

        this.observer.unobserve(img);
      }
    });
  }
}

// Usage
const lazyLoader = new LazyLoader();
document.querySelectorAll('img[data-src]').forEach(img => {
  lazyLoader.observe(img as HTMLImageElement);
});

React Lazy Loading Component

tsx
// LazyImage.tsx
import React, { useRef, useEffect, useState } from 'react';

interface LazyImageProps {
  src: string;
  srcset?: string;
  alt: string;
  width?: number;
  height?: number;
  className?: string;
}

export function LazyImage({
  src,
  srcset,
  alt,
  width,
  height,
  className,
}: LazyImageProps) {
  const imgRef = useRef<HTMLImageElement>(null);
  const [isLoaded, setIsLoaded] = useState(false);
  const [isInView, setIsInView] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsInView(true);
          observer.disconnect();
        }
      },
      { rootMargin: '50px' }
    );

    if (imgRef.current) {
      observer.observe(imgRef.current);
    }

    return () => observer.disconnect();
  }, []);

  return (
    <img
      ref={imgRef}
      src={isInView ? src : undefined}
      srcSet={isInView ? srcset : undefined}
      alt={alt}
      width={width}
      height={height}
      className={`${className || ''} ${isLoaded ? 'loaded' : 'loading'}`}
      onLoad={() => setIsLoaded(true)}
      loading="lazy"
    />
  );
}

Image CDN

Cloudinary

typescript
// cloudinary.ts
import { v2 as cloudinary } from 'cloudinary';

cloudinary.config({
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
});

function generateCloudinaryURL(
  publicId: string,
  options?: {
    width?: number;
    height?: number;
    quality?: number;
    format?: 'jpg' | 'png' | 'webp' | 'avif';
    crop?: 'fill' | 'fit' | 'limit' | 'mfit' | 'pad' | 'scale';
  }
): string {
  const transformations: string[] = [];

  if (options?.width) transformations.push(`w_${options.width}`);
  if (options?.height) transformations.push(`h_${options.height}`);
  if (options?.quality) transformations.push(`q_${options.quality}`);
  if (options?.format) transformations.push(`f_${options.format}`);
  if (options?.crop) transformations.push(`c_${options.crop}`);

  const transformation = transformations.join(',');

  return cloudinary.url(publicId, {
    transformation: transformation || undefined,
    fetch_format: options?.format || 'auto',
    quality: options?.quality || 'auto',
  });
}

// Usage
const url = generateCloudinaryURL('sample', {
  width: 800,
  height: 600,
  quality: 80,
  format: 'webp',
  crop: 'fill',
});

Imgix

typescript
// imgix.ts
function generateImgixURL(
  baseUrl: string,
  imagePath: string,
  options?: {
    width?: number;
    height?: number;
    quality?: number;
    format?: 'jpg' | 'png' | 'webp' | 'avif';
    fit?: 'fill' | 'fit' | 'max' | 'min' | 'scale';
    auto?: 'format' | 'compress' | 'enhance';
  }
): string {
  const params = new URLSearchParams();

  if (options?.width) params.set('w', options.width.toString());
  if (options?.height) params.set('h', options.height.toString());
  if (options?.quality) params.set('q', options.quality.toString());
  if (options?.format) params.set('fm', options.format);
  if (options?.fit) params.set('fit', options.fit);
  if (options?.auto) params.set('auto', options.auto);

  const queryString = params.toString();
  return `${baseUrl}${imagePath}${queryString ? `?${queryString}` : ''}`;
}

// Usage
const url = generateImgixURL('https://example.imgix.net', '/image.jpg', {
  width: 800,
  height: 600,
  quality: 80,
  format: 'webp',
  fit: 'fill',
  auto: 'format,compress',
});

Next.js Image Component

Basic Usage

tsx
// ImageComponent.tsx
import Image from 'next/image';

export function OptimizedImage() {
  return (
    <Image
      src="/images/photo.jpg"
      alt="Optimized image"
      width={800}
      height={600}
      priority  // Load above the fold
    />
  );
}

Responsive Images

tsx
// ResponsiveImage.tsx
import Image from 'next/image';

export function ResponsiveImage() {
  return (
    <Image
      src="/images/photo.jpg"
      alt="Responsive image"
      width={800}
      height={600}
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      priority
    />
  );
}

Remote Images

tsx
// RemoteImage.tsx
import Image from 'next/image';

export function RemoteImage() {
  return (
    <Image
      src="https://example.com/image.jpg"
      alt="Remote image"
      width={800}
      height={600}
      loader={({ src, width, quality }) => {
        return `${src}?w=${width}&q=${quality || 75}`;
      }}
    />
  );
}

Next.js Config

javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    domains: ['example.com', 'cdn.example.com'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    formats: ['image/avif', 'image/webp'],
    minimumCacheTTL: 60,
  },
};

module.exports = nextConfig;

Sharp Library (Node.js)

Basic Operations

typescript
// sharp-basics.ts
import sharp from 'sharp';

// Resize
async function resize(input: Buffer, width: number, height: number): Promise<Buffer> {
  return sharp(input)
    .resize(width, height)
    .toBuffer();
}

// Crop
async function crop(
  input: Buffer,
  left: number,
  top: number,
  width: number,
  height: number
): Promise<Buffer> {
  return sharp(input)
    .extract({ left, top, width, height })
    .toBuffer();
}

// Rotate
async function rotate(input: Buffer, angle: number): Promise<Buffer> {
  return sharp(input)
    .rotate(angle)
    .toBuffer();
}

// Flip and flop
async function flip(input: Buffer): Promise<Buffer> {
  return sharp(input)
    .flip()
    .toBuffer();
}

async function flop(input: Buffer): Promise<Buffer> {
  return sharp(input)
    .flop()
    .toBuffer();
}

Advanced Operations

typescript
// sharp-advanced.ts
import sharp from 'sharp';

// Blur
async function blur(input: Buffer, sigma: number = 3): Promise<Buffer> {
  return sharp(input)
    .blur(sigma)
    .toBuffer();
}

// Sharpen
async function sharpen(input: Buffer, options?: {
  sigma?: number;
  flat?: number;
  jagged?: number;
}): Promise<Buffer> {
  return sharp(input)
    .sharpen(options)
    .toBuffer();
}

// Adjust brightness, contrast, saturation
async function adjustColors(
  input: Buffer,
  options: {
    brightness?: number;  // -1 to 1
    contrast?: number;    // -1 to 1
    saturation?: number;   // -1 to 1
  }
): Promise<Buffer> {
  return sharp(input)
    .modulate({
      brightness: options.brightness,
      contrast: options.contrast,
      saturation: options.saturation,
    })
    .toBuffer();
}

// Add watermark
async function addWatermark(
  input: Buffer,
  watermark: Buffer,
  gravity: 'southeast' | 'southwest' | 'northeast' | 'northwest' | 'center' = 'southeast'
): Promise<Buffer> {
  return sharp(input)
    .composite([
      {
        input: watermark,
        gravity,
      },
    ])
    .toBuffer();
}

// Remove metadata
async function stripMetadata(input: Buffer): Promise<Buffer> {
  return sharp(input)
    .withMetadata({})  // Remove all metadata
    .toBuffer();
}

Pillow (Python)

Basic Operations

python
# pillow_basics.py
from PIL import Image, ImageOps, ImageFilter
from typing import Tuple

def resize(input_path: str, output_path: str, size: Tuple[int, int]) -> None:
    """Resize image to specified dimensions."""
    img = Image.open(input_path)
    resized = img.resize(size, Image.Resampling.LANCZOS)
    resized.save(output_path)

def crop(input_path: str, output_path: str, box: Tuple[int, int, int, int]) -> None:
    """Crop image to specified box (left, top, right, bottom)."""
    img = Image.open(input_path)
    cropped = img.crop(box)
    cropped.save(output_path)

def rotate(input_path: str, output_path: str, angle: float) -> None:
    """Rotate image by specified angle."""
    img = Image.open(input_path)
    rotated = img.rotate(angle, expand=True)
    rotated.save(output_path)

def flip(input_path: str, output_path: str) -> None:
    """Flip image vertically."""
    img = Image.open(input_path)
    flipped = ImageOps.flip(img)
    flipped.save(output_path)

def mirror(input_path: str, output_path: str) -> None:
    """Mirror image horizontally."""
    img = Image.open(input_path)
    mirrored = ImageOps.mirror(img)
    mirrored.save(output_path)

Advanced Operations

python
# pillow_advanced.py
from PIL import Image, ImageEnhance, ImageDraw, ImageFont

def blur(input_path: str, output_path: str, radius: int = 3) -> None:
    """Apply blur filter to image."""
    img = Image.open(input_path)
    blurred = img.filter(ImageFilter.GaussianBlur(radius))
    blurred.save(output_path)

def sharpen(input_path: str, output_path: str) -> None:
    """Apply sharpen filter to image."""
    img = Image.open(input_path)
    sharpened = img.filter(ImageFilter.SHARPEN)
    sharpened.save(output_path)

def adjust_brightness(input_path: str, output_path: str, factor: float = 1.0) -> None:
    """Adjust image brightness (factor > 1 = brighter, < 1 = darker)."""
    img = Image.open(input_path)
    enhancer = ImageEnhance.Brightness(img)
    adjusted = enhancer.enhance(factor)
    adjusted.save(output_path)

def adjust_contrast(input_path: str, output_path: str, factor: float = 1.0) -> None:
    """Adjust image contrast (factor > 1 = more contrast, < 1 = less)."""
    img = Image.open(input_path)
    enhancer = ImageEnhance.Contrast(img)
    adjusted = enhancer.enhance(factor)
    adjusted.save(output_path)

def add_watermark(
    input_path: str,
    output_path: str,
    watermark_text: str,
    position: str = 'bottom-right'
) -> None:
    """Add text watermark to image."""
    img = Image.open(input_path)
    
    # Convert to RGBA if necessary
    if img.mode != 'RGBA':
        img = img.convert('RGBA')
    
    # Create transparent overlay
    overlay = Image.new('RGBA', img.size, (255, 255, 255, 0))
    draw = ImageDraw.Draw(overlay)
    
    # Use default font or load custom font
    try:
        font = ImageFont.truetype('arial.ttf', 36)
    except:
        font = ImageFont.load_default()
    
    # Calculate position
    bbox = draw.textbbox((0, 0), watermark_text, font=font)
    text_width = bbox[2] - bbox[0]
    text_height = bbox[3] - bbox[1]
    
    if position == 'bottom-right':
        x = img.width - text_width - 20
        y = img.height - text_height - 20
    elif position == 'bottom-left':
        x = 20
        y = img.height - text_height - 20
    elif position == 'top-right':
        x = img.width - text_width - 20
        y = 20
    else:  # top-left
        x = 20
        y = 20
    
    # Draw text
    draw.text((x, y), watermark_text, font=font, fill=(255, 255, 255, 128))
    
    # Composite overlay onto image
    watermarked = Image.alpha_composite(img, overlay)
    watermarked.save(output_path)

Automated Optimization

Build-Time Optimization

typescript
// build-optimizer.ts
import fs from 'fs';
import path from 'path';
import sharp from 'sharp';

interface OptimizationConfig {
  inputDir: string;
  outputDir: string;
  formats: Array<{ format: 'jpeg' | 'webp' | 'avif'; quality?: number }>;
  sizes: Array<{ name: string; width: number; height?: number }>;
}

async function optimizeImages(config: OptimizationConfig): Promise<void> {
  const files = await fs.promises.readdir(config.inputDir, { recursive: true });

  for (const file of files) {
    const filePath = path.join(config.inputDir, file as string);
    const stat = await fs.promises.stat(filePath);

    if (stat.isFile() && /\.(jpg|jpeg|png|webp)$/i.test(file)) {
      const input = await fs.promises.readFile(filePath);
      const baseName = path.basename(file, path.extname(file));

      for (const format of config.formats) {
        for (const size of config.sizes) {
          let pipeline = sharp(input);

          if (size.width || size.height) {
            pipeline = pipeline.resize(size.width, size.height, {
              fit: 'inside',
              withoutEnlargement: true,
            });
          }

          pipeline = pipeline.toFormat(format.format, {
            quality: format.quality || 80,
          });

          const outputFileName = `${baseName}-${size.name}.${format.format}`;
          const outputPath = path.join(config.outputDir, outputFileName);

          await pipeline.toFile(outputPath);
          console.log(`Optimized: ${outputFileName}`);
        }
      }
    }
  }
}

// Usage
await optimizeImages({
  inputDir: './src/images',
  outputDir: './public/images',
  formats: [
    { format: 'avif', quality: 75 },
    { format: 'webp', quality: 80 },
    { format: 'jpeg', quality: 85 },
  ],
  sizes: [
    { name: 'thumbnail', width: 150, height: 150 },
    { name: 'small', width: 300 },
    { name: 'medium', width: 600 },
    { name: 'large', width: 1200 },
  ],
});

Runtime Optimization API

typescript
// optimize-api.ts
import express from 'express';
import sharp from 'sharp';

const app = express();

app.get('/image/:filename', async (req, res) => {
  const { filename } = req.params;
  const width = parseInt(req.query.w as string) || undefined;
  const height = parseInt(req.query.h as string) || undefined;
  const quality = parseInt(req.query.q as string) || 80;
  const format = (req.query.f as string) || 'webp';

  try {
    const inputPath = path.join('./images', filename);
    const input = await fs.promises.readFile(inputPath);

    let pipeline = sharp(input);

    if (width || height) {
      pipeline = pipeline.resize(width, height, {
        fit: 'inside',
        withoutEnlargement: true,
      });
    }

    switch (format) {
      case 'jpeg':
        pipeline = pipeline.jpeg({ quality });
        break;
      case 'png':
        pipeline = pipeline.png({ compressionLevel: 9 });
        break;
      case 'webp':
        pipeline = pipeline.webp({ quality });
        break;
      case 'avif':
        pipeline = pipeline.avif({ quality });
        break;
    }

    const output = await pipeline.toBuffer();

    res.setHeader('Content-Type', `image/${format}`);
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
    res.send(output);
  } catch (error) {
    res.status(404).send('Image not found');
  }
});

app.listen(3000);

Performance Metrics

Calculate Savings

typescript
// metrics.ts
interface ImageMetrics {
  originalSize: number;
  optimizedSize: number;
  savedBytes: number;
  savedPercentage: number;
  format: string;
  width: number;
  height: number;
}

async function calculateMetrics(
  original: Buffer,
  optimized: Buffer,
  format: string
): Promise<ImageMetrics> {
  const metadata = await sharp(original).metadata();

  return {
    originalSize: original.length,
    optimizedSize: optimized.length,
    savedBytes: original.length - optimized.length,
    savedPercentage: ((original.length - optimized.length) / original.length) * 100,
    format,
    width: metadata.width || 0,
    height: metadata.height || 0,
  };
}

// Usage
const metrics = await calculateMetrics(
  originalBuffer,
  optimizedBuffer,
  'webp'
);

console.log(`Original: ${(metrics.originalSize / 1024).toFixed(2)} KB`);
console.log(`Optimized: ${(metrics.optimizedSize / 1024).toFixed(2)} KB`);
console.log(`Saved: ${metrics.savedPercentage.toFixed(1)}%`);

Lighthouse Image Audit

typescript
// lighthouse.ts
import lighthouse from 'lighthouse';
import * as chromeLauncher from 'chrome-launcher';

async function auditImages(url: string): Promise<any> {
  const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] });

  const options = {
    logLevel: 'info',
    output: 'json',
    port: chrome.port,
    onlyCategories: ['performance'],
  };

  const runnerResult = await lighthouse(url, options);

  await chrome.kill();

  const audits = runnerResult.lhr.audits;
  const imageMetrics = {
    'modern-image-formats': audits['modern-image-formats'],
    'uses-responsive-images': audits['uses-responsive-images'],
    'efficient-animated-content': audits['efficient-animated-content'],
    'offscreen-images': audits['offscreen-images'],
    'unsized-images': audits['unsized-images'],
  };

  return imageMetrics;
}

Best Practices

1. Choose the Right Format

typescript
// format-selection.ts
function getOptimalFormat(
  imageType: 'photo' | 'graphic' | 'logo',
  hasTransparency: boolean,
  browserSupport: 'modern' | 'legacy'
): string {
  if (browserSupport === 'modern') {
    return 'avif';
  }

  if (imageType === 'photo') {
    return 'webp';
  }

  if (hasTransparency) {
    return 'png';
  }

  return 'webp';
}

2. Set Dimensions

html
<!-- Always set width and height to prevent layout shift -->
<img
  src="image.jpg"
  alt="Image with dimensions"
  width="800"
  height="600"
  loading="lazy"
/>

3. Use Progressive Loading

typescript
// progressive.ts
import sharp from 'sharp';

async function createProgressiveJPEG(input: Buffer): Promise<Buffer> {
  return sharp(input)
    .jpeg({
      progressive: true,
      quality: 80,
    })
    .toBuffer();
}

4. Remove Metadata

typescript
// strip-metadata.ts
import sharp from 'sharp';

async function stripAllMetadata(input: Buffer): Promise<Buffer> {
  return sharp(input)
    .withMetadata({})  // Remove all metadata
    .toBuffer();
}

5. Cache Optimized Images

typescript
// cache.ts
import { createHash } from 'crypto';
import fs from 'fs';
import path from 'path';

const cacheDir = './cache';

async function getCachedImage(
  key: string,
  generator: () => Promise<Buffer>
): Promise<Buffer> {
  const hash = createHash('md5').update(key).digest('hex');
  const cachePath = path.join(cacheDir, `${hash}.bin`);

  try {
    // Try to load from cache
    const cached = await fs.promises.readFile(cachePath);
    return cached;
  } catch {
    // Generate and cache
    const image = await generator();
    await fs.promises.writeFile(cachePath, image);
    return image;
  }
}

Summary

This skill covers comprehensive image optimization techniques including:

  • Image Formats: JPEG, PNG, WebP, AVIF with comparison
  • Compression: Lossy vs lossless, quality settings
  • Resizing and Scaling: Basic and advanced resizing
  • Responsive Images: srcset, sizes, picture element
  • Lazy Loading: Native and JavaScript-based
  • Image CDN: Cloudinary and Imgix integration
  • Next.js Image Component: Optimized images in Next.js
  • Sharp Library: Node.js image processing
  • Pillow: Python image processing
  • Automated Optimization: Build-time and runtime optimization
  • Performance Metrics: Calculating savings and Lighthouse audits
  • Best Practices: Format selection, dimensions, progressive loading, metadata removal, caching