Back to list
doanchienthangdev

processing-images

by doanchienthangdev

Omega Vibecode Kit

2🍴 1📅 Jan 21, 2026

SKILL.md


name: Processing Images description: Processes images with Sharp for optimization, resizing, format conversion, and batch operations. Use when optimizing web images, generating thumbnails, creating responsive image sets, or applying transformations. category: tools triggers:

  • image processing
  • sharp
  • image optimization
  • resize images
  • image conversion
  • thumbnail generation
  • webp avif

Processing Images

Quick Start

import sharp from 'sharp';

// Resize and optimize for web
async function optimizeImage(inputPath: string, outputPath: string): Promise<void> {
  await sharp(inputPath)
    .resize(1200, 1200, { fit: 'inside', withoutEnlargement: true })
    .webp({ quality: 80 })
    .toFile(outputPath);
}

// Generate thumbnail with smart crop
async function generateThumbnail(inputPath: string, outputPath: string): Promise<void> {
  await sharp(inputPath)
    .resize(300, 300, { fit: 'cover', position: sharp.strategy.attention })
    .jpeg({ quality: 85 })
    .toFile(outputPath);
}

// Convert to multiple formats
async function convertFormats(inputPath: string, outputDir: string): Promise<void> {
  const baseName = path.basename(inputPath, path.extname(inputPath));
  await Promise.all([
    sharp(inputPath).webp({ quality: 80 }).toFile(`${outputDir}/${baseName}.webp`),
    sharp(inputPath).avif({ quality: 70 }).toFile(`${outputDir}/${baseName}.avif`),
    sharp(inputPath).jpeg({ quality: 85, mozjpeg: true }).toFile(`${outputDir}/${baseName}.jpg`),
  ]);
}

Features

FeatureDescriptionGuide
ResizingScale images with various fit modesUse resize() with cover, contain, fill, inside, outside
Format ConversionConvert between JPEG, PNG, WebP, AVIFUse toFormat() or format-specific methods
OptimizationReduce file size while preserving qualitySet quality levels and use mozjpeg/effort options
Smart CroppingAuto-detect focal points for croppingUse sharp.strategy.attention for smart positioning
EffectsApply blur, sharpen, grayscale, tintUse blur(), sharpen(), grayscale(), tint()
WatermarksAdd text or image overlaysUse composite() with SVG or image buffers
MetadataRead EXIF data and image dimensionsUse metadata() for width, height, format info
Color AnalysisExtract dominant colorsUse raw() output with color quantization
LQIP GenerationCreate low-quality image placeholdersResize to ~20px with blur for base64 preview
Batch ProcessingProcess multiple images concurrentlyUse p-queue with controlled concurrency

Common Patterns

Responsive Image Set Generation

async function generateResponsiveSet(
  inputPath: string,
  outputDir: string,
  widths: number[] = [320, 640, 1024, 1920]
): Promise<{ srcset: string; sizes: string }> {
  const baseName = path.basename(inputPath, path.extname(inputPath));
  const srcsetParts: string[] = [];

  for (const width of widths) {
    const filename = `${baseName}-${width}w.webp`;
    await sharp(inputPath)
      .resize(width, null, { withoutEnlargement: true })
      .webp({ quality: 80 })
      .toFile(path.join(outputDir, filename));
    srcsetParts.push(`${filename} ${width}w`);
  }

  return {
    srcset: srcsetParts.join(', '),
    sizes: '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw',
  };
}

E-commerce Product Image Processing

async function processProductImage(inputPath: string, productId: string): Promise<ProductImages> {
  const outputDir = path.join(MEDIA_DIR, 'products', productId);
  await fs.mkdir(outputDir, { recursive: true });

  const sizes = [
    { name: 'thumb', width: 150, height: 150 },
    { name: 'small', width: 300, height: 300 },
    { name: 'medium', width: 600, height: 600 },
    { name: 'large', width: 1200, height: 1200 },
  ];

  const images: Record<string, string> = {};
  for (const size of sizes) {
    const outputPath = path.join(outputDir, `${size.name}.webp`);
    await sharp(inputPath)
      .resize(size.width, size.height, { fit: 'contain', background: '#ffffff' })
      .webp({ quality: 85 })
      .toFile(outputPath);
    images[size.name] = `/media/products/${productId}/${size.name}.webp`;
  }

  // Generate LQIP placeholder
  const lqipBuffer = await sharp(inputPath).resize(20).blur(5).jpeg({ quality: 20 }).toBuffer();
  const lqip = `data:image/jpeg;base64,${lqipBuffer.toString('base64')}`;

  return { images, lqip };
}

Image Watermarking

async function addWatermark(inputPath: string, outputPath: string, watermarkPath: string): Promise<void> {
  const metadata = await sharp(inputPath).metadata();
  const watermark = await sharp(watermarkPath)
    .resize(Math.round((metadata.width || 800) * 0.2))
    .toBuffer();

  await sharp(inputPath)
    .composite([{ input: watermark, gravity: 'southeast', blend: 'over' }])
    .toFile(outputPath);
}

async function addTextWatermark(inputPath: string, outputPath: string, text: string): Promise<void> {
  const metadata = await sharp(inputPath).metadata();
  const { width = 800, height = 600 } = metadata;

  const svg = `<svg width="${width}" height="${height}">
    <text x="${width - 20}" y="${height - 20}" text-anchor="end"
          font-size="24" fill="white" opacity="0.5">${text}</text>
  </svg>`;

  await sharp(inputPath)
    .composite([{ input: Buffer.from(svg), gravity: 'southeast' }])
    .toFile(outputPath);
}

Batch Processing with Progress

import PQueue from 'p-queue';

async function batchProcessImages(
  inputPaths: string[],
  outputDir: string,
  transform: (image: sharp.Sharp) => sharp.Sharp,
  onProgress?: (completed: number, total: number) => void
): Promise<Map<string, { success: boolean; error?: string }>> {
  const queue = new PQueue({ concurrency: 4 });
  const results = new Map<string, { success: boolean; error?: string }>();
  let completed = 0;

  for (const inputPath of inputPaths) {
    queue.add(async () => {
      const filename = path.basename(inputPath, path.extname(inputPath)) + '.webp';
      try {
        let image = sharp(inputPath);
        image = transform(image);
        await image.toFile(path.join(outputDir, filename));
        results.set(inputPath, { success: true });
      } catch (error) {
        results.set(inputPath, { success: false, error: error.message });
      }
      completed++;
      onProgress?.(completed, inputPaths.length);
    });
  }

  await queue.onIdle();
  return results;
}

Best Practices

DoAvoid
Use WebP/AVIF for modern browsers with JPEG fallbackServing only JPEG/PNG to all browsers
Generate LQIP placeholders for lazy loadingLoading full images without placeholders
Cache processed images to avoid reprocessingRe-processing the same image on each request
Use withoutEnlargement to prevent upscalingScaling images larger than their original size
Strip EXIF metadata for privacy and smaller filesExposing GPS and camera data in public images
Validate image dimensions and format before processingProcessing arbitrary files without validation
Use streams for large images to reduce memoryLoading very large images entirely into memory
Set appropriate quality (70-85) for web deliveryOver-compressing (below 60) or under-compressing
Use sharp.strategy.attention for thumbnailsUsing center crop for all images
Provide fallback formats for older browsersAssuming all browsers support WebP/AVIF
  • media-processing - Video and audio processing
  • frontend-design - Image usage in UI design

References

Score

Total Score

60/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon