Processes images with Sharp, the high-performance Node.js library for resizing, converting, and optimizing images. Use when building image pipelines, generating thumbnails, or optimizing uploads server-side.
Uses Sharp for high-performance image processing. Triggers when you need to resize, convert formats (WebP/AVIF), optimize images, or build image pipelines in Node.js applications.
/plugin marketplace add mgd34msu/goodvibes-plugin/plugin install goodvibes@goodvibes-marketThis skill inherits all available tools. When active, it can use any tool Claude has access to.
High-performance Node.js image processing. 4-5x faster than ImageMagick for resizing JPEG, PNG, WebP, AVIF, and TIFF.
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();
// 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');
| 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 |
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');
// 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');
// 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',
})
// 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 (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');
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');
// 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');
// 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 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');
// 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');
// 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, ... }] }
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 }
// 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',
},
});
}
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;
}
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,
};
}
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;
}
sharp.concurrency(1) for low-memory environmentsThis skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.