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
| Feature |
Description |
Guide |
| Resizing |
Scale images with various fit modes |
Use resize() with cover, contain, fill, inside, outside |
| Format Conversion |
Convert between JPEG, PNG, WebP, AVIF |
Use toFormat() or format-specific methods |
| Optimization |
Reduce file size while preserving quality |
Set quality levels and use mozjpeg/effort options |
| Smart Cropping |
Auto-detect focal points for cropping |
Use sharp.strategy.attention for smart positioning |
| Effects |
Apply blur, sharpen, grayscale, tint |
Use blur(), sharpen(), grayscale(), tint() |
| Watermarks |
Add text or image overlays |
Use composite() with SVG or image buffers |
| Metadata |
Read EXIF data and image dimensions |
Use metadata() for width, height, format info |
| Color Analysis |
Extract dominant colors |
Use raw() output with color quantization |
| LQIP Generation |
Create low-quality image placeholders |
Resize to ~20px with blur for base64 preview |
| Batch Processing |
Process multiple images concurrently |
Use 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
| Do |
Avoid |
| Use WebP/AVIF for modern browsers with JPEG fallback |
Serving only JPEG/PNG to all browsers |
| Generate LQIP placeholders for lazy loading |
Loading full images without placeholders |
| Cache processed images to avoid reprocessing |
Re-processing the same image on each request |
| Use withoutEnlargement to prevent upscaling |
Scaling images larger than their original size |
| Strip EXIF metadata for privacy and smaller files |
Exposing GPS and camera data in public images |
| Validate image dimensions and format before processing |
Processing arbitrary files without validation |
| Use streams for large images to reduce memory |
Loading very large images entirely into memory |
| Set appropriate quality (70-85) for web delivery |
Over-compressing (below 60) or under-compressing |
| Use sharp.strategy.attention for thumbnails |
Using center crop for all images |
| Provide fallback formats for older browsers |
Assuming all browsers support WebP/AVIF |
Related Skills
- media-processing - Video and audio processing
- frontend-design - Image usage in UI design
References
Converted and distributed by TomeVault — claim your Tome and manage your conversions.
1---2name: processing-images3description: 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. Use when this capability is needed.4---56# Processing Images78## Quick Start910```typescript11import sharp from 'sharp';1213// Resize and optimize for web14async function optimizeImage(inputPath: string, outputPath: string): Promise<void> {15 await sharp(inputPath)16 .resize(1200, 1200, { fit: 'inside', withoutEnlargement: true })17 .webp({ quality: 80 })18 .toFile(outputPath);19}2021// Generate thumbnail with smart crop22async function generateThumbnail(inputPath: string, outputPath: string): Promise<void> {23 await sharp(inputPath)24 .resize(300, 300, { fit: 'cover', position: sharp.strategy.attention })25 .jpeg({ quality: 85 })26 .toFile(outputPath);27}2829// Convert to multiple formats30async function convertFormats(inputPath: string, outputDir: string): Promise<void> {31 const baseName = path.basename(inputPath, path.extname(inputPath));32 await Promise.all([33 sharp(inputPath).webp({ quality: 80 }).toFile(`${outputDir}/${baseName}.webp`),34 sharp(inputPath).avif({ quality: 70 }).toFile(`${outputDir}/${baseName}.avif`),35 sharp(inputPath).jpeg({ quality: 85, mozjpeg: true }).toFile(`${outputDir}/${baseName}.jpg`),36 ]);37}38```3940## Features4142| Feature | Description | Guide |43|---------|-------------|-------|44| Resizing | Scale images with various fit modes | Use resize() with cover, contain, fill, inside, outside |45| Format Conversion | Convert between JPEG, PNG, WebP, AVIF | Use toFormat() or format-specific methods |46| Optimization | Reduce file size while preserving quality | Set quality levels and use mozjpeg/effort options |47| Smart Cropping | Auto-detect focal points for cropping | Use sharp.strategy.attention for smart positioning |48| Effects | Apply blur, sharpen, grayscale, tint | Use blur(), sharpen(), grayscale(), tint() |49| Watermarks | Add text or image overlays | Use composite() with SVG or image buffers |50| Metadata | Read EXIF data and image dimensions | Use metadata() for width, height, format info |51| Color Analysis | Extract dominant colors | Use raw() output with color quantization |52| LQIP Generation | Create low-quality image placeholders | Resize to ~20px with blur for base64 preview |53| Batch Processing | Process multiple images concurrently | Use p-queue with controlled concurrency |5455## Common Patterns5657### Responsive Image Set Generation5859```typescript60async function generateResponsiveSet(61 inputPath: string,62 outputDir: string,63 widths: number[] = [320, 640, 1024, 1920]64): Promise<{ srcset: string; sizes: string }> {65 const baseName = path.basename(inputPath, path.extname(inputPath));66 const srcsetParts: string[] = [];6768 for (const width of widths) {69 const filename = `${baseName}-${width}w.webp`;70 await sharp(inputPath)71 .resize(width, null, { withoutEnlargement: true })72 .webp({ quality: 80 })73 .toFile(path.join(outputDir, filename));74 srcsetParts.push(`${filename} ${width}w`);75 }7677 return {78 srcset: srcsetParts.join(', '),79 sizes: '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw',80 };81}82```8384### E-commerce Product Image Processing8586```typescript87async function processProductImage(inputPath: string, productId: string): Promise<ProductImages> {88 const outputDir = path.join(MEDIA_DIR, 'products', productId);89 await fs.mkdir(outputDir, { recursive: true });9091 const sizes = [92 { name: 'thumb', width: 150, height: 150 },93 { name: 'small', width: 300, height: 300 },94 { name: 'medium', width: 600, height: 600 },95 { name: 'large', width: 1200, height: 1200 },96 ];9798 const images: Record<string, string> = {};99 for (const size of sizes) {100 const outputPath = path.join(outputDir, `${size.name}.webp`);101 await sharp(inputPath)102 .resize(size.width, size.height, { fit: 'contain', background: '#ffffff' })103 .webp({ quality: 85 })104 .toFile(outputPath);105 images[size.name] = `/media/products/${productId}/${size.name}.webp`;106 }107108 // Generate LQIP placeholder109 const lqipBuffer = await sharp(inputPath).resize(20).blur(5).jpeg({ quality: 20 }).toBuffer();110 const lqip = `data:image/jpeg;base64,${lqipBuffer.toString('base64')}`;111112 return { images, lqip };113}114```115116### Image Watermarking117118```typescript119async function addWatermark(inputPath: string, outputPath: string, watermarkPath: string): Promise<void> {120 const metadata = await sharp(inputPath).metadata();121 const watermark = await sharp(watermarkPath)122 .resize(Math.round((metadata.width || 800) * 0.2))123 .toBuffer();124125 await sharp(inputPath)126 .composite([{ input: watermark, gravity: 'southeast', blend: 'over' }])127 .toFile(outputPath);128}129130async function addTextWatermark(inputPath: string, outputPath: string, text: string): Promise<void> {131 const metadata = await sharp(inputPath).metadata();132 const { width = 800, height = 600 } = metadata;133134 const svg = `<svg width="${width}" height="${height}">135 <text x="${width - 20}" y="${height - 20}" text-anchor="end"136 font-size="24" fill="white" opacity="0.5">${text}</text>137 </svg>`;138139 await sharp(inputPath)140 .composite([{ input: Buffer.from(svg), gravity: 'southeast' }])141 .toFile(outputPath);142}143```144145### Batch Processing with Progress146147```typescript148import PQueue from 'p-queue';149150async function batchProcessImages(151 inputPaths: string[],152 outputDir: string,153 transform: (image: sharp.Sharp) => sharp.Sharp,154 onProgress?: (completed: number, total: number) => void155): Promise<Map<string, { success: boolean; error?: string }>> {156 const queue = new PQueue({ concurrency: 4 });157 const results = new Map<string, { success: boolean; error?: string }>();158 let completed = 0;159160 for (const inputPath of inputPaths) {161 queue.add(async () => {162 const filename = path.basename(inputPath, path.extname(inputPath)) + '.webp';163 try {164 let image = sharp(inputPath);165 image = transform(image);166 await image.toFile(path.join(outputDir, filename));167 results.set(inputPath, { success: true });168 } catch (error) {169 results.set(inputPath, { success: false, error: error.message });170 }171 completed++;172 onProgress?.(completed, inputPaths.length);173 });174 }175176 await queue.onIdle();177 return results;178}179```180181## Best Practices182183| Do | Avoid |184|----|-------|185| Use WebP/AVIF for modern browsers with JPEG fallback | Serving only JPEG/PNG to all browsers |186| Generate LQIP placeholders for lazy loading | Loading full images without placeholders |187| Cache processed images to avoid reprocessing | Re-processing the same image on each request |188| Use withoutEnlargement to prevent upscaling | Scaling images larger than their original size |189| Strip EXIF metadata for privacy and smaller files | Exposing GPS and camera data in public images |190| Validate image dimensions and format before processing | Processing arbitrary files without validation |191| Use streams for large images to reduce memory | Loading very large images entirely into memory |192| Set appropriate quality (70-85) for web delivery | Over-compressing (below 60) or under-compressing |193| Use sharp.strategy.attention for thumbnails | Using center crop for all images |194| Provide fallback formats for older browsers | Assuming all browsers support WebP/AVIF |195196## Related Skills197198- **media-processing** - Video and audio processing199- **frontend-design** - Image usage in UI design200201## References202203- [Sharp Documentation](https://sharp.pixelplumbing.com/)204- [Web.dev Image Optimization](https://web.dev/fast/#optimize-your-images)205- [Squoosh](https://squoosh.app/) - Format comparison tool206- [Can I Use AVIF](https://caniuse.com/avif)207208---209> Converted and distributed by [TomeVault](https://tomevault.io/claim/doanchienthangdev) — claim your Tome and manage your conversions.210<!-- tomevault:4.0:skill_md:2026-04-13 -->