sharp

Installation
SKILL.md

Sharp

High-performance Node.js image processing. 4-5x faster than ImageMagick for resizing JPEG, PNG, WebP, AVIF, and TIFF.

Quick Start

npm install sharp
import sharp from 'sharp';

// Resize and convert
await sharp('input.jpg')
  .resize(800, 600)
  .toFormat('webp')
  .toFile('output.webp');

// From buffer
const buffer = await sharp(inputBuffer)
  .resize(400)
  .toBuffer();

Resize

// Fixed dimensions (may crop)
await sharp('input.jpg')
  .resize(800, 600)
  .toFile('output.jpg');

// Fit within dimensions (maintain aspect ratio)
await sharp('input.jpg')
  .resize(800, 600, { fit: 'inside' })
  .toFile('output.jpg');

// Fill dimensions (crop to fit)
await sharp('input.jpg')
  .resize(800, 600, { fit: 'cover' })
  .toFile('output.jpg');

// Width only (auto height)
await sharp('input.jpg')
  .resize({ width: 800 })
  .toFile('output.jpg');

// Without upscaling
await sharp('input.jpg')
  .resize(2000, null, { withoutEnlargement: true })
  .toFile('output.jpg');

Fit Options

Option Description
cover Crop to cover dimensions (default)
contain Fit within, add background if needed
fill Stretch to fill (ignores aspect ratio)
inside Fit within, never exceed
outside Fit to cover, may exceed

Position (for cover/contain)

await sharp('input.jpg')
  .resize(800, 600, {
    fit: 'cover',
    position: 'top'  // top, right top, right, right bottom, bottom, left bottom, left, left top, center
  })
  .toFile('output.jpg');

// Or use gravity
await sharp('input.jpg')
  .resize(800, 600, {
    fit: 'cover',
    position: sharp.strategy.attention  // Focus on interesting region
  })
  .toFile('output.jpg');

Format Conversion

// To WebP
await sharp('input.jpg')
  .webp({ quality: 80 })
  .toFile('output.webp');

// To AVIF (best compression)
await sharp('input.jpg')
  .avif({ quality: 60 })
  .toFile('output.avif');

// To PNG
await sharp('input.jpg')
  .png({ compressionLevel: 9 })
  .toFile('output.png');

// To JPEG
await sharp('input.png')
  .jpeg({ quality: 80, mozjpeg: true })
  .toFile('output.jpg');

// Auto format based on input
await sharp('input.jpg')
  .toFormat('webp', { quality: 80 })
  .toFile('output.webp');

Format Options

// JPEG
.jpeg({
  quality: 80,
  progressive: true,
  mozjpeg: true,  // Better compression
})

// PNG
.png({
  compressionLevel: 9,
  palette: true,     // For fewer colors
  quality: 80,       // For palette mode
})

// WebP
.webp({
  quality: 80,
  lossless: false,
  nearLossless: false,
  effort: 4,         // 0-6, higher = slower + smaller
})

// AVIF
.avif({
  quality: 60,
  effort: 4,         // 0-9, higher = slower + smaller
  chromaSubsampling: '4:4:4',
})

Operations

Crop/Extract

// Extract region
await sharp('input.jpg')
  .extract({ left: 100, top: 100, width: 300, height: 200 })
  .toFile('output.jpg');

// Trim whitespace/borders
await sharp('input.jpg')
  .trim()
  .toFile('output.jpg');

Rotate & Flip

// Rotate (auto from EXIF by default)
await sharp('input.jpg')
  .rotate(90)  // Degrees clockwise
  .toFile('output.jpg');

// Flip
await sharp('input.jpg')
  .flip()   // Vertical
  .flop()   // Horizontal
  .toFile('output.jpg');

Color Adjustments

await sharp('input.jpg')
  .grayscale()
  .toFile('output.jpg');

await sharp('input.jpg')
  .tint({ r: 255, g: 200, b: 200 })
  .toFile('output.jpg');

await sharp('input.jpg')
  .modulate({
    brightness: 1.2,  // 1 = no change
    saturation: 0.8,
    hue: 180,         // Degrees
  })
  .toFile('output.jpg');

await sharp('input.jpg')
  .negate()
  .toFile('output.jpg');

Effects

// Blur
await sharp('input.jpg')
  .blur(5)  // Sigma value, 0.3-1000
  .toFile('output.jpg');

// Sharpen
await sharp('input.jpg')
  .sharpen()  // Default
  .toFile('output.jpg');

await sharp('input.jpg')
  .sharpen({
    sigma: 1,
    m1: 1,
    m2: 3,
  })
  .toFile('output.jpg');

// Normalize (stretch contrast)
await sharp('input.jpg')
  .normalize()
  .toFile('output.jpg');

Composite (Overlays)

// Add watermark
await sharp('input.jpg')
  .composite([
    {
      input: 'watermark.png',
      gravity: 'southeast',
      blend: 'over',
    }
  ])
  .toFile('output.jpg');

// Multiple overlays
await sharp('base.jpg')
  .composite([
    { input: 'layer1.png', top: 0, left: 0 },
    { input: 'layer2.png', top: 100, left: 100, blend: 'multiply' },
    {
      input: Buffer.from('<svg>...</svg>'),
      top: 50,
      left: 50,
    }
  ])
  .toFile('output.jpg');

Add Background/Extend

// Add padding with background color
await sharp('input.png')
  .extend({
    top: 20,
    bottom: 20,
    left: 20,
    right: 20,
    background: { r: 255, g: 255, b: 255, alpha: 1 }
  })
  .toFile('output.png');

// Flatten transparency
await sharp('input.png')
  .flatten({ background: '#ffffff' })
  .toFile('output.jpg');

Pipeline Chaining

// All operations chain together
await sharp('input.jpg')
  .resize(800, 600, { fit: 'cover' })
  .rotate(90)
  .sharpen()
  .modulate({ brightness: 1.1 })
  .webp({ quality: 80 })
  .toFile('output.webp');

Metadata

// Get image info
const metadata = await sharp('input.jpg').metadata();
console.log(metadata);
// { width, height, format, space, channels, depth, density, hasAlpha, ... }

// Get stats (pixel analysis)
const stats = await sharp('input.jpg').stats();
console.log(stats);
// { channels: [{ min, max, sum, squaresSum, mean, stdev, ... }] }

Streams & Buffers

import fs from 'fs';

// Stream input/output
const readStream = fs.createReadStream('input.jpg');
const writeStream = fs.createWriteStream('output.webp');

readStream
  .pipe(sharp().resize(800).webp())
  .pipe(writeStream);

// Buffer to buffer
const inputBuffer = fs.readFileSync('input.jpg');
const outputBuffer = await sharp(inputBuffer)
  .resize(400)
  .toBuffer();

// With info
const { data, info } = await sharp(inputBuffer)
  .resize(400)
  .toBuffer({ resolveWithObject: true });

console.log(info);
// { format, width, height, channels, size }

Next.js / API Routes

// app/api/image/route.ts
import { NextRequest, NextResponse } from 'next/server';
import sharp from 'sharp';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const url = searchParams.get('url');
  const width = parseInt(searchParams.get('w') || '800');
  const quality = parseInt(searchParams.get('q') || '80');

  // Fetch original image
  const response = await fetch(url!);
  const buffer = Buffer.from(await response.arrayBuffer());

  // Process
  const processed = await sharp(buffer)
    .resize(width)
    .webp({ quality })
    .toBuffer();

  return new NextResponse(processed, {
    headers: {
      'Content-Type': 'image/webp',
      'Cache-Control': 'public, max-age=31536000',
    },
  });
}

Generate Thumbnails

async function generateThumbnails(inputPath, outputDir) {
  const sizes = [
    { name: 'thumb', width: 150, height: 150 },
    { name: 'small', width: 400 },
    { name: 'medium', width: 800 },
    { name: 'large', width: 1200 },
  ];

  const image = sharp(inputPath);
  const metadata = await image.metadata();
  const basename = path.basename(inputPath, path.extname(inputPath));

  const results = await Promise.all(
    sizes.map(async ({ name, width, height }) => {
      const outputPath = path.join(outputDir, `${basename}-${name}.webp`);

      await sharp(inputPath)
        .resize(width, height, { fit: height ? 'cover' : 'inside' })
        .webp({ quality: 80 })
        .toFile(outputPath);

      return { name, path: outputPath };
    })
  );

  return results;
}

Handle Uploads

import formidable from 'formidable';
import sharp from 'sharp';

async function handleUpload(req) {
  const form = formidable();
  const [fields, files] = await form.parse(req);
  const file = files.image[0];

  // Validate and process
  const metadata = await sharp(file.filepath).metadata();

  if (!['jpeg', 'png', 'webp'].includes(metadata.format)) {
    throw new Error('Invalid format');
  }

  // Process and save
  const filename = `${Date.now()}-${file.originalFilename}`;
  const processed = await sharp(file.filepath)
    .resize(1200, 1200, { fit: 'inside', withoutEnlargement: true })
    .webp({ quality: 80 })
    .toFile(`./uploads/${filename}.webp`);

  return {
    url: `/uploads/${filename}.webp`,
    width: processed.width,
    height: processed.height,
  };
}

Batch Processing

import sharp from 'sharp';
import { glob } from 'glob';
import path from 'path';

async function batchOptimize(inputGlob, outputDir) {
  const files = await glob(inputGlob);

  const results = await Promise.all(
    files.map(async (file) => {
      const basename = path.basename(file, path.extname(file));
      const output = path.join(outputDir, `${basename}.webp`);

      const info = await sharp(file)
        .resize(1920, 1080, { fit: 'inside', withoutEnlargement: true })
        .webp({ quality: 80 })
        .toFile(output);

      return { input: file, output, size: info.size };
    })
  );

  return results;
}

Best Practices

  1. Use streams for large files to reduce memory
  2. Set concurrency with sharp.concurrency(1) for low-memory environments
  3. Pre-compute sizes when possible (eager thumbnails)
  4. Use WebP or AVIF for best compression
  5. Cache processed images - don't reprocess on every request
  6. Handle EXIF rotation - Sharp auto-rotates by default
Related skills
Installs
1
GitHub Stars
4
First Seen
Mar 29, 2026